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), + ]