Skip to content

Commit 6e1b31b

Browse files
miss-islingtonJelleZijlstrancoghlan
authored
[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 46cbdf9) Co-authored-by: Jelle Zijlstra <[email protected]> Co-authored-by: Alyssa Coghlan <[email protected]>
1 parent caef946 commit 6e1b31b

File tree

3 files changed

+54
-0
lines changed

3 files changed

+54
-0
lines changed

Lib/dataclasses.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,6 +1338,13 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
13381338
or _update_func_cell_for__class__(member.fdel, cls, newcls)):
13391339
break
13401340

1341+
# gh-135228: Make sure the original class can be garbage collected.
1342+
# Bypass mapping proxy to allow __dict__ to be removed
1343+
old_cls_dict = cls.__dict__ | _deproxier
1344+
old_cls_dict.pop('__dict__', None)
1345+
if "__weakref__" in cls.__dict__:
1346+
del cls.__weakref__
1347+
13411348
return newcls
13421349

13431350

@@ -1732,3 +1739,11 @@ def _replace(self, /, **changes):
17321739
# changes that aren't fields, this will correctly raise a
17331740
# TypeError.
17341741
return self.__class__(**changes)
1742+
1743+
1744+
# Hack to the get the underlying dict out of a mappingproxy
1745+
# Use it with: cls.__dict__ | _deproxier
1746+
class _Deproxier:
1747+
def __ror__(self, other):
1748+
return other
1749+
_deproxier = _Deproxier()

Lib/test/test_dataclasses/__init__.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3804,6 +3804,41 @@ class WithCorrectSuper(CorrectSuper):
38043804
# that we create internally.
38053805
self.assertEqual(CorrectSuper.args, ["default", "default"])
38063806

3807+
def test_original_class_is_gced(self):
3808+
# gh-135228: Make sure when we replace the class with slots=True, the original class
3809+
# gets garbage collected.
3810+
def make_simple():
3811+
@dataclass(slots=True)
3812+
class SlotsTest:
3813+
pass
3814+
3815+
return SlotsTest
3816+
3817+
def make_with_annotations():
3818+
@dataclass(slots=True)
3819+
class SlotsTest:
3820+
x: int
3821+
3822+
return SlotsTest
3823+
3824+
def make_with_annotations_and_method():
3825+
@dataclass(slots=True)
3826+
class SlotsTest:
3827+
x: int
3828+
3829+
def method(self) -> int:
3830+
return self.x
3831+
3832+
return SlotsTest
3833+
3834+
for make in (make_simple, make_with_annotations, make_with_annotations_and_method):
3835+
with self.subTest(make=make):
3836+
C = make()
3837+
support.gc_collect()
3838+
candidates = [cls for cls in object.__subclasses__() if cls.__name__ == 'SlotsTest'
3839+
and cls.__firstlineno__ == make.__code__.co_firstlineno + 1]
3840+
self.assertEqual(candidates, [C])
3841+
38073842

38083843
class TestDescriptors(unittest.TestCase):
38093844
def test_set_name(self):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
When :mod:`dataclasses` replaces a class with a slotted dataclass, the
2+
original class is now garbage collected again. Earlier changes in Python
3+
3.14 caused this class to remain in existence together with the replacement
4+
class synthesized by :mod:`dataclasses`.

0 commit comments

Comments
 (0)