Skip to content

Commit 0b5f026

Browse files
[3.15] gh-160491: Fix location of invalid escapes in f-strings and t-strings (GH-160493) (#160530)
Literal parts of f-strings and t-strings are decoded only after the whole string has been parsed, so a decode error took the location of the last token. The resulting SyntaxError pointed at the closing quote, and for multi-line strings it could be on the wrong line. Raise the error at the literal part being decoded instead. (cherry picked from commit 2258026) Co-authored-by: Lysandros Nikolaou <lisandrosnik@gmail.com>
1 parent db7ec46 commit 0b5f026

5 files changed

Lines changed: 41 additions & 3 deletions

File tree

‎Lib/test/test_fstring.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,24 @@ def test_misformed_unicode_character_name(self):
10741074
r"'\N{GREEK CAPITAL LETTER DELTA'",
10751075
])
10761076

1077+
def test_invalid_escape_location(self):
1078+
cases = [
1079+
(r'f"\xZZ"', (1, 3, 1, 7)),
1080+
(r'f"{x}\xZZ"', (1, 6, 1, 10)),
1081+
(r'f"a{x}b\N{NOPE}{y}"', (1, 7, 1, 16)),
1082+
(r'f"ok" f"\xZZ"', (1, 9, 1, 13)),
1083+
('f"""\\xZZ\nabc"""', (1, 5, 2, 4)),
1084+
('f"""abc\ndef\\xZZ"""', (1, 5, 2, 8)),
1085+
('x = (1,\n f"\\xZZ")', (2, 5, 2, 9)),
1086+
]
1087+
for source, location in cases:
1088+
with self.subTest(source=source):
1089+
with self.assertRaisesRegex(SyntaxError, r'\(unicode error\)') as cm:
1090+
compile(source, '<string>', 'exec')
1091+
error = cm.exception
1092+
self.assertEqual((error.lineno, error.offset,
1093+
error.end_lineno, error.end_offset), location)
1094+
10771095
def test_backslashes_in_expression_part(self):
10781096
self.assertEqual(f"{(
10791097
1 +
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix the location reported for invalid escape sequences in f-strings and
2+
t-strings. The :exc:`SyntaxError` now points at the literal part that
3+
contains the escape, instead of at the closing quote.

‎Parser/action_helpers.c‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1332,7 +1332,7 @@ _PyPegen_decode_fstring_part(Parser* p, int is_raw, expr_ty constant, Token* tok
13321332
is_raw = is_raw || strchr(bstr, '\\') == NULL;
13331333
PyObject *str = _PyPegen_decode_string(p, is_raw, bstr, len, token);
13341334
if (str == NULL) {
1335-
_Pypegen_raise_decode_error(p);
1335+
_Pypegen_raise_decode_error_known_location(p, constant);
13361336
return NULL;
13371337
}
13381338
if (_PyArena_AddPyObject(p->arena, str) < 0) {

‎Parser/pegen.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ typedef enum {
174174
} TARGETS_TYPE;
175175

176176
int _Pypegen_raise_decode_error(Parser *p);
177+
int _Pypegen_raise_decode_error_known_location(Parser *p, expr_ty a);
177178
int _Pypegen_tokenizer_error(Parser *p);
178179
void *_PyPegen_raise_error(Parser *p, PyObject *errtype, int use_mark, const char *errmsg, ...);
179180
void *_PyPegen_raise_error_known_location(Parser *p, PyObject *errtype,

‎Parser/pegen_errors.c‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ _Pypegen_tokenizer_error(Parser *p)
8282

8383
int
8484
_Pypegen_raise_decode_error(Parser *p)
85+
{
86+
return _Pypegen_raise_decode_error_known_location(p, NULL);
87+
}
88+
89+
int
90+
_Pypegen_raise_decode_error_known_location(Parser *p, expr_ty a)
8591
{
8692
assert(PyErr_Occurred());
8793
const char *errtype = NULL;
@@ -99,12 +105,22 @@ _Pypegen_raise_decode_error(Parser *p)
99105
PyErr_Fetch(&type, &value, &tback);
100106
errstr = PyObject_Str(value);
101107
if (errstr) {
102-
RAISE_SYNTAX_ERROR("(%s) %U", errtype, errstr);
108+
if (a != NULL) {
109+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "(%s) %U", errtype, errstr);
110+
}
111+
else {
112+
RAISE_SYNTAX_ERROR("(%s) %U", errtype, errstr);
113+
}
103114
Py_DECREF(errstr);
104115
}
105116
else {
106117
PyErr_Clear();
107-
RAISE_SYNTAX_ERROR("(%s) unknown error", errtype);
118+
if (a != NULL) {
119+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "(%s) unknown error", errtype);
120+
}
121+
else {
122+
RAISE_SYNTAX_ERROR("(%s) unknown error", errtype);
123+
}
108124
}
109125
Py_XDECREF(type);
110126
Py_XDECREF(value);

0 commit comments

Comments
 (0)