8000 gh-105201: Add PyIter_NextItem() (#122331) · python/cpython@e006c73 · GitHub
[go: up one dir, main page]

Skip to content

Commit e006c73

Browse files
gh-105201: Add PyIter_NextItem() (#122331)
Return -1 and set an exception on error; return 0 if the iterator is exhausted, and return 1 if the next item was fetched successfully. Prefer this API to PyIter_Next(), which requires the caller to use PyErr_Occurred() to differentiate between iterator exhaustion and errors. Co-authered-by: Irit Katriel <iritkatriel@yahoo.com>
1 parent 540fcc6 commit e006c73

File tree

12 files changed

+156
-40
lines changed

12 files changed

+156
-40
lines changed

Doc/c-api/iter.rst

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ There are two functions specifically for working with iterators.
1010
.. c:function:: int PyIter_Check(PyObject *o)
1111
1212
Return non-zero if the object *o* can be safely passed to
13-
:c:func:`PyIter_Next`, and ``0`` otherwise. This function always succeeds.
13+
:c:func:`PyIter_NextItem` and ``0`` otherwise.
14+
This function always succeeds.
1415
1516
.. c:function:: int PyAIter_Check(PyObject *o)
1617
@@ -19,41 +20,27 @@ There are two functions specifically for working with iterators.
1920
2021
.. versionadded:: 3.10
2122
23+
.. c:function:: int PyIter_NextItem(PyObject *iter, PyObject **item)
24+
25+
Return ``1`` and set *item* to a :term:`strong reference` of the
26+
next value of the iterator *iter* on success.
27+
Return ``0`` and set *item* to ``NULL`` if there are no remaining values.
28+
Return ``-1``, set *item* to ``NULL`` and set an exception on error.
29+
30+
.. versionadded:: 3.14
31+
2232
.. c:function:: PyObject* PyIter_Next(PyObject *o)
2333
34+
This is an older version of :c:func:`!PyIter_NextItem`,
35+
which is retained for backwards compatibility.
36+
Prefer :c:func:`PyIter_NextItem`.
37+
2438
Return the next value from the iterator *o*. The object must be an iterator
2539
according to :c:func:`PyIter_Check` (it is up to the caller to check this).
2640
If there are no remaining values, returns ``NULL`` with no exception set.
2741
If an error occurs while retrieving the item, returns ``NULL`` and passes
2842
along the exception.
2943
30-
To write a loop which iterates over an iterator, the C code should look
31-
something like this::
32-
33-
PyObject *iterator = PyObject_GetIter(obj);
34-
PyObject *item;
35-
36-
if (iterator == NULL) {
37-
/* propagate error */
38-
}
39-
40-
while ((item = PyIter_Next(iterator))) {
41-
/* do something with item */
42-
...
43-
/* release reference when done */
44-
Py_DECREF(item);
45-
}
46-
47-
Py_DECREF(iterator);
48-
49-
if (PyErr_Occurred()) {
50-
/* propagate error */
51-
}
52-
else {
53-
/* continue doing useful work */
54-
}
55-
56-
5744
.. c:type:: PySendResult
5845
5946
The enum value used to represent different results of :c:func:`PyIter_Send`.

Doc/data/refcounts.dat

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,6 +1132,10 @@ PyAIter_Check:PyObject*:o:0:
11321132
PyIter_Next:PyObject*::+1:
11331133
PyIter_Next:PyObject*:o:0:
11341134

1135+
PyIter_NextItem:int:::
1136+
PyIter_NextItem:PyObject*:iter:0:
1137+
PyIter_NextItem:PyObject**:item:+1:
1138+
11351139
PyIter_Send:int:::
11361140
PyIter_Send:PyObject*:iter:0:
11371141
PyIter_Send:PyObject*:arg:0:

Doc/data/stable_abi.dat

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Doc/whatsnew/3.14.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,10 @@ New Features
404404

405405
(Contributed by Victor Stinner in :gh:`119182`.)
406406

407+
* Add :c:func:`PyIter_NextItem` to replace :c:func:`PyIter_Next`,
408+
which has an ambiguous return value.
409+
(Contributed by Irit Katriel and Erlend Aasland in :gh:`105201`.)
410+
407411
Porting to Python 3.14
408412
----------------------
409413

Include/abstract.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,13 +397,23 @@ PyAPI_FUNC(int) PyIter_Check(PyObject *);
397397
This function always succeeds. */
398398
PyAPI_FUNC(int) PyAIter_Check(PyObject *);
399399

400+
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030e0000
401+
/* Return 1 and set 'item' to the next item of 'iter' on success.
402+
* Return 0 and set 'item' to NULL when there are no remaining values.
403+
* Return -1, set 'item' to NULL and set an exception on error.
404+
*/
405+
PyAPI_FUNC(int) PyIter_NextItem(PyObject *iter, PyObject **item);
406+
#endif
407+
400408
/* Takes an iterator object and calls its tp_iternext slot,
401409
returning the next value.
402410
403411
If the iterator is exhausted, this returns NULL without setting an
404412
exception.
405413
406-
NULL with an exception means an error occurred. */
414+
NULL with an exception means an error occurred.
415+
416+
Prefer PyIter_NextItem() instead. */
407417
PyAPI_FUNC(PyObject *) PyIter_Next(PyObject *);
408418

409419
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030A0000

Lib/test/test_capi/test_abstract.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,46 @@ def test_object_generichash(self):
10071007
for obj in object(), 1, 'string', []:
10081008
self.assertEqual(generichash(obj), object.__hash__(obj))
10091009

1010+
def run_iter_api_test(self, next_func):
1011+
for data in (), [], (1, 2, 3), [1 , 2, 3], "123":
1012+
with self.subTest(data=data):
1013+
items = []
1014+
it = iter(data)
1015+
while (item := next_func(it)) is not None:
1016+
items.append(item)
1017+
self.assertEqual(items, list(data))
1018+
1019+
class Broken:
1020+
def __init__(self):
1021+
self.count = 0
1022+
1023+
def __next__(self):
1024+
if self.count < 3:
1025+
self.count += 1
1026+
return self.count
1027+
else:
1028+
raise TypeError('bad type')
1029+
1030+
it = Broken()
1031+
self.assertEqual(next_func(it), 1)
1032+
self.assertEqual(next_func(it), 2)
1033+
self.assertEqual(next_func(it), 3)
1034+
with self.assertRaisesRegex(TypeError, 'bad type'):
1035+
next_func(it)
1036+
1037+
def test_iter_next(self):
1038+
from _testcapi import PyIter_Next
1039+
self.run_iter_api_test(PyIter_Next)
1040+
# CRASHES PyIter_Next(10)
1041+
1042+
def test_iter_nextitem(self):
1043+
from _testcapi import PyIter_NextItem
1044+
self.run_iter_api_test(PyIter_NextItem)
1045+
1046+
regex = "expected.*iterator.*got.*'int'"
1047+
with self.assertRaisesRegex(TypeError, regex):
1048+
PyIter_NextItem(10)
1049+
10101050

10111051
if __name__ == "__main__":
10121052
unittest.main()

Lib/test/test_stable_abi_ctypes.py

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :c:func:`PyIter_NextItem` to replace :c:func:`PyIter_Next`, which has an
2+
ambiguous return value. Patch by Irit Katriel and Erlend Aasland.

Misc/stable_abi.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2508,3 +2508,5 @@
25082508

25092509
[function.Py_TYPE]
25102510
added = '3.14'
2511+
[function.PyIter_NextItem]
2512+
added = '3.14'

Modules/_testcapi/abstract.c

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,33 @@ mapping_getoptionalitem(PyObject *self, PyObject *args)
129129
}
130130
}
131131

