fix git support for v1.5.3 (or higher) by setting "--work-tree"
[translate_toolkit.git] / storage / base.py
blobd6c7e9f0290a3fe5b81a73dc087fc473dcf87171
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.
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.
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.
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
22 """Base classes for storage interfaces.
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 """
29 try:
30 import cPickle as pickle
31 except:
32 import pickle
33 from exceptions import NotImplementedError
35 def force_override(method, baseclass):
36 """Forces derived classes to override method."""
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__))
46 class ParseError(Exception):
47 pass
49 class TranslationUnit(object):
50 """Base class for translation units.
52 Our concept of a I{translation unit} is influenced heavily by XLIFF:
53 U{http://www.oasis-open.org/committees/xliff/documents/xliff-specification.htm}
55 As such most of the method- and variable names borrows from XLIFF terminology.
57 A translation unit consists of the following:
58 - A I{source} string. This is the original translatable text.
59 - A I{target} string. This is the translation of the I{source}.
60 - Zero or more I{notes} on the unit. Notes would typically be some
61 comments from a translator on the unit, or some comments originating from
62 the source code.
63 - Zero or more I{locations}. Locations indicate where in the original
64 source code this unit came from.
65 - Zero or more I{errors}. Some tools (eg. L{pofilter <filters.pofilter>}) can run checks on
66 translations and produce error messages.
68 @group Source: *source*
69 @group Target: *target*
70 @group Notes: *note*
71 @group Locations: *location*
72 @group Errors: *error*
73 """
75 def __init__(self, source):
76 """Constructs a TranslationUnit containing the given source string."""
78 self.source = source
79 self.target = None
80 self.notes = ""
81 super(TranslationUnit, self).__init__()
83 def __eq__(self, other):
84 """Compares two TranslationUnits.
86 @type other: L{TranslationUnit}
87 @param other: Another L{TranslationUnit}
88 @rtype: Boolean
89 @return: Returns True if the supplied TranslationUnit equals this unit.
91 """
93 return self.source == other.source and self.target == other.target
95 def settarget(self, target):
96 """Sets the target string to the given value."""
98 self.target = target
100 def gettargetlen(self):
101 """Returns the length of the target string.
103 @note: Plural forms might be combined.
104 @rtype: Integer
108 length = len(self.target or "")
109 strings = getattr(self.target, "strings", [])
110 if strings:
111 length += sum([len(pluralform) for pluralform in strings[1:]])
112 return length
114 def getid(self):
115 """A unique identifier for this unit.
117 @rtype: string
118 @return: an identifier for this unit that is unique in the store
120 Derived classes should override this in a way that guarantees a unique
121 identifier for each unit in the store.
123 return self.source
125 def getlocations(self):
126 """A list of source code locations.
128 @note: Shouldn't be implemented if the format doesn't support it.
129 @rtype: List
133 return []
135 def addlocation(self, location):
136 """Add one location to the list of locations.
138 @note: Shouldn't be implemented if the format doesn't support it.
141 pass
143 def addlocations(self, location):
144 """Add a location or a list of locations.
146 @note: Most classes shouldn't need to implement this,
147 but should rather implement L{addlocation()}.
148 @warning: This method might be removed in future.
152 if isinstance(location, list):
153 for item in location:
154 self.addlocation(item)
155 else:
156 self.addlocation(location)
158 def getcontext(self):
159 """Get the message context."""
160 return ""
162 def getnotes(self, origin=None):
163 """Returns all notes about this unit.
165 It will probably be freeform text or something reasonable that can be
166 synthesised by the format.
167 It should not include location comments (see L{getlocations()}).
170 return getattr(self, "notes", "")
172 def addnote(self, text, origin=None):
173 """Adds a note (comment).
175 @type text: string
176 @param text: Usually just a sentence or two.
177 @type origin: string
178 @param origin: Specifies who/where the comment comes from.
179 Origin can be one of the following text strings:
180 - 'translator'
181 - 'developer', 'programmer', 'source code' (synonyms)
184 if getattr(self, "notes", None):
185 self.notes += '\n'+text
186 else:
187 self.notes = text
189 def removenotes(self):
190 """Remove all the translator's notes."""
192 self.notes = u''
194 def adderror(self, errorname, errortext):
195 """Adds an error message to this unit.
197 @type errorname: string
198 @param errorname: A single word to id the error.
199 @type errortext: string
200 @param errortext: The text describing the error.
204 pass
206 def geterrors(self):
207 """Get all error messages.
209 @rtype: Dictionary
213 return {}
215 def markreviewneeded(self, needsreview=True, explanation=None):
216 """Marks the unit to indicate whether it needs review.
218 @keyword needsreview: Defaults to True.
219 @keyword explanation: Adds an optional explanation as a note.
223 pass
225 def istranslated(self):
226 """Indicates whether this unit is translated.
228 This should be used rather than deducing it from .target,
229 to ensure that other classes can implement more functionality
230 (as XLIFF does).
234 return bool(self.target) and not self.isfuzzy()
236 def istranslatable(self):
237 """Indicates whether this unit can be translated.
239 This should be used to distinguish real units for translation from
240 header, obsolete, binary or other blank units.
242 return True
244 def isfuzzy(self):
245 """Indicates whether this unit is fuzzy."""
247 return False
249 def markfuzzy(self, value=True):
250 """Marks the unit as fuzzy or not."""
251 pass
253 def isheader(self):
254 """Indicates whether this unit is a header."""
256 return False
258 def isreview(self):
259 """Indicates whether this unit needs review."""
260 return False
263 def isblank(self):
264 """Used to see if this unit has no source or target string.
266 @note: This is probably used more to find translatable units,
267 and we might want to move in that direction rather and get rid of this.
271 return not (self.source or self.target)
273 def hasplural(self):
274 """Tells whether or not this specific unit has plural strings."""
276 #TODO: Reconsider
277 return False
279 def merge(self, otherunit, overwrite=False, comments=True):
280 """Do basic format agnostic merging."""
282 if self.target == "" or overwrite:
283 self.target = otherunit.target
285 def unit_iter(self):
286 """Iterator that only returns this unit."""
287 yield self
289 def getunits(self):
290 """This unit in a list."""
291 return [self]
293 def buildfromunit(cls, unit):
294 """Build a native unit from a foreign unit, preserving as much
295 information as possible."""
297 if type(unit) == cls and hasattr(unit, "copy") and callable(unit.copy):
298 return unit.copy()
299 newunit = cls(unit.source)
300 newunit.target = unit.target
301 newunit.markfuzzy(unit.isfuzzy())
302 locations = unit.getlocations()
303 if locations:
304 newunit.addlocations(locations)
305 notes = unit.getnotes()
306 if notes:
307 newunit.addnote(notes)
308 return newunit
309 buildfromunit = classmethod(buildfromunit)
311 class TranslationStore(object):
312 """Base class for stores for multiple translation units of type UnitClass."""
314 UnitClass = TranslationUnit
316 def __init__(self, unitclass=None):
317 """Constructs a blank TranslationStore."""
319 self.units = []
320 self.filepath = None
321 self.translator = ""
322 self.date = ""
323 if unitclass:
324 self.UnitClass = unitclass
325 super(TranslationStore, self).__init__()
327 def unit_iter(self):
328 """Iterator over all the units in this store."""
329 for unit in self.units:
330 yield unit
332 def getunits(self):
333 """Return a list of all units in this store."""
334 return [unit for unit in self.unit_iter()]
336 def addunit(self, unit):
337 """Appends the given unit to the object's list of units.
339 This method should always be used rather than trying to modify the
340 list manually.
342 @type unit: L{TranslationUnit}
343 @param unit: The unit that will be added.
347 self.units.append(unit)
349 def addsourceunit(self, source):
350 """Adds and returns a new unit with the given source string.
352 @rtype: L{TranslationUnit}
356 unit = self.UnitClass(source)
357 self.addunit(unit)
358 return unit
360 def findunit(self, source):
361 """Finds the unit with the given source string.
363 @rtype: L{TranslationUnit} or None
367 if len(getattr(self, "sourceindex", [])):
368 if source in self.sourceindex:
369 return self.sourceindex[source]
370 else:
371 for unit in self.units:
372 if unit.source == source:
373 return unit
374 return None
376 def translate(self, source):
377 """Returns the translated string for a given source string.
379 @rtype: String or None
383 unit = self.findunit(source)
384 if unit and unit.target:
385 return unit.target
386 else:
387 return None
389 def makeindex(self):
390 """Indexes the items in this store. At least .sourceindex should be usefull."""
392 self.locationindex = {}
393 self.sourceindex = {}
394 for unit in self.units:
395 # Do we need to test if unit.source exists?
396 self.sourceindex[unit.source] = unit
397 if unit.hasplural():
398 for nounform in unit.source.strings[1:]:
399 self.sourceindex[nounform] = unit
400 for location in unit.getlocations():
401 if location in self.locationindex:
402 # if sources aren't unique, don't use them
403 self.locationindex[location] = None
404 else:
405 self.locationindex[location] = unit
407 def __str__(self):
408 """Converts to a string representation that can be parsed back using L{parsestring()}."""
410 # We can't pickle fileobj if it is there, so let's hide it for a while.
411 fileobj = getattr(self, "fileobj", None)
412 self.fileobj = None
413 dump = pickle.dumps(self)
414 self.fileobj = fileobj
415 return dump
417 def isempty(self):
418 """Returns True if the object doesn't contain any translation units."""
420 if len(self.units) == 0:
421 return True
422 for unit in self.units:
423 if not (unit.isblank() or unit.isheader()):
424 return False
425 return True
427 def _assignname(self):
428 """Tries to work out what the name of the filesystem file is and
429 assigns it to .filename."""
430 fileobj = getattr(self, "fileobj", None)
431 if fileobj:
432 filename = getattr(fileobj, "name", getattr(fileobj, "filename", None))
433 if filename:
434 self.filename = filename
436 def parsestring(cls, storestring):
437 """Converts the string representation back to an object."""
438 newstore = cls()
439 if storestring:
440 newstore.parse(storestring)
441 return newstore
442 parsestring = classmethod(parsestring)
444 def parse(self, data):
445 """parser to process the given source string"""
446 self.units = pickle.loads(data).units
448 def savefile(self, storefile):
449 """Writes the string representation to the given file (or filename)."""
450 if isinstance(storefile, basestring):
451 storefile = open(storefile, "w")
452 self.fileobj = storefile
453 self._assignname()
454 storestring = str(self)
455 storefile.write(storestring)
456 storefile.close()
458 def save(self):
459 """Save to the file that data was originally read from, if available."""
460 fileobj = getattr(self, "fileobj", None)
461 if not fileobj:
462 filename = getattr(self, "filename", None)
463 if filename:
464 fileobj = file(filename, "w")
465 else:
466 fileobj.close()
467 filename = getattr(fileobj, "name", getattr(fileobj, "filename", None))
468 if not filename:
469 raise ValueError("No file or filename to save to")
470 fileobj = fileobj.__class__(filename, "w")
471 self.savefile(fileobj)
473 def parsefile(cls, storefile):
474 """Reads the given file (or opens the given filename) and parses back to an object."""
476 if isinstance(storefile, basestring):
477 storefile = open(storefile, "r")
478 mode = getattr(storefile, "mode", "r")
479 #For some reason GzipFile returns 1, so we have to test for that here
480 if mode == 1 or "r" in mode:
481 storestring = storefile.read()
482 storefile.close()
483 else:
484 storestring = ""
485 newstore = cls.parsestring(storestring)
486 newstore.fileobj = storefile
487 newstore._assignname()
488 return newstore
489 parsefile = classmethod(parsefile)