Skip to content

Commit 0c4ccd9

Browse files
committed
bpo-43797: Improve syntax error for invalid comparisons
1 parent 6f37ebc commit 0c4ccd9

File tree

11 files changed

+1140
-640
lines changed

11 files changed

+1140
-640
lines changed

Grammar/python.gram

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,17 +163,20 @@ dotted_name[expr_ty]:
163163
| NAME
164164

165165
if_stmt[stmt_ty]:
166-
| 'if' a=named_expression &&':' b=block c=elif_stmt {
166+
| 'if' a=named_expression ':' b=block c=elif_stmt {
167167
_PyAST_If(a, b, CHECK(asdl_stmt_seq*, _PyPegen_singleton_seq(p, c)), EXTRA) }
168-
| 'if' a=named_expression &&':' b=block c=[else_block] { _PyAST_If(a, b, c, EXTRA) }
168+
| 'if' a=named_expression ':' b=block c=[else_block] { _PyAST_If(a, b, c, EXTRA) }
169+
| invalid_if_stmt
169170
elif_stmt[stmt_ty]:
170-
| 'elif' a=named_expression &&':' b=block c=elif_stmt {
171+
| 'elif' a=named_expression ':' b=block c=elif_stmt {
171172
_PyAST_If(a, b, CHECK(asdl_stmt_seq*, _PyPegen_singleton_seq(p, c)), EXTRA) }
172-
| 'elif' a=named_expression &&':' b=block c=[else_block] { _PyAST_If(a, b, c, EXTRA) }
173+
| 'elif' a=named_expression ':' b=block c=[else_block] { _PyAST_If(a, b, c, EXTRA) }
174+
| invalid_elif_stmt
173175
else_block[asdl_stmt_seq*]: 'else' &&':' b=block { b }
174176

175177
while_stmt[stmt_ty]:
176-
| 'while' a=named_expression &&':' b=block c=[else_block] { _PyAST_While(a, b, c, EXTRA) }
178+
| 'while' a=named_expression ':' b=block c=[else_block] { _PyAST_While(a, b, c, EXTRA) }
179+
| invalid_while_stmt
177180

178181
for_stmt[stmt_ty]:
179182
| 'for' t=star_targets 'in' ~ ex=star_expressions &&':' tc=[TYPE_COMMENT] b=block el=[else_block] {
@@ -438,10 +441,11 @@ star_named_expressions[asdl_expr_seq*]: a[asdl_expr_seq*]=','.star_named_express
438441
star_named_expression[expr_ty]:
439442
| '*' a=bitwise_or { _PyAST_Starred(a, Load, EXTRA) }
440443
| named_expression
444+
441445
named_expression[expr_ty]:
442446
| a=NAME ':=' ~ b=expression { _PyAST_NamedExpr(CHECK(expr_ty, _PyPegen_set_expr_context(p, a, Store)), b, EXTRA) }
443-
| expression !':='
444447
| invalid_named_expression
448+
| expression !':='
445449

446450
annotated_rhs[expr_ty]: yield_expr | star_expressions
447451

@@ -772,6 +776,12 @@ invalid_named_expression:
772776
| a=expression ':=' expression {
773777
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(
774778
a, "cannot use assignment expressions with %s", _PyPegen_get_expr_name(a)) }
779+
| a=NAME b='=' bitwise_or !('='|':='|',') {
780+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(b, "invalid syntax. Maybe you meant '==' or ':=' instead of '='?") }
781+
| !(list|tuple) a=bitwise_or b='=' bitwise_or !('='|':='|',') {
782+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(b, "cannot assign to %s. Maybe you meant '==' instead of '='?",
783+
_PyPegen_get_expr_name(a)) }
784+
775785
invalid_assignment:
776786
| a=invalid_ann_assign_target ':' expression {
777787
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(
@@ -860,6 +870,11 @@ invalid_except_block:
860870

861871
invalid_match_stmt:
862872
| "match" subject_expr !':' { CHECK_VERSION(void*, 10, "Pattern matching is", RAISE_SYNTAX_ERROR("expected ':'") ) }
863-
864873
invalid_case_block:
865874
| "case" patterns guard? !':' { RAISE_SYNTAX_ERROR("expected ':'") }
875+
invalid_if_stmt:
876+
| 'if' named_expression NEWLINE { RAISE_SYNTAX_ERROR("expected ':'") }
877+
invalid_elif_stmt:
878+
| 'elif' named_expression NEWLINE { RAISE_SYNTAX_ERROR("expected ':'") }
879+
invalid_while_stmt:
880+
| 'while' named_expression NEWLINE { RAISE_SYNTAX_ERROR("expected ':'") }

Lib/test/test_cmd_line_script.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ def test_syntaxerror_unindented_caret_position(self):
601601
exitcode, stdout, stderr = assert_python_failure(script_name)
602602
text = io.TextIOWrapper(io.BytesIO(stderr), 'ascii').read()
603603
# Confirm that the caret is located under the first 1 character
604-
self.assertIn("\n 1 + 1 = 2\n ^", text)
604+
self.assertIn("\n 1 + 1 = 2\n ^\n", text)
605605

606606
def test_syntaxerror_indented_caret_position(self):
607607
script = textwrap.dedent("""\
@@ -613,7 +613,7 @@ def test_syntaxerror_indented_caret_position(self):
613613
exitcode, stdout, stderr = assert_python_failure(script_name)
614614
text = io.TextIOWrapper(io.BytesIO(stderr), 'ascii').read()
615615
# Confirm that the caret is located under the first 1 character
616-
self.assertIn("\n 1 + 1 = 2\n ^", text)
616+
self.assertIn("\n 1 + 1 = 2\n ^\n", text)
617617

618618
# Try the same with a form feed at the start of the indented line
619619
script = (
@@ -624,7 +624,7 @@ def test_syntaxerror_indented_caret_position(self):
624624
exitcode, stdout, stderr = assert_python_failure(script_name)
625625
text = io.TextIOWrapper(io.BytesIO(stderr), "ascii").read()
626626
self.assertNotIn("\f", text)
627-
self.assertIn("\n 1 + 1 = 2\n ^", text)
627+
self.assertIn("\n 1 + 1 = 2\n ^\n", text)
628628

629629
def test_syntaxerror_multi_line_fstring(self):
630630
script = 'foo = f"""{}\nfoo"""\n'

Lib/test/test_codeop.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,6 @@ def test_invalid(self):
275275
ai("a = 'a\\\n")
276276

277277
ai("a = 1","eval")
278-
ai("a = (","eval")
279278
ai("]","eval")
280279
ai("())","eval")
281280
ai("[}","eval")

Lib/test/test_exceptions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ def baz():
260260
check('[*x for x in xs]', 1, 2)
261261
check('foo(x for x in range(10), 100)', 1, 5)
262262
check('for 1 in []: pass', 1, 5)
263-
check('(yield i) = 2', 1, 2)
263+
check('(yield i) = 2', 1, 11)
264264
check('def f(*):\n pass', 1, 8)
265265

266266
@cpython_only

Lib/test/test_fstring.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -990,7 +990,7 @@ def test_conversions(self):
990990
])
991991

992992
def test_assignment(self):
993-
self.assertAllRaise(SyntaxError, 'invalid syntax',
993+
self.assertAllRaise(SyntaxError, 'invalid syntax.',
994994
["f'' = 3",
995995
"f'{0}' = x",
996996
"f'{x}' = x",

Lib/test/test_generators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2013,7 +2013,7 @@ def printsolution(self, x):
20132013
>>> def f(): (yield bar) = y
20142014
Traceback (most recent call last):
20152015
...
2016-
SyntaxError: cannot assign to yield expression
2016+
SyntaxError: cannot assign to yield expression. Maybe you meant '==' instead of '='?
20172017
20182018
>>> def f(): (yield bar) += y
20192019
Traceback (most recent call last):

Lib/test/test_genexps.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@
103103
>>> dict(a = i for i in range(10))
104104
Traceback (most recent call last):
105105
...
106-
SyntaxError: invalid syntax
106+
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?
107107
108108
Verify that parenthesis are required when used as a keyword argument value
109109
@@ -153,7 +153,7 @@
153153
>>> (y for y in (1,2)) = 10
154154
Traceback (most recent call last):
155155
...
156-
SyntaxError: cannot assign to generator expression
156+
SyntaxError: cannot assign to generator expression. Maybe you meant '==' instead of '='?
157157
158158
>>> (y for y in (1,2)) += 10
159159
Traceback (most recent call last):

Lib/test/test_syntax.py

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,15 @@
3333
3434
>>> None = 1
3535
Traceback (most recent call last):
36-
SyntaxError: cannot assign to None
36+
SyntaxError: cannot assign to None. Maybe you meant '==' instead of '='?
3737
3838
>>> obj.True = 1
3939
Traceback (most recent call last):
4040
SyntaxError: invalid syntax
4141
4242
>>> True = 1
4343
Traceback (most recent call last):
44-
SyntaxError: cannot assign to True
44+
SyntaxError: cannot assign to True. Maybe you meant '==' instead of '='?
4545
4646
>>> (True := 1)
4747
Traceback (most recent call last):
@@ -61,7 +61,7 @@
6161
6262
>>> f() = 1
6363
Traceback (most recent call last):
64-
SyntaxError: cannot assign to function call
64+
SyntaxError: cannot assign to function call. Maybe you meant '==' instead of '='?
6565
6666
>>> yield = 1
6767
Traceback (most recent call last):
@@ -73,27 +73,27 @@
7373
7474
>>> a + 1 = 2
7575
Traceback (most recent call last):
76-
SyntaxError: cannot assign to operator
76+
SyntaxError: cannot assign to expression. Maybe you meant '==' instead of '='?
7777
7878
>>> (x for x in x) = 1
7979
Traceback (most recent call last):
80-
SyntaxError: cannot assign to generator expression
80+
SyntaxError: cannot assign to generator expression. Maybe you meant '==' instead of '='?
8181
8282
>>> 1 = 1
8383
Traceback (most recent call last):
84-
SyntaxError: cannot assign to literal
84+
SyntaxError: cannot assign to literal. Maybe you meant '==' instead of '='?
8585
8686
>>> "abc" = 1
8787
Traceback (most recent call last):
88-
SyntaxError: cannot assign to literal
88+
SyntaxError: cannot assign to literal. Maybe you meant '==' instead of '='?
8989
9090
>>> b"" = 1
9191
Traceback (most recent call last):
92-
SyntaxError: cannot assign to literal
92+
SyntaxError: cannot assign to literal. Maybe you meant '==' instead of '='?
9393
9494
>>> ... = 1
9595
Traceback (most recent call last):
96-
SyntaxError: cannot assign to Ellipsis
96+
SyntaxError: cannot assign to Ellipsis. Maybe you meant '==' instead of '='?
9797
9898
>>> `1` = 1
9999
Traceback (most recent call last):
@@ -126,15 +126,15 @@
126126
127127
>>> [a, b, c + 1] = [1, 2, 3]
128128
Traceback (most recent call last):
129-
SyntaxError: cannot assign to operator
129+
SyntaxError: cannot assign to expression
130130
131131
>>> [a, b[1], c + 1] = [1, 2, 3]
132132
Traceback (most recent call last):
133-
SyntaxError: cannot assign to operator
133+
SyntaxError: cannot assign to expression
134134
135135
>>> [a, b.c.d, c + 1] = [1, 2, 3]
136136
Traceback (most recent call last):
137-
SyntaxError: cannot assign to operator
137+
SyntaxError: cannot assign to expression
138138
139139
>>> a if 1 else b = 1
140140
Traceback (most recent call last):
@@ -181,7 +181,7 @@
181181
182182
>>> for (*a, b, c+1) in b: pass
183183
Traceback (most recent call last):
184-
SyntaxError: cannot assign to operator
184+
SyntaxError: cannot assign to expression
185185
186186
>>> for (x, *(y, z.d())) in b: pass
187187
Traceback (most recent call last):
@@ -193,7 +193,7 @@
193193
194194
>>> for a, b, (c + 1, d()): pass
195195
Traceback (most recent call last):
196-
SyntaxError: cannot assign to operator
196+
SyntaxError: cannot assign to expression
197197
198198
>>> for i < (): pass
199199
Traceback (most recent call last):
@@ -217,7 +217,7 @@
217217
218218
>>> with a as (*b, c, d+1): pass
219219
Traceback (most recent call last):
220-
SyntaxError: cannot assign to operator
220+
SyntaxError: cannot assign to expression
221221
222222
>>> with a as (x, *(y, z.d())): pass
223223
Traceback (most recent call last):
@@ -465,7 +465,7 @@
465465
# SyntaxError: expression cannot contain assignment, perhaps you meant "=="?
466466
# >>> f(True=2)
467467
# Traceback (most recent call last):
468-
# SyntaxError: cannot assign to True
468+
# SyntaxError: cannot assign to True. Maybe you meant '==' instead of '='?
469469
>>> f(__debug__=1)
470470
Traceback (most recent call last):
471471
SyntaxError: cannot assign to __debug__
@@ -684,15 +684,15 @@
684684
... pass
685685
Traceback (most recent call last):
686686
...
687-
SyntaxError: cannot assign to function call
687+
SyntaxError: cannot assign to function call. Maybe you meant '==' instead of '='?
688688
689689
>>> if 1:
690690
... pass
691691
... elif 1:
692692
... x() = 1
693693
Traceback (most recent call last):
694694
...
695-
SyntaxError: cannot assign to function call
695+
SyntaxError: cannot assign to function call. Maybe you meant '==' instead of '='?
696696
697697
>>> if 1:
698698
... x() = 1
@@ -702,7 +702,7 @@
702702
... pass
703703
Traceback (most recent call last):
704704
...
705-
SyntaxError: cannot assign to function call
705+
SyntaxError: cannot assign to function call. Maybe you meant '==' instead of '='?
706706
707707
>>> if 1:
708708
... pass
@@ -712,7 +712,7 @@
712712
... pass
713713
Traceback (most recent call last):
714714
...
715-
SyntaxError: cannot assign to function call
715+
SyntaxError: cannot assign to function call. Maybe you meant '==' instead of '='?
716716
717717
>>> if 1:
718718
... pass
@@ -722,7 +722,7 @@
722722
... x() = 1
723723
Traceback (most recent call last):
724724
...
725-
SyntaxError: cannot assign to function call
725+
SyntaxError: cannot assign to function call. Maybe you meant '==' instead of '='?
726726
727727
Missing ':' before suites:
728728
@@ -894,19 +894,19 @@
894894
895895
>>> {1, 2, 3} = 42
896896
Traceback (most recent call last):
897-
SyntaxError: cannot assign to set display
897+
SyntaxError: cannot assign to set display. Maybe you meant '==' instead of '='?
898898
899899
>>> {1: 2, 3: 4} = 42
900900
Traceback (most recent call last):
901-
SyntaxError: cannot assign to dict display
901+
SyntaxError: cannot assign to dict display. Maybe you meant '==' instead of '='?
902902
903903
>>> f'{x}' = 42
904904
Traceback (most recent call last):
905-
SyntaxError: cannot assign to f-string expression
905+
SyntaxError: cannot assign to f-string expression. Maybe you meant '==' instead of '='?
906906
907907
>>> f'{x}-{y}' = 42
908908
Traceback (most recent call last):
909-
SyntaxError: cannot assign to f-string expression
909+
SyntaxError: cannot assign to f-string expression. Maybe you meant '==' instead of '='?
910910
911911
>>> from t import x,
912912
Traceback (most recent call last):
@@ -988,7 +988,7 @@ def _check_error(self, code, errtext,
988988
def test_expression_with_assignment(self):
989989
self._check_error(
990990
"print(end1 + end2 = ' ')",
991-
'expression cannot contain assignment, perhaps you meant "=="?',
991+
"cannot assign to expression. Maybe you meant '==' instead of '='?",
992992
offset=19
993993
)
994994

@@ -1015,15 +1015,15 @@ def test_assign_del(self):
10151015
self._check_error("del f(), x", "delete function call")
10161016
self._check_error("del [a, b, ((c), (d,), e.f())]", "delete function call")
10171017
self._check_error("del (a if True else b)", "delete conditional")
1018-
self._check_error("del +a", "delete operator")
1019-
self._check_error("del a, +b", "delete operator")
1020-
self._check_error("del a + b", "delete operator")
1021-
self._check_error("del (a + b, c)", "delete operator")
1022-
self._check_error("del (c[0], a + b)", "delete operator")
1023-
self._check_error("del a.b.c + 2", "delete operator")
1024-
self._check_error("del a.b.c[0] + 2", "delete operator")
1025-
self._check_error("del (a, b, (c, d.e.f + 2))", "delete operator")
1026-
self._check_error("del [a, b, (c, d.e.f[0] + 2)]", "delete operator")
1018+
self._check_error("del +a", "delete expression")
1019+
self._check_error("del a, +b", "delete expression")
1020+
self._check_error("del a + b", "delete expression")
1021+
self._check_error("del (a + b, c)", "delete expression")
1022+
self._check_error("del (c[0], a + b)", "delete expression")
1023+
self._check_error("del a.b.c + 2", "delete expression")
1024+
self._check_error("del a.b.c[0] + 2", "delete expression")
1025+
self._check_error("del (a, b, (c, d.e.f + 2))", "delete expression")
1026+
self._check_error("del [a, b, (c, d.e.f[0] + 2)]", "delete expression")
10271027
self._check_error("del (a := 5)", "delete named expression")
10281028
# We don't have a special message for this, but make sure we don't
10291029
# report "cannot delete name"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improve ``SyntaxError`` error messages for invalid comparisons. Patch by
2+
Pablo Galindo.

0 commit comments

Comments
 (0)