gh-130453: pygettext: Allow specifying multiple keywords with the sam… · python/cpython@b6760b7 · GitHub
Skip to content

Commit b6760b7

Browse files
authored
gh-130453: pygettext: Allow specifying multiple keywords with the same function name (GH-131380)
1 parent 619edb8 commit b6760b7

5 files changed

Lines changed: 217 additions & 43 deletions

File tree

Lines changed: 38 additions & 0 deletions
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from gettext import gettext as foo
2+
3+
foo('bar')
4+
5+
foo('baz', 'qux')
6+
7+
# The 't' specifier is not supported, so the following
8+
# call is extracted as pgettext instead of ngettext.
9+
foo('corge', 'grault', 1)
10+
11+
foo('xyzzy', 'foo', 'foos', 1)

‎Lib/test/test_tools/test_i18n.py‎

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818

1919

2020
with imports_under_tool("i18n"):
21-
from pygettext import parse_spec
21+
from pygettext import (parse_spec, process_keywords, DEFAULTKEYWORDS,
22+
unparse_spec)
2223

2324

2425
def normalize_POT_file(pot):
@@ -483,20 +484,22 @@ def test_comments_not_extracted_without_tags(self):
483484

484485
def test_parse_keyword_spec(self):
485486
valid = (
486-
('foo', ('foo', {0: 'msgid'})),
487-
('foo:1', ('foo', {0: 'msgid'})),
488-
('foo:1,2', ('foo', {0: 'msgid', 1: 'msgid_plural'})),
489-
('foo:1, 2', ('foo', {0: 'msgid', 1: 'msgid_plural'})),
490-
('foo:1,2c', ('foo', {0: 'msgid', 1: 'msgctxt'})),
491-
('foo:2c,1', ('foo', {0: 'msgid', 1: 'msgctxt'})),
492-
('foo:2c ,1', ('foo', {0: 'msgid', 1: 'msgctxt'})),
493-
('foo:1,2,3c', ('foo', {0: 'msgid', 1: 'msgid_plural', 2: 'msgctxt'})),
494-
('foo:1, 2, 3c', ('foo', {0: 'msgid', 1: 'msgid_plural', 2: 'msgctxt'})),
495-
('foo:3c,1,2', ('foo', {0: 'msgid', 1: 'msgid_plural', 2: 'msgctxt'})),
487+
('foo', ('foo', {'msgid': 0})),
488+
('foo:1', ('foo', {'msgid': 0})),
489+
('foo:1,2', ('foo', {'msgid': 0, 'msgid_plural': 1})),
490+
('foo:1, 2', ('foo', {'msgid': 0, 'msgid_plural': 1})),
491+
('foo:1,2c', ('foo', {'msgid': 0, 'msgctxt': 1})),
492+
('foo:2c,1', ('foo', {'msgid': 0, 'msgctxt': 1})),
493+
('foo:2c ,1', ('foo', {'msgid': 0, 'msgctxt': 1})),
494+
('foo:1,2,3c', ('foo', {'msgid': 0, 'msgid_plural': 1, 'msgctxt': 2})),
495+
('foo:1, 2, 3c', ('foo', {'msgid': 0, 'msgid_plural': 1, 'msgctxt': 2})),
496+
('foo:3c,1,2', ('foo', {'msgid': 0, 'msgid_plural': 1, 'msgctxt': 2})),
496497
)
497498
for spec, expected in valid:
498499
with self.subTest(spec=spec):
499500
self.assertEqual(parse_spec(spec), expected)
501+
# test unparse-parse round-trip
502+
self.assertEqual(parse_spec(unparse_spec(*expected)), expected)
500503

