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