translate.storage.projstore

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

# Source Code for Module translate.storage.projstore

      1  #!/usr/bin/env python
      2  # -*- coding: utf-8 -*-
      3  #
      4  # Copyright 2010 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  import os
     22  from StringIO import StringIO
     23
     24  from lxml import etree
     25
     26
     27  __all__ = ['FileExistsInProjectError', 'FileNotInProjectError', 'ProjectStore']
     28
     29
     30 -class FileExistsInProjectError(Exception):
     31      pass
     32
     33
     34 -class FileNotInProjectError(Exception):
     35      pass
     36
     37
     38 -class ProjectStore(object):
     39      """Basic project file container."""
     40
     41      # INITIALIZERS #
     42 -    def __init__(self):
     43          self._files = {}
     44          self._sourcefiles = []
     45          self._targetfiles = []
     46          self._transfiles = []
     47          self.settings = {}
     48          self.convert_map = {}
     49          # The above map maps the conversion of input files (keys) to its output
     50          # file and template used (2-tuple). All values are project file names.
     51          # eg. convert_map = {
     52          #    'sources/doc.odt':   ('trans/doc.odt.xlf', None),
     53          #    'trans/doc.odt.xlf': ('targets/doc.odt', 'sources/doc.odt')
     54          #}
     55
     56          # The following dict groups together sets of mappings from a file
     57          # "type" string ("src", "tgt" or "trans") to various other values
     58          # or objects.
     59          self.TYPE_INFO = {
     60              # type => prefix for new files
     61              'f_prefix': {
     62                  'src': 'sources/',
     63                  'tgt': 'targets/',
     64                  'trans': 'trans/',
     65              },
     66              # type => list containing filenames for that type
     67              'lists': {
     68                  'src': self._sourcefiles,
     69                  'tgt': self._targetfiles,
     70                  'trans': self._transfiles,
     71              },
     72              # type => next type in process: src => trans => tgt
     73              'next_type': {
     74                  'src': 'trans',
     75                  'trans': 'tgt',
     76                  'tgt': None,
     77              },
     78              # type => name of the sub-section in the settings file/dict
     79              'settings': {
     80                  'src': 'sources',
     81                  'tgt': 'targets',
     82                  'trans': 'transfiles',
     83              }
     84          }
     85
     86 -    def __del__(self):
     87          try:
     88              self.close()
     89          except Exception:
     90              pass
     91
     92
     93      # ACCESSORS #
     94 -    def _get_sourcefiles(self):
     95          """Read-only access to C{self._sourcefiles}."""
     96          return tuple(self._sourcefiles)
     97      sourcefiles = property(_get_sourcefiles)
     98
     99 -    def _get_targetfiles(self):
    100          """Read-only access to C{self._targetfiles}."""
    101          return tuple(self._targetfiles)
    102      targetfiles = property(_get_targetfiles)
    103
    104 -    def _get_transfiles(self):
    105          """Read-only access to C{self._transfiles}."""
    106          return tuple(self._transfiles)
    107      transfiles = property(_get_transfiles)
    108
    109
    110      # SPECIAL METHODS #
    111 -    def __in__(self, lhs):
    112          """@returns C{True} if C{lhs} is a file name or file object in the project store."""
    113          return  lhs in self._sourcefiles or \
    114                  lhs in self._targetfiles or \
    115                  lhs in self._transfiles or \
    116                  lhs in self._files or \
    117                  lhs in self._files.values()
    118
    119
    120      # METHODS #
    121 -    def append_file(self, afile, fname, ftype='trans', delete_orig=False):
    122          """Append the given file to the project with the given filename, marked
    123              to be of type C{ftype} ('src', 'trans', 'tgt').
    124
    125              @type  delete_orig: bool
    126              @param delete_orig: Whether or not the original (given) file should
    127                                  be deleted after being appended. This is set to
    128                                  C{True} by L{project.convert_forward()}. Not
    129                                  used in this class."""
    130          if not ftype in self.TYPE_INFO['f_prefix']:
    131              raise ValueError('Invalid file type: %s' % (ftype))
    132
    133          if isinstance(afile, basestring) and os.path.isfile(afile) and not fname:
    134              # Try and use afile as the file name
    135              fname, afile = afile, open(afile)
    136
    137          # Check if we can get an real file name
    138          realfname = fname
    139          if realfname is None or not os.path.isfile(realfname):
    140              realfname = getattr(afile, 'name', None)
    141          if realfname is None or not os.path.isfile(realfname):
    142              realfname = getattr(afile, 'filename', None)
    143          if not realfname or not os.path.isfile(realfname):
    144              realfname = None
    145
    146          # Try to get the file name from the file object, if it was not given:
    147          if not fname:
    148              fname = getattr(afile, 'name', None)
    149          if not fname:
    150              fname = getattr(afile, 'filename', None)
    151
    152          fname = self._fix_type_filename(ftype, fname)
    153
    154          if not fname:
    155              raise ValueError('Could not deduce file name and none given')
    156          if fname in self._files:
    157              raise FileExistsInProjectError(fname)
    158
    159          if realfname is not None and os.path.isfile(realfname):
    160              self._files[fname] = realfname
    161          else:
    162              self._files[fname] = afile
    163          self.TYPE_INFO['lists'][ftype].append(fname)
    164
    165          return afile, fname
    166
    167 -    def append_sourcefile(self, afile, fname=None):
    168          return self.append_file(afile, fname, ftype='src')
    169
    170 -    def append_targetfile(self, afile, fname=None):
    171          return self.append_file(afile, fname, ftype='tgt')
    172
    173 -    def append_transfile(self, afile, fname=None):
    174          return self.append_file(afile, fname, ftype='trans')
    175
    176 -    def remove_file(self, fname, ftype=None):
    177          """Remove the file with the given project name from the project.
    178              If the file type ('src', 'trans' or 'tgt') is not given, it is
    179              guessed."""
    180          if fname not in self._files:
    181              raise FileNotInProjectError(fname)
    182          if not ftype:
    183              # Guess file type (source/trans/target)
    184              for ft, prefix in self.TYPE_INFO['f_prefix'].items():
    185                  if fname.startswith(prefix):
    186                      ftype = ft
    187                      break
    188
    189          self.TYPE_INFO['lists'][ftype].remove(fname)
    190          if self._files[fname] and hasattr(self._files[fname], 'close'):
    191              self._files[fname].close()
    192          del self._files[fname]
    193
    194 -    def remove_sourcefile(self, fname):
    195          self.remove_file(fname, ftype='src')
    196
    197 -    def remove_targetfile(self, fname):
    198          self.remove_file(fname, ftype='tgt')
    199
    200 -    def remove_transfile(self, fname):
    201          self.remove_file(fname, ftype='trans')
    202
    203 -    def close(self):
    204          self.save()
    205
    206 -    def get_file(self, fname, mode='rb'):
    207          """Retrieve the file with the given name from the project store.
    208
    209              The file is looked up in the C{self._files} dictionary. The values
    210              in this dictionary may be C{None}, to indicate that the file is not
    211              cacheable and needs to be retrieved in a special way. This special
    212              way must be defined in this method of sub-classes. The value may
    213              also be a string, which indicates that it is a real file accessible
    214              via C{open()}.
    215
    216              @type  mode: str
    217              @param mode: The mode in which to re-open the file (if it is closed)
    218              @see BundleProjectStore.get_file"""
    219          if fname not in self._files:
    220              raise FileNotInProjectError(fname)
    221
    222          rfile = self._files[fname]
    223          if isinstance(rfile, basestring):
    224              rfile = open(rfile, 'rb')
    225          # Check that the file is actually open
    226          if getattr(rfile, 'closed', False):
    227              rfname = fname
    228              if not os.path.isfile(rfname):
    229                  rfname = getattr(rfile, 'name', None)
    230              if not rfile or not os.path.isfile(rfname):
    231                  rfname = getattr(rfile, 'filename', None)
    232              if not rfile or not os.path.isfile(rfname):
    233                  raise IOError('Could not locate file: %s (%s)' % (rfile, fname))
    234              rfile = open(rfname, mode)
    235              self._files[fname] = rfile
    236
    237          return rfile
    238
    239 -    def get_filename_type(self, fname):
    240          """Get the type of file ('src', 'trans', 'tgt') with the given name."""
    241          for ftype in self.TYPE_INFO['lists']:
    242              if fname in self.TYPE_INFO['lists'][ftype]:
    243                  return ftype
    244          raise FileNotInProjectError(fname)
    245
    246 -    def get_proj_filename(self, realfname):
    247          """Try and find a project file name for the given real file name."""
    248          for fname in self._files:
    249              if fname == realfname or self._files[fname] == realfname:
    250                  return fname
    251          raise ValueError('Real file not in project store: %s' % (realfname))
    252
    253 -    def load(self, *args, **kwargs):
    254          """Load the project in some way. Undefined for this (base) class."""
    255          pass
    256
    257 -    def save(self, filename=None, *args, **kwargs):
    258          """Save the project in some way. Undefined for this (base) class."""
    259          pass
    260
    261 -    def update_file(self, pfname, infile):
    262          """Remove the project file with name C{pfname} and add the contents
    263              from C{infile} to the project under the same file name.
    264
    265              @returns: the results from L{self.append_file}."""
    266          ftype = self.get_filename_type(pfname)
    267          self.remove_file(pfname)
    268          self.append_file(infile, pfname, ftype)
    269
    270 -    def _fix_type_filename(self, ftype, fname):
    271          """Strip the path from the filename and prepend the correct prefix."""
    272          path, fname = os.path.split(fname)
    273          return self.TYPE_INFO['f_prefix'][ftype] + fname
    274
    275 -    def _generate_settings(self):
    276          """@returns A XML string that represents the current settings."""
    277          xml = etree.Element('translationproject')
    278
    279          # Add file names to settings XML
    280          if self._sourcefiles:
    281              sources_el = etree.Element('sources')
    282              for fname in self._sourcefiles:
    283                  src_el = etree.Element('filename')
    284                  src_el.text = fname
    285                  sources_el.append(src_el)
    286              xml.append(sources_el)
    287          if self._transfiles:
    288              transfiles_el = etree.Element('transfiles')
    289              for fname in self._transfiles:
    290                  trans_el = etree.Element('filename')
    291                  trans_el.text = fname
    292                  transfiles_el.append(trans_el)
    293              xml.append(transfiles_el)
    294          if self._targetfiles:
    295              target_el = etree.Element('targets')
    296              for fname in self._targetfiles:
    297                  tgt_el = etree.Element('filename')
    298                  tgt_el.text = fname
    299                  target_el.append(tgt_el)
    300              xml.append(target_el)
    301
    302          # Add conversion mappings
    303          if self.convert_map:
    304              conversions_el = etree.Element('conversions')
    305              for in_fname, (out_fname, templ_fname) in self.convert_map.iteritems():
    306                  if in_fname not in self._files or out_fname not in self._files:
    307                      continue
    308                  conv_el = etree.Element('conv')
    309
    310                  input_el = etree.Element('input')
    311                  input_el.text = in_fname
    312                  conv_el.append(input_el)
    313
    314                  output_el = etree.Element('output')
    315                  output_el.text = out_fname
    316                  conv_el.append(output_el)
    317
    318                  if templ_fname:
    319                      templ_el = etree.Element('template')
    320                      templ_el.text = templ_fname
    321                      conv_el.append(templ_el)
    322
    323                  conversions_el.append(conv_el)
    324              xml.append(conversions_el)
    325
    326          # Add options to settings
    327          if 'options' in self.settings:
    328              options_el = etree.Element('options')
    329              for option, value in self.settings['options'].items():
    330                  opt_el = etree.Element('option')
    331                  opt_el.attrib['name'] = option
    332                  opt_el.text = value
    333                  options_el.append(opt_el)
    334              xml.append(options_el)
    335
    336          return etree.tostring(xml, pretty_print=True)
    337
    338 -    def _load_settings(self, settingsxml):
    339          """Load project settings from the given XML string.
    340              C{settingsxml} is parsed into a DOM tree (L{lxml.etree.fromstring})
    341              which is then inspected."""
    342          settings = {}
    343          xml = etree.fromstring(settingsxml)
    344
    345          # Load files in project
    346          for section in ('sources', 'targets', 'transfiles'):
    347              groupnode = xml.find(section)
    348              if groupnode is None:
    349                  continue
    350
    351              settings[section] = []
    352              for fnode in groupnode.getchildren():
    353                  settings[section].append(fnode.text)
    354
    355          conversions_el = xml.find('conversions')
    356          if conversions_el is not None:
    357              self.convert_map = {}
    358              for conv_el in conversions_el.iterchildren():
    359                  in_fname, out_fname, templ_fname = None, None, None
    360                  for child_el in conv_el.iterchildren():
    361                      if child_el.tag == 'input':
    362                          in_fname = child_el.text
    363                      elif child_el.tag == 'output':
    364                          out_fname = child_el.text
    365                      elif child_el.tag == 'template':
    366                          templ_fname = child_el.text
    367                  # Make sure that in_fname and out_fname exist in
    368                  # settings['sources'], settings['targets'] or
    369                  # settings['transfiles']
    370                  in_found, out_found, templ_found = False, False, False
    371                  for section in ('sources', 'transfiles', 'targets'):
    372                      if section not in settings:
    373                          continue
    374                      if in_fname in settings[section]:
    375                          in_found = True
    376                      if out_fname in settings[section]:
    377                          out_found = True
    378                      if templ_fname and templ_fname in settings[section]:
    379                          templ_found = True
    380                  if in_found and out_found and (not templ_fname or templ_found):
    381                      self.convert_map[in_fname] = (out_fname, templ_fname)
    382
    383          # Load options
    384          groupnode = xml.find('options')
    385          if groupnode is not None:
    386              settings['options'] = {}
    387              for opt in groupnode.iterchildren():
    388                  settings['options'][opt.attrib['name']] = opt.text
    389
    390          self.settings = settings
    391