fix(extract): skip bytes literals in _parse_python_string (#1190) - #1298
fix(extract): skip bytes literals in _parse_python_string (#1190)#1298Mukller wants to merge 4 commits into
Conversation
…el#1190) _parse_python_string() is typed -> str | None but was returning bytes for literals like b'foo'. The caller appended the bytes value to `buf` and the subsequent ''.join(buf) raised: TypeError: sequence item 0: expected str instance, bytes found Guard the return with isinstance(body.value, str) so that byte-string literals are treated the same as other non-extractable expressions (i.e. silently ignored). bytes cannot be used as gettext messages anyway.
Mukller
left a comment
There was a problem hiding this comment.
Code Review
Bug Trace
# source.py: _(b'foo')
# tokenize produces: tok=STRING, value="b'foo'"
# _parse_python_string("b'foo'", encoding='UTF-8', future_flags=0)
# compile("b'foo'", '<string>', 'eval', ...) -> ast.Expression
# body = ast.Constant(value=b'foo') # bytes!
# isinstance(body, ast.Constant) -> True
# return body.value # returns b'foo' (bytes)
# Back in extract_python:
# val = b'foo' (not None, so not skipped)
# buf.append(b'foo') # buf = [b'foo']
# ''.join(buf) # TypeError: expected str, got bytesFix Correctness
# After fix:
if isinstance(body, ast.Constant):
if isinstance(body.value, str): # new guard
return body.value
# bytes falls through to implicit 'return None'| Token | body.value type |
Before | After |
|---|---|---|---|
'foo' |
str |
Returns 'foo' ✓ |
Returns 'foo' ✓ |
b'foo' |
bytes |
Returns b'foo' → crash ✗ |
Returns None → skipped ✓ |
42 |
int |
Returns 42 → crash ✗ |
Returns None → skipped ✓ |
3.14 |
float |
Returns 3.14 → crash ✗ |
Returns None → skipped ✓ |
Note: integer and float ast.Constant values would cause the same crash — this guard also fixes those edge cases.
Scope
- 2 lines changed inside
_parse_python_string(). - No new imports needed.
- Extracted
strmessages are unaffected. - The return type annotation
-> str | Noneis now correct.
| if isinstance(body.value, str): | ||
| return body.value |
There was a problem hiding this comment.
As noted in #1190, it would be useful to warn about an invalid value, instead of quietly ignoring it.
There was a problem hiding this comment.
Done — added a SyntaxWarning in the latest commit. The warning includes the literal value and a note to use a str literal instead:
SyntaxWarning: Bytes literal b'foo' passed to a gettext function; it will be skipped during message extraction. Use a str literal instead.
There was a problem hiding this comment.
Updated - added a SyntaxWarning for non-string, non-bytes constants (e.g. integer or float literals), in addition to the existing bytes warning. Both cases emit a SyntaxWarning with the literal value and are skipped during extraction. Tests for both are included.
Instead of silently returning None, emit a SyntaxWarning so users know their _(b"...") call is being skipped during extraction. Addresses review feedback from @akx on PR python-babel#1298.
Summary
Fixes #1190.
Running
pybabel extracton a file containing_(b'foo')(a byte-string literal as a translation argument) crashes with:Root Cause
_parse_python_string()is annotated as-> str | Nonebut returnsbytesfor byte-string literals. The function compiles the token to anast.Constantand returnsbody.valueunconditionally — forb'foo'that isb'foo'(bytes):The caller in
extract_pythonappends the returned value to a string fragment listbuf, then joins it:Fix
Guard the return with
isinstance(body.value, str)so that byte-string literals returnNoneand are silently skipped, exactly like any other non-extractable expression:Bytes literals are not valid
gettextmessages, so ignoring them (rather than crashing) is the correct and expected behavior.Test