From ce1b2ffcb9749c1637dae189cb8ba1b9d8955741 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 7 Oct 2025 12:51:43 +0200 Subject: [PATCH 1/4] gh-111489: Add PyTuple_FromArray() function --- Doc/c-api/tuple.rst | 11 +++++ Doc/whatsnew/3.15.rst | 3 ++ Include/cpython/tupleobject.h | 4 ++ Include/internal/pycore_tuple.h | 4 +- Lib/test/test_capi/test_tuple.py | 8 ++++ ...-10-07-12-51-32.gh-issue-111489.LCKKlg.rst | 2 + Modules/_testcapi/tuple.c | 42 +++++++++++++++++++ Objects/tupleobject.c | 2 +- 8 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/C_API/2025-10-07-12-51-32.gh-issue-111489.LCKKlg.rst diff --git a/Doc/c-api/tuple.rst b/Doc/c-api/tuple.rst index 815afddad19df1..e24d3ba9964a8e 100644 --- a/Doc/c-api/tuple.rst +++ b/Doc/c-api/tuple.rst @@ -37,6 +37,17 @@ Tuple Objects or ``NULL`` with an exception set on failure. +.. c:function:: PyObject* PyTuple_FromArray(PyObject *const *array, Py_ssize_t size) + + Create a tuple of *size* items and copy references from *array* to the new + tuple. + + * Return a new reference on success. + * Set an exception and return ``NULL`` on error. + + .. versionadded:: next + + .. c:function:: PyObject* PyTuple_Pack(Py_ssize_t n, ...) Return a new tuple object of size *n*, diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 4b176d6c8e6034..40286d4fe857e8 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -852,6 +852,9 @@ New features (Contributed by Victor Stinner in :gh:`129813`.) +* Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array. + (Contributed by Victor Stinner in :gh:`111489`.) + Porting to Python 3.15 ---------------------- diff --git a/Include/cpython/tupleobject.h b/Include/cpython/tupleobject.h index afb98ccbb81b2d..888baaf3358267 100644 --- a/Include/cpython/tupleobject.h +++ b/Include/cpython/tupleobject.h @@ -38,3 +38,7 @@ PyTuple_SET_ITEM(PyObject *op, Py_ssize_t index, PyObject *value) { } #define PyTuple_SET_ITEM(op, index, value) \ PyTuple_SET_ITEM(_PyObject_CAST(op), (index), _PyObject_CAST(value)) + +PyAPI_FUNC(PyObject*) PyTuple_FromArray( + PyObject *const *array, + Py_ssize_t size); diff --git a/Include/internal/pycore_tuple.h b/Include/internal/pycore_tuple.h index acf1bec46028ac..be1961cbf77a2d 100644 --- a/Include/internal/pycore_tuple.h +++ b/Include/internal/pycore_tuple.h @@ -23,7 +23,9 @@ extern PyStatus _PyTuple_InitGlobalObjects(PyInterpreterState *); #define _PyTuple_ITEMS(op) _Py_RVALUE(_PyTuple_CAST(op)->ob_item) -PyAPI_FUNC(PyObject *)_PyTuple_FromArray(PyObject *const *, Py_ssize_t); +// Alias for backward compatibility +#define _PyTuple_FromArray PyTuple_FromArray + PyAPI_FUNC(PyObject *)_PyTuple_FromStackRefStealOnSuccess(const union _PyStackRef *, Py_ssize_t); PyAPI_FUNC(PyObject *)_PyTuple_FromArraySteal(PyObject *const *, Py_ssize_t); diff --git a/Lib/test/test_capi/test_tuple.py b/Lib/test/test_capi/test_tuple.py index 7c07bc64e247c5..d4300447e5f6df 100644 --- a/Lib/test/test_capi/test_tuple.py +++ b/Lib/test/test_capi/test_tuple.py @@ -279,6 +279,14 @@ def my_iter(): self.assertEqual(tuple(my_iter()), (TAG, *range(10))) self.assertEqual(tuples, []) + def test_tuple_fromarray(self): + # Test PyTuple_FromArray() + tuple_fromarray = _testcapi.tuple_fromarray + + tup = tuple(object() for _ in range(5)) + copy = tuple_fromarray(tup) + self.assertEqual(copy, tup) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/C_API/2025-10-07-12-51-32.gh-issue-111489.LCKKlg.rst b/Misc/NEWS.d/next/C_API/2025-10-07-12-51-32.gh-issue-111489.LCKKlg.rst new file mode 100644 index 00000000000000..9c044f7796bd94 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2025-10-07-12-51-32.gh-issue-111489.LCKKlg.rst @@ -0,0 +1,2 @@ +Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array. +Patch by Victor Stinner. diff --git a/Modules/_testcapi/tuple.c b/Modules/_testcapi/tuple.c index d9c02ba0ff04fe..023681d34d7567 100644 --- a/Modules/_testcapi/tuple.c +++ b/Modules/_testcapi/tuple.c @@ -104,12 +104,54 @@ _check_tuple_item_is_NULL(PyObject *Py_UNUSED(module), PyObject *args) } +static PyObject * +tuple_fromarray(PyObject* Py_UNUSED(module), PyObject *args) +{ + PyObject *src; + if (!PyArg_ParseTuple(args, "O!", &PyTuple_Type, &src)) { + return NULL; + } + + Py_ssize_t size = PyTuple_GET_SIZE(src); + PyObject **array = PyMem_Malloc(size * sizeof(PyObject**)); + if (array == NULL) { + PyErr_NoMemory(); + return NULL; + } + for (Py_ssize_t i = 0; i < size; i++) { + array[i] = Py_NewRef(PyTuple_GET_ITEM(src, i)); + } + + PyObject *tuple = PyTuple_FromArray(array, size); + if (tuple == NULL) { + goto done; + } + + for (Py_ssize_t i = 0; i < size; i++) { + // check that the array is not modified + assert(array[i] == PyTuple_GET_ITEM(src, i)); + + // check that the array was copied properly + assert(PyTuple_GET_ITEM(tuple, i) == array[i]); + } + +done: + for (Py_ssize_t i = 0; i < size; i++) { + Py_DECREF(array[i]); + } + PyMem_Free(array); + + return tuple; +} + + static PyMethodDef test_methods[] = { {"tuple_get_size", tuple_get_size, METH_O}, {"tuple_get_item", tuple_get_item, METH_VARARGS}, {"tuple_set_item", tuple_set_item, METH_VARARGS}, {"_tuple_resize", _tuple_resize, METH_VARARGS}, {"_check_tuple_item_is_NULL", _check_tuple_item_is_NULL, METH_VARARGS}, + {"tuple_fromarray", tuple_fromarray, METH_VARARGS}, {NULL}, }; diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index 9b31758485ca5e..1fa4bae638a1fe 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -366,7 +366,7 @@ tuple_item(PyObject *op, Py_ssize_t i) } PyObject * -_PyTuple_FromArray(PyObject *const *src, Py_ssize_t n) +PyTuple_FromArray(PyObject *const *src, Py_ssize_t n) { if (n == 0) { return tuple_get_empty(); From b0fbeae2485043685291441bd9d2105a45c6c5e8 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 7 Oct 2025 13:02:47 +0200 Subject: [PATCH 2/4] Add test on empty tuple --- Lib/test/test_capi/test_tuple.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/test/test_capi/test_tuple.py b/Lib/test/test_capi/test_tuple.py index d4300447e5f6df..9613e28d4a78b2 100644 --- a/Lib/test/test_capi/test_tuple.py +++ b/Lib/test/test_capi/test_tuple.py @@ -287,6 +287,10 @@ def test_tuple_fromarray(self): copy = tuple_fromarray(tup) self.assertEqual(copy, tup) + tup = () + copy = tuple_fromarray(tup) + self.assertIs(copy, tup) + if __name__ == "__main__": unittest.main() From cc47b1035a33549a0cedb01d0db999f650e915bb Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 7 Oct 2025 14:12:04 +0200 Subject: [PATCH 3/4] Address Serhiy's review --- Doc/c-api/tuple.rst | 6 +++-- Lib/test/test_capi/test_tuple.py | 34 +++++++++++++++++---------- Modules/_testcapi/tuple.c | 40 +++++++++++--------------------- Objects/tupleobject.c | 4 ++++ 4 files changed, 43 insertions(+), 41 deletions(-) diff --git a/Doc/c-api/tuple.rst b/Doc/c-api/tuple.rst index e24d3ba9964a8e..65f8334c437974 100644 --- a/Doc/c-api/tuple.rst +++ b/Doc/c-api/tuple.rst @@ -42,8 +42,10 @@ Tuple Objects Create a tuple of *size* items and copy references from *array* to the new tuple. - * Return a new reference on success. - * Set an exception and return ``NULL`` on error. + *array* can be NULL if *size* is ``0``. + + On success, return a new reference. + On error, set an exception and return ``NULL``. .. versionadded:: next diff --git a/Lib/test/test_capi/test_tuple.py b/Lib/test/test_capi/test_tuple.py index 9613e28d4a78b2..c9fb2f6dd54924 100644 --- a/Lib/test/test_capi/test_tuple.py +++ b/Lib/test/test_capi/test_tuple.py @@ -62,6 +62,28 @@ def test_tuple_new(self): self.assertRaises(SystemError, tuple_new, PY_SSIZE_T_MIN) self.assertRaises(MemoryError, tuple_new, PY_SSIZE_T_MAX) + def test_tuple_fromarray(self): + # Test PyTuple_FromArray() + tuple_fromarray = _testcapi.tuple_fromarray + + tup = tuple([i] for i in range(5)) + copy = tuple_fromarray(tup) + self.assertEqual(copy, tup) + + tup = () + copy = tuple_fromarray(tup) + self.assertIs(copy, tup) + + copy = tuple_fromarray(NULL, 0) + self.assertIs(copy, ()) + + with self.assertRaises(ValueError): + tuple_fromarray(NULL, -1) + with self.assertRaises(ValueError): + tuple_fromarray(NULL, PY_SSIZE_T_MIN) + with self.assertRaises(MemoryError): + tuple_fromarray(NULL, PY_SSIZE_T_MAX) + def test_tuple_pack(self): # Test PyTuple_Pack() pack = _testlimitedcapi.tuple_pack @@ -279,18 +301,6 @@ def my_iter(): self.assertEqual(tuple(my_iter()), (TAG, *range(10))) self.assertEqual(tuples, []) - def test_tuple_fromarray(self): - # Test PyTuple_FromArray() - tuple_fromarray = _testcapi.tuple_fromarray - - tup = tuple(object() for _ in range(5)) - copy = tuple_fromarray(tup) - self.assertEqual(copy, tup) - - tup = () - copy = tuple_fromarray(tup) - self.assertIs(copy, tup) - if __name__ == "__main__": unittest.main() diff --git a/Modules/_testcapi/tuple.c b/Modules/_testcapi/tuple.c index 023681d34d7567..2e2184b4046650 100644 --- a/Modules/_testcapi/tuple.c +++ b/Modules/_testcapi/tuple.c @@ -108,40 +108,26 @@ static PyObject * tuple_fromarray(PyObject* Py_UNUSED(module), PyObject *args) { PyObject *src; - if (!PyArg_ParseTuple(args, "O!", &PyTuple_Type, &src)) { + Py_ssize_t size = -1; + if (!PyArg_ParseTuple(args, "O|n", &src, &size)) { return NULL; } - - Py_ssize_t size = PyTuple_GET_SIZE(src); - PyObject **array = PyMem_Malloc(size * sizeof(PyObject**)); - if (array == NULL) { - PyErr_NoMemory(); + if (src != Py_None && !PyTuple_Check(src)) { + PyErr_SetString(PyExc_TypeError, "expect a tuple"); return NULL; } - for (Py_ssize_t i = 0; i < size; i++) { - array[i] = Py_NewRef(PyTuple_GET_ITEM(src, i)); - } - - PyObject *tuple = PyTuple_FromArray(array, size); - if (tuple == NULL) { - goto done; - } - - for (Py_ssize_t i = 0; i < size; i++) { - // check that the array is not modified - assert(array[i] == PyTuple_GET_ITEM(src, i)); - // check that the array was copied properly - assert(PyTuple_GET_ITEM(tuple, i) == array[i]); + PyObject **items; + if (src != Py_None) { + items = &PyTuple_GET_ITEM(src, 0); + if (size < 0) { + size = PyTuple_GET_SIZE(src); + } } - -done: - for (Py_ssize_t i = 0; i < size; i++) { - Py_DECREF(array[i]); + else { + items = NULL; } - PyMem_Free(array); - - return tuple; + return PyTuple_FromArray(items, size); } diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index 1fa4bae638a1fe..d9c89fce93db89 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -371,6 +371,10 @@ PyTuple_FromArray(PyObject *const *src, Py_ssize_t n) if (n == 0) { return tuple_get_empty(); } + if (n < 0) { + PyErr_SetString(PyExc_ValueError, "size must be nonnegative"); + return NULL; + } PyTupleObject *tuple = tuple_alloc(n); if (tuple == NULL) { From 89f91fc0646aa91dea31b95ca197bb28f51b9cf7 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 7 Oct 2025 15:14:52 +0200 Subject: [PATCH 4/4] Use SystemError for negative size Use also UNINITIALIZED_SIZE in test code. --- Lib/test/test_capi/test_tuple.py | 4 ++-- Modules/_testcapi/tuple.c | 4 ++-- Objects/tupleobject.c | 4 ---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_capi/test_tuple.py b/Lib/test/test_capi/test_tuple.py index c9fb2f6dd54924..b6d6da008d0b7b 100644 --- a/Lib/test/test_capi/test_tuple.py +++ b/Lib/test/test_capi/test_tuple.py @@ -77,9 +77,9 @@ def test_tuple_fromarray(self): copy = tuple_fromarray(NULL, 0) self.assertIs(copy, ()) - with self.assertRaises(ValueError): + with self.assertRaises(SystemError): tuple_fromarray(NULL, -1) - with self.assertRaises(ValueError): + with self.assertRaises(SystemError): tuple_fromarray(NULL, PY_SSIZE_T_MIN) with self.assertRaises(MemoryError): tuple_fromarray(NULL, PY_SSIZE_T_MAX) diff --git a/Modules/_testcapi/tuple.c b/Modules/_testcapi/tuple.c index 2e2184b4046650..5de1c494c0a8c0 100644 --- a/Modules/_testcapi/tuple.c +++ b/Modules/_testcapi/tuple.c @@ -108,7 +108,7 @@ static PyObject * tuple_fromarray(PyObject* Py_UNUSED(module), PyObject *args) { PyObject *src; - Py_ssize_t size = -1; + Py_ssize_t size = UNINITIALIZED_SIZE; if (!PyArg_ParseTuple(args, "O|n", &src, &size)) { return NULL; } @@ -120,7 +120,7 @@ tuple_fromarray(PyObject* Py_UNUSED(module), PyObject *args) PyObject **items; if (src != Py_None) { items = &PyTuple_GET_ITEM(src, 0); - if (size < 0) { + if (size == UNINITIALIZED_SIZE) { size = PyTuple_GET_SIZE(src); } } diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index d9c89fce93db89..1fa4bae638a1fe 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -371,10 +371,6 @@ PyTuple_FromArray(PyObject *const *src, Py_ssize_t n) if (n == 0) { return tuple_get_empty(); } - if (n < 0) { - PyErr_SetString(PyExc_ValueError, "size must be nonnegative"); - return NULL; - } PyTupleObject *tuple = tuple_alloc(n); if (tuple == NULL) {