Skip to content

Commit dd64229

Browse files
committed
gh-158660: Add PyFrozenSetWriter C API
Use the new API in: * Modules/_abc.c * Modules/_asynciomodule.c * Modules/_hashopenssl.c * Modules/_ssl.c
1 parent aed4ad8 commit dd64229

11 files changed

Lines changed: 464 additions & 21 deletions

File tree

‎Doc/c-api/set.rst‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ the constructor functions work with any iterable Python object.
102102
set on success or ``NULL`` on failure. Raise :exc:`TypeError` if *iterable* is
103103
not actually iterable.
104104
105+
See also the :ref:`PyFrozenSetWriter API <pyfrozensetwriter>`.
106+
105107
.. note::
106108
107109
The operation is atomic on :term:`free threading <free-threaded build>`
@@ -213,3 +215,70 @@ Deprecated API
213215
If looking for the size of a set, use :c:func:`PySet_Size` instead.
214216
215217
.. soft-deprecated:: 3.14
218+
219+
220+
.. _pyfrozensetwriter:
221+
222+
PyFrozenSetWriter
223+
-----------------
224+
225+
The :c:type:`PyFrozenSetWriter` API can be used to create a Python :class:`frozenset`
226+
object.
227+
228+
See also the :c:func:`PyFrozenSet_New` function.
229+
230+
.. versionadded:: next
231+
232+
233+
.. c:type:: PyFrozenSetWriter
234+
235+
A frozenset writer object.
236+
237+
The API is **not thread safe**. To share a writer with multiple threads, a
238+
critical section or a lock is needed.
239+
240+
The instance must be destroyed by :c:func:`PyFrozenSetWriter_Finish` on
241+
success, or :c:func:`PyFrozenSetWriter_Discard` on error.
242+
243+
244+
.. c:function:: PyFrozenSetWriter* PyFrozenSetWriter_Create(void)
245+
246+
Create a :c:type:`PyFrozenSetWriter`.
247+
248+
On error, set an exception and return ``NULL``.
249+
250+
*size* must be positive or zero.
251+
252+
.. c:function:: PyObject* PyFrozenSetWriter_Finish(PyFrozenSetWriter *writer)
253+
254+
Finish a :c:type:`PyFrozenSetWriter` created by
255+
:c:func:`PyFrozenSetWriter_Create`.
256+
257+
On success, return a Python :class:`frozenset` object.
258+
On error, set an exception and return ``NULL``.
259+
260+
The writer instance is invalid after the call in any case.
261+
No API can be called on the writer after :c:func:`PyFrozenSetWriter_Finish`.
262+
263+
.. c:function:: void PyFrozenSetWriter_Discard(PyFrozenSetWriter *writer)
264+
265+
Discard a :c:type:`PyFrozenSetWriter` created by :c:func:`PyFrozenSetWriter_Create`.
266+
267+
Do nothing if *writer* is ``NULL``.
268+
269+
The writer instance is invalid after the call.
270+
No API can be called on the writer after :c:func:`PyFrozenSetWriter_Discard`.
271+
272+
.. c:function:: int PyFrozenSetWriter_Add(PyFrozenSetWriter *writer, PyObject *item)
273+
274+
Add *item* to a frozenset writer.
275+
276+
On success, return ``0``.
277+
On error, set an exception and return ``-1``.
278+
279+
.. c:function:: int PyFrozenSetWriter_Update(PyFrozenSetWriter *writer, PyObject *iterable)
280+
281+
Update the frozenset writer, adding elements from *iterable*.
282+
283+
On success, return ``0``.
284+
On error, set an exception and return ``-1``.

‎Doc/whatsnew/3.16.rst‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1295,6 +1295,17 @@ New features
12951295
* Add :c:func:`Py_HashBuffer` to the limited C API.
12961296
(Contributed by Victor Stinner in :gh:`148233`.)
12971297

