Mercurial > babel > old > mirror
annotate 0.9.x/babel/messages/frontend.py @ 507:56ea1ec02e16 stable
Back out r553 for now.
author | jruigrok |
---|---|
date | Tue, 22 Feb 2011 16:04:56 +0000 |
parents | 4faca8f11408 |
children | cd2dec0823c9 |
rev | line source |
---|---|
263 | 1 #!/usr/bin/env python |
2 # -*- coding: utf-8 -*- | |
3 # | |
4 # Copyright (C) 2007 Edgewall Software | |
5 # All rights reserved. | |
6 # | |
7 # This software is licensed as described in the file COPYING, which | |
8 # you should have received as part of this distribution. The terms | |
9 # are also available at http://babel.edgewall.org/wiki/License. | |
10 # | |
11 # This software consists of voluntary contributions made by many | |
12 # individuals. For the exact contribution history, see the revision | |
13 # history and logs, available at http://babel.edgewall.org/log/. | |
14 | |
15 """Frontends for the message extraction functionality.""" | |
16 | |
17 from ConfigParser import RawConfigParser | |
18 from datetime import datetime | |
19 from distutils import log | |
20 from distutils.cmd import Command | |
21 from distutils.errors import DistutilsOptionError, DistutilsSetupError | |
302 | 22 from locale import getpreferredencoding |
263 | 23 import logging |
24 from optparse import OptionParser | |
25 import os | |
26 import re | |
27 import shutil | |
28 from StringIO import StringIO | |
29 import sys | |
30 import tempfile | |
31 | |
32 from babel import __version__ as VERSION | |
33 from babel import Locale, localedata | |
34 from babel.core import UnknownLocaleError | |
35 from babel.messages.catalog import Catalog | |
36 from babel.messages.extract import extract_from_dir, DEFAULT_KEYWORDS, \ | |
37 DEFAULT_MAPPING | |
38 from babel.messages.mofile import write_mo | |
39 from babel.messages.pofile import read_po, write_po | |
40 from babel.messages.plurals import PLURALS | |
41 from babel.util import odict, LOCALTZ | |
42 | |
43 __all__ = ['CommandLineInterface', 'compile_catalog', 'extract_messages', | |
44 'init_catalog', 'check_message_extractors', 'update_catalog'] | |
45 __docformat__ = 'restructuredtext en' | |
46 | |
47 | |
48 class compile_catalog(Command): | |
49 """Catalog compilation command for use in ``setup.py`` scripts. | |
50 | |
51 If correctly installed, this command is available to Setuptools-using | |
52 setup scripts automatically. For projects using plain old ``distutils``, | |
53 the command needs to be registered explicitly in ``setup.py``:: | |
54 | |
55 from babel.messages.frontend import compile_catalog | |
56 | |
57 setup( | |
58 ... | |
59 cmdclass = {'compile_catalog': compile_catalog} | |
60 ) | |
61 | |
62 :since: version 0.9 | |
63 :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_ | |
64 :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_ | |
65 """ | |
66 | |
67 description = 'compile message catalogs to binary MO files' | |
68 user_options = [ | |
69 ('domain=', 'D', | |
70 "domain of PO file (default 'messages')"), | |
71 ('directory=', 'd', | |
72 'path to base directory containing the catalogs'), | |
73 ('input-file=', 'i', | |
74 'name of the input file'), | |
75 ('output-file=', 'o', | |
76 "name of the output file (default " | |
77 "'<output_dir>/<locale>/LC_MESSAGES/<domain>.po')"), | |
78 ('locale=', 'l', | |
79 'locale of the catalog to compile'), | |
80 ('use-fuzzy', 'f', | |
81 'also include fuzzy translations'), | |
82 ('statistics', None, | |
83 'print statistics about translations') | |
84 ] | |
85 boolean_options = ['use-fuzzy', 'statistics'] | |
86 | |
87 def initialize_options(self): | |
88 self.domain = 'messages' | |
89 self.directory = None | |
90 self.input_file = None | |
91 self.output_file = None | |
92 self.locale = None | |
93 self.use_fuzzy = False | |
94 self.statistics = False | |
95 | |
96 def finalize_options(self): | |
97 if not self.input_file and not self.directory: | |
98 raise DistutilsOptionError('you must specify either the input file ' | |
99 'or the base directory') | |
100 if not self.output_file and not self.directory: | |
101 raise DistutilsOptionError('you must specify either the input file ' | |
102 'or the base directory') | |
103 | |
104 def run(self): | |
105 po_files = [] | |
106 mo_files = [] | |
107 | |
108 if not self.input_file: | |
109 if self.locale: | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
110 po_files.append((self.locale, |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
111 os.path.join(self.directory, self.locale, |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
112 'LC_MESSAGES', |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
113 self.domain + '.po'))) |
263 | 114 mo_files.append(os.path.join(self.directory, self.locale, |
115 'LC_MESSAGES', | |
116 self.domain + '.mo')) | |
117 else: | |
118 for locale in os.listdir(self.directory): | |
119 po_file = os.path.join(self.directory, locale, | |
120 'LC_MESSAGES', self.domain + '.po') | |
121 if os.path.exists(po_file): | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
122 po_files.append((locale, po_file)) |
263 | 123 mo_files.append(os.path.join(self.directory, locale, |
124 'LC_MESSAGES', | |
125 self.domain + '.mo')) | |
126 else: | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
127 po_files.append((self.locale, self.input_file)) |
263 | 128 if self.output_file: |
129 mo_files.append(self.output_file) | |
130 else: | |
291 | 131 mo_files.append(os.path.join(self.directory, self.locale, |
263 | 132 'LC_MESSAGES', |
133 self.domain + '.mo')) | |
134 | |
135 if not po_files: | |
136 raise DistutilsOptionError('no message catalogs found') | |
137 | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
138 for idx, (locale, po_file) in enumerate(po_files): |
263 | 139 mo_file = mo_files[idx] |
140 infile = open(po_file, 'r') | |
141 try: | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
142 catalog = read_po(infile, locale) |
263 | 143 finally: |
144 infile.close() | |
145 | |
146 if self.statistics: | |
147 translated = 0 | |
148 for message in list(catalog)[1:]: | |
149 if message.string: | |
150 translated +=1 | |
320
cd588918443e
Ported [335:338], [345], and [351] to 0.9.x stable branch.
cmlenz
parents:
318
diff
changeset
|
151 percentage = 0 |
cd588918443e
Ported [335:338], [345], and [351] to 0.9.x stable branch.
cmlenz
parents:
318
diff
changeset
|
152 if len(catalog): |
cd588918443e
Ported [335:338], [345], and [351] to 0.9.x stable branch.
cmlenz
parents:
318
diff
changeset
|
153 percentage = translated * 100 // len(catalog) |
263 | 154 log.info('%d of %d messages (%d%%) translated in %r', |
320
cd588918443e
Ported [335:338], [345], and [351] to 0.9.x stable branch.
cmlenz
parents:
318
diff
changeset
|
155 translated, len(catalog), percentage, po_file) |
263 | 156 |
157 if catalog.fuzzy and not self.use_fuzzy: | |
158 log.warn('catalog %r is marked as fuzzy, skipping', po_file) | |
159 continue | |
160 | |
161 for message, errors in catalog.check(): | |
162 for error in errors: | |
163 log.error('error: %s:%d: %s', po_file, message.lineno, | |
164 error) | |
165 | |
166 log.info('compiling catalog %r to %r', po_file, mo_file) | |
167 | |
282 | 168 outfile = open(mo_file, 'wb') |
263 | 169 try: |
170 write_mo(outfile, catalog, use_fuzzy=self.use_fuzzy) | |
171 finally: | |
172 outfile.close() | |
173 | |
174 | |
175 class extract_messages(Command): | |
176 """Message extraction command for use in ``setup.py`` scripts. | |
177 | |
178 If correctly installed, this command is available to Setuptools-using | |
179 setup scripts automatically. For projects using plain old ``distutils``, | |
180 the command needs to be registered explicitly in ``setup.py``:: | |
181 | |
182 from babel.messages.frontend import extract_messages | |
183 | |
184 setup( | |
185 ... | |
186 cmdclass = {'extract_messages': extract_messages} | |
187 ) | |
188 | |
189 :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_ | |
190 :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_ | |
191 """ | |
192 | |
193 description = 'extract localizable strings from the project code' | |
194 user_options = [ | |
195 ('charset=', None, | |
196 'charset to use in the output file'), | |
197 ('keywords=', 'k', | |
198 'space-separated list of keywords to look for in addition to the ' | |
199 'defaults'), | |
200 ('no-default-keywords', None, | |
201 'do not include the default keywords'), | |
202 ('mapping-file=', 'F', | |
203 'path to the mapping configuration file'), | |
204 ('no-location', None, | |
205 'do not include location comments with filename and line number'), | |
206 ('omit-header', None, | |
207 'do not include msgid "" entry in header'), | |
208 ('output-file=', 'o', | |
209 'name of the output file'), | |
210 ('width=', 'w', | |
211 'set output line width (default 76)'), | |
212 ('no-wrap', None, | |
213 'do not break long message lines, longer than the output line width, ' | |
214 'into several lines'), | |
215 ('sort-output', None, | |
216 'generate sorted output (default False)'), | |
217 ('sort-by-file', None, | |
218 'sort output by file location (default False)'), | |
219 ('msgid-bugs-address=', None, | |
220 'set report address for msgid'), | |
221 ('copyright-holder=', None, | |
222 'set copyright holder in output'), | |
223 ('add-comments=', 'c', | |
224 'place comment block with TAG (or those preceding keyword lines) in ' | |
225 'output file. Seperate multiple TAGs with commas(,)'), | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
226 ('strip-comments', None, |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
227 'strip the comment TAGs from the comments.'), |
263 | 228 ('input-dirs=', None, |
229 'directories that should be scanned for messages'), | |
230 ] | |
231 boolean_options = [ | |
232 'no-default-keywords', 'no-location', 'omit-header', 'no-wrap', | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
233 'sort-output', 'sort-by-file', 'strip-comments' |
263 | 234 ] |
235 | |
236 def initialize_options(self): | |
237 self.charset = 'utf-8' | |
238 self.keywords = '' | |
239 self._keywords = DEFAULT_KEYWORDS.copy() | |
240 self.no_default_keywords = False | |
241 self.mapping_file = None | |
242 self.no_location = False | |
243 self.omit_header = False | |
244 self.output_file = None | |
245 self.input_dirs = None | |
478 | 246 self.width = None |
263 | 247 self.no_wrap = False |
248 self.sort_output = False | |
249 self.sort_by_file = False | |
250 self.msgid_bugs_address = None | |
251 self.copyright_holder = None | |
252 self.add_comments = None | |
253 self._add_comments = [] | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
254 self.strip_comments = False |
263 | 255 |
256 def finalize_options(self): | |
257 if self.no_default_keywords and not self.keywords: | |
258 raise DistutilsOptionError('you must specify new keywords if you ' | |
259 'disable the default ones') | |
260 if self.no_default_keywords: | |
261 self._keywords = {} | |
262 if self.keywords: | |
263 self._keywords.update(parse_keywords(self.keywords.split())) | |
264 | |
265 if not self.output_file: | |
266 raise DistutilsOptionError('no output file specified') | |
267 if self.no_wrap and self.width: | |
268 raise DistutilsOptionError("'--no-wrap' and '--width' are mutually " | |
269 "exclusive") | |
478 | 270 if not self.no_wrap and not self.width: |
271 self.width = 76 | |
272 elif self.width is not None: | |
263 | 273 self.width = int(self.width) |
274 | |
275 if self.sort_output and self.sort_by_file: | |
276 raise DistutilsOptionError("'--sort-output' and '--sort-by-file' " | |
277 "are mutually exclusive") | |
278 | |
279 if not self.input_dirs: | |
280 self.input_dirs = dict.fromkeys([k.split('.',1)[0] | |
281 for k in self.distribution.packages | |
282 ]).keys() | |
283 | |
284 if self.add_comments: | |
285 self._add_comments = self.add_comments.split(',') | |
286 | |
287 def run(self): | |
288 mappings = self._get_mappings() | |
289 outfile = open(self.output_file, 'w') | |
290 try: | |
291 catalog = Catalog(project=self.distribution.get_name(), | |
292 version=self.distribution.get_version(), | |
293 msgid_bugs_address=self.msgid_bugs_address, | |
294 copyright_holder=self.copyright_holder, | |
295 charset=self.charset) | |
296 | |
297 for dirname, (method_map, options_map) in mappings.items(): | |
298 def callback(filename, method, options): | |
299 if method == 'ignore': | |
300 return | |
301 filepath = os.path.normpath(os.path.join(dirname, filename)) | |
302 optstr = '' | |
303 if options: | |
304 optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for | |
305 k, v in options.items()]) | |
306 log.info('extracting messages from %s%s', filepath, optstr) | |
307 | |
308 extracted = extract_from_dir(dirname, method_map, options_map, | |
309 keywords=self._keywords, | |
310 comment_tags=self._add_comments, | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
311 callback=callback, |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
312 strip_comment_tags= |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
313 self.strip_comments) |
263 | 314 for filename, lineno, message, comments in extracted: |
315 filepath = os.path.normpath(os.path.join(dirname, filename)) | |
316 catalog.add(message, None, [(filepath, lineno)], | |
317 auto_comments=comments) | |
318 | |
319 log.info('writing PO template file to %s' % self.output_file) | |
320 write_po(outfile, catalog, width=self.width, | |
321 no_location=self.no_location, | |
322 omit_header=self.omit_header, | |
323 sort_output=self.sort_output, | |
324 sort_by_file=self.sort_by_file) | |
325 finally: | |
326 outfile.close() | |
327 | |
328 def _get_mappings(self): | |
329 mappings = {} | |
330 | |
331 if self.mapping_file: | |
332 fileobj = open(self.mapping_file, 'U') | |
333 try: | |
334 method_map, options_map = parse_mapping(fileobj) | |
335 for dirname in self.input_dirs: | |
336 mappings[dirname] = method_map, options_map | |
337 finally: | |
338 fileobj.close() | |
339 | |
340 elif getattr(self.distribution, 'message_extractors', None): | |
341 message_extractors = self.distribution.message_extractors | |
342 for dirname, mapping in message_extractors.items(): | |
343 if isinstance(mapping, basestring): | |
344 method_map, options_map = parse_mapping(StringIO(mapping)) | |
345 else: | |
346 method_map, options_map = [], {} | |
347 for pattern, method, options in mapping: | |
348 method_map.append((pattern, method)) | |
349 options_map[pattern] = options or {} | |
350 mappings[dirname] = method_map, options_map | |
351 | |
352 else: | |
353 for dirname in self.input_dirs: | |
354 mappings[dirname] = DEFAULT_MAPPING, {} | |
355 | |
356 return mappings | |
357 | |
358 | |
359 def check_message_extractors(dist, name, value): | |
360 """Validate the ``message_extractors`` keyword argument to ``setup()``. | |
361 | |
362 :param dist: the distutils/setuptools ``Distribution`` object | |
363 :param name: the name of the keyword argument (should always be | |
364 "message_extractors") | |
365 :param value: the value of the keyword argument | |
366 :raise `DistutilsSetupError`: if the value is not valid | |
367 :see: `Adding setup() arguments | |
368 <http://peak.telecommunity.com/DevCenter/setuptools#adding-setup-arguments>`_ | |
369 """ | |
370 assert name == 'message_extractors' | |
371 if not isinstance(value, dict): | |
372 raise DistutilsSetupError('the value of the "message_extractors" ' | |
373 'parameter must be a dictionary') | |
374 | |
375 | |
376 class init_catalog(Command): | |
377 """New catalog initialization command for use in ``setup.py`` scripts. | |
378 | |
379 If correctly installed, this command is available to Setuptools-using | |
380 setup scripts automatically. For projects using plain old ``distutils``, | |
381 the command needs to be registered explicitly in ``setup.py``:: | |
382 | |
383 from babel.messages.frontend import init_catalog | |
384 | |
385 setup( | |
386 ... | |
387 cmdclass = {'init_catalog': init_catalog} | |
388 ) | |
389 | |
390 :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_ | |
391 :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_ | |
392 """ | |
393 | |
394 description = 'create a new catalog based on a POT file' | |
395 user_options = [ | |
396 ('domain=', 'D', | |
397 "domain of PO file (default 'messages')"), | |
398 ('input-file=', 'i', | |
399 'name of the input file'), | |
400 ('output-dir=', 'd', | |
401 'path to output directory'), | |
402 ('output-file=', 'o', | |
403 "name of the output file (default " | |
404 "'<output_dir>/<locale>/LC_MESSAGES/<domain>.po')"), | |
405 ('locale=', 'l', | |
406 'locale for the new localized catalog'), | |
407 ] | |
408 | |
409 def initialize_options(self): | |
410 self.output_dir = None | |
411 self.output_file = None | |
412 self.input_file = None | |
413 self.locale = None | |
414 self.domain = 'messages' | |
415 | |
416 def finalize_options(self): | |
417 if not self.input_file: | |
418 raise DistutilsOptionError('you must specify the input file') | |
419 | |
420 if not self.locale: | |
421 raise DistutilsOptionError('you must provide a locale for the ' | |
422 'new catalog') | |
423 try: | |
424 self._locale = Locale.parse(self.locale) | |
425 except UnknownLocaleError, e: | |
426 raise DistutilsOptionError(e) | |
427 | |
428 if not self.output_file and not self.output_dir: | |
429 raise DistutilsOptionError('you must specify the output directory') | |
430 if not self.output_file: | |
431 self.output_file = os.path.join(self.output_dir, self.locale, | |
432 'LC_MESSAGES', self.domain + '.po') | |
433 | |
434 if not os.path.exists(os.path.dirname(self.output_file)): | |
435 os.makedirs(os.path.dirname(self.output_file)) | |
436 | |
437 def run(self): | |
438 log.info('creating catalog %r based on %r', self.output_file, | |
439 self.input_file) | |
440 | |
441 infile = open(self.input_file, 'r') | |
442 try: | |
381 | 443 # Although reading from the catalog template, read_po must be fed |
444 # the locale in order to correcly calculate plurals | |
445 catalog = read_po(infile, locale=self.locale) | |
263 | 446 finally: |
447 infile.close() | |
448 | |
449 catalog.locale = self._locale | |
450 catalog.fuzzy = False | |
451 | |
452 outfile = open(self.output_file, 'w') | |
453 try: | |
454 write_po(outfile, catalog) | |
455 finally: | |
456 outfile.close() | |
457 | |
458 | |
459 class update_catalog(Command): | |
460 """Catalog merging command for use in ``setup.py`` scripts. | |
461 | |
462 If correctly installed, this command is available to Setuptools-using | |
463 setup scripts automatically. For projects using plain old ``distutils``, | |
464 the command needs to be registered explicitly in ``setup.py``:: | |
465 | |
466 from babel.messages.frontend import update_catalog | |
467 | |
468 setup( | |
469 ... | |
470 cmdclass = {'update_catalog': update_catalog} | |
471 ) | |
472 | |
473 :since: version 0.9 | |
474 :see: `Integrating new distutils commands <http://docs.python.org/dist/node32.html>`_ | |
475 :see: `setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_ | |
476 """ | |
477 | |
478 description = 'update message catalogs from a POT file' | |
479 user_options = [ | |
480 ('domain=', 'D', | |
481 "domain of PO file (default 'messages')"), | |
482 ('input-file=', 'i', | |
483 'name of the input file'), | |
484 ('output-dir=', 'd', | |
485 'path to base directory containing the catalogs'), | |
486 ('output-file=', 'o', | |
487 "name of the output file (default " | |
488 "'<output_dir>/<locale>/LC_MESSAGES/<domain>.po')"), | |
489 ('locale=', 'l', | |
490 'locale of the catalog to compile'), | |
491 ('ignore-obsolete=', None, | |
492 'whether to omit obsolete messages from the output'), | |
493 ('no-fuzzy-matching', 'N', | |
494 'do not use fuzzy matching'), | |
495 ('previous', None, | |
496 'keep previous msgids of translated messages') | |
497 ] | |
498 boolean_options = ['ignore_obsolete', 'no_fuzzy_matching', 'previous'] | |
499 | |
500 def initialize_options(self): | |
501 self.domain = 'messages' | |
502 self.input_file = None | |
503 self.output_dir = None | |
504 self.output_file = None | |
505 self.locale = None | |
506 self.ignore_obsolete = False | |
507 self.no_fuzzy_matching = False | |
508 self.previous = False | |
509 | |
510 def finalize_options(self): | |
511 if not self.input_file: | |
512 raise DistutilsOptionError('you must specify the input file') | |
513 if not self.output_file and not self.output_dir: | |
514 raise DistutilsOptionError('you must specify the output file or ' | |
515 'directory') | |
516 if self.output_file and not self.locale: | |
517 raise DistutilsOptionError('you must specify the locale') | |
518 if self.no_fuzzy_matching and self.previous: | |
519 self.previous = False | |
520 | |
521 def run(self): | |
522 po_files = [] | |
523 if not self.output_file: | |
524 if self.locale: | |
525 po_files.append((self.locale, | |
526 os.path.join(self.output_dir, self.locale, | |
527 'LC_MESSAGES', | |
528 self.domain + '.po'))) | |
529 else: | |
530 for locale in os.listdir(self.output_dir): | |
531 po_file = os.path.join(self.output_dir, locale, | |
532 'LC_MESSAGES', | |
533 self.domain + '.po') | |
534 if os.path.exists(po_file): | |
535 po_files.append((locale, po_file)) | |
536 else: | |
537 po_files.append((self.locale, self.output_file)) | |
538 | |
539 domain = self.domain | |
540 if not domain: | |
541 domain = os.path.splitext(os.path.basename(self.input_file))[0] | |
542 | |
543 infile = open(self.input_file, 'U') | |
544 try: | |
545 template = read_po(infile) | |
546 finally: | |
547 infile.close() | |
548 | |
549 if not po_files: | |
550 raise DistutilsOptionError('no message catalogs found') | |
551 | |
552 for locale, filename in po_files: | |
553 log.info('updating catalog %r based on %r', filename, | |
554 self.input_file) | |
555 infile = open(filename, 'U') | |
556 try: | |
557 catalog = read_po(infile, locale=locale, domain=domain) | |
558 finally: | |
559 infile.close() | |
560 | |
561 catalog.update(template, self.no_fuzzy_matching) | |
562 | |
563 tmpname = os.path.join(os.path.dirname(filename), | |
564 tempfile.gettempprefix() + | |
565 os.path.basename(filename)) | |
566 tmpfile = open(tmpname, 'w') | |
567 try: | |
568 try: | |
569 write_po(tmpfile, catalog, | |
570 ignore_obsolete=self.ignore_obsolete, | |
571 include_previous=self.previous) | |
572 finally: | |
573 tmpfile.close() | |
574 except: | |
575 os.remove(tmpname) | |
576 raise | |
577 | |
578 try: | |
579 os.rename(tmpname, filename) | |
580 except OSError: | |
581 # We're probably on Windows, which doesn't support atomic | |
582 # renames, at least not through Python | |
583 # If the error is in fact due to a permissions problem, that | |
584 # same error is going to be raised from one of the following | |
585 # operations | |
586 os.remove(filename) | |
587 shutil.copy(tmpname, filename) | |
588 os.remove(tmpname) | |
589 | |
590 | |
591 class CommandLineInterface(object): | |
592 """Command-line interface. | |
593 | |
594 This class provides a simple command-line interface to the message | |
595 extraction and PO file generation functionality. | |
596 """ | |
597 | |
598 usage = '%%prog %s [options] %s' | |
599 version = '%%prog %s' % VERSION | |
600 commands = { | |
601 'compile': 'compile message catalogs to MO files', | |
602 'extract': 'extract messages from source files and generate a POT file', | |
603 'init': 'create new message catalogs from a POT file', | |
604 'update': 'update existing message catalogs from a POT file' | |
605 } | |
606 | |
607 def run(self, argv=sys.argv): | |
608 """Main entry point of the command-line interface. | |
609 | |
610 :param argv: list of arguments passed on the command-line | |
611 """ | |
612 self.parser = OptionParser(usage=self.usage % ('command', '[args]'), | |
613 version=self.version) | |
614 self.parser.disable_interspersed_args() | |
615 self.parser.print_help = self._help | |
616 self.parser.add_option('--list-locales', dest='list_locales', | |
617 action='store_true', | |
618 help="print all known locales and exit") | |
619 self.parser.add_option('-v', '--verbose', action='store_const', | |
620 dest='loglevel', const=logging.DEBUG, | |
621 help='print as much as possible') | |
622 self.parser.add_option('-q', '--quiet', action='store_const', | |
623 dest='loglevel', const=logging.ERROR, | |
624 help='print as little as possible') | |
625 self.parser.set_defaults(list_locales=False, loglevel=logging.INFO) | |
626 | |
627 options, args = self.parser.parse_args(argv[1:]) | |
628 | |
629 # Configure logging | |
630 self.log = logging.getLogger('babel') | |
631 self.log.setLevel(options.loglevel) | |
507 | 632 handler = logging.StreamHandler() |
633 handler.setLevel(options.loglevel) | |
263 | 634 formatter = logging.Formatter('%(message)s') |
635 handler.setFormatter(formatter) | |
507 | 636 self.log.addHandler(handler) |
263 | 637 |
638 if options.list_locales: | |
639 identifiers = localedata.list() | |
640 longest = max([len(identifier) for identifier in identifiers]) | |
487 | 641 identifiers.sort() |
263 | 642 format = u'%%-%ds %%s' % (longest + 1) |
487 | 643 for identifier in identifiers: |
263 | 644 locale = Locale.parse(identifier) |
272 | 645 output = format % (identifier, locale.english_name) |
302 | 646 print output.encode(sys.stdout.encoding or |
647 getpreferredencoding() or | |
648 'ascii', 'replace') | |
263 | 649 return 0 |
650 | |
651 if not args: | |
476 | 652 self.parser.error('no valid command or option passed. ' |
653 'Try the -h/--help option for more information.') | |
263 | 654 |
655 cmdname = args[0] | |
656 if cmdname not in self.commands: | |
657 self.parser.error('unknown command "%s"' % cmdname) | |
658 | |
659 return getattr(self, cmdname)(args[1:]) | |
660 | |
661 def _help(self): | |
662 print self.parser.format_help() | |
663 print "commands:" | |
664 longest = max([len(command) for command in self.commands]) | |
665 format = " %%-%ds %%s" % max(8, longest + 1) | |
666 commands = self.commands.items() | |
667 commands.sort() | |
668 for name, description in commands: | |
669 print format % (name, description) | |
670 | |
671 def compile(self, argv): | |
672 """Subcommand for compiling a message catalog to a MO file. | |
673 | |
674 :param argv: the command arguments | |
675 :since: version 0.9 | |
676 """ | |
677 parser = OptionParser(usage=self.usage % ('compile', ''), | |
678 description=self.commands['compile']) | |
679 parser.add_option('--domain', '-D', dest='domain', | |
680 help="domain of MO and PO files (default '%default')") | |
681 parser.add_option('--directory', '-d', dest='directory', | |
682 metavar='DIR', help='base directory of catalog files') | |
683 parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', | |
684 help='locale of the catalog') | |
685 parser.add_option('--input-file', '-i', dest='input_file', | |
686 metavar='FILE', help='name of the input file') | |
687 parser.add_option('--output-file', '-o', dest='output_file', | |
688 metavar='FILE', | |
689 help="name of the output file (default " | |
690 "'<output_dir>/<locale>/LC_MESSAGES/" | |
691 "<domain>.mo')") | |
692 parser.add_option('--use-fuzzy', '-f', dest='use_fuzzy', | |
693 action='store_true', | |
694 help='also include fuzzy translations (default ' | |
695 '%default)') | |
696 parser.add_option('--statistics', dest='statistics', | |
697 action='store_true', | |
698 help='print statistics about translations') | |
699 | |
700 parser.set_defaults(domain='messages', use_fuzzy=False, | |
701 compile_all=False, statistics=False) | |
702 options, args = parser.parse_args(argv) | |
703 | |
704 po_files = [] | |
705 mo_files = [] | |
706 if not options.input_file: | |
707 if not options.directory: | |
708 parser.error('you must specify either the input file or the ' | |
709 'base directory') | |
710 if options.locale: | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
711 po_files.append((options.locale, |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
712 os.path.join(options.directory, |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
713 options.locale, 'LC_MESSAGES', |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
714 options.domain + '.po'))) |
263 | 715 mo_files.append(os.path.join(options.directory, options.locale, |
716 'LC_MESSAGES', | |
717 options.domain + '.mo')) | |
718 else: | |
719 for locale in os.listdir(options.directory): | |
720 po_file = os.path.join(options.directory, locale, | |
721 'LC_MESSAGES', options.domain + '.po') | |
722 if os.path.exists(po_file): | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
723 po_files.append((locale, po_file)) |
263 | 724 mo_files.append(os.path.join(options.directory, locale, |
725 'LC_MESSAGES', | |
726 options.domain + '.mo')) | |
727 else: | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
728 po_files.append((options.locale, options.input_file)) |
263 | 729 if options.output_file: |
730 mo_files.append(options.output_file) | |
731 else: | |
732 if not options.directory: | |
733 parser.error('you must specify either the input file or ' | |
734 'the base directory') | |
295 | 735 mo_files.append(os.path.join(options.directory, options.locale, |
263 | 736 'LC_MESSAGES', |
737 options.domain + '.mo')) | |
738 if not po_files: | |
739 parser.error('no message catalogs found') | |
740 | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
741 for idx, (locale, po_file) in enumerate(po_files): |
263 | 742 mo_file = mo_files[idx] |
743 infile = open(po_file, 'r') | |
744 try: | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
745 catalog = read_po(infile, locale) |
263 | 746 finally: |
747 infile.close() | |
748 | |
749 if options.statistics: | |
750 translated = 0 | |
751 for message in list(catalog)[1:]: | |
752 if message.string: | |
753 translated +=1 | |
320
cd588918443e
Ported [335:338], [345], and [351] to 0.9.x stable branch.
cmlenz
parents:
318
diff
changeset
|
754 percentage = 0 |
cd588918443e
Ported [335:338], [345], and [351] to 0.9.x stable branch.
cmlenz
parents:
318
diff
changeset
|
755 if len(catalog): |
cd588918443e
Ported [335:338], [345], and [351] to 0.9.x stable branch.
cmlenz
parents:
318
diff
changeset
|
756 percentage = translated * 100 // len(catalog) |
263 | 757 self.log.info("%d of %d messages (%d%%) translated in %r", |
320
cd588918443e
Ported [335:338], [345], and [351] to 0.9.x stable branch.
cmlenz
parents:
318
diff
changeset
|
758 translated, len(catalog), percentage, po_file) |
263 | 759 |
760 if catalog.fuzzy and not options.use_fuzzy: | |
761 self.log.warn('catalog %r is marked as fuzzy, skipping', | |
762 po_file) | |
763 continue | |
764 | |
765 for message, errors in catalog.check(): | |
766 for error in errors: | |
767 self.log.error('error: %s:%d: %s', po_file, message.lineno, | |
768 error) | |
769 | |
770 self.log.info('compiling catalog %r to %r', po_file, mo_file) | |
771 | |
282 | 772 outfile = open(mo_file, 'wb') |
263 | 773 try: |
774 write_mo(outfile, catalog, use_fuzzy=options.use_fuzzy) | |
775 finally: | |
776 outfile.close() | |
777 | |
778 def extract(self, argv): | |
779 """Subcommand for extracting messages from source files and generating | |
780 a POT file. | |
781 | |
782 :param argv: the command arguments | |
783 """ | |
784 parser = OptionParser(usage=self.usage % ('extract', 'dir1 <dir2> ...'), | |
785 description=self.commands['extract']) | |
786 parser.add_option('--charset', dest='charset', | |
787 help='charset to use in the output (default ' | |
788 '"%default")') | |
789 parser.add_option('-k', '--keyword', dest='keywords', action='append', | |
790 help='keywords to look for in addition to the ' | |
791 'defaults. You can specify multiple -k flags on ' | |
792 'the command line.') | |
793 parser.add_option('--no-default-keywords', dest='no_default_keywords', | |
794 action='store_true', | |
795 help="do not include the default keywords") | |
796 parser.add_option('--mapping', '-F', dest='mapping_file', | |
797 help='path to the extraction mapping file') | |
798 parser.add_option('--no-location', dest='no_location', | |
799 action='store_true', | |
800 help='do not include location comments with filename ' | |
801 'and line number') | |
802 parser.add_option('--omit-header', dest='omit_header', | |
803 action='store_true', | |
804 help='do not include msgid "" entry in header') | |
805 parser.add_option('-o', '--output', dest='output', | |
806 help='path to the output POT file') | |
807 parser.add_option('-w', '--width', dest='width', type='int', | |
478 | 808 help="set output line width (default 76)") |
263 | 809 parser.add_option('--no-wrap', dest='no_wrap', action = 'store_true', |
810 help='do not break long message lines, longer than ' | |
811 'the output line width, into several lines') | |
812 parser.add_option('--sort-output', dest='sort_output', | |
813 action='store_true', | |
814 help='generate sorted output (default False)') | |
815 parser.add_option('--sort-by-file', dest='sort_by_file', | |
816 action='store_true', | |
817 help='sort output by file location (default False)') | |
818 parser.add_option('--msgid-bugs-address', dest='msgid_bugs_address', | |
819 metavar='EMAIL@ADDRESS', | |
820 help='set report address for msgid') | |
821 parser.add_option('--copyright-holder', dest='copyright_holder', | |
822 help='set copyright holder in output') | |
484 | 823 parser.add_option('--project', dest='project', |
824 help='set project name in output') | |
825 parser.add_option('--version', dest='version', | |
826 help='set project version in output') | |
263 | 827 parser.add_option('--add-comments', '-c', dest='comment_tags', |
828 metavar='TAG', action='append', | |
829 help='place comment block with TAG (or those ' | |
830 'preceding keyword lines) in output file. One ' | |
831 'TAG per argument call') | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
832 parser.add_option('--strip-comment-tags', '-s', |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
833 dest='strip_comment_tags', action='store_true', |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
834 help='Strip the comment tags from the comments.') |
263 | 835 |
836 parser.set_defaults(charset='utf-8', keywords=[], | |
837 no_default_keywords=False, no_location=False, | |
478 | 838 omit_header = False, width=None, no_wrap=False, |
263 | 839 sort_output=False, sort_by_file=False, |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
840 comment_tags=[], strip_comment_tags=False) |
263 | 841 options, args = parser.parse_args(argv) |
842 if not args: | |
843 parser.error('incorrect number of arguments') | |
844 | |
845 if options.output not in (None, '-'): | |
846 outfile = open(options.output, 'w') | |
847 else: | |
848 outfile = sys.stdout | |
849 | |
850 keywords = DEFAULT_KEYWORDS.copy() | |
851 if options.no_default_keywords: | |
852 if not options.keywords: | |
853 parser.error('you must specify new keywords if you disable the ' | |
854 'default ones') | |
855 keywords = {} | |
856 if options.keywords: | |
857 keywords.update(parse_keywords(options.keywords)) | |
858 | |
859 if options.mapping_file: | |
860 fileobj = open(options.mapping_file, 'U') | |
861 try: | |
862 method_map, options_map = parse_mapping(fileobj) | |
863 finally: | |
864 fileobj.close() | |
865 else: | |
866 method_map = DEFAULT_MAPPING | |
867 options_map = {} | |
868 | |
869 if options.width and options.no_wrap: | |
870 parser.error("'--no-wrap' and '--width' are mutually exclusive.") | |
871 elif not options.width and not options.no_wrap: | |
872 options.width = 76 | |
873 | |
874 if options.sort_output and options.sort_by_file: | |
875 parser.error("'--sort-output' and '--sort-by-file' are mutually " | |
876 "exclusive") | |
877 | |
878 try: | |
484 | 879 catalog = Catalog(project=options.project, |
880 version=options.version, | |
881 msgid_bugs_address=options.msgid_bugs_address, | |
263 | 882 copyright_holder=options.copyright_holder, |
883 charset=options.charset) | |
884 | |
885 for dirname in args: | |
886 if not os.path.isdir(dirname): | |
887 parser.error('%r is not a directory' % dirname) | |
888 | |
889 def callback(filename, method, options): | |
890 if method == 'ignore': | |
891 return | |
892 filepath = os.path.normpath(os.path.join(dirname, filename)) | |
893 optstr = '' | |
894 if options: | |
895 optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for | |
896 k, v in options.items()]) | |
897 self.log.info('extracting messages from %s%s', filepath, | |
898 optstr) | |
899 | |
900 extracted = extract_from_dir(dirname, method_map, options_map, | |
901 keywords, options.comment_tags, | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
902 callback=callback, |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
903 strip_comment_tags= |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
904 options.strip_comment_tags) |
263 | 905 for filename, lineno, message, comments in extracted: |
906 filepath = os.path.normpath(os.path.join(dirname, filename)) | |
907 catalog.add(message, None, [(filepath, lineno)], | |
908 auto_comments=comments) | |
909 | |
910 if options.output not in (None, '-'): | |
911 self.log.info('writing PO template file to %s' % options.output) | |
912 write_po(outfile, catalog, width=options.width, | |
913 no_location=options.no_location, | |
914 omit_header=options.omit_header, | |
915 sort_output=options.sort_output, | |
916 sort_by_file=options.sort_by_file) | |
917 finally: | |
918 if options.output: | |
919 outfile.close() | |
920 | |
921 def init(self, argv): | |
922 """Subcommand for creating new message catalogs from a template. | |
923 | |
924 :param argv: the command arguments | |
925 """ | |
926 parser = OptionParser(usage=self.usage % ('init', ''), | |
927 description=self.commands['init']) | |
928 parser.add_option('--domain', '-D', dest='domain', | |
929 help="domain of PO file (default '%default')") | |
930 parser.add_option('--input-file', '-i', dest='input_file', | |
931 metavar='FILE', help='name of the input file') | |
932 parser.add_option('--output-dir', '-d', dest='output_dir', | |
933 metavar='DIR', help='path to output directory') | |
934 parser.add_option('--output-file', '-o', dest='output_file', | |
935 metavar='FILE', | |
936 help="name of the output file (default " | |
937 "'<output_dir>/<locale>/LC_MESSAGES/" | |
938 "<domain>.po')") | |
939 parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', | |
940 help='locale for the new localized catalog') | |
941 | |
942 parser.set_defaults(domain='messages') | |
943 options, args = parser.parse_args(argv) | |
944 | |
945 if not options.locale: | |
946 parser.error('you must provide a locale for the new catalog') | |
947 try: | |
948 locale = Locale.parse(options.locale) | |
949 except UnknownLocaleError, e: | |
950 parser.error(e) | |
951 | |
952 if not options.input_file: | |
953 parser.error('you must specify the input file') | |
954 | |
955 if not options.output_file and not options.output_dir: | |
956 parser.error('you must specify the output file or directory') | |
957 | |
958 if not options.output_file: | |
959 options.output_file = os.path.join(options.output_dir, | |
960 options.locale, 'LC_MESSAGES', | |
961 options.domain + '.po') | |
962 if not os.path.exists(os.path.dirname(options.output_file)): | |
963 os.makedirs(os.path.dirname(options.output_file)) | |
964 | |
965 infile = open(options.input_file, 'r') | |
966 try: | |
381 | 967 # Although reading from the catalog template, read_po must be fed |
968 # the locale in order to correcly calculate plurals | |
969 catalog = read_po(infile, locale=options.locale) | |
263 | 970 finally: |
971 infile.close() | |
972 | |
973 catalog.locale = locale | |
974 catalog.revision_date = datetime.now(LOCALTZ) | |
975 | |
976 self.log.info('creating catalog %r based on %r', options.output_file, | |
977 options.input_file) | |
978 | |
979 outfile = open(options.output_file, 'w') | |
980 try: | |
981 write_po(outfile, catalog) | |
982 finally: | |
983 outfile.close() | |
984 | |
985 def update(self, argv): | |
986 """Subcommand for updating existing message catalogs from a template. | |
987 | |
988 :param argv: the command arguments | |
989 :since: version 0.9 | |
990 """ | |
991 parser = OptionParser(usage=self.usage % ('update', ''), | |
992 description=self.commands['update']) | |
993 parser.add_option('--domain', '-D', dest='domain', | |
994 help="domain of PO file (default '%default')") | |
995 parser.add_option('--input-file', '-i', dest='input_file', | |
996 metavar='FILE', help='name of the input file') | |
997 parser.add_option('--output-dir', '-d', dest='output_dir', | |
998 metavar='DIR', help='path to output directory') | |
999 parser.add_option('--output-file', '-o', dest='output_file', | |
1000 metavar='FILE', | |
1001 help="name of the output file (default " | |
1002 "'<output_dir>/<locale>/LC_MESSAGES/" | |
1003 "<domain>.po')") | |
1004 parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', | |
1005 help='locale of the translations catalog') | |
1006 parser.add_option('--ignore-obsolete', dest='ignore_obsolete', | |
1007 action='store_true', | |
1008 help='do not include obsolete messages in the output ' | |
472 | 1009 '(default %default)') |
263 | 1010 parser.add_option('--no-fuzzy-matching', '-N', dest='no_fuzzy_matching', |
1011 action='store_true', | |
472 | 1012 help='do not use fuzzy matching (default %default)') |
263 | 1013 parser.add_option('--previous', dest='previous', action='store_true', |
1014 help='keep previous msgids of translated messages ' | |
472 | 1015 '(default %default)') |
263 | 1016 |
1017 parser.set_defaults(domain='messages', ignore_obsolete=False, | |
1018 no_fuzzy_matching=False, previous=False) | |
1019 options, args = parser.parse_args(argv) | |
1020 | |
1021 if not options.input_file: | |
1022 parser.error('you must specify the input file') | |
1023 if not options.output_file and not options.output_dir: | |
1024 parser.error('you must specify the output file or directory') | |
1025 if options.output_file and not options.locale: | |
472 | 1026 parser.error('you must specify the locale') |
263 | 1027 if options.no_fuzzy_matching and options.previous: |
1028 options.previous = False | |
1029 | |
1030 po_files = [] | |
1031 if not options.output_file: | |
1032 if options.locale: | |
1033 po_files.append((options.locale, | |
1034 os.path.join(options.output_dir, | |
1035 options.locale, 'LC_MESSAGES', | |
1036 options.domain + '.po'))) | |
1037 else: | |
1038 for locale in os.listdir(options.output_dir): | |
1039 po_file = os.path.join(options.output_dir, locale, | |
1040 'LC_MESSAGES', | |
1041 options.domain + '.po') | |
1042 if os.path.exists(po_file): | |
1043 po_files.append((locale, po_file)) | |
1044 else: | |
1045 po_files.append((options.locale, options.output_file)) | |
1046 | |
1047 domain = options.domain | |
1048 if not domain: | |
1049 domain = os.path.splitext(os.path.basename(options.input_file))[0] | |
1050 | |
1051 infile = open(options.input_file, 'U') | |
1052 try: | |
1053 template = read_po(infile) | |
1054 finally: | |
1055 infile.close() | |
1056 | |
1057 if not po_files: | |
1058 parser.error('no message catalogs found') | |
1059 | |
1060 for locale, filename in po_files: | |
1061 self.log.info('updating catalog %r based on %r', filename, | |
1062 options.input_file) | |
1063 infile = open(filename, 'U') | |
1064 try: | |
1065 catalog = read_po(infile, locale=locale, domain=domain) | |
1066 finally: | |
1067 infile.close() | |
1068 | |
1069 catalog.update(template, options.no_fuzzy_matching) | |
1070 | |
1071 tmpname = os.path.join(os.path.dirname(filename), | |
1072 tempfile.gettempprefix() + | |
1073 os.path.basename(filename)) | |
1074 tmpfile = open(tmpname, 'w') | |
1075 try: | |
1076 try: | |
1077 write_po(tmpfile, catalog, | |
1078 ignore_obsolete=options.ignore_obsolete, | |
1079 include_previous=options.previous) | |
1080 finally: | |
1081 tmpfile.close() | |
1082 except: | |
1083 os.remove(tmpname) | |
1084 raise | |
1085 | |
1086 try: | |
1087 os.rename(tmpname, filename) | |
1088 except OSError: | |
1089 # We're probably on Windows, which doesn't support atomic | |
1090 # renames, at least not through Python | |
1091 # If the error is in fact due to a permissions problem, that | |
1092 # same error is going to be raised from one of the following | |
1093 # operations | |
1094 os.remove(filename) | |
1095 shutil.copy(tmpname, filename) | |
1096 os.remove(tmpname) | |
1097 | |
1098 | |
1099 def main(): | |
1100 return CommandLineInterface().run(sys.argv) | |
1101 | |
1102 def parse_mapping(fileobj, filename=None): | |
1103 """Parse an extraction method mapping from a file-like object. | |
1104 | |
1105 >>> buf = StringIO(''' | |
1106 ... [extractors] | |
1107 ... custom = mypackage.module:myfunc | |
1108 ... | |
1109 ... # Python source files | |
1110 ... [python: **.py] | |
1111 ... | |
1112 ... # Genshi templates | |
1113 ... [genshi: **/templates/**.html] | |
1114 ... include_attrs = | |
1115 ... [genshi: **/templates/**.txt] | |
1116 ... template_class = genshi.template:TextTemplate | |
1117 ... encoding = latin-1 | |
1118 ... | |
1119 ... # Some custom extractor | |
1120 ... [custom: **/custom/*.*] | |
1121 ... ''') | |
1122 | |
1123 >>> method_map, options_map = parse_mapping(buf) | |
1124 >>> len(method_map) | |
1125 4 | |
1126 | |
1127 >>> method_map[0] | |
1128 ('**.py', 'python') | |
1129 >>> options_map['**.py'] | |
1130 {} | |
1131 >>> method_map[1] | |
1132 ('**/templates/**.html', 'genshi') | |
1133 >>> options_map['**/templates/**.html']['include_attrs'] | |
1134 '' | |
1135 >>> method_map[2] | |
1136 ('**/templates/**.txt', 'genshi') | |
1137 >>> options_map['**/templates/**.txt']['template_class'] | |
1138 'genshi.template:TextTemplate' | |
1139 >>> options_map['**/templates/**.txt']['encoding'] | |
1140 'latin-1' | |
1141 | |
1142 >>> method_map[3] | |
1143 ('**/custom/*.*', 'mypackage.module:myfunc') | |
1144 >>> options_map['**/custom/*.*'] | |
1145 {} | |
1146 | |
1147 :param fileobj: a readable file-like object containing the configuration | |
1148 text to parse | |
1149 :return: a `(method_map, options_map)` tuple | |
1150 :rtype: `tuple` | |
1151 :see: `extract_from_directory` | |
1152 """ | |
1153 extractors = {} | |
1154 method_map = [] | |
1155 options_map = {} | |
1156 | |
1157 parser = RawConfigParser() | |
1158 parser._sections = odict(parser._sections) # We need ordered sections | |
1159 parser.readfp(fileobj, filename) | |
1160 for section in parser.sections(): | |
1161 if section == 'extractors': | |
1162 extractors = dict(parser.items(section)) | |
1163 else: | |
1164 method, pattern = [part.strip() for part in section.split(':', 1)] | |
1165 method_map.append((pattern, method)) | |
1166 options_map[pattern] = dict(parser.items(section)) | |
1167 | |
1168 if extractors: | |
1169 for idx, (pattern, method) in enumerate(method_map): | |
1170 if method in extractors: | |
1171 method = extractors[method] | |
1172 method_map[idx] = (pattern, method) | |
1173 | |
1174 return (method_map, options_map) | |
1175 | |
1176 def parse_keywords(strings=[]): | |
1177 """Parse keywords specifications from the given list of strings. | |
1178 | |
1179 >>> kw = parse_keywords(['_', 'dgettext:2', 'dngettext:2,3']) | |
1180 >>> for keyword, indices in sorted(kw.items()): | |
1181 ... print (keyword, indices) | |
1182 ('_', None) | |
1183 ('dgettext', (2,)) | |
1184 ('dngettext', (2, 3)) | |
1185 """ | |
1186 keywords = {} | |
1187 for string in strings: | |
1188 if ':' in string: | |
1189 funcname, indices = string.split(':') | |
1190 else: | |
1191 funcname, indices = string, None | |
1192 if funcname not in keywords: | |
1193 if indices: | |
1194 indices = tuple([(int(x)) for x in indices.split(',')]) | |
1195 keywords[funcname] = indices | |
1196 return keywords | |
1197 | |
1198 | |
1199 if __name__ == '__main__': | |
1200 main() |