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

Skip to content

Commit 217707e

Browse files
erlend-aaslandblhsing
authored andcommitted
pythongh-105201: Add PyIter_NextItem() (python#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 5890a6c commit 217707e

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

0 commit comments

Comments
 (0)
0