[3.14] gh-135228: When @dataclass(slots=True) replaces a dataclass, make the original class collectible (GH-136893) (#136960)

gh-135228: When @dataclass(slots=True) replaces a dataclass, make the original class collectible (GH-136893)

An interesting hack, but more localized in scope than GH-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.
(cherry picked from commit 46cbdf967a)

Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
Co-authored-by: Alyssa Coghlan <ncoghlan@gmail.com>
This commit is contained in:
Miss Islington (bot) 2025-07-22 07:08:15 +02:00 committed by GitHub
parent caef946a25
commit 6e1b31b87e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 54 additions and 0 deletions

View 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)): or _update_func_cell_for__class__(member.fdel, cls, newcls)):
break 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 return newcls
@ -1732,3 +1739,11 @@ def _replace(self, /, **changes):
# changes that aren't fields, this will correctly raise a # changes that aren't fields, this will correctly raise a
# TypeError. # TypeError.
return self.__class__(**changes) 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()

View file

@ -3804,6 +3804,41 @@ class WithCorrectSuper(CorrectSuper):
# that we create internally. # that we create internally.
self.assertEqual(CorrectSuper.args, ["default", "default"]) 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): class TestDescriptors(unittest.TestCase):
def test_set_name(self): def test_set_name(self):

View file

@ -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`.