#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright 2004-2006 Zuza Software Foundation # # This file is part of translate. # # translate is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # translate is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with translate; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Handles converting of files between formats (used by translate.convert tools)""" import sys import os.path from translate.misc import optrecurse # don't import optparse ourselves, get the version from optrecurse optparse = optrecurse.optparse from translate import __version__ try: from cStringIO import StringIO except ImportError: from StringIO import StringIO class ConvertOptionParser(optrecurse.RecursiveOptionParser, object): """a specialized Option Parser for convertor tools...""" def __init__(self, formats, usetemplates=False, usepots=False, allowmissingtemplate=False, description=None): """construct the specialized Option Parser""" optrecurse.RecursiveOptionParser.__init__(self, formats, usetemplates, allowmissingtemplate=allowmissingtemplate, description=description) self.usepots = usepots self.setpotoption() self.set_usage() def add_fuzzy_option(self, default=False): """adds an option to include / exclude fuzzy translations""" fuzzyhelp = "use translations marked fuzzy" nofuzzyhelp = "don't use translations marked fuzzy" if default: fuzzyhelp += " (default)" else: nofuzzyhelp += " (default)" self.add_option("", "--fuzzy", dest="includefuzzy", action="store_true", default=default, help=fuzzyhelp) self.add_option("", "--nofuzzy", dest="includefuzzy", action="store_false", default=default, help=nofuzzyhelp) self.passthrough.append("includefuzzy") def add_duplicates_option(self, default="merge"): """adds an option to say what to do with duplicate strings""" self.add_option("", "--duplicates", dest="duplicatestyle", default=default, type="choice", choices=["msgid_comment", "msgctxt", "merge", "keep", "msgid_comment_all"], help="what to do with duplicate strings (identical source text): merge (default), msgid_comment, msgctxt, keep, msgid_comment_all", metavar="DUPLICATESTYLE") self.passthrough.append("duplicatestyle") def add_multifile_option(self, default="single"): """adds an option to say how to split the po/pot files""" self.add_option("", "--multifile", dest="multifilestyle", default="single", type="choice", choices=["single", "toplevel", "onefile"], help="how to split po/pot files (single, toplevel or onefile)", metavar="MULTIFILESTYLE") self.passthrough.append("multifilestyle") def potifyformat(self, fileformat): """converts a .po to a .pot where required""" if fileformat is None: return fileformat elif fileformat == "po": return "pot" elif fileformat.endswith(os.extsep + "po"): return fileformat + "t" else: return fileformat def getformathelp(self, formats): """make a nice help string for describing formats...""" # include implicit pot options... helpformats = [] for fileformat in formats: helpformats.append(fileformat) potformat = self.potifyformat(fileformat) if potformat != fileformat: helpformats.append(potformat) return super(ConvertOptionParser, self).getformathelp(helpformats) def filterinputformats(self, options): """filters input formats, processing relevant switches in options""" if self.usepots and options.pot: return [self.potifyformat(inputformat) for inputformat in self.inputformats] else: return self.inputformats def filteroutputoptions(self, options): """filters output options, processing relevant switches in options""" if self.usepots and options.pot: outputoptions = {} for (inputformat, templateformat), (outputformat, convertor) in self.outputoptions.iteritems(): inputformat = self.potifyformat(inputformat) templateformat = self.potifyformat(templateformat) outputformat = self.potifyformat(outputformat) outputoptions[(inputformat, templateformat)] = (outputformat, convertor) return outputoptions else: return self.outputoptions def setpotoption(self): """sets the -P/--pot option depending on input/output formats etc""" if self.usepots: potoption = optparse.Option("-P", "--pot", \ action="store_true", dest="pot", default=False, \ help="output PO Templates (.pot) rather than PO files (.po)") self.define_option(potoption) def verifyoptions(self, options): """verifies that the options are valid (required options are present, etc)""" pass def run(self, argv=None): """parses the command line options and runs the conversion""" (options, args) = self.parse_args(argv) options.inputformats = self.filterinputformats(options) options.outputoptions = self.filteroutputoptions(options) self.usepsyco(options) self.verifyoptions(options) self.recursiveprocess(options) def copyinput(inputfile, outputfile, templatefile, **kwargs): """copies the input file to the output file""" outputfile.write(inputfile.read()) return True def copytemplate(inputfile, outputfile, templatefile, **kwargs): """copies the template file to the output file""" outputfile.write(templatefile.read()) return True class Replacer: """an object that knows how to replace strings in files""" def __init__(self, searchstring, replacestring): self.searchstring = searchstring self.replacestring = replacestring def doreplace(self, text): """actually replace the text""" if self.searchstring is not None and self.replacestring is not None: return text.replace(self.searchstring, self.replacestring) else: return text def searchreplaceinput(self, inputfile, outputfile, templatefile, **kwargs): """copies the input file to the output file, searching and replacing""" outputfile.write(self.doreplace(inputfile.read())) return True def searchreplacetemplate(self, inputfile, outputfile, templatefile, **kwargs): """copies the template file to the output file, searching and replacing""" outputfile.write(self.doreplace(templatefile.read())) return True # archive files need to know how to: # - openarchive: creates an archive object for the archivefilename # * requires a constructor that takes the filename # - iterarchivefile: iterate through the names in the archivefile # * requires the default iterator to do this # - archivefileexists: check if a given pathname exists inside the archivefile # * uses the in operator - requires __contains__ (or will use __iter__ by default) # - openarchiveinputfile: returns an open input file from the archive, given the path # * requires an archivefile.openinputfile method that takes the pathname # - openarchiveoutputfile: returns an open output file from the archive, given the path # * requires an archivefile.openoutputfile method that takes the pathname class ArchiveConvertOptionParser(ConvertOptionParser): """ConvertOptionParser that can handle recursing into single archive files. archiveformats maps extension to class. if the extension doesn't matter, it can be None. if the extension is only valid for input/output/template, it can be given as (extension, filepurpose)""" def __init__(self, formats, usetemplates=False, usepots=False, description=None, archiveformats=None): if archiveformats is None: self.archiveformats = {} else: self.archiveformats = archiveformats self.archiveoptions = {} ConvertOptionParser.__init__(self, formats, usetemplates, usepots, description=description) def setarchiveoptions(self, **kwargs): """allows setting options that will always be passed to openarchive""" self.archiveoptions = kwargs def isrecursive(self, fileoption, filepurpose='input'): """checks if fileoption is a recursive file""" if self.isarchive(fileoption, filepurpose): return True return super(ArchiveConvertOptionParser, self).isrecursive(fileoption, filepurpose) def isarchive(self, fileoption, filepurpose='input'): """returns whether the file option is an archive file""" if not isinstance(fileoption, (str, unicode)): return False mustexist = (filepurpose != 'output') if mustexist and not os.path.isfile(fileoption): return False fileext = self.splitext(fileoption)[1] # if None is in the archive formats, then treat all non-directory inputs as archives return self.getarchiveclass(fileext, filepurpose, os.path.isdir(fileoption)) is not None def getarchiveclass(self, fileext, filepurpose, isdir=False): """returns the archiveclass for the given fileext and filepurpose""" archiveclass = self.archiveformats.get(fileext, None) if archiveclass is not None: return archiveclass archiveclass = self.archiveformats.get((fileext, filepurpose), None) if archiveclass is not None: return archiveclass if not isdir: archiveclass = self.archiveformats.get(None, None) if archiveclass is not None: return archiveclass archiveclass = self.archiveformats.get((None, filepurpose), None) if archiveclass is not None: return archiveclass return None def openarchive(self, archivefilename, filepurpose, **kwargs): """creates an archive object for the given file""" archiveext = self.splitext(archivefilename)[1] archiveclass = self.getarchiveclass(archiveext, filepurpose, os.path.isdir(archivefilename)) archiveoptions = self.archiveoptions.copy() archiveoptions.update(kwargs) return archiveclass(archivefilename, **archiveoptions) def recurseinputfiles(self, options): """recurse through archive file / directories and return files to be converted""" if self.isarchive(options.input, 'input'): options.inputarchive = self.openarchive(options.input, 'input') return self.recursearchivefiles(options) else: return super(ArchiveConvertOptionParser, self).recurseinputfiles(options) def recursearchivefiles(self, options): """recurse through archive files and convert files""" inputfiles = [] for inputpath in options.inputarchive: if self.isexcluded(options, inputpath): continue top, name = os.path.split(inputpath) if not self.isvalidinputname(options, name): continue inputfiles.append(inputpath) return inputfiles def openinputfile(self, options, fullinputpath): """opens the input file""" if self.isarchive(options.input, 'input'): return options.inputarchive.openinputfile(fullinputpath) else: return super(ArchiveConvertOptionParser, self).openinputfile(options, fullinputpath) def getfullinputpath(self, options, inputpath): """gets the absolute path to an input file""" if self.isarchive(options.input, 'input'): return inputpath else: return os.path.join(options.input, inputpath) def opentemplatefile(self, options, fulltemplatepath): """opens the template file (if required)""" if fulltemplatepath is not None: if options.recursivetemplate and self.isarchive(options.template, 'template'): # TODO: deal with different names in input/template archives if fulltemplatepath in options.templatearchive: return options.templatearchive.openinputfile(fulltemplatepath) else: self.warning("missing template file %s" % fulltemplatepath) return super(ArchiveConvertOptionParser, self).opentemplatefile(options, fulltemplatepath) def getfulltemplatepath(self, options, templatepath): """gets the absolute path to a template file""" if templatepath is not None and self.usetemplates and options.template: if self.isarchive(options.template, 'template'): return templatepath elif not options.recursivetemplate: return templatepath else: return os.path.join(options.template, templatepath) else: return None def templateexists(self, options, templatepath): """returns whether the given template exists...""" if templatepath is not None: if self.isarchive(options.template, 'template'): # TODO: deal with different names in input/template archives return templatepath in options.templatearchive return super(ArchiveConvertOptionParser, self).templateexists(options, templatepath) def getfulloutputpath(self, options, outputpath): """gets the absolute path to an output file""" if self.isarchive(options.output, 'output'): return outputpath elif options.recursiveoutput and options.output: return os.path.join(options.output, outputpath) else: return outputpath def checkoutputsubdir(self, options, subdir): """checks to see if subdir under options.output needs to be created, creates if neccessary""" if not self.isarchive(options.output, 'output'): super(ArchiveConvertOptionParser, self).checkoutputsubdir(options, subdir) def openoutputfile(self, options, fulloutputpath): """opens the output file""" if self.isarchive(options.output, 'output'): outputstream = options.outputarchive.openoutputfile(fulloutputpath) if outputstream is None: self.warning("Could not find where to put %s in output archive; writing to tmp" % fulloutputpath) return StringIO() return outputstream else: return super(ArchiveConvertOptionParser, self).openoutputfile(options, fulloutputpath) def inittemplatearchive(self, options): """opens the templatearchive if not already open""" if not self.usetemplates: return if options.template and self.isarchive(options.template, 'template') and not hasattr(options, "templatearchive"): options.templatearchive = self.openarchive(options.template, 'template') def initoutputarchive(self, options): """creates an outputarchive if required""" if options.output and self.isarchive(options.output, 'output'): options.outputarchive = self.openarchive(options.output, 'output', mode="w") def recursiveprocess(self, options): """recurse through directories and convert files""" if hasattr(options, "multifilestyle"): self.setarchiveoptions(multifilestyle=options.multifilestyle) for filetype in ("input", "output", "template"): allowoption = "allowrecursive%s" % filetype if options.multifilestyle == "onefile" and getattr(options, allowoption, True): setattr(options, allowoption, False) self.inittemplatearchive(options) self.initoutputarchive(options) return super(ArchiveConvertOptionParser, self).recursiveprocess(options) def processfile(self, fileprocessor, options, fullinputpath, fulloutputpath, fulltemplatepath): """run an invidividual conversion""" if self.isarchive(options.output, 'output'): inputfile = self.openinputfile(options, fullinputpath) # TODO: handle writing back to same archive as input/template templatefile = self.opentemplatefile(options, fulltemplatepath) outputfile = self.openoutputfile(options, fulloutputpath) passthroughoptions = self.getpassthroughoptions(options) if fileprocessor(inputfile, outputfile, templatefile, **passthroughoptions): if not outputfile.isatty(): outputfile.close() return True else: if fulloutputpath and os.path.isfile(fulloutputpath): outputfile.close() os.unlink(fulloutputpath) return False else: return super(ArchiveConvertOptionParser, self).processfile(fileprocessor, options, fullinputpath, fulloutputpath, fulltemplatepath) def main(argv=None): parser = ArchiveConvertOptionParser({}, description=__doc__) parser.run(argv)