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