# HG changeset patch # User cmlenz # Date 1181163804 0 # Node ID f8469ab4b257ba4c64bcc2b10cb1e24fdbd67246 # Parent 29b88754e13ace036ead52503c0524976a19e6c4 Support passing extraction method mapping and options from the frontends (see #4). No distutils/setuptools keyword supported yet, but the rest seems to be working okay. diff --git a/babel/catalog/extract.py b/babel/catalog/extract.py --- a/babel/catalog/extract.py +++ b/babel/catalog/extract.py @@ -52,8 +52,9 @@ } -def extract_from_dir(dirname, method_map=DEFAULT_MAPPING, - options_map=None, keywords=DEFAULT_KEYWORDS): +def extract_from_dir(dirname=os.getcwd(), method_map=DEFAULT_MAPPING, + options_map=None, keywords=DEFAULT_KEYWORDS, + callback=None): """Extract messages from any source files found in the given directory. This function generates tuples of the form: @@ -109,6 +110,10 @@ that should be recognized as translation functions) to tuples that specify which of their arguments contain localizable strings + :param callback: a function that is called for every file that message are + extracted from, just before the extraction itself is + performed; the function is passed the filename and the + options dictionary as positional arguments, in that order :return: an iterator over ``(filename, lineno, funcname, message)`` tuples :rtype: ``iterator`` :see: `pathmatch` @@ -132,10 +137,12 @@ for opattern, odict in options_map.items(): if pathmatch(opattern, filename): options = odict + if callback: + callback(filename, options) for line, func, key in extract_from_file(method, filepath, keywords=keywords, options=options): - yield filepath, line, func, key + yield filename, line, func, key def extract_from_file(method, filename, keywords=DEFAULT_KEYWORDS, options=None): @@ -224,8 +231,23 @@ """ from genshi.filters.i18n import Translator from genshi.template import MarkupTemplate - tmpl = MarkupTemplate(fileobj, filename=getattr(fileobj, 'name')) - translator = Translator(None) + + template_class = options.get('template_class', MarkupTemplate) + if isinstance(template_class, basestring): + module, clsname = template_class.split(':', 1) + template_class = getattr(__import__(module, {}, {}, [clsname]), clsname) + encoding = options.get('encoding', None) + + ignore_tags = options.get('ignore_tags', Translator.IGNORE_TAGS) + if isinstance(ignore_tags, basestring): + ignore_tags = ignore_tags.split() + include_attrs = options.get('include_attrs', Translator.INCLUDE_ATTRS) + if isinstance(include_attrs, basestring): + include_attrs = include_attrs.split() + + tmpl = template_class(fileobj, filename=getattr(fileobj, 'name'), + encoding=encoding) + translator = Translator(None, ignore_tags, include_attrs) for message in translator.extract(tmpl.stream, gettext_functions=keywords): yield message diff --git a/babel/catalog/frontend.py b/babel/catalog/frontend.py --- a/babel/catalog/frontend.py +++ b/babel/catalog/frontend.py @@ -18,10 +18,12 @@ from distutils.errors import DistutilsOptionError from optparse import OptionParser import os +import re import sys from babel import __version__ as VERSION -from babel.catalog.extract import extract_from_dir, DEFAULT_KEYWORDS +from babel.catalog.extract import extract_from_dir, DEFAULT_KEYWORDS, \ + DEFAULT_MAPPING from babel.catalog.pofile import write_po __all__ = ['extract_messages', 'main'] @@ -55,6 +57,8 @@ 'defaults'), ('no-default-keywords', None, 'do not include the default keywords'), + ('mapping-file=', 'F', + 'path to the mapping configuration file'), ('no-location', None, 'do not include location comments with filename and line number'), ('omit-header', None, @@ -73,21 +77,16 @@ def initialize_options(self): self.charset = 'utf-8' - self.width = 76 - self.no_wrap = False self.keywords = self._keywords = DEFAULT_KEYWORDS.copy() self.no_default_keywords = False + self.mapping_file = None self.no_location = False self.omit_header = False self.output_file = None - self.input_dirs = None + self.width = 76 + self.no_wrap = False def finalize_options(self): - if not self.input_dirs: - self.input_dirs = dict.fromkeys([k.split('.',1)[0] - for k in self.distribution.packages - ]).keys() - if self.no_default_keywords and not self.keywords: raise DistutilsOptionError('you must specify new keywords if you ' 'disable the default ones') @@ -106,15 +105,33 @@ self.width = int(self.width) def run(self): + if self.mapping_file: + fileobj = open(self.mapping_file, 'U') + try: + method_map, options_map = parse_mapping(fileobj) + finally: + fileobj.close() + else: + method_map = DEFAULT_MAPPING + options_map = {} + outfile = open(self.output_file, 'w') try: + def callback(filename, options): + optstr = '' + if options: + optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for k, v + in options.items()]) + log.info('extracting messages from %s%s' % (filename, optstr)) + messages = [] - for dirname in self.input_dirs: - log.info('extracting messages from %r' % dirname) - extracted = extract_from_dir(dirname, keywords=self.keywords) - for filename, lineno, funcname, message in extracted: - messages.append((os.path.join(dirname, filename), lineno, - funcname, message, None)) + extracted = extract_from_dir(method_map=method_map, + options_map=options_map, + keywords=self.keywords, + callback=callback) + for filename, lineno, funcname, message in extracted: + filepath = os.path.normpath(filename) + messages.append((filepath, lineno, funcname, message, None)) log.info('writing PO file to %s' % self.output_file) write_po(outfile, messages, project=self.distribution.get_name(), @@ -143,8 +160,9 @@ 'line.') parser.add_option('--no-default-keywords', dest='no_default_keywords', action='store_true', default=False, - help="do not include the default keywords defined by " - "Babel") + help="do not include the default keywords") + parser.add_option('--mapping', '-F', dest='mapping_file', + help='path to the extraction mapping file') parser.add_option('--no-location', dest='no_location', default=False, action='store_true', help='do not include location comments with filename and ' @@ -177,7 +195,17 @@ keywords = {} if options.keywords: keywords.update(parse_keywords(options.keywords)) - + + if options.mapping_file: + fileobj = open(options.mapping_file, 'U') + try: + method_map, options_map = parse_mapping(fileobj) + finally: + fileobj.close() + else: + method_map = DEFAULT_MAPPING + options_map = {} + if options.width and options.no_wrap: parser.error("'--no-wrap' and '--width' are mutually exclusive.") elif not options.width and not options.no_wrap: @@ -190,10 +218,11 @@ for dirname in args: if not os.path.isdir(dirname): parser.error('%r is not a directory' % dirname) - extracted = extract_from_dir(dirname, keywords=keywords) + extracted = extract_from_dir(dirname, method_map, options_map, + keywords) for filename, lineno, funcname, message in extracted: - messages.append((os.path.join(dirname, filename), lineno, - funcname, message, None)) + filepath = os.path.normpath(os.path.join(dirname, filename)) + messages.append((filepath, lineno, funcname, message, None)) write_po(outfile, messages, width=options.width, charset=options.charset, no_location=options.no_location, omit_header=options.omit_header) @@ -201,6 +230,65 @@ if options.output: outfile.close() +def parse_mapping(fileobj): + """Parse an extraction method mapping from a file-like object. + + >>> from StringIO import StringIO + >>> buf = StringIO(''' + ... # Python source files + ... python: foobar/**.py + ... + ... # Genshi templates + ... genshi: foobar/**/templates/**.html + ... include_attrs = + ... genshi: foobar/**/templates/**.txt + ... template_class = genshi.template.text.TextTemplate + ... encoding = latin-1 + ... ''') + + >>> method_map, options_map = parse_mapping(buf) + + >>> method_map['foobar/**.py'] + 'python' + >>> options_map['foobar/**.py'] + {} + >>> method_map['foobar/**/templates/**.html'] + 'genshi' + >>> options_map['foobar/**/templates/**.html']['include_attrs'] + '' + >>> method_map['foobar/**/templates/**.txt'] + 'genshi' + >>> options_map['foobar/**/templates/**.txt']['template_class'] + 'genshi.template.text.TextTemplate' + >>> options_map['foobar/**/templates/**.txt']['encoding'] + 'latin-1' + + :param fileobj: a readable file-like object containing the configuration + text to parse + :return: a `(method_map, options_map)` tuple + :rtype: `tuple` + :see: `extract_from_directory` + """ + method_map = {} + options_map = {} + + method = None + for line in fileobj.readlines(): + if line.startswith('#'): # comment + continue + match = re.match('(\w+): (.+)', line) + if match: + method, pattern = match.group(1, 2) + method_map[pattern] = method + options_map[pattern] = {} + elif method: + match = re.match('\s+(\w+)\s*=\s*(.*)', line) + if match: + option, value = match.group(1, 2) + options_map[pattern][option] = value.strip() + + return (method_map, options_map) + def parse_keywords(strings=[]): """Parse keywords specifications from the given list of strings. diff --git a/babel/util.py b/babel/util.py --- a/babel/util.py +++ b/babel/util.py @@ -39,6 +39,7 @@ True >>> pathmatch('**.py', 'templates/index.html') False + >>> pathmatch('**/templates/*.html', 'templates/index.html') True >>> pathmatch('**/templates/*.html', 'templates/foo/bar.html')