1298+
* Add a new :ref:`PyFrozenSetWriter <pyfrozensetwriter>` C API to create
1299+
a :class:`frozenset` object. Add functions:
1300+
1301+
* :c:func:`PyFrozenSetWriter_Create`
1302+
* :c:func:`PyFrozenSetWriter_Finish`
1303+
* :c:func:`PyFrozenSetWriter_Discard`
1304+
* :c:func:`PyFrozenSetWriter_Add`
1305+
* :c:func:`PyFrozenSetWriter_Update`
1306+
1307+
(Contributed by Victor Stinner in :gh:`160147`.)
1308+
12981309
* Add :c:macro:`Py_SETREF` and :c:macro:`Py_XSETREF` macros to the limited C
12991310
API.
13001311
(Contributed by Nathan Goldbaum and Victor Stinner in :gh:`158842`.)

‎Include/cpython/setobject.h‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,18 @@ static inline Py_ssize_t PySet_GET_SIZE(PyObject *so) {
6969
#endif
7070
}
7171
#define PySet_GET_SIZE(so) PySet_GET_SIZE(_PyObject_CAST(so))
72+
73+
74+
// --- PyFrozenSetWriter API ------------------------------------------------
75+
76+
typedef struct PyFrozenSetWriter PyFrozenSetWriter;
77+
78+
PyAPI_FUNC(PyFrozenSetWriter*) PyFrozenSetWriter_Create(void);
79+
PyAPI_FUNC(PyObject*) PyFrozenSetWriter_Finish(PyFrozenSetWriter *writer);
80+
PyAPI_FUNC(void) PyFrozenSetWriter_Discard(PyFrozenSetWriter *writer);
81+
PyAPI_FUNC(int) PyFrozenSetWriter_Add(
82+
PyFrozenSetWriter *writer,
83+
PyObject *item);
84+
PyAPI_FUNC(int) PyFrozenSetWriter_Update(
85+
PyFrozenSetWriter *writer,
86+
PyObject *iterable);

‎Lib/test/test_capi/test_set.py‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1+
import gc
2+
import threading
13
import unittest
24

35
from test.support import import_helper
6+
from test.support import threading_helper
47

58
# Skip this test if the _testcapi, _testlimitedcapi or _testinternalcapi
69
# modules aren't available.
710
_testcapi = import_helper.import_module('_testcapi')
811
_testlimitedcapi = import_helper.import_module('_testlimitedcapi')
912
_testinternalcapi = import_helper.import_module('_testinternalcapi')
1013

14+
1115
class set_subclass(set):
1216
pass
1317

@@ -273,5 +277,74 @@ def test_set_next_entry(self):
273277
# CRASHES: set_next(NULL, 0)
274278

275279

280+
class FrozenSetWriterTests(unittest.TestCase):
281+
def create_writer(self):
282+
# Test PyFrozenSetWriter_Create()
283+
return _testcapi.PyFrozenSetWriter()
284+
285+
def frozenset_equal(self, result, expected):
286+
if not isinstance(expected, frozenset):
287+
expected = frozenset(expected)
288+
self.assertEqual(result, expected)
289+
self.assertEqual(type(result), frozenset)
290+
291+
def test_append(self):
292+
# Test PyFrozenSetWriter_Append()
293+
items = (1, 2, 3)
294+
writer = self.create_writer()
295+
for item in items:
296+
writer.add(item)
297+
self.frozenset_equal(writer.finish(), items)
298+
299+
def test_update(self):
300+
# Test PyFrozenSetWriter_Update()
301+
writer = self.create_writer()
302+
writer.update((2, 5))
303+
writer.update((5, 7))
304+
self.frozenset_equal(writer.finish(), {2, 5, 7})
305+
306+
def test_finish(self):
307+
# Test PyFrozenSetWriter_Finish(): track the frozenset in the GC if at
308+
# least one item is tracked by the GC
309+
writer = self.create_writer()
310+
writer.update([2, 5, 7])
311+
self.assertFalse(gc.is_tracked(writer.finish()))
312+
313+
class MyObject:
314+
pass
315+
316+
obj = MyObject()
317+
writer = self.create_writer()
318+
writer.add(obj)
319+
self.assertTrue(gc.is_tracked(writer.finish()))
320+
321+
# test again, now using PyFrozenSetWriter_Update()
322+
writer = self.create_writer()
323+
writer.update([obj])
324+
self.assertTrue(gc.is_tracked(writer.finish()))
325+
326+
def test_discard(self):
327+
# Test PyFrozenSetWriter_Discard(): must not leak memory
328+
writer = self.create_writer()
329+
writer.update([2, 5, 7])
330+
writer.discard()
331+
332+
@threading_helper.requires_working_threading()
333+
def test_thread(self):
334+
# PyFrozenSetWriter can be used by multiple threads: it's up to the caller
335+
# to implement a lock to prevent concurrent accesses.
336+
writer = self.create_writer()
337+
338+
def thread_func(writer):
339+
writer.add('Hello')
340+
writer.update(['World!'])
341+
342+
thread = threading.Thread(target=thread_func, args=(writer,))
343+
thread.start()
344+
threading_helper.join_thread(thread)
345+
346+
self.frozenset_equal(writer.finish(), {'Hello', 'World!'})
347+
348+
276349
if __name__ == "__main__":
277350
unittest.main()
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Add a new :ref:`PyFrozenSetWriter <pyfrozensetwriter>` C API to create a
2+
:class:`frozenset` object. Add functions:
3+
4+
* :c:func:`PyFrozenSetWriter_Create`
5+
* :c:func:`PyFrozenSetWriter_Finish`
6+
* :c:func:`PyFrozenSetWriter_Discard`
7+
* :c:func:`PyFrozenSetWriter_Add`
8+
* :c:func:`PyFrozenSetWriter_Update`
9+
10+
Patch by Victor Stinner.

