cmlenz@62: #!/usr/bin/env python cmlenz@1: # -*- coding: utf-8 -*- cmlenz@1: # jruigrok@530: # Copyright (C) 2007-2011 Edgewall Software cmlenz@1: # All rights reserved. cmlenz@1: # cmlenz@1: # This software is licensed as described in the file COPYING, which cmlenz@1: # you should have received as part of this distribution. The terms cmlenz@1: # are also available at http://babel.edgewall.org/wiki/License. cmlenz@1: # cmlenz@1: # This software consists of voluntary contributions made by many cmlenz@1: # individuals. For the exact contribution history, see the revision cmlenz@1: # history and logs, available at http://babel.edgewall.org/log/. cmlenz@1: cmlenz@1: """Frontends for the message extraction functionality.""" cmlenz@1: cmlenz@48: from ConfigParser import RawConfigParser cmlenz@104: from datetime import datetime cmlenz@1: from distutils import log cmlenz@1: from distutils.cmd import Command cmlenz@49: from distutils.errors import DistutilsOptionError, DistutilsSetupError jruigrok@298: from locale import getpreferredencoding cmlenz@232: import logging cmlenz@1: from optparse import OptionParser cmlenz@1: import os cmlenz@197: import shutil cmlenz@49: from StringIO import StringIO cmlenz@1: import sys cmlenz@197: import tempfile cmlenz@1: cmlenz@1: from babel import __version__ as VERSION cmlenz@185: from babel import Locale, localedata palgarvio@51: from babel.core import UnknownLocaleError cmlenz@56: from babel.messages.catalog import Catalog cmlenz@54: from babel.messages.extract import extract_from_dir, DEFAULT_KEYWORDS, \ cmlenz@54: DEFAULT_MAPPING cmlenz@160: from babel.messages.mofile import write_mo cmlenz@104: from babel.messages.pofile import read_po, write_po cmlenz@104: from babel.util import odict, LOCALTZ cmlenz@1: cmlenz@178: __all__ = ['CommandLineInterface', 'compile_catalog', 'extract_messages', cmlenz@234: 'init_catalog', 'check_message_extractors', 'update_catalog'] cmlenz@1: __docformat__ = 'restructuredtext en' cmlenz@1: cmlenz@1: cmlenz@160: class compile_catalog(Command): cmlenz@160: """Catalog compilation command for use in ``setup.py`` scripts. cmlenz@160: cmlenz@160: If correctly installed, this command is available to Setuptools-using cmlenz@160: setup scripts automatically. For projects using plain old ``distutils``, cmlenz@160: the command needs to be registered explicitly in ``setup.py``:: cmlenz@160: cmlenz@160: from babel.messages.frontend import compile_catalog cmlenz@160: cmlenz@160: setup( cmlenz@160: ... palgarvio@169: cmdclass = {'compile_catalog': compile_catalog} cmlenz@160: ) cmlenz@160: cmlenz@234: :since: version 0.9 cmlenz@160: :see: `Integrating new distutils commands `_ cmlenz@160: :see: `setuptools `_ cmlenz@160: """ cmlenz@160: cmlenz@181: description = 'compile message catalogs to binary MO files' cmlenz@160: user_options = [ cmlenz@160: ('domain=', 'D', cmlenz@160: "domain of PO file (default 'messages')"), cmlenz@160: ('directory=', 'd', cmlenz@160: 'path to base directory containing the catalogs'), cmlenz@160: ('input-file=', 'i', cmlenz@160: 'name of the input file'), cmlenz@160: ('output-file=', 'o', cmlenz@160: "name of the output file (default " cmlenz@160: "'//LC_MESSAGES/.po')"), cmlenz@160: ('locale=', 'l', cmlenz@160: 'locale of the catalog to compile'), cmlenz@160: ('use-fuzzy', 'f', cmlenz@160: 'also include fuzzy translations'), palgarvio@207: ('statistics', None, palgarvio@207: 'print statistics about translations') cmlenz@160: ] palgarvio@207: boolean_options = ['use-fuzzy', 'statistics'] cmlenz@160: cmlenz@160: def initialize_options(self): cmlenz@160: self.domain = 'messages' cmlenz@160: self.directory = None cmlenz@160: self.input_file = None cmlenz@160: self.output_file = None cmlenz@160: self.locale = None cmlenz@160: self.use_fuzzy = False palgarvio@207: self.statistics = False cmlenz@160: cmlenz@160: def finalize_options(self): cmlenz@177: if not self.input_file and not self.directory: cmlenz@177: raise DistutilsOptionError('you must specify either the input file ' cmlenz@177: 'or the base directory') cmlenz@177: if not self.output_file and not self.directory: cmlenz@177: raise DistutilsOptionError('you must specify either the input file ' cmlenz@177: 'or the base directory') cmlenz@160: cmlenz@160: def run(self): cmlenz@177: po_files = [] cmlenz@177: mo_files = [] palgarvio@175: cmlenz@177: if not self.input_file: cmlenz@177: if self.locale: cmlenz@331: po_files.append((self.locale, cmlenz@331: os.path.join(self.directory, self.locale, cmlenz@331: 'LC_MESSAGES', cmlenz@331: self.domain + '.po'))) cmlenz@177: mo_files.append(os.path.join(self.directory, self.locale, cmlenz@177: 'LC_MESSAGES', cmlenz@177: self.domain + '.mo')) cmlenz@177: else: cmlenz@177: for locale in os.listdir(self.directory): cmlenz@177: po_file = os.path.join(self.directory, locale, cmlenz@177: 'LC_MESSAGES', self.domain + '.po') cmlenz@177: if os.path.exists(po_file): cmlenz@331: po_files.append((locale, po_file)) cmlenz@177: mo_files.append(os.path.join(self.directory, locale, cmlenz@177: 'LC_MESSAGES', cmlenz@177: self.domain + '.mo')) palgarvio@170: else: cmlenz@331: po_files.append((self.locale, self.input_file)) cmlenz@177: if self.output_file: cmlenz@177: mo_files.append(self.output_file) cmlenz@177: else: cmlenz@288: mo_files.append(os.path.join(self.directory, self.locale, cmlenz@177: 'LC_MESSAGES', cmlenz@177: self.domain + '.mo')) cmlenz@177: palgarvio@208: if not po_files: palgarvio@208: raise DistutilsOptionError('no message catalogs found') palgarvio@208: cmlenz@331: for idx, (locale, po_file) in enumerate(po_files): cmlenz@177: mo_file = mo_files[idx] cmlenz@177: infile = open(po_file, 'r') palgarvio@170: try: cmlenz@331: catalog = read_po(infile, locale) palgarvio@170: finally: palgarvio@170: infile.close() palgarvio@175: palgarvio@207: if self.statistics: palgarvio@207: translated = 0 palgarvio@207: for message in list(catalog)[1:]: palgarvio@207: if message.string: palgarvio@207: translated +=1 cmlenz@317: percentage = 0 cmlenz@317: if len(catalog): cmlenz@317: percentage = translated * 100 // len(catalog) cmlenz@231: log.info('%d of %d messages (%d%%) translated in %r', cmlenz@317: translated, len(catalog), percentage, po_file) palgarvio@207: cmlenz@177: if catalog.fuzzy and not self.use_fuzzy: cmlenz@231: log.warn('catalog %r is marked as fuzzy, skipping', po_file) cmlenz@177: continue cmlenz@177: cmlenz@220: for message, errors in catalog.check(): cmlenz@220: for error in errors: cmlenz@231: log.error('error: %s:%d: %s', po_file, message.lineno, cmlenz@231: error) cmlenz@220: cmlenz@231: log.info('compiling catalog %r to %r', po_file, mo_file) cmlenz@177: cmlenz@279: outfile = open(mo_file, 'wb') palgarvio@170: try: palgarvio@170: write_mo(outfile, catalog, use_fuzzy=self.use_fuzzy) palgarvio@170: finally: palgarvio@170: outfile.close() cmlenz@160: cmlenz@160: cmlenz@1: class extract_messages(Command): cmlenz@1: """Message extraction command for use in ``setup.py`` scripts. cmlenz@52: cmlenz@1: If correctly installed, this command is available to Setuptools-using cmlenz@1: setup scripts automatically. For projects using plain old ``distutils``, cmlenz@1: the command needs to be registered explicitly in ``setup.py``:: cmlenz@52: cmlenz@54: from babel.messages.frontend import extract_messages cmlenz@52: cmlenz@1: setup( cmlenz@1: ... cmlenz@1: cmdclass = {'extract_messages': extract_messages} cmlenz@1: ) cmlenz@52: cmlenz@1: :see: `Integrating new distutils commands `_ cmlenz@1: :see: `setuptools `_ cmlenz@1: """ cmlenz@1: cmlenz@1: description = 'extract localizable strings from the project code' cmlenz@1: user_options = [ cmlenz@1: ('charset=', None, cmlenz@1: 'charset to use in the output file'), cmlenz@1: ('keywords=', 'k', palgarvio@10: 'space-separated list of keywords to look for in addition to the ' cmlenz@1: 'defaults'), palgarvio@10: ('no-default-keywords', None, cmlenz@12: 'do not include the default keywords'), cmlenz@47: ('mapping-file=', 'F', cmlenz@47: 'path to the mapping configuration file'), cmlenz@1: ('no-location', None, cmlenz@1: 'do not include location comments with filename and line number'), cmlenz@1: ('omit-header', None, cmlenz@1: 'do not include msgid "" entry in header'), cmlenz@5: ('output-file=', 'o', cmlenz@1: 'name of the output file'), palgarvio@23: ('width=', 'w', cmlenz@24: 'set output line width (default 76)'), palgarvio@23: ('no-wrap', None, cmlenz@24: 'do not break long message lines, longer than the output line width, ' cmlenz@58: 'into several lines'), palgarvio@71: ('sort-output', None, palgarvio@71: 'generate sorted output (default False)'), palgarvio@71: ('sort-by-file', None, palgarvio@71: 'sort output by file location (default False)'), palgarvio@78: ('msgid-bugs-address=', None, palgarvio@78: 'set report address for msgid'), palgarvio@79: ('copyright-holder=', None, palgarvio@79: 'set copyright holder in output'), palgarvio@80: ('add-comments=', 'c', palgarvio@80: 'place comment block with TAG (or those preceding keyword lines) in ' palgarvio@80: 'output file. Seperate multiple TAGs with commas(,)'), aronacher@338: ('strip-comments', None, aronacher@338: 'strip the comment TAGs from the comments.'), palgarvio@59: ('input-dirs=', None, cmlenz@57: 'directories that should be scanned for messages'), cmlenz@1: ] palgarvio@23: boolean_options = [ palgarvio@71: 'no-default-keywords', 'no-location', 'omit-header', 'no-wrap', aronacher@338: 'sort-output', 'sort-by-file', 'strip-comments' palgarvio@23: ] cmlenz@1: cmlenz@1: def initialize_options(self): cmlenz@1: self.charset = 'utf-8' cmlenz@117: self.keywords = '' cmlenz@117: self._keywords = DEFAULT_KEYWORDS.copy() cmlenz@12: self.no_default_keywords = False cmlenz@47: self.mapping_file = None cmlenz@1: self.no_location = False cmlenz@1: self.omit_header = False cmlenz@1: self.output_file = None cmlenz@57: self.input_dirs = None palgarvio@423: self.width = None cmlenz@47: self.no_wrap = False palgarvio@71: self.sort_output = False palgarvio@71: self.sort_by_file = False palgarvio@78: self.msgid_bugs_address = None palgarvio@79: self.copyright_holder = None palgarvio@80: self.add_comments = None cmlenz@95: self._add_comments = [] aronacher@338: self.strip_comments = False cmlenz@1: cmlenz@1: def finalize_options(self): palgarvio@23: if self.no_default_keywords and not self.keywords: cmlenz@24: raise DistutilsOptionError('you must specify new keywords if you ' cmlenz@24: 'disable the default ones') cmlenz@12: if self.no_default_keywords: palgarvio@23: self._keywords = {} cmlenz@117: if self.keywords: palgarvio@23: self._keywords.update(parse_keywords(self.keywords.split())) cmlenz@24: cmlenz@117: if not self.output_file: cmlenz@117: raise DistutilsOptionError('no output file specified') palgarvio@23: if self.no_wrap and self.width: palgarvio@71: raise DistutilsOptionError("'--no-wrap' and '--width' are mutually " cmlenz@24: "exclusive") palgarvio@423: if not self.no_wrap and not self.width: palgarvio@423: self.width = 76 palgarvio@423: elif self.width is not None: palgarvio@23: self.width = int(self.width) cmlenz@95: palgarvio@71: if self.sort_output and self.sort_by_file: palgarvio@71: raise DistutilsOptionError("'--sort-output' and '--sort-by-file' " palgarvio@71: "are mutually exclusive") cmlenz@1: cmlenz@57: if not self.input_dirs: cmlenz@177: self.input_dirs = dict.fromkeys([k.split('.',1)[0] cmlenz@177: for k in self.distribution.packages cmlenz@57: ]).keys() cmlenz@95: palgarvio@80: if self.add_comments: palgarvio@80: self._add_comments = self.add_comments.split(',') cmlenz@57: cmlenz@1: def run(self): cmlenz@62: mappings = self._get_mappings() cmlenz@1: outfile = open(self.output_file, 'w') cmlenz@1: try: cmlenz@102: catalog = Catalog(project=self.distribution.get_name(), cmlenz@102: version=self.distribution.get_version(), cmlenz@102: msgid_bugs_address=self.msgid_bugs_address, palgarvio@105: copyright_holder=self.copyright_holder, cmlenz@102: charset=self.charset) cmlenz@102: cmlenz@62: for dirname, (method_map, options_map) in mappings.items(): cmlenz@57: def callback(filename, method, options): cmlenz@57: if method == 'ignore': cmlenz@57: return cmlenz@57: filepath = os.path.normpath(os.path.join(dirname, filename)) cmlenz@57: optstr = '' cmlenz@57: if options: cmlenz@57: optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for cmlenz@57: k, v in options.items()]) cmlenz@232: log.info('extracting messages from %s%s', filepath, optstr) cmlenz@47: cmlenz@57: extracted = extract_from_dir(dirname, method_map, options_map, cmlenz@117: keywords=self._keywords, cmlenz@84: comment_tags=self._add_comments, aronacher@338: callback=callback, aronacher@338: strip_comment_tags= aronacher@338: self.strip_comments) palgarvio@80: for filename, lineno, message, comments in extracted: cmlenz@57: filepath = os.path.normpath(os.path.join(dirname, filename)) palgarvio@80: catalog.add(message, None, [(filepath, lineno)], palgarvio@105: auto_comments=comments) cmlenz@24: palgarvio@51: log.info('writing PO template file to %s' % self.output_file) cmlenz@104: write_po(outfile, catalog, width=self.width, cmlenz@104: no_location=self.no_location, cmlenz@104: omit_header=self.omit_header, cmlenz@104: sort_output=self.sort_output, palgarvio@105: sort_by_file=self.sort_by_file) cmlenz@1: finally: cmlenz@1: outfile.close() cmlenz@1: cmlenz@62: def _get_mappings(self): cmlenz@62: mappings = {} cmlenz@62: cmlenz@62: if self.mapping_file: cmlenz@62: fileobj = open(self.mapping_file, 'U') cmlenz@62: try: cmlenz@62: method_map, options_map = parse_mapping(fileobj) cmlenz@62: for dirname in self.input_dirs: cmlenz@62: mappings[dirname] = method_map, options_map cmlenz@62: finally: cmlenz@62: fileobj.close() cmlenz@62: cmlenz@123: elif getattr(self.distribution, 'message_extractors', None): cmlenz@62: message_extractors = self.distribution.message_extractors cmlenz@62: for dirname, mapping in message_extractors.items(): cmlenz@62: if isinstance(mapping, basestring): cmlenz@62: method_map, options_map = parse_mapping(StringIO(mapping)) cmlenz@62: else: cmlenz@62: method_map, options_map = [], {} cmlenz@62: for pattern, method, options in mapping: cmlenz@62: method_map.append((pattern, method)) cmlenz@62: options_map[pattern] = options or {} cmlenz@62: mappings[dirname] = method_map, options_map cmlenz@62: cmlenz@62: else: cmlenz@62: for dirname in self.input_dirs: cmlenz@62: mappings[dirname] = DEFAULT_MAPPING, {} cmlenz@62: cmlenz@62: return mappings cmlenz@62: cmlenz@1: cmlenz@52: def check_message_extractors(dist, name, value): cmlenz@52: """Validate the ``message_extractors`` keyword argument to ``setup()``. cmlenz@47: cmlenz@52: :param dist: the distutils/setuptools ``Distribution`` object cmlenz@52: :param name: the name of the keyword argument (should always be cmlenz@52: "message_extractors") cmlenz@52: :param value: the value of the keyword argument cmlenz@52: :raise `DistutilsSetupError`: if the value is not valid cmlenz@52: :see: `Adding setup() arguments cmlenz@52: `_ cmlenz@52: """ cmlenz@52: assert name == 'message_extractors' cmlenz@62: if not isinstance(value, dict): cmlenz@62: raise DistutilsSetupError('the value of the "message_extractors" ' cmlenz@62: 'parameter must be a dictionary') cmlenz@1: cmlenz@52: cmlenz@181: class init_catalog(Command): cmlenz@181: """New catalog initialization command for use in ``setup.py`` scripts. palgarvio@51: palgarvio@51: If correctly installed, this command is available to Setuptools-using palgarvio@51: setup scripts automatically. For projects using plain old ``distutils``, palgarvio@51: the command needs to be registered explicitly in ``setup.py``:: palgarvio@51: cmlenz@181: from babel.messages.frontend import init_catalog palgarvio@51: palgarvio@51: setup( palgarvio@51: ... cmlenz@181: cmdclass = {'init_catalog': init_catalog} palgarvio@51: ) palgarvio@51: palgarvio@51: :see: `Integrating new distutils commands `_ palgarvio@51: :see: `setuptools `_ palgarvio@51: """ palgarvio@51: cmlenz@181: description = 'create a new catalog based on a POT file' palgarvio@51: user_options = [ palgarvio@55: ('domain=', 'D', cmlenz@104: "domain of PO file (default 'messages')"), palgarvio@51: ('input-file=', 'i', palgarvio@51: 'name of the input file'), palgarvio@51: ('output-dir=', 'd', palgarvio@51: 'path to output directory'), palgarvio@51: ('output-file=', 'o', palgarvio@51: "name of the output file (default " palgarvio@87: "'//LC_MESSAGES/.po')"), palgarvio@51: ('locale=', 'l', palgarvio@51: 'locale for the new localized catalog'), palgarvio@51: ] palgarvio@51: palgarvio@51: def initialize_options(self): palgarvio@51: self.output_dir = None palgarvio@51: self.output_file = None palgarvio@51: self.input_file = None palgarvio@51: self.locale = None cmlenz@104: self.domain = 'messages' palgarvio@51: palgarvio@51: def finalize_options(self): palgarvio@51: if not self.input_file: palgarvio@51: raise DistutilsOptionError('you must specify the input file') palgarvio@87: palgarvio@51: if not self.locale: palgarvio@51: raise DistutilsOptionError('you must provide a locale for the ' palgarvio@87: 'new catalog') cmlenz@104: try: cmlenz@104: self._locale = Locale.parse(self.locale) cmlenz@104: except UnknownLocaleError, e: cmlenz@104: raise DistutilsOptionError(e) cmlenz@52: palgarvio@51: if not self.output_file and not self.output_dir: palgarvio@51: raise DistutilsOptionError('you must specify the output directory') cmlenz@104: if not self.output_file: cmlenz@104: self.output_file = os.path.join(self.output_dir, self.locale, cmlenz@104: 'LC_MESSAGES', self.domain + '.po') palgarvio@87: palgarvio@87: if not os.path.exists(os.path.dirname(self.output_file)): palgarvio@87: os.makedirs(os.path.dirname(self.output_file)) palgarvio@51: palgarvio@51: def run(self): cmlenz@90: log.info('creating catalog %r based on %r', self.output_file, palgarvio@55: self.input_file) cmlenz@52: cmlenz@104: infile = open(self.input_file, 'r') cmlenz@104: try: palgarvio@370: # Although reading from the catalog template, read_po must be fed palgarvio@370: # the locale in order to correcly calculate plurals palgarvio@370: catalog = read_po(infile, locale=self.locale) cmlenz@104: finally: cmlenz@104: infile.close() palgarvio@87: cmlenz@104: catalog.locale = self._locale pjenvey@251: catalog.fuzzy = False cmlenz@104: cmlenz@104: outfile = open(self.output_file, 'w') cmlenz@104: try: cmlenz@104: write_po(outfile, catalog) cmlenz@104: finally: cmlenz@104: outfile.close() palgarvio@51: palgarvio@51: cmlenz@181: class update_catalog(Command): cmlenz@181: """Catalog merging command for use in ``setup.py`` scripts. cmlenz@181: cmlenz@181: If correctly installed, this command is available to Setuptools-using cmlenz@181: setup scripts automatically. For projects using plain old ``distutils``, cmlenz@181: the command needs to be registered explicitly in ``setup.py``:: cmlenz@181: cmlenz@181: from babel.messages.frontend import update_catalog cmlenz@181: cmlenz@181: setup( cmlenz@181: ... cmlenz@181: cmdclass = {'update_catalog': update_catalog} cmlenz@181: ) cmlenz@181: cmlenz@234: :since: version 0.9 cmlenz@181: :see: `Integrating new distutils commands `_ cmlenz@181: :see: `setuptools `_ cmlenz@181: """ cmlenz@181: cmlenz@181: description = 'update message catalogs from a POT file' cmlenz@181: user_options = [ cmlenz@181: ('domain=', 'D', cmlenz@181: "domain of PO file (default 'messages')"), cmlenz@181: ('input-file=', 'i', cmlenz@181: 'name of the input file'), cmlenz@181: ('output-dir=', 'd', cmlenz@181: 'path to base directory containing the catalogs'), cmlenz@181: ('output-file=', 'o', cmlenz@181: "name of the output file (default " cmlenz@181: "'//LC_MESSAGES/.po')"), cmlenz@181: ('locale=', 'l', cmlenz@181: 'locale of the catalog to compile'), cmlenz@191: ('ignore-obsolete=', None, palgarvio@200: 'whether to omit obsolete messages from the output'), palgarvio@200: ('no-fuzzy-matching', 'N', palgarvio@200: 'do not use fuzzy matching'), palgarvio@200: ('previous', None, palgarvio@200: 'keep previous msgids of translated messages') cmlenz@181: ] palgarvio@200: boolean_options = ['ignore_obsolete', 'no_fuzzy_matching', 'previous'] cmlenz@181: cmlenz@181: def initialize_options(self): cmlenz@181: self.domain = 'messages' cmlenz@181: self.input_file = None cmlenz@181: self.output_dir = None cmlenz@181: self.output_file = None cmlenz@181: self.locale = None cmlenz@191: self.ignore_obsolete = False palgarvio@200: self.no_fuzzy_matching = False palgarvio@200: self.previous = False cmlenz@181: cmlenz@181: def finalize_options(self): cmlenz@181: if not self.input_file: cmlenz@181: raise DistutilsOptionError('you must specify the input file') cmlenz@181: if not self.output_file and not self.output_dir: cmlenz@181: raise DistutilsOptionError('you must specify the output file or ' cmlenz@181: 'directory') cmlenz@196: if self.output_file and not self.locale: cmlenz@196: raise DistutilsOptionError('you must specify the locale') palgarvio@200: if self.no_fuzzy_matching and self.previous: palgarvio@200: self.previous = False cmlenz@181: cmlenz@181: def run(self): cmlenz@181: po_files = [] cmlenz@181: if not self.output_file: cmlenz@181: if self.locale: cmlenz@196: po_files.append((self.locale, cmlenz@196: os.path.join(self.output_dir, self.locale, cmlenz@196: 'LC_MESSAGES', cmlenz@196: self.domain + '.po'))) cmlenz@181: else: cmlenz@181: for locale in os.listdir(self.output_dir): cmlenz@181: po_file = os.path.join(self.output_dir, locale, cmlenz@181: 'LC_MESSAGES', cmlenz@181: self.domain + '.po') cmlenz@181: if os.path.exists(po_file): cmlenz@196: po_files.append((locale, po_file)) cmlenz@181: else: cmlenz@196: po_files.append((self.locale, self.output_file)) cmlenz@196: cmlenz@196: domain = self.domain cmlenz@196: if not domain: cmlenz@196: domain = os.path.splitext(os.path.basename(self.input_file))[0] cmlenz@181: cmlenz@181: infile = open(self.input_file, 'U') cmlenz@181: try: cmlenz@181: template = read_po(infile) cmlenz@181: finally: cmlenz@181: infile.close() cmlenz@181: palgarvio@208: if not po_files: palgarvio@208: raise DistutilsOptionError('no message catalogs found') palgarvio@208: cmlenz@196: for locale, filename in po_files: cmlenz@196: log.info('updating catalog %r based on %r', filename, cmlenz@181: self.input_file) cmlenz@196: infile = open(filename, 'U') cmlenz@181: try: cmlenz@196: catalog = read_po(infile, locale=locale, domain=domain) cmlenz@181: finally: cmlenz@181: infile.close() cmlenz@181: cmlenz@204: catalog.update(template, self.no_fuzzy_matching) cmlenz@181: cmlenz@197: tmpname = os.path.join(os.path.dirname(filename), palgarvio@200: tempfile.gettempprefix() + cmlenz@197: os.path.basename(filename)) cmlenz@197: tmpfile = open(tmpname, 'w') cmlenz@181: try: cmlenz@197: try: cmlenz@197: write_po(tmpfile, catalog, palgarvio@200: ignore_obsolete=self.ignore_obsolete, cmlenz@205: include_previous=self.previous) cmlenz@197: finally: cmlenz@197: tmpfile.close() cmlenz@197: except: cmlenz@197: os.remove(tmpname) cmlenz@197: raise cmlenz@197: cmlenz@197: try: cmlenz@197: os.rename(tmpname, filename) cmlenz@197: except OSError: cmlenz@197: # We're probably on Windows, which doesn't support atomic cmlenz@197: # renames, at least not through Python cmlenz@197: # If the error is in fact due to a permissions problem, that cmlenz@197: # same error is going to be raised from one of the following cmlenz@197: # operations cmlenz@197: os.remove(filename) cmlenz@197: shutil.copy(tmpname, filename) cmlenz@197: os.remove(tmpname) cmlenz@181: cmlenz@181: cmlenz@52: class CommandLineInterface(object): cmlenz@52: """Command-line interface. cmlenz@52: cmlenz@52: This class provides a simple command-line interface to the message cmlenz@52: extraction and PO file generation functionality. palgarvio@51: """ cmlenz@52: cmlenz@52: usage = '%%prog %s [options] %s' cmlenz@52: version = '%%prog %s' % VERSION cmlenz@161: commands = { cmlenz@177: 'compile': 'compile message catalogs to MO files', palgarvio@63: 'extract': 'extract messages from source files and generate a POT file', cmlenz@181: 'init': 'create new message catalogs from a POT file', cmlenz@181: 'update': 'update existing message catalogs from a POT file' palgarvio@63: } cmlenz@52: cmlenz@52: def run(self, argv=sys.argv): cmlenz@52: """Main entry point of the command-line interface. cmlenz@52: cmlenz@52: :param argv: list of arguments passed on the command-line cmlenz@52: """ cmlenz@127: self.parser = OptionParser(usage=self.usage % ('command', '[args]'), cmlenz@185: version=self.version) palgarvio@63: self.parser.disable_interspersed_args() palgarvio@63: self.parser.print_help = self._help cmlenz@185: self.parser.add_option('--list-locales', dest='list_locales', cmlenz@185: action='store_true', cmlenz@185: help="print all known locales and exit") cmlenz@232: self.parser.add_option('-v', '--verbose', action='store_const', cmlenz@232: dest='loglevel', const=logging.DEBUG, cmlenz@232: help='print as much as possible') cmlenz@232: self.parser.add_option('-q', '--quiet', action='store_const', cmlenz@232: dest='loglevel', const=logging.ERROR, cmlenz@232: help='print as little as possible') cmlenz@232: self.parser.set_defaults(list_locales=False, loglevel=logging.INFO) cmlenz@185: palgarvio@63: options, args = self.parser.parse_args(argv[1:]) cmlenz@185: fschwarz@523: self._configure_logging(options.loglevel) cmlenz@185: if options.list_locales: fschwarz@546: identifiers = localedata.locale_identifiers() cmlenz@185: longest = max([len(identifier) for identifier in identifiers]) pjenvey@440: identifiers.sort() cmlenz@210: format = u'%%-%ds %%s' % (longest + 1) pjenvey@440: for identifier in identifiers: cmlenz@185: locale = Locale.parse(identifier) cmlenz@267: output = format % (identifier, locale.english_name) jruigrok@298: print output.encode(sys.stdout.encoding or jruigrok@298: getpreferredencoding() or jruigrok@298: 'ascii', 'replace') cmlenz@185: return 0 cmlenz@185: cmlenz@52: if not args: palgarvio@424: self.parser.error('no valid command or option passed. ' palgarvio@424: 'Try the -h/--help option for more information.') cmlenz@52: cmlenz@52: cmdname = args[0] cmlenz@52: if cmdname not in self.commands: cmlenz@127: self.parser.error('unknown command "%s"' % cmdname) cmlenz@52: cmlenz@185: return getattr(self, cmdname)(args[1:]) cmlenz@52: fschwarz@523: def _configure_logging(self, loglevel): fschwarz@523: self.log = logging.getLogger('babel') fschwarz@523: self.log.setLevel(loglevel) fschwarz@523: # Don't add a new handler for every instance initialization (#227), this fschwarz@523: # would cause duplicated output when the CommandLineInterface as an fschwarz@523: # normal Python class. fschwarz@523: if self.log.handlers: fschwarz@523: handler = self.log.handlers[0] fschwarz@523: else: fschwarz@523: handler = logging.StreamHandler() fschwarz@523: self.log.addHandler(handler) fschwarz@523: handler.setLevel(loglevel) fschwarz@523: formatter = logging.Formatter('%(message)s') fschwarz@523: handler.setFormatter(formatter) fschwarz@523: palgarvio@63: def _help(self): palgarvio@63: print self.parser.format_help() cmlenz@127: print "commands:" palgarvio@63: longest = max([len(command) for command in self.commands]) cmlenz@185: format = " %%-%ds %%s" % max(8, longest + 1) cmlenz@161: commands = self.commands.items() cmlenz@161: commands.sort() cmlenz@161: for name, description in commands: cmlenz@161: print format % (name, description) palgarvio@87: cmlenz@160: def compile(self, argv): cmlenz@160: """Subcommand for compiling a message catalog to a MO file. cmlenz@160: cmlenz@160: :param argv: the command arguments cmlenz@234: :since: version 0.9 cmlenz@160: """ cmlenz@177: parser = OptionParser(usage=self.usage % ('compile', ''), cmlenz@177: description=self.commands['compile']) cmlenz@160: parser.add_option('--domain', '-D', dest='domain', cmlenz@160: help="domain of MO and PO files (default '%default')") cmlenz@160: parser.add_option('--directory', '-d', dest='directory', cmlenz@160: metavar='DIR', help='base directory of catalog files') cmlenz@177: parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', cmlenz@177: help='locale of the catalog') cmlenz@160: parser.add_option('--input-file', '-i', dest='input_file', cmlenz@160: metavar='FILE', help='name of the input file') cmlenz@160: parser.add_option('--output-file', '-o', dest='output_file', cmlenz@160: metavar='FILE', cmlenz@160: help="name of the output file (default " cmlenz@160: "'//LC_MESSAGES/" cmlenz@160: ".mo')") cmlenz@160: parser.add_option('--use-fuzzy', '-f', dest='use_fuzzy', cmlenz@160: action='store_true', cmlenz@160: help='also include fuzzy translations (default ' palgarvio@207: '%default)') palgarvio@207: parser.add_option('--statistics', dest='statistics', palgarvio@207: action='store_true', palgarvio@207: help='print statistics about translations') cmlenz@160: palgarvio@170: parser.set_defaults(domain='messages', use_fuzzy=False, palgarvio@207: compile_all=False, statistics=False) palgarvio@170: options, args = parser.parse_args(argv) cmlenz@160: cmlenz@177: po_files = [] cmlenz@177: mo_files = [] cmlenz@177: if not options.input_file: cmlenz@177: if not options.directory: cmlenz@177: parser.error('you must specify either the input file or the ' cmlenz@177: 'base directory') cmlenz@177: if options.locale: cmlenz@331: po_files.append((options.locale, cmlenz@331: os.path.join(options.directory, cmlenz@331: options.locale, 'LC_MESSAGES', cmlenz@331: options.domain + '.po'))) cmlenz@177: mo_files.append(os.path.join(options.directory, options.locale, cmlenz@177: 'LC_MESSAGES', cmlenz@177: options.domain + '.mo')) cmlenz@177: else: cmlenz@177: for locale in os.listdir(options.directory): cmlenz@177: po_file = os.path.join(options.directory, locale, cmlenz@177: 'LC_MESSAGES', options.domain + '.po') cmlenz@177: if os.path.exists(po_file): cmlenz@331: po_files.append((locale, po_file)) cmlenz@177: mo_files.append(os.path.join(options.directory, locale, cmlenz@177: 'LC_MESSAGES', cmlenz@177: options.domain + '.mo')) cmlenz@177: else: cmlenz@331: po_files.append((options.locale, options.input_file)) cmlenz@177: if options.output_file: cmlenz@177: mo_files.append(options.output_file) cmlenz@177: else: cmlenz@177: if not options.directory: cmlenz@177: parser.error('you must specify either the input file or ' cmlenz@177: 'the base directory') pjenvey@290: mo_files.append(os.path.join(options.directory, options.locale, cmlenz@177: 'LC_MESSAGES', cmlenz@177: options.domain + '.mo')) palgarvio@208: if not po_files: palgarvio@208: parser.error('no message catalogs found') cmlenz@160: cmlenz@331: for idx, (locale, po_file) in enumerate(po_files): cmlenz@177: mo_file = mo_files[idx] cmlenz@177: infile = open(po_file, 'r') palgarvio@170: try: cmlenz@331: catalog = read_po(infile, locale) palgarvio@170: finally: palgarvio@170: infile.close() cmlenz@177: palgarvio@207: if options.statistics: palgarvio@207: translated = 0 palgarvio@207: for message in list(catalog)[1:]: palgarvio@207: if message.string: palgarvio@207: translated +=1 cmlenz@317: percentage = 0 cmlenz@317: if len(catalog): cmlenz@317: percentage = translated * 100 // len(catalog) cmlenz@232: self.log.info("%d of %d messages (%d%%) translated in %r", cmlenz@317: translated, len(catalog), percentage, po_file) palgarvio@207: palgarvio@175: if catalog.fuzzy and not options.use_fuzzy: cmlenz@232: self.log.warn('catalog %r is marked as fuzzy, skipping', cmlenz@232: po_file) cmlenz@177: continue cmlenz@177: cmlenz@220: for message, errors in catalog.check(): cmlenz@220: for error in errors: cmlenz@232: self.log.error('error: %s:%d: %s', po_file, message.lineno, cmlenz@232: error) cmlenz@220: cmlenz@232: self.log.info('compiling catalog %r to %r', po_file, mo_file) cmlenz@177: cmlenz@279: outfile = open(mo_file, 'wb') palgarvio@170: try: palgarvio@170: write_mo(outfile, catalog, use_fuzzy=options.use_fuzzy) palgarvio@170: finally: palgarvio@170: outfile.close() cmlenz@160: cmlenz@52: def extract(self, argv): cmlenz@52: """Subcommand for extracting messages from source files and generating cmlenz@52: a POT file. cmlenz@52: cmlenz@52: :param argv: the command arguments cmlenz@52: """ palgarvio@66: parser = OptionParser(usage=self.usage % ('extract', 'dir1 ...'), cmlenz@161: description=self.commands['extract']) cmlenz@52: parser.add_option('--charset', dest='charset', cmlenz@160: help='charset to use in the output (default ' cmlenz@160: '"%default")') cmlenz@52: parser.add_option('-k', '--keyword', dest='keywords', action='append', cmlenz@52: help='keywords to look for in addition to the ' cmlenz@52: 'defaults. You can specify multiple -k flags on ' cmlenz@52: 'the command line.') cmlenz@52: parser.add_option('--no-default-keywords', dest='no_default_keywords', cmlenz@52: action='store_true', cmlenz@52: help="do not include the default keywords") cmlenz@52: parser.add_option('--mapping', '-F', dest='mapping_file', cmlenz@52: help='path to the extraction mapping file') cmlenz@52: parser.add_option('--no-location', dest='no_location', cmlenz@52: action='store_true', cmlenz@52: help='do not include location comments with filename ' cmlenz@52: 'and line number') cmlenz@52: parser.add_option('--omit-header', dest='omit_header', cmlenz@52: action='store_true', cmlenz@52: help='do not include msgid "" entry in header') cmlenz@52: parser.add_option('-o', '--output', dest='output', cmlenz@52: help='path to the output POT file') cmlenz@52: parser.add_option('-w', '--width', dest='width', type='int', palgarvio@423: help="set output line width (default 76)") cmlenz@52: parser.add_option('--no-wrap', dest='no_wrap', action = 'store_true', cmlenz@52: help='do not break long message lines, longer than ' cmlenz@52: 'the output line width, into several lines') palgarvio@71: parser.add_option('--sort-output', dest='sort_output', palgarvio@71: action='store_true', palgarvio@71: help='generate sorted output (default False)') palgarvio@71: parser.add_option('--sort-by-file', dest='sort_by_file', palgarvio@71: action='store_true', palgarvio@71: help='sort output by file location (default False)') palgarvio@78: parser.add_option('--msgid-bugs-address', dest='msgid_bugs_address', palgarvio@78: metavar='EMAIL@ADDRESS', palgarvio@78: help='set report address for msgid') palgarvio@79: parser.add_option('--copyright-holder', dest='copyright_holder', palgarvio@79: help='set copyright holder in output') dfraser@429: parser.add_option('--project', dest='project', dfraser@429: help='set project name in output') dfraser@429: parser.add_option('--version', dest='version', dfraser@429: help='set project version in output') cmlenz@95: parser.add_option('--add-comments', '-c', dest='comment_tags', palgarvio@80: metavar='TAG', action='append', palgarvio@80: help='place comment block with TAG (or those ' palgarvio@80: 'preceding keyword lines) in output file. One ' palgarvio@80: 'TAG per argument call') aronacher@338: parser.add_option('--strip-comment-tags', '-s', aronacher@338: dest='strip_comment_tags', action='store_true', aronacher@338: help='Strip the comment tags from the comments.') cmlenz@52: cmlenz@52: parser.set_defaults(charset='utf-8', keywords=[], cmlenz@52: no_default_keywords=False, no_location=False, palgarvio@423: omit_header = False, width=None, no_wrap=False, palgarvio@80: sort_output=False, sort_by_file=False, aronacher@338: comment_tags=[], strip_comment_tags=False) cmlenz@52: options, args = parser.parse_args(argv) cmlenz@52: if not args: cmlenz@52: parser.error('incorrect number of arguments') cmlenz@52: cmlenz@52: if options.output not in (None, '-'): cmlenz@52: outfile = open(options.output, 'w') cmlenz@52: else: cmlenz@52: outfile = sys.stdout cmlenz@52: cmlenz@52: keywords = DEFAULT_KEYWORDS.copy() cmlenz@52: if options.no_default_keywords: cmlenz@52: if not options.keywords: cmlenz@52: parser.error('you must specify new keywords if you disable the ' cmlenz@52: 'default ones') cmlenz@52: keywords = {} cmlenz@52: if options.keywords: cmlenz@52: keywords.update(parse_keywords(options.keywords)) cmlenz@52: cmlenz@52: if options.mapping_file: cmlenz@52: fileobj = open(options.mapping_file, 'U') cmlenz@52: try: cmlenz@52: method_map, options_map = parse_mapping(fileobj) cmlenz@52: finally: cmlenz@52: fileobj.close() cmlenz@52: else: cmlenz@52: method_map = DEFAULT_MAPPING cmlenz@52: options_map = {} cmlenz@52: cmlenz@52: if options.width and options.no_wrap: cmlenz@52: parser.error("'--no-wrap' and '--width' are mutually exclusive.") cmlenz@52: elif not options.width and not options.no_wrap: cmlenz@52: options.width = 76 palgarvio@87: palgarvio@71: if options.sort_output and options.sort_by_file: palgarvio@71: parser.error("'--sort-output' and '--sort-by-file' are mutually " palgarvio@71: "exclusive") cmlenz@52: cmlenz@52: try: dfraser@429: catalog = Catalog(project=options.project, dfraser@429: version=options.version, dfraser@429: msgid_bugs_address=options.msgid_bugs_address, palgarvio@105: copyright_holder=options.copyright_holder, cmlenz@102: charset=options.charset) cmlenz@102: cmlenz@52: for dirname in args: cmlenz@52: if not os.path.isdir(dirname): cmlenz@52: parser.error('%r is not a directory' % dirname) cmlenz@232: cmlenz@232: def callback(filename, method, options): cmlenz@232: if method == 'ignore': cmlenz@232: return cmlenz@232: filepath = os.path.normpath(os.path.join(dirname, filename)) cmlenz@232: optstr = '' cmlenz@232: if options: cmlenz@232: optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for cmlenz@232: k, v in options.items()]) cmlenz@232: self.log.info('extracting messages from %s%s', filepath, cmlenz@232: optstr) cmlenz@232: cmlenz@95: extracted = extract_from_dir(dirname, method_map, options_map, cmlenz@232: keywords, options.comment_tags, aronacher@338: callback=callback, aronacher@338: strip_comment_tags= aronacher@338: options.strip_comment_tags) palgarvio@80: for filename, lineno, message, comments in extracted: cmlenz@52: filepath = os.path.normpath(os.path.join(dirname, filename)) cmlenz@177: catalog.add(message, None, [(filepath, lineno)], pjenvey@110: auto_comments=comments) cmlenz@56: cmlenz@232: if options.output not in (None, '-'): cmlenz@232: self.log.info('writing PO template file to %s' % options.output) cmlenz@104: write_po(outfile, catalog, width=options.width, cmlenz@104: no_location=options.no_location, cmlenz@104: omit_header=options.omit_header, cmlenz@104: sort_output=options.sort_output, pjenvey@112: sort_by_file=options.sort_by_file) cmlenz@52: finally: cmlenz@52: if options.output: cmlenz@52: outfile.close() cmlenz@52: cmlenz@52: def init(self, argv): cmlenz@52: """Subcommand for creating new message catalogs from a template. cmlenz@177: cmlenz@52: :param argv: the command arguments cmlenz@52: """ cmlenz@165: parser = OptionParser(usage=self.usage % ('init', ''), cmlenz@161: description=self.commands['init']) palgarvio@66: parser.add_option('--domain', '-D', dest='domain', cmlenz@104: help="domain of PO file (default '%default')") palgarvio@66: parser.add_option('--input-file', '-i', dest='input_file', cmlenz@104: metavar='FILE', help='name of the input file') palgarvio@66: parser.add_option('--output-dir', '-d', dest='output_dir', cmlenz@104: metavar='DIR', help='path to output directory') palgarvio@66: parser.add_option('--output-file', '-o', dest='output_file', cmlenz@104: metavar='FILE', palgarvio@66: help="name of the output file (default " palgarvio@87: "'//LC_MESSAGES/" palgarvio@87: ".po')") cmlenz@104: parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', palgarvio@66: help='locale for the new localized catalog') palgarvio@87: cmlenz@104: parser.set_defaults(domain='messages') palgarvio@66: options, args = parser.parse_args(argv) palgarvio@87: cmlenz@104: if not options.locale: cmlenz@104: parser.error('you must provide a locale for the new catalog') cmlenz@104: try: cmlenz@104: locale = Locale.parse(options.locale) cmlenz@104: except UnknownLocaleError, e: cmlenz@104: parser.error(e) palgarvio@87: palgarvio@66: if not options.input_file: palgarvio@66: parser.error('you must specify the input file') palgarvio@87: cmlenz@104: if not options.output_file and not options.output_dir: cmlenz@104: parser.error('you must specify the output file or directory') palgarvio@87: cmlenz@104: if not options.output_file: palgarvio@66: options.output_file = os.path.join(options.output_dir, cmlenz@104: options.locale, 'LC_MESSAGES', palgarvio@66: options.domain + '.po') palgarvio@87: if not os.path.exists(os.path.dirname(options.output_file)): palgarvio@87: os.makedirs(os.path.dirname(options.output_file)) palgarvio@87: palgarvio@66: infile = open(options.input_file, 'r') cmlenz@104: try: palgarvio@371: # Although reading from the catalog template, read_po must be fed palgarvio@371: # the locale in order to correcly calculate plurals palgarvio@371: catalog = read_po(infile, locale=options.locale) cmlenz@104: finally: cmlenz@104: infile.close() palgarvio@87: cmlenz@104: catalog.locale = locale cmlenz@132: catalog.revision_date = datetime.now(LOCALTZ) palgarvio@66: cmlenz@232: self.log.info('creating catalog %r based on %r', options.output_file, cmlenz@232: options.input_file) palgarvio@66: cmlenz@104: outfile = open(options.output_file, 'w') cmlenz@104: try: cmlenz@104: write_po(outfile, catalog) cmlenz@104: finally: cmlenz@104: outfile.close() cmlenz@52: cmlenz@181: def update(self, argv): cmlenz@181: """Subcommand for updating existing message catalogs from a template. cmlenz@181: cmlenz@181: :param argv: the command arguments cmlenz@234: :since: version 0.9 cmlenz@181: """ cmlenz@181: parser = OptionParser(usage=self.usage % ('update', ''), cmlenz@181: description=self.commands['update']) cmlenz@181: parser.add_option('--domain', '-D', dest='domain', cmlenz@181: help="domain of PO file (default '%default')") cmlenz@181: parser.add_option('--input-file', '-i', dest='input_file', cmlenz@181: metavar='FILE', help='name of the input file') cmlenz@181: parser.add_option('--output-dir', '-d', dest='output_dir', cmlenz@181: metavar='DIR', help='path to output directory') cmlenz@181: parser.add_option('--output-file', '-o', dest='output_file', cmlenz@181: metavar='FILE', cmlenz@181: help="name of the output file (default " cmlenz@181: "'//LC_MESSAGES/" cmlenz@181: ".po')") cmlenz@181: parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', cmlenz@181: help='locale of the translations catalog') cmlenz@191: parser.add_option('--ignore-obsolete', dest='ignore_obsolete', cmlenz@191: action='store_true', cmlenz@191: help='do not include obsolete messages in the output ' palgarvio@417: '(default %default)') palgarvio@200: parser.add_option('--no-fuzzy-matching', '-N', dest='no_fuzzy_matching', palgarvio@200: action='store_true', palgarvio@417: help='do not use fuzzy matching (default %default)') palgarvio@200: parser.add_option('--previous', dest='previous', action='store_true', palgarvio@200: help='keep previous msgids of translated messages ' palgarvio@417: '(default %default)') cmlenz@181: palgarvio@200: parser.set_defaults(domain='messages', ignore_obsolete=False, palgarvio@200: no_fuzzy_matching=False, previous=False) cmlenz@181: options, args = parser.parse_args(argv) cmlenz@181: cmlenz@181: if not options.input_file: cmlenz@181: parser.error('you must specify the input file') cmlenz@181: if not options.output_file and not options.output_dir: cmlenz@181: parser.error('you must specify the output file or directory') cmlenz@196: if options.output_file and not options.locale: palgarvio@417: parser.error('you must specify the locale') palgarvio@200: if options.no_fuzzy_matching and options.previous: palgarvio@200: options.previous = False cmlenz@181: cmlenz@181: po_files = [] cmlenz@181: if not options.output_file: cmlenz@181: if options.locale: cmlenz@196: po_files.append((options.locale, cmlenz@196: os.path.join(options.output_dir, cmlenz@196: options.locale, 'LC_MESSAGES', cmlenz@196: options.domain + '.po'))) cmlenz@181: else: cmlenz@181: for locale in os.listdir(options.output_dir): cmlenz@181: po_file = os.path.join(options.output_dir, locale, cmlenz@181: 'LC_MESSAGES', cmlenz@181: options.domain + '.po') cmlenz@181: if os.path.exists(po_file): cmlenz@196: po_files.append((locale, po_file)) cmlenz@181: else: cmlenz@196: po_files.append((options.locale, options.output_file)) cmlenz@196: cmlenz@196: domain = options.domain cmlenz@196: if not domain: cmlenz@196: domain = os.path.splitext(os.path.basename(options.input_file))[0] cmlenz@181: cmlenz@181: infile = open(options.input_file, 'U') cmlenz@181: try: cmlenz@181: template = read_po(infile) cmlenz@181: finally: cmlenz@181: infile.close() cmlenz@181: palgarvio@208: if not po_files: palgarvio@208: parser.error('no message catalogs found') palgarvio@208: cmlenz@196: for locale, filename in po_files: cmlenz@232: self.log.info('updating catalog %r based on %r', filename, cmlenz@232: options.input_file) cmlenz@196: infile = open(filename, 'U') cmlenz@181: try: cmlenz@196: catalog = read_po(infile, locale=locale, domain=domain) cmlenz@181: finally: cmlenz@181: infile.close() cmlenz@181: cmlenz@204: catalog.update(template, options.no_fuzzy_matching) cmlenz@197: cmlenz@197: tmpname = os.path.join(os.path.dirname(filename), palgarvio@200: tempfile.gettempprefix() + cmlenz@197: os.path.basename(filename)) cmlenz@197: tmpfile = open(tmpname, 'w') cmlenz@181: try: cmlenz@197: try: cmlenz@197: write_po(tmpfile, catalog, palgarvio@200: ignore_obsolete=options.ignore_obsolete, cmlenz@205: include_previous=options.previous) cmlenz@197: finally: cmlenz@197: tmpfile.close() cmlenz@197: except: cmlenz@197: os.remove(tmpname) cmlenz@197: raise cmlenz@197: cmlenz@197: try: cmlenz@197: os.rename(tmpname, filename) cmlenz@197: except OSError: cmlenz@197: # We're probably on Windows, which doesn't support atomic cmlenz@197: # renames, at least not through Python cmlenz@197: # If the error is in fact due to a permissions problem, that cmlenz@197: # same error is going to be raised from one of the following cmlenz@197: # operations cmlenz@197: os.remove(filename) cmlenz@197: shutil.copy(tmpname, filename) cmlenz@197: os.remove(tmpname) cmlenz@181: cmlenz@165: cmlenz@52: def main(): cmlenz@185: return CommandLineInterface().run(sys.argv) cmlenz@1: cmlenz@48: def parse_mapping(fileobj, filename=None): cmlenz@47: """Parse an extraction method mapping from a file-like object. cmlenz@52: cmlenz@47: >>> buf = StringIO(''' cmlenz@250: ... [extractors] cmlenz@250: ... custom = mypackage.module:myfunc cmlenz@250: ... cmlenz@47: ... # Python source files cmlenz@62: ... [python: **.py] cmlenz@52: ... cmlenz@47: ... # Genshi templates cmlenz@62: ... [genshi: **/templates/**.html] cmlenz@48: ... include_attrs = cmlenz@62: ... [genshi: **/templates/**.txt] cmlenz@144: ... template_class = genshi.template:TextTemplate cmlenz@48: ... encoding = latin-1 cmlenz@250: ... cmlenz@250: ... # Some custom extractor cmlenz@250: ... [custom: **/custom/*.*] cmlenz@47: ... ''') cmlenz@52: cmlenz@47: >>> method_map, options_map = parse_mapping(buf) cmlenz@250: >>> len(method_map) cmlenz@250: 4 cmlenz@52: cmlenz@62: >>> method_map[0] cmlenz@62: ('**.py', 'python') cmlenz@62: >>> options_map['**.py'] cmlenz@47: {} cmlenz@62: >>> method_map[1] cmlenz@62: ('**/templates/**.html', 'genshi') cmlenz@62: >>> options_map['**/templates/**.html']['include_attrs'] cmlenz@47: '' cmlenz@62: >>> method_map[2] cmlenz@62: ('**/templates/**.txt', 'genshi') cmlenz@62: >>> options_map['**/templates/**.txt']['template_class'] cmlenz@144: 'genshi.template:TextTemplate' cmlenz@62: >>> options_map['**/templates/**.txt']['encoding'] cmlenz@47: 'latin-1' cmlenz@52: cmlenz@250: >>> method_map[3] cmlenz@250: ('**/custom/*.*', 'mypackage.module:myfunc') cmlenz@250: >>> options_map['**/custom/*.*'] cmlenz@250: {} cmlenz@250: cmlenz@47: :param fileobj: a readable file-like object containing the configuration cmlenz@47: text to parse cmlenz@47: :return: a `(method_map, options_map)` tuple cmlenz@47: :rtype: `tuple` cmlenz@47: :see: `extract_from_directory` cmlenz@47: """ cmlenz@250: extractors = {} cmlenz@62: method_map = [] cmlenz@47: options_map = {} cmlenz@47: cmlenz@48: parser = RawConfigParser() cmlenz@62: parser._sections = odict(parser._sections) # We need ordered sections cmlenz@48: parser.readfp(fileobj, filename) cmlenz@48: for section in parser.sections(): cmlenz@250: if section == 'extractors': cmlenz@250: extractors = dict(parser.items(section)) cmlenz@250: else: cmlenz@250: method, pattern = [part.strip() for part in section.split(':', 1)] cmlenz@250: method_map.append((pattern, method)) cmlenz@250: options_map[pattern] = dict(parser.items(section)) cmlenz@250: cmlenz@250: if extractors: cmlenz@250: for idx, (pattern, method) in enumerate(method_map): cmlenz@250: if method in extractors: cmlenz@250: method = extractors[method] cmlenz@250: method_map[idx] = (pattern, method) cmlenz@47: cmlenz@47: return (method_map, options_map) cmlenz@47: cmlenz@12: def parse_keywords(strings=[]): cmlenz@12: """Parse keywords specifications from the given list of strings. cmlenz@52: pjenvey@414: >>> kw = parse_keywords(['_', 'dgettext:2', 'dngettext:2,3']).items() pjenvey@414: >>> kw.sort() pjenvey@414: >>> for keyword, indices in kw: cmlenz@12: ... print (keyword, indices) cmlenz@12: ('_', None) cmlenz@12: ('dgettext', (2,)) cmlenz@12: ('dngettext', (2, 3)) cmlenz@12: """ cmlenz@12: keywords = {} cmlenz@12: for string in strings: cmlenz@12: if ':' in string: cmlenz@12: funcname, indices = string.split(':') cmlenz@12: else: cmlenz@12: funcname, indices = string, None cmlenz@12: if funcname not in keywords: cmlenz@12: if indices: cmlenz@12: indices = tuple([(int(x)) for x in indices.split(',')]) cmlenz@12: keywords[funcname] = indices cmlenz@12: return keywords cmlenz@12: cmlenz@52: cmlenz@1: if __name__ == '__main__': palgarvio@55: main()