Skip to content
26 changes: 26 additions & 0 deletions Lib/test/test_tools/test_i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,32 @@ def test_process_keywords(self):
no_default_keywords=no_default_keywords)
self.assertEqual(processed, expected)

def test_multiple_keywords_same_funcname_errors(self):
# If at least one keyword spec for a given funcname matches,
# no error should be printed.
msgids, stderr = self.extract_from_str(dedent('''\
_("foo", 42)
_(42, "bar")
'''), args=('--keyword=_:1', '--keyword=_:2'), with_stderr=True)
self.assertIn('foo', msgids)
self.assertIn('bar', msgids)
self.assertEqual(stderr, b'')

# If no keyword spec for a given funcname matches,
# all errors are printed.
msgids, stderr = self.extract_from_str(dedent('''\
_(x, 42)
_(42, y)
'''), args=('--keyword=_:1', '--keyword=_:2'), with_stderr=True,
strict=False)
self.assertEqual(msgids, [''])
self.assertEqual(
stderr,
b'*** test.py:1: Expected a string constant for argument 1, got x\n'
b'*** test.py:1: Expected a string constant for argument 2, got 42\n'
Copy link
Member

Choose a reason for hiding this comment

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

This is misleading, since only one of arguments needs to be a string.

b'*** test.py:2: Expected a string constant for argument 1, got 42\n'
b'*** test.py:2: Expected a string constant for argument 2, got y\n')


def extract_from_snapshots():
snapshots = {
Expand Down
36 changes: 18 additions & 18 deletions Tools/i18n/pygettext.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,46 +475,46 @@ def _extract_docstring(self, node):

def _extract_message(self, node):
func_name = self._get_func_name(node)
specs = self.options.keywords.get(func_name, [])
for spec in specs:
extracted = self._extract_message_with_spec(node, spec)
if extracted:
errors = []
for spec in self.options.keywords.get(func_name, []):
err = self._extract_message_with_spec(node, spec)
if err is None:
break
errors.append(err)
else:
for err in errors:
print(err, file=sys.stderr)

def _extract_message_with_spec(self, node, spec):
"""Extract a gettext call with the given spec.

Return True if the gettext call was successfully extracted, False
otherwise.
Return `None` if the gettext call was successfully extracted,
otherwise return an error message.
"""
max_index = max(spec.values())
has_var_positional = any(isinstance(arg, ast.Starred) for
arg in node.args[:max_index+1])
if has_var_positional:
print(f'*** {self.filename}:{node.lineno}: Variable positional '
f'arguments are not allowed in gettext calls', file=sys.stderr)
return False
return (f'*** {self.filename}:{node.lineno}: Variable positional '
f'arguments are not allowed in gettext calls')
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
f'arguments are not allowed in gettext calls')
f'arguments are not allowed in gettext calls')

Copy link
Member

Choose a reason for hiding this comment

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

This error will still be printed multiple times.


if max_index >= len(node.args):
print(f'*** {self.filename}:{node.lineno}: Expected at least '
f'{max_index + 1} positional argument(s) in gettext call, '
f'got {len(node.args)}', file=sys.stderr)
return False
return (f'*** {self.filename}:{node.lineno}: Expected at least '
f'{max_index + 1} positional argument(s) in gettext call, '
f'got {len(node.args)}')
Copy link
Member

Choose a reason for hiding this comment

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

With --keyword=_:1 --keyword=_:2, multiple errors will be printed for _(): "Expected at least 1 positional argument(s)...", "Expected at least 2 positional argument(s)...".


msg_data = {}
for arg_type, position in spec.items():
arg = node.args[position]
if not self._is_string_const(arg):
print(f'*** {self.filename}:{arg.lineno}: Expected a string '
f'constant for argument {position + 1}, '
f'got {ast.unparse(arg)}', file=sys.stderr)
return False
return (f'*** {self.filename}:{arg.lineno}: Expected a string '
f'constant for argument {position + 1}, '
f'got {ast.unparse(arg)}')
msg_data[arg_type] = arg.value

lineno = node.lineno
comments = self._extract_comments(node)
self._add_message(lineno, **msg_data, comments=comments)
return True

def _extract_comments(self, node):
"""Extract translator comments.
Expand Down
Loading