Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
a3292c2
Do not track frozenset objects with immutables
eendebakpt Oct 16, 2025
cd294a6
cleanup
eendebakpt Oct 16, 2025
7e28cf2
cleanup
eendebakpt Oct 16, 2025
30057a5
Merge branch 'main' into frozenset_immutable_tracking
eendebakpt Oct 16, 2025
c4deb03
fix test
eendebakpt Oct 16, 2025
607237a
📜🤖 Added by blurb_it.
blurb-it[bot] Oct 16, 2025
2735a71
Update Objects/setobject.c
eendebakpt Oct 17, 2025
c05db54
make sure PySet_Add tracks frozensets if needed
eendebakpt Oct 24, 2025
7f6bc4b
Merge branch 'frozenset_immutable_tracking' of github.com:eendebakpt/…
eendebakpt Oct 24, 2025
0b97604
review comment
eendebakpt Oct 24, 2025
948daed
Merge branch 'main' into frozenset_immutable_tracking
eendebakpt Oct 24, 2025
08e22c3
use _testcapi for testing
eendebakpt Oct 26, 2025
62afc76
whitespace
eendebakpt Oct 26, 2025
eab653e
Merge branch 'main' into frozenset_immutable_tracking
eendebakpt Oct 26, 2025
37fc61d
Apply suggestions from code review
eendebakpt Oct 26, 2025
4f8bda7
review comment
eendebakpt Oct 26, 2025
e9d42b4
Merge branch 'frozenset_immutable_tracking' of github.com:eendebakpt/…
eendebakpt Oct 26, 2025
4b39149
Apply suggestions from code review
eendebakpt Oct 28, 2025
2859802
review comments
eendebakpt Oct 28, 2025
08f43c5
review comments
eendebakpt Oct 28, 2025
d12102c
Merge branch 'main' into frozenset_immutable_tracking
eendebakpt Jan 18, 2026
5f8b1f2
Update Objects/setobject.c
eendebakpt Jan 18, 2026
ae5cc7f
review comments
eendebakpt Jan 23, 2026
786019d
Update Lib/test/test_set.py
eendebakpt Jan 23, 2026
f37f46e
Update Modules/_testcapimodule.c
eendebakpt Jan 23, 2026
afd8a5b
Merge branch 'main' into frozenset_immutable_tracking
eendebakpt Jan 23, 2026
377e6d8
Apply suggestions from code review
eendebakpt Jan 23, 2026
c62fa9a
revert to fastcall
eendebakpt Jan 24, 2026
69b728f
Merge branch 'frozenset_immutable_tracking' of github.com:eendebakpt/…
eendebakpt Jan 24, 2026
71d9eba
fix header
eendebakpt Jan 25, 2026
5c19de2
Fully write test in C
eendebakpt Jan 25, 2026
e7dd248
adjust tests
eendebakpt Jan 26, 2026
075b582
cleanup tests
eendebakpt Jan 26, 2026
3dd4c95
cleanup tests
eendebakpt Jan 26, 2026
8d864ef
rework
eendebakpt Jan 26, 2026
e262963
refactor code
eendebakpt Jan 26, 2026
6027391
cleanup
eendebakpt Jan 26, 2026
6269f68
Merge branch 'main' into frozenset_immutable_tracking
eendebakpt Jan 26, 2026
37af64f
Update Modules/_testlimitedcapi/set.c
eendebakpt Jan 27, 2026
ea3ba39
Merge branch 'main' into frozenset_immutable_tracking
eendebakpt Jan 27, 2026
ff61155
Update Modules/_testlimitedcapi/set.c
eendebakpt Jan 27, 2026
6bb0d58
Apply suggestions from code review
eendebakpt Jan 27, 2026
7a4859b
Update Objects/setobject.c
vstinner Jan 27, 2026
a2bcfdb
review comment
eendebakpt Jan 27, 2026
88c3f4a
whitespace
eendebakpt Jan 27, 2026
be20046
Merge branch 'main' into frozenset_immutable_tracking
eendebakpt Jan 27, 2026
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
26 changes: 26 additions & 0 deletions Lib/test/test_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import warnings
import weakref
from random import randrange, shuffle
import _testcapi
from test import support
from test.support import warnings_helper

Expand Down Expand Up @@ -2212,6 +2213,31 @@ def test_cuboctahedron(self):
for cubevert in edge:
self.assertIn(cubevert, g)

class TestPySet_Add(unittest.TestCase):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move these tests to Lib/test/test_capi/test_set.py.

def test_set(self):
# Test the PySet_Add c-api for set objects
s = set()
self.assertEqual(_testcapi.pyset_add(s, 1), {1})
self.assertRaises(TypeError, _testcapi.pyset_add, s, [])

def test_frozenset(self):
# Test the PySet_Add c-api for frozenset objects
self.assertEqual(_testcapi.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, _testcapi.pyset_add, frozen_set, 1)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why the second test fails, whereas the first succeed.

Copy link
Copy Markdown
Contributor Author

@eendebakpt eendebakpt Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second test fails because the argument frozen_set is not uniquely referenced. The error is raised here:

(!PyFrozenSet_Check(anyset) || !_PyObject_IsUniquelyReferenced(anyset))) {

I will add a comment to the test


def test_frozenset_gc_tracking(self):
# see gh-140234
Comment thread
efimov-mikhail marked this conversation as resolved.
Outdated
class TrackedHashableClass():
pass

a = TrackedHashableClass()
result_set = _testcapi.pyset_add(frozenset(), 1)
self.assertFalse(gc.is_tracked(result_set))
result_set = _testcapi.pyset_add(frozenset(), a)
self.assertTrue(gc.is_tracked(result_set))


#==============================================================================
Comment thread
eendebakpt marked this conversation as resolved.

Expand Down
5 changes: 4 additions & 1 deletion Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -1876,7 +1876,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')
Comment thread
sergey-miryanov marked this conversation as resolved.
from collections import OrderedDict
class OD(OrderedDict):
__slots__ = 'a', 'b', 'c'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Frozenset objects with immutable elements are no longer tracked by the garbage collector.
23 changes: 22 additions & 1 deletion Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2435,6 +2435,26 @@ test_critical_sections(PyObject *module, PyObject *Py_UNUSED(args))
}