132+
static PyObject *
133+
pyiter_next(PyObject *self, PyObject *iter)
134+
{
135+
PyObject *item = PyIter_Next(iter);
136+
if (item == NULL && !PyErr_Occurred()) {
137+
Py_RETURN_NONE;
138+
}
139+
return item;
140+
}
141+
142+
static PyObject *
143+
pyiter_nextitem(PyObject *self, PyObject *iter)
144+
{
145+
PyObject *item;
146+
int rc = PyIter_NextItem(iter, &item);
147+
if (rc < 0) {
148+
assert(PyErr_Occurred());
149+
assert(item == NULL);
150+
return NULL;
151+
}
152+
assert(!PyErr_Occurred());
153+
if (item == NULL) {
154+
Py_RETURN_NONE;
155+
}
156+
return item;
157+
}
158+
132159

133160
static PyMethodDef test_methods[] = {
134161
{"object_getoptionalattr", object_getoptionalattr, METH_VARARGS},
@@ -138,6 +165,8 @@ static PyMethodDef test_methods[] = {
138165
{"mapping_getoptionalitem", mapping_getoptionalitem, METH_VARARGS},
139166
{"mapping_getoptionalitemstring", mapping_getoptionalitemstring, METH_VARARGS},
140167

168+
{"PyIter_Next", pyiter_next, METH_O},
169+
{"PyIter_NextItem", pyiter_nextitem, METH_O},
141170
{NULL},
142171
};
143172

Objects/abstract.c

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2881,7 +2881,50 @@ PyAIter_Check(PyObject *obj)
28812881
tp->tp_as_async->am_anext != &_PyObject_NextNotImplemented);
28822882
}
28832883

