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