class Message(object):
"""Representation of a single message in a catalog."""
- def __init__(self, id, string='', locations=(), flags=()):
+ def __init__(self, id, string='', locations=(), flags=(), comments=[]):
"""Create the message object.
:param id: the message ID, or a ``(singular, plural)`` tuple for
``(singular, plural)`` tuple for pluralizable messages
:param locations: a sequence of ``(filenname, lineno)`` tuples
:param flags: a set or sequence of flags
+ :param comments: a list of comments for the msgid
"""
self.id = id
if not string and self.pluralizable:
self.flags.add('python-format')
else:
self.flags.discard('python-format')
+ self.comments = comments
def __repr__(self):
return '<%s %r>' % (type(self).__name__, self.id)
assert isinstance(message.string, (list, tuple))
self._messages[key] = message
- def add(self, id, string=None, locations=(), flags=()):
+ def add(self, id, string=None, locations=(), flags=(), comments=[]):
"""Add or update the message with the specified ID.
>>> catalog = Catalog()
``(singular, plural)`` tuple for pluralizable messages
:param locations: a sequence of ``(filenname, lineno)`` tuples
:param flags: a set or sequence of flags
+ :param comments: a list of comments for the msgid
"""
- self[id] = Message(id, string, list(locations), flags)
+ self[id] = Message(id, string, list(locations), flags, comments)
def _key_for(self, id):
"""The key for a message is just the singular ID even for pluralizable
except NameError:
from sets import Set as set
import sys
-from tokenize import generate_tokens, NAME, OP, STRING
+from tokenize import generate_tokens, NAME, OP, STRING, COMMENT
from babel.util import pathmatch, relpath
def extract_from_dir(dirname=os.getcwd(), method_map=DEFAULT_MAPPING,
options_map=None, keywords=DEFAULT_KEYWORDS,
- callback=None):
+ comments_tags=[], callback=None):
"""Extract messages from any source files found in the given directory.
This function generates tuples of the form:
options = odict
if callback:
callback(filename, method, options)
- for lineno, message in extract_from_file(method, filepath,
- keywords=keywords,
- options=options):
- yield filename, lineno, message
+ for lineno, message, comments in \
+ extract_from_file(method, filepath,
+ keywords=keywords,
+ comments_tags=comments_tags,
+ options=options):
+ yield filename, lineno, message, comments
break
def extract_from_file(method, filename, keywords=DEFAULT_KEYWORDS,
- options=None):
+ comments_tags=[], options=None):
"""Extract messages from a specific file.
This function returns a list of tuples of the form:
"""
fileobj = open(filename, 'U')
try:
- return list(extract(method, fileobj, keywords, options=options))
+ return list(extract(method, fileobj, keywords,
+ comments_tags=comments_tags, options=options))
finally:
fileobj.close()
-def extract(method, fileobj, keywords=DEFAULT_KEYWORDS, options=None):
+def extract(method, fileobj, keywords=DEFAULT_KEYWORDS, comments_tags=[],
+ options=None):
"""Extract messages from the given file-like object using the specified
extraction method.
This function returns a list of tuples of the form:
- ``(lineno, message)``
+ ``(lineno, message, comments)``
The implementation dispatches the actual extraction to plugins, based on the
value of the ``method`` parameter.
>>> from StringIO import StringIO
>>> for message in extract('python', StringIO(source)):
... print message
- (3, 'Hello, world!')
+ (3, 'Hello, world!', [])
:param method: a string specifying the extraction method (.e.g. "python")
:param fileobj: the file-like object the messages should be extracted from
that should be recognized as translation functions) to
tuples that specify which of their arguments contain
localizable strings
+ :param comments_tags: a list of translator tags to search for and include in
+ output
:param options: a dictionary of additional options (optional)
:return: the list of extracted messages
:rtype: `list`
for entry_point in working_set.iter_entry_points(GROUP_NAME, method):
func = entry_point.load(require=True)
m = []
- for lineno, funcname, messages in func(fileobj, keywords.keys(),
- options=options or {}):
+ for lineno, funcname, messages, comments in \
+ func(fileobj,
+ keywords.keys(),
+ comments_tags=comments_tags,
+ options=options or {}):
if isinstance(messages, (list, tuple)):
msgs = []
for index in keywords[funcname]:
messages = tuple(msgs)
if len(messages) == 1:
messages = messages[0]
- yield lineno, messages
+ yield lineno, messages, comments
return
raise ValueError('Unknown extraction method %r' % method)
-def extract_nothing(fileobj, keywords, options):
+def extract_nothing(fileobj, keywords, comments_tags, options):
"""Pseudo extractor that does not actually extract anything, but simply
returns an empty list.
"""
return []
-def extract_genshi(fileobj, keywords, options):
+def extract_genshi(fileobj, keywords, comments_tags, options):
"""Extract messages from Genshi templates.
:param fileobj: the file-like object the messages should be extracted from
tmpl = template_class(fileobj, filename=getattr(fileobj, 'name'),
encoding=encoding)
translator = Translator(None, ignore_tags, include_attrs)
- for message in translator.extract(tmpl.stream, gettext_functions=keywords):
- yield message
+ for lineno, func, message in translator.extract(tmpl.stream,
+ gettext_functions=keywords):
+ yield lineno, func, message, []
-def extract_python(fileobj, keywords, options):
+def extract_python(fileobj, keywords, comments_tags, options):
"""Extract messages from Python source code.
:param fileobj: the file-like object the messages should be extracted from
lineno = None
buf = []
messages = []
+ translator_comments = []
in_args = False
+ in_translator_comments = False
tokens = generate_tokens(fileobj.readline)
for tok, value, (lineno, _), _, _ in tokens:
if funcname and tok == OP and value == '(':
in_args = True
+ elif tok == COMMENT:
+ if in_translator_comments is True:
+ translator_comments.append(value[1:].strip())
+ continue
+ for comments_tag in comments_tags:
+ if comments_tag in value:
+ if in_translator_comments is not True:
+ in_translator_comments = True
+ translator_comments.append(value[1:].strip())
elif funcname and in_args:
if tok == OP and value == ')':
- in_args = False
+ in_args = in_translator_comments = False
if buf:
messages.append(''.join(buf))
del buf[:]
messages = tuple(messages)
else:
messages = messages[0]
- yield lineno, funcname, messages
+ yield lineno, funcname, messages, translator_comments
funcname = lineno = None
messages = []
+ translator_comments = []
elif tok == STRING:
if lineno is None:
lineno = stup[0]
'set report address for msgid'),
('copyright-holder=', None,
'set copyright holder in output'),
+ ('add-comments=', 'c',
+ 'place comment block with TAG (or those preceding keyword lines) in '
+ 'output file. Seperate multiple TAGs with commas(,)'),
('input-dirs=', None,
'directories that should be scanned for messages'),
]
self.sort_by_file = False
self.msgid_bugs_address = None
self.copyright_holder = None
+ self.add_comments = None
+ self._add_comments = None
def finalize_options(self):
if self.no_default_keywords and not self.keywords:
self.input_dirs = dict.fromkeys([k.split('.',1)[0]
for k in self.distribution.packages
]).keys()
+
+ if self.add_comments:
+ self._add_comments = self.add_comments.split(',')
def run(self):
mappings = self._get_mappings()
extracted = extract_from_dir(dirname, method_map, options_map,
keywords=self.keywords,
+ comments_tags=self._add_comments,
callback=callback)
- for filename, lineno, message in extracted:
+ for filename, lineno, message, comments in extracted:
filepath = os.path.normpath(os.path.join(dirname, filename))
- catalog.add(message, None, [(filepath, lineno)])
+ catalog.add(message, None, [(filepath, lineno)],
+ comments=comments)
log.info('writing PO template file to %s' % self.output_file)
write_pot(outfile, catalog, project=self.distribution.get_name(),
help='set report address for msgid')
parser.add_option('--copyright-holder', dest='copyright_holder',
help='set copyright holder in output')
+ parser.add_option('--add-comments', '-c', dest='add_comments',
+ metavar='TAG', action='append',
+ help='place comment block with TAG (or those '
+ 'preceding keyword lines) in output file. One '
+ 'TAG per argument call')
parser.set_defaults(charset='utf-8', keywords=[],
no_default_keywords=False, no_location=False,
omit_header = False, width=76, no_wrap=False,
- sort_output=False, sort_by_file=False)
+ sort_output=False, sort_by_file=False,
+ add_comments=[])
options, args = parser.parse_args(argv)
if not args:
parser.error('incorrect number of arguments')
for dirname in args:
if not os.path.isdir(dirname):
parser.error('%r is not a directory' % dirname)
- extracted = extract_from_dir(dirname, method_map, options_map,
- keywords)
- for filename, lineno, message in extracted:
+ extracted = extract_from_dir(dirname, method_map,
+ options_map, keywords,
+ comments=options.comments)
+ for filename, lineno, message, comments in extracted:
filepath = os.path.normpath(os.path.join(dirname, filename))
- catalog.add(message, None, [(filepath, lineno)])
+ catalog.add(message, None, [(filepath, lineno)],
+ comments=comments)
write_pot(outfile, catalog, width=options.width,
charset=options.charset, no_location=options.no_location,
>>> from StringIO import StringIO
>>> buf = StringIO()
>>> write_pot(buf, catalog, omit_header=True)
-
>>> print buf.getvalue()
#: main.py:1
#, fuzzy, python-format
'project': project,
'copyright_holder': _copyright_holder,
})
+
+ if message.comments:
+ for comment in message.comments:
+ for line in textwrap.wrap(comment,
+ width, break_long_words=False):
+ _write('#. %s\n' % line.strip())
if not no_location:
locs = u' '.join([u'%s:%d' % item for item in message.locations])
def test_python_format(self):
assert catalog.PYTHON_FORMAT('foo %d bar')
assert catalog.PYTHON_FORMAT('foo %s bar')
- assert catalog.PYTHON_FORMAT('foo %r bar')
+ assert catalog.PYTHON_FORMAT('foo %r bar')
+
+ def test_translator_comments(self):
+ mess = catalog.Message('foo', comments=['Comment About `foo`'])
+ self.assertEqual(mess.comments, ['Comment About `foo`'])
+ mess = catalog.Message('foo',
+ comments=['Comment 1 About `foo`',
+ 'Comment 2 About `foo`'])
+ self.assertEqual(mess.comments, ['Comment 1 About `foo`',
+ 'Comment 2 About `foo`'])
class CatalogTestCase(unittest.TestCase):
def test_unicode_string_arg(self):
buf = StringIO("msg = _(u'Foo Bar')")
- messages = list(extract.extract_python(buf, ('_',), {}))
+ messages = list(extract.extract_python(buf, ('_',), {}, []))
self.assertEqual('Foo Bar', messages[0][2])
" throw us into an infinite "
"loop\n"
msgstr ""''', buf.getvalue().strip())
+
+ def test_pot_with_translator_comments(self):
+ catalog = Catalog()
+ catalog.add(u'foo', locations=[('main.py', 1)],
+ comments=['Comment About `foo`'])
+ catalog.add(u'bar', locations=[('utils.py', 3)],
+ comments=['Comment About `bar` with',
+ 'multiple lines.'])
+ buf = StringIO()
+ pofile.write_pot(buf, catalog, omit_header=True)
+ self.assertEqual('''#. Comment About `foo`
+#: main.py:1
+msgid "foo"
+msgstr ""
+
+#. Comment About `bar` with
+#. multiple lines.
+#: utils.py:3
+msgid "bar"
+msgstr ""''', buf.getvalue().strip())
def suite():
set report address for msgid
--copyright-holder=COPYRIGHT_HOLDER
set copyright holder in output
+ -c TAG, --add-comments=TAG
+ place comment block with TAG (or those preceding
+ keyword lines) in output file. One TAG per argument
+ call
init
--sort-by-file sort output by file location (default False)
--msgid-bugs-address set report address for msgid
--copyright-holder set copyright holder in output
+ --add-comments (-c) place comment block with TAG (or those preceding
+ keyword lines) in output file. Seperate multiple TAGs
+ with commas(,)
--input-dirs directories that should be scanned for messages
usage: setup.py [global_opts] cmd1 [cmd1_opts] [cmd2 [cmd2_opts] ...]