]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-135228: When @dataclass(slots=True) replaces a dataclass, make the original class...
authorJelle Zijlstra <jelle.zijlstra@gmail.com>
Tue, 22 Jul 2025 04:43:34 +0000 (21:43 -0700)
committerGitHub <noreply@github.com>
Tue, 22 Jul 2025 04:43:34 +0000 (21:43 -0700)
An interesting hack, but more localized in scope than #135230.

This may be a breaking change if people intentionally keep the original class around
when using `@dataclass(slots=True)`, and then use `__dict__` or `__weakref__` on the
original class.

Co-authored-by: Alyssa Coghlan <ncoghlan@gmail.com>
Lib/dataclasses.py
Lib/test/test_dataclasses/__init__.py
Misc/NEWS.d/next/Library/2025-07-20-16-56-55.gh-issue-135228.n_XIao.rst [new file with mode: 0644]

index 83ea623dce6281be6e0e738b0746554ce5ab3139..22b78bb2fbe6ed3784d701cf8a4480a065ac7d03 100644 (file)
@@ -1338,6 +1338,13 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
                 or _update_func_cell_for__class__(member.fdel, cls, newcls)):
                 break
 
+    # gh-135228: Make sure the original class can be garbage collected.
+    # Bypass mapping proxy to allow __dict__ to be removed
+    old_cls_dict = cls.__dict__ | _deproxier
+    old_cls_dict.pop('__dict__', None)
+    if "__weakref__" in cls.__dict__:
+        del cls.__weakref__
+
     return newcls
 
 
@@ -1732,3 +1739,11 @@ def _replace(self, /, **changes):
     # changes that aren't fields, this will correctly raise a
     # TypeError.
     return self.__class__(**changes)
+
+
+# Hack to the get the underlying dict out of a mappingproxy
+# Use it with: cls.__dict__ | _deproxier
+class _Deproxier:
+    def __ror__(self, other):
+        return other
+_deproxier = _Deproxier()
index e98a8f284cec9fd0d2b8257f1f9b74318c06840a..6bf5e5b3e5554be954ac4b34b5c78fda27531311 100644 (file)
@@ -3804,6 +3804,41 @@ class TestSlots(unittest.TestCase):
         # that we create internally.
         self.assertEqual(CorrectSuper.args, ["default", "default"])
 
+    def test_original_class_is_gced(self):
+        # gh-135228: Make sure when we replace the class with slots=True, the original class
+        # gets garbage collected.
+        def make_simple():
+            @dataclass(slots=True)
+            class SlotsTest:
+                pass
+
+            return SlotsTest
+
+        def make_with_annotations():
+            @dataclass(slots=True)
+            class SlotsTest:
+                x: int
+
+            return SlotsTest
+
+        def make_with_annotations_and_method():
+            @dataclass(slots=True)
+            class SlotsTest:
+                x: int
+
+                def method(self) -> int:
+                    return self.x
+
+            return SlotsTest
+
+        for make in (make_simple, make_with_annotations, make_with_annotations_and_method):
+            with self.subTest(make=make):
+                C = make()
+                support.gc_collect()
+                candidates = [cls for cls in object.__subclasses__() if cls.__name__ == 'SlotsTest'
+                              and cls.__firstlineno__ == make.__code__.co_firstlineno + 1]
+                self.assertEqual(candidates, [C])
+
 
 class TestDescriptors(unittest.TestCase):
     def test_set_name(self):
diff --git a/Misc/NEWS.d/next/Library/2025-07-20-16-56-55.gh-issue-135228.n_XIao.rst b/Misc/NEWS.d/next/Library/2025-07-20-16-56-55.gh-issue-135228.n_XIao.rst
new file mode 100644 (file)
index 0000000..ee8962c
--- /dev/null
@@ -0,0 +1,4 @@
+When :mod:`dataclasses` replaces a class with a slotted dataclass, the
+original class is now garbage collected again. Earlier changes in Python
+3.14 caused this class to remain in existence together with the replacement
+class synthesized by :mod:`dataclasses`.