Skip to content

Commit c9e96e6

Browse files
committed
Add PyFrozenSetWriter_UpdateArray() function
Add PyFrozenSetWriter_UpdateArray() and PyFrozenSetWriter_UpdateArraySteal() functions. Add tests on PyFrozenSetWriter_AddSteal(). Typoe: rename frozsetwriter_check_consistency() to frozensetwriter_check_consistency().
1 parent 96bd4f2 commit c9e96e6

7 files changed

Lines changed: 218 additions & 18 deletions

File tree

‎Doc/c-api/set.rst‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,3 +289,22 @@ See also the :c:func:`PyFrozenSet_New` function.
289289
290290
On success, return ``0``.
291291
On error, set an exception and return ``-1``.
292+
293+
On error, elements already added are kept.
294+
295+
.. c:function:: int PyFrozenSetWriter_UpdateArray(PyFrozenSetWriter *writer, PyObject *const *items, Py_ssize_t nitem)
296+
297+
Update the frozenset writer, adding *nitem* elements from *items* array:
298+
copy references from *items* to the writer.
299+
300+
On success, return ``0``.
301+
On error, set an exception and return ``-1``.
302+
303+
On error, elements already added are kept.
304+
305+
*nitem* must be positive or zero.
306+
307+
.. c:function:: int PyFrozenSetWriter_UpdateArraySteal(PyFrozenSetWriter *writer, PyObject *const *items, Py_ssize_t nitem)
308+
309+
Similar to :c:func:`PyFrozenSetWriter_UpdateArray`, but :term:`steal`
310+
references instead of copying references.

‎Doc/whatsnew/3.16.rst‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1304,6 +1304,8 @@ New features
13041304
* :c:func:`PyFrozenSetWriter_Add`
13051305
* :c:func:`PyFrozenSetWriter_AddSteal`
13061306
* :c:func:`PyFrozenSetWriter_Update`
1307+
* :c:func:`PyFrozenSetWriter_UpdateArray`
1308+
* :c:func:`PyFrozenSetWriter_UpdateArraySteal`
13071309

13081310
(Contributed by Victor Stinner in :gh:`160147`.)
13091311

