diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst
index f45ab397e936938..ddfda5445270338 100644
--- a/Doc/library/functions.rst
+++ b/Doc/library/functions.rst
@@ -65,14 +65,49 @@ are always available. They are listed here in alphabetical order.
.. function:: aiter(async_iterable, /)
+ aiter(callable, /, stop_value, *, stop_exception=StopAsyncIteration)
+ aiter(callable, /, *, stop_exception)
Return an :term:`asynchronous iterator` for an :term:`asynchronous iterable`.
Equivalent to calling ``x.__aiter__()``.
- Note: Unlike :func:`iter`, :func:`aiter` has no 2-argument variant.
+ If *stop_value* or *stop_exception* is given,
+ then the first argument must be a callable object.
+ The asynchronous iterator created in this case
+ calls *callable* with no arguments and awaits the result
+ for each call to its :meth:`~object.__anext__` method;
+ if the awaited value is equal to *stop_value*,
+ or if the call raises an exception matching *stop_exception*,
+ :exc:`StopAsyncIteration` will be raised,
+ otherwise the value will be returned.
+ The callable is only called when the result of :meth:`~object.__anext__`
+ is awaited.
+
+ *stop_exception* is an exception class or a tuple of exception classes.
+ If *stop_value* is not specified,
+ the iteration stops only when the callable raises an exception.
+ If the callable raises :exc:`StopAsyncIteration` which does not match
+ *stop_exception*, it is replaced with a :exc:`RuntimeError`,
+ as for asynchronous generators (see :pep:`525`).
+
+ For example, reading fixed-size chunks from an asynchronous stream
+ until the end of file is reached::
+
+ from functools import partial
+ async for chunk in aiter(partial(reader.read, 1024), b''):
+ process_chunk(chunk)
+
+ Or consuming an :class:`asyncio.Queue` until it is shut down::
+
+ from asyncio import QueueShutDown
+ async for item in aiter(queue.get, stop_exception=QueueShutDown):
+ process_item(item)
.. versionadded:: 3.10
+ .. versionchanged:: next
+ Added the *stop_value* and *stop_exception* parameters.
+
.. function:: all(iterable, /)
Return ``True`` if all elements of the *iterable* are true (or if the iterable
@@ -1143,21 +1178,31 @@ are always available. They are listed here in alphabetical order.
.. function:: iter(iterable, /)
- iter(callable, sentinel, /)
+ iter(callable, /, stop_value, *, stop_exception=StopIteration)
+ iter(callable, /, *, stop_exception)
Return an :term:`iterator` object. The first argument is interpreted very
- differently depending on the presence of the second argument. Without a
- second argument, the single argument must be a collection object which supports the
+ differently depending on the presence of the other arguments. Without other
+ arguments, the single argument must be a collection object which supports the
:term:`iterable` protocol (the :meth:`~object.__iter__` method),
or it must support
the sequence protocol (the :meth:`~object.__getitem__` method with integer arguments
starting at ``0``). If it does not support either of those protocols,
- :exc:`TypeError` is raised. If the second argument, *sentinel*, is given,
+ :exc:`TypeError` is raised.
+
+ If *stop_value* or *stop_exception* is given,
then the first argument must be a callable object. The iterator created in this case
will call *callable* with no arguments for each call to its
:meth:`~iterator.__next__` method; if the value returned is equal to
- *sentinel*, :exc:`StopIteration` will be raised, otherwise the value will
- be returned.
+ *stop_value*, or if the call raises an exception matching *stop_exception*,
+ :exc:`StopIteration` will be raised, otherwise the value will be returned.
+
+ *stop_exception* is an exception class or a tuple of exception classes.
+ If *stop_value* is not specified,
+ the iteration stops only when the callable raises an exception.
+ If the callable raises :exc:`StopIteration` which does not match
+ *stop_exception*, it is replaced with a :exc:`RuntimeError`,
+ as for generators (see :pep:`479`).
See also :ref:`typeiter`.
@@ -1170,6 +1215,17 @@ are always available. They are listed here in alphabetical order.
for block in iter(partial(f.read, 64), b''):
process_block(block)
+ *stop_exception* is useful for callables which report exhaustion by raising an
+ exception instead of returning a special value.
+ For example, draining a queue::
+
+ from queue import Empty
+ for item in iter(queue.get_nowait, stop_exception=Empty):
+ process_item(item)
+
+ .. versionchanged:: next
+ Added the *stop_exception* parameter and allowed passing *stop_value* by keyword.
+
.. function:: len(object, /)
diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index a1a8415482b97aa..aa8b1633d11ee43 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -75,6 +75,13 @@ New features
Other language changes
======================
+* The :func:`iter` function now accepts the *stop_exception* parameter.
+ The created iterator stops when the callable raises the specified exception.
+ The second parameter is now named *stop_value* and can be passed by keyword.
+ :func:`aiter` now accepts the same *stop_value* and *stop_exception*
+ parameters, calling an asynchronous callable and awaiting the result.
+ (Contributed by Serhiy Storchaka in :gh:`64862`.)
+
* :meth:`memoryview.cast` now allows casting a multidimensional
F-contiguous view to a one-dimensional view.
(Contributed by Jaemin Park in :gh:`91484`.)
diff --git a/Include/internal/pycore_genobject.h b/Include/internal/pycore_genobject.h
index c86ae242feac1ed..266add0fb7a9c34 100644
--- a/Include/internal/pycore_genobject.h
+++ b/Include/internal/pycore_genobject.h
@@ -29,6 +29,9 @@ PyAPI_FUNC(int) _PyGen_SetStopIterationValue(PyObject *);
// Export for '_asyncio' shared extension
PyAPI_FUNC(int) _PyGen_FetchStopIterationValue(PyObject **);
+// Set the exception passed to throw(typ[, val[, tb]]).
+// Return 0 on success, -1 on failure.
+extern int _PyGen_SetException(PyObject *typ, PyObject *val, PyObject *tb);
PyAPI_FUNC(PyObject *)_PyCoro_GetAwaitableIter(PyObject *o);
PyAPI_FUNC(PyObject *)_PyAsyncGenValueWrapperNew(PyThreadState *state, PyObject *);
diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h
index c982f76431c52d1..7b869d16b81ac2f 100644
--- a/Include/internal/pycore_global_objects_fini_generated.h
+++ b/Include/internal/pycore_global_objects_fini_generated.h
@@ -2104,6 +2104,8 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(stdout));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(step));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(steps));
+ _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(stop_exception));
+ _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(stop_value));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(store_name));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(strategy));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(strftime));
diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h
index c430d0fb99bc41d..6e7835f2aa7a0b3 100644
--- a/Include/internal/pycore_global_strings.h
+++ b/Include/internal/pycore_global_strings.h
@@ -827,6 +827,8 @@ struct _Py_global_strings {
STRUCT_FOR_ID(stdout)
STRUCT_FOR_ID(step)
STRUCT_FOR_ID(steps)
+ STRUCT_FOR_ID(stop_exception)
+ STRUCT_FOR_ID(stop_value)
STRUCT_FOR_ID(store_name)
STRUCT_FOR_ID(strategy)
STRUCT_FOR_ID(strftime)
diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h
index 0623adce693d465..6c907e0cf79894d 100644
--- a/Include/internal/pycore_interp_structs.h
+++ b/Include/internal/pycore_interp_structs.h
@@ -538,7 +538,7 @@ struct _py_func_state {
If you add a new static type to the standard library, you may have to
update one of these numbers.
*/
-#define _Py_NUM_MANAGED_PREINITIALIZED_TYPES 120
+#define _Py_NUM_MANAGED_PREINITIALIZED_TYPES 122
#define _Py_MAX_MANAGED_STATIC_BUILTIN_TYPES \
(_Py_NUM_MANAGED_PREINITIALIZED_TYPES + 83)
#define _Py_MAX_MANAGED_STATIC_EXT_TYPES 10
diff --git a/Include/internal/pycore_iterobject.h b/Include/internal/pycore_iterobject.h
new file mode 100644
index 000000000000000..90b444976e2f19c
--- /dev/null
+++ b/Include/internal/pycore_iterobject.h
@@ -0,0 +1,28 @@
+#ifndef Py_INTERNAL_ITEROBJECT_H
+#define Py_INTERNAL_ITEROBJECT_H
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef Py_BUILD_CORE
+# error "this header requires Py_BUILD_CORE define"
+#endif
+
+extern PyTypeObject _PyACallIter_Type;
+extern PyTypeObject _PyACallIterAwaitable_Type;
+
+// Like PyCallIter_New(), but the iteration also stops when *callable* raises
+// an exception matching *stop_exc* (an exception class or a tuple of exception
+// classes). *sentinel* can be NULL; NULL *stop_exc* means StopIteration.
+extern PyObject *_PyCallIter_NewEx(PyObject *callable, PyObject *sentinel,
+ PyObject *stop_exc);
+
+// The asynchronous counterpart of _PyCallIter_NewEx(): the result of
+// *callable* is awaited, and NULL *stop_exc* means StopAsyncIteration.
+extern PyObject *_PyACallIter_New(PyObject *callable, PyObject *sentinel,
+ PyObject *stop_exc);
+
+#ifdef __cplusplus
+}
+#endif
+#endif /* !Py_INTERNAL_ITEROBJECT_H */
diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h
index 5a194478f562314..730c5e5ae94c766 100644
--- a/Include/internal/pycore_runtime_init_generated.h
+++ b/Include/internal/pycore_runtime_init_generated.h
@@ -2102,6 +2102,8 @@ extern "C" {
INIT_ID(stdout), \
INIT_ID(step), \
INIT_ID(steps), \
+ INIT_ID(stop_exception), \
+ INIT_ID(stop_value), \
INIT_ID(store_name), \
INIT_ID(strategy), \
INIT_ID(strftime), \
diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h
index 25529c5ca713a95..13cc349358b5479 100644
--- a/Include/internal/pycore_unicodeobject_generated.h
+++ b/Include/internal/pycore_unicodeobject_generated.h
@@ -3088,6 +3088,14 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
assert(PyUnicode_GET_LENGTH(string) != 1);
+ string = &_Py_ID(stop_exception);
+ _PyUnicode_InternStatic(interp, &string);
+ assert(_PyUnicode_CheckConsistency(string, 1));
+ assert(PyUnicode_GET_LENGTH(string) != 1);
+ string = &_Py_ID(stop_value);
+ _PyUnicode_InternStatic(interp, &string);
+ assert(_PyUnicode_CheckConsistency(string, 1));
+ assert(PyUnicode_GET_LENGTH(string) != 1);
string = &_Py_ID(store_name);
_PyUnicode_InternStatic(interp, &string);
assert(_PyUnicode_CheckConsistency(string, 1));
diff --git a/Lib/test/test_asyncgen.py b/Lib/test/test_asyncgen.py
index 70a285dd91f385f..cdae58b3e89ae36 100644
--- a/Lib/test/test_asyncgen.py
+++ b/Lib/test/test_asyncgen.py
@@ -789,6 +789,164 @@ async def gen():
applied_twice = aiter(applied_once)
self.assertIs(applied_once, applied_twice)
+ def make_counter(self):
+ state = {'n': 0}
+ async def counter():
+ state['n'] += 1
+ return state['n']
+ return counter
+
+ def collect(self, ait):
+ async def consume():
+ return [i async for i in ait]
+ return self.loop.run_until_complete(consume())
+
+ def test_aiter_callable_stop(self):
+ self.assertEqual(self.collect(aiter(self.make_counter(), 4)), [1, 2, 3])
+ self.assertEqual(self.collect(aiter(self.make_counter(), stop_value=4)),
+ [1, 2, 3])
+
+ def test_aiter_callable_stop_exception(self):
+ counter = self.make_counter()
+ async def spam():
+ value = await counter()
+ if value > 3:
+ raise LookupError
+ return value
+ self.assertEqual(self.collect(aiter(spam, stop_exception=LookupError)),
+ [1, 2, 3])
+ counter = self.make_counter()
+ self.assertEqual(
+ self.collect(aiter(spam, stop_exception=(ZeroDivisionError,
+ LookupError))),
+ [1, 2, 3])
+
+ def test_aiter_callable_stop_and_exception(self):
+ counter = self.make_counter()
+ async def spam():
+ value = await counter()
+ if value > 5:
+ raise LookupError
+ return value
+ self.assertEqual(
+ self.collect(aiter(spam, 3, stop_exception=LookupError)), [1, 2])
+ counter = self.make_counter()
+ self.assertEqual(
+ self.collect(aiter(spam, 100, stop_exception=LookupError)),
+ [1, 2, 3, 4, 5])
+
+ def test_aiter_callable_stop_async_iteration(self):
+ # StopAsyncIteration is the default stop exception
+ counter = self.make_counter()
+ async def spam():
+ value = await counter()
+ if value > 3:
+ raise StopAsyncIteration
+ return value
+ self.assertEqual(
+ self.collect(aiter(spam, stop_exception=StopAsyncIteration)),
+ [1, 2, 3])
+
+ def test_aiter_callable_leak_from_await(self):
+ # A StopAsyncIteration leaking from the await is replaced with
+ # RuntimeError (see PEP 525)
+ async def spam():
+ raise StopAsyncIteration
+ it = aiter(spam, 10, stop_exception=LookupError)
+ with self.assertRaisesRegex(RuntimeError,
+ 'callable raised StopAsyncIteration') as cm:
+ self.loop.run_until_complete(anext(it))
+ self.assertIsInstance(cm.exception.__cause__, StopAsyncIteration)
+ # but if it matches stop_exception, it stops the iteration
+ it = aiter(spam, 10, stop_exception=(LookupError, StopAsyncIteration))
+ with self.assertRaises(StopAsyncIteration):
+ self.loop.run_until_complete(anext(it))
+
+ def test_aiter_callable_leak_from_call(self):
+ # StopIteration and StopAsyncIteration leaking from the call are
+ # replaced with RuntimeError (see PEP 525)
+ for exc in StopIteration, StopAsyncIteration:
+ with self.subTest(exc=exc):
+ def spam():
+ raise exc
+ it = aiter(spam, 10, stop_exception=LookupError)
+ with self.assertRaisesRegex(
+ RuntimeError, f'callable raised {exc.__name__}') as cm:
+ self.loop.run_until_complete(anext(it))
+ self.assertIsInstance(cm.exception.__cause__, exc)
+ # but if it matches stop_exception, it stops the iteration
+ it = aiter(spam, 10, stop_exception=(LookupError, exc))
+ with self.assertRaises(StopAsyncIteration):
+ self.loop.run_until_complete(anext(it))
+
+ def test_aiter_callable_other_exception(self):
+ async def spam():
+ raise ZeroDivisionError
+ it = aiter(spam, stop_exception=LookupError)
+ with self.assertRaises(ZeroDivisionError):
+ self.loop.run_until_complete(anext(it))
+
+ def test_aiter_callable_exhausted(self):
+ it = aiter(self.make_counter(), 3)
+ self.assertEqual(self.collect(it), [1, 2])
+ self.assertEqual(self.loop.run_until_complete(anext(it, 'default')),
+ 'default')
+ with self.assertRaises(StopAsyncIteration):
+ self.loop.run_until_complete(anext(it))
+
+ def test_aiter_callable_lazy(self):
+ # The callable is only called when the awaitable is awaited
+ calls = []
+ async def spam():
+ calls.append(1)
+ return len(calls)
+ it = aiter(spam, 10)
+ awaitable = it.__anext__()
+ self.assertEqual(calls, [])
+ self.assertEqual(self.loop.run_until_complete(awaitable), 1)
+ self.assertEqual(calls, [1])
+
+ def test_aiter_callable_awaitable(self):
+ it = aiter(self.make_counter(), 10)
+ awaitable = it.__anext__()
+ self.assertIsNone(awaitable.close())
+ with self.assertRaises(RuntimeError):
+ self.loop.run_until_complete(awaitable)
+ awaitable = it.__anext__()
+ with self.assertRaises(KeyError):
+ awaitable.throw(KeyError('injected'))
+
+ def test_aiter_callable_cancel(self):
+ # Cancellation is delivered to the awaited callable result
+ cancelled = []
+ async def spam():
+ try:
+ await asyncio.sleep(10)
+ except asyncio.CancelledError:
+ cancelled.append(1)
+ raise
+ async def consume():
+ async for _ in aiter(spam, None):
+ pass
+ async def main():
+ task = asyncio.ensure_future(consume())
+ await asyncio.sleep(0)
+ task.cancel()
+ with self.assertRaises(asyncio.CancelledError):
+ await task
+ self.loop.run_until_complete(main())
+ self.assertEqual(cancelled, [1])
+
+ def test_aiter_callable_errors(self):
+ async def gen():
+ yield 1
+ self.assertRaises(TypeError, aiter, gen(), 1)
+ self.assertRaises(TypeError, aiter, [1, 2], stop_exception=LookupError)
+ self.assertRaises(TypeError, aiter, len, stop_exception=42)
+ self.assertRaises(TypeError, aiter, len,
+ stop_exception=(LookupError, 42))
+ self.assertRaises(TypeError, aiter, len, stop_exception=LookupError())
+
def test_anext_bad_args(self):
async def gen():
yield 1
diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py
index c68c643cb97fb45..9e42f883c1f3f42 100644
--- a/Lib/test/test_inspect/test_inspect.py
+++ b/Lib/test/test_inspect/test_inspect.py
@@ -6171,10 +6171,12 @@ def test_builtins_have_signatures(self):
'dict', 'frozendict', 'int', 'str'}
# These need PEP 457 groups
needs_groups = {"range", "slice", "dir", "getattr",
- "next", "iter", "vars"}
+ "next", "vars"}
no_signature |= needs_groups
# These have unrepresentable parameter default values of NULL
unsupported_signature = {"anext"}
+ # These have text signatures with PEP 457 groups
+ unsupported_signature |= {"aiter", "iter"}
# These need *args support in Argument Clinic
needs_varargs = {"min", "max", "__build_class__"}
no_signature |= needs_varargs
diff --git a/Lib/test/test_iter.py b/Lib/test/test_iter.py
index 18e4b676c532368..0de501e94917bac 100644
--- a/Lib/test/test_iter.py
+++ b/Lib/test/test_iter.py
@@ -350,6 +350,97 @@ def spam(state=[0]):
return i
self.check_iterator(iter(spam, 20), list(range(10)), pickle=False)
+ # Test iter() with the stop value passed by keyword
+ def test_iter_keyword_stop(self):
+ self.check_iterator(iter(CallableIterClass(), stop_value=10), list(range(10)))
+
+ # Test iter() with the exception argument
+ def test_iter_exception(self):
+ self.check_iterator(iter(CallableIterClass(), stop_exception=IndexError),
+ list(range(101)))
+
+ def test_iter_exception_tuple(self):
+ self.check_iterator(
+ iter(CallableIterClass(), stop_exception=(ZeroDivisionError, IndexError)),
+ list(range(101)))
+
+ # Test iter() with both the stop value and the exception argument
+ def test_iter_exception_and_stop(self):
+ self.check_iterator(iter(CallableIterClass(), 10, stop_exception=IndexError),
+ list(range(10)))
+ self.check_iterator(iter(CallableIterClass(), 200, stop_exception=IndexError),
+ list(range(101)))
+
+ # A leaking StopIteration is replaced with RuntimeError (see PEP 479)
+ def test_iter_exception_stop_iteration_leak(self):
+ def spam():
+ raise StopIteration
+ it = iter(spam, stop_exception=IndexError)
+ with self.assertRaisesRegex(RuntimeError,
+ 'callable raised StopIteration') as cm:
+ next(it)
+ self.assertIsInstance(cm.exception.__cause__, StopIteration)
+ # but if it matches stop_exception, it stops the iteration
+ it = iter(spam, stop_exception=(IndexError, StopIteration))
+ self.assertRaises(StopIteration, next, it)
+
+ # Other exceptions are propagated
+ def test_iter_exception_not_matching(self):
+ def spam():
+ raise ZeroDivisionError
+ it = iter(spam, stop_exception=IndexError)
+ self.assertRaises(ZeroDivisionError, next, it)
+
+ def test_iter_exception_errors(self):
+ self.assertRaises(TypeError, iter, [1, 2], stop_exception=IndexError)
+ self.assertRaises(TypeError, iter, len, stop_exception=42)
+ self.assertRaises(TypeError, iter, len, stop_exception=(IndexError, 42))
+ self.assertRaises(TypeError, iter, len, stop_exception=IndexError())
+
+ # StopIteration is the default stop exception
+ def test_iter_exception_stop_iteration(self):
+ def spam(state=[0]):
+ i = state[0]
+ if i == 10:
+ raise StopIteration
+ state[0] = i+1
+ return i
+ self.check_iterator(iter(spam, stop_exception=StopIteration),
+ list(range(10)), pickle=False)
+
+ def test_calliter_reduce(self):
+ c = CallableIterClass()
+ # The form without the stop exception is pickled as iter(c, stop)
+ self.assertEqual(iter(c, 10).__reduce__(), (iter, (c, 10)))
+ self.assertEqual(iter(c, 10, stop_exception=StopIteration).__reduce__(),
+ (iter, (c, 10)))
+ self.assertEqual(iter(c, 10, stop_exception=()).__reduce__(),
+ (iter, (c, None), ((10,), ())))
+ self.assertEqual(iter(c, stop_exception=StopIteration).__reduce__(),
+ (iter, (c, None), ((), StopIteration)))
+ self.assertEqual(iter(c, stop_exception=IndexError).__reduce__(),
+ (iter, (c, None), ((), IndexError)))
+ self.assertEqual(iter(c, 10, stop_exception=IndexError).__reduce__(),
+ (iter, (c, None), ((10,), IndexError)))
+
+ def test_calliter_setstate(self):
+ c = CallableIterClass()
+ it = iter(c, stop_exception=IndexError)
+ self.assertRaises(TypeError, it.__setstate__, 42)
+ self.assertRaises(TypeError, it.__setstate__, ((), IndexError, ()))
+ self.assertRaises(TypeError, it.__setstate__, ([], IndexError))
+ self.assertRaises(TypeError, it.__setstate__, ((1, 2), IndexError))
+ self.assertRaises(TypeError, it.__setstate__, ((), 42))
+ self.assertRaises(TypeError, it.__setstate__, ((), None))
+ it.__setstate__(((10,), StopIteration))
+ self.assertEqual(it.__reduce__(), (iter, (c, 10)))
+ it.__setstate__(((10,), ()))
+ self.assertEqual(it.__reduce__(), (iter, (c, None), ((10,), ())))
+ it.__setstate__(((), IndexError))
+ self.assertEqual(it.__reduce__(), (iter, (c, None), ((), IndexError)))
+ it.__setstate__(((10,), StopIteration))
+ self.assertEqual(list(it), list(range(10)))
+
def test_iter_function_concealing_reentrant_exhaustion(self):
# gh-101892: Test two-argument iter() with a function that
# exhausts its associated iterator but forgets to either return
diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py
index f2adce532595e70..4308de227a46ce4 100644
--- a/Lib/test/test_sys.py
+++ b/Lib/test/test_sys.py
@@ -1726,7 +1726,7 @@ def get_gen(): yield 1
check(iter('abc'), size('lP'))
# callable-iterator
import re
- check(re.finditer('',''), size('2P'))
+ check(re.finditer('',''), size('3P'))
# list
check(list([]), vsize('Pn'))
check(list([1]), vsize('Pn') + 2*self.P)
diff --git a/Makefile.pre.in b/Makefile.pre.in
index d3d24a13898d992..6a50ef51394f560 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -1357,6 +1357,7 @@ PYTHON_HEADERS= \
$(srcdir)/Include/internal/pycore_interpframe_structs.h \
$(srcdir)/Include/internal/pycore_interpolation.h \
$(srcdir)/Include/internal/pycore_intrinsics.h \
+ $(srcdir)/Include/internal/pycore_iterobject.h \
$(srcdir)/Include/internal/pycore_jit.h \
$(srcdir)/Include/internal/pycore_lazyimportobject.h \
$(srcdir)/Include/internal/pycore_list.h \
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-23-21-30-00.gh-issue-64862.Kx7Qm2.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-23-21-30-00.gh-issue-64862.Kx7Qm2.rst
new file mode 100644
index 000000000000000..61a42300994aa80
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-23-21-30-00.gh-issue-64862.Kx7Qm2.rst
@@ -0,0 +1,5 @@
+The :func:`iter` function now accepts the *stop_exception* parameter.
+The created iterator stops when the callable raises the specified exception.
+The second parameter is now named *stop_value* and can be passed by keyword.
+:func:`aiter` now accepts the same *stop_value* and *stop_exception*
+parameters, calling an asynchronous callable and awaiting the result.
diff --git a/Objects/genobject.c b/Objects/genobject.c
index 6529a66fc35a6b4..6a96bc27d9a950d 100644
--- a/Objects/genobject.c
+++ b/Objects/genobject.c
@@ -542,8 +542,8 @@ gen_close(PyObject *self, PyObject *args)
// Set an exception for a gen.throw() call.
// Return 0 on success, -1 on failure.
-static int
-gen_set_exception(PyObject *typ, PyObject *val, PyObject *tb)
+int
+_PyGen_SetException(PyObject *typ, PyObject *val, PyObject *tb)
{
/* First, check the traceback argument, replacing None with
NULL. */
@@ -640,7 +640,7 @@ _gen_throw(PyGenObject *gen, int close_on_genexit,
"cannot reuse already awaited coroutine");
return NULL;
}
- gen_set_exception(typ, val, tb);
+ _PyGen_SetException(typ, val, tb);
return NULL;
}
@@ -718,7 +718,7 @@ _gen_throw(PyGenObject *gen, int close_on_genexit,
throw_here:
assert(FT_ATOMIC_LOAD_INT8_RELAXED(gen->gi_frame_state) == FRAME_EXECUTING);
- if (gen_set_exception(typ, val, tb) < 0) {
+ if (_PyGen_SetException(typ, val, tb) < 0) {
FT_ATOMIC_STORE_INT8_RELEASE(gen->gi_frame_state, frame_state);
return NULL;
}
diff --git a/Objects/iterobject.c b/Objects/iterobject.c
index e323987601d5d4c..5a5ca8d5e1f9b8c 100644
--- a/Objects/iterobject.c
+++ b/Objects/iterobject.c
@@ -5,7 +5,10 @@
#include "pycore_call.h" // _PyObject_CallNoArgs()
#include "pycore_ceval.h" // _PyEval_GetBuiltin()
#include "pycore_genobject.h" // _PyCoro_GetAwaitableIter()
+#include "pycore_iterobject.h" // _PyCallIter_NewEx()
#include "pycore_object.h" // _PyObject_GC_TRACK()
+#include "pycore_pyerrors.h" // _PyErr_FormatFromCause()
+#include "pycore_pystate.h" // _PyThreadState_GET()
typedef struct {
@@ -185,22 +188,38 @@ PyTypeObject PySeqIter_Type = {
typedef struct {
PyObject_HEAD
- PyObject *it_callable; /* Set to NULL when iterator is exhausted */
- PyObject *it_sentinel; /* Set to NULL when iterator is exhausted */
+ /* Both are set to NULL when the iterator is exhausted */
+ PyObject *it_callable;
+ PyObject *it_sentinel; /* can be NULL */
+ PyObject *it_stop_exc; /* never NULL */
} calliterobject;
PyObject *
-PyCallIter_New(PyObject *callable, PyObject *sentinel)
+_PyCallIter_NewEx(PyObject *callable, PyObject *sentinel, PyObject *stop_exc)
{
calliterobject *it;
+ if (stop_exc == NULL) {
+ stop_exc = PyExc_StopIteration;
+ }
+ else if (_PyEval_CheckExceptTypeValid(_PyThreadState_GET(), stop_exc) < 0) {
+ return NULL;
+ }
it = PyObject_GC_New(calliterobject, &PyCallIter_Type);
if (it == NULL)
return NULL;
it->it_callable = Py_NewRef(callable);
- it->it_sentinel = Py_NewRef(sentinel);
+ it->it_sentinel = Py_XNewRef(sentinel);
+ it->it_stop_exc = Py_NewRef(stop_exc);
_PyObject_GC_TRACK(it);
return (PyObject *)it;
}
+
+PyObject *
+PyCallIter_New(PyObject *callable, PyObject *sentinel)
+{
+ return _PyCallIter_NewEx(callable, sentinel, NULL);
+}
+
static void
calliter_dealloc(PyObject *op)
{
@@ -208,6 +227,7 @@ calliter_dealloc(PyObject *op)
_PyObject_GC_UNTRACK(it);
Py_XDECREF(it->it_callable);
Py_XDECREF(it->it_sentinel);
+ Py_XDECREF(it->it_stop_exc);
PyObject_GC_Del(it);
}
@@ -217,6 +237,7 @@ calliter_traverse(PyObject *op, visitproc visit, void *arg)
calliterobject *it = (calliterobject*)op;
Py_VISIT(it->it_callable);
Py_VISIT(it->it_sentinel);
+ Py_VISIT(it->it_stop_exc);
return 0;
}
@@ -231,10 +252,12 @@ calliter_iternext(PyObject *op)
}
result = _PyObject_CallNoArgs(it->it_callable);
- if (result != NULL && it->it_sentinel != NULL){
- int ok;
-
- ok = PyObject_RichCompareBool(it->it_sentinel, result, Py_EQ);
+ /* The call can exhaust the iterator re-entrantly. */
+ if (result != NULL && it->it_callable != NULL) {
+ if (it->it_sentinel == NULL) {
+ return result; /* Common case, fast path */
+ }
+ int ok = PyObject_RichCompareBool(it->it_sentinel, result, Py_EQ);
if (ok == 0) {
return result; /* Common case, fast path */
}
@@ -244,11 +267,16 @@ calliter_iternext(PyObject *op)
Py_CLEAR(it->it_sentinel);
}
}
- else if (PyErr_ExceptionMatches(PyExc_StopIteration)) {
+ else if (PyErr_ExceptionMatches(it->it_stop_exc)) {
PyErr_Clear();
Py_CLEAR(it->it_callable);
Py_CLEAR(it->it_sentinel);
}
+ else if (PyErr_ExceptionMatches(PyExc_StopIteration)) {
+ /* It would be mistaken for the end of the iteration (see PEP 479). */
+ _PyErr_FormatFromCause(PyExc_RuntimeError,
+ "callable raised StopIteration");
+ }
Py_XDECREF(result);
return NULL;
}
@@ -263,14 +291,57 @@ calliter_reduce(PyObject *op, PyObject *Py_UNUSED(ignored))
* call must be before access of iterator pointers.
* see issue #101765 */
- if (it->it_callable != NULL && it->it_sentinel != NULL)
- return Py_BuildValue("N(OO)", iter, it->it_callable, it->it_sentinel);
- else
+ if (it->it_callable == NULL) {
return Py_BuildValue("N(())", iter);
+ }
+ /* Only the sentinel can be passed as an argument of iter(), so other
+ attributes are restored from the state (see calliter_setstate()). */
+ if (it->it_sentinel == NULL) {
+ return Py_BuildValue("N(OO)(()O)", iter, it->it_callable, Py_None,
+ it->it_stop_exc);
+ }
+ else if (it->it_stop_exc == PyExc_StopIteration) {
+ return Py_BuildValue("N(OO)", iter, it->it_callable, it->it_sentinel);
+ }
+ else {
+ return Py_BuildValue("N(OO)((O)O)", iter, it->it_callable, Py_None,
+ it->it_sentinel, it->it_stop_exc);
+ }
+}
+
+static PyObject *
+calliter_setstate(PyObject *op, PyObject *state)
+{
+ calliterobject *it = (calliterobject*)op;
+ PyObject *sentinel, *stop_exc;
+
+ if (!PyTuple_Check(state) || PyTuple_GET_SIZE(state) != 2) {
+ goto error;
+ }
+ sentinel = PyTuple_GET_ITEM(state, 0);
+ stop_exc = PyTuple_GET_ITEM(state, 1);
+ if (!PyTuple_Check(sentinel) || PyTuple_GET_SIZE(sentinel) > 1) {
+ goto error;
+ }
+ if (_PyEval_CheckExceptTypeValid(_PyThreadState_GET(), stop_exc) < 0) {
+ return NULL;
+ }
+ if (it->it_callable != NULL) {
+ Py_XSETREF(it->it_sentinel,
+ PyTuple_GET_SIZE(sentinel) ?
+ Py_NewRef(PyTuple_GET_ITEM(sentinel, 0)) : NULL);
+ Py_SETREF(it->it_stop_exc, Py_NewRef(stop_exc));
+ }
+ Py_RETURN_NONE;
+
+error:
+ PyErr_SetString(PyExc_TypeError, "invalid state for callable_iterator");
+ return NULL;
}
static PyMethodDef calliter_methods[] = {
{"__reduce__", calliter_reduce, METH_NOARGS, reduce_doc},
+ {"__setstate__", calliter_setstate, METH_O, setstate_doc},
{NULL, NULL} /* sentinel */
};
@@ -337,10 +408,10 @@ anextawaitable_traverse(PyObject *op, visitproc visit, void *arg)
}
static PyObject *
-anextawaitable_getiter(anextawaitableobject *obj)
+awaitable_getiter(PyObject *owner, PyObject *wrapped)
{
- assert(obj->wrapped != NULL);
- PyObject *awaitable = _PyCoro_GetAwaitableIter(obj->wrapped);
+ assert(wrapped != NULL);
+ PyObject *awaitable = _PyCoro_GetAwaitableIter(wrapped);
if (awaitable == NULL) {
return NULL;
}
@@ -359,7 +430,7 @@ anextawaitable_getiter(anextawaitableobject *obj)
if (!PyIter_Check(awaitable)) {
PyErr_Format(PyExc_TypeError,
"%T.__await__() must return an iterable, not %T",
- obj, awaitable);
+ owner, awaitable);
Py_DECREF(awaitable);
return NULL;
}
@@ -391,7 +462,7 @@ anextawaitable_iternext(PyObject *op)
* gen.__anext__().__next__()
*/
anextawaitableobject *obj = anextawaitableobject_CAST(op);
- PyObject *awaitable = anextawaitable_getiter(obj);
+ PyObject *awaitable = awaitable_getiter(op, obj->wrapped);
if (awaitable == NULL) {
return NULL;
}
@@ -411,7 +482,7 @@ anextawaitable_iternext(PyObject *op)
static PyObject *
anextawaitable_proxy(anextawaitableobject *obj, char *meth, PyObject *arg)
{
- PyObject *awaitable = anextawaitable_getiter(obj);
+ PyObject *awaitable = awaitable_getiter((PyObject *)obj, obj->wrapped);
if (awaitable == NULL) {
return NULL;
}
@@ -540,3 +611,362 @@ PyAnextAwaitable_New(PyObject *awaitable, PyObject *default_value)
_PyObject_GC_TRACK(anext);
return (PyObject *)anext;
}
+
+
+/* -------------------------------------- */
+
+/* The asynchronous counterpart of calliterobject: the callable is called
+ and its result is awaited for every __anext__(). */
+
+typedef struct {
+ PyObject_HEAD
+ /* Both are set to NULL when the iterator is exhausted */
+ PyObject *it_callable;
+ PyObject *it_sentinel; /* can be NULL */
+ PyObject *it_stop_exc; /* never NULL */
+} acalliterobject;
+
+#define acalliterobject_CAST(op) ((acalliterobject *)(op))
+
+/* The awaitable returned by acalliter_anext(). The callable is only
+ called when this object is awaited. */
+typedef struct {
+ PyObject_HEAD
+ PyObject *aw_iterator; /* the iterator which created this object */
+ PyObject *aw_wrapped; /* the awaitable returned by the callable */
+ char aw_closed;
+} acallawaitableobject;
+
+#define acallawaitableobject_CAST(op) ((acallawaitableobject *)(op))
+
+PyObject *
+_PyACallIter_New(PyObject *callable, PyObject *sentinel, PyObject *stop_exc)
+{
+ if (stop_exc == NULL) {
+ stop_exc = PyExc_StopAsyncIteration;
+ }
+ else if (_PyEval_CheckExceptTypeValid(_PyThreadState_GET(), stop_exc) < 0) {
+ return NULL;
+ }
+ acalliterobject *it = PyObject_GC_New(acalliterobject, &_PyACallIter_Type);
+ if (it == NULL) {
+ return NULL;
+ }
+ it->it_callable = Py_NewRef(callable);
+ it->it_sentinel = Py_XNewRef(sentinel);
+ it->it_stop_exc = Py_NewRef(stop_exc);
+ _PyObject_GC_TRACK(it);
+ return (PyObject *)it;
+}
+
+static void
+acalliter_exhaust(acalliterobject *it)
+{
+ Py_CLEAR(it->it_callable);
+ Py_CLEAR(it->it_sentinel);
+}
+
+static void
+acalliter_dealloc(PyObject *op)
+{
+ acalliterobject *it = acalliterobject_CAST(op);
+ _PyObject_GC_UNTRACK(it);
+ Py_XDECREF(it->it_callable);
+ Py_XDECREF(it->it_sentinel);
+ Py_XDECREF(it->it_stop_exc);
+ PyObject_GC_Del(it);
+}
+
+static int
+acalliter_traverse(PyObject *op, visitproc visit, void *arg)
+{
+ acalliterobject *it = acalliterobject_CAST(op);
+ Py_VISIT(it->it_callable);
+ Py_VISIT(it->it_sentinel);
+ Py_VISIT(it->it_stop_exc);
+ return 0;
+}
+
+static PyObject *acallawaitable_new(PyObject *iterator);
+
+static PyObject *
+acalliter_anext(PyObject *op)
+{
+ return acallawaitable_new(op);
+}
+
+static PyAsyncMethods acalliter_as_async = {
+ 0, /* am_await */
+ PyObject_SelfIter, /* am_aiter */
+ acalliter_anext, /* am_anext */
+ 0, /* am_send */
+};
+
+PyTypeObject _PyACallIter_Type = {
+ PyVarObject_HEAD_INIT(&PyType_Type, 0)
+ "async_callable_iterator", /* tp_name */
+ sizeof(acalliterobject), /* tp_basicsize */
+ 0, /* tp_itemsize */
+ /* methods */
+ acalliter_dealloc, /* tp_dealloc */
+ 0, /* tp_vectorcall_offset */
+ 0, /* tp_getattr */
+ 0, /* tp_setattr */
+ &acalliter_as_async, /* tp_as_async */
+ 0, /* tp_repr */
+ 0, /* tp_as_number */
+ 0, /* tp_as_sequence */
+ 0, /* tp_as_mapping */
+ 0, /* tp_hash */
+ 0, /* tp_call */
+ 0, /* tp_str */
+ PyObject_GenericGetAttr, /* tp_getattro */
+ 0, /* tp_setattro */
+ 0, /* tp_as_buffer */
+ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */
+ 0, /* tp_doc */
+ acalliter_traverse, /* tp_traverse */
+};
+
+/* -------------------------------------- */
+
+static PyObject *
+acallawaitable_new(PyObject *iterator)
+{
+ acallawaitableobject *aw = PyObject_GC_New(
+ acallawaitableobject, &_PyACallIterAwaitable_Type);
+ if (aw == NULL) {
+ return NULL;
+ }
+ aw->aw_iterator = Py_NewRef(iterator);
+ aw->aw_wrapped = NULL;
+ aw->aw_closed = 0;
+ _PyObject_GC_TRACK(aw);
+ return (PyObject *)aw;
+}
+
+static void
+acallawaitable_dealloc(PyObject *op)
+{
+ acallawaitableobject *aw = acallawaitableobject_CAST(op);
+ _PyObject_GC_UNTRACK(aw);
+ Py_XDECREF(aw->aw_iterator);
+ Py_XDECREF(aw->aw_wrapped);
+ PyObject_GC_Del(aw);
+}
+
+static int
+acallawaitable_traverse(PyObject *op, visitproc visit, void *arg)
+{
+ acallawaitableobject *aw = acallawaitableobject_CAST(op);
+ Py_VISIT(aw->aw_iterator);
+ Py_VISIT(aw->aw_wrapped);
+ return 0;
+}
+
+/* Call the callable. Return 0 on success, -1 on failure. */
+static int
+acallawaitable_start(acallawaitableobject *aw)
+{
+ acalliterobject *it = acalliterobject_CAST(aw->aw_iterator);
+
+ if (aw->aw_closed) {
+ PyErr_SetString(PyExc_RuntimeError,
+ "cannot reuse already awaited __anext__()");
+ return -1;
+ }
+ if (it->it_callable == NULL) {
+ PyErr_SetNone(PyExc_StopAsyncIteration);
+ return -1;
+ }
+ PyObject *awaitable = _PyObject_CallNoArgs(it->it_callable);
+ if (awaitable == NULL) {
+ if (PyErr_ExceptionMatches(it->it_stop_exc)) {
+ PyErr_Clear();
+ acalliter_exhaust(it);
+ PyErr_SetNone(PyExc_StopAsyncIteration);
+ }
+ else if (PyErr_ExceptionMatches(PyExc_StopIteration)) {
+ /* It would be mistaken for the result of the await (PEP 525). */
+ _PyErr_FormatFromCause(PyExc_RuntimeError,
+ "callable raised StopIteration");
+ }
+ else if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration)) {
+ /* It would be mistaken for the end of the iteration (PEP 525). */
+ _PyErr_FormatFromCause(PyExc_RuntimeError,
+ "callable raised StopAsyncIteration");
+ }
+ return -1;
+ }
+ aw->aw_wrapped = awaitable;
+ return 0;
+}
+
+/* Turn the exception raised by the wrapped awaitable into the result of
+ the await. Always returns NULL. */
+static PyObject *
+acallawaitable_handle_error(acallawaitableobject *aw)
+{
+ acalliterobject *it = acalliterobject_CAST(aw->aw_iterator);
+
+ if (PyErr_ExceptionMatches(PyExc_StopIteration)) {
+ PyObject *value;
+ if (_PyGen_FetchStopIterationValue(&value) < 0) {
+ return NULL;
+ }
+ int ok = 0;
+ if (it->it_sentinel != NULL) {
+ ok = PyObject_RichCompareBool(it->it_sentinel, value, Py_EQ);
+ }
+ if (ok == 0) {
+ (void)_PyGen_SetStopIterationValue(value);
+ }
+ else if (ok > 0) {
+ acalliter_exhaust(it);
+ PyErr_SetNone(PyExc_StopAsyncIteration);
+ }
+ Py_DECREF(value);
+ return NULL;
+ }
+ if (PyErr_ExceptionMatches(it->it_stop_exc)) {
+ PyErr_Clear();
+ acalliter_exhaust(it);
+ PyErr_SetNone(PyExc_StopAsyncIteration);
+ }
+ else if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration)) {
+ /* It would be mistaken for the end of the iteration (see PEP 525). */
+ _PyErr_FormatFromCause(PyExc_RuntimeError,
+ "callable raised StopAsyncIteration");
+ }
+ return NULL;
+}
+
+static PyObject *
+acallawaitable_iternext(PyObject *op)
+{
+ acallawaitableobject *aw = acallawaitableobject_CAST(op);
+
+ if (aw->aw_wrapped == NULL && acallawaitable_start(aw) < 0) {
+ return NULL;
+ }
+ PyObject *awaitable = awaitable_getiter(op, aw->aw_wrapped);
+ if (awaitable == NULL) {
+ return NULL;
+ }
+ PyObject *result = (*Py_TYPE(awaitable)->tp_iternext)(awaitable);
+ Py_DECREF(awaitable);
+ if (result != NULL) {
+ return result;
+ }
+ return acallawaitable_handle_error(aw);
+}
+
+static PyObject *
+acallawaitable_proxy(acallawaitableobject *aw, char *meth, PyObject *arg)
+{
+ PyObject *awaitable = awaitable_getiter((PyObject *)aw, aw->aw_wrapped);
+ if (awaitable == NULL) {
+ return NULL;
+ }
+ // When specified, 'arg' may be a tuple (if coming from a METH_VARARGS
+ // method) or a single object (if coming from a METH_O method).
+ PyObject *ret = arg == NULL
+ ? PyObject_CallMethod(awaitable, meth, NULL)
+ : PyObject_CallMethod(awaitable, meth, "O", arg);
+ Py_DECREF(awaitable);
+ if (ret != NULL) {
+ return ret;
+ }
+ return acallawaitable_handle_error(aw);
+}
+
+static PyObject *
+acallawaitable_send(PyObject *op, PyObject *arg)
+{
+ acallawaitableobject *aw = acallawaitableobject_CAST(op);
+
+ if (aw->aw_wrapped == NULL && acallawaitable_start(aw) < 0) {
+ return NULL;
+ }
+ return acallawaitable_proxy(aw, "send", arg);
+}
+
+static PyObject *
+acallawaitable_throw(PyObject *op, PyObject *args)
+{
+ acallawaitableobject *aw = acallawaitableobject_CAST(op);
+
+ if (aw->aw_wrapped == NULL) {
+ /* Not started, so the exception is raised at the point of the
+ await, as for a not started coroutine. */
+ PyObject *typ, *val = NULL, *tb = NULL;
+ if (!PyArg_UnpackTuple(args, "throw", 1, 3, &typ, &val, &tb)) {
+ return NULL;
+ }
+ aw->aw_closed = 1;
+ (void)_PyGen_SetException(typ, val, tb);
+ return NULL;
+ }
+ return acallawaitable_proxy(aw, "throw", args);
+}
+
+static PyObject *
+acallawaitable_close(PyObject *op, PyObject *Py_UNUSED(dummy))
+{
+ acallawaitableobject *aw = acallawaitableobject_CAST(op);
+
+ if (aw->aw_wrapped == NULL) {
+ /* Not started, so there is nothing to close. */
+ aw->aw_closed = 1;
+ Py_RETURN_NONE;
+ }
+ PyObject *result = acallawaitable_proxy(aw, "close", NULL);
+ aw->aw_closed = 1;
+ return result;
+}
+
+static PyMethodDef acallawaitable_methods[] = {
+ {"send", acallawaitable_send, METH_O, send_doc},
+ {"throw", acallawaitable_throw, METH_VARARGS, throw_doc},
+ {"close", acallawaitable_close, METH_NOARGS, close_doc},
+ {NULL, NULL} /* Sentinel */
+};
+
+static PyAsyncMethods acallawaitable_as_async = {
+ PyObject_SelfIter, /* am_await */
+ 0, /* am_aiter */
+ 0, /* am_anext */
+ 0, /* am_send */
+};
+
+PyTypeObject _PyACallIterAwaitable_Type = {
+ PyVarObject_HEAD_INIT(&PyType_Type, 0)
+ "async_callable_iterator_awaitable", /* tp_name */
+ sizeof(acallawaitableobject), /* tp_basicsize */
+ 0, /* tp_itemsize */
+ /* methods */
+ acallawaitable_dealloc, /* tp_dealloc */
+ 0, /* tp_vectorcall_offset */
+ 0, /* tp_getattr */
+ 0, /* tp_setattr */
+ &acallawaitable_as_async, /* tp_as_async */
+ 0, /* tp_repr */
+ 0, /* tp_as_number */
+ 0, /* tp_as_sequence */
+ 0, /* tp_as_mapping */
+ 0, /* tp_hash */
+ 0, /* tp_call */
+ 0, /* tp_str */
+ PyObject_GenericGetAttr, /* tp_getattro */
+ 0, /* tp_setattro */
+ 0, /* tp_as_buffer */
+ Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */
+ 0, /* tp_doc */
+ acallawaitable_traverse, /* tp_traverse */
+ 0, /* tp_clear */
+ 0, /* tp_richcompare */
+ 0, /* tp_weaklistoffset */
+ PyObject_SelfIter, /* tp_iter */
+ acallawaitable_iternext, /* tp_iternext */
+ acallawaitable_methods, /* tp_methods */
+};
diff --git a/Objects/object.c b/Objects/object.c
index fadd9273a36607c..c0cb0da7a0d92e5 100644
--- a/Objects/object.c
+++ b/Objects/object.c
@@ -2519,6 +2519,8 @@ _PyObject_FiniState(PyInterpreterState *interp)
}
+extern PyTypeObject _PyACallIter_Type;
+extern PyTypeObject _PyACallIterAwaitable_Type;
extern PyTypeObject _PyAnextAwaitable_Type;
extern PyTypeObject _PyLegacyEventHandler_Type;
extern PyTypeObject _PyLineIterator;
@@ -2612,6 +2614,8 @@ static PyTypeObject* static_types[_Py_NUM_MANAGED_PREINITIALIZED_TYPES] = {
&PyWrapperDescr_Type,
&PyZip_Type,
&Py_GenericAliasType,
+ &_PyACallIter_Type,
+ &_PyACallIterAwaitable_Type,
&_PyAnextAwaitable_Type,
&_PyAsyncGenASend_Type,
&_PyAsyncGenAThrow_Type,
diff --git a/PCbuild/pythoncore.vcxproj b/PCbuild/pythoncore.vcxproj
index 7fec674c550e027..60bb98775e528bf 100644
--- a/PCbuild/pythoncore.vcxproj
+++ b/PCbuild/pythoncore.vcxproj
@@ -276,6 +276,7 @@
+
diff --git a/PCbuild/pythoncore.vcxproj.filters b/PCbuild/pythoncore.vcxproj.filters
index cb0f6fce86df51a..9bf21ffbe322d10 100644
--- a/PCbuild/pythoncore.vcxproj.filters
+++ b/PCbuild/pythoncore.vcxproj.filters
@@ -735,6 +735,9 @@
Include\cpython
+
+ Include\cpython
+
Include\internal
diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c
index cbe59c8883d5a57..d28e6fa9cd01aed 100644
--- a/Python/bltinmodule.c
+++ b/Python/bltinmodule.c
@@ -10,6 +10,7 @@
#include "pycore_floatobject.h" // _PyFloat_ExactDealloc()
#include "pycore_interp.h" // _PyInterpreterState_GetConfig()
#include "pycore_import.h" // _PyImport_LazyImportModuleLevelObject ()
+#include "pycore_iterobject.h" // _PyCallIter_NewEx()
#include "pycore_long.h" // _PyLong_CompactValue
#include "pycore_modsupport.h" // _PyArg_NoKwnames()
#include "pycore_object.h" // _Py_AddToAllObjects()
@@ -1893,50 +1894,70 @@ builtin_hex(PyObject *module, PyObject *integer)
}
-/* AC: cannot convert yet, as needs PEP 457 group support in inspect */
+/*[clinic input]
+@text_signature "($module, object, /, [stop_value], *, stop_exception=StopIteration)"
+iter as builtin_iter
+
+ object: object
+ /
+ stop_value: object = NULL
+ *
+ stop_exception: object = NULL
+
+Get an iterator from an object.
+
+In the first form, the argument must supply its own iterator, or be a
+sequence. In the second form, the callable is called until it returns
+the stop value or raises the specified exception.
+[clinic start generated code]*/
+
static PyObject *
-builtin_iter(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
+builtin_iter_impl(PyObject *module, PyObject *object, PyObject *stop_value,
+ PyObject *stop_exception)
+/*[clinic end generated code: output=eb9c9ae8f77bf400 input=d3a2f767f29d9ae6]*/
{
- PyObject *v;
-
- if (!_PyArg_CheckPositional("iter", nargs, 1, 2))
- return NULL;
- v = args[0];
- if (nargs == 1)
- return PyObject_GetIter(v);
- if (!PyCallable_Check(v)) {
+ if (stop_value == NULL && stop_exception == NULL) {
+ return PyObject_GetIter(object);
+ }
+ if (!PyCallable_Check(object)) {
PyErr_SetString(PyExc_TypeError,
- "iter(v, w): v must be callable");
+ "iter(): the first argument must be callable");
return NULL;
}
- PyObject *sentinel = args[1];
- return PyCallIter_New(v, sentinel);
+ return _PyCallIter_NewEx(object, stop_value, stop_exception);
}
-PyDoc_STRVAR(iter_doc,
-"iter(iterable) -> iterator\n\
-iter(callable, sentinel) -> iterator\n\
-\n\
-Get an iterator from an object. In the first form, the argument must\n\
-supply its own iterator, or be a sequence.\n\
-In the second form, the callable is called until it returns the\n\
-sentinel.");
-
/*[clinic input]
+@text_signature "($module, object, /, [stop_value], *, stop_exception=StopAsyncIteration)"
aiter as builtin_aiter
- async_iterable: object
+ object: object
/
+ stop_value: object = NULL
+ *
+ stop_exception: object = NULL
Return an AsyncIterator for an AsyncIterable object.
+
+In the second form, the callable is called and its result is awaited
+until it returns the stop value or raises the specified exception.
[clinic start generated code]*/
static PyObject *
-builtin_aiter(PyObject *module, PyObject *async_iterable)
-/*[clinic end generated code: output=1bae108d86f7960e input=473993d0cacc7d23]*/
+builtin_aiter_impl(PyObject *module, PyObject *object, PyObject *stop_value,
+ PyObject *stop_exception)
+/*[clinic end generated code: output=2865edb3fbc45693 input=2adb37d12adafd0c]*/
{
- return PyObject_GetAIter(async_iterable);
+ if (stop_value == NULL && stop_exception == NULL) {
+ return PyObject_GetAIter(object);
+ }
+ if (!PyCallable_Check(object)) {
+ PyErr_SetString(PyExc_TypeError,
+ "aiter(): the first argument must be callable");
+ return NULL;
+ }
+ return _PyACallIter_New(object, stop_value, stop_exception);
}
PyObject *PyAnextAwaitable_New(PyObject *, PyObject *);
@@ -3472,7 +3493,7 @@ static PyMethodDef builtin_methods[] = {
BUILTIN_INPUT_METHODDEF
BUILTIN_ISINSTANCE_METHODDEF
BUILTIN_ISSUBCLASS_METHODDEF
- {"iter", _PyCFunction_CAST(builtin_iter), METH_FASTCALL, iter_doc},
+ BUILTIN_ITER_METHODDEF
BUILTIN_AITER_METHODDEF
BUILTIN_LEN_METHODDEF
BUILTIN_LOCALS_METHODDEF
diff --git a/Python/clinic/bltinmodule.c.h b/Python/clinic/bltinmodule.c.h
index 4a38e0df61708c0..c10bb03d8178161 100644
--- a/Python/clinic/bltinmodule.c.h
+++ b/Python/clinic/bltinmodule.c.h
@@ -850,14 +850,166 @@ PyDoc_STRVAR(builtin_hex__doc__,
#define BUILTIN_HEX_METHODDEF \
{"hex", (PyCFunction)builtin_hex, METH_O, builtin_hex__doc__},
+PyDoc_STRVAR(builtin_iter__doc__,
+"iter($module, object, /, [stop_value], *, stop_exception=StopIteration)\n"
+"--\n"
+"\n"
+"Get an iterator from an object.\n"
+"\n"
+"In the first form, the argument must supply its own iterator, or be a\n"
+"sequence. In the second form, the callable is called until it returns\n"
+"the stop value or raises the specified exception.");
+
+#define BUILTIN_ITER_METHODDEF \
+ {"iter", _PyCFunction_CAST(builtin_iter), METH_FASTCALL|METH_KEYWORDS, builtin_iter__doc__},
+
+static PyObject *
+builtin_iter_impl(PyObject *module, PyObject *object, PyObject *stop_value,
+ PyObject *stop_exception);
+
+static PyObject *
+builtin_iter(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+ PyObject *return_value = NULL;
+ #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+ #define NUM_KEYWORDS 2
+ static struct {
+ PyGC_Head _this_is_not_used;
+ PyObject_VAR_HEAD
+ Py_hash_t ob_hash;
+ PyObject *ob_item[NUM_KEYWORDS];
+ } _kwtuple = {
+ .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+ .ob_hash = -1,
+ .ob_item = { &_Py_ID(stop_value), &_Py_ID(stop_exception), },
+ };
+ #undef NUM_KEYWORDS
+ #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+ #else // !Py_BUILD_CORE
+ # define KWTUPLE NULL
+ #endif // !Py_BUILD_CORE
+
+ static const char * const _keywords[] = {"", "stop_value", "stop_exception", NULL};
+ static _PyArg_Parser _parser = {
+ .keywords = _keywords,
+ .fname = "iter",
+ .kwtuple = KWTUPLE,
+ };
+ #undef KWTUPLE
+ PyObject *argsbuf[3];
+ Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1;
+ PyObject *object;
+ PyObject *stop_value = NULL;
+ PyObject *stop_exception = NULL;
+
+ args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
+ /*minpos*/ 1, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
+ if (!args) {
+ goto exit;
+ }
+ object = args[0];
+ if (!noptargs) {
+ goto skip_optional_pos;
+ }
+ if (args[1]) {
+ stop_value = args[1];
+ if (!--noptargs) {
+ goto skip_optional_pos;
+ }
+ }
+skip_optional_pos:
+ if (!noptargs) {
+ goto skip_optional_kwonly;
+ }
+ stop_exception = args[2];
+skip_optional_kwonly:
+ return_value = builtin_iter_impl(module, object, stop_value, stop_exception);
+
+exit:
+ return return_value;
+}
+
PyDoc_STRVAR(builtin_aiter__doc__,
-"aiter($module, async_iterable, /)\n"
+"aiter($module, object, /, [stop_value], *, stop_exception=StopAsyncIteration)\n"
"--\n"
"\n"
-"Return an AsyncIterator for an AsyncIterable object.");
+"Return an AsyncIterator for an AsyncIterable object.\n"
+"\n"
+"In the second form, the callable is called and its result is awaited\n"
+"until it returns the stop value or raises the specified exception.");
#define BUILTIN_AITER_METHODDEF \
- {"aiter", (PyCFunction)builtin_aiter, METH_O, builtin_aiter__doc__},
+ {"aiter", _PyCFunction_CAST(builtin_aiter), METH_FASTCALL|METH_KEYWORDS, builtin_aiter__doc__},
+
+static PyObject *
+builtin_aiter_impl(PyObject *module, PyObject *object, PyObject *stop_value,
+ PyObject *stop_exception);
+
+static PyObject *
+builtin_aiter(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
+{
+ PyObject *return_value = NULL;
+ #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+ #define NUM_KEYWORDS 2
+ static struct {
+ PyGC_Head _this_is_not_used;
+ PyObject_VAR_HEAD
+ Py_hash_t ob_hash;
+ PyObject *ob_item[NUM_KEYWORDS];
+ } _kwtuple = {
+ .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+ .ob_hash = -1,
+ .ob_item = { &_Py_ID(stop_value), &_Py_ID(stop_exception), },
+ };
+ #undef NUM_KEYWORDS
+ #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+ #else // !Py_BUILD_CORE
+ # define KWTUPLE NULL
+ #endif // !Py_BUILD_CORE
+
+ static const char * const _keywords[] = {"", "stop_value", "stop_exception", NULL};
+ static _PyArg_Parser _parser = {
+ .keywords = _keywords,
+ .fname = "aiter",
+ .kwtuple = KWTUPLE,
+ };
+ #undef KWTUPLE
+ PyObject *argsbuf[3];
+ Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1;
+ PyObject *object;
+ PyObject *stop_value = NULL;
+ PyObject *stop_exception = NULL;
+
+ args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
+ /*minpos*/ 1, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
+ if (!args) {
+ goto exit;
+ }
+ object = args[0];
+ if (!noptargs) {
+ goto skip_optional_pos;
+ }
+ if (args[1]) {
+ stop_value = args[1];
+ if (!--noptargs) {
+ goto skip_optional_pos;
+ }
+ }
+skip_optional_pos:
+ if (!noptargs) {
+ goto skip_optional_kwonly;
+ }
+ stop_exception = args[2];
+skip_optional_kwonly:
+ return_value = builtin_aiter_impl(module, object, stop_value, stop_exception);
+
+exit:
+ return return_value;
+}
PyDoc_STRVAR(builtin_anext__doc__,
"anext($module, async_iterator, default=, /)\n"
@@ -1387,4 +1539,4 @@ builtin_issubclass(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
exit:
return return_value;
}
-/*[clinic end generated code: output=84efa9c5cc737ce5 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=5fb1ac6a4253ee2f input=a9049054013a1b77]*/
diff --git a/Tools/c-analyzer/cpython/globals-to-fix.tsv b/Tools/c-analyzer/cpython/globals-to-fix.tsv
index db575d870be5c53..148f6e68ab806e5 100644
--- a/Tools/c-analyzer/cpython/globals-to-fix.tsv
+++ b/Tools/c-analyzer/cpython/globals-to-fix.tsv
@@ -58,6 +58,8 @@ Objects/genobject.c - _PyCoroWrapper_Type -
Objects/interpolationobject.c - _PyInterpolation_Type -
Objects/iterobject.c - PyCallIter_Type -
Objects/iterobject.c - PySeqIter_Type -
+Objects/iterobject.c - _PyACallIter_Type -
+Objects/iterobject.c - _PyACallIterAwaitable_Type -
Objects/iterobject.c - _PyAnextAwaitable_Type -
Objects/lazyimportobject.c - PyLazyImport_Type -
Objects/listobject.c - PyListIter_Type -
@@ -73,6 +75,8 @@ Objects/moduleobject.c - PyModule_Type -
Objects/namespaceobject.c - _PyNamespace_Type -
Objects/object.c - _PyNone_Type -
Objects/object.c - _PyNotImplemented_Type -
+Objects/object.c - _PyACallIter_Type -
+Objects/object.c - _PyACallIterAwaitable_Type -
Objects/object.c - _PyAnextAwaitable_Type -
Objects/odictobject.c - PyODictItems_Type -
Objects/odictobject.c - PyODictIter_Type -