Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
92 changes: 51 additions & 41 deletions babel/messages/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ class _FileObj(SupportsRead[bytes], SupportsReadline[bytes], Protocol):
def seek(self, __offset: int, __whence: int = ...) -> int: ...
def tell(self) -> int: ...

_Keyword: TypeAlias = tuple[int | tuple[int, int] | tuple[int, str], ...] | None
_SimpleKeyword: TypeAlias = tuple[int | tuple[int, int] | tuple[int, str], ...] | None
_Keyword: TypeAlias = dict[int | None, _SimpleKeyword] | _SimpleKeyword

# 5-tuple of (filename, lineno, messages, comments, context)
_FileExtractionResult: TypeAlias = tuple[str, int, str | tuple[str, ...], list[str], str | None]
Expand Down Expand Up @@ -400,56 +401,65 @@ def extract(
options=options or {})

for lineno, funcname, messages, comments in results:
spec = keywords[funcname] or (1,) if funcname else (1,)
specs = keywords[funcname] or (1,) if funcname else (1,)
Comment thread
akx marked this conversation as resolved.
Outdated
if not isinstance(specs, dict): # for backwards compatibility
specs = {None: specs}
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]

# None matches all arities.
for arity in (None, len(messages)):
try:
spec = specs[arity]
except KeyError:
continue
Comment thread
jeanas marked this conversation as resolved.
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
break
msgs.append(message)
if invalid:
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 invalid:
continue

# keyword spec indexes are 1 based, therefore '-1'
if isinstance(spec[0], tuple):
# context-aware *gettext method
first_msg_index = spec[1] - 1
else:
first_msg_index = spec[0] - 1
if not messages[first_msg_index]:
# An empty string msgid isn't valid, emit a warning
filename = (getattr(fileobj, "name", None) or "(unknown)")
sys.stderr.write(
f"{filename}:{lineno}: warning: Empty msgid. It is reserved by GNU gettext: gettext(\"\") "
f"returns the header entry with meta information, not the empty string.\n"
)
continue
# keyword spec indexes are 1 based, therefore '-1'
if isinstance(spec[0], tuple):
# context-aware *gettext method
first_msg_index = spec[1] - 1
else:
first_msg_index = spec[0] - 1
if not messages[first_msg_index]:
# An empty string msgid isn't valid, emit a warning
filename = (getattr(fileobj, "name", None) or "(unknown)")
sys.stderr.write(
f"{filename}:{lineno}: warning: Empty msgid. It is reserved by GNU gettext: gettext(\"\") "
f"returns the header entry with meta information, not the empty string.\n"
)
continue

messages = tuple(msgs)
if len(messages) == 1:
messages = messages[0]
msgs = tuple(msgs)
if len(msgs) == 1:
msgs = msgs[0]

if strip_comment_tags:
_strip_comment_tags(comments, comment_tags)
yield lineno, messages, comments, context
if strip_comment_tags:
_strip_comment_tags(comments, comment_tags)
yield lineno, msgs, comments, context


def extract_nothing(
Expand Down
36 changes: 26 additions & 10 deletions babel/messages/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -1111,30 +1111,46 @@ def parse_mapping(fileobj, filename=None):
def parse_keywords(strings: Iterable[str] = ()):
"""Parse keywords specifications from the given list of strings.
Comment thread
jeanas marked this conversation as resolved.

>>> kw = sorted(parse_keywords(['_', 'dgettext:2', 'dngettext:2,3', 'pgettext:1c,2']).items())
>>> kw = sorted(parse_keywords(['_', 'dgettext:2', 'dngettext:2,3', 'pgettext:1c,2', 'polymorphic:1', 'polymorphic:2,2t', 'polymorphic:3c,3t']).items())
>>> for keyword, indices in kw:
... print((keyword, indices))
('_', None)
('dgettext', (2,))
('dngettext', (2, 3))
('pgettext', ((1, 'c'), 2))
('polymorphic', {None: (1,), 2: (2,), 3: ((3, 'c'),)})
"""
keywords = {}
for string in strings:
if ':' in string:
funcname, indices = string.split(':')
else:
funcname, indices = string, None
number = None
if indices:
inds = []
for x in indices.split(','):
if x[-1] == 't':
number = int(x[:-1])
elif x[-1] == 'c':
inds.append((int(x[:-1]), 'c'))
else:
inds.append(int(x))
inds = tuple(inds)
else:
inds = None
if funcname not in keywords:
if indices:
inds = []
for x in indices.split(','):
if x[-1] == 'c':
inds.append((int(x[:-1]), 'c'))
else:
inds.append(int(x))
indices = tuple(inds)
keywords[funcname] = indices
if number is None:
# For best backwards compatibility, collapse {None: x} into x.
keywords[funcname] = inds
else:
keywords[funcname] = {number: inds}
else:
if isinstance(keywords[funcname], tuple) or keywords[funcname] is None:
keywords[funcname] = {None: keywords[funcname]}
else:
assert isinstance(keywords[funcname], dict)
Comment thread
jeanas marked this conversation as resolved.
Outdated
keywords[funcname][number] = inds
return keywords


Expand Down
33 changes: 31 additions & 2 deletions tests/messages/test_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@
import time
import unittest
from datetime import datetime
from io import StringIO
from io import BytesIO, StringIO

import pytest
from freezegun import freeze_time
from setuptools import Distribution

from babel import __version__ as VERSION
from babel.dates import format_datetime
from babel.messages import Catalog, frontend
from babel.messages import Catalog, extract, frontend
from babel.messages.frontend import (
BaseError,
CommandLineInterface,
Expand Down Expand Up @@ -1320,6 +1320,35 @@ def test_parse_keywords():
}


def test_parse_keywords_with_t():
kw = frontend.parse_keywords(['_:1', '_:2,2t', '_:2c,3,3t'])

assert kw == {
'_': {
None: (1,),
2: (2,),
3: ((2, 'c'), 3),
}
}

def test_extract_messages_with_t():
content = rb"""
_("1 arg, arg 1")
_("2 args, arg 1", "2 args, arg 2")
_("3 args, arg 1", "3 args, arg 2", "3 args, arg 3")
_("4 args, arg 1", "4 args, arg 2", "4 args, arg 3", "4 args, arg 4")
"""
kw = frontend.parse_keywords(['_:1', '_:2,2t', '_:2c,3,3t'])
result = list(extract.extract("python", BytesIO(content), kw))
expected = [(2, '1 arg, arg 1', [], None),
(3, '2 args, arg 1', [], None),
(3, '2 args, arg 2', [], None),
(4, '3 args, arg 1', [], None),
(4, '3 args, arg 3', [], '3 args, arg 2'),
(5, '4 args, arg 1', [], None)]
assert result == expected


def configure_cli_command(cmdline):
"""
Helper to configure a command class, but not run it just yet.
Expand Down