‎Include/cpython/setobject.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,11 @@ PyAPI_FUNC(int) PyFrozenSetWriter_AddSteal(
8787
PyAPI_FUNC(int) PyFrozenSetWriter_Update(
8888
PyFrozenSetWriter *writer,
8989
PyObject *iterable);
90+
PyAPI_FUNC(int) PyFrozenSetWriter_UpdateArray(
91+
PyFrozenSetWriter *writer,
92+
PyObject *const *items,
93+
Py_ssize_t nitem);
94+
PyAPI_FUNC(int) PyFrozenSetWriter_UpdateArraySteal(
95+
PyFrozenSetWriter *writer,
96+
PyObject *const *items,
97+
Py_ssize_t nitem);

‎Lib/test/test_capi/test_set.py‎

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ def test_set_next_entry(self):
279279

280280
class FrozenSetWriterTests(unittest.TestCase):
281281
PySet_MINSIZE = _testcapi.PySet_MINSIZE
282+
UNHASHABLE = [1, 2, 3]
282283

283284
def create_writer(self, size=0):
284285
# Test PyFrozenSetWriter_Create()
@@ -296,6 +297,10 @@ def frozenset_equal(self, result, expected):
296297
self.assertEqual(result, expected)
297298
self.assertEqual(type(result), frozenset)
298299

300+
def assert_unhashable(self):
301+
err_msg = f"unhashable type: '{type(self.UNHASHABLE).__name__}'"
302+
return self.assertRaisesRegex(TypeError, err_msg)
303+
299304
def test_get_table_set(self):
300305
# Test size parameter of PyFrozenSetWriter_Create()
301306
PySet_MINSIZE = self.PySet_MINSIZE
@@ -313,40 +318,78 @@ def create_items(self, nitem):
313318
# Use mortal (and unique) objects to be able to detect reference leaks
314319
return tuple(object() for _ in range(nitem))
315320

316-
def test_add(self):
317-
# Test PyFrozenSetWriter_Append()
321+
def check_add_method(self, add_method):
318322
items = self.create_items(self.PySet_MINSIZE * 2 - 1)
319323
table_size = self.compute_table_size(len(items))
320324
for prealloc in (0, len(items) // 2, len(items), len(items) * 10):
321325
with self.subTest(size=prealloc):
322326
writer = self.create_writer()
327+
add = getattr(writer, add_method)
323328
for item in items:
324-
writer.add(item)
329+
add(item)
325330
self.frozenset_equal(writer.finish(), items)
326331

327332
# Test error (unhashable type)
328333
writer = self.create_writer(0)
329334
writer.update('abc')
330-
with self.assertRaisesRegex(TypeError, "unhashable type: 'list'"):
331-
writer.add([])
335+
add = getattr(writer, add_method)
336+
with self.assert_unhashable():
337+
add(self.UNHASHABLE)
332338
self.frozenset_equal(writer.finish(), frozenset('abc'))
333339

334-
def test_update(self):
335-
# Test PyFrozenSetWriter_Update()
336-
items1 = self.create_items(self.PySet_MINSIZE - 1)
340+
def test_add(self):
341+
# Test PyFrozenSetWriter_Add()
342+
self.check_add_method('add')
343+
344+
def test_add_steal(self):
345+
# Test PyFrozenSetWriter_AddSteal()
346+
self.check_add_method('add_steal')
347+
348+
def check_update_method(self, update_method):
349+
items = self.create_items(self.PySet_MINSIZE - 1)
337350
items2 = self.create_items(self.PySet_MINSIZE - 1)
338351
common = self.create_items(self.PySet_MINSIZE - 1)
339-
expected = set(items1) | set(items2) | set(common)
352+
expected = set(items) | set(items2) | set(common)
340353
total = len(expected)
341-
self.assertEqual(total, len(items1) + len(items2) + len(common))
354+
self.assertEqual(total, len(items) + len(items2) + len(common))
355+
empty = ()
342356

343357
for prealloc in (0, total // 2, total, total * 10):
344358
with self.subTest(size=prealloc):
345359
writer = self.create_writer(prealloc)
346-
writer.update(items1 + common)
347-
writer.update(common + items2)
360+
update = getattr(writer, update_method)
361+
update(items + common)
362+
update(common + items2)
363+
update(empty) # test nitem=0
348364
self.frozenset_equal(writer.finish(), expected)
349365

366+
# Test error path: first or last item is not hashable
367+
for first in (True, False):
368+
with self.subTest(first=first):
369+
if first:
370+
items_error = (self.UNHASHABLE, *items)
371+
expected = frozenset()
372+
else:
373+
items_error = (*items, self.UNHASHABLE)
374+
expected = frozenset(items)
375+
writer = self.create_writer(0)
376+
update = getattr(writer, update_method)
377+
with self.assert_unhashable():
378+
update(items_error)
379+
self.frozenset_equal(writer.finish(), expected)
380+
381+
def test_update(self):
382+
# Test PyFrozenSetWriter_Update()
383+
self.check_update_method('update')
384+
385+
def test_update_array(self):
386+
# Test PyFrozenSetWriter_UpdateArray()
387+
self.check_update_method('update_array')
388+
389+
def test_update_array_steal(self):
390+
# Test PyFrozenSetWriter_UpdateArraySteal()
391+
self.check_update_method('update_array_steal')
392+
350393
def test_finish(self):
351394
# Test PyFrozenSetWriter_Finish(): track the frozenset in the GC if at
352395
# least one item is tracked by the GC

‎Misc/NEWS.d/next/C_API/2026-10-21-18-20-57.gh-issue-160147.7uo4pF.rst‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@ Add a new :ref:`PyFrozenSetWriter <pyfrozensetwriter>` C API to create a
77
* :c:func:`PyFrozenSetWriter_Add`
88
* :c:func:`PyFrozenSetWriter_AddSteal`
99
* :c:func:`PyFrozenSetWriter_Update`
10+
* :c:func:`PyFrozenSetWriter_UpdateArray`
11+
* :c:func:`PyFrozenSetWriter_UpdateArraySteal`
1012

1113
Patch by Victor Stinner.

‎Modules/_testcapi/set.c‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,58 @@ writer_update(PyObject *self_raw, PyObject *iterable)
150150
}
151151

152152

153+
static PyObject*
154+
_writer_update_array(PyObject *self_raw, PyObject *tuple, int steal)
155+
{
156+
WriterObject *self = (WriterObject *)self_raw;
157+
if (writer_check(self) < 0) {
158+
return NULL;
159+
}
160+
161+
if (!PyTuple_Check(tuple)) {
162+
PyErr_Format(PyExc_TypeError,
163+
"expect tuple, got %T", tuple);
164+
return NULL;
165+
}
166+
PyObject **items = &PyTuple_GET_ITEM(tuple, 0);
167+
Py_ssize_t len = PyTuple_GET_SIZE(tuple);
168+
169+
if (steal) {
170+
for (Py_ssize_t i = 0; i < len; i++) {
171+
Py_INCREF(items[i]);
172+
}
173+
}
174+
175+
int res;
176+
if (steal) {
177+
res = PyFrozenSetWriter_UpdateArraySteal(self->writer, items, len);
178+
}
179+
else {
180+
res = PyFrozenSetWriter_UpdateArray(self->writer, items, len);
181+
}
182+
if (res < 0) {
183+
return NULL;
184+
}
185+
Py_RETURN_NONE;
186+
}
187+
188+
189+
// Test PyFrozenSetWriter_UpdateArray()
190+
static PyObject*
191+
writer_update_array(PyObject *self, PyObject *tuple)
192+
{
193+
return _writer_update_array(self, tuple, 0);
194+
}
195+
196+
197+
// Test PyFrozenSetWriter_UpdateArraySteal()
198+
static PyObject*
199+
writer_update_array_steal(PyObject *self, PyObject *tuple)
200+
{
201+
return _writer_update_array(self, tuple, 1);
202+
}
203+
204+
153205
static PyObject*
154206
writer_get_table_size(PyObject *self_raw, PyObject *Py_UNUSED(args))
155207
{
@@ -198,6 +250,8 @@ static PyMethodDef writer_methods[] = {
198250
{"add", _PyCFunction_CAST(writer_add), METH_O},
199251
{"add_steal", _PyCFunction_CAST(writer_add_steal), METH_O},
200252
{"update", _PyCFunction_CAST(writer_update), METH_O},
253+
{"update_array", _PyCFunction_CAST(writer_update_array), METH_O},
254+
{"update_array_steal", _PyCFunction_CAST(writer_update_array_steal), METH_O},
201255
{"get_table_size", _PyCFunction_CAST(writer_get_table_size), METH_NOARGS},
202256
{"finish", _PyCFunction_CAST(writer_finish), METH_NOARGS},
203257
{"discard", _PyCFunction_CAST(writer_discard), METH_NOARGS},

‎Objects/setobject.c‎

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3229,10 +3229,11 @@ static PyObject _dummy_struct = _PyObject_HEAD_INIT(&_PySetDummy_Type);
32293229

32303230
#ifndef NDEBUG
32313231
static int
3232-
frozsetwriter_check_consistency(PyFrozenSetWriter *writer)
3232+
frozensetwriter_check_consistency(PyFrozenSetWriter *writer)
32333233
{
32343234
assert(writer != NULL);
32353235
PyObject *frozenset = _PyObject_CAST(writer);
3236+
assert(PyFrozenSet_CheckExact(frozenset));
32363237
// Don't test _PyObject_IsUniquelyReferenced() since a frozenset writer
32373238
// can be shared between multiple threads with external locking.
32383239
assert(Py_REFCNT(frozenset) == 1);
@@ -3263,7 +3264,7 @@ PyFrozenSetWriter_Create(Py_ssize_t size)
32633264
}
32643265

32653266
PyFrozenSetWriter *writer = (PyFrozenSetWriter*)frozenset;
3266-
assert(frozsetwriter_check_consistency(writer));
3267+
assert(frozensetwriter_check_consistency(writer));
32673268
return writer;
32683269
}
32693270

@@ -3275,7 +3276,7 @@ PyFrozenSetWriter_Discard(PyFrozenSetWriter *writer)
32753276
return;
32763277
}
32773278

3278-
assert(frozsetwriter_check_consistency(writer));
3279+
assert(frozensetwriter_check_consistency(writer));
32793280
PyObject *frozenset = _PyObject_CAST(writer);
32803281
Py_DECREF(frozenset);
32813282
}
@@ -3284,7 +3285,7 @@ PyFrozenSetWriter_Discard(PyFrozenSetWriter *writer)
32843285
PyObject*
32853286
PyFrozenSetWriter_Finish(PyFrozenSetWriter *writer)
32863287
{
3287-
assert(frozsetwriter_check_consistency(writer));
3288+
assert(frozensetwriter_check_consistency(writer));
32883289
PySetObject *frozenset = _PySet_CAST(writer);
32893290

32903291
// Now that the frozenset is fully initialized, it's safe to track it in
@@ -3306,7 +3307,7 @@ PyFrozenSetWriter_Finish(PyFrozenSetWriter *writer)
33063307
int
33073308
PyFrozenSetWriter_AddSteal(PyFrozenSetWriter *writer, PyObject *item)
33083309
{
3309-
assert(frozsetwriter_check_consistency(writer));
3310+
assert(frozensetwriter_check_consistency(writer));
33103311

33113312
Py_hash_t hash = PyObject_Hash(item);
33123313
if (hash == -1) {
@@ -3330,7 +3331,78 @@ PyFrozenSetWriter_Add(PyFrozenSetWriter *writer, PyObject *item)
33303331
int
33313332
PyFrozenSetWriter_Update(PyFrozenSetWriter *writer, PyObject *iterable)
33323333
{
3333-
assert(frozsetwriter_check_consistency(writer));
3334+
assert(frozensetwriter_check_consistency(writer));
33343335
PySetObject *frozenset = _PySet_CAST(writer);
33353336
return set_update_local(frozenset, iterable);
33363337
}
3338+
3339+
3340+
static inline int
3341+
frozensetwriter_update_array(PyFrozenSetWriter *writer,
3342+
PyObject *const *items, Py_ssize_t nitem,
3343+
int steal)
3344+
{
3345+
if (nitem < 0) {
3346+
PyErr_SetString(PyExc_ValueError, "nitem must be >= 0");
3347+
return -1;
3348+
}
3349+
3350+
#define CLEANUP(START) \
3351+
if (steal) { \
3352+
for (Py_ssize_t j = (START); j < nitem; j++) { \
3353+
Py_DECREF(items[j]); \
3354+
} \
3355+
}
3356+
3357+
assert(frozensetwriter_check_consistency(writer));
3358+
PySetObject *frozenset = _PySet_CAST(writer);
3359+
3360+
/* Do one big resize at the start, rather than
3361+
* incrementally resizing as we insert new keys.
3362+
*/
3363+
if ((frozenset->fill + nitem) * 5 >= frozenset->mask * 3) {
3364+
if (set_table_resize(frozenset, (frozenset->used + nitem) * 2) != 0) {
3365+
CLEANUP(0);
3366+
return -1;
3367+
}
3368+
}
3369+
3370+
for (Py_ssize_t i = 0; i < nitem; i++) {
3371+
PyObject *item = items[i];
3372+
3373+
Py_hash_t hash = PyObject_Hash(item);
3374+
if (hash == -1) {
3375+
set_unhashable_type(item);
3376+
// CLEANUP() calls Py_DECREF(item)
3377+
CLEANUP(i);
3378+
return -1;
3379+
}
3380+
3381+
if (!steal) {
3382+
item = Py_NewRef(item);
3383+
}
3384+
if (set_add_entry_takeref(frozenset, item, hash) < 0) {
3385+
CLEANUP(i);
3386+
return -1;
3387+
}
3388+
}
3389+
return 0;
3390+
3391+
#undef CLEANUP
3392+
}
3393+
3394+
3395+
int
3396+
PyFrozenSetWriter_UpdateArray(PyFrozenSetWriter *writer,
3397+
PyObject *const *items, Py_ssize_t nitem)
3398+
{
3399+
return frozensetwriter_update_array(writer, items, nitem, 0);
3400+
}
3401+
3402+
3403+
int
3404+
PyFrozenSetWriter_UpdateArraySteal(PyFrozenSetWriter *writer,
3405+
PyObject *const *items, Py_ssize_t nitem)
3406+
{
3407+
return frozensetwriter_update_array(writer, items, nitem, 1);
3408+
}

0 commit comments

Comments
 (0)