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

Source Code for Module translate.storage.base

  1  #!/usr/bin/env python 
  2  # -*- coding: utf-8 -*- 
  3  #  
  4  # Copyright 2006-2008 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  """Base classes for storage interfaces. 
 23   
 24  @organization: Zuza Software Foundation 
 25  @copyright: 2006-2007 Zuza Software Foundation 
 26  @license: U{GPL <http://www.fsf.org/licensing/licenses/gpl.html>} 
 27  """ 
 28   
 29  try: 
 30      import cPickle as pickle 
 31  except: 
 32      import pickle 
 33  from exceptions import NotImplementedError 
 34   
35 -def force_override(method, baseclass):
36 """Forces derived classes to override method.""" 37 38 if type(method.im_self) == type(baseclass): 39 # then this is a classmethod and im_self is the actual class 40 actualclass = method.im_self 41 else: 42 actualclass = method.im_class 43 if actualclass != baseclass: 44 raise NotImplementedError("%s does not reimplement %s as required by %s" % (actualclass.__name__, method.__name__, baseclass.__name__))
45
46 -class TranslationUnit(object):
47 """Base class for translation units. 48 49 Our concept of a I{translation unit} is influenced heavily by XLIFF: 50 U{http://www.oasis-open.org/committees/xliff/documents/xliff-specification.htm} 51 52 As such most of the method- and variable names borrows from XLIFF terminology. 53 54 A translation unit consists of the following: 55 - A I{source} string. This is the original translatable text. 56 - A I{target} string. This is the translation of the I{source}. 57 - Zero or more I{notes} on the unit. Notes would typically be some 58 comments from a translator on the unit, or some comments originating from 59 the source code. 60 - Zero or more I{locations}. Locations indicate where in the original 61 source code this unit came from. 62 - Zero or more I{errors}. Some tools (eg. L{pofilter <filters.pofilter>}) can run checks on 63 translations and produce error messages. 64 65 @group Source: *source* 66 @group Target: *target* 67 @group Notes: *note* 68 @group Locations: *location* 69 @group Errors: *error* 70 """ 71
72 - def __init__(self, source):
73 """Constructs a TranslationUnit containing the given source string.""" 74 75 self.source = source 76 self.target = None 77 self.notes = "" 78 super(TranslationUnit, self).__init__()
79
80 - def __eq__(self, other):
81 """Compares two TranslationUnits. 82 83 @type other: L{TranslationUnit} 84 @param other: Another L{TranslationUnit} 85 @rtype: Boolean 86 @return: Returns True if the supplied TranslationUnit equals this unit. 87 88 """ 89 90 return self.source == other.source and self.target == other.target
91
92 - def settarget(self, target):
93 """Sets the target string to the given value.""" 94 95 self.target = target
96
97 - def gettargetlen(self):
98 """Returns the length of the target string. 99 100 @note: Plural forms might be combined. 101 @rtype: Integer 102 103 """ 104 105 length = len(self.target or "") 106 strings = getattr(self.target, "strings", []) 107 if strings: 108 length += sum([len(pluralform) for pluralform in strings[1:]]) 109 return length
110
111 - def getid(self):
112 """A unique identifier for this unit. 113 114 @rtype: string 115 @return: an identifier for this unit that is unique in the store 116 117 Derived classes should override this in a way that guarantees a unique 118 identifier for each unit in the store. 119 """ 120 return self.source
121
122 - def getlocations(self):
123 """A list of source code locations. 124 125 @note: Shouldn't be implemented if the format doesn't support it. 126 @rtype: List 127 128 """ 129 130 return []
131
132 - def addlocation(self, location):
133 """Add one location to the list of locations. 134 135 @note: Shouldn't be implemented if the format doesn't support it. 136 137 """ 138 pass
139
140 - def addlocations(self, location):
141 """Add a location or a list of locations. 142 143 @note: Most classes shouldn't need to implement this, 144 but should rather implement L{addlocation()}. 145 @warning: This method might be removed in future. 146 147 """ 148 149 if isinstance(location, list): 150 for item in location: 151 self.addlocation(item) 152 else: 153 self.addlocation(location)
154
155 - def getcontext(self):
156 """Get the message context.""" 157 return ""
158
159 - def getnotes(self, origin=None):
160 """Returns all notes about this unit. 161 162 It will probably be freeform text or something reasonable that can be 163 synthesised by the format. 164 It should not include location comments (see L{getlocations()}). 165 166 """ 167 return getattr(self, "notes", "")
168
169 - def addnote(self, text, origin=None):
170 """Adds a note (comment). 171 172 @type text: string 173 @param text: Usually just a sentence or two. 174 @type origin: string 175 @param origin: Specifies who/where the comment comes from. 176 Origin can be one of the following text strings: 177 - 'translator' 178 - 'developer', 'programmer', 'source code' (synonyms) 179 180 """ 181 if getattr(self, "notes", None): 182 self.notes += '\n'+text 183 else: 184 self.notes = text
185
186 - def removenotes(self):
187 """Remove all the translator's notes.""" 188 189 self.notes = u''
190
191 - def adderror(self, errorname, errortext):
192 """Adds an error message to this unit. 193 194 @type errorname: string 195 @param errorname: A single word to id the error. 196 @type errortext: string 197 @param errortext: The text describing the error. 198 199 """ 200 201 pass
202
203 - def geterrors(self):
204 """Get all error messages. 205 206 @rtype: Dictionary 207 208 """ 209 210 return {}
211
212 - def markreviewneeded(self, needsreview=True, explanation=None):
213 """Marks the unit to indicate whether it needs review. 214 215 @keyword needsreview: Defaults to True. 216 @keyword explanation: Adds an optional explanation as a note. 217 218 """ 219 220 pass
221
222 - def istranslated(self):
223 """Indicates whether this unit is translated. 224 225 This should be used rather than deducing it from .target, 226 to ensure that other classes can implement more functionality 227 (as XLIFF does). 228 229 """ 230 231 return bool(self.target) and not self.isfuzzy()
232
233 - def istranslatable(self):
234 """Indicates whether this unit can be translated. 235 236 This should be used to distinguish real units for translation from 237 header, obsolete, binary or other blank units. 238 """ 239 return True
240
241 - def isfuzzy(self):
242 """Indicates whether this unit is fuzzy.""" 243 244 return False
245
246 - def markfuzzy(self, value=True):
247 """Marks the unit as fuzzy or not.""" 248 pass
249
250 - def isheader(self):
251 """Indicates whether this unit is a header.""" 252 253 return False
254
255 - def isreview(self):
256 """Indicates whether this unit needs review.""" 257 return False
258 259
260 - def isblank(self):
261 """Used to see if this unit has no source or target string. 262 263 @note: This is probably used more to find translatable units, 264 and we might want to move in that direction rather and get rid of this. 265 266 """ 267 268 return not (self.source or self.target)
269
270 - def hasplural(self):
271 """Tells whether or not this specific unit has plural strings.""" 272 273 #TODO: Reconsider 274 return False
275
276 - def merge(self, otherunit, overwrite=False, comments=True):
277 """Do basic format agnostic merging.""" 278 279 if self.target == "" or overwrite: 280 self.target = otherunit.target
281
282 - def unit_iter(self):
283 """Iterator that only returns this unit.""" 284 yield self
285
286 - def getunits(self):
287 """This unit in a list.""" 288 return [self]
289
290 - def buildfromunit(cls, unit):
291 """Build a native unit from a foreign unit, preserving as much 292 information as possible.""" 293 294 if type(unit) == cls and hasattr(unit, "copy") and callable(unit.copy): 295 return unit.copy() 296 newunit = cls(unit.source) 297 newunit.target = unit.target 298 newunit.markfuzzy(unit.isfuzzy()) 299 locations = unit.getlocations() 300 if locations: 301 newunit.addlocations(locations) 302 notes = unit.getnotes() 303 if notes: 304 newunit.addnote(notes) 305 return newunit
306 buildfromunit = classmethod(buildfromunit)
307
308 -class TranslationStore(object):
309 """Base class for stores for multiple translation units of type UnitClass.""" 310 311 UnitClass = TranslationUnit 312
313 - def __init__(self, unitclass=None):
314 """Constructs a blank TranslationStore.""" 315 316 self.units = [] 317 self.filepath = None 318 self.translator = "" 319 self.date = "" 320 if unitclass: 321 self.UnitClass = unitclass 322 super(TranslationStore, self).__init__()
323
324 - def unit_iter(self):
325 """Iterator over all the units in this store.""" 326 for unit in self.units: 327 yield unit
328
329 - def getunits(self):
330 """Return a list of all units in this store.""" 331 return [unit for unit in self.unit_iter()]
332
333 - def addunit(self, unit):
334 """Appends the given unit to the object's list of units. 335 336 This method should always be used rather than trying to modify the 337 list manually. 338 339 @type unit: L{TranslationUnit} 340 @param unit: The unit that will be added. 341 342 """ 343 344 self.units.append(unit)
345
346 - def addsourceunit(self, source):
347 """Adds and returns a new unit with the given source string. 348 349 @rtype: L{TranslationUnit} 350 351 """ 352 353 unit = self.UnitClass(source) 354 self.addunit(unit) 355 return unit
356
357 - def findunit(self, source):
358 """Finds the unit with the given source string. 359 360 @rtype: L{TranslationUnit} or None 361 362 """ 363 364 if len(getattr(self, "sourceindex", [])): 365 if source in self.sourceindex: 366 return self.sourceindex[source] 367 else: 368 for unit in self.units: 369 if unit.source == source: 370 return unit 371 return None
372
373 - def translate(self, source):
374 """Returns the translated string for a given source string. 375 376 @rtype: String or None 377 378 """ 379 380 unit = self.findunit(source) 381 if unit and unit.target: 382 return unit.target 383 else: 384 return None
385
386 - def makeindex(self):
387 """Indexes the items in this store. At least .sourceindex should be usefull.""" 388 389 self.locationindex = {} 390 self.sourceindex = {} 391 for unit in self.units: 392 # Do we need to test if unit.source exists? 393 self.sourceindex[unit.source] = unit 394 if unit.hasplural(): 395 for nounform in unit.source.strings[1:]: 396 self.sourceindex[nounform] = unit 397 for location in unit.getlocations(): 398 if location in self.locationindex: 399 # if sources aren't unique, don't use them 400 self.locationindex[location] = None 401 else: 402 self.locationindex[location] = unit
403
404 - def __str__(self):
405 """Converts to a string representation that can be parsed back using L{parsestring()}.""" 406 407 # We can't pickle fileobj if it is there, so let's hide it for a while. 408 fileobj = getattr(self, "fileobj", None) 409 self.fileobj = None 410 dump = pickle.dumps(self) 411 self.fileobj = fileobj 412 return dump
413
414 - def isempty(self):
415 """Returns True if the object doesn't contain any translation units.""" 416 417 if len(self.units) == 0: 418 return True 419 for unit in self.units: 420 if not (unit.isblank() or unit.isheader()): 421 return False 422 return True
423
424 - def _assignname(self):
425 """Tries to work out what the name of the filesystem file is and 426 assigns it to .filename.""" 427 fileobj = getattr(self, "fileobj", None) 428 if fileobj: 429 filename = getattr(fileobj, "name", getattr(fileobj, "filename", None)) 430 if filename: 431 self.filename = filename
432
433 - def parsestring(cls, storestring):
434 """Converts the string representation back to an object.""" 435 newstore = cls() 436 if storestring: 437 newstore.parse(storestring) 438 return newstore
439 parsestring = classmethod(parsestring) 440
441 - def parse(self, data):
442 """parser to process the given source string""" 443 self.units = pickle.loads(data).units
444
445 - def savefile(self, storefile):
446 """Writes the string representation to the given file (or filename).""" 447 if isinstance(storefile, basestring): 448 storefile = open(storefile, "w") 449 self.fileobj = storefile 450 self._assignname() 451 storestring = str(self) 452 storefile.write(storestring) 453 storefile.close()
454
455 - def save(self):
456 """Save to the file that data was originally read from, if available.""" 457 fileobj = getattr(self, "fileobj", None) 458 if not fileobj: 459 filename = getattr(self, "filename", None) 460 if filename: 461 fileobj = file(filename, "w") 462 else: 463 fileobj.close() 464 filename = getattr(fileobj, "name", getattr(fileobj, "filename", None)) 465 if not filename: 466 raise ValueError("No file or filename to save to") 467 fileobj = fileobj.__class__(filename, "w") 468 self.savefile(fileobj)
469
470 - def parsefile(cls, storefile):
471 """Reads the given file (or opens the given filename) and parses back to an object.""" 472 473 if isinstance(storefile, basestring): 474 storefile = open(storefile, "r") 475 mode = getattr(storefile, "mode", "r") 476 #For some reason GzipFile returns 1, so we have to test for that here 477 if mode == 1 or "r" in mode: 478 storestring = storefile.read() 479 storefile.close() 480 else: 481 storestring = "" 482 newstore = cls.parsestring(storestring) 483 newstore.fileobj = storefile 484 newstore._assignname() 485 return newstore
486 parsefile = classmethod(parsefile)
487