Mercurial > babel > old > mirror
annotate 0.9.x/babel/messages/frontend.py @ 526:22ad1d9936e7 stable
merge r573, r575 from trunk to 0.9 branch
author | fschwarz |
---|---|
date | Sat, 05 Mar 2011 14:58:58 +0000 |
parents | cd2dec0823c9 |
children |
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 | |
526 | 629 self._configure_logging(options.loglevel) |
263 | 630 if options.list_locales: |
631 identifiers = localedata.list() | |
632 longest = max([len(identifier) for identifier in identifiers]) | |
487 | 633 identifiers.sort() |
263 | 634 format = u'%%-%ds %%s' % (longest + 1) |
487 | 635 for identifier in identifiers: |
263 | 636 locale = Locale.parse(identifier) |
272 | 637 output = format % (identifier, locale.english_name) |
302 | 638 print output.encode(sys.stdout.encoding or |
639 getpreferredencoding() or | |
640 'ascii', 'replace') | |
263 | 641 return 0 |
642 | |
643 if not args: | |
476 | 644 self.parser.error('no valid command or option passed. ' |
645 'Try the -h/--help option for more information.') | |
263 | 646 |
647 cmdname = args[0] | |
648 if cmdname not in self.commands: | |
649 self.parser.error('unknown command "%s"' % cmdname) | |
650 | |
651 return getattr(self, cmdname)(args[1:]) | |
652 | |
526 | 653 def _configure_logging(self, loglevel): |
654 self.log = logging.getLogger('babel') | |
655 self.log.setLevel(loglevel) | |
656 # Don't add a new handler for every instance initialization (#227), this | |
657 # would cause duplicated output when the CommandLineInterface as an | |
658 # normal Python class. | |
659 if self.log.handlers: | |
660 handler = self.log.handlers[0] | |
661 else: | |
662 handler = logging.StreamHandler() | |
663 self.log.addHandler(handler) | |
664 handler.setLevel(loglevel) | |
665 formatter = logging.Formatter('%(message)s') | |
666 handler.setFormatter(formatter) | |
667 | |
263 | 668 def _help(self): |
669 print self.parser.format_help() | |
670 print "commands:" | |
671 longest = max([len(command) for command in self.commands]) | |
672 format = " %%-%ds %%s" % max(8, longest + 1) | |
673 commands = self.commands.items() | |
674 commands.sort() | |
675 for name, description in commands: | |
676 print format % (name, description) | |
677 | |
678 def compile(self, argv): | |
679 """Subcommand for compiling a message catalog to a MO file. | |
680 | |
681 :param argv: the command arguments | |
682 :since: version 0.9 | |
683 """ | |
684 parser = OptionParser(usage=self.usage % ('compile', ''), | |
685 description=self.commands['compile']) | |
686 parser.add_option('--domain', '-D', dest='domain', | |
687 help="domain of MO and PO files (default '%default')") | |
688 parser.add_option('--directory', '-d', dest='directory', | |
689 metavar='DIR', help='base directory of catalog files') | |
690 parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', | |
691 help='locale of the catalog') | |
692 parser.add_option('--input-file', '-i', dest='input_file', | |
693 metavar='FILE', help='name of the input file') | |
694 parser.add_option('--output-file', '-o', dest='output_file', | |
695 metavar='FILE', | |
696 help="name of the output file (default " | |
697 "'<output_dir>/<locale>/LC_MESSAGES/" | |
698 "<domain>.mo')") | |
699 parser.add_option('--use-fuzzy', '-f', dest='use_fuzzy', | |
700 action='store_true', | |
701 help='also include fuzzy translations (default ' | |
702 '%default)') | |
703 parser.add_option('--statistics', dest='statistics', | |
704 action='store_true', | |
705 help='print statistics about translations') | |
706 | |
707 parser.set_defaults(domain='messages', use_fuzzy=False, | |
708 compile_all=False, statistics=False) | |
709 options, args = parser.parse_args(argv) | |
710 | |
711 po_files = [] | |
712 mo_files = [] | |
713 if not options.input_file: | |
714 if not options.directory: | |
715 parser.error('you must specify either the input file or the ' | |
716 'base directory') | |
717 if options.locale: | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
718 po_files.append((options.locale, |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
719 os.path.join(options.directory, |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
720 options.locale, 'LC_MESSAGES', |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
721 options.domain + '.po'))) |
263 | 722 mo_files.append(os.path.join(options.directory, options.locale, |
723 'LC_MESSAGES', | |
724 options.domain + '.mo')) | |
725 else: | |
726 for locale in os.listdir(options.directory): | |
727 po_file = os.path.join(options.directory, locale, | |
728 'LC_MESSAGES', options.domain + '.po') | |
729 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
|
730 po_files.append((locale, po_file)) |
263 | 731 mo_files.append(os.path.join(options.directory, locale, |
732 'LC_MESSAGES', | |
733 options.domain + '.mo')) | |
734 else: | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
735 po_files.append((options.locale, options.input_file)) |
263 | 736 if options.output_file: |
737 mo_files.append(options.output_file) | |
738 else: | |
739 if not options.directory: | |
740 parser.error('you must specify either the input file or ' | |
741 'the base directory') | |
295 | 742 mo_files.append(os.path.join(options.directory, options.locale, |
263 | 743 'LC_MESSAGES', |
744 options.domain + '.mo')) | |
745 if not po_files: | |
746 parser.error('no message catalogs found') | |
747 | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
748 for idx, (locale, po_file) in enumerate(po_files): |
263 | 749 mo_file = mo_files[idx] |
750 infile = open(po_file, 'r') | |
751 try: | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
752 catalog = read_po(infile, locale) |
263 | 753 finally: |
754 infile.close() | |
755 | |
756 if options.statistics: | |
757 translated = 0 | |
758 for message in list(catalog)[1:]: | |
759 if message.string: | |
760 translated +=1 | |
320
cd588918443e
Ported [335:338], [345], and [351] to 0.9.x stable branch.
cmlenz
parents:
318
diff
changeset
|
761 percentage = 0 |
cd588918443e
Ported [335:338], [345], and [351] to 0.9.x stable branch.
cmlenz
parents:
318
diff
changeset
|
762 if len(catalog): |
cd588918443e
Ported [335:338], [345], and [351] to 0.9.x stable branch.
cmlenz
parents:
318
diff
changeset
|
763 percentage = translated * 100 // len(catalog) |
263 | 764 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
|
765 translated, len(catalog), percentage, po_file) |
263 | 766 |
767 if catalog.fuzzy and not options.use_fuzzy: | |
768 self.log.warn('catalog %r is marked as fuzzy, skipping', | |
769 po_file) | |
770 continue | |
771 | |
772 for message, errors in catalog.check(): | |
773 for error in errors: | |
774 self.log.error('error: %s:%d: %s', po_file, message.lineno, | |
775 error) | |
776 | |
777 self.log.info('compiling catalog %r to %r', po_file, mo_file) | |
778 | |
282 | 779 outfile = open(mo_file, 'wb') |
263 | 780 try: |
781 write_mo(outfile, catalog, use_fuzzy=options.use_fuzzy) | |
782 finally: | |
783 outfile.close() | |
784 | |
785 def extract(self, argv): | |
786 """Subcommand for extracting messages from source files and generating | |
787 a POT file. | |
788 | |
789 :param argv: the command arguments | |
790 """ | |
791 parser = OptionParser(usage=self.usage % ('extract', 'dir1 <dir2> ...'), | |
792 description=self.commands['extract']) | |
793 parser.add_option('--charset', dest='charset', | |
794 help='charset to use in the output (default ' | |
795 '"%default")') | |
796 parser.add_option('-k', '--keyword', dest='keywords', action='append', | |
797 help='keywords to look for in addition to the ' | |
798 'defaults. You can specify multiple -k flags on ' | |
799 'the command line.') | |
800 parser.add_option('--no-default-keywords', dest='no_default_keywords', | |
801 action='store_true', | |
802 help="do not include the default keywords") | |
803 parser.add_option('--mapping', '-F', dest='mapping_file', | |
804 help='path to the extraction mapping file') | |
805 parser.add_option('--no-location', dest='no_location', | |
806 action='store_true', | |
807 help='do not include location comments with filename ' | |
808 'and line number') | |
809 parser.add_option('--omit-header', dest='omit_header', | |
810 action='store_true', | |
811 help='do not include msgid "" entry in header') | |
812 parser.add_option('-o', '--output', dest='output', | |
813 help='path to the output POT file') | |
814 parser.add_option('-w', '--width', dest='width', type='int', | |
478 | 815 help="set output line width (default 76)") |
263 | 816 parser.add_option('--no-wrap', dest='no_wrap', action = 'store_true', |
817 help='do not break long message lines, longer than ' | |
818 'the output line width, into several lines') | |
819 parser.add_option('--sort-output', dest='sort_output', | |
820 action='store_true', | |
821 help='generate sorted output (default False)') | |
822 parser.add_option('--sort-by-file', dest='sort_by_file', | |
823 action='store_true', | |
824 help='sort output by file location (default False)') | |
825 parser.add_option('--msgid-bugs-address', dest='msgid_bugs_address', | |
826 metavar='EMAIL@ADDRESS', | |
827 help='set report address for msgid') | |
828 parser.add_option('--copyright-holder', dest='copyright_holder', | |
829 help='set copyright holder in output') | |
484 | 830 parser.add_option('--project', dest='project', |
831 help='set project name in output') | |
832 parser.add_option('--version', dest='version', | |
833 help='set project version in output') | |
263 | 834 parser.add_option('--add-comments', '-c', dest='comment_tags', |
835 metavar='TAG', action='append', | |
836 help='place comment block with TAG (or those ' | |
837 'preceding keyword lines) in output file. One ' | |
838 'TAG per argument call') | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
839 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
|
840 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
|
841 help='Strip the comment tags from the comments.') |
263 | 842 |
843 parser.set_defaults(charset='utf-8', keywords=[], | |
844 no_default_keywords=False, no_location=False, | |
478 | 845 omit_header = False, width=None, no_wrap=False, |
263 | 846 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
|
847 comment_tags=[], strip_comment_tags=False) |
263 | 848 options, args = parser.parse_args(argv) |
849 if not args: | |
850 parser.error('incorrect number of arguments') | |
851 | |
852 if options.output not in (None, '-'): | |
853 outfile = open(options.output, 'w') | |
854 else: | |
855 outfile = sys.stdout | |
856 | |
857 keywords = DEFAULT_KEYWORDS.copy() | |
858 if options.no_default_keywords: | |
859 if not options.keywords: | |
860 parser.error('you must specify new keywords if you disable the ' | |
861 'default ones') | |
862 keywords = {} | |
863 if options.keywords: | |
864 keywords.update(parse_keywords(options.keywords)) | |
865 | |
866 if options.mapping_file: | |
867 fileobj = open(options.mapping_file, 'U') | |
868 try: | |
869 method_map, options_map = parse_mapping(fileobj) | |
870 finally: | |
871 fileobj.close() | |
872 else: | |
873 method_map = DEFAULT_MAPPING | |
874 options_map = {} | |
875 | |
876 if options.width and options.no_wrap: | |
877 parser.error("'--no-wrap' and '--width' are mutually exclusive.") | |
878 elif not options.width and not options.no_wrap: | |
879 options.width = 76 | |
880 | |
881 if options.sort_output and options.sort_by_file: | |
882 parser.error("'--sort-output' and '--sort-by-file' are mutually " | |
883 "exclusive") | |
884 | |
885 try: | |
484 | 886 catalog = Catalog(project=options.project, |
887 version=options.version, | |
888 msgid_bugs_address=options.msgid_bugs_address, | |
263 | 889 copyright_holder=options.copyright_holder, |
890 charset=options.charset) | |
891 | |
892 for dirname in args: | |
893 if not os.path.isdir(dirname): | |
894 parser.error('%r is not a directory' % dirname) | |
895 | |
896 def callback(filename, method, options): | |
897 if method == 'ignore': | |
898 return | |
899 filepath = os.path.normpath(os.path.join(dirname, filename)) | |
900 optstr = '' | |
901 if options: | |
902 optstr = ' (%s)' % ', '.join(['%s="%s"' % (k, v) for | |
903 k, v in options.items()]) | |
904 self.log.info('extracting messages from %s%s', filepath, | |
905 optstr) | |
906 | |
907 extracted = extract_from_dir(dirname, method_map, options_map, | |
908 keywords, options.comment_tags, | |
348
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
909 callback=callback, |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
910 strip_comment_tags= |
05975a0e7021
Merged revisions [358:360], [364:370], [373:378], [380:382] from [source:trunk].
cmlenz
parents:
320
diff
changeset
|
911 options.strip_comment_tags) |
263 | 912 for filename, lineno, message, comments in extracted: |
913 filepath = os.path.normpath(os.path.join(dirname, filename)) | |
914 catalog.add(message, None, [(filepath, lineno)], | |
915 auto_comments=comments) | |
916 | |
917 if options.output not in (None, '-'): | |
918 self.log.info('writing PO template file to %s' % options.output) | |
919 write_po(outfile, catalog, width=options.width, | |
920 no_location=options.no_location, | |
921 omit_header=options.omit_header, | |
922 sort_output=options.sort_output, | |
923 sort_by_file=options.sort_by_file) | |
924 finally: | |
925 if options.output: | |
926 outfile.close() | |
927 | |
928 def init(self, argv): | |
929 """Subcommand for creating new message catalogs from a template. | |
930 | |
931 :param argv: the command arguments | |
932 """ | |
933 parser = OptionParser(usage=self.usage % ('init', ''), | |
934 description=self.commands['init']) | |
935 parser.add_option('--domain', '-D', dest='domain', | |
936 help="domain of PO file (default '%default')") | |
937 parser.add_option('--input-file', '-i', dest='input_file', | |
938 metavar='FILE', help='name of the input file') | |
939 parser.add_option('--output-dir', '-d', dest='output_dir', | |
940 metavar='DIR', help='path to output directory') | |
941 parser.add_option('--output-file', '-o', dest='output_file', | |
942 metavar='FILE', | |
943 help="name of the output file (default " | |
944 "'<output_dir>/<locale>/LC_MESSAGES/" | |
945 "<domain>.po')") | |
946 parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', | |
947 help='locale for the new localized catalog') | |
948 | |
949 parser.set_defaults(domain='messages') | |
950 options, args = parser.parse_args(argv) | |
951 | |
952 if not options.locale: | |
953 parser.error('you must provide a locale for the new catalog') | |
954 try: | |
955 locale = Locale.parse(options.locale) | |
956 except UnknownLocaleError, e: | |
957 parser.error(e) | |
958 | |
959 if not options.input_file: | |
960 parser.error('you must specify the input file') | |
961 | |
962 if not options.output_file and not options.output_dir: | |
963 parser.error('you must specify the output file or directory') | |
964 | |
965 if not options.output_file: | |
966 options.output_file = os.path.join(options.output_dir, | |
967 options.locale, 'LC_MESSAGES', | |
968 options.domain + '.po') | |
969 if not os.path.exists(os.path.dirname(options.output_file)): | |
970 os.makedirs(os.path.dirname(options.output_file)) | |
971 | |
972 infile = open(options.input_file, 'r') | |
973 try: | |
381 | 974 # Although reading from the catalog template, read_po must be fed |
975 # the locale in order to correcly calculate plurals | |
976 catalog = read_po(infile, locale=options.locale) | |
263 | 977 finally: |
978 infile.close() | |
979 | |
980 catalog.locale = locale | |
981 catalog.revision_date = datetime.now(LOCALTZ) | |
982 | |
983 self.log.info('creating catalog %r based on %r', options.output_file, | |
984 options.input_file) | |
985 | |
986 outfile = open(options.output_file, 'w') | |
987 try: | |
988 write_po(outfile, catalog) | |
989 finally: | |
990 outfile.close() | |
991 | |
992 def update(self, argv): | |
993 """Subcommand for updating existing message catalogs from a template. | |
994 | |
995 :param argv: the command arguments | |
996 :since: version 0.9 | |
997 """ | |
998 parser = OptionParser(usage=self.usage % ('update', ''), | |
999 description=self.commands['update']) | |
1000 parser.add_option('--domain', '-D', dest='domain', | |
1001 help="domain of PO file (default '%default')") | |
1002 parser.add_option('--input-file', '-i', dest='input_file', | |
1003 metavar='FILE', help='name of the input file') | |
1004 parser.add_option('--output-dir', '-d', dest='output_dir', | |
1005 metavar='DIR', help='path to output directory') | |
1006 parser.add_option('--output-file', '-o', dest='output_file', | |
1007 metavar='FILE', | |
1008 help="name of the output file (default " | |
1009 "'<output_dir>/<locale>/LC_MESSAGES/" | |
1010 "<domain>.po')") | |
1011 parser.add_option('--locale', '-l', dest='locale', metavar='LOCALE', | |
1012 help='locale of the translations catalog') | |
1013 parser.add_option('--ignore-obsolete', dest='ignore_obsolete', | |
1014 action='store_true', | |
1015 help='do not include obsolete messages in the output ' | |
472 | 1016 '(default %default)') |
263 | 1017 parser.add_option('--no-fuzzy-matching', '-N', dest='no_fuzzy_matching', |
1018 action='store_true', | |
472 | 1019 help='do not use fuzzy matching (default %default)') |
263 | 1020 parser.add_option('--previous', dest='previous', action='store_true', |
1021 help='keep previous msgids of translated messages ' | |
472 | 1022 '(default %default)') |
263 | 1023 |
1024 parser.set_defaults(domain='messages', ignore_obsolete=False, | |
1025 no_fuzzy_matching=False, previous=False) | |
1026 options, args = parser.parse_args(argv) | |
1027 | |
1028 if not options.input_file: | |
1029 parser.error('you must specify the input file') | |
1030 if not options.output_file and not options.output_dir: | |
1031 parser.error('you must specify the output file or directory') | |
1032 if options.output_file and not options.locale: | |
472 | 1033 parser.error('you must specify the locale') |
263 | 1034 if options.no_fuzzy_matching and options.previous: |
1035 options.previous = False | |
1036 | |
1037 po_files = [] | |
1038 if not options.output_file: | |
1039 if options.locale: | |
1040 po_files.append((options.locale, | |
1041 os.path.join(options.output_dir, | |
1042 options.locale, 'LC_MESSAGES', | |
1043 options.domain + '.po'))) | |
1044 else: | |
1045 for locale in os.listdir(options.output_dir): | |
1046 po_file = os.path.join(options.output_dir, locale, | |
1047 'LC_MESSAGES', | |
1048 options.domain + '.po') | |
1049 if os.path.exists(po_file): | |
1050 po_files.append((locale, po_file)) | |
1051 else: | |
1052 po_files.append((options.locale, options.output_file)) | |
1053 | |
1054 domain = options.domain | |
1055 if not domain: | |
1056 domain = os.path.splitext(os.path.basename(options.input_file))[0] | |
1057 | |
1058 infile = open(options.input_file, 'U') | |
1059 try: | |
1060 template = read_po(infile) | |
1061 finally: | |
1062 infile.close() | |
1063 | |
1064 if not po_files: | |
1065 parser.error('no message catalogs found') | |
1066 | |
1067 for locale, filename in po_files: | |
1068 self.log.info('updating catalog %r based on %r', filename, | |
1069 options.input_file) | |
1070 infile = open(filename, 'U') | |
1071 try: | |
1072 catalog = read_po(infile, locale=locale, domain=domain) | |
1073 finally: | |
1074 infile.close() | |
1075 | |
1076 catalog.update(template, options.no_fuzzy_matching) | |
1077 | |
1078 tmpname = os.path.join(os.path.dirname(filename), | |
1079 tempfile.gettempprefix() + | |
1080 os.path.basename(filename)) | |
1081 tmpfile = open(tmpname, 'w') | |
1082 try: | |
1083 try: | |
1084 write_po(tmpfile, catalog, | |
1085 ignore_obsolete=options.ignore_obsolete, | |
1086 include_previous=options.previous) | |
1087 finally: | |
1088 tmpfile.close() | |
1089 except: | |
1090 os.remove(tmpname) | |
1091 raise | |
1092 | |
1093 try: | |
1094 os.rename(tmpname, filename) | |
1095 except OSError: | |
1096 # We're probably on Windows, which doesn't support atomic | |
1097 # renames, at least not through Python | |
1098 # If the error is in fact due to a permissions problem, that | |
1099 # same error is going to be raised from one of the following | |
1100 # operations | |
1101 os.remove(filename) | |
1102 shutil.copy(tmpname, filename) | |
1103 os.remove(tmpname) | |
1104 | |
1105 | |
1106 def main(): | |
1107 return CommandLineInterface().run(sys.argv) | |
1108 | |
1109 def parse_mapping(fileobj, filename=None): | |
1110 """Parse an extraction method mapping from a file-like object. | |
1111 | |
1112 >>> buf = StringIO(''' | |
1113 ... [extractors] | |
1114 ... custom = mypackage.module:myfunc | |
1115 ... | |
1116 ... # Python source files | |
1117 ... [python: **.py] | |
1118 ... | |
1119 ... # Genshi templates | |
1120 ... [genshi: **/templates/**.html] | |
1121 ... include_attrs = | |
1122 ... [genshi: **/templates/**.txt] | |
1123 ... template_class = genshi.template:TextTemplate | |
1124 ... encoding = latin-1 | |
1125 ... | |
1126 ... # Some custom extractor | |
1127 ... [custom: **/custom/*.*] | |
1128 ... ''') | |
1129 | |
1130 >>> method_map, options_map = parse_mapping(buf) | |
1131 >>> len(method_map) | |
1132 4 | |
1133 | |
1134 >>> method_map[0] | |
1135 ('**.py', 'python') | |
1136 >>> options_map['**.py'] | |
1137 {} | |
1138 >>> method_map[1] | |
1139 ('**/templates/**.html', 'genshi') | |
1140 >>> options_map['**/templates/**.html']['include_attrs'] | |
1141 '' | |
1142 >>> method_map[2] | |
1143 ('**/templates/**.txt', 'genshi') | |
1144 >>> options_map['**/templates/**.txt']['template_class'] | |
1145 'genshi.template:TextTemplate' | |
1146 >>> options_map['**/templates/**.txt']['encoding'] | |
1147 'latin-1' | |
1148 | |
1149 >>> method_map[3] | |
1150 ('**/custom/*.*', 'mypackage.module:myfunc') | |
1151 >>> options_map['**/custom/*.*'] | |
1152 {} | |
1153 | |
1154 :param fileobj: a readable file-like object containing the configuration | |
1155 text to parse | |
1156 :return: a `(method_map, options_map)` tuple | |
1157 :rtype: `tuple` | |
1158 :see: `extract_from_directory` | |
1159 """ | |
1160 extractors = {} | |
1161 method_map = [] | |
1162 options_map = {} | |
1163 | |
1164 parser = RawConfigParser() | |
1165 parser._sections = odict(parser._sections) # We need ordered sections | |
1166 parser.readfp(fileobj, filename) | |
1167 for section in parser.sections(): | |
1168 if section == 'extractors': | |
1169 extractors = dict(parser.items(section)) | |
1170 else: | |
1171 method, pattern = [part.strip() for part in section.split(':', 1)] | |
1172 method_map.append((pattern, method)) | |
1173 options_map[pattern] = dict(parser.items(section)) | |
1174 | |
1175 if extractors: | |
1176 for idx, (pattern, method) in enumerate(method_map): | |
1177 if method in extractors: | |
1178 method = extractors[method] | |
1179 method_map[idx] = (pattern, method) | |
1180 | |
1181 return (method_map, options_map) | |
1182 | |
1183 def parse_keywords(strings=[]): | |
1184 """Parse keywords specifications from the given list of strings. | |
1185 | |
509
cd2dec0823c9
Python 2.3 compatibility: backporting r456 and r457 to 0.9 branch (see #233)
fschwarz
parents:
507
diff
changeset
|
1186 >>> kw = parse_keywords(['_', 'dgettext:2', 'dngettext:2,3']).items() |
cd2dec0823c9
Python 2.3 compatibility: backporting r456 and r457 to 0.9 branch (see #233)
fschwarz
parents:
507
diff
changeset
|
1187 >>> kw.sort() |
cd2dec0823c9
Python 2.3 compatibility: backporting r456 and r457 to 0.9 branch (see #233)
fschwarz
parents:
507
diff
changeset
|
1188 >>> for keyword, indices in kw: |
263 | 1189 ... print (keyword, indices) |
1190 ('_', None) | |
1191 ('dgettext', (2,)) | |
1192 ('dngettext', (2, 3)) | |
1193 """ | |
1194 keywords = {} | |
1195 for string in strings: | |
1196 if ':' in string: | |
1197 funcname, indices = string.split(':') | |
1198 else: | |
1199 funcname, indices = string, None | |
1200 if funcname not in keywords: | |
1201 if indices: | |
1202 indices = tuple([(int(x)) for x in indices.split(',')]) | |
1203 keywords[funcname] = indices | |
1204 return keywords | |
1205 | |
1206 | |
1207 if __name__ == '__main__': | |
1208 main() |