501504
invalid = (
502505
('foo:', "Invalid keyword spec 'foo:': missing argument positions"),
@@ -516,6 +519,70 @@ def test_parse_keyword_spec(self):
516519
parse_spec(spec)
517520
self.assertEqual(str(cm.exception), message)
518521

522+
def test_process_keywords(self):
523+
default_keywords = {name: [spec] for name, spec
524+
in DEFAULTKEYWORDS.items()}
525+
inputs = (
526+
(['foo'], True),
527+
(['_:1,2'], True),
528+
(['foo', 'foo:1,2'], True),
529+
(['foo'], False),
530+
(['_:1,2', '_:1c,2,3', 'pgettext'], False),
531+
# Duplicate entries
532+
(['foo', 'foo'], True),
533+
(['_'], False)
534+
)
535+
expected = (
536+
{'foo': [{'msgid': 0}]},
537+
{'_': [{'msgid': 0, 'msgid_plural': 1}]},
538+
{'foo': [{'msgid': 0}, {'msgid': 0, 'msgid_plural': 1}]},
539+
default_keywords | {'foo': [{'msgid': 0}]},
540+
default_keywords | {'_': [{'msgid': 0, 'msgid_plural': 1},
541+
{'msgctxt': 0, 'msgid': 1, 'msgid_plural': 2},
542+
{'msgid': 0}],
543+
'pgettext': [{'msgid': 0},
544+
{'msgctxt': 0, 'msgid': 1}]},
545+
{'foo': [{'msgid': 0}]},
546+
default_keywords,
547+
)
548+
for (keywords, no_default_keywords), expected in zip(inputs, expected):
549+
with self.subTest(keywords=keywords,
550+
no_default_keywords=no_default_keywords):
551+
processed = process_keywords(
552+
keywords,
553+
no_default_keywords=no_default_keywords)
554+
self.assertEqual(processed, expected)
555+
556+
def test_multiple_keywords_same_funcname_errors(self):
557+
# If at least one keyword spec for a given funcname matches,
558+
# no error should be printed.
559+
msgids, stderr = self.extract_from_str(dedent('''\
560+
_("foo", 42)
561+
_(42, "bar")
562+
'''), args=('--keyword=_:1', '--keyword=_:2'), with_stderr=True)
563+
self.assertIn('foo', msgids)
564+
self.assertIn('bar', msgids)
565+
self.assertEqual(stderr, b'')
566+
567+
# If no keyword spec for a given funcname matches,
568+
# all errors are printed.
569+
msgids, stderr = self.extract_from_str(dedent('''\
570+
_(x, 42)
571+
_(42, y)
572+
'''), args=('--keyword=_:1', '--keyword=_:2'), with_stderr=True,
573+
strict=False)
574+
self.assertEqual(msgids, [''])
575+
# Normalize line endings on Windows
576+
stderr = stderr.decode('utf-8').replace('\r', '')
577+
self.assertEqual(
578+
stderr,
579+
'*** test.py:1: No keywords matched gettext call "_":\n'
580+
'\tkeyword="_": Expected a string constant for argument 1, got x\n'
581+
'\tkeyword="_:2": Expected a string constant for argument 2, got 42\n'
582+
'*** test.py:2: No keywords matched gettext call "_":\n'
583+
'\tkeyword="_": Expected a string constant for argument 1, got 42\n'
584+
'\tkeyword="_:2": Expected a string constant for argument 2, got y\n')
585+
519586

520587
def extract_from_snapshots():
521588
snapshots = {
@@ -526,6 +593,10 @@ def extract_from_snapshots():
526593
'custom_keywords.py': ('--keyword=foo', '--keyword=nfoo:1,2',
527594
'--keyword=pfoo:1c,2',
528595
'--keyword=npfoo:1c,2,3', '--keyword=_:1,2'),
596+
'multiple_keywords.py': ('--keyword=foo:1c,2,3', '--keyword=foo:1c,2',
597+
'--keyword=foo:1,2',
598+
# repeat a keyword to make sure it is extracted only once
599+
'--keyword=foo', '--keyword=foo'),
529600
# == Test character escaping
530601
# Escape ascii and unicode:
531602
'escapes.py': ('--escape', '--add-comments='),
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Allow passing multiple keyword arguments with the same function name in
2+
:program:`pygettext`.

‎Tools/i18n/pygettext.py‎

Lines changed: 84 additions & 32 deletions

0 commit comments

Comments
 (0)