[Python-checkins] bpo-35494: Improve syntax error messages for unbalanced parentheses in f-string. (GH-11161)

Serhiy Storchaka webhook-mailer at python.org
Sat Jan 12 02:46:53 EST 2019


https://github.com/python/cpython/commit/58159ef856846d0235e0779aeb6013d70499570d
commit: 58159ef856846d0235e0779aeb6013d70499570d
branch: master
author: Serhiy Storchaka <storchaka at gmail.com>
committer: GitHub <noreply at github.com>
date: 2019-01-12T09:46:50+02:00
summary:

bpo-35494: Improve syntax error messages for unbalanced parentheses in f-string. (GH-11161)

files:
A Misc/NEWS.d/next/Core and Builtins/2018-12-14-18-02-34.bpo-35494.IWOPtb.rst
M Lib/test/test_fstring.py
M Python/ast.c

diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py
index fe3804b2215d..9e45770f80b8 100644
--- a/Lib/test/test_fstring.py
+++ b/Lib/test/test_fstring.py
@@ -368,9 +368,27 @@ def test_unterminated_string(self):
                              ])
 
     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 @@ def test_comments(self):
                             ["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 @@ def test_parens_in_expressions(self):
                              "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 @@ def test_str_format_differences(self):
         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 '\[' \(<fstring>, line 1\)",
-                            [r"f'{a[4)}'"])
-        self.assertAllRaise(SyntaxError,
-                            r"closing parenthesis '\]' does not match "
-                            r"opening parenthesis '\(' \(<fstring>, 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 000000000000..0813b35ec87d
--- /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 8a305a80ffac..69dfe3c3c435 100644
--- a/Python/ast.c
+++ b/Python/ast.c
@@ -13,6 +13,8 @@
 #include <assert.h>
 #include <stdbool.h>
 
+#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;
     }
 



More information about the Python-checkins mailing list