Skip to content
Merged
70 changes: 70 additions & 0 deletions Lib/test/test_tools/test_i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,76 @@ class D(L[1:2], F({1: 2}), metaclass=M(lambda x: x)):
'''))
self.assertIn('doc', msgids)

def test_calls_in_fstrings(self):
msgids = self.extract_docstrings_from_str(dedent('''\
f"{_('foo bar')}"
'''))
self.assertIn('foo bar', msgids)

def test_calls_in_fstrings_raw(self):
msgids = self.extract_docstrings_from_str(dedent('''\
rf"{_('foo bar')}"
'''))
self.assertIn('foo bar', msgids)

def test_calls_in_fstrings_nested(self):
msgids = self.extract_docstrings_from_str(dedent('''\
f"""{f'{_("foo bar")}'}"""
'''))
self.assertIn('foo bar', msgids)

def test_calls_in_fstrings_attribute(self):
msgids = self.extract_docstrings_from_str(dedent('''\
f"{obj._('foo bar')}"
'''))
self.assertIn('foo bar', msgids)

def test_calls_in_fstrings_with_call_on_call(self):
msgids = self.extract_docstrings_from_str(dedent('''\
f"{type(str)('foo bar')}"
'''))
self.assertNotIn('foo bar', msgids)

def test_calls_in_fstrings_with_format(self):
msgids = self.extract_docstrings_from_str(dedent('''\
f"{_('foo {bar}').format(bar='baz')}"
'''))
self.assertIn('foo {bar}', msgids)

def test_calls_in_fstrings_with_wrong_input_1(self):
msgids = self.extract_docstrings_from_str(dedent('''\
f"{_(f'foo {bar}')}"
'''))
self.assertFalse([msgid for msgid in msgids if 'foo {bar}' in msgid])

def test_calls_in_fstrings_with_wrong_input_2(self):
msgids = self.extract_docstrings_from_str(dedent('''\
f"{_(1)}"
'''))
self.assertNotIn(1, msgids)

def test_calls_in_fstring_with_multiple_args(self):
msgids = self.extract_docstrings_from_str(dedent('''\
f"{_('foo', 'bar')}"
'''))
self.assertNotIn('foo', msgids)
self.assertNotIn('bar', msgids)

def test_calls_in_fstring_with_keyword_args(self):
msgids = self.extract_docstrings_from_str(dedent('''\
f"{_('foo', bar='baz')}"
'''))
self.assertNotIn('foo', msgids)
self.assertNotIn('bar', msgids)
self.assertNotIn('baz', msgids)

def test_calls_in_fstring_with_partially_wrong_expression(self):
msgids = self.extract_docstrings_from_str(dedent('''\
f"{_(f'foo') + _('bar')}"
'''))
self.assertNotIn('foo', msgids)
self.assertIn('bar', msgids)

def test_files_list(self):
"""Make sure the directories are inspected for source files
bpo-31920
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,7 @@ Ivan Krstić
Anselm Kruis
Steven Kryskalla
Andrew Kuchling
Jakub Kuczys
Dave Kuhlman
Jon Kuhn
Ilya Kulakov
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Allow :file:`Tools/i18n/pygettext.py` to detect calls to ``gettext`` in
f-strings.
53 changes: 53 additions & 0 deletions Tools/i18n/pygettext.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@
import glob
import time
import getopt
import ast
import token
import tokenize

Expand Down Expand Up @@ -343,6 +344,58 @@ def __waiting(self, ttype, tstring, lineno):
return
if ttype == tokenize.NAME and tstring in opts.keywords:
self.__state = self.__keywordseen
return
if ttype == tokenize.STRING:
maybe_fstring = ast.parse(tstring, mode='eval').body
if not isinstance(maybe_fstring, ast.JoinedStr):
return
for value in filter(lambda node: isinstance(node, ast.FormattedValue),
maybe_fstring.values):
for call in filter(lambda node: isinstance(node, ast.Call),
ast.walk(value)):
func = call.func
if isinstance(func, ast.Name):
func_name = func.id
elif isinstance(func, ast.Attribute):
func_name = func.attr
else:
continue

if func_name not in opts.keywords:
continue
if len(call.args) != 1:
print(_(
'*** %(file)s:%(lineno)s: Seen unexpected amount of'
' positional arguments in gettext call: %(source_segment)s'
) % {
'source_segment': ast.get_source_segment(tstring, call) or tstring,
'file': self.__curfile,
'lineno': lineno
}, file=sys.stderr)
continue
if call.keywords:
print(_(
'*** %(file)s:%(lineno)s: Seen unexpected keyword arguments'
' in gettext call: %(source_segment)s'
) % {
'source_segment': ast.get_source_segment(tstring, call) or tstring,
'file': self.__curfile,
'lineno': lineno
}, file=sys.stderr)
continue
arg = call.args[0]
if not isinstance(arg, ast.Constant):
print(_(
'*** %(file)s:%(lineno)s: Seen unexpected argument type'
' in gettext call: %(source_segment)s'
) % {
'source_segment': ast.get_source_segment(tstring, call) or tstring,
'file': self.__curfile,
'lineno': lineno
}, file=sys.stderr)
continue
if isinstance(arg.value, str):
self.__addentry(arg.value, lineno)

def __suiteseen(self, ttype, tstring, lineno):
# skip over any enclosure pairs until we see the colon
Expand Down