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