Skip to content

gh-105201: Add PyIter_NextItem to replace PyIter_Next which has an ambiguous output #105202

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
Closed
29 changes: 21 additions & 8 deletions Doc/c-api/iter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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 */
Expand All @@ -46,14 +48,25 @@ something like this::

Py_DECREF(iterator);

if (PyErr_Occurred()) {
if (err < 0) {
/* propagate error */
}
else {
/* continue doing useful work */
}


.. 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`.
Expand Down
1 change: 1 addition & 0 deletions Doc/data/stable_abi.dat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions Include/abstract.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
35 changes: 35 additions & 0 deletions Lib/test/test_capi/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions Lib/test/test_stable_abi_ctypes.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Misc/stable_abi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
38 changes: 38 additions & 0 deletions Modules/_testcapimodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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},
Expand Down
3 changes: 2 additions & 1 deletion Modules/_xxtestfuzz/fuzzer.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
38 changes: 27 additions & 11 deletions Objects/abstract.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions PC/python3dll.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.