From 0e9983569eaf609c9a27834ec96cabc35282b261 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 7 Sep 2023 19:30:10 -0700 Subject: [PATCH 1/5] New test --- Lib/test/test_type_params.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Lib/test/test_type_params.py b/Lib/test/test_type_params.py index f93d088ea758a9..40a17834951fa9 100644 --- a/Lib/test/test_type_params.py +++ b/Lib/test/test_type_params.py @@ -412,6 +412,32 @@ def test_comprehension_02(self): func, = T.__bound__ self.assertEqual(func(), 1) + def test_gen_exp_in_nested_class(self): + class C[T]: + T = "class" + class Inner(make_base(T for _ in (1,)), make_base(T)): + pass + T, = C.__type_params__ + base1, base2 = C.Inner.__bases__ + self.assertEqual(list(base1.__arg__), [T]) + self.assertEqual(base2.__arg__, "class") + + def test_gen_exp_in_nested_generic_class(self): + class C[T]: + T = "class" + class Inner[U](make_base(T for _ in (1,)), make_base(T)): + pass + T, = C.__type_params__ + base1, base2, _ = C.Inner.__bases__ + self.assertEqual(list(base1.__arg__), [T]) + self.assertEqual(base2.__arg__, "class") + + +def make_base(arg): + class Base: + __arg__ = arg + return Base + def global_generic_func[T](): pass From cb30ae19aef89762a86c5982ac9f33218782809f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 9 Sep 2023 10:45:09 -0700 Subject: [PATCH 2/5] more and more isolated tests --- Lib/test/test_type_params.py | 56 ++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_type_params.py b/Lib/test/test_type_params.py index 40a17834951fa9..301768dc8cefc3 100644 --- a/Lib/test/test_type_params.py +++ b/Lib/test/test_type_params.py @@ -413,25 +413,65 @@ def test_comprehension_02(self): self.assertEqual(func(), 1) def test_gen_exp_in_nested_class(self): - class C[T]: - T = "class" - class Inner(make_base(T for _ in (1,)), make_base(T)): - pass + code = """ + from test.test_type_params import make_base + + class C[T]: + T = "class" + class Inner(make_base(T for _ in (1,)), make_base(T)): + pass + """ + C = run_code(code)["C"] T, = C.__type_params__ base1, base2 = C.Inner.__bases__ self.assertEqual(list(base1.__arg__), [T]) self.assertEqual(base2.__arg__, "class") def test_gen_exp_in_nested_generic_class(self): - class C[T]: - T = "class" - class Inner[U](make_base(T for _ in (1,)), make_base(T)): - pass + code = """ + from test.test_type_params import make_base + + class C[T]: + T = "class" + class Inner[U](make_base(T for _ in (1,)), make_base(T)): + pass + """ + C = run_code(code)["C"] T, = C.__type_params__ base1, base2, _ = C.Inner.__bases__ self.assertEqual(list(base1.__arg__), [T]) self.assertEqual(base2.__arg__, "class") + def test_listcomp_in_nested_class(self): + code = """ + from test.test_type_params import make_base + + class C[T]: + T = "class" + class Inner(make_base([T for _ in (1,)]), make_base(T)): + pass + """ + C = run_code(code)["C"] + T, = C.__type_params__ + base1, base2 = C.Inner.__bases__ + self.assertEqual(base1.__arg__, [T]) + self.assertEqual(base2.__arg__, "class") + + def test_listcomp_in_nested_generic_class(self): + code = """ + from test.test_type_params import make_base + + class C[T]: + T = "class" + class Inner[U](make_base([T for _ in (1,)]), make_base(T)): + pass + """ + C = run_code(code)["C"] + T, = C.__type_params__ + base1, base2, _ = C.Inner.__bases__ + self.assertEqual(base1.__arg__, [T]) + self.assertEqual(base2.__arg__, "class") + def make_base(arg): class Base: From 725ca369637e11f1036b037bdd7419e3ab6b1100 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 9 Sep 2023 11:03:54 -0700 Subject: [PATCH 3/5] Similar test with aliases, which already passes --- Lib/test/test_type_params.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Lib/test/test_type_params.py b/Lib/test/test_type_params.py index 301768dc8cefc3..042cbcfd478af4 100644 --- a/Lib/test/test_type_params.py +++ b/Lib/test/test_type_params.py @@ -472,6 +472,30 @@ class Inner[U](make_base([T for _ in (1,)]), make_base(T)): self.assertEqual(base1.__arg__, [T]) self.assertEqual(base2.__arg__, "class") + def test_lambda_in_generic_alias(self): + code = """ + class C[T]: + T = "class" + type Alias1[T] = lambda: T + type Alias2 = lambda: T + type Alias3[T] = (T for _ in (1,)) + type Alias4 = (T for _ in (1,)) + type Alias5[T] = [T for _ in (1,)] + type Alias6 = [T for _ in (1,)] + """ + C = run_code(code)["C"] + outer_T = C.__type_params__[0] + T1 = C.Alias1.__type_params__[0] + self.assertIs(C.Alias1.__value__(), T1) + # Shouldn't pick up the T from the class scope + self.assertIs(C.Alias2.__value__(), outer_T) + T3 = C.Alias3.__type_params__[0] + self.assertEqual(list(C.Alias3.__value__), [T3]) + self.assertEqual(list(C.Alias4.__value__), [outer_T]) + T5 = C.Alias5.__type_params__[0] + self.assertEqual(C.Alias5.__value__, [T5]) + self.assertEqual(C.Alias6.__value__, [outer_T]) + def make_base(arg): class Base: From d7be560b39cd92a2edd9af6a9ec2c3ec796f2231 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 9 Sep 2023 12:50:00 -0700 Subject: [PATCH 4/5] gh-109118: Disallow nested scopes within PEP 695 scopes within classes Fixes #109118. Fixes #109194. --- Lib/test/test_type_params.py | 59 ++++++++++--------- ...-09-09-12-49-46.gh-issue-109118.gx0X4h.rst | 2 + Python/symtable.c | 23 ++++++++ 3 files changed, 56 insertions(+), 28 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2023-09-09-12-49-46.gh-issue-109118.gx0X4h.rst diff --git a/Lib/test/test_type_params.py b/Lib/test/test_type_params.py index 042cbcfd478af4..36e0a11a9fd451 100644 --- a/Lib/test/test_type_params.py +++ b/Lib/test/test_type_params.py @@ -436,11 +436,9 @@ class C[T]: class Inner[U](make_base(T for _ in (1,)), make_base(T)): pass """ - C = run_code(code)["C"] - T, = C.__type_params__ - base1, base2, _ = C.Inner.__bases__ - self.assertEqual(list(base1.__arg__), [T]) - self.assertEqual(base2.__arg__, "class") + with self.assertRaisesRegex(SyntaxError, + "Cannot use comprehension in annotation scope within class scope"): + run_code(code) def test_listcomp_in_nested_class(self): code = """ @@ -466,35 +464,40 @@ class C[T]: class Inner[U](make_base([T for _ in (1,)]), make_base(T)): pass """ - C = run_code(code)["C"] - T, = C.__type_params__ - base1, base2, _ = C.Inner.__bases__ - self.assertEqual(base1.__arg__, [T]) - self.assertEqual(base2.__arg__, "class") + with self.assertRaisesRegex(SyntaxError, + "Cannot use comprehension in annotation scope within class scope"): + run_code(code) + + def test_gen_exp_in_generic_method(self): + code = """ + class C[T]: + T = "class" + def meth[U](x: (T for _ in (1,)), y: T): + pass + """ + with self.assertRaisesRegex(SyntaxError, + "Cannot use comprehension in annotation scope within class scope"): + run_code(code) def test_lambda_in_generic_alias(self): code = """ class C[T]: T = "class" - type Alias1[T] = lambda: T - type Alias2 = lambda: T - type Alias3[T] = (T for _ in (1,)) - type Alias4 = (T for _ in (1,)) - type Alias5[T] = [T for _ in (1,)] - type Alias6 = [T for _ in (1,)] + {} """ - C = run_code(code)["C"] - outer_T = C.__type_params__[0] - T1 = C.Alias1.__type_params__[0] - self.assertIs(C.Alias1.__value__(), T1) - # Shouldn't pick up the T from the class scope - self.assertIs(C.Alias2.__value__(), outer_T) - T3 = C.Alias3.__type_params__[0] - self.assertEqual(list(C.Alias3.__value__), [T3]) - self.assertEqual(list(C.Alias4.__value__), [outer_T]) - T5 = C.Alias5.__type_params__[0] - self.assertEqual(C.Alias5.__value__, [T5]) - self.assertEqual(C.Alias6.__value__, [outer_T]) + error_cases = [ + "type Alias1[T] = lambda: T", + "type Alias2 = lambda: T", + "type Alias3[T] = (T for _ in (1,))", + "type Alias4 = (T for _ in (1,))", + "type Alias5[T] = [T for _ in (1,)]", + "type Alias6 = [T for _ in (1,)]", + ] + for case in error_cases: + with self.subTest(case=case): + with self.assertRaisesRegex(SyntaxError, + r"Cannot use [a-z]+ in annotation scope within class scope"): + run_code(code.format(case)) def make_base(arg): diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-09-09-12-49-46.gh-issue-109118.gx0X4h.rst b/Misc/NEWS.d/next/Core and Builtins/2023-09-09-12-49-46.gh-issue-109118.gx0X4h.rst new file mode 100644 index 00000000000000..87069c85870410 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2023-09-09-12-49-46.gh-issue-109118.gx0X4h.rst @@ -0,0 +1,2 @@ +Disallow nested scopes (lambdas, generator expressions, and comprehensions) +within PEP 695 annotation scopes that are nested within classes. diff --git a/Python/symtable.c b/Python/symtable.c index 7eef6f7231c035..65c18382c66dda 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -2011,6 +2011,17 @@ symtable_visit_expr(struct symtable *st, expr_ty e) VISIT(st, expr, e->v.UnaryOp.operand); break; case Lambda_kind: { + if (st->st_cur->ste_can_see_class_scope) { + // gh-109118 + PyErr_Format(PyExc_SyntaxError, + "Cannot use lambda in annotation scope within class scope"); + PyErr_RangedSyntaxLocationObject(st->st_filename, + e->lineno, + e->col_offset + 1, + e->end_lineno, + e->end_col_offset + 1); + VISIT_QUIT(st, 0); + } if (e->v.Lambda.args->defaults) VISIT_SEQ(st, expr, e->v.Lambda.args->defaults); if (e->v.Lambda.args->kw_defaults) @@ -2460,6 +2471,18 @@ symtable_handle_comprehension(struct symtable *st, expr_ty e, identifier scope_name, asdl_comprehension_seq *generators, expr_ty elt, expr_ty value) { + if (st->st_cur->ste_can_see_class_scope) { + // gh-109118 + PyErr_Format(PyExc_SyntaxError, + "Cannot use comprehension in annotation scope within class scope"); + PyErr_RangedSyntaxLocationObject(st->st_filename, + e->lineno, + e->col_offset + 1, + e->end_lineno, + e->end_col_offset + 1); + VISIT_QUIT(st, 0); + } + int is_generator = (e->kind == GeneratorExp_kind); comprehension_ty outermost = ((comprehension_ty) asdl_seq_GET(generators, 0)); From ea69d360993bf9b6cd7250cdd7aa67a93cfc2f16 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 11 Sep 2023 09:18:31 -0700 Subject: [PATCH 5/5] Update Lib/test/test_type_params.py Co-authored-by: Carl Meyer --- Lib/test/test_type_params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_type_params.py b/Lib/test/test_type_params.py index 36e0a11a9fd451..b1848aee4753a1 100644 --- a/Lib/test/test_type_params.py +++ b/Lib/test/test_type_params.py @@ -479,7 +479,7 @@ def meth[U](x: (T for _ in (1,)), y: T): "Cannot use comprehension in annotation scope within class scope"): run_code(code) - def test_lambda_in_generic_alias(self): + def test_nested_scope_in_generic_alias(self): code = """ class C[T]: T = "class"