From aa0f056a00c4bcaef83d729e042359ddae903382 Mon Sep 17 00:00:00 2001 From: Matthieu Dartiailh Date: Tue, 5 Apr 2022 15:47:13 +0200 Subject: bpo-47212: Improve error messages for un-parenthesized generator expressions (GH-32302) --- Grammar/python.gram | 6 +++--- Lib/test/test_exceptions.py | 11 ++++++++++- Lib/test/test_syntax.py | 7 +++++++ .../2022-04-05-11-29-21.bpo-47212.leF4pz.rst | 3 +++ Parser/action_helpers.c | 2 +- Parser/parser.c | 6 +++--- Parser/pegen.h | 1 + 7 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2022-04-05-11-29-21.bpo-47212.leF4pz.rst diff --git a/Grammar/python.gram b/Grammar/python.gram index b9965d2..15c40b6 100644 --- a/Grammar/python.gram +++ b/Grammar/python.gram @@ -1073,12 +1073,12 @@ func_type_comment[Token*]: invalid_arguments: | a=args ',' '*' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "iterable argument unpacking follows keyword argument unpacking") } | a=expression b=for_if_clauses ',' [args | expression for_if_clauses] { - RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, PyPegen_last_item(b, comprehension_ty)->target, "Generator expression must be parenthesized") } + RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, _PyPegen_get_last_comprehension_item(PyPegen_last_item(b, comprehension_ty)), "Generator expression must be parenthesized") } | a=NAME b='=' expression for_if_clauses { RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "invalid syntax. Maybe you meant '==' or ':=' instead of '='?")} | a=args b=for_if_clauses { _PyPegen_nonparen_genexp_in_call(p, a, b) } | args ',' a=expression b=for_if_clauses { - RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, asdl_seq_GET(b, b->size-1)->target, "Generator expression must be parenthesized") } + RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, _PyPegen_get_last_comprehension_item(PyPegen_last_item(b, comprehension_ty)), "Generator expression must be parenthesized") } | a=args ',' args { _PyPegen_arguments_parsing_error(p, a) } invalid_kwarg: | a[Token*]=('True'|'False'|'None') b='=' { @@ -1257,7 +1257,7 @@ invalid_finally_stmt: invalid_except_stmt_indent: | a='except' expression ['as' NAME ] ':' NEWLINE !INDENT { RAISE_INDENTATION_ERROR("expected an indented block after 'except' statement on line %d", a->lineno) } - | a='except' ':' NEWLINE !INDENT { RAISE_SYNTAX_ERROR("expected an indented block after except statement on line %d", a->lineno) } + | a='except' ':' NEWLINE !INDENT { RAISE_INDENTATION_ERROR("expected an indented block after 'except' statement on line %d", a->lineno) } invalid_except_star_stmt_indent: | a='except' '*' expression ['as' NAME ] ':' NEWLINE !INDENT { RAISE_INDENTATION_ERROR("expected an indented block after 'except*' statement on line %d", a->lineno) } diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index a75b7fa..6dca79e 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -198,12 +198,17 @@ class ExceptionTests(unittest.TestCase): s = '''if True:\n print()\n\texec "mixed tabs and spaces"''' ckmsg(s, "inconsistent use of tabs and spaces in indentation", TabError) - def check(self, src, lineno, offset, encoding='utf-8'): + def check(self, src, lineno, offset, end_lineno=None, end_offset=None, encoding='utf-8'): with self.subTest(source=src, lineno=lineno, offset=offset): with self.assertRaises(SyntaxError) as cm: compile(src, '', 'exec') self.assertEqual(cm.exception.lineno, lineno) self.assertEqual(cm.exception.offset, offset) + if end_lineno is not None: + self.assertEqual(cm.exception.end_lineno, end_lineno) + if end_offset is not None: + self.assertEqual(cm.exception.end_offset, end_offset) + if cm.exception.text is not None: if not isinstance(src, str): src = src.decode(encoding, 'replace') @@ -235,6 +240,10 @@ class ExceptionTests(unittest.TestCase): check('match ...:\n case {**rest, "key": value}:\n ...', 2, 19) check("[a b c d e f]", 1, 2) check("for x yfff:", 1, 7) + check("f(a for a in b, c)", 1, 3, 1, 15) + check("f(a for a in b if a, c)", 1, 3, 1, 20) + check("f(a, b for b in c)", 1, 6, 1, 18) + check("f(a, b for b in c, d)", 1, 6, 1, 18) # Errors thrown by compile.c check('class foo:return 1', 1, 11) diff --git a/Lib/test/test_syntax.py b/Lib/test/test_syntax.py index 3e79ebf..96e5c12 100644 --- a/Lib/test/test_syntax.py +++ b/Lib/test/test_syntax.py @@ -1309,6 +1309,13 @@ Specialized indentation errors: >>> try: ... something() + ... except: + ... pass + Traceback (most recent call last): + IndentationError: expected an indented block after 'except' statement on line 3 + + >>> try: + ... something() ... except A: ... pass Traceback (most recent call last): diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-04-05-11-29-21.bpo-47212.leF4pz.rst b/Misc/NEWS.d/next/Core and Builtins/2022-04-05-11-29-21.bpo-47212.leF4pz.rst new file mode 100644 index 0000000..8f1f6b6 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-04-05-11-29-21.bpo-47212.leF4pz.rst @@ -0,0 +1,3 @@ +Raise :exc:`IndentationError` instead of :exc:`SyntaxError` for a bare +``except`` with no following indent. Improve :exc:`SyntaxError` locations for +an un-parenthesized generator used as arguments. Patch by Matthieu Dartiailh. diff --git a/Parser/action_helpers.c b/Parser/action_helpers.c index e5d7b66..d1be679 100644 --- a/Parser/action_helpers.c +++ b/Parser/action_helpers.c @@ -1145,7 +1145,7 @@ _PyPegen_get_expr_name(expr_ty e) } } -static inline expr_ty +expr_ty _PyPegen_get_last_comprehension_item(comprehension_ty comprehension) { if (comprehension->ifs == NULL || asdl_seq_LEN(comprehension->ifs) == 0) { return comprehension->iter; diff --git a/Parser/parser.c b/Parser/parser.c index 40c5d62..adc8d50 100644 --- a/Parser/parser.c +++ b/Parser/parser.c @@ -18968,7 +18968,7 @@ invalid_arguments_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ invalid_arguments[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "expression for_if_clauses ',' [args | expression for_if_clauses]")); - _res = RAISE_SYNTAX_ERROR_KNOWN_RANGE ( a , PyPegen_last_item ( b , comprehension_ty ) -> target , "Generator expression must be parenthesized" ); + _res = RAISE_SYNTAX_ERROR_KNOWN_RANGE ( a , _PyPegen_get_last_comprehension_item ( PyPegen_last_item ( b , comprehension_ty ) ) , "Generator expression must be parenthesized" ); if (_res == NULL && PyErr_Occurred()) { p->error_indicator = 1; p->level--; @@ -19061,7 +19061,7 @@ invalid_arguments_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ invalid_arguments[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "args ',' expression for_if_clauses")); - _res = RAISE_SYNTAX_ERROR_KNOWN_RANGE ( a , asdl_seq_GET ( b , b -> size - 1 ) -> target , "Generator expression must be parenthesized" ); + _res = RAISE_SYNTAX_ERROR_KNOWN_RANGE ( a , _PyPegen_get_last_comprehension_item ( PyPegen_last_item ( b , comprehension_ty ) ) , "Generator expression must be parenthesized" ); if (_res == NULL && PyErr_Occurred()) { p->error_indicator = 1; p->level--; @@ -22190,7 +22190,7 @@ invalid_except_stmt_indent_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ invalid_except_stmt_indent[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "'except' ':' NEWLINE !INDENT")); - _res = RAISE_SYNTAX_ERROR ( "expected an indented block after except statement on line %d" , a -> lineno ); + _res = RAISE_INDENTATION_ERROR ( "expected an indented block after 'except' statement on line %d" , a -> lineno ); if (_res == NULL && PyErr_Occurred()) { p->error_indicator = 1; p->level--; diff --git a/Parser/pegen.h b/Parser/pegen.h index 77d5ca8..fe0c327 100644 --- a/Parser/pegen.h +++ b/Parser/pegen.h @@ -324,6 +324,7 @@ int _PyPegen_check_barry_as_flufl(Parser *, Token *); int _PyPegen_check_legacy_stmt(Parser *p, expr_ty t); mod_ty _PyPegen_make_module(Parser *, asdl_stmt_seq *); void *_PyPegen_arguments_parsing_error(Parser *, expr_ty); +expr_ty _PyPegen_get_last_comprehension_item(comprehension_ty comprehension); void *_PyPegen_nonparen_genexp_in_call(Parser *p, expr_ty args, asdl_comprehension_seq *comprehensions); // Parser API -- cgit v0.12