‎Modules/_abc.c‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ static int
362362
compute_abstract_methods(PyObject *self)
363363
{
364364
int ret = -1;
365-
PyObject *abstracts = PyFrozenSet_New(NULL);
365+
PyFrozenSetWriter *abstracts = PyFrozenSetWriter_Create();
366366
if (abstracts == NULL) {
367367
return -1;
368368
}
@@ -404,7 +404,7 @@ compute_abstract_methods(PyObject *self)
404404
Py_INCREF(key);
405405
int is_abstract = _PyObject_IsAbstract(value);
406406
if (is_abstract < 0 ||
407-
(is_abstract && PySet_Add(abstracts, key) < 0)) {
407+
(is_abstract && PyFrozenSetWriter_Add(abstracts, key) < 0)) {
408408
Py_DECREF(it);
409409
Py_DECREF(key);
410410
goto error;
@@ -454,7 +454,7 @@ compute_abstract_methods(PyObject *self)
454454
int is_abstract = _PyObject_IsAbstract(value);
455455
Py_DECREF(value);
456456
if (is_abstract < 0 ||
457-
(is_abstract && PySet_Add(abstracts, key) < 0))
457+
(is_abstract && PyFrozenSetWriter_Add(abstracts, key) < 0))
458458
{
459459
Py_DECREF(key);
460460
Py_DECREF(iter);
@@ -468,13 +468,20 @@ compute_abstract_methods(PyObject *self)
468468
}
469469
}
470470

471-
if (PyObject_SetAttr(self, &_Py_ID(__abstractmethods__), abstracts) < 0) {
471+
PyObject *abstracts_set = PyFrozenSetWriter_Finish(abstracts);
472+
abstracts = NULL;
473+
if (abstracts_set == NULL) {
472474
goto error;
473475
}
476+
if (PyObject_SetAttr(self, &_Py_ID(__abstractmethods__), abstracts_set) < 0) {
477+
Py_DECREF(abstracts_set);
478+
goto error;
479+
}
480+
Py_DECREF(abstracts_set);
474481

475482
ret = 0;
476483
error:
477-
Py_DECREF(abstracts);
484+
PyFrozenSetWriter_Discard(abstracts);
478485
Py_XDECREF(ns);
479486
Py_XDECREF(items);
480487
Py_XDECREF(bases);

‎Modules/_asynciomodule.c‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,15 +1339,15 @@ _asyncio_Future__asyncio_awaited_by_get_impl(FutureObj *self)
13391339
return PyFrozenSet_New(self->fut_awaited_by);
13401340
}
13411341

