From 58159ef856846d0235e0779aeb6013d70499570d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 12 Jan 2019 09:46:50 +0200 Subject: [PATCH] bpo-35494: Improve syntax error messages for unbalanced parentheses in f-string. (GH-11161) --- Lib/test/test_fstring.py | 36 ++++++++++++------- .../2018-12-14-18-02-34.bpo-35494.IWOPtb.rst | 1 + Python/ast.c | 31 +++++++++++++--- 3 files changed, 51 insertions(+), 17 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2018-12-14-18-02-34.bpo-35494.IWOPtb.rst diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index fe3804b2215..9e45770f80b 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -368,9 +368,27 @@ non-important content ]) def test_mismatched_parens(self): - self.assertAllRaise(SyntaxError, 'f-string: mismatched', + self.assertAllRaise(SyntaxError, r"f-string: closing parenthesis '\}' " + r"does not match opening parenthesis '\('", ["f'{((}'", ]) + self.assertAllRaise(SyntaxError, r"f-string: closing parenthesis '\)' " + r"does not match opening parenthesis '\['", + ["f'{a[4)}'", + ]) + self.assertAllRaise(SyntaxError, r"f-string: closing parenthesis '\]' " + r"does not match opening parenthesis '\('", + ["f'{a(4]}'", + ]) + self.assertAllRaise(SyntaxError, r"f-string: closing parenthesis '\}' " + r"does not match opening parenthesis '\['", + ["f'{a[4}'", + ]) + self.assertAllRaise(SyntaxError, r"f-string: closing parenthesis '\}' " + r"does not match opening parenthesis '\('", + ["f'{a(4}'", + ]) + self.assertRaises(SyntaxError, eval, "f'{" + "("*500 + "}'") def test_double_braces(self): self.assertEqual(f'{{', '{') @@ -448,7 +466,9 @@ non-important content ["f'{1#}'", # error because the expression becomes "(1#)" "f'{3(#)}'", "f'{#}'", - "f'{)#}'", # When wrapped in parens, this becomes + ]) + self.assertAllRaise(SyntaxError, r"f-string: unmatched '\)'", + ["f'{)#}'", # When wrapped in parens, this becomes # '()#)'. Make sure that doesn't compile. ]) @@ -577,7 +597,7 @@ non-important content "f'{,}'", # this is (,), which is an error ]) - self.assertAllRaise(SyntaxError, "f-string: expecting '}'", + self.assertAllRaise(SyntaxError, r"f-string: unmatched '\)'", ["f'{3)+(4}'", ]) @@ -1003,16 +1023,6 @@ non-important content self.assertEqual('{d[a]}'.format(d=d), 'string') self.assertEqual('{d[0]}'.format(d=d), 'integer') - def test_invalid_expressions(self): - self.assertAllRaise(SyntaxError, - r"closing parenthesis '\)' does not match " - r"opening parenthesis '\[' \(, line 1\)", - [r"f'{a[4)}'"]) - self.assertAllRaise(SyntaxError, - r"closing parenthesis '\]' does not match " - r"opening parenthesis '\(' \(, line 1\)", - [r"f'{a(4]}'"]) - def test_errors(self): # see issue 26287 self.assertAllRaise(TypeError, 'unsupported', diff --git a/Misc/NEWS.d/next/Core and Builtins/2018-12-14-18-02-34.bpo-35494.IWOPtb.rst b/Misc/NEWS.d/next/Core and Builtins/2018-12-14-18-02-34.bpo-35494.IWOPtb.rst new file mode 100644 index 00000000000..0813b35ec87 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2018-12-14-18-02-34.bpo-35494.IWOPtb.rst @@ -0,0 +1 @@ +Improved syntax error messages for unbalanced parentheses in f-string. diff --git a/Python/ast.c b/Python/ast.c index 8a305a80ffa..69dfe3c3c43 100644 --- a/Python/ast.c +++ b/Python/ast.c @@ -13,6 +13,8 @@ #include #include +#define MAXLEVEL 200 /* Max parentheses level */ + static int validate_stmts(asdl_seq *); static int validate_exprs(asdl_seq *, expr_context_ty, int); 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, /* Keep track of nesting level for braces/parens/brackets in expressions. */ Py_ssize_t nested_depth = 0; + char parenstack[MAXLEVEL]; /* Can only nest one level deep. */ if (recurse_lvl >= 2) { @@ -4553,10 +4556,12 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, /* Start looking for the end of the string. */ quote_char = ch; } else if (ch == '[' || ch == '{' || ch == '(') { + if (nested_depth >= MAXLEVEL) { + ast_error(c, n, "f-string: too many nested parenthesis"); + return -1; + } + parenstack[nested_depth] = ch; nested_depth++; - } else if (nested_depth != 0 && - (ch == ']' || ch == '}' || ch == ')')) { - nested_depth--; } else if (ch == '#') { /* Error: can't include a comment character, inside parens or not. */ @@ -4573,6 +4578,23 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, } /* Normal way out of this loop. */ break; + } else if (ch == ']' || ch == '}' || ch == ')') { + if (!nested_depth) { + ast_error(c, n, "f-string: unmatched '%c'", ch); + return -1; + } + nested_depth--; + int opening = parenstack[nested_depth]; + if (!((opening == '(' && ch == ')') || + (opening == '[' && ch == ']') || + (opening == '{' && ch == '}'))) + { + ast_error(c, n, + "f-string: closing parenthesis '%c' " + "does not match opening parenthesis '%c'", + ch, opening); + return -1; + } } else { /* Just consume this char and loop around. */ } @@ -4587,7 +4609,8 @@ fstring_find_expr(const char **str, const char *end, int raw, int recurse_lvl, return -1; } if (nested_depth) { - ast_error(c, n, "f-string: mismatched '(', '{', or '['"); + int opening = parenstack[nested_depth - 1]; + ast_error(c, n, "f-string: unmatched '%c'", opening); return -1; }