Skip to content

Commit

Permalink
Fix extracted lineno with nested calls
Browse files Browse the repository at this point in the history
When a gettext call had a nested function call on a new line, the
extract function would use that nested call's line number when
extracting the terms for the gettext call.

The reason is that we set the line number on any encounter of an opening
parenthesis after a gettext keyword. This does not work if either we
have a nested call, or our first term starts on a new line.

This commit fixes that by only setting the line number when we encounter
the first argument inside a gettext call.

Existing tests were adapted to work according to `xgettext` with regards
to the line numbers.

Fixes python-babel#1123
  • Loading branch information
dylankiss authored and akx committed Oct 19, 2024
1 parent ea84d9d commit 5795711
Show file tree
Hide file tree
Showing 2 changed files with 16 additions and 4 deletions.
10 changes: 8 additions & 2 deletions babel/messages/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from functools import lru_cache
from os.path import relpath
from textwrap import dedent
from tokenize import COMMENT, NAME, OP, STRING, generate_tokens
from tokenize import COMMENT, NAME, NL, OP, STRING, generate_tokens
from typing import TYPE_CHECKING, Any

from babel.messages._compat import find_entrypoints
Expand Down Expand Up @@ -530,7 +530,6 @@ def extract_python(
in_def = False
continue
if funcname:
message_lineno = lineno
call_stack += 1
elif in_def and tok == OP and value == ':':
# End of a class definition without parens
Expand Down Expand Up @@ -580,11 +579,15 @@ def extract_python(
elif tok == STRING:
val = _parse_python_string(value, encoding, future_flags)
if val is not None:
if not message_lineno:
message_lineno = lineno
buf.append(val)

# Python 3.12+, see https://peps.python.org/pep-0701/#new-tokens
elif tok == FSTRING_START:
current_fstring_start = value
if not message_lineno:
message_lineno = lineno
elif tok == FSTRING_MIDDLE:
if current_fstring_start is not None:
current_fstring_start += value
Expand All @@ -608,6 +611,9 @@ def extract_python(
# for the comment to still be a valid one
old_lineno, old_comment = translator_comments.pop()
translator_comments.append((old_lineno + 1, old_comment))

elif tok != NL and not message_lineno:
message_lineno = lineno
elif call_stack > 0 and tok == OP and value == ')':
call_stack -= 1
elif funcname and call_stack == -1:
Expand Down
10 changes: 8 additions & 2 deletions tests/messages/test_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ def test_nested_calls(self):
msg8 = gettext('Rabbit')
msg9 = dgettext('wiki', model.addPage())
msg10 = dngettext(getDomain(), 'Page', 'Pages', 3)
msg11 = ngettext(
"bunny",
"bunnies",
len(bunnies)
)
""")
messages = list(extract.extract_python(buf,
extract.DEFAULT_KEYWORDS.keys(),
Expand All @@ -49,6 +54,7 @@ def test_nested_calls(self):
(8, 'gettext', 'Rabbit', []),
(9, 'dgettext', ('wiki', None), []),
(10, 'dngettext', (None, 'Page', 'Pages', None), []),
(12, 'ngettext', ('bunny', 'bunnies', None), []),
]

def test_extract_default_encoding_ascii(self):
Expand Down Expand Up @@ -97,10 +103,10 @@ def test_comments_with_calls_that_spawn_multiple_lines(self):
messages = list(extract.extract_python(buf, ('ngettext', '_'), ['NOTE:'],

{'strip_comment_tags': False}))
assert messages[0] == (3, 'ngettext', ('Catalog deleted.', 'Catalogs deleted.', None), ['NOTE: This Comment SHOULD Be Extracted'])
assert messages[0] == (2, 'ngettext', ('Catalog deleted.', 'Catalogs deleted.', None), ['NOTE: This Comment SHOULD Be Extracted'])
assert messages[1] == (6, '_', 'Locale deleted.', ['NOTE: This Comment SHOULD Be Extracted'])
assert messages[2] == (10, 'ngettext', ('Foo deleted.', 'Foos deleted.', None), ['NOTE: This Comment SHOULD Be Extracted'])
assert messages[3] == (15, 'ngettext', ('Bar deleted.', 'Bars deleted.', None), ['NOTE: This Comment SHOULD Be Extracted', 'NOTE: And This One Too'])
assert messages[3] == (14, 'ngettext', ('Bar deleted.', 'Bars deleted.', None), ['NOTE: This Comment SHOULD Be Extracted', 'NOTE: And This One Too'])

def test_declarations(self):
buf = BytesIO(b"""\
Expand Down

0 comments on commit 5795711

Please sign in to comment.