diff options
author | Nick Coghlan <ncoghlan@gmail.com> | 2019-08-25 13:45:40 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-08-25 13:45:40 (GMT) |
commit | 5dbe0f59b7a4f39c7c606b48056bc29e406ebf78 (patch) | |
tree | 9dd53ae948d0e49719d85d5e7814a6b1db61fdf3 /Lib/test | |
parent | ce6a070414ed1e1374d1e6212bfbff61b6d5d755 (diff) | |
download | cpython-5dbe0f59b7a4f39c7c606b48056bc29e406ebf78.zip cpython-5dbe0f59b7a4f39c7c606b48056bc29e406ebf78.tar.gz cpython-5dbe0f59b7a4f39c7c606b48056bc29e406ebf78.tar.bz2 |
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)
Diffstat (limited to 'Lib/test')
-rw-r--r-- | Lib/test/exception_hierarchy.txt | 1 | ||||
-rw-r--r-- | Lib/test/test_named_expressions.py | 120 |
2 files changed, 84 insertions, 37 deletions
diff --git a/Lib/test/exception_hierarchy.txt b/Lib/test/exception_hierarchy.txt index 15f4491..763a6c8 100644 --- a/Lib/test/exception_hierarchy.txt +++ b/Lib/test/exception_hierarchy.txt @@ -42,7 +42,6 @@ BaseException | +-- NotImplementedError | +-- RecursionError +-- SyntaxError - | +-- TargetScopeError | +-- IndentationError | +-- TabError +-- SystemError diff --git a/Lib/test/test_named_expressions.py b/Lib/test/test_named_expressions.py index f73e6fe..b1027ce 100644 --- a/Lib/test/test_named_expressions.py +++ b/Lib/test/test_named_expressions.py @@ -104,15 +104,69 @@ class NamedExpressionInvalidTest(unittest.TestCase): with self.assertRaisesRegex(SyntaxError, "invalid syntax"): exec(code, {}, {}) - def test_named_expression_invalid_18(self): + def test_named_expression_invalid_in_class_body(self): code = """class Foo(): [(42, 1 + ((( j := i )))) for i in range(5)] """ - with self.assertRaisesRegex(TargetScopeError, - "named expression within a comprehension cannot be used in a class body"): + with self.assertRaisesRegex(SyntaxError, + "assignment expression within a comprehension cannot be used in a class body"): exec(code, {}, {}) + def test_named_expression_invalid_rebinding_comprehension_iteration_variable(self): + cases = [ + ("Local reuse", 'i', "[i := 0 for i in range(5)]"), + ("Nested reuse", 'j', "[[(j := 0) for i in range(5)] for j in range(5)]"), + ("Reuse inner loop target", 'j', "[(j := 0) for i in range(5) for j in range(5)]"), + ("Unpacking reuse", 'i', "[i := 0 for i, j in [(0, 1)]]"), + ("Reuse in loop condition", 'i', "[i+1 for i in range(5) if (i := 0)]"), + ("Unreachable reuse", 'i', "[False or (i:=0) for i in range(5)]"), + ("Unreachable nested reuse", 'i', + "[(i, j) for i in range(5) for j in range(5) if True or (i:=10)]"), + ] + for case, target, code in cases: + msg = f"assignment expression cannot rebind comprehension iteration variable '{target}'" + with self.subTest(case=case): + with self.assertRaisesRegex(SyntaxError, msg): + exec(code, {}, {}) + + def test_named_expression_invalid_rebinding_comprehension_inner_loop(self): + cases = [ + ("Inner reuse", 'j', "[i for i in range(5) if (j := 0) for j in range(5)]"), + ("Inner unpacking reuse", 'j', "[i for i in range(5) if (j := 0) for j, k in [(0, 1)]]"), + ] + for case, target, code in cases: + msg = f"comprehension inner loop cannot rebind assignment expression target '{target}'" + with self.subTest(case=case): + with self.assertRaisesRegex(SyntaxError, msg): + exec(code, {}) # Module scope + with self.assertRaisesRegex(SyntaxError, msg): + exec(code, {}, {}) # Class scope + with self.assertRaisesRegex(SyntaxError, msg): + exec(f"lambda: {code}", {}) # Function scope + + def test_named_expression_invalid_comprehension_iterable_expression(self): + cases = [ + ("Top level", "[i for i in (i := range(5))]"), + ("Inside tuple", "[i for i in (2, 3, i := range(5))]"), + ("Inside list", "[i for i in [2, 3, i := range(5)]]"), + ("Different name", "[i for i in (j := range(5))]"), + ("Lambda expression", "[i for i in (lambda:(j := range(5)))()]"), + ("Inner loop", "[i for i in range(5) for j in (i := range(5))]"), + ("Nested comprehension", "[i for i in [j for j in (k := range(5))]]"), + ("Nested comprehension condition", "[i for i in [j for j in range(5) if (j := True)]]"), + ("Nested comprehension body", "[i for i in [(j := True) for j in range(5)]]"), + ] + msg = "assignment expression cannot be used in a comprehension iterable expression" + for case, code in cases: + with self.subTest(case=case): + with self.assertRaisesRegex(SyntaxError, msg): + exec(code, {}) # Module scope + with self.assertRaisesRegex(SyntaxError, msg): + exec(code, {}, {}) # Class scope + with self.assertRaisesRegex(SyntaxError, msg): + exec(f"lambda: {code}", {}) # Function scope + class NamedExpressionAssignmentTest(unittest.TestCase): @@ -306,39 +360,6 @@ print(a)""" self.assertEqual(res, [0, 1, 2, 3, 4]) self.assertEqual(j, 4) - def test_named_expression_scope_12(self): - res = [i := i for i in range(5)] - - self.assertEqual(res, [0, 1, 2, 3, 4]) - self.assertEqual(i, 4) - - def test_named_expression_scope_13(self): - res = [i := 0 for i, j in [(1, 2), (3, 4)]] - - self.assertEqual(res, [0, 0]) - self.assertEqual(i, 0) - - def test_named_expression_scope_14(self): - res = [(i := 0, j := 1) for i, j in [(1, 2), (3, 4)]] - - self.assertEqual(res, [(0, 1), (0, 1)]) - self.assertEqual(i, 0) - self.assertEqual(j, 1) - - def test_named_expression_scope_15(self): - res = [(i := i, j := j) for i, j in [(1, 2), (3, 4)]] - - self.assertEqual(res, [(1, 2), (3, 4)]) - self.assertEqual(i, 3) - self.assertEqual(j, 4) - - def test_named_expression_scope_16(self): - res = [(i := j, j := i) for i, j in [(1, 2), (3, 4)]] - - self.assertEqual(res, [(2, 2), (4, 4)]) - self.assertEqual(i, 4) - self.assertEqual(j, 4) - def test_named_expression_scope_17(self): b = 0 res = [b := i + b for i in range(5)] @@ -421,6 +442,33 @@ spam()""" self.assertEqual(ns["a"], 20) + def test_named_expression_variable_reuse_in_comprehensions(self): + # The compiler is expected to raise syntax error for comprehension + # iteration variables, but should be fine with rebinding of other + # names (e.g. globals, nonlocals, other assignment expressions) + + # The cases are all defined to produce the same expected result + # Each comprehension is checked at both function scope and module scope + rebinding = "[x := i for i in range(3) if (x := i) or not x]" + filter_ref = "[x := i for i in range(3) if x or not x]" + body_ref = "[x for i in range(3) if (x := i) or not x]" + nested_ref = "[j for i in range(3) if x or not x for j in range(3) if (x := i)][:-3]" + cases = [ + ("Rebind global", f"x = 1; result = {rebinding}"), + ("Rebind nonlocal", f"result, x = (lambda x=1: ({rebinding}, x))()"), + ("Filter global", f"x = 1; result = {filter_ref}"), + ("Filter nonlocal", f"result, x = (lambda x=1: ({filter_ref}, x))()"), + ("Body global", f"x = 1; result = {body_ref}"), + ("Body nonlocal", f"result, x = (lambda x=1: ({body_ref}, x))()"), + ("Nested global", f"x = 1; result = {nested_ref}"), + ("Nested nonlocal", f"result, x = (lambda x=1: ({nested_ref}, x))()"), + ] + for case, code in cases: + with self.subTest(case=case): + ns = {} + exec(code, ns) + self.assertEqual(ns["x"], 2) + self.assertEqual(ns["result"], [0, 1, 2]) if __name__ == "__main__": unittest.main() |