From 916ce1bfd665054ef8133ded21cf03919794524c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 21 Jul 2025 21:43:34 -0700 Subject: [PATCH 1/7] gh-135228: When @dataclass(slots=True) replaces a dataclass, make the original class collectible (#136893) 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. Co-authored-by: Alyssa Coghlan --- Lib/dataclasses.py | 15 ++++++++ Lib/test/test_dataclasses/__init__.py | 35 +++++++++++++++++++ ...-07-20-16-56-55.gh-issue-135228.n_XIao.rst | 4 +++ 3 files changed, 54 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..22b78bb2fbe6ed 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1338,6 +1338,13 @@ 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. + # Bypass mapping proxy to allow __dict__ to be removed + old_cls_dict = cls.__dict__ | _deproxier + old_cls_dict.pop('__dict__', None) + if "__weakref__" in cls.__dict__: + del cls.__weakref__ + return newcls @@ -1732,3 +1739,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 6dc0f4804655b57efd7db6637876910335b09d4d Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 23 Jul 2025 08:29:40 -0700 Subject: [PATCH 2/7] Use a new private sys function instead --- Lib/dataclasses.py | 13 +------------ Python/clinic/sysmodule.c.h | 13 ++++++++++++- Python/sysmodule.c | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 22b78bb2fbe6ed..7db595250e96b9 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1340,10 +1340,7 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields): # gh-135228: Make sure the original class can be garbage collected. # Bypass mapping proxy to allow __dict__ to be removed - old_cls_dict = cls.__dict__ | _deproxier - old_cls_dict.pop('__dict__', None) - if "__weakref__" in cls.__dict__: - del cls.__weakref__ + sys._clear_type_descriptors(cls) return newcls @@ -1739,11 +1736,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/Python/clinic/sysmodule.c.h b/Python/clinic/sysmodule.c.h index a47e4d11b54441..2e934b60384899 100644 --- a/Python/clinic/sysmodule.c.h +++ b/Python/clinic/sysmodule.c.h @@ -1793,6 +1793,17 @@ 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__}, + PyDoc_STRVAR(sys__is_gil_enabled__doc__, "_is_gil_enabled($module, /)\n" "--\n" @@ -1948,4 +1959,4 @@ _jit_is_active(PyObject *module, PyObject *Py_UNUSED(ignored)) #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=f75cd2babc1841db input=a9049054013a1b77]*/ diff --git a/Python/sysmodule.c b/Python/sysmodule.c index ae6cf306735939..b7d4a757fafb2e 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -2641,6 +2641,38 @@ 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); + PyObject *dict = _PyType_GetDict(typeobj); + if (PyDict_PopString(dict, "__dict__", NULL) < 0) { + return NULL; + } + if (PyDict_PopString(dict, "__weakref__", NULL) < 0) { + return NULL; + } + PyType_Modified(typeobj); + Py_RETURN_NONE; +} + + /*[clinic input] sys._is_gil_enabled -> bool @@ -2837,6 +2869,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 From b9fb155331c85efcaef5af24a98e4a571a7f71e3 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 23 Jul 2025 08:43:06 -0700 Subject: [PATCH 3/7] obsolete comment --- Lib/dataclasses.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 7db595250e96b9..53b3b54cfb3fc7 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -1339,7 +1339,6 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields): break # gh-135228: Make sure the original class can be garbage collected. - # Bypass mapping proxy to allow __dict__ to be removed sys._clear_type_descriptors(cls) return newcls From 52c7d2bb6e192b25909850697a3174df689bb6d6 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 23 Jul 2025 14:20:56 -0700 Subject: [PATCH 4/7] restrict to heap types --- Python/sysmodule.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Python/sysmodule.c b/Python/sysmodule.c index b7d4a757fafb2e..b06166f761c030 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -2661,6 +2661,10 @@ sys__clear_type_descriptors(PyObject *module, PyObject *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); if (PyDict_PopString(dict, "__dict__", NULL) < 0) { return NULL; From 250f3e9935d8188c27dcd91e6cb61293166475c9 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 24 Jul 2025 17:13:36 -0700 Subject: [PATCH 5/7] more paranoid --- Python/sysmodule.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Python/sysmodule.c b/Python/sysmodule.c index b06166f761c030..bca07f7c4e4cee 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -2666,13 +2666,22 @@ sys__clear_type_descriptors(PyObject *module, PyObject *type) return NULL; } PyObject *dict = _PyType_GetDict(typeobj); - if (PyDict_PopString(dict, "__dict__", NULL) < 0) { + PyObject *dunder_dict = NULL; + if (PyDict_PopString(dict, "__dict__", &dunder_dict) < 0) { return NULL; } - if (PyDict_PopString(dict, "__weakref__", NULL) < 0) { + 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; } From a9483b405746c6e5b60f7177fc34152208c62cba Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 29 Jul 2025 07:29:01 -0700 Subject: [PATCH 6/7] Apply suggestions from code review Co-authored-by: Petr Viktorin Co-authored-by: Serhiy Storchaka --- Python/sysmodule.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Python/sysmodule.c b/Python/sysmodule.c index bca07f7c4e4cee..84acae66da6c12 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -2661,17 +2661,17 @@ sys__clear_type_descriptors(PyObject *module, PyObject *type) return NULL; } PyTypeObject *typeobj = (PyTypeObject *)(type); - if (!_PyType_HasFeature(typeobj, Py_TPFLAGS_HEAPTYPE)) { - PyErr_SetString(PyExc_TypeError, "argument must be a heap 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_PopString(dict, "__dict__", &dunder_dict) < 0) { + if (PyDict_Pop(dict, &_Py_ID(__dict__), &dunder_dict) < 0) { return NULL; } PyObject *dunder_weakref = NULL; - if (PyDict_PopString(dict, "__weakref__", &dunder_weakref) < 0) { + if (PyDict_Pop(dict, &_Py_ID(__weakref__), &dunder_weakref) < 0) { PyType_Modified(typeobj); Py_XDECREF(dunder_dict); return NULL; From 8c2c1e1849753b6c1fa1588f7837eea55792bb33 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 29 Jul 2025 17:28:33 -0700 Subject: [PATCH 7/7] Update Python/sysmodule.c --- Python/sysmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 84acae66da6c12..a25da631feabaf 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -2661,7 +2661,7 @@ sys__clear_type_descriptors(PyObject *module, PyObject *type) return NULL; } PyTypeObject *typeobj = (PyTypeObject *)(type); - if (!_PyType_HasFeature(typeobj, Py_TPFLAGS_IMMUTABLETYPE)) { + if (_PyType_HasFeature(typeobj, Py_TPFLAGS_IMMUTABLETYPE)) { PyErr_SetString(PyExc_TypeError, "argument is immutable"); return NULL; } 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