Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 40 additions & 29 deletions babel/messages/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,23 @@
import sys
from tokenize import generate_tokens, COMMENT, NAME, OP, STRING

from babel.util import parse_encoding, pathmatch, relpath
from babel.util import parse_encoding, pathmatch, relpath, len_recurse
from babel._compat import PY2, text_type
from textwrap import dedent


GROUP_NAME = 'babel.extractors'

DEFAULT_KEYWORDS = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the format of the keywords dict should be documented.

In addition, it's a backward-incompatible change for client apps that pass keywords as code, so it should probably support the old format as well as the new one.

'_': None,
'gettext': None,
'ngettext': (1, 2),
'ugettext': None,
'ungettext': (1, 2),
'dgettext': (2,),
'dngettext': (2, 3),
'N_': None,
'pgettext': ((1, 'c'), 2)
'_': (None,),
'gettext': (None,),
'ngettext': ((1, 2),),
'ugettext': (None,),
'ungettext': ((1, 2),),
'dgettext': ((2,),),
'dngettext': ((2, 3),),
'N_': (None,),
'pgettext': (((1, 'c'), 2,),)
}

DEFAULT_MAPPING = [('**.py', 'python')]
Expand Down Expand Up @@ -266,34 +266,45 @@ def extract(method, fileobj, keywords=DEFAULT_KEYWORDS, comment_tags=(),

for lineno, funcname, messages, comments in results:
if funcname:
spec = keywords[funcname] or (1,)
specs = keywords[funcname] or ((1,), )
else:
spec = (1,)
specs = ((1,), )
if not isinstance(messages, (list, tuple)):
messages = [messages]
if not messages:
continue

# Validate the messages against the keyword's specification
context = None
msgs = []
invalid = False
# last_index is 1 based like the keyword spec
last_index = len(messages)
for index in spec:
if isinstance(index, tuple):
context = messages[index[0] - 1]
continue
if last_index < index:
# Not enough arguments
invalid = True
break
message = messages[index - 1]
if message is None:
invalid = True
all_invalid = True
spec = None
# sort to make the longest argument list appear first
for spec_try in reversed(sorted(specs, key=len_recurse)):
if spec_try is None:
spec_try = (1, )
context = None
msgs = []
invalid = False
# last_index is 1 based like the keyword spec
for index in spec_try:
if isinstance(index, tuple):
context = messages[index[0] - 1]
continue
if last_index < index:
# Not enough arguments
invalid = True
break
message = messages[index - 1]
if message is None:
invalid = True
break
msgs.append(message)
if not invalid:
# current argument list matched, stop it, we are done
all_invalid = False
spec = spec_try
break
msgs.append(message)
if invalid:
if all_invalid:
continue

# keyword spec indexes are 1 based, therefore '-1'
Expand Down
17 changes: 9 additions & 8 deletions babel/messages/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from distutils import log
from distutils.cmd import Command
from distutils.errors import DistutilsOptionError, DistutilsSetupError
from collections import defaultdict
from locale import getpreferredencoding
import logging
from optparse import OptionParser
Expand Down Expand Up @@ -1227,22 +1228,22 @@ def parse_mapping(fileobj, filename=None):
def parse_keywords(strings=[]):
"""Parse keywords specifications from the given list of strings.

>>> kw = parse_keywords(['_', 'dgettext:2', 'dngettext:2,3', 'pgettext:1c,2']).items()
>>> kw = parse_keywords(['_', '_:1,2', 'dgettext:2', 'dngettext:2,3', 'pgettext:1c,2']).items()
>>> kw.sort()
>>> for keyword, indices in kw:
... print (keyword, indices)
('_', None)
('dgettext', (2,))
('dngettext', (2, 3))
('pgettext', ((1, 'c'), 2))
('_', [None, (1, 2)])
('dgettext', [(2,)])
('dngettext', [(2, 3)])
('pgettext', [((1, 'c'), 2)])
"""
keywords = {}
keywords = defaultdict(list)
for string in strings:
if ':' in string:
funcname, indices = string.split(':')
else:
funcname, indices = string, None
if funcname not in keywords:
if funcname not in keywords or indices not in keywords[funcname]:
if indices:
inds = []
for x in indices.split(','):
Expand All @@ -1251,7 +1252,7 @@ def parse_keywords(strings=[]):
else:
inds.append(int(x))
indices = tuple(inds)
keywords[funcname] = indices
keywords[funcname].append(indices)
return keywords


Expand Down
20 changes: 20 additions & 0 deletions babel/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,26 @@ def dst(self, dt):
return ZERO


def len_recurse(l):
"""Count length of given object recursively.

>>> len_recurse(None)
1
>>> len_recurse([1, 2])
2
>>> len_recurse([(1, 'c'), 2])
3
"""
if not isinstance(l, (list, tuple)):
return 1
curr_len = 0
for elem in l:
if isinstance(elem, (list, tuple)):
curr_len += len_recurse(elem)
else:
curr_len += 1
return curr_len

import pytz as _pytz
from babel import localtime

Expand Down
10 changes: 5 additions & 5 deletions tests/messages/test_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -1098,11 +1098,11 @@ def test_parse_mapping():


def test_parse_keywords():
kw = frontend.parse_keywords(['_', 'dgettext:2',
kw = frontend.parse_keywords(['_', '_:1,2', 'dgettext:2',
'dngettext:2,3', 'pgettext:1c,2'])
assert kw == {
'_': None,
'dgettext': (2,),
'dngettext': (2, 3),
'pgettext': ((1, 'c'), 2),
'_': [None, (1, 2)],
'dgettext': [(2,)],
'dngettext': [(2, 3)],
'pgettext': [((1, 'c'), 2)],
}