Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5dbe0f5

Browse files
authored
bpo-37757: Disallow PEP 572 cases that expose implementation details (GH-15131)
- drop TargetScopeError in favour of raising SyntaxError directly as per the updated PEP 572 - comprehension iteration variables are explicitly local, but named expression targets in comprehensions are nonlocal or global. Raise SyntaxError as specified in PEP 572 - named expression targets in the outermost iterable of a comprehension have an ambiguous target scope. Avoid resolving that question now by raising SyntaxError. PEP 572 originally required this only for cases where the bound name conflicts with the iteration variable in the comprehension, but CPython can't easily restrict the exception to that case (as it doesn't know the target variable names when visiting the outermost iterator expression)
1 parent ce6a070 commit 5dbe0f5

File tree

10 files changed

+191
-76
lines changed

10 files changed

+191
-76
lines changed

Doc/c-api/exceptions.rst

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -809,7 +809,6 @@ the variables:
809809
single: PyExc_SystemError
810810
single: PyExc_SystemExit
811811
single: PyExc_TabError
812-
single: PyExc_TargetScopeError
813812
single: PyExc_TimeoutError
814813
single: PyExc_TypeError
815814
single: PyExc_UnboundLocalError
@@ -911,8 +910,6 @@ the variables:
911910
+-----------------------------------------+---------------------------------+----------+
912911
| :c:data:`PyExc_TabError` | :exc:`TabError` | |
913912
+-----------------------------------------+---------------------------------+----------+
914-
| :c:data:`PyExc_TargetScopeError` | :exc:`TargetScopeError` | |
915-
+-----------------------------------------+---------------------------------+----------+
916913
| :c:data:`PyExc_TimeoutError` | :exc:`TimeoutError` | |
917914
+-----------------------------------------+---------------------------------+----------+
918915
| :c:data:`PyExc_TypeError` | :exc:`TypeError` | |

Include/pyerrors.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ PyAPI_DATA(PyObject *) PyExc_NotImplementedError;
9797
PyAPI_DATA(PyObject *) PyExc_SyntaxError;
9898
PyAPI_DATA(PyObject *) PyExc_IndentationError;
9999
PyAPI_DATA(PyObject *) PyExc_TabError;
100-
PyAPI_DATA(PyObject *) PyExc_TargetScopeError;
101100
PyAPI_DATA(PyObject *) PyExc_ReferenceError;
102101
PyAPI_DATA(PyObject *) PyExc_SystemError;
103102
PyAPI_DATA(PyObject *) PyExc_SystemExit;

