diff --git a/Lib/test/test_listcomps.py b/Lib/test/test_listcomps.py index f227e9519af345..a986fc285d8e54 100644 --- a/Lib/test/test_listcomps.py +++ b/Lib/test/test_listcomps.py @@ -1,4 +1,5 @@ import doctest +import textwrap import unittest @@ -87,154 +88,143 @@ >>> [None for i in range(10)] [None, None, None, None, None, None, None, None, None, None] -########### Tests for various scoping corner cases ############ - -Return lambdas that use the iteration variable as a default argument - - >>> items = [(lambda i=i: i) for i in range(5)] - >>> [x() for x in items] - [0, 1, 2, 3, 4] - -Same again, only this time as a closure variable - - >>> items = [(lambda: i) for i in range(5)] - >>> [x() for x in items] - [4, 4, 4, 4, 4] - -Another way to test that the iteration variable is local to the list comp - - >>> items = [(lambda: i) for i in range(5)] - >>> i = 20 - >>> [x() for x in items] - [4, 4, 4, 4, 4] - -And confirm that a closure can jump over the list comp scope - - >>> items = [(lambda: y) for i in range(5)] - >>> y = 2 - >>> [x() for x in items] - [2, 2, 2, 2, 2] - -We also repeat each of the above scoping tests inside a function: - - >>> def test_func(): - ... items = [(lambda i=i: i) for i in range(5)] - ... return [x() for x in items] - >>> test_func() - [0, 1, 2, 3, 4] - - >>> def test_func(): - ... items = [(lambda: i) for i in range(5)] - ... return [x() for x in items] - >>> test_func() - [4, 4, 4, 4, 4] - - >>> def test_func(): - ... items = [(lambda: i) for i in range(5)] - ... i = 20 - ... return [x() for x in items] - >>> test_func() - [4, 4, 4, 4, 4] - - >>> def test_func(): - ... items = [(lambda: y) for i in range(5)] - ... y = 2 - ... return [x() for x in items] - >>> test_func() - [2, 2, 2, 2, 2] - -And in class scope: - - >>> class C: - ... items = [(lambda i=i: i) for i in range(5)] - ... ret = [x() for x in items] - >>> C.ret - [0, 1, 2, 3, 4] - - >>> class C: - ... items = [(lambda: i) for i in range(5)] - ... ret = [x() for x in items] - >>> C.ret - [4, 4, 4, 4, 4] - - >>> class C: - ... items = [(lambda: i) for i in range(5)] - ... i = 20 - ... ret = [x() for x in items] - >>> C.ret - [4, 4, 4, 4, 4] - >>> C.i - 20 - - >>> class C: - ... items = [(lambda: y) for i in range(5)] - ... y = 2 - ... ret = [x() for x in items] - >>> C.ret - [2, 2, 2, 2, 2] - -Some more tests for scoping edge cases, each in func/module/class scope: - - >>> def test_func(): - ... y = 10 - ... items = [(lambda: y) for y in range(5)] - ... x = y - ... y = 20 - ... return x, [z() for z in items] - >>> test_func() - (10, [4, 4, 4, 4, 4]) - - >>> g = -1 - >>> def test_func(): - ... def inner(): - ... return g - ... [g for g in range(5)] - ... return inner - >>> test_func()() - -1 - - >>> def test_func(): - ... x = -1 - ... items = [(x:=y) for y in range(3)] - ... return x - >>> test_func() - 2 - - >>> def test_func(lst): - ... ret = [lambda: x for x in lst] - ... inc = [x + 1 for x in lst] - ... [x for x in inc] - ... return ret - >>> test_func(range(3))[0]() - 2 - - >>> def test_func(lst): - ... x = -1 - ... funcs = [lambda: x for x in lst] - ... items = [x + 1 for x in lst] - ... return x - >>> test_func(range(3)) - -1 - - >>> def test_func(x): - ... return [x for x in x] - >>> test_func([1]) - [1] - - >>> def test_func(): - ... x = 1 - ... def g(): - ... [x for x in range(3)] - ... return x - ... g() - ... return x - >>> test_func() - 1 - """ class ListComprehensionTest(unittest.TestCase): + def _check_in_scopes(self, code, outputs, ns=None, scopes=None): + code = textwrap.dedent(code) + scopes = scopes or ["module", "class", "function"] + for scope in scopes: + with self.subTest(scope=scope): + if scope == "class": + newcode = textwrap.dedent(""" + class C: + {code} + """).format(code=textwrap.indent(code, " ")) + def get_output(moddict, name): + return getattr(moddict["C"], name) + elif scope == "function": + newcode = textwrap.dedent(""" + def f(): + {code} + return locals() + """).format(code=textwrap.indent(code, " ")) + def get_output(moddict, name): + return moddict["f"]()[name] + else: + newcode = code + def get_output(moddict, name): + return moddict[name] + ns = ns or {} + exec(newcode, ns) + for k, v in outputs.items(): + self.assertEqual(get_output(ns, k), v) + + def test_lambdas_with_iteration_var_as_default(self): + code = """ + items = [(lambda i=i: i) for i in range(5)] + y = [x() for x in items] + """ + outputs = {"y": [0, 1, 2, 3, 4]} + self._check_in_scopes(code, outputs) + + def test_lambdas_with_free_var(self): + code = """ + items = [(lambda: i) for i in range(5)] + y = [x() for x in items] + """ + outputs = {"y": [4, 4, 4, 4, 4]} + self._check_in_scopes(code, outputs) + + def test_inner_cell_shadows_outer(self): + code = """ + items = [(lambda: i) for i in range(5)] + i = 20 + y = [x() for x in items] + """ + outputs = {"y": [4, 4, 4, 4, 4]} + self._check_in_scopes(code, outputs) + + def test_closure_can_jump_over_comp_scope(self): + code = """ + items = [(lambda: y) for i in range(5)] + y = 2 + z = [x() for x in items] + """ + outputs = {"z": [2, 2, 2, 2, 2]} + self._check_in_scopes(code, outputs) + + def test_inner_cell_shadows_outer_redefined(self): + code = """ + y = 10 + items = [(lambda: y) for y in range(5)] + x = y + y = 20 + out = [z() for z in items] + """ + outputs = {"x": 10, "out": [4, 4, 4, 4, 4]} + self._check_in_scopes(code, outputs) + + def test_shadows_outer_cell(self): + code = """ + def inner(): + return g + [g for g in range(5)] + x = inner() + """ + outputs = {"x": -1} + self._check_in_scopes(code, outputs, ns={"g": -1}) + + def test_assignment_expression(self): + code = """ + x = -1 + items = [(x:=y) for y in range(3)] + """ + outputs = {"x": 2} + # assignment expression in comprehension is disallowed in class scope + self._check_in_scopes(code, outputs, scopes=["module", "function"]) + + def test_free_var_in_comp_child(self): + code = """ + lst = range(3) + funcs = [lambda: x for x in lst] + inc = [x + 1 for x in lst] + [x for x in inc] + x = funcs[0]() + """ + outputs = {"x": 2} + self._check_in_scopes(code, outputs) + + def test_shadow_with_free_and_local(self): + code = """ + lst = range(3) + x = -1 + funcs = [lambda: x for x in lst] + items = [x + 1 for x in lst] + """ + outputs = {"x": -1} + self._check_in_scopes(code, outputs) + + def test_shadow_comp_iterable_name(self): + code = """ + x = [1] + y = [x for x in x] + """ + outputs = {"x": [1]} + self._check_in_scopes(code, outputs) + + def test_nested_free(self): + code = """ + x = 1 + def g(): + [x for x in range(3)] + return x + g() + """ + outputs = {"x": 1} + self._check_in_scopes(code, outputs) + def test_unbound_local_after_comprehension(self): def f(): if False: