Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bfc6b63

Browse files
bpo-36310: Allow pygettext.py to detect calls to gettext in f-strings. (GH-19875)
Adds support to Tools/i18n/pygettext.py for gettext calls in f-strings. This process is done by parsing the f-strings, processing each value, and flagging the ones which contain a gettext call. Co-authored-by: Batuhan Taskaya <[email protected]>
1 parent 1f73c32 commit bfc6b63

4 files changed

Lines changed: 126 additions & 0 deletions

File tree

Lib/test/test_tools/test_i18n.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,76 @@ class D(L[1:2], F({1: 2}), metaclass=M(lambda x: x)):
220220
'''))
221221
self.assertIn('doc', msgids)
222222

223+
def test_calls_in_fstrings(self):
224+
msgids = self.extract_docstrings_from_str(dedent('''\
225+
f"{_('foo bar')}"
226+
'''))
227+
self.assertIn('foo bar', msgids)
228+
229+
def test_calls_in_fstrings_raw(self):
230+
msgids = self.extract_docstrings_from_str(dedent('''\
231+
rf"{_('foo bar')}"
232+
'''))
233+
self.assertIn('foo bar', msgids)
234+
235+
def test_calls_in_fstrings_nested(self):
236+
msgids = self.extract_docstrings_from_str(dedent('''\
237+
f"""{f'{_("foo bar")}'}"""
238+
'''))
239+
self.assertIn('foo bar', msgids)
240+
241+
def test_calls_in_fstrings_attribute(self):
242+
msgids = self.extract_docstrings_from_str(dedent('''\
243+
f"{obj._('foo bar')}"
244+
'''))
245+
self.assertIn('foo bar', msgids)
246+
247+
def test_calls_in_fstrings_with_call_on_call(self):
248+
msgids = self.extract_docstrings_from_str(dedent('''\
249+
f"{type(str)('foo bar')}"
250+
'''))
251+
self.assertNotIn('foo bar', msgids)
252+
253+
def test_calls_in_fstrings_with_format(self):
254+
msgids = self.extract_docstrings_from_str(dedent('''\
255+
f"{_('foo {bar}').format(bar='baz')}"
256+
'''))
257+
self.assertIn('foo {bar}', msgids)
258+
259+
def test_calls_in_fstrings_with_wrong_input_1(self):
260+
msgids = self.extract_docstrings_from_str(dedent('''\
261+
f"{_(f'foo {bar}')}"
262+
'''))
263+
self.assertFalse([msgid for msgid in msgids if 'foo {bar}' in msgid])
264+
265+
def test_calls_in_fstrings_with_wrong_input_2(self):
266+
msgids = self.extract_docstrings_from_str(dedent('''\
267+
f"{_(1)}"
268+
'''))
269+
self.assertNotIn(1, msgids)
270+
271+
def test_calls_in_fstring_with_multiple_args(self):
272+
msgids = self.extract_docstrings_from_str(dedent('''\
273+
f"{_('foo', 'bar')}"
274+
'''))
275+
self.assertNotIn('foo', msgids)
276+
self.assertNotIn('bar', msgids)
277+
278+
def test_calls_in_fstring_with_keyword_args(self):
279+
msgids = self.extract_docstrings_from_str(dedent('''\
280+
f"{_('foo', bar='baz')}"
281+
'''))
282+
self.assertNotIn('foo', msgids)
283+
self.assertNotIn('bar', msgids)
284+
self.assertNotIn('baz', msgids)
285+
286+
def test_calls_in_fstring_with_partially_wrong_expression(self):
287+
msgids = self.extract_docstrings_from_str(dedent('''\
288+
f"{_(f'foo') + _('bar')}"
289+
'''))
290+
self.assertNotIn('foo', msgids)
291+
self.assertIn('bar', msgids)
292+
223293
def test_files_list(self):
224294
"""Make sure the directories are inspected for source files
225295
bpo-31920

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,7 @@ Ivan Krstić
949949
Anselm Kruis
950950
Steven Kryskalla
951951
Andrew Kuchling
952+
Jakub Kuczys
952953
Dave Kuhlman
953954
Jon Kuhn
954955
Ilya Kulakov
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Allow :file:`Tools/i18n/pygettext.py` to detect calls to ``gettext`` in
2+
f-strings.

Tools/i18n/pygettext.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@
162162
import glob
163163
import time
164164
import getopt
165+
import ast
165166
import token
166167
import tokenize
167168

@@ -343,6 +344,58 @@ def __waiting(self, ttype, tstring, lineno):
343344
return
344345
if ttype == tokenize.NAME and tstring in opts.keywords:
345346
self.__state = self.__keywordseen
347+
return
348+
if ttype == tokenize.STRING:
349+
maybe_fstring = ast.parse(tstring, mode='eval').body
350+
if not isinstance(maybe_fstring, ast.JoinedStr):
351+
return
352+
for value in filter(lambda node: isinstance(node, ast.FormattedValue),
353+
maybe_fstring.values):
354+
for call in filter(lambda node: isinstance(node, ast.Call),
355+
ast.walk(value)):
356+
func = call.func
357+
if isinstance(func, ast.Name):
358+
func_name = func.id
359+
elif isinstance(func, ast.Attribute):
360+
func_name = func.attr
361+
else:
362+
continue
363+
364+
if func_name not in opts.keywords:
365+
continue
366+
if len(call.args) != 1:
367+
print(_(
368+
'*** %(file)s:%(lineno)s: Seen unexpected amount of'
369+
' positional arguments in gettext call: %(source_segment)s'
370+
) % {
371+
'source_segment': ast.get_source_segment(tstring, call) or tstring,
372+
'file': self.__curfile,
373+
'lineno': lineno
374+
}, file=sys.stderr)
375+
continue
376+
if call.keywords:
377+
print(_(
378+
'*** %(file)s:%(lineno)s: Seen unexpected keyword arguments'
379+
' in gettext call: %(source_segment)s'
380+
) % {
381+
'source_segment': ast.get_source_segment(tstring, call) or tstring,
382+
'file': self.__curfile,
383+
'lineno': lineno
384+
}, file=sys.stderr)
385+
continue
386+
arg = call.args[0]
387+
if not isinstance(arg, ast.Constant):
388+
print(_(
389+
'*** %(file)s:%(lineno)s: Seen unexpected argument type'
390+
' in gettext call: %(source_segment)s'
391+
) % {
392+
'source_segment': ast.get_source_segment(tstring, call) or tstring,
393+
'file': self.__curfile,
394+
'lineno': lineno
395+
}, file=sys.stderr)
396+
continue
397+
if isinstance(arg.value, str):
398+
self.__addentry(arg.value, lineno)
346399

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

0 commit comments

Comments
 (0)