]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-150700: Fix class-scope inline comprehensions when nested scopes reference `__clas...
authorBartosz Sławecki <bartosz@ilikepython.com>
Tue, 9 Jun 2026 22:22:13 +0000 (00:22 +0200)
committerGitHub <noreply@github.com>
Tue, 9 Jun 2026 22:22:13 +0000 (15:22 -0700)
* Fix class-scope inline comprehensions when nested scopes reference `__class__` and friends

In `inline_comprehension()`, when `__class__` / `__classdict__` /
`__conditional_annotations__` appears as `FREE` in a comprehension's
symbol table because a nested scope captured it (e.g. nested lambdas),
this name is still discarded from `comp_free` unconditionally.

This prevents `drop_class_free()` from seeing it, so the appropriate
`ste_needs_(...)` flag is never set on the enclosing class.
That leads to `codegen_make_closure()` throwing `SystemError` when it
couldn't find `__class__` / `__classdict__` /
`__conditional_annotations__` in the class's cellvars.

From now on we just discard from `comp_free` when no child scope
(e.g. a lambda) still needs the name as `FREE`. When a child scope does
need it, keep it in `comp_free` so `drop_class_free()` can set the
appropriate flag and the class creates the implicit cell.

* Fix tests

* Fix typo

* Fix formatting

* Add test checking validity of `__class__` returned

* Prefer 'used' to 'deferred'

Lib/test/test_listcomps.py
Misc/NEWS.d/next/Core_and_Builtins/2026-06-01-19-00-00.gh-issue-150700.W8CzVR.rst [new file with mode: 0644]
Python/symtable.c

index cee528722b85aa00e958fa8f7b34add6f7e9f04c..cf3796d9480801598103051b27db63c726454de0 100644 (file)
@@ -171,6 +171,17 @@ class ListComprehensionTest(unittest.TestCase):
         """
         self._check_in_scopes(code, raises=NameError)
 
+    def test_references___class___nested(self):
+        code = """
+            res = [(lambda: __class__)() for _ in [1]]
+        """
+        self._check_in_scopes(code, raises=NameError)
+
+    def test_references___class___nested_used(self):
+        class _C:
+            res = [lambda: __class__ for _ in [1]]
+        self.assertIs(_C.res[0](), _C)
+
     def test_references___class___defined(self):
         code = """
             __class__ = 2
@@ -180,18 +191,38 @@ class ListComprehensionTest(unittest.TestCase):
                 code, outputs={"res": [2]}, scopes=["module", "function"])
         self._check_in_scopes(code, raises=NameError, scopes=["class"])
 
+    def test_references___class___defined_nested(self):
+        code = """
+            __class__ = 2
+            res = [(lambda: __class__)() for x in [1]]
+        """
+        self._check_in_scopes(
+                code, outputs={"res": [2]}, scopes=["module", "function"])
+        self._check_in_scopes(code, raises=NameError, scopes=["class"])
+
     def test_references___classdict__(self):
         code = """
             class i: [__classdict__ for x in y]
         """
         self._check_in_scopes(code, raises=NameError)
 
+    def test_references___classdict___nested(self):
+        class _C:
+            res = [(lambda: __classdict__)() for _ in [1]]
+        self.assertIn("res", _C.res[0])
+
     def test_references___conditional_annotations__(self):
         code = """
             class i: [__conditional_annotations__ for x in y]
         """
         self._check_in_scopes(code, raises=NameError)
 
+    def test_references___conditional_annotations___nested(self):
+        code = """
+            class i: [lambda: __conditional_annotations__ for x in y]
+        """
+        self._check_in_scopes(code, raises=NameError)
+
     def test_references___class___enclosing(self):
         code = """
             __class__ = 2
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-01-19-00-00.gh-issue-150700.W8CzVR.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-01-19-00-00.gh-issue-150700.W8CzVR.rst
new file mode 100644 (file)
index 0000000..e773403
--- /dev/null
@@ -0,0 +1,3 @@
+Fix a :exc:`SystemError` when compiling a class-scope comprehension containing
+a ``lambda`` that references ``__class__``, ``__classdict__``, or
+``__conditional_annotations__``. Patch by Bartosz Sławecki.
index 070e374101b5cd5632cf28fe089ffd7d2388d319..00ac510fe76b97204fd00afc3344b41ab2dc2042 100644 (file)
@@ -834,17 +834,22 @@ inline_comprehension(PySTEntryObject *ste, PySTEntryObject *comp,
             return 0;
         }
         // __class__, __classdict__ and __conditional_annotations__ are
-        // never allowed to be free through a class scope (see
-        // drop_class_free)
+        // not allowed to be free through a class scope (see
+        // drop_class_free) unless children scopes need it
         if (scope == FREE && ste->ste_type == ClassBlock &&
                 (_PyUnicode_EqualToASCIIString(k, "__class__") ||
                  _PyUnicode_EqualToASCIIString(k, "__classdict__") ||
                  _PyUnicode_EqualToASCIIString(k, "__conditional_annotations__"))) {
             scope = GLOBAL_IMPLICIT;
-            if (PySet_Discard(comp_free, k) < 0) {
+            int child_needs_free = is_free_in_any_child(comp, k);
+            if (child_needs_free < 0) {
                 return 0;
             }
-
+            if (!child_needs_free) {
+                if (PySet_Discard(comp_free, k) < 0) {
+                    return 0;
+                }
+            }
             if (_PyUnicode_EqualToASCIIString(k, "__class__")) {
                 remove_dunder_class = 1;
             }