2884+
static int
2885+
iternext(PyObject *iter, PyObject **item)
2886+
{
2887+
iternextfunc tp_iternext = Py_TYPE(iter)->tp_iternext;
2888+
if ((*item = tp_iternext(iter))) {
2889+
return 1;
2890+
}
2891+
2892+
PyThreadState *tstate = _PyThreadState_GET();
2893+
/* When the iterator is exhausted it must return NULL;
2894+
* a StopIteration exception may or may not be set. */
2895+
if (!_PyErr_Occurred(tstate)) {
2896+
return 0;
2897+
}
2898+
if (_PyErr_ExceptionMatches(tstate, PyExc_StopIteration)) {
2899+
_PyErr_Clear(tstate);
2900+
return 0;
2901+
}
2902+
2903+
/* Error case: an exception (different than StopIteration) is set. */
2904+
return -1;
2905+
}
2906+
2907+
/* Return 1 and set 'item' to the next item of 'iter' on success.
2908+
* Return 0 and set 'item' to NULL when there are no remaining values.
2909+
* Return -1, set 'item' to NULL and set an exception on error.
2910+
*/
2911+
int
2912+
PyIter_NextItem(PyObject *iter, PyObject **item)
2913+
{
2914+
assert(iter != NULL);
2915+
assert(item != NULL);
2916+
2917+
if (Py_TYPE(iter)->tp_iternext == NULL) {
2918+
*item = NULL;
2919+
PyErr_Format(PyExc_TypeError, "expected an iterator, got '%T'", iter);
2920+
return -1;
2921+
}
2922+
2923+
return iternext(iter, item);
2924+
}
2925+
28842926
/* Return next item.
2927+
*
28852928
* If an error occurs, return NULL. PyErr_Occurred() will be true.
28862929
* If the iteration terminates normally, return NULL and clear the
28872930
* PyExc_StopIteration exception (if it was set). PyErr_Occurred()
@@ -2891,17 +2934,9 @@ PyAIter_Check(PyObject *obj)
28912934
PyObject *
28922935
PyIter_Next(PyObject *iter)
28932936
{
2894-
PyObject *result;
2895-
result = (*Py_TYPE(iter)->tp_iternext)(iter);
2896-
if (result == NULL) {
2897-
PyThreadState *tstate = _PyThreadState_GET();
2898-
if (_PyErr_Occurred(tstate)
2899-
&& _PyErr_ExceptionMatches(tstate, PyExc_StopIteration))
2900-
{
2901-
_PyErr_Clear(tstate);
2902-
}
2903-
}
2904-
return result;
2937+
PyObject *item;
2938+
(void)iternext(iter, &item);
2939+
return item;
29052940
}
29062941

29072942
PySendResult

PC/python3dll.c

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
0