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 <batuhanosmantaskaya@gmail.com>
This commit is contained in:
parent
1f73c320e2
commit
bfc6b63102
|
@ -220,6 +220,76 @@ class Test_pygettext(unittest.TestCase):
|
||||||
'''))
|
'''))
|
||||||
self.assertIn('doc', msgids)
|
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):
|
def test_files_list(self):
|
||||||
"""Make sure the directories are inspected for source files
|
"""Make sure the directories are inspected for source files
|
||||||
bpo-31920
|
bpo-31920
|
||||||
|
|
|
@ -949,6 +949,7 @@ Ivan Krstić
|
||||||
Anselm Kruis
|
Anselm Kruis
|
||||||
Steven Kryskalla
|
Steven Kryskalla
|
||||||
Andrew Kuchling
|
Andrew Kuchling
|
||||||
|
Jakub Kuczys
|
||||||
Dave Kuhlman
|
Dave Kuhlman
|
||||||
Jon Kuhn
|
Jon Kuhn
|
||||||
Ilya Kulakov
|
Ilya Kulakov
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
Allow :file:`Tools/i18n/pygettext.py` to detect calls to ``gettext`` in
|
||||||
|
f-strings.
|
|
@ -162,6 +162,7 @@ import sys
|
||||||
import glob
|
import glob
|
||||||
import time
|
import time
|
||||||
import getopt
|
import getopt
|
||||||
|
import ast
|
||||||
import token
|
import token
|
||||||
import tokenize
|
import tokenize
|
||||||
|
|
||||||
|
@ -343,6 +344,58 @@ class TokenEater:
|
||||||
return
|
return
|
||||||
if ttype == tokenize.NAME and tstring in opts.keywords:
|
if ttype == tokenize.NAME and tstring in opts.keywords:
|
||||||
self.__state = self.__keywordseen
|
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):
|
def __suiteseen(self, ttype, tstring, lineno):
|
||||||
# skip over any enclosure pairs until we see the colon
|
# skip over any enclosure pairs until we see the colon
|
||||||
|
|
Loading…
Reference in New Issue