Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions Lib/test/test_tools/i18n_data/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -33,65 +33,69 @@ msgid ""
" multiline!\n"
msgstr ""

#: messages.py:46 messages.py:89 messages.py:90 messages.py:93 messages.py:94
#: messages.py:99 messages.py:100 messages.py:101
#: messages.py:32
msgid "f-strings!"
msgstr ""

#: messages.py:50 messages.py:93 messages.py:94 messages.py:97 messages.py:98
#: messages.py:103 messages.py:104 messages.py:105
msgid "foo"
msgid_plural "foos"
msgstr[0] ""
msgstr[1] ""

#: messages.py:47
#: messages.py:51
msgid "something"
msgstr ""

#: messages.py:50
#: messages.py:54
msgid "Hello, {}!"
msgstr ""

#: messages.py:54
#: messages.py:58
msgid "1"
msgstr ""

#: messages.py:54
#: messages.py:58
msgid "2"
msgstr ""

#: messages.py:55 messages.py:56
#: messages.py:59 messages.py:60
msgid "A"
msgstr ""

#: messages.py:55 messages.py:56
#: messages.py:59 messages.py:60
msgid "B"
msgstr ""

#: messages.py:57
#: messages.py:61
msgid "set"
msgstr ""

#: messages.py:62 messages.py:63
#: messages.py:66 messages.py:67
msgid "nested string"
msgstr ""

#: messages.py:68
#: messages.py:72
msgid "baz"
msgstr ""

#: messages.py:71 messages.py:75
#: messages.py:75 messages.py:79
msgid "default value"
msgstr ""

#: messages.py:91 messages.py:92 messages.py:95 messages.py:96
#: messages.py:95 messages.py:96 messages.py:99 messages.py:100
msgctxt "context"
msgid "foo"
msgid_plural "foos"
msgstr[0] ""
msgstr[1] ""

#: messages.py:102
#: messages.py:106
msgid "domain foo"
msgstr ""

#: messages.py:118 messages.py:119
#: messages.py:122 messages.py:123
msgid "world"
msgid_plural "worlds"
msgstr[0] ""
Expand Down
4 changes: 4 additions & 0 deletions Lib/test/test_tools/i18n_data/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
multiline!
""")

# F-strings without formatted values are allowed
_(f"f-strings!")

# Invalid arguments
_()
_(None)
Expand All @@ -38,6 +41,7 @@
_("string"[3])
_("string"[:3])
_({"string": "foo"})
_(f"Hello, {world}!")

# pygettext does not allow keyword arguments, but both xgettext and pybabel do
_(x="kwargs are not allowed!")
Expand Down
11 changes: 0 additions & 11 deletions Lib/test/test_tools/test_i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,6 @@ def test_msgid_bytes(self):
msgids = self.extract_docstrings_from_str('_(b"""doc""")')
self.assertFalse([msgid for msgid in msgids if 'doc' in msgid])

def test_msgid_fstring(self):
msgids = self.extract_docstrings_from_str('_(f"""doc""")')
self.assertFalse([msgid for msgid in msgids if 'doc' in msgid])

def test_funcdocstring_annotated_args(self):
""" Test docstrings for functions with annotated args """
msgids = self.extract_docstrings_from_str(dedent('''\
Expand Down Expand Up @@ -349,13 +345,6 @@ def test_calls_in_fstring_with_keyword_args(self):
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_function_and_class_names(self):
"""Test that function and class names are not mistakenly extracted."""
msgids = self.extract_from_str(dedent('''\
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Support extracting constant f-strings (f-strings not containing formatted
values) in :program:`pygettext.py`.
27 changes: 23 additions & 4 deletions Tools/i18n/pygettext.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,12 +364,13 @@ def _extract_message(self, node):
msg_data = {}
for position, arg_type in spec.items():
arg = node.args[position]
if not self._is_string_const(arg):
value = self._parse_string_const(arg)
if value is None:
print(f'*** {self.filename}:{arg.lineno}: Expected a string '
f'constant for argument {position + 1}, '
f'got {ast.unparse(arg)}', file=sys.stderr)
return
msg_data[arg_type] = arg.value
msg_data[arg_type] = value

lineno = node.lineno
self._add_message(lineno, **msg_data)
Expand Down Expand Up @@ -413,8 +414,26 @@ def _get_func_name(self, node):
case _:
return None

def _is_string_const(self, node):
return isinstance(node, ast.Constant) and isinstance(node.value, str)
def _parse_string_const(self, node):
value = self._parse_literal_string(node)
if value is not None:
return value
return self._parse_literal_fstring(node)
Comment on lines +419 to +421
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
if value is not None:
return value
return self._parse_literal_fstring(node)
return self._parse_literal_string(node) or self._parse_literal_fstring(node)

Copy link
Member Author

@tomasr8 tomasr8 Feb 15, 2025

Choose a reason for hiding this comment

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

self._parse_literal_string(node) could return '' which is falsey so we'd try to parse an f-string there which is not what we want


def _parse_literal_fstring(self, node):
if not isinstance(node, ast.JoinedStr):
return None
parts = []
for value in node.values:
part = self._parse_literal_string(value)
if part is None:
return None
parts.append(part)
return ''.join(parts)

def _parse_literal_string(self, node):
if isinstance(node, ast.Constant) and isinstance(node.value, str):
return node.value

def write_pot_file(messages, options, fp):
timestamp = time.strftime('%Y-%m-%d %H:%M%z')
Expand Down
Loading