]> git.ipfire.org Git - thirdparty/babel.git/commitdiff
Fix extracted lineno with nested calls (#1126)
authorDylan Kiss <dyki@odoo.com>
Sat, 19 Oct 2024 12:17:36 +0000 (14:17 +0200)
committerGitHub <noreply@github.com>
Sat, 19 Oct 2024 12:17:36 +0000 (12:17 +0000)
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 https://github.com/python-babel/babel/issues/1123

babel/messages/extract.py
tests/messages/test_extract.py

index 8d4bbeaf8c67a8d2bc4c4a4ae86cef69de4e2947..1b2a37fc67579f791657217c7c722951edec98ba 100644 (file)
@@ -33,7 +33,7 @@ from collections.abc import (
 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
@@ -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
@@ -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
@@ -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:
index 7d3a05aa73c898fc02d5b0dd5e009e5861358ba0..bcc6aa475e6da19d250b1c3837c3ad8a67fc6b55 100644 (file)
@@ -34,6 +34,11 @@ msg7 = _(hello.there)
 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(),
@@ -49,6 +54,7 @@ msg10 = dngettext(getDomain(), 'Page', 'Pages', 3)
             (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):
@@ -97,10 +103,10 @@ add_notice(req, ngettext("Bar deleted.",
         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"""\