diff --git a/Doc/c-api/tuple.rst b/Doc/c-api/tuple.rst index 815afddad19df1..65f8334c437974 100644 --- a/Doc/c-api/tuple.rst +++ b/Doc/c-api/tuple.rst @@ -37,6 +37,19 @@ 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. + + *array* can be NULL if *size* is ``0``. + + On success, return a new reference. + On error, set an exception and return ``NULL``. + + .. 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..b6d6da008d0b7b 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(SystemError): + tuple_fromarray(NULL, -1) + with self.assertRaises(SystemError): + 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 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..5de1c494c0a8c0 100644 --- a/Modules/_testcapi/tuple.c +++ b/Modules/_testcapi/tuple.c @@ -104,12 +104,40 @@ _check_tuple_item_is_NULL(PyObject *Py_UNUSED(module), PyObject *args) } +static PyObject * +tuple_fromarray(PyObject* Py_UNUSED(module), PyObject *args) +{ + PyObject *src; + Py_ssize_t size = UNINITIALIZED_SIZE; + if (!PyArg_ParseTuple(args, "O|n", &src, &size)) { + return NULL; + } + if (src != Py_None && !PyTuple_Check(src)) { + PyErr_SetString(PyExc_TypeError, "expect a tuple"); + return NULL; + } + + PyObject **items; + if (src != Py_None) { + items = &PyTuple_GET_ITEM(src, 0); + if (size == UNINITIALIZED_SIZE) { + size = PyTuple_GET_SIZE(src); + } + } + else { + items = NULL; + } + return PyTuple_FromArray(items, size); +} + + 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();