Skip to content

Commit 58159ef

Browse files
bpo-35494: Improve syntax error messages for unbalanced parentheses in f-string. (GH-11161)
1 parent 44cc482 commit 58159ef

File tree

3 files changed

+51
-17
lines changed

3 files changed

+51
-17
lines changed

Lib/test/test_fstring.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -368,9 +368,27 @@ def test_unterminated_string(self):
368368
])
369369

370370
def test_mismatched_parens(self):
371-
self.assertAllRaise(SyntaxError, 'f-string: mismatched',
371+
self.assertAllRaise(SyntaxError, r"f-string: closing parenthesis '\}' "
372+
r"does not match opening parenthesis '\('",
372373
["f'{((}'",
373374
])
375+
self.assertAllRaise(SyntaxError, r"f-string: closing parenthesis '\)' "
376+
r"does not match opening parenthesis '\['",
377+
["f'{a[4)}'",
378+
])
379+
self.assertAllRaise(SyntaxError, r"f-string: closing parenthesis '\]' "
380+
r"does not match opening parenthesis '\('",
381+
["f'{a(4]}'",
382+
])
383+
self.assertAllRaise(SyntaxError, r"f-string: closing parenthesis '\}' "
384+
r"does not match opening parenthesis '\['",
385+
["f'{a[4}'",
386+
])
387+
self.assertAllRaise(SyntaxError, r"f-string: closing parenthesis '\}' "
388+
r"does not match opening parenthesis '\('",
389+
["f'{a(4}'",
390+
])
391+
self.assertRaises(SyntaxError, eval, "f'{" + "("*500 + "}'")
374392

375393
def test_double_braces(self):
376394
self.assertEqual(f'{{', '{')
@@ -448,7 +466,9 @@ def test_comments(self):
448466
["f'{1#}'", # error because the expression becomes "(1#)"
449467
"f'{3(#)}'",
450468
"f'{#}'",
451-
"f'{)#}'", # When wrapped in parens, this becomes
469+
])
470+
self.assertAllRaise(SyntaxError, r"f-string: unmatched '\)'",
471+
["f'{)#}'", # When wrapped in parens, this becomes
452472
# '()#)'. Make sure that doesn't compile.
453473
])
454474

@@ -577,7 +597,7 @@ def test_parens_in_expressions(self):
577597
"f'{,}'", # this is (,), which is an error
578598
])
579599

580-
self.assertAllRaise(SyntaxError, "f-string: expecting '}'",
600+
self.assertAllRaise(SyntaxError, r"f-string: unmatched '\)'",
581601
["f'{3)+(4}'",
582602
])
583603

@@ -1003,16 +1023,6 @@ def test_str_format_differences(self):
10031023
self.assertEqual('{d[a]}'.format(d=d), 'string')
10041024
self.assertEqual('{d[0]}'.format(d=d), 'integer')
10051025

1006-
def test_invalid_expressions(self):
1007-
self.assertAllRaise(SyntaxError,
1008-
r"closing parenthesis '\)' does not match "
1009-
r"opening parenthesis '\[' \(<fstring>, line 1\)",
1010-
[r"f'{a[4)}'"])
1011-
self.assertAllRaise(SyntaxError,
1012-
r"closing parenthesis '\]' does not match "
1013-
r"opening parenthesis '\(' \(<fstring>, line 1\)",
1014-
[r"f'{a(4]}'"])
1015-
10161026
def test_errors(self):
10171027
# see issue 26287
10181028
self.assertAllRaise(TypeError, 'unsupported',
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improved syntax error messages for unbalanced parentheses in f-string.

Python/ast.c

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
#include <assert.h>
1414
#include <stdbool.h>
1515

16+
#define MAXLEVEL 200 /* Max parentheses level */
17+
1618
static int validate_stmts(asdl_seq *);
1719
static int validate_exprs(asdl_seq *, expr_context_ty, int);
1820
static int validate_nonempty_seq(asdl_seq *, const char *, const char *);
@@ -4479,6 +4481,7 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
44794481
/* Keep track of nesting level for braces/parens/brackets in
44804482
expressions. */
44814483
Py_ssize_t nested_depth = 0;
4484+
char parenstack[MAXLEVEL];
44824485

44834486
/* Can only nest one level deep. */
44844487
if (recurse_lvl >= 2) {
@@ -4553,10 +4556,12 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
45534556
/* Start looking for the end of the string. */
45544557
quote_char = ch;
45554558
} else if (ch == '[' || ch == '{' || ch == '(') {
4559+
if (nested_depth >= MAXLEVEL) {
4560+
ast_error(c, n, "f-string: too many nested parenthesis");
4561+
return -1;
4562+
}
4563+
parenstack[nested_depth] = ch;
45564564
nested_depth++;
4557-
} else if (nested_depth != 0 &&
4558-
(ch == ']' || ch == '}' || ch == ')')) {
4559-
nested_depth--;
45604565
} else if (ch == '#') {
45614566
/* Error: can't include a comment character, inside parens
45624567
or not. */
@@ -4573,6 +4578,23 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
45734578
}
45744579
/* Normal way out of this loop. */
45754580
break;
4581+
} else if (ch == ']' || ch == '}' || ch == ')') {
4582+
if (!nested_depth) {
4583+
ast_error(c, n, "f-string: unmatched '%c'", ch);
4584+
return -1;
4585+
}
4586+
nested_depth--;
4587+
int opening = parenstack[nested_depth];
4588+
if (!((opening == '(' && ch == ')') ||
4589+
(opening == '[' && ch == ']') ||
4590+
(opening == '{' && ch == '}')))
4591+
{
4592+
ast_error(c, n,
4593+
"f-string: closing parenthesis '%c' "
4594+
"does not match opening parenthesis '%c'",
4595+
ch, opening);
4596+
return -1;
4597+
}
45764598
} else {
45774599
/* Just consume this char and loop around. */
45784600
}
@@ -4587,7 +4609,8 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl,
45874609
return -1;
45884610
}
45894611
if (nested_depth) {
4590-
ast_error(c, n, "f-string: mismatched '(', '{', or '['");
4612+
int opening = parenstack[nested_depth - 1];
4613+
ast_error(c, n, "f-string: unmatched '%c'", opening);
45914614
return -1;
45924615
}
45934616

0 commit comments

Comments
 (0)