From 7cf121002aa1c4cefef3ae6a274d14fe14dba8ed Mon Sep 17 00:00:00 2001 From: simpleqt Date: Tue, 6 Oct 2026 18:38:39 +0800 Subject: [PATCH] Warn and skip byte string literals in the Python extractor `_(b'foo')` crashed `pybabel extract` with `TypeError: sequence item 0: expected str instance, bytes found` because `_parse_python_string` returned the raw `bytes` value of an `ast.Constant`, violating its `str | None` annotation, and the value was later fed to `''.join(buf)`. Byte string literals are not translatable messages, so following the behavior agreed in #1190 they are now skipped with a UserWarning instead of aborting the whole extraction run. Fixes #1190. --- babel/messages/extract.py | 13 ++++++++++++- tests/messages/test_extract_python.py | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/babel/messages/extract.py b/babel/messages/extract.py index 6fad84304..433c85959 100644 --- a/babel/messages/extract.py +++ b/babel/messages/extract.py @@ -710,7 +710,18 @@ def _parse_python_string(value: str, encoding: str, future_flags: int) -> str | if isinstance(code, ast.Expression): body = code.body if isinstance(body, ast.Constant): - return body.value + value = body.value + if isinstance(value, bytes): + # Byte string literals (e.g. `_(b'foo')`) are not translatable + # messages; warn and leave them unextracted instead of letting + # the bytes value crash the string buffer later on. + warnings.warn( + f"Byte string literal {value!r} is not a translatable message and was not extracted.", + UserWarning, + stacklevel=2, + ) + return None + return value if isinstance(body, ast.JoinedStr): # f-string if all(isinstance(node, ast.Constant) for node in body.values): return ''.join(node.value for node in body.values) diff --git a/tests/messages/test_extract_python.py b/tests/messages/test_extract_python.py index be1cf970f..6828d1405 100644 --- a/tests/messages/test_extract_python.py +++ b/tests/messages/test_extract_python.py @@ -505,3 +505,19 @@ def test_nested_messages(): assert messages[6][3] == [] assert messages[7][2] == 'Armin' assert messages[7][3] == [] + + +def test_extract_byte_string_literal_warns_and_skips(): + # `_(b'foo')` used to crash the extractor with a TypeError from + # `''.join(buf)` because the bytes value leaked through + # `_parse_python_string`. Byte literals are not translatable messages, so + # they are now skipped with a warning (see #1190). + buf = BytesIO(b"""\ +msg1 = _(b'foo') +msg2 = _('bar') +""") + with pytest.warns(UserWarning, match="Byte string literal"): + messages = list(extract.extract("python", buf, extract.DEFAULT_KEYWORDS, [])) + assert messages == [ + (2, 'bar', [], None), + ]