Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1338,6 +1338,9 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
or _update_func_cell_for__class__(member.fdel, cls, newcls)):
break

# gh-135228: Make sure the original class can be garbage collected.
sys._clear_type_descriptors(cls)

return newcls


Expand Down
35 changes: 35 additions & 0 deletions Lib/test/test_dataclasses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3804,6 +3804,41 @@ class WithCorrectSuper(CorrectSuper):
# 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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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`.
13 changes: 12 additions & 1 deletion Python/clinic/sysmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions Python/sysmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2641,6 +2641,51 @@ sys__baserepl_impl(PyObject *module)
Py_RETURN_NONE;
}

/*[clinic input]
sys._clear_type_descriptors

type: object
/

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(PyObject *module, PyObject *type)
/*[clinic end generated code: output=7d5cefcf861909e0 input=5fdc23500d477de6]*/
{
if (!PyType_Check(type)) {
PyErr_SetString(PyExc_TypeError, "argument must be a type");
return NULL;
}
PyTypeObject *typeobj = (PyTypeObject *)(type);
if (!_PyType_HasFeature(typeobj, Py_TPFLAGS_HEAPTYPE)) {
PyErr_SetString(PyExc_TypeError, "argument must be a heap type");
return NULL;
}
PyObject *dict = _PyType_GetDict(typeobj);
PyObject *dunder_dict = NULL;
if (PyDict_PopString(dict, "__dict__", &dunder_dict) < 0) {
return NULL;
}
PyObject *dunder_weakref = NULL;
if (PyDict_PopString(dict, "__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

Expand Down Expand Up @@ -2837,6 +2882,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
Expand Down
Loading