Package translate :: Package storage :: Module cpo |
[hide private]
[frames] | no frames]

# Source Code for Module translate.storage.cpo

      1  #!/usr/bin/env python
      2  # -*- coding: utf-8 -*-
      3  #
      4  # Copyright 2002-2007 Zuza Software Foundation
      5  #
      6  # This file is part of translate.
      7  #
      8  # translate 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  # translate 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 translate; if not, write to the Free Software
     20  # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
     21
     22  """Classes that hold units of .po files (pounit) or entire files (pofile).
     23
     24  Gettext-style .po (or .pot) files are used in translations for KDE, GNOME and
     25  many other projects.
     26
     27  This uses libgettextpo from the gettext package. Any version before 0.17 will
     28  at least cause some subtle bugs or may not work at all. Developers might want
     29  to have a look at gettext-tools/libgettextpo/gettext-po.h from the gettext
     30  package for the public API of the library.
     31  """
     32
     33  from ctypes import c_size_t, c_int, c_uint, c_char_p, c_long, CFUNCTYPE, POINTER
     34  from ctypes import Structure, cdll
     35  import ctypes.util
     36  import os
     37  import re
     38  import sys
     39  import tempfile
     40
     41  from translate.lang import data
     42  from translate.misc.multistring import multistring
     43  from translate.storage import base, pocommon
     44  from translate.storage import pypo
     45  from translate.storage.pocommon import encodingToUse
     46
     47  lsep = " "
     48  """Seperator for #: entries"""
     49
     50  STRING = c_char_p
     51
     52
     53  # Structures
     54 -class po_message(Structure):
     55      _fields_ = []
     56
     57  # Function prototypes
     58  xerror_prototype = CFUNCTYPE(None, c_int, POINTER(po_message), STRING, c_uint, c_uint, c_int, STRING)
     59  xerror2_prototype = CFUNCTYPE(None, c_int, POINTER(po_message), STRING, c_uint, c_uint, c_int, STRING, POINTER(po_message), STRING, c_uint, c_uint, c_int, STRING)
     60
     61
     62  # Structures (error handler)
     63 -class po_xerror_handler(Structure):
     64      _fields_ = [('xerror', xerror_prototype),
     65                  ('xerror2', xerror2_prototype)]
     66
     67
     68 -class po_error_handler(Structure):
     69      _fields_ = [
     70      ('error', CFUNCTYPE(None, c_int, c_int, STRING)),
     71      ('error_at_line', CFUNCTYPE(None, c_int, c_int, STRING, c_uint, STRING)),
     72      ('multiline_warning', CFUNCTYPE(None, STRING, STRING)),
     73      ('multiline_error', CFUNCTYPE(None, STRING, STRING)),
     74  ]
     75
     76
     77  # Callback functions for po_xerror_handler
     78 -def xerror_cb(severity, message, filename, lineno, column, multilint_p, message_text):
     79      print >> sys.stderr, "xerror_cb", severity, message, filename, lineno, column, multilint_p, message_text
     80      if severity >= 1:
     81          raise ValueError(message_text)
     82
     83
     84 -def xerror2_cb(severity, message1, filename1, lineno1, column1, multiline_p1, message_text1, message2, filename2, lineno2, column2, multiline_p2, message_text2):
     85      print >> sys.stderr, "xerror2_cb", severity, message1, filename1, lineno1, column1, multiline_p1, message_text1, message2, filename2, lineno2, column2, multiline_p2, message_text2
     86      if severity >= 1:
     87          raise ValueError(message_text1)
     88
     89
     90  # Load libgettextpo
     91  gpo = None
     92  # 'gettextpo' is recognised on Unix, while only 'libgettextpo' is recognised on
     93  # windows. Therefore we test both.
     94  names = ['gettextpo', 'libgettextpo']
     95  for name in names:
     96      lib_location = ctypes.util.find_library(name)
     97      if lib_location:
     98          gpo = cdll.LoadLibrary(lib_location)
     99          if gpo:
    100              break
    101  else:
    102      # Now we are getting desperate, so let's guess a unix type DLL that might
    103      # be in LD_LIBRARY_PATH or loaded with LD_PRELOAD
    104      try:
    105          gpo = cdll.LoadLibrary('libgettextpo.so')
    106      except OSError, e:
    107          raise ImportError("gettext PO library not found")
    108
    109  # Setup return and paramater types
    110  # File access
    111  gpo.po_file_read_v3.argtypes = [STRING, POINTER(po_xerror_handler)]
    112  gpo.po_file_write_v2.argtypes = [c_int, STRING, POINTER(po_xerror_handler)]
    113  gpo.po_file_write_v2.retype = c_int
    114
    115  # Header
    116  gpo.po_file_domain_header.restype = STRING
    117  gpo.po_header_field.restype = STRING
    118  gpo.po_header_field.argtypes = [STRING, STRING]
    119
    120  # Locations (filepos)
    121  gpo.po_filepos_file.restype = STRING
    122  gpo.po_message_filepos.restype = c_int
    123  gpo.po_message_filepos.argtypes = [c_int, c_int]
    124  gpo.po_message_add_filepos.argtypes = [c_int, STRING, c_size_t]
    125
    126  # Message (get methods)
    127  gpo.po_message_comments.restype = STRING
    128  gpo.po_message_extracted_comments.restype = STRING
    129  gpo.po_message_prev_msgctxt.restype = STRING
    130  gpo.po_message_prev_msgid.restype = STRING
    131  gpo.po_message_prev_msgid_plural.restype = STRING
    132  gpo.po_message_is_format.restype = c_int
    133  gpo.po_message_is_format.argtypes = [c_int, STRING]
    134  gpo.po_message_set_format.argtypes = [c_int, STRING, c_int]
    135  gpo.po_message_msgctxt.restype = STRING
    136  gpo.po_message_msgid.restype = STRING
    137  gpo.po_message_msgid_plural.restype = STRING
    138  gpo.po_message_msgstr.restype = STRING
    139  gpo.po_message_msgstr_plural.restype = STRING
    140
    141  # Message (set methods)
    142  gpo.po_message_set_comments.argtypes = [c_int, STRING]
    143  gpo.po_message_set_extracted_comments.argtypes = [c_int, STRING]
    144  gpo.po_message_set_fuzzy.argtypes = [c_int, c_int]
    145  gpo.po_message_set_msgctxt.argtypes = [c_int, STRING]
    146
    147  # Setup the po_xerror_handler
    148  xerror_handler = po_xerror_handler()
    149  xerror_handler.xerror = xerror_prototype(xerror_cb)
    150  xerror_handler.xerror2 = xerror2_prototype(xerror2_cb)
    151
    152
    153 -def escapeforpo(text):
    154      return pypo.escapeforpo(text)
    155
    156
    157 -def quoteforpo(text):
    158      return pypo.quoteforpo(text)
    159
    160
    161 -def unquotefrompo(postr):
    162      return pypo.unquotefrompo(postr)
    163
    164
    165 -def get_libgettextpo_version():
    166      """Returns the libgettextpo version
    167
    168         @rtype: three-value tuple
    169         @return: libgettextpo version in the following format::
    170             (major version, minor version, subminor version)
    171      """
    172      libversion = c_long.in_dll(gpo, 'libgettextpo_version')
    173      major = libversion.value >> 16
    174      minor = libversion.value >> 8
    175      subminor = libversion.value - (major << 16) - (minor << 8)
    176      return major, minor, subminor
    177
    178
    179 -class pounit(pocommon.pounit):
    180
    181 -    def __init__(self, source=None, encoding='utf-8', gpo_message=None):
    182          self._rich_source = None
    183          self._rich_target = None
    184          self._encoding = encoding or 'utf-8'
    185          if not gpo_message:
    186              self._gpo_message = gpo.po_message_create()
    187          if source or source == "":
    188              self.source = source
    189              self.target = ""
    190          elif gpo_message:
    191              self._gpo_message = gpo_message
    192          self.infer_state()
    193
    194 -    def infer_state(self):
    195          #FIXME: do obsolete
    196          if gpo.po_message_is_obsolete(self._gpo_message):
    197              self.set_state_n(self.STATE[self.S_OBSOLETE][0])
    198          elif gpo.po_message_is_fuzzy(self._gpo_message):
    199              self.set_state_n(self.STATE[self.S_FUZZY][0])
    200          elif self.gettarget():
    201              self.set_state_n(self.STATE[self.S_TRANSLATED][0])
    202          else:
    203              self.set_state_n(self.STATE[self.S_UNTRANSLATED][0])
    204
    205 -    def setmsgid_plural(self, msgid_plural):
    206          if isinstance(msgid_plural, list):
    207              msgid_plural = "".join(msgid_plural)
    208          gpo.po_message_set_msgid_plural(self._gpo_message, msgid_plural)
    209      msgid_plural = property(None, setmsgid_plural)
    210
    211 -    def getsource(self):
    212
    213          def remove_msgid_comments(text):
    214              if not text:
    215                  return text
    216              if text.startswith("_:"):
    217                  remainder = re.search(r"_: .*\n(.*)", text)
    218                  if remainder:
    219                      return remainder.group(1)
    220                  else:
    221                      return u""
    222              else:
    223                  return text
    224          singular = remove_msgid_comments((gpo.po_message_msgid(self._gpo_message) or "").decode(self._encoding))
    225          if singular:
    226              if self.hasplural():
    227                  multi = multistring(singular, self._encoding)
    228                  pluralform = (gpo.po_message_msgid_plural(self._gpo_message) or "").decode(self._encoding)
    229                  multi.strings.append(pluralform)
    230                  return multi
    231              else:
    232                  return singular
    233          else:
    234              return u""
    235
    236 -    def setsource(self, source):
    237          if isinstance(source, multistring):
    238              source = source.strings
    239          if isinstance(source, unicode):
    240              source = source.encode(self._encoding)
    241          if isinstance(source, list):
    242              gpo.po_message_set_msgid(self._gpo_message, source[0].encode(self._encoding))
    243              if len(source) > 1:
    244                  gpo.po_message_set_msgid_plural(self._gpo_message, source[1].encode(self._encoding))
    245          else:
    246              gpo.po_message_set_msgid(self._gpo_message, source)
    247              gpo.po_message_set_msgid_plural(self._gpo_message, None)
    248      source = property(getsource, setsource)
    249
    250 -    def gettarget(self):
    251          if self.hasplural():
    252              plurals = []
    253              nplural = 0
    254              plural = gpo.po_message_msgstr_plural(self._gpo_message, nplural)
    255              while plural:
    256                  plurals.append(plural.decode(self._encoding))
    257                  nplural += 1
    258                  plural = gpo.po_message_msgstr_plural(self._gpo_message, nplural)
    259              if plurals:
    260                  multi = multistring(plurals, encoding=self._encoding)
    261              else:
    262                  multi = multistring(u"")
    263          else:
    264              multi = (gpo.po_message_msgstr(self._gpo_message) or "").decode(self._encoding)
    265          return multi
    266
    267 -    def settarget(self, target):
    268          # for plural strings: convert 'target' into a list
    269          if self.hasplural():
    270              if isinstance(target, multistring):
    271                  target = target.strings
    272              elif isinstance(target, basestring):
    273                  target = [target]
    274          # for non-plurals: check number of items in 'target'
    275          elif isinstance(target, (dict, list)):
    276              if len(target) == 1:
    277                  target = target[0]
    278              else:
    279                  raise ValueError("po msgid element has no plural but msgstr has %d elements (%s)" % (len(target), target))
    280          # empty the previous list of messages
    281          # TODO: the "pypo" implementation does not remove the previous items of
    282          #   the target, if self.target == target (essentially: comparing only
    283          #   the first item of a plural string with the single new string)
    284          #   Maybe this behaviour should be unified.
    285          if isinstance(target, (dict, list)):
    286              i = 0
    287              message = gpo.po_message_msgstr_plural(self._gpo_message, i)
    288              while message is not None:
    289                  gpo.po_message_set_msgstr_plural(self._gpo_message, i, None)
    290                  i += 1
    291                  message = gpo.po_message_msgstr_plural(self._gpo_message, i)
    292          # add the items of a list
    293          if isinstance(target, list):
    294              for i in range(len(target)):
    295                  targetstring = target[i]
    296                  if isinstance(targetstring, unicode):
    297                      targetstring = targetstring.encode(self._encoding)
    298                  gpo.po_message_set_msgstr_plural(self._gpo_message, i, targetstring)
    299          # add the values of a dict
    300          elif isinstance(target, dict):
    301              for i, targetstring in enumerate(target.itervalues()):
    302                  gpo.po_message_set_msgstr_plural(self._gpo_message, i, targetstring)
    303          # add a single string
    304          else:
    305              if isinstance(target, unicode):
    306                  target = target.encode(self._encoding)
    307              if target is None:
    308                  gpo.po_message_set_msgstr(self._gpo_message, "")
    309              else:
    310                  gpo.po_message_set_msgstr(self._gpo_message, target)
    311      target = property(gettarget, settarget)
    312
    313 -    def getid(self):
    314          """The unique identifier for this unit according to the convensions in
    315          .mo files."""
    316          id = (gpo.po_message_msgid(self._gpo_message) or "").decode(self._encoding)
    317          # Gettext does not consider the plural to determine duplicates, only
    318          # the msgid. For generation of .mo files, we might want to use this
    319          # code to generate the entry for the hash table, but for now, it is
    320          # commented out for conformance to gettext.
    321  #        plural = gpo.po_message_msgid_plural(self._gpo_message)
    322  #        if not plural is None:
    323  #            id = '%s\0%s' % (id, plural)
    324          context = gpo.po_message_msgctxt(self._gpo_message)
    325          if context:
    326              id = u"%s\04%s" % (context.decode(self._encoding), id)
    327          return id
    328
    329 -    def getnotes(self, origin=None):
    330          if origin == None:
    331              comments = gpo.po_message_comments(self._gpo_message) + \
    332                         gpo.po_message_extracted_comments(self._gpo_message)
    333          elif origin == "translator":
    334              comments = gpo.po_message_comments(self._gpo_message)
    335          elif origin in ["programmer", "developer", "source code"]:
    336              comments = gpo.po_message_extracted_comments(self._gpo_message)
    337          else:
    338              raise ValueError("Comment type not valid")
    339
    340          if comments and get_libgettextpo_version() < (0, 17, 0):
    341              comments = "\n".join([line for line in comments.split("\n")])
    342          # Let's drop the last newline
    343          return comments[:-1].decode(self._encoding)
    344
    345 -    def addnote(self, text, origin=None, position="append"):
    346          # ignore empty strings and strings without non-space characters
    347          if not (text and text.strip()):
    348              return
    349          text = data.forceunicode(text)
    350          oldnotes = self.getnotes(origin)
    351          newnotes = None
    352          if oldnotes:
    353              if position == "append":
    354                  newnotes = oldnotes + "\n" + text
    355              elif position == "merge":
    356                  if oldnotes != text:
    357                      oldnoteslist = oldnotes.split("\n")
    358                      for newline in text.split("\n"):
    359                          newline = newline.rstrip("\r")
    360                          # avoid duplicate comment lines (this might cause some problems)
    361                          if newline not in oldnotes or len(newline) < 5:
    362                              oldnoteslist.append(newline)
    363                      newnotes = "\n".join(oldnoteslist)
    364              else:
    365                  newnotes = text + '\n' + oldnotes
    366          else:
    367              newnotes = "\n".join([line.rstrip("\r") for line in text.split("\n")])
    368
    369          if newnotes:
    370              newlines = []
    371              needs_space = get_libgettextpo_version() < (0, 17, 0)
    372              for line in newnotes.split("\n"):
    373                  if line and needs_space:
    374                      newlines.append(" " + line)
    375                  else:
    376                      newlines.append(line)
    377              newnotes = "\n".join(newlines).encode(self._encoding)
    378              if origin in ["programmer", "developer", "source code"]:
    379                  gpo.po_message_set_extracted_comments(self._gpo_message, newnotes)
    380              else:
    381                  gpo.po_message_set_comments(self._gpo_message, newnotes)
    382
    383 -    def removenotes(self):
    384          gpo.po_message_set_comments(self._gpo_message, "")
    385
    386 -    def copy(self):
    387          newpo = self.__class__()
    388          newpo._gpo_message = self._gpo_message
    389          return newpo
    390
    391 -    def merge(self, otherpo, overwrite=False, comments=True, authoritative=False):
    392          """Merges the otherpo (with the same msgid) into this one.
    393
    394          Overwrite non-blank self.msgstr only if overwrite is True
    395          merge comments only if comments is True
    396          """
    397
    398          if not isinstance(otherpo, pounit):
    399              super(pounit, self).merge(otherpo, overwrite, comments)
    400              return
    401          if comments:
    402              self.addnote(otherpo.getnotes("translator"), origin="translator", position="merge")
    403              # FIXME mergelists(self.typecomments, otherpo.typecomments)
    404              if not authoritative:
    405                  # We don't bring across otherpo.automaticcomments as we consider ourself
    406                  # to be the the authority.  Same applies to otherpo.msgidcomments
    407                  self.addnote(otherpo.getnotes("developer"), origin="developer", position="merge")
    408                  self.msgidcomment = otherpo._extract_msgidcomments() or None
    409                  self.addlocations(otherpo.getlocations())
    410          if not self.istranslated() or overwrite:
    411              # Remove kde-style comments from the translation (if any).
    412              if self._extract_msgidcomments(otherpo.target):
    413                  otherpo.target = otherpo.target.replace('_: ' + otherpo._extract_msgidcomments() + '\n', '')
    414              self.target = otherpo.target
    415              if self.source != otherpo.source or self.getcontext() != otherpo.getcontext():
    416                  self.markfuzzy()
    417              else:
    418                  self.markfuzzy(otherpo.isfuzzy())
    419          elif not otherpo.istranslated():
    420              if self.source != otherpo.source:
    421                  self.markfuzzy()
    422          else:
    423              if self.target != otherpo.target:
    424                  self.markfuzzy()
    425
    426 -    def isheader(self):
    427          #return self.source == u"" and self.target != u""
    428          # we really want to make sure that there is no msgidcomment or msgctxt
    429          return self.getid() == "" and len(self.target) > 0
    430
    431 -    def isblank(self):
    432          return len(self.source) == len(self.target) == len(self.getcontext()) == 0
    433
    434 -    def hastypecomment(self, typecomment):
    435          return gpo.po_message_is_format(self._gpo_message, typecomment)
    436
    437 -    def settypecomment(self, typecomment, present=True):
    438          gpo.po_message_set_format(self._gpo_message, typecomment, present)
    439
    440 -    def hasmarkedcomment(self, commentmarker):
    441          commentmarker = "(%s)" % commentmarker
    442          for comment in self.getnotes("translator").split("\n"):
    443              if comment.startswith(commentmarker):
    444                  return True
    445          return False
    446
    447 -    def isfuzzy(self):
    448          return gpo.po_message_is_fuzzy(self._gpo_message)
    449
    450 -    def _domarkfuzzy(self, present=True):
    451          gpo.po_message_set_fuzzy(self._gpo_message, present)
    452
    453 -    def makeobsolete(self):
    454          # FIXME: libgettexpo currently does not reset other data, we probably want to do that
    455          # but a better solution would be for libgettextpo to output correct data on serialisation
    456          gpo.po_message_set_obsolete(self._gpo_message, True)
    457          self.infer_state()
    458
    459 -    def resurrect(self):
    460          gpo.po_message_set_obsolete(self._gpo_message, False)
    461          self.infer_state()
    462
    463 -    def hasplural(self):
    464          return gpo.po_message_msgid_plural(self._gpo_message) is not None
    465
    466 -    def _extract_msgidcomments(self, text=None):
    467          """Extract KDE style msgid comments from the unit.
    468
    469          @rtype: String
    470          @return: Returns the extracted msgidcomments found in this unit's msgid.
    471          """
    472          if not text:
    473              text = (gpo.po_message_msgid(self._gpo_message) or "").decode(self._encoding)
    474          if text:
    475              return pocommon.extract_msgid_comment(text)
    476          return u""
    477
    478 -    def setmsgidcomment(self, msgidcomment):
    479          if msgidcomment:
    480              self.source = u"_: %s\n%s" % (msgidcomment, self.source)
    481      msgidcomment = property(_extract_msgidcomments, setmsgidcomment)
    482
    483 -    def __str__(self):
    484          pf = pofile(noheader=True)
    485          pf.addunit(self)
    486          return str(pf)
    487
    488 -    def getlocations(self):
    489          locations = []
    490          i = 0
    491          location = gpo.po_message_filepos(self._gpo_message, i)
    492          while location:
    493              locname = gpo.po_filepos_file(location)
    494              locline = gpo.po_filepos_start_line(location)
    495              if locline == -1:
    496                  locstring = locname
    497              else:
    498                  locstring = locname + ":" + str(locline)
    499              locations.append(pocommon.unquote_plus(locstring))
    500              i += 1
    501              location = gpo.po_message_filepos(self._gpo_message, i)
    502          return locations
    503
    504 -    def addlocation(self, location):
    505          if location.find(" ") != -1:
    506              location = pocommon.quote_plus(location)
    507          parts = location.split(":")
    508          file = parts[0]
    509          if len(parts) == 2:
    510              line = int(parts[1] or "0")
    511          else:
    512              line = -1
    513          gpo.po_message_add_filepos(self._gpo_message, file, line)
    514
    515 -    def getcontext(self):
    516          msgctxt = gpo.po_message_msgctxt(self._gpo_message)
    517          if msgctxt:
    518              return msgctxt.decode(self._encoding)
    519          else:
    520              msgidcomment = self._extract_msgidcomments()
    521              return msgidcomment
    522
    523 -    def setcontext(self, context):
    524          context = data.forceunicode(context)
    525          gpo.po_message_set_msgctxt(self._gpo_message, context)
    526
    527 -    def buildfromunit(cls, unit, encoding=None):
    528          """Build a native unit from a foreign unit, preserving as much
    529          information as possible."""
    530          if type(unit) == cls and hasattr(unit, "copy") and callable(unit.copy):
    531              return unit.copy()
    532          elif isinstance(unit, pocommon.pounit):
    533              newunit = cls(unit.source, encoding)
    534              newunit.target = unit.target
    535              #context
    536              newunit.msgidcomment = unit._extract_msgidcomments()
    537              context = unit.getcontext()
    538              if not newunit.msgidcomment and context:
    539                  gpo.po_message_set_msgctxt(newunit._gpo_message, context)
    540
    541              locations = unit.getlocations()
    542              if locations:
    543                  newunit.addlocations(locations)
    544              notes = unit.getnotes("developer")
    545              if notes:
    546                  newunit.addnote(notes, "developer")
    547              notes = unit.getnotes("translator")
    548              if notes:
    549                  newunit.addnote(notes, "translator")
    550              if unit.isobsolete():
    551                  newunit.makeobsolete()
    552              newunit.markfuzzy(unit.isfuzzy())
    553              for tc in ['python-format', 'c-format', 'php-format']:
    554                  if unit.hastypecomment(tc):
    555                      newunit.settypecomment(tc)
    556                      # We assume/guess/hope that there will only be one
    557                      break
    558              return newunit
    559          else:
    560              return base.TranslationUnit.buildfromunit(unit)
    561      buildfromunit = classmethod(buildfromunit)
    562
    563
    564 -class pofile(pocommon.pofile):
    565      UnitClass = pounit
    566
    567 -    def __init__(self, inputfile=None, encoding=None, unitclass=pounit, noheader=False):
    568          self._gpo_memory_file = None
    569          self._gpo_message_iterator = None
    570          self.units = []
    571          self.sourcelanguage = None
    572          self.targetlanguage = None
    573          self._encoding = 'utf-8'
    574          if inputfile is None:
    575              self._gpo_memory_file = gpo.po_file_create()
    576              self._gpo_message_iterator = gpo.po_message_iterator(self._gpo_memory_file, None)
    577              if not noheader:
    578                  self.init_headers()
    579          else:
    580              super(pofile, self).__init__(inputfile=inputfile, encoding=encoding)
    581
    582 -    def addunit(self, unit, new=True):
    583          if new:
    584              gpo.po_message_insert(self._gpo_message_iterator, unit._gpo_message)
    585          super(pofile, self).addunit(unit)
    586
    587 -    def _insert_header(self, header):
    588          header._store = self
    589          self.units.insert(0, header)
    590          gpo.po_message_iterator_free(self._gpo_message_iterator)
    591          self._gpo_message_iterator = gpo.po_message_iterator(self._gpo_memory_file, None)
    592          gpo.po_message_insert(self._gpo_message_iterator, header._gpo_message)
    593          while gpo.po_next_message(self._gpo_message_iterator):
    594              pass
    595
    596 -    def removeduplicates(self, duplicatestyle="merge"):
    597          """make sure each msgid is unique ; merge comments etc from duplicates into original"""
    598          # TODO: can we handle consecutive calls to removeduplicates()? What
    599          # about files already containing msgctxt? - test
    600          id_dict = {}
    601          uniqueunits = []
    602          # TODO: this is using a list as the pos aren't hashable, but this is slow.
    603          # probably not used frequently enough to worry about it, though.
    604          markedpos = []
    605
    606          def addcomment(thepo):
    607              thepo.msgidcomment = " ".join(thepo.getlocations())
    608              markedpos.append(thepo)
    609          for thepo in self.units:
    610              id = thepo.getid()
    611              if thepo.isheader() and not thepo.getlocations():
    612                  # header msgids shouldn't be merged...
    613                  uniqueunits.append(thepo)
    614              elif id in id_dict:
    615                  if duplicatestyle == "merge":
    616                      if id:
    617                          id_dict[id].merge(thepo)
    618                      else:
    619                          addcomment(thepo)
    620                          uniqueunits.append(thepo)
    621                  elif duplicatestyle == "msgctxt":
    622                      origpo = id_dict[id]
    623                      if origpo not in markedpos:
    624                          gpo.po_message_set_msgctxt(origpo._gpo_message, " ".join(origpo.getlocations()))
    625                          markedpos.append(thepo)
    626                      gpo.po_message_set_msgctxt(thepo._gpo_message, " ".join(thepo.getlocations()))
    627                      uniqueunits.append(thepo)
    628              else:
    629                  if not id:
    630                      if duplicatestyle == "merge":
    631                          addcomment(thepo)
    632                      else:
    633                          gpo.po_message_set_msgctxt(thepo._gpo_message, " ".join(thepo.getlocations()))
    634                  id_dict[id] = thepo
    635                  uniqueunits.append(thepo)
    636          new_gpo_memory_file = gpo.po_file_create()
    637          new_gpo_message_iterator = gpo.po_message_iterator(new_gpo_memory_file, None)
    638          for unit in uniqueunits:
    639              gpo.po_message_insert(new_gpo_message_iterator, unit._gpo_message)
    640          gpo.po_message_iterator_free(self._gpo_message_iterator)
    641          self._gpo_message_iterator = new_gpo_message_iterator
    642          self._gpo_memory_file = new_gpo_memory_file
    643          self.units = uniqueunits
    644
    645 -    def __str__(self):
    646
    647          def obsolete_workaround():
    648              # Remove all items that are not output by msgmerge when a unit is obsolete.  This is a work
    649              # around for bug in libgettextpo
    650              # FIXME Do version test in case they fix this bug
    651              for unit in self.units:
    652                  if unit.isobsolete():
    653                      gpo.po_message_set_extracted_comments(unit._gpo_message, "")
    654                      location = gpo.po_message_filepos(unit._gpo_message, 0)
    655                      while location:
    656                          gpo.po_message_remove_filepos(unit._gpo_message, 0)
    657                          location = gpo.po_message_filepos(unit._gpo_message, 0)
    658          outputstring = ""
    659          if self._gpo_memory_file:
    660              obsolete_workaround()
    661              f, fname = tempfile.mkstemp(prefix='translate', suffix='.po')
    662              os.close(f)
    663              self._gpo_memory_file = gpo.po_file_write_v2(self._gpo_memory_file, fname, xerror_handler)
    664              f = open(fname)
    665              outputstring = f.read()
    666              f.close()
    667              os.remove(fname)
    668          return outputstring
    669
    670 -    def isempty(self):
    671          """Returns True if the object doesn't contain any translation units."""
    672          if len(self.units) == 0:
    673              return True
    674          # Skip the first unit if it is a header.
    675          if self.units[0].isheader():
    676              units = self.units[1:]
    677          else:
    678              units = self.units
    679
    680          for unit in units:
    681              if not unit.isblank() and not unit.isobsolete():
    682                  return False
    683          return True
    684
    685 -    def parse(self, input):
    686          if hasattr(input, 'name'):
    687              self.filename = input.name
    688          elif not getattr(self, 'filename', ''):
    689              self.filename = ''
    690
    691          if hasattr(input, "read"):
    692              posrc = input.read()
    693              input.close()
    694              input = posrc
    695
    696          needtmpfile = not os.path.isfile(input)
    697          if needtmpfile:
    698              # This is not a file - we write the string to a temporary file
    699              fd, fname = tempfile.mkstemp(prefix='translate', suffix='.po')
    700              os.write(fd, input)
    701              input = fname
    702              os.close(fd)
    703
    704          self._gpo_memory_file = gpo.po_file_read_v3(input, xerror_handler)
    705          if self._gpo_memory_file is None:
    706              print >> sys.stderr, "Error:"
    707
    708          if needtmpfile:
    709              os.remove(input)
    710
    711          self.units = []
    712          # Handle xerrors here
    713          self._header = gpo.po_file_domain_header(self._gpo_memory_file, None)
    714          if self._header:
    715              charset = gpo.po_header_field(self._header, "Content-Type")
    716              if charset:
    717                  charset = re.search("charset=([^\\s]+)", charset).group(1)
    718              self._encoding = encodingToUse(charset)
    719          self._gpo_message_iterator = gpo.po_message_iterator(self._gpo_memory_file, None)
    720          newmessage = gpo.po_next_message(self._gpo_message_iterator)
    721          while newmessage:
    722              newunit = pounit(gpo_message=newmessage, encoding=self._encoding)
    723              self.addunit(newunit, new=False)
    724              newmessage = gpo.po_next_message(self._gpo_message_iterator)
    725          self._free_iterator()
    726
    727 -    def __del__(self):
    728          # We currently disable this while we still get segmentation faults.
    729          # Note that this is definitely leaking memory because of this.
    730          return
    731          self._free_iterator()
    732          if self._gpo_memory_file is not None:
    733              gpo.po_file_free(self._gpo_memory_file)
    734              self._gpo_memory_file = None
    735
    736 -    def _free_iterator(self):
    737          # We currently disable this while we still get segmentation faults.
    738          # Note that this is definitely leaking memory because of this.
    739          return
    740          if self._gpo_message_iterator is not None:
    741              gpo.po_message_iterator_free(self._gpo_message_iterator)
    742              self._gpo_message_iterator = None
    743