From 027f629dd5711be1348e5461177b3de7ed0dc44e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 20 Jul 2025 16:57:40 -0700 Subject: [PATCH 1/7] gh-135228: When @dataclass(slots=True) replaces a dataclass, make the original class collectible An interesting hack, but more localized in scope than #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. --- Lib/dataclasses.py | 13 +++++++ Lib/test/test_dataclasses/__init__.py | 35 +++++++++++++++++++ ...-07-20-16-56-55.gh-issue-135228.n_XIao.rst | 4 +++ 3 files changed, 52 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-07-20-16-56-55.gh-issue-135228.n_XIao.rst diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 83ea623dce6281..9a1d5071e4157c 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1338,6 +1338,11 @@ 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. + old_cls_dict = cls.__dict__ | _deproxier + old_cls_dict.pop('__weakref__', None) + old_cls_dict.pop('__dict__', None) + return newcls @@ -1732,3 +1737,11 @@ def _replace(self, /, **changes): # changes that aren't fields, this will correctly raise a # TypeError. return self.__class__(**changes) + + +# Hack to the get the underlying dict out of a mappingproxy +# Use it with: cls.__dict__ | _deproxier +class _Deproxier: + def __ror__(self, other): + return other +_deproxier = _Deproxier() diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index e98a8f284cec9f..6bf5e5b3e5554b 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -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): diff --git a/Misc/NEWS.d/next/Library/2025-07-20-16-56-55.gh-issue-135228.n_XIao.rst b/Misc/NEWS.d/next/Library/2025-07-20-16-56-55.gh-issue-135228.n_XIao.rst new file mode 100644 index 00000000000000..ee8962c6f46e75 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-20-16-56-55.gh-issue-135228.n_XIao.rst @@ -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`. From c82d86bb7a6c7dc1887fa95f407d355f0e1f6421 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 22 Jul 2025 08:32:17 +0200 Subject: [PATCH 2/7] Keep just the test --- Lib/dataclasses.py | 13 ------------- .../2025-07-20-16-56-55.gh-issue-135228.n_XIao.rst | 4 ---- 2 files changed, 17 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2025-07-20-16-56-55.gh-issue-135228.n_XIao.rst diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 9a1d5071e4157c..83ea623dce6281 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1338,11 +1338,6 @@ 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. - old_cls_dict = cls.__dict__ | _deproxier - old_cls_dict.pop('__weakref__', None) - old_cls_dict.pop('__dict__', None) - return newcls @@ -1737,11 +1732,3 @@ def _replace(self, /, **changes): # changes that aren't fields, this will correctly raise a # TypeError. return self.__class__(**changes) - - -# Hack to the get the underlying dict out of a mappingproxy -# Use it with: cls.__dict__ | _deproxier -class _Deproxier: - def __ror__(self, other): - return other -_deproxier = _Deproxier() diff --git a/Misc/NEWS.d/next/Library/2025-07-20-16-56-55.gh-issue-135228.n_XIao.rst b/Misc/NEWS.d/next/Library/2025-07-20-16-56-55.gh-issue-135228.n_XIao.rst deleted file mode 100644 index ee8962c6f46e75..00000000000000 --- a/Misc/NEWS.d/next/Library/2025-07-20-16-56-55.gh-issue-135228.n_XIao.rst +++ /dev/null @@ -1,4 +0,0 @@ -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`. From 9ef8632fbfddf90d5a0eb4060f7e511adf6d235b Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 22 Jul 2025 09:14:34 +0200 Subject: [PATCH 3/7] Create __dict__ and __weakref__ descriptors for `object` --- Objects/typeobject.c | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 379c4d0467c487..bef59746acab96 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -4038,22 +4038,25 @@ subtype_getweakref(PyObject *obj, void *context) /* Three variants on the subtype_getsets list. */ +static char subtype_getset_dict_name[] = "__dict__"; +static char subtype_getset_weakref_name[] = "__weakref__"; + static PyGetSetDef subtype_getsets_full[] = { - {"__dict__", subtype_dict, subtype_setdict, + {subtype_getset_dict_name, subtype_dict, subtype_setdict, PyDoc_STR("dictionary for instance variables")}, - {"__weakref__", subtype_getweakref, NULL, + {subtype_getset_weakref_name, subtype_getweakref, NULL, PyDoc_STR("list of weak references to the object")}, {0} }; static PyGetSetDef subtype_getsets_dict_only[] = { - {"__dict__", subtype_dict, subtype_setdict, + {subtype_getset_dict_name, subtype_dict, subtype_setdict, PyDoc_STR("dictionary for instance variables")}, {0} }; static PyGetSetDef subtype_getsets_weakref_only[] = { - {"__weakref__", subtype_getweakref, NULL, + {subtype_getset_weakref_name, subtype_getweakref, NULL, PyDoc_STR("list of weak references to the object")}, {0} }; @@ -8329,7 +8332,16 @@ type_add_getset(PyTypeObject *type) PyObject *dict = lookup_tp_dict(type); for (; gsp->name != NULL; gsp++) { - PyObject *descr = PyDescr_NewGetSet(type, gsp); + PyTypeObject *descr_type = type; + // Hack: dict and weakref descriptors are created for `object`, + // rather than this specific type. + // We identify their PyGetSetDef by pointer equality on name. + if (gsp->name == subtype_getset_dict_name + || gsp->name == subtype_getset_weakref_name) + { + descr_type = &PyBaseObject_Type; + } + PyObject *descr = PyDescr_NewGetSet(descr_type, gsp); if (descr == NULL) { return -1; } From 696f1d0e6a2c8eba84c005cd0f103d7f865ac1f8 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 22 Jul 2025 09:26:45 +0200 Subject: [PATCH 4/7] Adjust internals of inspect.getattr_static --- Lib/inspect.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index 183e67fabf966e..d7814bfeb2b885 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1698,7 +1698,8 @@ def _shadowed_dict_from_weakref_mro_tuple(*weakref_mro): class_dict = dunder_dict['__dict__'] if not (type(class_dict) is types.GetSetDescriptorType and class_dict.__name__ == "__dict__" and - class_dict.__objclass__ is entry): + (class_dict.__objclass__ is object or + class_dict.__objclass__ is entry)): return class_dict return _sentinel From d0d2f73566699e2c0114fcd1466e023b53e01b77 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 30 Jul 2025 17:05:15 +0200 Subject: [PATCH 5/7] Share __dict__ and __weakref__ descriptors --- Include/internal/pycore_interp_structs.h | 7 ++ Include/internal/pycore_typeobject.h | 1 + Objects/typeobject.c | 114 ++++++++++++++--------- Python/pylifecycle.c | 1 + 4 files changed, 78 insertions(+), 45 deletions(-) diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index 542a75617b4d3c..540e25d46c4df3 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -691,6 +691,13 @@ struct _Py_interp_cached_objects { PyTypeObject *paramspecargs_type; PyTypeObject *paramspeckwargs_type; PyTypeObject *constevaluator_type; + + /* Descriptors for __dict__ and __weakref__ */ +#ifdef Py_GIL_DISABLED + PyMutex descriptor_mutex; +#endif + PyObject *dict_descriptor; + PyObject *weakref_descriptor; }; struct _Py_interp_static_objects { diff --git a/Include/internal/pycore_typeobject.h b/Include/internal/pycore_typeobject.h index 0ee7d555c56cdd..24df69aa93fda2 100644 --- a/Include/internal/pycore_typeobject.h +++ b/Include/internal/pycore_typeobject.h @@ -40,6 +40,7 @@ extern void _PyTypes_FiniTypes(PyInterpreterState *); extern void _PyTypes_FiniExtTypes(PyInterpreterState *interp); extern void _PyTypes_Fini(PyInterpreterState *); extern void _PyTypes_AfterFork(void); +extern void _PyTypes_FiniCachedDescriptors(PyInterpreterState *); static inline PyObject ** _PyStaticType_GET_WEAKREFS_LISTPTR(managed_static_type_state *state) diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 04df120860c621..992c045028c22b 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -4036,29 +4036,15 @@ subtype_getweakref(PyObject *obj, void *context) return Py_NewRef(result); } -/* Three variants on the subtype_getsets list. */ - -static char subtype_getset_dict_name[] = "__dict__"; -static char subtype_getset_weakref_name[] = "__weakref__"; - -static PyGetSetDef subtype_getsets_full[] = { - {subtype_getset_dict_name, subtype_dict, subtype_setdict, - PyDoc_STR("dictionary for instance variables")}, - {subtype_getset_weakref_name, subtype_getweakref, NULL, - PyDoc_STR("list of weak references to the object")}, - {0} +/* getset definitions for common descriptors */ +static PyGetSetDef subtype_getset_dict = { + "__dict__", subtype_dict, subtype_setdict, + PyDoc_STR("dictionary for instance variables"), }; -static PyGetSetDef subtype_getsets_dict_only[] = { - {subtype_getset_dict_name, subtype_dict, subtype_setdict, - PyDoc_STR("dictionary for instance variables")}, - {0} -}; - -static PyGetSetDef subtype_getsets_weakref_only[] = { - {subtype_getset_weakref_name, subtype_getweakref, NULL, - PyDoc_STR("list of weak references to the object")}, - {0} +static PyGetSetDef subtype_getset_weakref = { + "__weakref__", subtype_getweakref, NULL, + PyDoc_STR("list of weak references to the object"), }; static int @@ -4594,10 +4580,36 @@ type_new_classmethod(PyObject *dict, PyObject *attr) return 0; } +/* Add __dict__ or __weakref__ descriptor */ +static int +type_add_common_descriptor(PyInterpreterState *interp, + PyObject **cache, + PyGetSetDef *getset_def, + PyObject *dict) +{ +#ifdef Py_GIL_DISABLED + PyMutex_Lock(&interp->cached_objects.descriptor_mutex); +#endif + PyObject *descr = *cache; + if (!descr) { + descr = PyDescr_NewGetSet(&PyBaseObject_Type, getset_def); + *cache = descr; + } +#ifdef Py_GIL_DISABLED + PyMutex_Unlock(&interp->cached_objects.descriptor_mutex); +#endif + if (!descr) { + return -1; + } + if (PyDict_SetDefaultRef(dict, PyDescr_NAME(descr), descr, NULL) < 0) { + return -1; + } + return 0; +} /* Add descriptors for custom slots from __slots__, or for __dict__ */ static int -type_new_descriptors(const type_new_ctx *ctx, PyTypeObject *type) +type_new_descriptors(const type_new_ctx *ctx, PyTypeObject *type, PyObject *dict) { PyHeapTypeObject *et = (PyHeapTypeObject *)type; Py_ssize_t slotoffset = ctx->base->tp_basicsize; @@ -4635,6 +4647,30 @@ type_new_descriptors(const type_new_ctx *ctx, PyTypeObject *type) type->tp_basicsize = slotoffset; type->tp_itemsize = ctx->base->tp_itemsize; type->tp_members = _PyHeapType_GET_MEMBERS(et); + + PyInterpreterState *interp = _PyInterpreterState_GET(); + + if (type->tp_dictoffset) { + if (type_add_common_descriptor( + interp, + &interp->cached_objects.dict_descriptor, + &subtype_getset_dict, + dict) < 0) + { + return -1; + } + } + if (type->tp_weaklistoffset) { + if (type_add_common_descriptor( + interp, + &interp->cached_objects.weakref_descriptor, + &subtype_getset_weakref, + dict) < 0) + { + return -1; + } + } + return 0; } @@ -4642,18 +4678,7 @@ type_new_descriptors(const type_new_ctx *ctx, PyTypeObject *type) static void type_new_set_slots(const type_new_ctx *ctx, PyTypeObject *type) { - if (type->tp_weaklistoffset && type->tp_dictoffset) { - type->tp_getset = subtype_getsets_full; - } - else if (type->tp_weaklistoffset && !type->tp_dictoffset) { - type->tp_getset = subtype_getsets_weakref_only; - } - else if (!type->tp_weaklistoffset && type->tp_dictoffset) { - type->tp_getset = subtype_getsets_dict_only; - } - else { - type->tp_getset = NULL; - } + type->tp_getset = NULL; /* Special case some slots */ if (type->tp_dictoffset != 0 || ctx->nslot > 0) { @@ -4758,7 +4783,7 @@ type_new_set_attrs(const type_new_ctx *ctx, PyTypeObject *type) return -1; } - if (type_new_descriptors(ctx, type) < 0) { + if (type_new_descriptors(ctx, type, dict) < 0) { return -1; } @@ -6642,6 +6667,14 @@ _PyStaticType_FiniBuiltin(PyInterpreterState *interp, PyTypeObject *type) } +void +_PyTypes_FiniCachedDescriptors(PyInterpreterState *interp) +{ + Py_CLEAR(interp->cached_objects.dict_descriptor); + Py_CLEAR(interp->cached_objects.weakref_descriptor); +} + + static void type_dealloc(PyObject *self) { @@ -8332,16 +8365,7 @@ type_add_getset(PyTypeObject *type) PyObject *dict = lookup_tp_dict(type); for (; gsp->name != NULL; gsp++) { - PyTypeObject *descr_type = type; - // Hack: dict and weakref descriptors are created for `object`, - // rather than this specific type. - // We identify their PyGetSetDef by pointer equality on name. - if (gsp->name == subtype_getset_dict_name - || gsp->name == subtype_getset_weakref_name) - { - descr_type = &PyBaseObject_Type; - } - PyObject *descr = PyDescr_NewGetSet(descr_type, gsp); + PyObject *descr = PyDescr_NewGetSet(type, gsp); if (descr == NULL) { return -1; } diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index e22a9cc1c75050..b6b1d2845ec2f1 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -1906,6 +1906,7 @@ finalize_interp_clear(PyThreadState *tstate) _PyXI_Fini(tstate->interp); _PyExc_ClearExceptionGroupType(tstate->interp); _Py_clear_generic_types(tstate->interp); + _PyTypes_FiniCachedDescriptors(tstate->interp); /* Clear interpreter state and all thread states */ _PyInterpreterState_Clear(tstate); From dc541cdcfcd2f2c7acd788fb546b3b8dcfd6b3ea Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 30 Jul 2025 17:22:18 +0200 Subject: [PATCH 6/7] Adjust __repr__ of generic descriptors (ones whose __objclass__ is type) --- Objects/descrobject.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Objects/descrobject.c b/Objects/descrobject.c index d3d17e92b6d1e8..06a81a4fdbd865 100644 --- a/Objects/descrobject.c +++ b/Objects/descrobject.c @@ -39,41 +39,41 @@ descr_name(PyDescrObject *descr) } static PyObject * -descr_repr(PyDescrObject *descr, const char *format) +descr_repr(PyDescrObject *descr, const char *kind) { PyObject *name = NULL; if (descr->d_name != NULL && PyUnicode_Check(descr->d_name)) name = descr->d_name; - return PyUnicode_FromFormat(format, name, "?", descr->d_type->tp_name); + if (descr->d_type == &PyBaseObject_Type) { + return PyUnicode_FromFormat("<%s '%V'>", kind, name, "?"); + } + return PyUnicode_FromFormat("<%s '%V' of '%s' objects>", + kind, name, "?", descr->d_type->tp_name); } static PyObject * method_repr(PyObject *descr) { - return descr_repr((PyDescrObject *)descr, - ""); + return descr_repr((PyDescrObject *)descr, "method"); } static PyObject * member_repr(PyObject *descr) { - return descr_repr((PyDescrObject *)descr, - ""); + return descr_repr((PyDescrObject *)descr, "member"); } static PyObject * getset_repr(PyObject *descr) { - return descr_repr((PyDescrObject *)descr, - ""); + return descr_repr((PyDescrObject *)descr, "attribute"); } static PyObject * wrapperdescr_repr(PyObject *descr) { - return descr_repr((PyDescrObject *)descr, - ""); + return descr_repr((PyDescrObject *)descr, "slot wrapper"); } static int From 1e2fd8a8aca77b32dadcbc50aedf64afc9981d9b Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 30 Jul 2025 17:40:18 +0200 Subject: [PATCH 7/7] Add PyGetSetDef to c-analyzer's list of types --- Tools/c-analyzer/cpython/_analyzer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Tools/c-analyzer/cpython/_analyzer.py b/Tools/c-analyzer/cpython/_analyzer.py index 6204353e9bd26a..6f0f464892845f 100644 --- a/Tools/c-analyzer/cpython/_analyzer.py +++ b/Tools/c-analyzer/cpython/_analyzer.py @@ -67,6 +67,7 @@ 'PyMethodDef', 'PyMethodDef[]', 'PyMemberDef[]', + 'PyGetSetDef', 'PyGetSetDef[]', 'PyNumberMethods', 'PySequenceMethods', pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy