Skip to content

Commit 6859b95

Browse files
JelleZijlstrancoghlanencukouserhiy-storchaka
authored
gh-135228: When @DataClass(slots=True) replaces a dataclass, make the original class collectible (take 2) (GH-137047)
Remove the `__dict__` and `__weakref__` descriptors from the original class when creating a dataclass from it. 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. Co-authored-by: Alyssa Coghlan <[email protected]> Co-authored-by: Petr Viktorin <[email protected]> Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 027cacb commit 6859b95

File tree

5 files changed

+117
-7
lines changed

5 files changed

+117
-7
lines changed

Lib/dataclasses.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,6 +1283,10 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
12831283
if '__slots__' in cls.__dict__:
12841284
raise TypeError(f'{cls.__name__} already specifies __slots__')
12851285

1286+
# gh-102069: Remove existing __weakref__ descriptor.
1287+
# gh-135228: Make sure the original class can be garbage collected.
1288+
sys._clear_type_descriptors(cls)
1289+
12861290
# Create a new dict for our new class.
12871291
cls_dict = dict(cls.__dict__)
12881292
field_names = tuple(f.name for f in fields(cls))
@@ -1300,12 +1304,6 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
13001304
# available in _MARKER.
13011305
cls_dict.pop(field_name, None)
13021306

1303-
# Remove __dict__ itself.
1304-
cls_dict.pop('__dict__', None)
1305-
1306-
# Clear existing `__weakref__` descriptor, it belongs to a previous type:
1307-
cls_dict.pop('__weakref__', None) # gh-102069
1308-
13091307
# And finally create the class.
13101308
qualname = getattr(cls, '__qualname__', None)
13111309
newcls = type(cls)(cls.__name__, cls.__bases__, cls_dict)

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 can now be garbage collected again. Earlier changes in Python
3+
3.14 caused this class to always remain in existence together with the replacement
4+
class synthesized by :mod:`dataclasses`.

Python/clinic/sysmodule.c.h

Lines changed: 32 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/sysmodule.c

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2641,6 +2641,47 @@ sys__baserepl_impl(PyObject *module)
26412641
Py_RETURN_NONE;
26422642
}
26432643

2644+
/*[clinic input]
2645+
sys._clear_type_descriptors
2646+
2647+
type: object(subclass_of='&PyType_Type')
2648+
/
2649+
2650+
Private function for clearing certain descriptors from a type's dictionary.
2651+
2652+
See gh-135228 for context.
2653+
[clinic start generated code]*/
2654+
2655+
static PyObject *
2656+
sys__clear_type_descriptors_impl(PyObject *module, PyObject *type)
2657+
/*[clinic end generated code: output=5ad17851b762b6d9 input=dc536c97fde07251]*/
2658+
{
2659+
PyTypeObject *typeobj = (PyTypeObject *)type;
2660+
if (_PyType_HasFeature(typeobj, Py_TPFLAGS_IMMUTABLETYPE)) {
2661+
PyErr_SetString(PyExc_TypeError, "argument is immutable");
2662+
return NULL;
2663+
}
2664+
PyObject *dict = _PyType_GetDict(typeobj);
2665+
PyObject *dunder_dict = NULL;
2666+
if (PyDict_Pop(dict, &_Py_ID(__dict__), &dunder_dict) < 0) {
2667+
return NULL;
2668+
}
2669+
PyObject *dunder_weakref = NULL;
2670+
if (PyDict_Pop(dict, &_Py_ID(__weakref__), &dunder_weakref) < 0) {
2671+
PyType_Modified(typeobj);
2672+
Py_XDECREF(dunder_dict);
2673+
return NULL;
2674+
}
2675+
PyType_Modified(typeobj);
2676+
// We try to hold onto a reference to these until after we call
2677+
// PyType_Modified(), in case their deallocation triggers somer user code
2678+
// that tries to do something to the type.
2679+
Py_XDECREF(dunder_dict);
2680+
Py_XDECREF(dunder_weakref);
2681+
Py_RETURN_NONE;
2682+
}
2683+
2684+
26442685
/*[clinic input]
26452686
sys._is_gil_enabled -> bool
26462687
@@ -2837,6 +2878,7 @@ static PyMethodDef sys_methods[] = {
28372878
SYS__STATS_DUMP_METHODDEF
28382879
#endif
28392880
SYS__GET_CPU_COUNT_CONFIG_METHODDEF
2881+
SYS__CLEAR_TYPE_DESCRIPTORS_METHODDEF
28402882
SYS__IS_GIL_ENABLED_METHODDEF
28412883
SYS__DUMP_TRACELETS_METHODDEF
28422884
{NULL, NULL} // sentinel

0 commit comments

Comments
 (0)