mirror of
https://github.com/python/cpython.git
synced 2025-11-24 12:20:42 +00:00
[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:
parent
daed07a1f9
commit
4dda1768be
5 changed files with 117 additions and 7 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
33
Python/clinic/sysmodule.c.h
generated
33
Python/clinic/sysmodule.c.h
generated
|
|
@ -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]*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue