diff --git a/Lib/test/test_capi/test_set.py b/Lib/test/test_capi/test_set.py index 62d90a3f94326d..02211a73388a90 100644 --- a/Lib/test/test_capi/test_set.py +++ b/Lib/test/test_capi/test_set.py @@ -1,3 +1,4 @@ +import gc import unittest from test.support import import_helper @@ -220,6 +221,32 @@ def test_clear(self): # CRASHES: clear(NULL) +class TestPySet_Add(unittest.TestCase): + def test_set(self): + # Test the PySet_Add c-api for set objects + s = set() + self.assertEqual(_testlimitedcapi.pyset_add(s, 1), {1}) + self.assertRaises(TypeError, _testlimitedcapi.pyset_add, s, []) + + def test_frozenset(self): + # Test the PySet_Add c-api for frozenset objects + self.assertEqual(_testlimitedcapi.pyset_add(frozenset(), 1), frozenset([1])) + frozen_set = frozenset() + # if the argument to PySet_Add is a frozenset that is not uniquely references an error is generated + self.assertRaises(SystemError, _testlimitedcapi.pyset_add, frozen_set, 1) + + def test_frozenset_gc_tracking(self): + # see gh-140234 + class TrackedHashableClass(): + pass + + a = TrackedHashableClass() + result_set = _testlimitedcapi.pyset_add(frozenset(), 1) + self.assertFalse(gc.is_tracked(result_set)) + result_set = _testlimitedcapi.pyset_add(frozenset(), a) + self.assertTrue(gc.is_tracked(result_set)) + + class TestInternalCAPI(BaseSetTests, unittest.TestCase): def test_set_update(self): update = _testinternalcapi.set_update diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index a5708b298c84a5..b44e0c9779aa59 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -1880,7 +1880,10 @@ class S(set): check(S(), set(), '3P') class FS(frozenset): __slots__ = 'a', 'b', 'c' - check(FS(), frozenset(), '3P') + + class mytuple(tuple): + pass + check(FS([mytuple()]), frozenset([mytuple()]), '3P') from collections import OrderedDict class OD(OrderedDict): __slots__ = 'a', 'b', 'c' diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-16-22-36-05.gh-issue-140232.u3srgv.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-16-22-36-05.gh-issue-140232.u3srgv.rst new file mode 100644 index 00000000000000..e40daacbc45b7b --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-16-22-36-05.gh-issue-140232.u3srgv.rst @@ -0,0 +1 @@ +Frozenset objects with immutable elements are no longer tracked by the garbage collector. diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index c0ab35cda191c8..836fde3450efb8 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2658,7 +2658,7 @@ static PyMethodDef TestMethods[] = { {"return_null_without_error", return_null_without_error, METH_NOARGS}, {"return_result_with_error", return_result_with_error, METH_NOARGS}, {"getitem_with_error", getitem_with_error, METH_VARARGS}, - {"Py_CompileString", pycompilestring, METH_O}, + {"Py_CompileString", pycompilestring, METH_O}, {"raise_SIGINT_then_send_None", raise_SIGINT_then_send_None, METH_VARARGS}, {"stack_pointer", stack_pointer, METH_NOARGS}, #ifdef W_STOPCODE diff --git a/Modules/_testlimitedcapi/set.c b/Modules/_testlimitedcapi/set.c index 34ed6b1d60b5a4..08e1fba55f5114 100644 --- a/Modules/_testlimitedcapi/set.c +++ b/Modules/_testlimitedcapi/set.c @@ -1,6 +1,14 @@ +#include "pyconfig.h" // Py_GIL_DISABLED + +#if !defined(Py_GIL_DISABLED) && !defined(Py_LIMITED_API) + // Need limited C API for METH_FASTCALL + #define Py_LIMITED_API 0x030d0000 +#endif + #include "parts.h" #include "util.h" + static PyObject * set_check(PyObject *self, PyObject *obj) { @@ -200,6 +208,25 @@ test_set_contains_does_not_convert_unhashable_key(PyObject *self, PyObject *Py_U return NULL; } +// Interface to PySet_Add, returning the set +static PyObject * +pyset_add(PyObject *self, PyObject *const *args, Py_ssize_t nargs) +{ + if (nargs != 2) { + PyErr_SetString(PyExc_TypeError, + "pyset_add requires exactly 2 arguments"); + return NULL; + } + PyObject *set = args[0]; + PyObject *item = args[1]; + + int return_value = PySet_Add(set, item); + if (return_value < 0) { + return NULL; + } + return Py_NewRef(set); +} + static PyMethodDef test_methods[] = { {"set_check", set_check, METH_O}, {"set_checkexact", set_checkexact, METH_O}, @@ -221,6 +248,7 @@ static PyMethodDef test_methods[] = { {"test_frozenset_add_in_capi", test_frozenset_add_in_capi, METH_NOARGS}, {"test_set_contains_does_not_convert_unhashable_key", test_set_contains_does_not_convert_unhashable_key, METH_NOARGS}, + {"pyset_add", _PyCFunction_CAST(pyset_add), METH_FASTCALL}, {NULL}, }; diff --git a/Objects/setobject.c b/Objects/setobject.c index 378f221bcfd1e1..cd9fd32c3fcc85 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -1368,6 +1368,26 @@ make_new_set_basetype(PyTypeObject *type, PyObject *iterable) return make_new_set(type, iterable); } +void +// gh-140232: check whether a frozenset can be untracked from the GC +_PyFrozenSet_MaybeUntrack(PyObject *op) +{ + assert(op != NULL); + // subclasses of a frozenset can generate reference cycles, so do not untrack + if (!PyFrozenSet_CheckExact(op)) { + return; + } + // if no elements of a frozenset are tracked by the GC, we untrack the object + Py_ssize_t pos = 0; + setentry *entry; + while (set_next((PySetObject *)op, &pos, &entry)) { + if (_PyObject_GC_MAY_BE_TRACKED(entry->key)) { + return; + } + } + _PyObject_GC_UNTRACK(op); +} + static PyObject * make_new_frozenset(PyTypeObject *type, PyObject *iterable) { @@ -1379,7 +1399,11 @@ make_new_frozenset(PyTypeObject *type, PyObject *iterable) /* frozenset(f) is idempotent */ return Py_NewRef(iterable); } - return make_new_set(type, iterable); + PyObject *obj = make_new_set(type, iterable); + if (obj != NULL) { + _PyFrozenSet_MaybeUntrack(obj); + } + return obj; } static PyObject * @@ -2932,7 +2956,11 @@ PySet_New(PyObject *iterable) PyObject * PyFrozenSet_New(PyObject *iterable) { - return make_new_set(&PyFrozenSet_Type, iterable); + PyObject *result = make_new_set(&PyFrozenSet_Type, iterable); + if (result != 0) { + _PyFrozenSet_MaybeUntrack(result); + } + return result; } Py_ssize_t @@ -3010,6 +3038,10 @@ PySet_Add(PyObject *anyset, PyObject *key) // API limits the usage of `PySet_Add` to "fill in the values of brand // new frozensets before they are exposed to other code". In this case, // this can be done without a lock. + // since another key is added to the set, we must track the frozenset if needed + if (PyFrozenSet_CheckExact(anyset) && PyObject_GC_IsTracked(key) && !PyObject_GC_IsTracked(anyset) ) { + _PyObject_GC_TRACK(anyset); + } return set_add_key((PySetObject *)anyset, key); }