1 # -*- coding: utf-8 -*-
3 babel.messages.frontend
4 ~~~~~~~~~~~~~~~~~~~~~~~
6 Frontends for the message extraction functionality.
8 :copyright: (c) 2013 by the Babel Team.
9 :license: BSD, see LICENSE for more details.
11 from __future__ import print_function
20 from datetime import datetime
21 from locale import getpreferredencoding
23 from babel import __version__ as VERSION
24 from babel import Locale, localedata
25 from babel._compat import StringIO, string_types, text_type
26 from babel.core import UnknownLocaleError
27 from babel.messages.catalog import Catalog
28 from babel.messages.extract import DEFAULT_KEYWORDS, DEFAULT_MAPPING, check_and_call_extract_file, extract_from_dir
29 from babel.messages.mofile import write_mo
30 from babel.messages.pofile import read_po, write_po
31 from babel.util import LOCALTZ, odict
32 from distutils import log as distutils_log
33 from distutils.cmd import Command as _Command
34 from distutils.errors import DistutilsOptionError, DistutilsSetupError
37 from ConfigParser import RawConfigParser
39 from configparser import RawConfigParser
42 def listify_value(arg, split=None):
44 Make a list out of an argument.
46 Values from `distutils` argument parsing are always single strings;
47 values from `optparse` parsing may be lists of strings that may need
50 No matter the input, this function returns a flat list of whitespace-trimmed
51 strings, with `None` values filtered out.
53 >>> listify_value("foo bar")
55 >>> listify_value(["foo bar"])
57 >>> listify_value([["foo"], "bar"])
59 >>> listify_value([["foo"], ["bar", None, "foo"]])
61 >>> listify_value("foo, bar, quux", ",")
62 ['foo', 'bar', 'quux']
64 :param arg: A string or a list of strings
65 :param split: The argument to pass to `str.split()`.
70 if not isinstance(arg, (list, tuple)):
76 if isinstance(val, (list, tuple)):
77 out.extend(listify_value(val, split=split))
79 out.extend(s.strip() for s in text_type(val).split(split))
80 assert all(isinstance(val, string_types) for val in out)
84 class Command(_Command):
85 # This class is a small shim between Distutils commands and
86 # optparse option parsing in the frontend command line.
88 #: Option name to be input as `args` on the script command line.
91 #: Options which allow multiple values.
92 #: This is used by the `optparse` transmogrification code.
93 multiple_value_options = ()
95 #: Options which are booleans.
96 #: This is used by the `optparse` transmogrification code.
97 # (This is actually used by distutils code too, but is never
98 # declared in the base class.)
101 #: Option aliases, to retain standalone command compatibility.
102 #: Distutils does not support option aliases, but optparse does.
103 #: This maps the distutils argument name to an iterable of aliases
104 #: that are usable with optparse.
107 #: Log object. To allow replacement in the script command line runner.
110 def __init__(self, dist=None):
111 # A less strict version of distutils' `__init__`.
112 self.distribution = dist
113 self.initialize_options()
121 class compile_catalog(Command):
122 """Catalog compilation command for use in ``setup.py`` scripts.
124 If correctly installed, this command is available to Setuptools-using
125 setup scripts automatically. For projects using plain old ``distutils``,
126 the command needs to be registered explicitly in ``setup.py``::
128 from babel.messages.frontend import compile_catalog
132 cmdclass = {'compile_catalog': compile_catalog}
135 .. versionadded:: 0.9
138 description = 'compile message catalogs to binary MO files'
141 "domains of PO files (space separated list, default 'messages')"),
143 'path to base directory containing the catalogs'),
145 'name of the input file'),
146 ('output-file=', 'o',
147 "name of the output file (default "
148 "'<output_dir>/<locale>/LC_MESSAGES/<domain>.mo')"),
150 'locale of the catalog to compile'),
152 'also include fuzzy translations'),
154 'print statistics about translations')
156 boolean_options = ['use-fuzzy', 'statistics']
158 def initialize_options(self):
159 self.domain = 'messages'
160 self.directory = None
161 self.input_file = None
162 self.output_file = None
164 self.use_fuzzy = False
165 self.statistics = False
167 def finalize_options(self):
168 self.domain = listify_value(self.domain)
169 if not self.input_file and not self.directory:
170 raise DistutilsOptionError('you must specify either the input file '
171 'or the base directory')
172 if not self.output_file and not self.directory:
173 raise DistutilsOptionError('you must specify either the output file '
174 'or the base directory')
177 for domain in self.domain:
178 self._run_domain(domain)
180 def _run_domain(self, domain):
184 if not self.input_file:
186 po_files.append((self.locale,
187 os.path.join(self.directory, self.locale,
190 mo_files.append(os.path.join(self.directory, self.locale,
194 for locale in os.listdir(self.directory):
195 po_file = os.path.join(self.directory, locale,
196 'LC_MESSAGES', domain + '.po')
197 if os.path.exists(po_file):
198 po_files.append((locale, po_file))
199 mo_files.append(os.path.join(self.directory, locale,
203 po_files.append((self.locale, self.input_file))
205 mo_files.append(self.output_file)
207 mo_files.append(os.path.join(self.directory, self.locale,
212 raise DistutilsOptionError('no message catalogs found')
214 for idx, (locale, po_file) in enumerate(po_files):
215 mo_file = mo_files[idx]
216 infile = open(po_file, 'rb')
218 catalog = read_po(infile, locale)
224 for message in list(catalog)[1:]:
229 percentage = translated * 100 // len(catalog)
231 '%d of %d messages (%d%%) translated in %s',
232 translated, len(catalog), percentage, po_file
235 if catalog.fuzzy and not self.use_fuzzy:
236 self.log.info('catalog %s is marked as fuzzy, skipping', po_file)
239 for message, errors in catalog.check():
242 'error: %s:%d: %s', po_file, message.lineno, error
245 self.log.info('compiling catalog %s to %s', po_file, mo_file)
247 outfile = open(mo_file, 'wb')
249 write_mo(outfile, catalog, use_fuzzy=self.use_fuzzy)
254 class extract_messages(Command):
255 """Message extraction command for use in ``setup.py`` scripts.
257 If correctly installed, this command is available to Setuptools-using
258 setup scripts automatically. For projects using plain old ``distutils``,
259 the command needs to be registered explicitly in ``setup.py``::
261 from babel.messages.frontend import extract_messages
265 cmdclass = {'extract_messages': extract_messages}
269 description = 'extract localizable strings from the project code'
272 'charset to use in the output file (default "utf-8")'),
274 'space-separated list of keywords to look for in addition to the '
275 'defaults (may be repeated multiple times)'),
276 ('no-default-keywords', None,
277 'do not include the default keywords'),
278 ('mapping-file=', 'F',
279 'path to the mapping configuration file'),
280 ('no-location', None,
281 'do not include location comments with filename and line number'),
282 ('omit-header', None,
283 'do not include msgid "" entry in header'),
284 ('output-file=', 'o',
285 'name of the output file'),
287 'set output line width (default 76)'),
289 'do not break long message lines, longer than the output line width, '
290 'into several lines'),
291 ('sort-output', None,
292 'generate sorted output (default False)'),
293 ('sort-by-file', None,
294 'sort output by file location (default False)'),
295 ('msgid-bugs-address=', None,
296 'set report address for msgid'),
297 ('copyright-holder=', None,
298 'set copyright holder in output'),
300 'set project name in output'),
302 'set project version in output'),
303 ('add-comments=', 'c',
304 'place comment block with TAG (or those preceding keyword lines) in '
305 'output file. Separate multiple TAGs with commas(,)'), # TODO: Support repetition of this argument
306 ('strip-comments', 's',
307 'strip the comment TAGs from the comments.'),
308 ('input-paths=', None,
309 'files or directories that should be scanned for messages. Separate multiple '
310 'files or directories with commas(,)'), # TODO: Support repetition of this argument
311 ('input-dirs=', None, # TODO (3.x): Remove me.
312 'alias for input-paths (does allow files as well as directories).'),
315 'no-default-keywords', 'no-location', 'omit-header', 'no-wrap',
316 'sort-output', 'sort-by-file', 'strip-comments'
318 as_args = 'input-paths'
319 multiple_value_options = ('add-comments', 'keywords')
321 'keywords': ('--keyword',),
322 'mapping-file': ('--mapping',),
323 'output-file': ('--output',),
324 'strip-comments': ('--strip-comment-tags',),
327 def initialize_options(self):
328 self.charset = 'utf-8'
330 self.no_default_keywords = False
331 self.mapping_file = None
332 self.no_location = False
333 self.omit_header = False
334 self.output_file = None
335 self.input_dirs = None
336 self.input_paths = None
339 self.sort_output = False
340 self.sort_by_file = False
341 self.msgid_bugs_address = None
342 self.copyright_holder = None
345 self.add_comments = None
346 self.strip_comments = False
348 def finalize_options(self):
350 if not self.input_paths:
351 self.input_paths = self.input_dirs
353 raise DistutilsOptionError(
354 'input-dirs and input-paths are mutually exclusive'
357 if self.no_default_keywords:
360 keywords = DEFAULT_KEYWORDS.copy()
362 keywords.update(parse_keywords(listify_value(self.keywords)))
364 self.keywords = keywords
366 if not self.keywords:
367 raise DistutilsOptionError('you must specify new keywords if you '
368 'disable the default ones')
370 if not self.output_file:
371 raise DistutilsOptionError('no output file specified')
372 if self.no_wrap and self.width:
373 raise DistutilsOptionError("'--no-wrap' and '--width' are mutually "
375 if not self.no_wrap and not self.width:
377 elif self.width is not None:
378 self.width = int(self.width)
380 if self.sort_output and self.sort_by_file:
381 raise DistutilsOptionError("'--sort-output' and '--sort-by-file' "
382 "are mutually exclusive")
385 if isinstance(self.input_paths, string_types):
386 self.input_paths = re.split(',\s*', self.input_paths)
387 elif self.distribution is not None:
388 self.input_paths = dict.fromkeys([
390 for k in (self.distribution.packages or ())
393 self.input_paths = []
395 if not self.input_paths:
396 raise DistutilsOptionError("no input files or directories specified")
398 for path in self.input_paths:
399 if not os.path.exists(path):
400 raise DistutilsOptionError("Input path: %s does not exist" % path)
402 self.add_comments = listify_value(self.add_comments or (), ",")
404 if self.distribution:
406 self.project = self.distribution.get_name()
408 self.version = self.distribution.get_version()
411 mappings = self._get_mappings()
412 with open(self.output_file, 'wb') as outfile:
413 catalog = Catalog(project=self.project,
414 version=self.version,
415 msgid_bugs_address=self.msgid_bugs_address,
416 copyright_holder=self.copyright_holder,
417 charset=self.charset)
419 for path, (method_map, options_map) in mappings.items():
420 def callback(filename, method, options):
421 if method == 'ignore':
424 # If we explicitly provide a full filepath, just use that.
425 # Otherwise, path will be the directory path and filename
426 # is the relative path from that dir to the file.
427 # So we can join those to get the full filepath.
428 if os.path.isfile(path):
431 filepath = os.path.normpath(os.path.join(path, filename))
435 optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for
436 k, v in options.items()])
437 self.log.info('extracting messages from %s%s', filepath, optstr)
439 if os.path.isfile(path):
440 current_dir = os.getcwd()
441 extracted = check_and_call_extract_file(
442 path, method_map, options_map,
443 callback, self.keywords, self.add_comments,
444 self.strip_comments, current_dir
447 extracted = extract_from_dir(
448 path, method_map, options_map,
449 keywords=self.keywords,
450 comment_tags=self.add_comments,
452 strip_comment_tags=self.strip_comments
454 for filename, lineno, message, comments, context in extracted:
455 if os.path.isfile(path):
456 filepath = filename # already normalized
458 filepath = os.path.normpath(os.path.join(path, filename))
460 catalog.add(message, None, [(filepath, lineno)],
461 auto_comments=comments, context=context)
463 self.log.info('writing PO template file to %s' % self.output_file)
464 write_po(outfile, catalog, width=self.width,
465 no_location=self.no_location,
466 omit_header=self.omit_header,
467 sort_output=self.sort_output,
468 sort_by_file=self.sort_by_file)
470 def _get_mappings(self):
473 if self.mapping_file:
474 fileobj = open(self.mapping_file, 'U')
476 method_map, options_map = parse_mapping(fileobj)
477 for path in self.input_paths:
478 mappings[path] = method_map, options_map
482 elif getattr(self.distribution, 'message_extractors', None):
483 message_extractors = self.distribution.message_extractors
484 for path, mapping in message_extractors.items():
485 if isinstance(mapping, string_types):
486 method_map, options_map = parse_mapping(StringIO(mapping))
488 method_map, options_map = [], {}
489 for pattern, method, options in mapping:
490 method_map.append((pattern, method))
491 options_map[pattern] = options or {}
492 mappings[path] = method_map, options_map
495 for path in self.input_paths:
496 mappings[path] = DEFAULT_MAPPING, {}
501 def check_message_extractors(dist, name, value):
502 """Validate the ``message_extractors`` keyword argument to ``setup()``.
504 :param dist: the distutils/setuptools ``Distribution`` object
505 :param name: the name of the keyword argument (should always be
506 "message_extractors")
507 :param value: the value of the keyword argument
508 :raise `DistutilsSetupError`: if the value is not valid
510 assert name == 'message_extractors'
511 if not isinstance(value, dict):
512 raise DistutilsSetupError('the value of the "message_extractors" '
513 'parameter must be a dictionary')
516 class init_catalog(Command):
517 """New catalog initialization command for use in ``setup.py`` scripts.
519 If correctly installed, this command is available to Setuptools-using
520 setup scripts automatically. For projects using plain old ``distutils``,
521 the command needs to be registered explicitly in ``setup.py``::
523 from babel.messages.frontend import init_catalog
527 cmdclass = {'init_catalog': init_catalog}
531 description = 'create a new catalog based on a POT file'
534 "domain of PO file (default 'messages')"),
536 'name of the input file'),
538 'path to output directory'),
539 ('output-file=', 'o',
540 "name of the output file (default "
541 "'<output_dir>/<locale>/LC_MESSAGES/<domain>.po')"),
543 'locale for the new localized catalog'),
545 'set output line width (default 76)'),
547 'do not break long message lines, longer than the output line width, '
548 'into several lines'),
550 boolean_options = ['no-wrap']
552 def initialize_options(self):
553 self.output_dir = None
554 self.output_file = None
555 self.input_file = None
557 self.domain = 'messages'
561 def finalize_options(self):
562 if not self.input_file:
563 raise DistutilsOptionError('you must specify the input file')
566 raise DistutilsOptionError('you must provide a locale for the '
569 self._locale = Locale.parse(self.locale)
570 except UnknownLocaleError as e:
571 raise DistutilsOptionError(e)
573 if not self.output_file and not self.output_dir:
574 raise DistutilsOptionError('you must specify the output directory')
575 if not self.output_file:
576 self.output_file = os.path.join(self.output_dir, self.locale,
577 'LC_MESSAGES', self.domain + '.po')
579 if not os.path.exists(os.path.dirname(self.output_file)):
580 os.makedirs(os.path.dirname(self.output_file))
581 if self.no_wrap and self.width:
582 raise DistutilsOptionError("'--no-wrap' and '--width' are mutually "
584 if not self.no_wrap and not self.width:
586 elif self.width is not None:
587 self.width = int(self.width)
591 'creating catalog %s based on %s', self.output_file, self.input_file
594 infile = open(self.input_file, 'rb')
596 # Although reading from the catalog template, read_po must be fed
597 # the locale in order to correctly calculate plurals
598 catalog = read_po(infile, locale=self.locale)
602 catalog.locale = self._locale
603 catalog.revision_date = datetime.now(LOCALTZ)
604 catalog.fuzzy = False
606 outfile = open(self.output_file, 'wb')
608 write_po(outfile, catalog, width=self.width)
613 class update_catalog(Command):
614 """Catalog merging command for use in ``setup.py`` scripts.
616 If correctly installed, this command is available to Setuptools-using
617 setup scripts automatically. For projects using plain old ``distutils``,
618 the command needs to be registered explicitly in ``setup.py``::
620 from babel.messages.frontend import update_catalog
624 cmdclass = {'update_catalog': update_catalog}
627 .. versionadded:: 0.9
630 description = 'update message catalogs from a POT file'
633 "domain of PO file (default 'messages')"),
635 'name of the input file'),
637 'path to base directory containing the catalogs'),
638 ('output-file=', 'o',
639 "name of the output file (default "
640 "'<output_dir>/<locale>/LC_MESSAGES/<domain>.po')"),
642 'locale of the catalog to compile'),
644 'set output line width (default 76)'),
646 'do not break long message lines, longer than the output line width, '
647 'into several lines'),
648 ('ignore-obsolete=', None,
649 'whether to omit obsolete messages from the output'),
650 ('no-fuzzy-matching', 'N',
651 'do not use fuzzy matching'),
652 ('update-header-comment', None,
653 'update target header comment'),
655 'keep previous msgids of translated messages')
657 boolean_options = ['no-wrap', 'ignore-obsolete', 'no-fuzzy-matching', 'previous', 'update-header-comment']
659 def initialize_options(self):
660 self.domain = 'messages'
661 self.input_file = None
662 self.output_dir = None
663 self.output_file = None
667 self.ignore_obsolete = False
668 self.no_fuzzy_matching = False
669 self.update_header_comment = False
670 self.previous = False
672 def finalize_options(self):
673 if not self.input_file:
674 raise DistutilsOptionError('you must specify the input file')
675 if not self.output_file and not self.output_dir:
676 raise DistutilsOptionError('you must specify the output file or '
678 if self.output_file and not self.locale:
679 raise DistutilsOptionError('you must specify the locale')
680 if self.no_wrap and self.width:
681 raise DistutilsOptionError("'--no-wrap' and '--width' are mutually "
683 if not self.no_wrap and not self.width:
685 elif self.width is not None:
686 self.width = int(self.width)
687 if self.no_fuzzy_matching and self.previous:
688 self.previous = False
692 if not self.output_file:
694 po_files.append((self.locale,
695 os.path.join(self.output_dir, self.locale,
697 self.domain + '.po')))
699 for locale in os.listdir(self.output_dir):
700 po_file = os.path.join(self.output_dir, locale,
703 if os.path.exists(po_file):
704 po_files.append((locale, po_file))
706 po_files.append((self.locale, self.output_file))
710 domain = os.path.splitext(os.path.basename(self.input_file))[0]
712 infile = open(self.input_file, 'rb')
714 template = read_po(infile)
719 raise DistutilsOptionError('no message catalogs found')
721 for locale, filename in po_files:
722 self.log.info('updating catalog %s based on %s', filename, self.input_file)
723 infile = open(filename, 'rb')
725 catalog = read_po(infile, locale=locale, domain=domain)
730 template, self.no_fuzzy_matching,
731 update_header_comment=self.update_header_comment
734 tmpname = os.path.join(os.path.dirname(filename),
735 tempfile.gettempprefix() +
736 os.path.basename(filename))
737 tmpfile = open(tmpname, 'wb')
740 write_po(tmpfile, catalog,
741 ignore_obsolete=self.ignore_obsolete,
742 include_previous=self.previous, width=self.width)
750 os.rename(tmpname, filename)
752 # We're probably on Windows, which doesn't support atomic
753 # renames, at least not through Python
754 # If the error is in fact due to a permissions problem, that
755 # same error is going to be raised from one of the following
758 shutil.copy(tmpname, filename)
762 class CommandLineInterface(object):
763 """Command-line interface.
765 This class provides a simple command-line interface to the message
766 extraction and PO file generation functionality.
769 usage = '%%prog %s [options] %s'
770 version = '%%prog %s' % VERSION
772 'compile': 'compile message catalogs to MO files',
773 'extract': 'extract messages from source files and generate a POT file',
774 'init': 'create new message catalogs from a POT file',
775 'update': 'update existing message catalogs from a POT file'
779 'compile': compile_catalog,
780 'extract': extract_messages,
781 'init': init_catalog,
782 'update': update_catalog,
785 log = None # Replaced on instance level
787 def run(self, argv=None):
788 """Main entry point of the command-line interface.
790 :param argv: list of arguments passed on the command-line
796 self.parser = optparse.OptionParser(usage=self.usage % ('command', '[args]'),
797 version=self.version)
798 self.parser.disable_interspersed_args()
799 self.parser.print_help = self._help
800 self.parser.add_option('--list-locales', dest='list_locales',
802 help="print all known locales and exit")
803 self.parser.add_option('-v', '--verbose', action='store_const',
804 dest='loglevel', const=logging.DEBUG,
805 help='print as much as possible')
806 self.parser.add_option('-q', '--quiet', action='store_const',
807 dest='loglevel', const=logging.ERROR,
808 help='print as little as possible')
809 self.parser.set_defaults(list_locales=False, loglevel=logging.INFO)
811 options, args = self.parser.parse_args(argv[1:])
813 self._configure_logging(options.loglevel)
814 if options.list_locales:
815 identifiers = localedata.locale_identifiers()
816 longest = max([len(identifier) for identifier in identifiers])
818 format = u'%%-%ds %%s' % (longest + 1)
819 for identifier in identifiers:
820 locale = Locale.parse(identifier)
821 output = format % (identifier, locale.english_name)
822 print(output.encode(sys.stdout.encoding or
823 getpreferredencoding() or
828 self.parser.error('no valid command or option passed. '
829 'Try the -h/--help option for more information.')
832 if cmdname not in self.commands:
833 self.parser.error('unknown command "%s"' % cmdname)
835 cmdinst = self._configure_command(cmdname, args[1:])
838 def _configure_logging(self, loglevel):
839 self.log = logging.getLogger('babel')
840 self.log.setLevel(loglevel)
841 # Don't add a new handler for every instance initialization (#227), this
842 # would cause duplicated output when the CommandLineInterface as an
843 # normal Python class.
844 if self.log.handlers:
845 handler = self.log.handlers[0]
847 handler = logging.StreamHandler()
848 self.log.addHandler(handler)
849 handler.setLevel(loglevel)
850 formatter = logging.Formatter('%(message)s')
851 handler.setFormatter(formatter)
854 print(self.parser.format_help())
856 longest = max([len(command) for command in self.commands])
857 format = " %%-%ds %%s" % max(8, longest + 1)
858 commands = sorted(self.commands.items())
859 for name, description in commands:
860 print(format % (name, description))
862 def _configure_command(self, cmdname, argv):
865 :type argv: list[str]
867 cmdclass = self.command_classes[cmdname]
870 cmdinst.log = self.log # Use our logger, not distutils'.
871 assert isinstance(cmdinst, Command)
872 cmdinst.initialize_options()
874 parser = optparse.OptionParser(
875 usage=self.usage % (cmdname, ''),
876 description=self.commands[cmdname]
878 as_args = getattr(cmdclass, "as_args", ())
879 for long, short, help in cmdclass.user_options:
880 name = long.strip("=")
881 default = getattr(cmdinst, name.replace('-', '_'))
882 strs = ["--%s" % name]
884 strs.append("-%s" % short)
885 strs.extend(cmdclass.option_aliases.get(name, ()))
887 parser.usage += "<%s>" % name
888 elif name in cmdclass.boolean_options:
889 parser.add_option(*strs, action="store_true", help=help)
890 elif name in cmdclass.multiple_value_options:
891 parser.add_option(*strs, action="append", help=help)
893 parser.add_option(*strs, help=help, default=default)
894 options, args = parser.parse_args(argv)
897 setattr(options, as_args.replace('-', '_'), args)
899 for key, value in vars(options).items():
900 setattr(cmdinst, key, value)
903 cmdinst.ensure_finalized()
904 except DistutilsOptionError as err:
905 parser.error(str(err))
911 return CommandLineInterface().run(sys.argv)
914 def parse_mapping(fileobj, filename=None):
915 """Parse an extraction method mapping from a file-like object.
917 >>> buf = StringIO('''
919 ... custom = mypackage.module:myfunc
921 ... # Python source files
924 ... # Genshi templates
925 ... [genshi: **/templates/**.html]
927 ... [genshi: **/templates/**.txt]
928 ... template_class = genshi.template:TextTemplate
929 ... encoding = latin-1
931 ... # Some custom extractor
932 ... [custom: **/custom/*.*]
935 >>> method_map, options_map = parse_mapping(buf)
941 >>> options_map['**.py']
944 ('**/templates/**.html', 'genshi')
945 >>> options_map['**/templates/**.html']['include_attrs']
948 ('**/templates/**.txt', 'genshi')
949 >>> options_map['**/templates/**.txt']['template_class']
950 'genshi.template:TextTemplate'
951 >>> options_map['**/templates/**.txt']['encoding']
955 ('**/custom/*.*', 'mypackage.module:myfunc')
956 >>> options_map['**/custom/*.*']
959 :param fileobj: a readable file-like object containing the configuration
961 :see: `extract_from_directory`
967 parser = RawConfigParser()
968 parser._sections = odict(parser._sections) # We need ordered sections
969 parser.readfp(fileobj, filename)
970 for section in parser.sections():
971 if section == 'extractors':
972 extractors = dict(parser.items(section))
974 method, pattern = [part.strip() for part in section.split(':', 1)]
975 method_map.append((pattern, method))
976 options_map[pattern] = dict(parser.items(section))
979 for idx, (pattern, method) in enumerate(method_map):
980 if method in extractors:
981 method = extractors[method]
982 method_map[idx] = (pattern, method)
984 return (method_map, options_map)
987 def parse_keywords(strings=[]):
988 """Parse keywords specifications from the given list of strings.
990 >>> kw = sorted(parse_keywords(['_', 'dgettext:2', 'dngettext:2,3', 'pgettext:1c,2']).items())
991 >>> for keyword, indices in kw:
992 ... print((keyword, indices))
995 ('dngettext', (2, 3))
996 ('pgettext', ((1, 'c'), 2))
999 for string in strings:
1001 funcname, indices = string.split(':')
1003 funcname, indices = string, None
1004 if funcname not in keywords:
1007 for x in indices.split(','):
1009 inds.append((int(x[:-1]), 'c'))
1012 indices = tuple(inds)
1013 keywords[funcname] = indices
1017 if __name__ == '__main__':