From dd642299435a04562aa731284db3e45d5f81aeb6 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 21 Oct 2026 15:34:15 -0500 Subject: [PATCH] gh-158660: Add `PyFrozenSetWriter` C API Use the new API in: * Modules/_abc.c * Modules/_asynciomodule.c * Modules/_hashopenssl.c * Modules/_ssl.c --- Doc/c-api/set.rst | 69 +++++++ Doc/whatsnew/3.16.rst | 11 ++ Include/cpython/setobject.h | 15 ++ Lib/test/test_capi/test_set.py | 73 ++++++++ ...-10-21-18-20-57.gh-issue-160147.7uo4pF.rst | 10 + Modules/_abc.c | 17 +- Modules/_asynciomodule.c | 8 +- Modules/_hashopenssl.c | 11 +- Modules/_ssl.c | 13 +- Modules/_testcapi/set.c | 174 +++++++++++++++++- Objects/setobject.c | 84 +++++++++ 11 files changed, 464 insertions(+), 21 deletions(-) create mode 100644 Misc/NEWS.d/next/C_API/2026-10-21-18-20-57.gh-issue-160147.7uo4pF.rst diff --git a/Doc/c-api/set.rst b/Doc/c-api/set.rst index db537aff2e6ce5..dfee22596d46ac 100644 --- a/Doc/c-api/set.rst +++ b/Doc/c-api/set.rst @@ -102,6 +102,8 @@ the constructor functions work with any iterable Python object. set on success or ``NULL`` on failure. Raise :exc:`TypeError` if *iterable* is not actually iterable. + See also the :ref:`PyFrozenSetWriter API `. + .. note:: The operation is atomic on :term:`free threading ` @@ -213,3 +215,70 @@ Deprecated API If looking for the size of a set, use :c:func:`PySet_Size` instead. .. soft-deprecated:: 3.14 + + +.. _pyfrozensetwriter: + +PyFrozenSetWriter +----------------- + +The :c:type:`PyFrozenSetWriter` API can be used to create a Python :class:`frozenset` +object. + +See also the :c:func:`PyFrozenSet_New` function. + +.. versionadded:: next + + +.. c:type:: PyFrozenSetWriter + + A frozenset writer object. + + The API is **not thread safe**. To share a writer with multiple threads, a + critical section or a lock is needed. + + The instance must be destroyed by :c:func:`PyFrozenSetWriter_Finish` on + success, or :c:func:`PyFrozenSetWriter_Discard` on error. + + +.. c:function:: PyFrozenSetWriter* PyFrozenSetWriter_Create(void) + + Create a :c:type:`PyFrozenSetWriter`. + + On error, set an exception and return ``NULL``. + + *size* must be positive or zero. + +.. c:function:: PyObject* PyFrozenSetWriter_Finish(PyFrozenSetWriter *writer) + + Finish a :c:type:`PyFrozenSetWriter` created by + :c:func:`PyFrozenSetWriter_Create`. + + On success, return a Python :class:`frozenset` object. + On error, set an exception and return ``NULL``. + + The writer instance is invalid after the call in any case. + No API can be called on the writer after :c:func:`PyFrozenSetWriter_Finish`. + +.. c:function:: void PyFrozenSetWriter_Discard(PyFrozenSetWriter *writer) + + Discard a :c:type:`PyFrozenSetWriter` created by :c:func:`PyFrozenSetWriter_Create`. + + Do nothing if *writer* is ``NULL``. + + The writer instance is invalid after the call. + No API can be called on the writer after :c:func:`PyFrozenSetWriter_Discard`. + +.. c:function:: int PyFrozenSetWriter_Add(PyFrozenSetWriter *writer, PyObject *item) + + Add *item* to a frozenset writer. + + On success, return ``0``. + On error, set an exception and return ``-1``. + +.. c:function:: int PyFrozenSetWriter_Update(PyFrozenSetWriter *writer, PyObject *iterable) + + Update the frozenset writer, adding elements from *iterable*. + + On success, return ``0``. + On error, set an exception and return ``-1``. diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index bd92a0a48f1f70..7e74bd849dc8d5 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -1295,6 +1295,17 @@ New features * Add :c:func:`Py_HashBuffer` to the limited C API. (Contributed by Victor Stinner in :gh:`148233`.) +* Add a new :ref:`PyFrozenSetWriter ` C API to create + a :class:`frozenset` object. Add functions: + + * :c:func:`PyFrozenSetWriter_Create` + * :c:func:`PyFrozenSetWriter_Finish` + * :c:func:`PyFrozenSetWriter_Discard` + * :c:func:`PyFrozenSetWriter_Add` + * :c:func:`PyFrozenSetWriter_Update` + + (Contributed by Victor Stinner in :gh:`160147`.) + * Add :c:macro:`Py_SETREF` and :c:macro:`Py_XSETREF` macros to the limited C API. (Contributed by Nathan Goldbaum and Victor Stinner in :gh:`158842`.) diff --git a/Include/cpython/setobject.h b/Include/cpython/setobject.h index 89565cb29212fc..8a87cb11aa059f 100644 --- a/Include/cpython/setobject.h +++ b/Include/cpython/setobject.h @@ -69,3 +69,18 @@ static inline Py_ssize_t PySet_GET_SIZE(PyObject *so) { #endif } #define PySet_GET_SIZE(so) PySet_GET_SIZE(_PyObject_CAST(so)) + + +// --- PyFrozenSetWriter API ------------------------------------------------ + +typedef struct PyFrozenSetWriter PyFrozenSetWriter; + +PyAPI_FUNC(PyFrozenSetWriter*) PyFrozenSetWriter_Create(void); +PyAPI_FUNC(PyObject*) PyFrozenSetWriter_Finish(PyFrozenSetWriter *writer); +PyAPI_FUNC(void) PyFrozenSetWriter_Discard(PyFrozenSetWriter *writer); +PyAPI_FUNC(int) PyFrozenSetWriter_Add( + PyFrozenSetWriter *writer, + PyObject *item); +PyAPI_FUNC(int) PyFrozenSetWriter_Update( + PyFrozenSetWriter *writer, + PyObject *iterable); diff --git a/Lib/test/test_capi/test_set.py b/Lib/test/test_capi/test_set.py index a41c2fcaf12270..ff22757d1b2ac7 100644 --- a/Lib/test/test_capi/test_set.py +++ b/Lib/test/test_capi/test_set.py @@ -1,6 +1,9 @@ +import gc +import threading import unittest from test.support import import_helper +from test.support import threading_helper # Skip this test if the _testcapi, _testlimitedcapi or _testinternalcapi # modules aren't available. @@ -8,6 +11,7 @@ _testlimitedcapi = import_helper.import_module('_testlimitedcapi') _testinternalcapi = import_helper.import_module('_testinternalcapi') + class set_subclass(set): pass @@ -273,5 +277,74 @@ def test_set_next_entry(self): # CRASHES: set_next(NULL, 0) +class FrozenSetWriterTests(unittest.TestCase): + def create_writer(self): + # Test PyFrozenSetWriter_Create() + return _testcapi.PyFrozenSetWriter() + + def frozenset_equal(self, result, expected): + if not isinstance(expected, frozenset): + expected = frozenset(expected) + self.assertEqual(result, expected) + self.assertEqual(type(result), frozenset) + + def test_append(self): + # Test PyFrozenSetWriter_Append() + items = (1, 2, 3) + writer = self.create_writer() + for item in items: + writer.add(item) + self.frozenset_equal(writer.finish(), items) + + def test_update(self): + # Test PyFrozenSetWriter_Update() + writer = self.create_writer() + writer.update((2, 5)) + writer.update((5, 7)) + self.frozenset_equal(writer.finish(), {2, 5, 7}) + + def test_finish(self): + # Test PyFrozenSetWriter_Finish(): track the frozenset in the GC if at + # least one item is tracked by the GC + writer = self.create_writer() + writer.update([2, 5, 7]) + self.assertFalse(gc.is_tracked(writer.finish())) + + class MyObject: + pass + + obj = MyObject() + writer = self.create_writer() + writer.add(obj) + self.assertTrue(gc.is_tracked(writer.finish())) + + # test again, now using PyFrozenSetWriter_Update() + writer = self.create_writer() + writer.update([obj]) + self.assertTrue(gc.is_tracked(writer.finish())) + + def test_discard(self): + # Test PyFrozenSetWriter_Discard(): must not leak memory + writer = self.create_writer() + writer.update([2, 5, 7]) + writer.discard() + + @threading_helper.requires_working_threading() + def test_thread(self): + # PyFrozenSetWriter can be used by multiple threads: it's up to the caller + # to implement a lock to prevent concurrent accesses. + writer = self.create_writer() + + def thread_func(writer): + writer.add('Hello') + writer.update(['World!']) + + thread = threading.Thread(target=thread_func, args=(writer,)) + thread.start() + threading_helper.join_thread(thread) + + self.frozenset_equal(writer.finish(), {'Hello', 'World!'}) + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/C_API/2026-10-21-18-20-57.gh-issue-160147.7uo4pF.rst b/Misc/NEWS.d/next/C_API/2026-10-21-18-20-57.gh-issue-160147.7uo4pF.rst new file mode 100644 index 00000000000000..bcdcda3bb0b070 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2026-10-21-18-20-57.gh-issue-160147.7uo4pF.rst @@ -0,0 +1,10 @@ +Add a new :ref:`PyFrozenSetWriter ` C API to create a +:class:`frozenset` object. Add functions: + +* :c:func:`PyFrozenSetWriter_Create` +* :c:func:`PyFrozenSetWriter_Finish` +* :c:func:`PyFrozenSetWriter_Discard` +* :c:func:`PyFrozenSetWriter_Add` +* :c:func:`PyFrozenSetWriter_Update` + +Patch by Victor Stinner. diff --git a/Modules/_abc.c b/Modules/_abc.c index bca9066d340ee4..1e6d11677c0e87 100644 --- a/Modules/_abc.c +++ b/Modules/_abc.c @@ -362,7 +362,7 @@ static int compute_abstract_methods(PyObject *self) { int ret = -1; - PyObject *abstracts = PyFrozenSet_New(NULL); + PyFrozenSetWriter *abstracts = PyFrozenSetWriter_Create(); if (abstracts == NULL) { return -1; } @@ -404,7 +404,7 @@ compute_abstract_methods(PyObject *self) Py_INCREF(key); int is_abstract = _PyObject_IsAbstract(value); if (is_abstract < 0 || - (is_abstract && PySet_Add(abstracts, key) < 0)) { + (is_abstract && PyFrozenSetWriter_Add(abstracts, key) < 0)) { Py_DECREF(it); Py_DECREF(key); goto error; @@ -454,7 +454,7 @@ compute_abstract_methods(PyObject *self) int is_abstract = _PyObject_IsAbstract(value); Py_DECREF(value); if (is_abstract < 0 || - (is_abstract && PySet_Add(abstracts, key) < 0)) + (is_abstract && PyFrozenSetWriter_Add(abstracts, key) < 0)) { Py_DECREF(key); Py_DECREF(iter); @@ -468,13 +468,20 @@ compute_abstract_methods(PyObject *self) } } - if (PyObject_SetAttr(self, &_Py_ID(__abstractmethods__), abstracts) < 0) { + PyObject *abstracts_set = PyFrozenSetWriter_Finish(abstracts); + abstracts = NULL; + if (abstracts_set == NULL) { goto error; } + if (PyObject_SetAttr(self, &_Py_ID(__abstractmethods__), abstracts_set) < 0) { + Py_DECREF(abstracts_set); + goto error; + } + Py_DECREF(abstracts_set); ret = 0; error: - Py_DECREF(abstracts); + PyFrozenSetWriter_Discard(abstracts); Py_XDECREF(ns); Py_XDECREF(items); Py_XDECREF(bases); diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 5569f7a3670378..05eb9ff4534afe 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -1339,15 +1339,15 @@ _asyncio_Future__asyncio_awaited_by_get_impl(FutureObj *self) return PyFrozenSet_New(self->fut_awaited_by); } - PyObject *set = PyFrozenSet_New(NULL); + PyFrozenSetWriter *set = PyFrozenSetWriter_Create(); if (set == NULL) { return NULL; } - if (PySet_Add(set, self->fut_awaited_by)) { - Py_DECREF(set); + if (PyFrozenSetWriter_Add(set, self->fut_awaited_by)) { + PyFrozenSetWriter_Discard(set); return NULL; } - return set; + return PyFrozenSetWriter_Finish(set); } diff --git a/Modules/_hashopenssl.c b/Modules/_hashopenssl.c index 9d7602b39d7b93..786b1c18c61356 100644 --- a/Modules/_hashopenssl.c +++ b/Modules/_hashopenssl.c @@ -2477,7 +2477,7 @@ PyType_Spec HMACtype_spec = { /* State for our callback function so that it can accumulate a result. */ typedef struct _internal_name_mapper_state { - PyObject *set; + PyFrozenSetWriter *set; int error; } _InternalNameMapperState; @@ -2506,7 +2506,7 @@ _openssl_hash_name_mapper(const EVP_MD *md, const char *from, if (py_name == NULL) { state->error = 1; } else { - if (PySet_Add(state->set, py_name) != 0) { + if (PyFrozenSetWriter_Add(state->set, py_name) != 0) { state->error = 1; } Py_DECREF(py_name); @@ -2519,7 +2519,7 @@ static int hashlib_md_meth_names(PyObject *module) { _InternalNameMapperState state = { - .set = PyFrozenSet_New(NULL), + .set = PyFrozenSetWriter_Create(), .error = 0 }; if (state.set == NULL) { @@ -2534,11 +2534,12 @@ hashlib_md_meth_names(PyObject *module) #endif if (state.error) { - Py_DECREF(state.set); + PyFrozenSetWriter_Discard(state.set); return -1; } - return PyModule_Add(module, "openssl_md_meth_names", state.set); + PyObject *names = PyFrozenSetWriter_Finish(state.set); + return PyModule_Add(module, "openssl_md_meth_names", names); } /*[clinic input] diff --git a/Modules/_ssl.c b/Modules/_ssl.c index ee0df146421d7f..34b5905aba3c8b 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -6506,7 +6506,6 @@ parseKeyUsage(PCCERT_CONTEXT pCertCtx, DWORD flags) { CERT_ENHKEY_USAGE *usage; DWORD size, error, i; - PyObject *retval; if (!CertGetEnhancedKeyUsage(pCertCtx, flags, NULL, &size)) { error = GetLastError(); @@ -6530,7 +6529,7 @@ parseKeyUsage(PCCERT_CONTEXT pCertCtx, DWORD flags) } return PyErr_SetFromWindowsErr(error); } - retval = PyFrozenSet_New(NULL); + PyFrozenSetWriter *retval = PyFrozenSetWriter_Create(); if (retval == NULL) { goto error; } @@ -6540,20 +6539,22 @@ parseKeyUsage(PCCERT_CONTEXT pCertCtx, DWORD flags) int err; oid = PyUnicode_FromString(usage->rgpszUsageIdentifier[i]); if (oid == NULL) { - Py_CLEAR(retval); goto error; } - err = PySet_Add(retval, oid); + err = PyFrozenSetWriter_Add(retval, oid); Py_DECREF(oid); if (err == -1) { - Py_CLEAR(retval); goto error; } } } + PyMem_Free(usage); + return PyFrozenSetWriter_Finish(retval); + error: PyMem_Free(usage); - return retval; + PyFrozenSetWriter_Discard(retval); + return NULL; } static HCERTSTORE diff --git a/Modules/_testcapi/set.c b/Modules/_testcapi/set.c index 092715ab7d0aa4..0bd27953e8940a 100644 --- a/Modules/_testcapi/set.c +++ b/Modules/_testcapi/set.c @@ -31,14 +31,186 @@ test_set_type_size(PyObject *self, PyObject *Py_UNUSED(ignored)) } +typedef struct { + PyObject_HEAD + PyFrozenSetWriter *writer; +} WriterObject; + + +static inline int +writer_check(WriterObject *self) +{ + if (self->writer == NULL) { + PyErr_SetString(PyExc_ValueError, + "operation on finished/discarded writer"); + return -1; + } + return 0; +} + + +static PyObject * +writer_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + WriterObject *self = (WriterObject *)type->tp_alloc(type, 0); + if (!self) { + return NULL; + } + self->writer = NULL; + return (PyObject*)self; +} + + +// Test PyFrozenSetWriter_Create() +static int +writer_init(PyObject *self_raw, PyObject *args, PyObject *kwargs) +{ + if (PyTuple_GET_SIZE(args)) { + PyErr_Format(PyExc_TypeError, + "PyFrozenSetWriter() takes no positional arguments"); + return -1; + } + if (kwargs && PyDict_GET_SIZE(kwargs)) { + PyErr_Format(PyExc_TypeError, + "PyFrozenSetWriter() takes no keyword arguments"); + return -1; + } + + WriterObject *self = (WriterObject *)self_raw; + PyFrozenSetWriter_Discard(self->writer); + + self->writer = PyFrozenSetWriter_Create(); + if (self->writer == NULL) { + return -1; + } + + return 0; +} + + +// Test PyFrozenSetWriter_Discard() +static void +writer_dealloc(PyObject *self_raw) +{ + WriterObject *self = (WriterObject *)self_raw; + PyTypeObject *tp = Py_TYPE(self); + if (self->writer) { + PyFrozenSetWriter_Discard(self->writer); + } + tp->tp_free(self); + Py_DECREF(tp); +} + + +// Test PyFrozenSetWriter_Add() +static PyObject* +writer_add(PyObject *self_raw, PyObject *item) +{ + WriterObject *self = (WriterObject *)self_raw; + if (writer_check(self) < 0) { + return NULL; + } + + if (PyFrozenSetWriter_Add(self->writer, item) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + + +// Test PyFrozenSetWriter_Update() +static PyObject* +writer_update(PyObject *self_raw, PyObject *iterable) +{ + WriterObject *self = (WriterObject *)self_raw; + if (writer_check(self) < 0) { + return NULL; + } + + if (PyFrozenSetWriter_Update(self->writer, iterable) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + + +// Test PyFrozenSetWriter_Finish() +static PyObject* +writer_finish(PyObject *self_raw, PyObject *Py_UNUSED(args)) +{ + WriterObject *self = (WriterObject *)self_raw; + if (writer_check(self) < 0) { + return NULL; + } + + PyObject *frozenset = PyFrozenSetWriter_Finish(self->writer); + self->writer = NULL; + return frozenset; +} + + +// Test PyFrozenSetWriter_Discard() +static PyObject* +writer_discard(PyObject *self_raw, PyObject *Py_UNUSED(args)) +{ + WriterObject *self = (WriterObject *)self_raw; + if (writer_check(self) < 0) { + return NULL; + } + + PyFrozenSetWriter_Discard(self->writer); + self->writer = NULL; + Py_RETURN_NONE; +} + + +static PyMethodDef writer_methods[] = { + {"add", _PyCFunction_CAST(writer_add), METH_O}, + {"update", _PyCFunction_CAST(writer_update), METH_O}, + {"finish", _PyCFunction_CAST(writer_finish), METH_NOARGS}, + {"discard", _PyCFunction_CAST(writer_discard), METH_NOARGS}, + {NULL, NULL} /* sentinel */ +}; + +static PyType_Slot Writer_Type_slots[] = { + {Py_tp_new, writer_new}, + {Py_tp_init, writer_init}, + {Py_tp_dealloc, writer_dealloc}, + {Py_tp_methods, writer_methods}, + {0, 0}, /* sentinel */ +}; + +static PyType_Spec Writer_spec = { + .name = "_testcapi.PyFrozenSetWriter", + .basicsize = sizeof(WriterObject), + .flags = Py_TPFLAGS_DEFAULT, + .slots = Writer_Type_slots, +}; + + static PyMethodDef test_methods[] = { {"set_get_size", set_get_size, METH_O}, {"test_set_type_size", test_set_type_size, METH_NOARGS}, {NULL}, }; + int _PyTestCapi_Init_Set(PyObject *m) { - return PyModule_AddFunctions(m, test_methods); + if (PyModule_AddFunctions(m, test_methods) < 0) { + return -1; + } + + PyTypeObject *writer_type = (PyTypeObject *)PyType_FromSpec(&Writer_spec); + if (writer_type == NULL) { + return -1; + } + if (PyModule_AddType(m, writer_type) < 0) { + Py_DECREF(writer_type); + return -1; + } + Py_DECREF(writer_type); + + return 0; } diff --git a/Objects/setobject.c b/Objects/setobject.c index a762edf5933e18..2be4e5b5076e2c 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -3223,3 +3223,87 @@ static PyTypeObject _PySetDummy_Type = { }; static PyObject _dummy_struct = _PyObject_HEAD_INIT(&_PySetDummy_Type); + + +// --- PyFrozenSetWriter API ------------------------------------------------ + +#ifndef NDEBUG +static int +frozsetwriter_check_consistency(PyFrozenSetWriter *writer) +{ + assert(writer != NULL); + PyObject *frozenset = _PyObject_CAST(writer); + // Don't test _PyObject_IsUniquelyReferenced() since a frozenset writer + // can be shared between multiple threads with external locking. + assert(Py_REFCNT(frozenset) == 1); + assert(!_PyObject_GC_IS_TRACKED(frozenset)); + return 1; +} +#endif + + +PyFrozenSetWriter* +PyFrozenSetWriter_Create(void) +{ + PyObject *frozenset = make_new_set_untracked(&PyFrozenSet_Type, NULL); + if (frozenset == NULL) { + return NULL; + } + + PyFrozenSetWriter *writer = (PyFrozenSetWriter*)frozenset; + assert(frozsetwriter_check_consistency(writer)); + return writer; +} + + +void +PyFrozenSetWriter_Discard(PyFrozenSetWriter *writer) +{ + if (writer == NULL) { + return; + } + + assert(frozsetwriter_check_consistency(writer)); + PyObject *frozenset = _PyObject_CAST(writer); + Py_DECREF(frozenset); +} + + +PyObject* +PyFrozenSetWriter_Finish(PyFrozenSetWriter *writer) +{ + assert(frozsetwriter_check_consistency(writer)); + PySetObject *frozenset = _PySet_CAST(writer); + + // Now that the frozenset is fully initialized, it's safe to track it in + // the GC. Track the frozenset if at least one item is tracked by the GC. + Py_ssize_t pos = 0; + setentry *entry; + while (set_next(frozenset, &pos, &entry)) { + PyObject *item = entry->key; + if (_PyObject_IS_GC(item) && _PyObject_GC_IS_TRACKED(item)) { + _PyObject_GC_TRACK(frozenset); + break; + } + } + + return _PyObject_CAST(frozenset); +} + + +int +PyFrozenSetWriter_Add(PyFrozenSetWriter *writer, PyObject *item) +{ + assert(frozsetwriter_check_consistency(writer)); + PySetObject *frozenset = _PySet_CAST(writer); + return set_add_key(frozenset, item); +} + + +int +PyFrozenSetWriter_Update(PyFrozenSetWriter *writer, PyObject *iterable) +{ + assert(frozsetwriter_check_consistency(writer)); + PySetObject *frozenset = _PySet_CAST(writer); + return set_update_local(frozenset, iterable); +}