From 78e87d2b247ed4f3b02a9909757ff2a48f66acb6 Mon Sep 17 00:00:00 2001 From: ychampion <68075205+ychampion@users.noreply.github.com> Date: Sat, 14 Nov 2026 05:43:24 +0530 Subject: [PATCH] Only honor second-line encoding comments after blank or comment lines --- babel/util.py | 19 ++++---------- tests/messages/test_extract_python.py | 26 ++++++++++++++++++ tests/test_util.py | 38 +++++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 14 deletions(-) diff --git a/babel/util.py b/babel/util.py index a2bf728cc..f3c873d9a 100644 --- a/babel/util.py +++ b/babel/util.py @@ -51,6 +51,8 @@ def distinct(iterable: Iterable[_T]) -> Generator[_T, None, None]: rb'[ \t\f]* \# .* coding[=:][ \t]*([-\w.]+)', flags=re.VERBOSE, ) +# Only blank or comment first lines allow a second-line encoding declaration +_PYTHON_BLANK_OR_COMMENT_re = re.compile(rb'[ \t\f]*(?:[#\r\n]|$)') def parse_encoding(fp: IO[bytes]) -> str | None: @@ -73,20 +75,9 @@ def parse_encoding(fp: IO[bytes]) -> str | None: line1 = line1[len(codecs.BOM_UTF8) :] m = PYTHON_MAGIC_COMMENT_re.match(line1) - if not m: - try: - import ast - - ast.parse(line1.decode('latin-1')) - except (ImportError, SyntaxError, UnicodeEncodeError): - # Either it's a real syntax error, in which case the source is - # not valid python source, or line2 is a continuation of line1, - # in which case we don't want to scan line2 for a magic - # comment. - pass - else: - line2 = fp.readline() - m = PYTHON_MAGIC_COMMENT_re.match(line2) + if not m and _PYTHON_BLANK_OR_COMMENT_re.match(line1): + line2 = fp.readline() + m = PYTHON_MAGIC_COMMENT_re.match(line2) if has_bom: if m: diff --git a/tests/messages/test_extract_python.py b/tests/messages/test_extract_python.py index be1cf970f..2d0d50fd1 100644 --- a/tests/messages/test_extract_python.py +++ b/tests/messages/test_extract_python.py @@ -447,6 +447,32 @@ def test_utf8_bom_with_latin_magic_comment_fails(): list(extract.extract_python(buf, ('_',), ['NOTE:'], {})) +def test_second_line_coding_after_code_ignored(): + for first_line in ('x = 1', '"""module docstring"""', ';'): + buf = BytesIO( + f'{first_line}\n# coding: latin-1\nmsg = _("café")\n'.encode(), + ) + messages = list(extract.extract_python(buf, ('_',), [], {})) + assert [m[2] for m in messages] == ['café'] + + +def test_second_line_coding_after_comment_or_blank_honored(): + for prefix in ('', '#!/usr/bin/env python3', ' # comment'): + buf = BytesIO( + f'{prefix}\n# coding: latin-1\nmsg = _("café")\n'.encode('latin-1'), + ) + messages = list(extract.extract_python(buf, ('_',), [], {})) + assert [m[2] for m in messages] == ['café'] + + +def test_utf8_bom_with_second_line_coding_after_code(): + buf = BytesIO( + codecs.BOM_UTF8 + 'x = 1\n# coding: latin-1\nmsg = _("café")\n'.encode(), + ) + messages = list(extract.extract_python(buf, ('_',), [], {})) + assert [m[2] for m in messages] == ['café'] + + def test_utf8_raw_strings_match_unicode_strings(): buf = BytesIO(codecs.BOM_UTF8 + """ msg = _('Bonjour à tous') diff --git a/tests/test_util.py b/tests/test_util.py index f51626e25..e6561c432 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -73,6 +73,44 @@ def test_parse_encoding_non_ascii(): assert parse_encoding('K\xf6ln') is None +def test_parse_encoding_second_line_allowed(): + for prefix in ( + '\n', + '\r\n', + ' \t\f\n', + '#!/usr/bin/env python3\n', + ' # comment\n', + ): + assert parse_encoding(f'{prefix}# coding: latin-1\n') == 'latin-1' + + assert util.parse_encoding(BytesIO(b'# K\xf6ln\n# coding: latin-1\n')) == 'latin-1' + + +def test_parse_encoding_second_line_ignored_after_code(): + for first_line in ( + 'x = 1\n', + '"""docstring"""\n', + ';\n', + 'pass\n', + '\\\n', + '\x0b\n', + ' x = 1 # trailing comment\n', + ): + assert parse_encoding(f'{first_line}# coding: latin-1\n') is None + + +def test_parse_encoding_bom_with_code_ignores_second_line_coding(): + buf = BytesIO('x = 1\n# coding: latin-1\n'.encode('utf-8-sig')) + assert util.parse_encoding(buf) == 'utf-8' + + +def test_parse_encoding_preserves_stream_position(): + buf = BytesIO(b'x = 1\n# coding: latin-1\n') + buf.seek(5) + assert util.parse_encoding(buf) is None + assert buf.tell() == 5 + + @pytest.mark.parametrize(('source', 'result'), [ (''' from __future__ import print_function,