]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-148947: dataclasses: fix error on empty __class__ cell (#148948)
authorJelle Zijlstra <jelle.zijlstra@gmail.com>
Sat, 25 Apr 2026 15:31:22 +0000 (08:31 -0700)
committerGitHub <noreply@github.com>
Sat, 25 Apr 2026 15:31:22 +0000 (08:31 -0700)
Also add a test demonstrating the need for the existing "is oldcls" check.

Co-authored-by: Bartosz Sławecki <bartosz@ilikepython.com>
Lib/dataclasses.py
Lib/test/test_dataclasses/__init__.py
Misc/NEWS.d/next/Library/2026-04-23-21-47-49.gh-issue-148947.W4V2lG.rst [new file with mode: 0644]

index 9d5bed6b96fc49dfc2e24cc1b703afe53bc1ba29..988edfed6f4dcbd97a881fcf34494b57c40ef75f 100644 (file)
@@ -1298,10 +1298,18 @@ def _update_func_cell_for__class__(f, oldcls, newcls):
         # This function doesn't reference __class__, so nothing to do.
         return False
     # Fix the cell to point to the new class, if it's already pointing
-    # at the old class.  I'm not convinced that the "is oldcls" test
-    # is needed, but other than performance can't hurt.
+    # at the old class.
     closure = f.__closure__[idx]
-    if closure.cell_contents is oldcls:
+
+    try:
+        contents = closure.cell_contents
+    except ValueError:
+        # Cell is empty
+        return False
+
+    # This check makes it so we avoid updating an incorrect cell if the
+    # class body contains a function that was defined in a different class.
+    if contents is oldcls:
         closure.cell_contents = newcls
         return True
     return False
index e0cfe3df3e63578d2b62d39a6c8d758abeae8d50..6ff82b8810abed9baf071040fcf30f5395e3865e 100644 (file)
@@ -5375,5 +5375,51 @@ class TestZeroArgumentSuperWithSlots(unittest.TestCase):
         # one will be keeping a reference to the underlying class A.
         self.assertIs(A().cls(), B)
 
+    def test_empty_class_cell(self):
+        # gh-148947: Make sure that we explicitly handle the empty class cell.
+        def maker():
+            if False:
+                __class__ = 42
+
+            def method(self):
+                return __class__
+            return method
+
+        from dataclasses import dataclass
+
+        @dataclass(slots=True)
+        class X:
+            a: int
+
+            meth = maker()
+
+        with self.assertRaisesRegex(NameError, '__class__'):
+            X(1).meth()
+
+    def test_class_cell_from_other_class(self):
+        # This test fails without the "is oldcls" check in
+        # _update_func_cell_for__class__.
+        class Base:
+            def meth(self):
+                return "Base"
+
+        class Child(Base):
+            def meth(self):
+                return super().meth() + " Child"
+
+        @dataclass(slots=True)
+        class DC(Child):
+            a: int
+
+            meth = Child.meth
+
+        closure = DC.meth.__closure__
+        self.assertEqual(len(closure), 1)
+        self.assertIs(closure[0].cell_contents, Child)
+
+        self.assertEqual(DC(1).meth(), "Base Child")
+
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/Misc/NEWS.d/next/Library/2026-04-23-21-47-49.gh-issue-148947.W4V2lG.rst b/Misc/NEWS.d/next/Library/2026-04-23-21-47-49.gh-issue-148947.W4V2lG.rst
new file mode 100644 (file)
index 0000000..f978326
--- /dev/null
@@ -0,0 +1,2 @@
+Fix crash in :deco:`dataclasses.dataclass` with ``slots=True`` that occurred
+when a function found within the class had an empty ``__class__`` cell.