diff --git a/Doc/c-api/iter.rst b/Doc/c-api/iter.rst index 434d2021cea8e6..ecdc010083db4c 100644 --- a/Doc/c-api/iter.rst +++ b/Doc/c-api/iter.rst @@ -10,7 +10,8 @@ There are two functions specifically for working with iterators. .. c:function:: int PyIter_Check(PyObject *o) Return non-zero if the object *o* can be safely passed to - :c:func:`PyIter_Next`, and ``0`` otherwise. This function always succeeds. + :c:func:`PyIter_NextItem` (or the legacy :c:func:`PyIter_Next`), + and ``0`` otherwise. This function always succeeds. .. c:function:: int PyAIter_Check(PyObject *o) @@ -19,25 +20,26 @@ There are two functions specifically for working with iterators. .. versionadded:: 3.10 -.. c:function:: PyObject* PyIter_Next(PyObject *o) +.. c:function:: PyObject* PyIter_NextItem(PyObject *o, int *err) Return the next value from the iterator *o*. The object must be an iterator according to :c:func:`PyIter_Check` (it is up to the caller to check this). - If there are no remaining values, returns ``NULL`` with no exception set. - If an error occurs while retrieving the item, returns ``NULL`` and passes - along the exception. + If there are no remaining values, returns ``NULL`` with no exception set + and ``*err`` set to 0. If an error occurs while retrieving the item, + returns ``NULL`` and sets ``*err`` to 1. To write a loop which iterates over an iterator, the C code should look something like this:: PyObject *iterator = PyObject_GetIter(obj); - PyObject *item; if (iterator == NULL) { /* propagate error */ } - while ((item = PyIter_Next(iterator))) { + PyObject *item; + int err; + while (item = PyIter_NextItem(iterator, &err)) { /* do something with item */ ... /* release reference when done */ @@ -46,7 +48,7 @@ something like this:: Py_DECREF(iterator); - if (PyErr_Occurred()) { + if (err < 0) { /* propagate error */ } else { @@ -54,6 +56,17 @@ something like this:: } +.. c:function:: PyObject* PyIter_Next(PyObject *o) + + This is an older version of :c:func:`PyIter_NextItem`, which is retained + for backwards compatibility. Prefer :c:func:`PyIter_NextItem`. + + Return the next value from the iterator *o*. The object must be an iterator + according to :c:func:`PyIter_Check` (it is up to the caller to check this). + If there are no remaining values, returns ``NULL`` with no exception set. + If an error occurs while retrieving the item, returns ``NULL`` and passes + along the exception. + .. c:type:: PySendResult The enum value used to represent different results of :c:func:`PyIter_Send`. diff --git a/Doc/data/stable_abi.dat b/Doc/data/stable_abi.dat index 767025b76f8b11..2a33631a5963ec 100644 --- a/Doc/data/stable_abi.dat +++ b/Doc/data/stable_abi.dat @@ -326,6 +326,7 @@ function,PyInterpreterState_GetID,3.7,, function,PyInterpreterState_New,3.2,, function,PyIter_Check,3.8,, function,PyIter_Next,3.2,, +function,PyIter_NextItem,3.13,, function,PyIter_Send,3.10,, var,PyListIter_Type,3.2,, var,PyListRevIter_Type,3.2,, diff --git a/Include/abstract.h b/Include/abstract.h index b4c2bedef442bf..ea57fdafd196e4 100644 --- a/Include/abstract.h +++ b/Include/abstract.h @@ -397,6 +397,16 @@ PyAPI_FUNC(int) PyIter_Check(PyObject *); This function always succeeds. */ PyAPI_FUNC(int) PyAIter_Check(PyObject *); +/* Take an iterator object and call its tp_iternext slot, + returning the next value. + + If the iterator is exhausted, this returns NULL without setting an + exception, and sets *err to 0. + + NULL with *err == -1 means an error occurred, and an exception has + been set. */ +PyAPI_FUNC(PyObject*) PyIter_NextItem(PyObject *iter, int *err); + /* Takes an iterator object and calls its tp_iternext slot, returning the next value. diff --git a/Lib/test/test_capi/test_misc.py b/Lib/test/test_capi/test_misc.py index 037c8112d53e7a..15155afb6d68d5 100644 --- a/Lib/test/test_capi/test_misc.py +++ b/Lib/test/test_capi/test_misc.py @@ -410,6 +410,41 @@ def __delitem__(self, index): _testcapi.sequence_del_slice(mapping, 1, 3) self.assertEqual(mapping, {1: 'a', 2: 'b', 3: 'c'}) + def run_iter_api_test(self, next_func): + inputs = [ (), (1,2,3), + [], [1,2,3]] + + for inp in inputs: + items = [] + it = iter(inp) + while (item := next_func(it)) is not None: + items.append(item) + self.assertEqual(items, list(inp)) + + class Broken: + def __init__(self): + self.count = 0 + + def __next__(self): + if self.count < 3: + self.count += 1 + return self.count + else: + raise TypeError('bad type') + + it = Broken() + self.assertEqual(next_func(it), 1) + self.assertEqual(next_func(it), 2) + self.assertEqual(next_func(it), 3) + with self.assertRaisesRegex(TypeError, 'bad type'): + next_func(it) + + def test_iter_next(self): + self.run_iter_api_test(_testcapi.call_pyiter_next) + + def test_iter_nextitem(self): + self.run_iter_api_test(_testcapi.call_pyiter_nextitem) + @unittest.skipUnless(hasattr(_testcapi, 'negative_refcount'), 'need _testcapi.negative_refcount') def test_negative_refcount(self): diff --git a/Lib/test/test_stable_abi_ctypes.py b/Lib/test/test_stable_abi_ctypes.py index 60ad3603ae9223..66f00a7b442df9 100644 --- a/Lib/test/test_stable_abi_ctypes.py +++ b/Lib/test/test_stable_abi_ctypes.py @@ -356,6 +356,7 @@ def test_windows_feature_macros(self): "PyInterpreterState_New", "PyIter_Check", "PyIter_Next", + "PyIter_NextItem", "PyIter_Send", "PyListIter_Type", "PyListRevIter_Type", diff --git a/Misc/stable_abi.toml b/Misc/stable_abi.toml index 2209f6e02cfcd1..7ff516c5787c69 100644 --- a/Misc/stable_abi.toml +++ b/Misc/stable_abi.toml @@ -895,6 +895,8 @@ added = '3.2' [function.PyIter_Next] added = '3.2' +[function.PyIter_NextItem] + added = '3.13' [data.PyListIter_Type] added = '3.2' [data.PyListRevIter_Type] diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index d7c89f48f792ed..fe13298060bdbe 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -282,6 +282,42 @@ dict_getitem_knownhash(PyObject *self, PyObject *args) return Py_XNewRef(result); } +static PyObject* +call_pyiter_next(PyObject* self, PyObject *args) +{ + PyObject *iter; + if (!PyArg_ParseTuple(args, "O:call_pyiter_next", &iter)) { + return NULL; + } + assert(PyIter_Check(iter) || PyAIter_Check(iter)); + PyObject *item = PyIter_Next(iter); + if (item == NULL && !PyErr_Occurred()) { + Py_RETURN_NONE; + } + return item; +} + +static PyObject* +call_pyiter_nextitem(PyObject* self, PyObject *args) +{ + PyObject *iter; + if (!PyArg_ParseTuple(args, "O:call_pyiter_nextitem", &iter)) { + return NULL; + } + assert(PyIter_Check(iter) || PyAIter_Check(iter)); + int err; + PyObject *item = PyIter_NextItem(iter, &err); + if (err < 0) { + return NULL; + } + if (item == NULL) { + assert(!PyErr_Occurred()); + Py_RETURN_NONE; + } + return item; +} + + /* Issue #4701: Check that PyObject_Hash implicitly calls * PyType_Ready if it hasn't already been called */ @@ -3286,6 +3322,8 @@ static PyMethodDef TestMethods[] = { {"test_list_api", test_list_api, METH_NOARGS}, {"test_dict_iteration", test_dict_iteration, METH_NOARGS}, {"dict_getitem_knownhash", dict_getitem_knownhash, METH_VARARGS}, + {"call_pyiter_next", call_pyiter_next, METH_VARARGS}, + {"call_pyiter_nextitem", call_pyiter_nextitem, METH_VARARGS}, {"test_lazy_hash_inheritance", test_lazy_hash_inheritance,METH_NOARGS}, {"test_xincref_doesnt_leak",test_xincref_doesnt_leak, METH_NOARGS}, {"test_incref_doesnt_leak", test_incref_doesnt_leak, METH_NOARGS}, diff --git a/Modules/_xxtestfuzz/fuzzer.c b/Modules/_xxtestfuzz/fuzzer.c index 37d402824853f0..cc6db8011f06a1 100644 --- a/Modules/_xxtestfuzz/fuzzer.c +++ b/Modules/_xxtestfuzz/fuzzer.c @@ -377,7 +377,8 @@ static int fuzz_csv_reader(const char* data, size_t size) { if (reader) { /* Consume all of the reader as an iterator */ PyObject* parsed_line; - while ((parsed_line = PyIter_Next(reader))) { + int err; + while ((parsed_line = PyIter_NextItem(reader, &err))) { Py_DECREF(parsed_line); } } diff --git a/Objects/abstract.c b/Objects/abstract.c index e95785900c9c5f..9e6edcc7237960 100644 --- a/Objects/abstract.c +++ b/Objects/abstract.c @@ -2833,6 +2833,31 @@ PyAIter_Check(PyObject *obj) tp->tp_as_async->am_anext != &_PyObject_NextNotImplemented); } +/* Set *item to the next item. Return 0 on success and -1 on error. + * If the iteration terminates normally, set *item to NULL and clear + * the PyExc_StopIteration exception (if it was set). + */ +PyObject* +PyIter_NextItem(PyObject *iter, int *err) +{ + PyObject *item = (*Py_TYPE(iter)->tp_iternext)(iter); + if (item == NULL) { + PyThreadState *tstate = _PyThreadState_GET(); + if (_PyErr_Occurred(tstate)) { + if (_PyErr_ExceptionMatches(tstate, PyExc_StopIteration)) { + _PyErr_Clear(tstate); + item = NULL; + } + else { + *err = -1; + return NULL; + } + } + } + *err = 0; + return item; +} + /* Return next item. * If an error occurs, return NULL. PyErr_Occurred() will be true. * If the iteration terminates normally, return NULL and clear the @@ -2843,17 +2868,8 @@ PyAIter_Check(PyObject *obj) PyObject * PyIter_Next(PyObject *iter) { - PyObject *result; - result = (*Py_TYPE(iter)->tp_iternext)(iter); - if (result == NULL) { - PyThreadState *tstate = _PyThreadState_GET(); - if (_PyErr_Occurred(tstate) - && _PyErr_ExceptionMatches(tstate, PyExc_StopIteration)) - { - _PyErr_Clear(tstate); - } - } - return result; + int err; + return PyIter_NextItem(iter, &err); } PySendResult diff --git a/PC/python3dll.c b/PC/python3dll.c index f2c0d9dee883d9..4a22b05f354b1f 100755 --- a/PC/python3dll.c +++ b/PC/python3dll.c @@ -318,6 +318,7 @@ EXPORT_FUNC(PyInterpreterState_GetID) EXPORT_FUNC(PyInterpreterState_New) EXPORT_FUNC(PyIter_Check) EXPORT_FUNC(PyIter_Next) +EXPORT_FUNC(PyIter_NextItem) EXPORT_FUNC(PyIter_Send) EXPORT_FUNC(PyList_Append) EXPORT_FUNC(PyList_AsTuple)