Package translate :: Package storage :: Module xliff

[hide private]
[frames] | no frames]

# Source Code for Module translate.storage.xliff

      1  #!/usr/bin/env python
      2  # -*- coding: utf-8 -*-
      3  #
      4  # Copyright 2005-2009 Zuza Software Foundation
      5  #
      6  # This file is part of the Translate Toolkit.
      7  #
      8  # This program is free software; you can redistribute it and/or modify
      9  # it under the terms of the GNU General Public License as published by
     10  # the Free Software Foundation; either version 2 of the License, or
     11  # (at your option) any later version.
     12  #
     13  # This program is distributed in the hope that it will be useful,
     14  # but WITHOUT ANY WARRANTY; without even the implied warranty of
     15  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     16  # GNU General Public License for more details.
     17  #
     18  # You should have received a copy of the GNU General Public License
     19  # along with this program; if not, see <http://www.gnu.org/licenses/>.
     20
     21  """Module for handling XLIFF files for translation.
     22
     23  The official recommendation is to use the extention .xlf for XLIFF files.
     24  """
     25
     26  from lxml import etree
     27
     28  from translate.misc.multistring import multistring
     29  from translate.misc.xml_helpers import *
     30  from translate.storage import base, lisa
     31  from translate.storage.lisa import getXMLspace
     32  from translate.storage.placeables.lisa import xml_to_strelem, strelem_to_xml
     33
     34  # TODO: handle translation types
     35
     36 -class xliffunit(lisa.LISAunit):
     37      """A single term in the xliff file."""
     38
     39      rootNode = "trans-unit"
     40      languageNode = "source"
     41      textNode = ""
     42      namespace = 'urn:oasis:names:tc:xliff:document:1.1'
     43
     44      _default_xml_space = "default"
     45
     46      #TODO: id and all the trans-unit level stuff
     47
     48 -    def __init__(self, source, empty=False, **kwargs):
     49          """Override the constructor to set xml:space="preserve"."""
     50          if empty:
     51              return
     52          super(xliffunit, self).__init__(source, empty, **kwargs)
     53          lisa.setXMLspace(self.xmlelement, "preserve")
     54
     55 -    def createlanguageNode(self, lang, text, purpose):
     56          """Returns an xml Element setup with given parameters."""
     57
     58          #TODO: for now we do source, but we have to test if it is target, perhaps
     59          # with parameter. Alternatively, we can use lang, if supplied, since an xliff
     60          #file has to conform to the bilingual nature promised by the header.
     61          assert purpose
     62          langset = etree.Element(self.namespaced(purpose))
     63          #TODO: check language
     64  #        lisa.setXMLlang(langset, lang)
     65
     66  #        self.createPHnodes(langset, text)
     67          langset.text = text
     68          return langset
     69
     70 -    def getlanguageNodes(self):
     71          """We override this to get source and target nodes."""
     72          source = None
     73          target = None
     74          nodes = []
     75          try:
     76              source = self.xmlelement.iterchildren(self.namespaced(self.languageNode)).next()
     77              target = self.xmlelement.iterchildren(self.namespaced('target')).next()
     78              nodes = [source, target]
     79          except StopIteration:
     80              if source is not None:
     81                  nodes.append(source)
     82              if not target is None:
     83                  nodes.append(target)
     84          return nodes
     85
     86 -    def set_rich_source(self, value, sourcelang='en'):
     87          sourcelanguageNode = self.get_source_dom()
     88          if sourcelanguageNode is None:
     89              sourcelanguageNode = self.createlanguageNode(sourcelang, u'', "source")
     90              self.set_source_dom(sourcelanguageNode)
     91
     92          # Clear sourcelanguageNode first
     93          for i in range(len(sourcelanguageNode)):
     94              del sourcelanguageNode[0]
     95          sourcelanguageNode.text = None
     96
     97          strelem_to_xml(sourcelanguageNode, value[0])
     98
     99 -    def get_rich_source(self):
    100          #rsrc = xml_to_strelem(self.source_dom)
    101          #logging.debug('rich source: %s' % (repr(rsrc)))
    102          #from dubulib.debug.misc import print_stack_funcs
    103          #print_stack_funcs()
    104          return [xml_to_strelem(self.source_dom, getXMLspace(self.xmlelement, self._default_xml_space))]
    105      rich_source = property(get_rich_source, set_rich_source)
    106
    107 -    def set_rich_target(self, value, lang='xx', append=False):
    108          if value is None:
    109              self.set_target_dom(self.createlanguageNode(lang, u'', "target"))
    110              return
    111
    112          languageNode = self.get_target_dom()
    113          if languageNode is None:
    114              languageNode = self.createlanguageNode(lang, u'', "target")
    115              self.set_target_dom(languageNode, append)
    116
    117          # Clear languageNode first
    118          for i in range(len(languageNode)):
    119              del languageNode[0]
    120          languageNode.text = None
    121
    122          strelem_to_xml(languageNode, value[0])
    123
    124 -    def get_rich_target(self, lang=None):
    125          """retrieves the "target" text (second entry), or the entry in the
    126          specified language, if it exists"""
    127          return [xml_to_strelem(self.get_target_dom(lang), getXMLspace(self.xmlelement, self._default_xml_space))]
    128      rich_target = property(get_rich_target, set_rich_target)
    129
    130 -    def addalttrans(self, txt, origin=None, lang=None, sourcetxt=None, matchquality=None):
    131          """Adds an alt-trans tag and alt-trans components to the unit.
    132
    133          @type txt: String
    134          @param txt: Alternative translation of the source text.
    135          """
    136
    137          #TODO: support adding a source tag ad match quality attribute.  At
    138          # the source tag is needed to inject fuzzy matches from a TM.
    139          if isinstance(txt, str):
    140              txt = txt.decode("utf-8")
    141          alttrans = etree.SubElement(self.xmlelement, self.namespaced("alt-trans"))
    142          lisa.setXMLspace(alttrans, "preserve")
    143          if sourcetxt:
    144              if isinstance(sourcetxt, str):
    145                  sourcetxt = sourcetxt.decode("utf-8")
    146              altsource = etree.SubElement(alttrans, self.namespaced("source"))
    147              altsource.text = sourcetxt
    148          alttarget = etree.SubElement(alttrans, self.namespaced("target"))
    149          alttarget.text = txt
    150          if matchquality:
    151              alttrans.set("match-quality", matchquality)
    152          if origin:
    153              alttrans.set("origin", origin)
    154          if lang:
    155              lisa.setXMLlang(alttrans, lang)
    156
    157 -    def getalttrans(self, origin=None):
    158          """Returns <alt-trans> for the given origin as a list of units. No
    159          origin means all alternatives."""
    160          translist = []
    161          for node in self.xmlelement.iterdescendants(self.namespaced("alt-trans")):
    162              if self.correctorigin(node, origin):
    163                  # We build some mini units that keep the xmlelement. This
    164                  # makes it easier to delete it if it is passed back to us.
    165                  newunit = base.TranslationUnit(self.source)
    166
    167                  # the source tag is optional
    168                  sourcenode = node.iterdescendants(self.namespaced("source"))
    169                  try:
    170                      newunit.source = lisa.getText(sourcenode.next(), getXMLspace(node, self._default_xml_space))
    171                  except StopIteration:
    172                      pass
    173
    174                  # must have one or more targets
    175                  targetnode = node.iterdescendants(self.namespaced("target"))
    176                  newunit.target = lisa.getText(targetnode.next(), getXMLspace(node, self._default_xml_space))
    177                  #TODO: support multiple targets better
    178                  #TODO: support notes in alt-trans
    179                  newunit.xmlelement = node
    180
    181                  translist.append(newunit)
    182          return translist
    183
    184 -    def delalttrans(self, alternative):
    185          """Removes the supplied alternative from the list of alt-trans tags"""
    186          self.xmlelement.remove(alternative.xmlelement)
    187
    188 -    def addnote(self, text, origin=None):
    189          """Add a note specifically in a "note" tag"""
    190          if text:
    191              text = text.strip()
    192          if not text:
    193              return
    194          if isinstance(text, str):
    195              text = text.decode("utf-8")
    196          note = etree.SubElement(self.xmlelement, self.namespaced("note"))
    197          note.text = text
    198          if origin:
    199              note.set("from", origin)
    200
    201 -    def getnotelist(self, origin=None):
    202          """Private method that returns the text from notes matching 'origin' or all notes."""
    203          notenodes = self.xmlelement.iterdescendants(self.namespaced("note"))
    204          # TODO: consider using xpath to construct initial_list directly
    205          # or to simply get the correct text from the outset (just remember to
    206          # check for duplication.
    207          initial_list = [lisa.getText(note, getXMLspace(self.xmlelement, self._default_xml_space)) for note in notenodes if self.correctorigin(note, origin)]
    208
    209          # Remove duplicate entries from list:
    210          dictset = {}
    211          notelist = [dictset.setdefault(note, note) for note in initial_list if note not in dictset]
    212
    213          return notelist
    214
    215 -    def getnotes(self, origin=None):
    216          return '\n'.join(self.getnotelist(origin=origin))
    217
    218 -    def removenotes(self, origin="translator"):
    219          """Remove all the translator notes."""
    220          notes = self.xmlelement.iterdescendants(self.namespaced("note"))
    221          for note in notes:
    222              if self.correctorigin(note, origin=origin):
    223                  self.xmlelement.remove(note)
    224
    225 -    def adderror(self, errorname, errortext):
    226          """Adds an error message to this unit."""
    227          #TODO: consider factoring out: some duplication between XLIFF and TMX
    228          text = errorname + ': ' + errortext
    229          self.addnote(text, origin="pofilter")
    230
    231 -    def geterrors(self):
    232          """Get all error messages."""
    233          #TODO: consider factoring out: some duplication between XLIFF and TMX
    234          notelist = self.getnotelist(origin="pofilter")
    235          errordict = {}
    236          for note in notelist:
    237              errorname, errortext = note.split(': ')
    238              errordict[errorname] = errortext
    239          return errordict
    240
    241 -    def isapproved(self):
    242          """States whether this unit is approved."""
    243          return self.xmlelement.get("approved") == "yes"
    244
    245 -    def markapproved(self, value=True):
    246          """Mark this unit as approved."""
    247          if value:
    248              self.xmlelement.set("approved", "yes")
    249          elif self.isapproved():
    250              self.xmlelement.set("approved", "no")
    251
    252 -    def isreview(self):
    253          """States whether this unit needs to be reviewed"""
    254          targetnode = self.getlanguageNode(lang=None, index=1)
    255          return not targetnode is None and \
    256                  "needs-review" in targetnode.get("state", "")
    257
    258 -    def markreviewneeded(self, needsreview=True, explanation=None):
    259          """Marks the unit to indicate whether it needs review. Adds an optional explanation as a note."""
    260          targetnode = self.getlanguageNode(lang=None, index=1)
    261          if not targetnode is None:
    262              if needsreview:
    263                  targetnode.set("state", "needs-review-translation")
    264                  if explanation:
    265                      self.addnote(explanation, origin="translator")
    266              else:
    267                  del targetnode.attrib["state"]
    268
    269 -    def isfuzzy(self):
    270  #        targetnode = self.getlanguageNode(lang=None, index=1)
    271  #        return not targetnode is None and \
    272  #                (targetnode.get("state-qualifier") == "fuzzy-match" or \
    273  #                targetnode.get("state") == "needs-review-translation")
    274          return not self.isapproved()
    275
    276 -    def markfuzzy(self, value=True):
    277          if value:
    278              self.markapproved(False)
    279          else:
    280              self.markapproved(True)
    281          targetnode = self.getlanguageNode(lang=None, index=1)
    282          if not targetnode is None:
    283              if value:
    284                  targetnode.set("state", "needs-review-translation")
    285              else:
    286                  for attribute in ["state", "state-qualifier"]:
    287                      if attribute in targetnode.attrib:
    288                          del targetnode.attrib[attribute]
    289
    290 -    def settarget(self, text, lang='xx', append=False):
    291          """Sets the target string to the given value."""
    292          super(xliffunit, self).settarget(text, lang, append)
    293          if text:
    294              self.marktranslated()
    295
    296  # This code is commented while this will almost always return false.
    297  # This way pocount, etc. works well.
    298  #    def istranslated(self):
    299  #        targetnode = self.getlanguageNode(lang=None, index=1)
    300  #        return not targetnode is None and \
    301  #                (targetnode.get("state") == "translated")
    302
    303 -    def istranslatable(self):
    304          value = self.xmlelement.get("translate")
    305          if value and value.lower() == 'no':
    306              return False
    307          return True
    308
    309 -    def marktranslated(self):
    310          targetnode = self.getlanguageNode(lang=None, index=1)
    311          if targetnode is None:
    312              return
    313          if self.isfuzzy() and "state-qualifier" in targetnode.attrib:
    314              #TODO: consider
    315              del targetnode.attrib["state-qualifier"]
    316          targetnode.set("state", "translated")
    317
    318 -    def setid(self, id):
    319          self.xmlelement.set("id", id)
    320
    321 -    def getid(self):
    322          return self.xmlelement.get("id") or ""
    323
    324 -    def addlocation(self, location):
    325          self.setid(location)
    326
    327 -    def getlocations(self):
    328          return [self.getid()]
    329
    330 -    def createcontextgroup(self, name, contexts=None, purpose=None):
    331          """Add the context group to the trans-unit with contexts a list with
    332          (type, text) tuples describing each context."""
    333          assert contexts
    334          group = etree.Element(self.namespaced("context-group"))
    335          # context-group tags must appear at the start within <group>
    336          # tags. Otherwise it must be appended to the end of a group
    337          # of tags.
    338          if self.xmlelement.tag == self.namespaced("group"):
    339              self.xmlelement.insert(0, group)
    340          else:
    341              self.xmlelement.append(group)
    342          group.set("name", name)
    343          if purpose:
    344              group.set("purpose", purpose)
    345          for type, text in contexts:
    346              if isinstance(text, str):
    347                  text = text.decode("utf-8")
    348              context = etree.SubElement(group, self.namespaced("context"))
    349              context.text = text
    350              context.set("context-type", type)
    351
    352 -    def getcontextgroups(self, name):
    353          """Returns the contexts in the context groups with the specified name"""
    354          groups = []
    355          grouptags = self.xmlelement.iterdescendants(self.namespaced("context-group"))
    356          #TODO: conbine name in query
    357          for group in grouptags:
    358              if group.get("name") == name:
    359                  contexts = group.iterdescendants(self.namespaced("context"))
    360                  pairs = []
    361                  for context in contexts:
    362                      pairs.append((context.get("context-type"), lisa.getText(context, getXMLspace(self.xmlelement, self._default_xml_space))))
    363                  groups.append(pairs) #not extend
    364          return groups
    365
    366 -    def getrestype(self):
    367          """returns the restype attribute in the trans-unit tag"""
    368          return self.xmlelement.get("restype")
    369
    370 -    def merge(self, otherunit, overwrite=False, comments=True, authoritative=False):
    371          #TODO: consider other attributes like "approved"
    372          super(xliffunit, self).merge(otherunit, overwrite, comments)
    373          if self.target:
    374              self.marktranslated()
    375              if otherunit.isfuzzy():
    376                  self.markfuzzy()
    377              elif otherunit.source == self.source:
    378                  self.markfuzzy(False)
    379          if comments:
    380              self.addnote(otherunit.getnotes())
    381
    382 -    def correctorigin(self, node, origin):
    383          """Check against node tag's origin (e.g note or alt-trans)"""
    384          if origin == None:
    385              return True
    386          elif origin in node.get("from", ""):
    387              return True
    388          elif origin in node.get("origin", ""):
    389              return True
    390          else:
    391              return False
    392
    393 -    def multistring_to_rich(self, mstr):
    394          """Override L{TranslationUnit.multistring_to_rich} which is used by the
    395              C{rich_source} and C{rich_target} properties."""
    396          strings = mstr
    397          if isinstance(mstr, multistring):
    398              strings = mstr.strings
    399          elif isinstance(mstr, basestring):
    400              strings = [mstr]
    401
    402          return [xml_to_strelem(s) for s in strings]
    403      multistring_to_rich = classmethod(multistring_to_rich)
    404
    405 -    def rich_to_multistring(self, elem_list):
    406          """Override L{TranslationUnit.rich_to_multistring} which is used by the
    407              C{rich_source} and C{rich_target} properties."""
    408          return multistring([unicode(elem) for elem in elem_list])
    409      rich_to_multistring = classmethod(rich_to_multistring)
    410
    411
    412 -class xlifffile(lisa.LISAfile):
    413      """Class representing a XLIFF file store."""
    414      UnitClass = xliffunit
    415      Name = _("XLIFF Translation File")
    416      Mimetypes  = ["application/x-xliff", "application/x-xliff+xml"]
    417      Extensions = ["xlf", "xliff"]
    418      rootNode = "xliff"
    419      bodyNode = "body"
    420      XMLskeleton = '''<?xml version="1.0" ?>
    421  <xliff version='1.1' xmlns='urn:oasis:names:tc:xliff:document:1.1'>
    422  <file original='NoName' source-language='en' datatype='plaintext'>
    423  <body>
    424  </body>
    425  </file>
    426  </xliff>'''
    427      namespace = 'urn:oasis:names:tc:xliff:document:1.1'
    428      suggestions_in_format = True
    429      """xliff units have alttrans tags which can be used to store suggestions"""
    430
    431 -    def __init__(self, *args, **kwargs):
    432          self._filename = None
    433          lisa.LISAfile.__init__(self, *args, **kwargs)
    434          self._messagenum = 0
    435
    436
    437 -    def initbody(self):
    438          self.namespace = self.document.getroot().nsmap.get(None, None)
    439
    440          if self._filename:
    441              self.body = self.getcontextnode(self._filename)
    442          else:
    443              self.body = self.document.getroot()
    444
    445          filenode = self.document.getroot().iterchildren(self.namespaced('file')).next()
    446          sourcelanguage = filenode.get('source-language')
    447          if sourcelanguage:
    448              self.setsourcelanguage(sourcelanguage)
    449          targetlanguage = filenode.get('target-language')
    450          if targetlanguage:
    451              self.settargetlanguage(targetlanguage)
    452
    453 -    def addheader(self):
    454          """Initialise the file header."""
    455          filenode = self.document.getroot().iterchildren(self.namespaced("file")).next()
    456          filenode.set("source-language", self.sourcelanguage)
    457          if self.targetlanguage:
    458              filenode.set("target-language", self.targetlanguage)
    459
    460 -    def createfilenode(self, filename, sourcelanguage=None, targetlanguage=None, datatype='plaintext'):
    461          """creates a filenode with the given filename. All parameters are needed
    462          for XLIFF compliance."""
    463          self.removedefaultfile()
    464          if sourcelanguage is None:
    465              sourcelanguage = self.sourcelanguage
    466          if targetlanguage is None:
    467              targetlanguage = self.targetlanguage
    468          filenode = etree.Element(self.namespaced("file"))
    469          filenode.set("original", filename)
    470          filenode.set("source-language", sourcelanguage)
    471          if targetlanguage:
    472              filenode.set("target-language", targetlanguage)
    473          filenode.set("datatype", datatype)
    474          bodyNode = etree.SubElement(filenode, self.namespaced(self.bodyNode))
    475          return filenode
    476
    477 -    def getfilename(self, filenode):
    478          """returns the name of the given file"""
    479          return filenode.get("original")
    480
    481 -    def setfilename(self, filenode, filename):
    482          """set the name of the given file"""
    483          return filenode.set("original", filename)
    484
    485 -    def getfilenames(self):
    486          """returns all filenames in this XLIFF file"""
    487          filenodes = self.document.getroot().iterchildren(self.namespaced("file"))
    488          filenames = [self.getfilename(filenode) for filenode in filenodes]
    489          filenames = filter(None, filenames)
    490          if len(filenames) == 1 and filenames[0] == '':
    491              filenames = []
    492          return filenames
    493
    494 -    def getfilenode(self, filename):
    495          """finds the filenode with the given name"""
    496          filenodes = self.document.getroot().iterchildren(self.namespaced("file"))
    497          for filenode in filenodes:
    498              if self.getfilename(filenode) == filename:
    499                  return filenode
    500          return None
    501
    502 -    def getdatatype(self, filename=None):
    503          """Returns the datatype of the stored file. If no filename is given,
    504          the datatype of the first file is given."""
    505          if filename:
    506              node = self.getfilenode(filename)
    507              if not node is None:
    508                  return node.get("datatype")
    509          else:
    510              filenames = self.getfilenames()
    511              if len(filenames) > 0 and filenames[0] != "NoName":
    512                  return self.getdatatype(filenames[0])
    513          return ""
    514
    515 -    def getdate(self, filename=None):
    516          """Returns the date attribute for the file. If no filename is given,
    517          the date of the first file is given. If the date attribute is not
    518          specified, None is returned."""
    519          if filename:
    520              node = self.getfilenode(filename)
    521              if not node is None:
    522                  return node.get("date")
    523          else:
    524              filenames = self.getfilenames()
    525              if len(filenames) > 0 and filenames[0] != "NoName":
    526                  return self.getdate(filenames[0])
    527          return None
    528
    529 -    def removedefaultfile(self):
    530          """We want to remove the default file-tag as soon as possible if we
    531          know if still present and empty."""
    532          filenodes = list(self.document.getroot().iterchildren(self.namespaced("file")))
    533          if len(filenodes) > 1:
    534              for filenode in filenodes:
    535                  if filenode.get("original") == "NoName" and \
    536                          not list(filenode.iterdescendants(self.namespaced(self.UnitClass.rootNode))):
    537                      self.document.getroot().remove(filenode)
    538                  break
    539
    540 -    def getheadernode(self, filenode, createifmissing=False):
    541          """finds the header node for the given filenode"""
    542          # TODO: Deprecated?
    543          headernode = filenode.iterchildren(self.namespaced("header"))
    544          try:
    545              return headernode.next()
    546          except StopIteration:
    547              pass
    548          if not createifmissing:
    549              return None
    550          headernode = etree.SubElement(filenode, self.namespaced("header"))
    551          return headernode
    552
    553 -    def getbodynode(self, filenode, createifmissing=False):
    554          """finds the body node for the given filenode"""
    555          bodynode = filenode.iterchildren(self.namespaced("body"))
    556          try:
    557              return bodynode.next()
    558          except StopIteration:
    559              pass
    560          if not createifmissing:
    561              return None
    562          bodynode = etree.SubElement(filenode, self.namespaced("body"))
    563          return bodynode
    564
    565 -    def addsourceunit(self, source, filename="NoName", createifmissing=False):
    566          """adds the given trans-unit to the last used body node if the filename has changed it uses the slow method instead (will create the nodes required if asked). Returns success"""
    567          if self._filename != filename:
    568              if not self.switchfile(filename, createifmissing):
    569                  return None
    570          unit = super(xlifffile, self).addsourceunit(source)
    571          self._messagenum += 1
    572          unit.setid("%d" % self._messagenum)
    573          return unit
    574
    575 -    def switchfile(self, filename, createifmissing=False):
    576          """adds the given trans-unit (will create the nodes required if asked). Returns success"""
    577          self._filename = filename
    578          filenode = self.getfilenode(filename)
    579          if filenode is None:
    580              if not createifmissing:
    581                  return False
    582              filenode = self.createfilenode(filename)
    583              self.document.getroot().append(filenode)
    584
    585          self.body = self.getbodynode(filenode, createifmissing=createifmissing)
    586          if self.body is None:
    587              return False
    588          self._messagenum = len(list(self.body.iterdescendants(self.namespaced("trans-unit"))))
    589          #TODO: was 0 based before - consider
    590      #    messagenum = len(self.units)
    591          #TODO: we want to number them consecutively inside a body/file tag
    592          #instead of globally in the whole XLIFF file, but using len(self.units)
    593          #will be much faster
    594          return True
    595
    596 -    def creategroup(self, filename="NoName", createifmissing=False, restype=None):
    597          """adds a group tag into the specified file"""
    598          if self._filename != filename:
    599              if not self.switchfile(filename, createifmissing):
    600                  return None
    601          group = etree.SubElement(self.body, self.namespaced("group"))
    602          if restype:
    603              group.set("restype", restype)
    604          return group
    605
    606 -    def __str__(self):
    607          self.removedefaultfile()
    608          return super(xlifffile, self).__str__()
    609
    610 -    def parsestring(cls, storestring):
    611          """Parses the string to return the correct file object"""
    612          xliff = super(xlifffile, cls).parsestring(storestring)
    613          if xliff.units:
    614              header = xliff.units[0]
    615              if ("gettext-domain-header" in (header.getrestype() or "") \
    616                      or xliff.getdatatype() == "po") \
    617                      and cls.__name__.lower() != "poxlifffile":
    618                  import poxliff
    619                  xliff = poxliff.PoXliffFile.parsestring(storestring)
    620          return xliff
    621      parsestring = classmethod(parsestring)
    622