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

Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
Co-authored-by: Alyssa Coghlan <ncoghlan@gmail.com>
Co-authored-by: Petr Viktorin <encukou@gmail.com>
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
Miss Islington (bot) 2025-08-12 16:17:26 +02:00 committed by GitHub
parent daed07a1f9
commit 4dda1768be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 117 additions and 7 deletions

View file

@ -1283,6 +1283,10 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
if '__slots__' in cls.__dict__:
raise TypeError(f'{cls.__name__} already specifies __slots__')
# gh-102069: Remove existing __weakref__ descriptor.
# gh-135228: Make sure the original class can be garbage collected.
sys._clear_type_descriptors(cls)
# Create a new dict for our new class.
cls_dict = dict(cls.__dict__)
field_names = tuple(f.name for f in fields(cls))
@ -1300,12 +1304,6 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
# available in _MARKER.
cls_dict.pop(field_name, None)
# Remove __dict__ itself.
cls_dict.pop('__dict__', None)
# Clear existing `__weakref__` descriptor, it belongs to a previous type:
cls_dict.pop('__weakref__', None) # gh-102069
# And finally create the class.
qualname = getattr(cls, '__qualname__', None)
newcls = type(cls)(cls.__name__, cls.__bases__, cls_dict)

View 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):

View file

@ -0,0 +1,4 @@
When :mod:`dataclasses` replaces a class with a slotted dataclass, the
original class can now be garbage collected again. Earlier changes in Python
3.14 caused this class to always remain in existence together with the replacement
class synthesized by :mod:`dataclasses`.

View file

@ -1793,6 +1793,37 @@ sys__baserepl(PyObject *module, PyObject *Py_UNUSED(ignored))
return sys__baserepl_impl(module);
}
PyDoc_STRVAR(sys__clear_type_descriptors__doc__,
"_clear_type_descriptors($module, type, /)\n"
"--\n"
"\n"
"Private function for clearing certain descriptors from a type\'s dictionary.\n"
"\n"
"See gh-135228 for context.");
#define SYS__CLEAR_TYPE_DESCRIPTORS_METHODDEF \
{"_clear_type_descriptors", (PyCFunction)sys__clear_type_descriptors, METH_O, sys__clear_type_descriptors__doc__},
static PyObject *
sys__clear_type_descriptors_impl(PyObject *module, PyObject *type);
static PyObject *
sys__clear_type_descriptors(PyObject *module, PyObject *arg)
{
PyObject *return_value = NULL;
PyObject *type;
if (!PyObject_TypeCheck(arg, &PyType_Type)) {
_PyArg_BadArgument("_clear_type_descriptors", "argument", (&PyType_Type)->tp_name, arg);
goto exit;
}
type = arg;
return_value = sys__clear_type_descriptors_impl(module, type);
exit:
return return_value;
}
PyDoc_STRVAR(sys__is_gil_enabled__doc__,
"_is_gil_enabled($module, /)\n"
"--\n"
@ -1948,4 +1979,4 @@ exit:
#ifndef SYS_GETANDROIDAPILEVEL_METHODDEF
#define SYS_GETANDROIDAPILEVEL_METHODDEF
#endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */
/*[clinic end generated code: output=449d16326e69dcf6 input=a9049054013a1b77]*/
/*[clinic end generated code: output=9052f399f40ca32d input=a9049054013a1b77]*/

View file

@ -2640,6 +2640,47 @@ sys__baserepl_impl(PyObject *module)
Py_RETURN_NONE;
}
/*[clinic input]
sys._clear_type_descriptors
type: object(subclass_of='&PyType_Type')
/
Private function for clearing certain descriptors from a type's dictionary.
See gh-135228 for context.
[clinic start generated code]*/
static PyObject *
sys__clear_type_descriptors_impl(PyObject *module, PyObject *type)
/*[clinic end generated code: output=5ad17851b762b6d9 input=dc536c97fde07251]*/
{
PyTypeObject *typeobj = (PyTypeObject *)type;
if (_PyType_HasFeature(typeobj, Py_TPFLAGS_IMMUTABLETYPE)) {
PyErr_SetString(PyExc_TypeError, "argument is immutable");
return NULL;
}
PyObject *dict = _PyType_GetDict(typeobj);
PyObject *dunder_dict = NULL;
if (PyDict_Pop(dict, &_Py_ID(__dict__), &dunder_dict) < 0) {
return NULL;
}
PyObject *dunder_weakref = NULL;
if (PyDict_Pop(dict, &_Py_ID(__weakref__), &dunder_weakref) < 0) {
PyType_Modified(typeobj);
Py_XDECREF(dunder_dict);
return NULL;
}
PyType_Modified(typeobj);
// We try to hold onto a reference to these until after we call
// PyType_Modified(), in case their deallocation triggers somer user code
// that tries to do something to the type.
Py_XDECREF(dunder_dict);
Py_XDECREF(dunder_weakref);
Py_RETURN_NONE;
}
/*[clinic input]
sys._is_gil_enabled -> bool
@ -2836,6 +2877,7 @@ static PyMethodDef sys_methods[] = {
SYS__STATS_DUMP_METHODDEF
#endif
SYS__GET_CPU_COUNT_CONFIG_METHODDEF
SYS__CLEAR_TYPE_DESCRIPTORS_METHODDEF
SYS__IS_GIL_ENABLED_METHODDEF
SYS__DUMP_TRACELETS_METHODDEF
{NULL, NULL} // sentinel