static PyObject *
// Interface to PySet_Add, returning the set
pyset_add(PyObject* self, PyObject* const* args, Py_ssize_t nargsf)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this function to Modules/_testlimitedcapi/set.c.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the function and converted the calling convention to METH_VARARGS. This adds another reference to the set causing the test to fail.

I will convert back to METH_FASTCALL, or define the set at C level.

{
Py_ssize_t nargs = _PyVectorcall_NARGS(nargsf);
if (nargs != 2) {
PyErr_SetString(PyExc_ValueError, "pyset_add requires exactly two arguments");
return NULL;
Comment thread
eendebakpt marked this conversation as resolved.
Outdated
}
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);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should return None on success.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From #140234 (comment) I gathered we prefer to write the tests in Python (as much as possible). I can return None here, but then I have convert the code from test_set to C (possible, but more code and a bit harder to read)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vstinner A branch with the tests written in C is here:

main...eendebakpt:cpython:frozenset_immutable_tracking_c

Let me know if you prefer that approach, then I will merge the branch into this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can return None here, but then I have convert the code from test_set to C (possible, but more code and a bit harder to read)

Why do you have to write tests in C if this function returns None? Are you talking about tests on frozenset? I see this code in PySet_Add():

    if (PyFrozenSet_Check(anyset) && _PyObject_IsUniquelyReferenced(anyset)) {
        // We can only change frozensets if they are uniquely referenced. The
        // 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.
        return set_add_key((PySetObject *)anyset, key);
    }

If you want to write tests for this code path, writing them in C would be more reliable to have a fine control on the reference count, right. But other tests on the set types can be written in Python, no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason for returning the set in pyset_add was indeed to be able test the code path you mention for the frozenset. Writing

x = frozenset()
_testinternalcapi.pyset_add(x, 1)

fails (because the frozenset is not uniquely referenced). For that reason I used:

x =_testinternalcapi.pyset_add(frozenset(), 1)
assert 1 in x

Doing that in C indeed seems better.

The other tests (for this PR the TestPySet_Add.test_set and TestPySet_Add.test_frozenset) can still be written in Python, although there are a couple already there for set and set subclasses, so there is not much left to test from the Python side.

}

// Used by `finalize_thread_hang`.
Comment thread
eendebakpt marked this conversation as resolved.
#if defined(_POSIX_THREADS) && !defined(__wasi__)
static void finalize_thread_hang_cleanup_callback(void *Py_UNUSED(arg)) {
Expand Down Expand Up @@ -2658,7 +2678,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},
Comment thread
eendebakpt marked this conversation as resolved.
Outdated
{"raise_SIGINT_then_send_None", raise_SIGINT_then_send_None, METH_VARARGS},
{"stack_pointer", stack_pointer, METH_NOARGS},
#ifdef W_STOPCODE
Expand All @@ -2679,6 +2699,7 @@ static PyMethodDef TestMethods[] = {
{"gen_get_code", gen_get_code, METH_O, NULL},
{"get_feature_macros", get_feature_macros, METH_NOARGS, NULL},
{"test_code_api", test_code_api, METH_NOARGS, NULL},
{"pyset_add", _PyCFunction_CAST(pyset_add), METH_FASTCALL, NULL},
{"settrace_to_error", settrace_to_error, METH_O, NULL},
{"settrace_to_record", settrace_to_record, METH_O, NULL},
{"test_macros", test_macros, METH_NOARGS, NULL},
Expand Down
37 changes: 35 additions & 2 deletions Objects/setobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1368,6 +1368,26 @@ make_new_set_basetype(PyTypeObject *type, PyObject *iterable)
return make_new_set(type, iterable);
}

void
Comment thread
vstinner marked this conversation as resolved.
Outdated
Comment thread
vstinner marked this conversation as resolved.
Outdated
// gh-140232: check whether a frozenset can be untracked from the GC
Comment thread
eendebakpt marked this conversation as resolved.
Outdated
_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)) {
Comment thread
efimov-mikhail marked this conversation as resolved.
return;
}
}
_PyObject_GC_UNTRACK(op);
}

static PyObject *
make_new_frozenset(PyTypeObject *type, PyObject *iterable)
{
Expand All @@ -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;
Comment thread
eendebakpt marked this conversation as resolved.
}

static PyObject *
Expand Down Expand Up @@ -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) {
Comment thread
eendebakpt marked this conversation as resolved.
Outdated
_PyFrozenSet_MaybeUntrack(result);
}
return result;
}

Py_ssize_t
Expand Down Expand Up @@ -3010,6 +3038,11 @@ 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.

Comment thread
eendebakpt marked this conversation as resolved.
Outdated
// since another key is added to the set, we must track the frozenset if needed
Comment thread
eendebakpt marked this conversation as resolved.
Outdated
if (PyFrozenSet_CheckExact(anyset) && PyObject_GC_IsTracked(key) && !PyObject_GC_IsTracked(anyset) ) {
Comment thread
eendebakpt marked this conversation as resolved.
Outdated
_PyObject_GC_TRACK(anyset);
}
return set_add_key((PySetObject *)anyset, key);
}

Expand Down
Loading