Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
69 changes: 69 additions & 0 deletions Doc/c-api/set.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <pyfrozensetwriter>`.

.. note::

The operation is atomic on :term:`free threading <free-threaded build>`
Expand Down Expand Up @@ -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``.
11 changes: 11 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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`.)
Expand Down
15 changes: 15 additions & 0 deletions Include/cpython/setobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
73 changes: 73 additions & 0 deletions Lib/test/test_capi/test_set.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
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.
_testcapi = import_helper.import_module('_testcapi')
_testlimitedcapi = import_helper.import_module('_testlimitedcapi')
_testinternalcapi = import_helper.import_module('_testinternalcapi')


class set_subclass(set):
pass

Expand Down Expand Up @@ -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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Add a new :ref:`PyFrozenSetWriter <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.
17 changes: 12 additions & 5 deletions Modules/_abc.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions Modules/_asynciomodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}


Expand Down
11 changes: 6 additions & 5 deletions Modules/_hashopenssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand All @@ -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]
Expand Down
13 changes: 7 additions & 6 deletions Modules/_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
}
Expand All @@ -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
Expand Down
Loading
Loading