1342-
PyObject *set = PyFrozenSet_New(NULL);
1342+
PyFrozenSetWriter *set = PyFrozenSetWriter_Create();
13431343
if (set == NULL) {
13441344
return NULL;
13451345
}
1346-
if (PySet_Add(set, self->fut_awaited_by)) {
1347-
Py_DECREF(set);
1346+
if (PyFrozenSetWriter_Add(set, self->fut_awaited_by)) {
1347+
PyFrozenSetWriter_Discard(set);
13481348
return NULL;
13491349
}
1350-
return set;
1350+
return PyFrozenSetWriter_Finish(set);
13511351
}
13521352

13531353

‎Modules/_hashopenssl.c‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2477,7 +2477,7 @@ PyType_Spec HMACtype_spec = {
24772477

24782478
/* State for our callback function so that it can accumulate a result. */
24792479
typedef struct _internal_name_mapper_state {
2480-
PyObject *set;
2480+
PyFrozenSetWriter *set;
24812481
int error;
24822482
} _InternalNameMapperState;
24832483

@@ -2506,7 +2506,7 @@ _openssl_hash_name_mapper(const EVP_MD *md, const char *from,
25062506
if (py_name == NULL) {
25072507
state->error = 1;
25082508
} else {
2509-
if (PySet_Add(state->set, py_name) != 0) {
2509+
if (PyFrozenSetWriter_Add(state->set, py_name) != 0) {
25102510
state->error = 1;
25112511
}
25122512
Py_DECREF(py_name);
@@ -2519,7 +2519,7 @@ static int
25192519
hashlib_md_meth_names(PyObject *module)
25202520
{
25212521
_InternalNameMapperState state = {
2522-
.set = PyFrozenSet_New(NULL),
2522+
.set = PyFrozenSetWriter_Create(),
25232523
.error = 0
25242524
};
25252525
if (state.set == NULL) {
@@ -2534,11 +2534,12 @@ hashlib_md_meth_names(PyObject *module)
25342534
#endif
25352535

25362536
if (state.error) {
2537-
Py_DECREF(state.set);
2537+
PyFrozenSetWriter_Discard(state.set);
25382538
return -1;
25392539
}
25402540

2541-
return PyModule_Add(module, "openssl_md_meth_names", state.set);
2541+
PyObject *names = PyFrozenSetWriter_Finish(state.set);
2542+
return PyModule_Add(module, "openssl_md_meth_names", names);
25422543
}
25432544

25442545
/*[clinic input]

‎Modules/_ssl.c‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6506,7 +6506,6 @@ parseKeyUsage(PCCERT_CONTEXT pCertCtx, DWORD flags)
65066506
{
65076507
CERT_ENHKEY_USAGE *usage;
65086508
DWORD size, error, i;
6509-
PyObject *retval;
65106509

65116510
if (!CertGetEnhancedKeyUsage(pCertCtx, flags, NULL, &size)) {
65126511
error = GetLastError();
@@ -6530,7 +6529,7 @@ parseKeyUsage(PCCERT_CONTEXT pCertCtx, DWORD flags)
65306529
}
65316530
return PyErr_SetFromWindowsErr(error);
65326531
}
6533-
retval = PyFrozenSet_New(NULL);
6532+
PyFrozenSetWriter *retval = PyFrozenSetWriter_Create();
65346533
if (retval == NULL) {
65356534
goto error;
65366535
}
@@ -6540,20 +6539,22 @@ parseKeyUsage(PCCERT_CONTEXT pCertCtx, DWORD flags)
65406539
int err;
65416540
oid = PyUnicode_FromString(usage->rgpszUsageIdentifier[i]);
65426541
if (oid == NULL) {
6543-
Py_CLEAR(retval);
65446542
goto error;
65456543
}
6546-
err = PySet_Add(retval, oid);
6544+
err = PyFrozenSetWriter_Add(retval, oid);
65476545
Py_DECREF(oid);
65486546
if (err == -1) {
6549-
Py_CLEAR(retval);
65506547
goto error;
65516548
}
65526549
}
65536550
}
6551+
PyMem_Free(usage);
6552+
return PyFrozenSetWriter_Finish(retval);
6553+
65546554
error:
65556555
PyMem_Free(usage);
6556-
return retval;
6556+
PyFrozenSetWriter_Discard(retval);
6557+
return NULL;
65576558
}
65586559

65596560
static HCERTSTORE

0 commit comments

Comments
 (0)