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