Include/symtable.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ typedef struct _symtable_entry {
5858
unsigned ste_needs_class_closure : 1; /* for class scopes, true if a
5959
closure over __class__
6060
should be created */
61+
unsigned ste_comp_iter_target : 1; /* true if visiting comprehension target */
62+
int ste_comp_iter_expr; /* non-zero if visiting a comprehension range expression */
6163
int ste_lineno; /* first line of block */
6264
int ste_col_offset; /* offset of first line of block */
6365
int ste_opt_lineno; /* lineno of last exec or import * */
@@ -94,6 +96,7 @@ PyAPI_FUNC(void) PySymtable_Free(struct symtable *);
9496
#define DEF_FREE_CLASS 2<<5 /* free variable from class's method */
9597
#define DEF_IMPORT 2<<6 /* assignment occurred via import */
9698
#define DEF_ANNOT 2<<7 /* this name is annotated */
99+
#define DEF_COMP_ITER 2<<8 /* this name is a comprehension iteration variable */
97100

98101
#define DEF_BOUND (DEF_LOCAL | DEF_PARAM | DEF_IMPORT)
99102

Lib/_compat_pickle.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@
128128
"SystemError",
129129
"SystemExit",
130130
"TabError",
131-
"TargetScopeError",
132131
"TypeError",
133132
"UnboundLocalError",
134133
"UnicodeDecodeError",

Lib/test/exception_hierarchy.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ BaseException
4242
| +-- NotImplementedError
4343
| +-- RecursionError
4444
+-- SyntaxError
45-
| +-- TargetScopeError
4645
| +-- IndentationError
4746
| +-- TabError
4847
+-- SystemError

Lib/test/test_named_expressions.py

Lines changed: 84 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -104,15 +104,69 @@ def test_named_expression_invalid_17(self):
104104
with self.assertRaisesRegex(SyntaxError, "invalid syntax"):
105105
exec(code, {}, {})
106106

107-
def test_named_expression_invalid_18(self):
107+
def test_named_expression_invalid_in_class_body(self):
108108
code = """class Foo():
109109
[(42, 1 + ((( j := i )))) for i in range(5)]
110110
"""
111111

112-
with self.assertRaisesRegex(TargetScopeError,
113-
"named expression within a comprehension cannot be used in a class body"):
112+
with self.assertRaisesRegex(SyntaxError,
113+
"assignment expression within a comprehension cannot be used in a class body"):
114114
exec(code, {}, {})
115115

116+
def test_named_expression_invalid_rebinding_comprehension_iteration_variable(self):
117+
cases = [
118+
("Local reuse", 'i', "[i := 0 for i in range(5)]"),
119+
("Nested reuse", 'j', "[[(j := 0) for i in range(5)] for j in range(5)]"),
120+
("Reuse inner loop target", 'j', "[(j := 0) for i in range(5) for j in range(5)]"),
121+
("Unpacking reuse", 'i', "[i := 0 for i, j in [(0, 1)]]"),
122+
("Reuse in loop condition", 'i', "[i+1 for i in range(5) if (i := 0)]"),
123+
("Unreachable reuse", 'i', "[False or (i:=0) for i in range(5)]"),
124+
("Unreachable nested reuse", 'i',
125+
"[(i, j) for i in range(5) for j in range(5) if True or (i:=10)]"),
126+
]
127+
for case, target, code in cases:
128+
msg = f"assignment expression cannot rebind comprehension iteration variable '{target}'"
129+
with self.subTest(case=case):
130+
with self.assertRaisesRegex(SyntaxError, msg):
131+
exec(code, {}, {})
132+
133+
def test_named_expression_invalid_rebinding_comprehension_inner_loop(self):
134+
cases = [
135+
("Inner reuse", 'j', "[i for i in range(5) if (j := 0) for j in range(5)]"),
136+
("Inner unpacking reuse", 'j', "[i for i in range(5) if (j := 0) for j, k in [(0, 1)]]"),
137+
]
138+
for case, target, code in cases:
139+
msg = f"comprehension inner loop cannot rebind assignment expression target '{target}'"
140+
with self.subTest(case=case):
141+
with self.assertRaisesRegex(SyntaxError, msg):
142+
exec(code, {}) # Module scope
143+
with self.assertRaisesRegex(SyntaxError, msg):
144+
exec(code, {}, {}) # Class scope
145+
with self.assertRaisesRegex(SyntaxError, msg):
146+
exec(f"lambda: {code}", {}) # Function scope
147+
148+
def test_named_expression_invalid_comprehension_iterable_expression(self):
149+
cases = [
150+
("Top level", "[i for i in (i := range(5))]"),
151+
("Inside tuple", "[i for i in (2, 3, i := range(5))]"),
152+
("Inside list", "[i for i in [2, 3, i := range(5)]]"),
153+
("Different name", "[i for i in (j := range(5))]"),
154+
("Lambda expression", "[i for i in (lambda:(j := range(5)))()]"),
155+
("Inner loop", "[i for i in range(5) for j in (i := range(5))]"),
156+
("Nested comprehension", "[i for i in [j for j in (k := range(5))]]"),
157+
("Nested comprehension condition", "[i for i in [j for j in range(5) if (j := True)]]"),
158+
("Nested comprehension body", "[i for i in [(j := True) for j in range(5)]]"),
159+
]
160+
msg = "assignment expression cannot be used in a comprehension iterable expression"
161+
for case, code in cases:
162+
with self.subTest(case=case):
163+
with self.assertRaisesRegex(SyntaxError, msg):
164+
exec(code, {}) # Module scope
165+
with self.assertRaisesRegex(SyntaxError, msg):
166+
exec(code, {}, {}) # Class scope
167+
with self.assertRaisesRegex(SyntaxError, msg):
168+
exec(f"lambda: {code}", {}) # Function scope
169+
116170

117171
class NamedExpressionAssignmentTest(unittest.TestCase):
118172

@@ -306,39 +360,6 @@ def test_named_expression_scope_11(self):
306360
self.assertEqual(res, [0, 1, 2, 3, 4])
307361
self.assertEqual(j, 4)
308362

309-
def test_named_expression_scope_12(self):
310-
res = [i := i for i in range(5)]
311-
312-
self.assertEqual(res, [0, 1, 2, 3, 4])
313-
self.assertEqual(i, 4)
314-
315-
def test_named_expression_scope_13(self):
316-
res = [i := 0 for i, j in [(1, 2), (3, 4)]]
317-
318-
self.assertEqual(res, [0, 0])
319-
self.assertEqual(i, 0)
320-
321-
def test_named_expression_scope_14(self):
322-
res = [(i := 0, j := 1) for i, j in [(1, 2), (3, 4)]]
323-
324-
self.assertEqual(res, [(0, 1), (0, 1)])
325-
self.assertEqual(i, 0)
326-
self.assertEqual(j, 1)
327-
328-
def test_named_expression_scope_15(self):
329-
res = [(i := i, j := j) for i, j in [(1, 2), (3, 4)]]
330-
331-
self.assertEqual(res, [(1, 2), (3, 4)])
332-
self.assertEqual(i, 3)
333-
self.assertEqual(j, 4)
334-
335-
def test_named_expression_scope_16(self):
336-
res = [(i := j, j := i) for i, j in [(1, 2), (3, 4)]]
337-
338-
self.assertEqual(res, [(2, 2), (4, 4)])
339-
self.assertEqual(i, 4)
340-
self.assertEqual(j, 4)
341-
342363
def test_named_expression_scope_17(self):
343364
b = 0
344365
res = [b := i + b for i in range(5)]
@@ -421,6 +442,33 @@ def spam():
421442

422443
self.assertEqual(ns["a"], 20)
423444

445+
def test_named_expression_variable_reuse_in_comprehensions(self):
446+
# The compiler is expected to raise syntax error for comprehension
447+
# iteration variables, but should be fine with rebinding of other
448+
# names (e.g. globals, nonlocals, other assignment expressions)
449+
450+
# The cases are all defined to produce the same expected result
451+
# Each comprehension is checked at both function scope and module scope
452+
rebinding = "[x := i for i in range(3) if (x := i) or not x]"
453+
filter_ref = "[x := i for i in range(3) if x or not x]"
454+
body_ref = "[x for i in range(3) if (x := i) or not x]"
455+
nested_ref = "[j for i in range(3) if x or not x for j in range(3) if (x := i)][:-3]"
456+
cases = [
457+
("Rebind global", f"x = 1; result = {rebinding}"),
458+
("Rebind nonlocal", f"result, x = (lambda x=1: ({rebinding}, x))()"),
459+
("Filter global", f"x = 1; result = {filter_ref}"),
460+
("Filter nonlocal", f"result, x = (lambda x=1: ({filter_ref}, x))()"),
461+
("Body global", f"x = 1; result = {body_ref}"),
462+
("Body nonlocal", f"result, x = (lambda x=1: ({body_ref}, x))()"),
463+
("Nested global", f"x = 1; result = {nested_ref}"),
464+
("Nested nonlocal", f"result, x = (lambda x=1: ({nested_ref}, x))()"),
465+
]
466+
for case, code in cases:
467+
with self.subTest(case=case):
468+
ns = {}
469+
exec(code, ns)
470+
self.assertEqual(ns["x"], 2)
471+
self.assertEqual(ns["result"], [0, 1, 2])
424472

425473
if __name__ == "__main__":
426474
unittest.main()
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
:pep:`572`: As described in the PEP, assignment expressions now raise
2+
:exc:`SyntaxError` when their interaction with comprehension scoping results
3+
in an ambiguous target scope.
4+
5+
The ``TargetScopeError`` subclass originally proposed by the PEP has been
6+
removed in favour of just raising regular syntax errors for the disallowed
7+
cases.

Objects/exceptions.c

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1521,13 +1521,6 @@ MiddlingExtendsException(PyExc_SyntaxError, IndentationError, SyntaxError,
15211521
"Improper indentation.");
15221522

15231523

1524-
/*
1525-
* TargetScopeError extends SyntaxError
1526-
*/
1527-
MiddlingExtendsException(PyExc_SyntaxError, TargetScopeError, SyntaxError,
1528-
"Improper scope target.");
1529-
1530-
15311524
/*
15321525
* TabError extends IndentationError
15331526
*/
@@ -2539,7 +2532,6 @@ _PyExc_Init(void)
25392532
PRE_INIT(AttributeError);
25402533
PRE_INIT(SyntaxError);
25412534
PRE_INIT(IndentationError);
2542-
PRE_INIT(TargetScopeError);
25432535
PRE_INIT(TabError);
25442536
PRE_INIT(LookupError);
25452537
PRE_INIT(IndexError);
@@ -2680,7 +2672,6 @@ _PyBuiltins_AddExceptions(PyObject *bltinmod)
26802672
POST_INIT(AttributeError);
26812673
POST_INIT(SyntaxError);
26822674
POST_INIT(IndentationError);
2683-
POST_INIT(TargetScopeError);
26842675
POST_INIT(TabError);
26852676
POST_INIT(LookupError);
26862677
POST_INIT(IndexError);

PC/python3.def

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,6 @@ EXPORTS
235235
PyExc_SystemError=python39.PyExc_SystemError DATA
236236
PyExc_SystemExit=python39.PyExc_SystemExit DATA
237237
PyExc_TabError=python39.PyExc_TabError DATA
238-
PyExc_TargetScopeError=python39.PyExc_TargetScopeError DATA
239238
PyExc_TimeoutError=python39.PyExc_TimeoutError DATA
240239
PyExc_TypeError=python39.PyExc_TypeError DATA
241240
PyExc_UnboundLocalError=python39.PyExc_UnboundLocalError DATA

0 commit comments

Comments
 (0)