Skip to content

Commit f0a6fde

Browse files
jablordmauvejustin39
authored
bpo-31861: Add aiter and anext to builtins (#23847)
Co-authored-by: jab <[email protected]> Co-authored-by: Daniel Pope <[email protected]> Co-authored-by: Justin Wang <[email protected]>
1 parent 94faa07 commit f0a6fde

File tree

10 files changed

+373
-28
lines changed

10 files changed

+373
-28
lines changed

Doc/library/functions.rst

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,31 @@ are always available. They are listed here in alphabetical order.
1212
+=========================+=======================+=======================+=========================+
1313
| | **A** | | **E** | | **L** | | **R** |
1414
| | :func:`abs` | | :func:`enumerate` | | :func:`len` | | |func-range|_ |
15-
| | :func:`all` | | :func:`eval` | | |func-list|_ | | :func:`repr` |
16-
| | :func:`any` | | :func:`exec` | | :func:`locals` | | :func:`reversed` |
17-
| | :func:`ascii` | | | | | | :func:`round` |
18-
| | | | **F** | | **M** | | |
19-
| | **B** | | :func:`filter` | | :func:`map` | | **S** |
20-
| | :func:`bin` | | :func:`float` | | :func:`max` | | |func-set|_ |
21-
| | :func:`bool` | | :func:`format` | | |func-memoryview|_ | | :func:`setattr` |
22-
| | :func:`breakpoint` | | |func-frozenset|_ | | :func:`min` | | :func:`slice` |
23-
| | |func-bytearray|_ | | | | | | :func:`sorted` |
24-
| | |func-bytes|_ | | **G** | | **N** | | :func:`staticmethod` |
25-
| | | | :func:`getattr` | | :func:`next` | | |func-str|_ |
26-
| | **C** | | :func:`globals` | | | | :func:`sum` |
27-
| | :func:`callable` | | | | **O** | | :func:`super` |
28-
| | :func:`chr` | | **H** | | :func:`object` | | |
29-
| | :func:`classmethod` | | :func:`hasattr` | | :func:`oct` | | **T** |
30-
| | :func:`compile` | | :func:`hash` | | :func:`open` | | |func-tuple|_ |
31-
| | :func:`complex` | | :func:`help` | | :func:`ord` | | :func:`type` |
32-
| | | | :func:`hex` | | | | |
33-
| | **D** | | | | **P** | | **V** |
34-
| | :func:`delattr` | | **I** | | :func:`pow` | | :func:`vars` |
35-
| | |func-dict|_ | | :func:`id` | | :func:`print` | | |
36-
| | :func:`dir` | | :func:`input` | | :func:`property` | | **Z** |
37-
| | :func:`divmod` | | :func:`int` | | | | :func:`zip` |
38-
| | | | :func:`isinstance` | | | | |
39-
| | | | :func:`issubclass` | | | | **_** |
15+
| | :func:`aiter` | | :func:`eval` | | |func-list|_ | | :func:`repr` |
16+
| | :func:`all` | | :func:`exec` | | :func:`locals` | | :func:`reversed` |
17+
| | :func:`any` | | | | | | :func:`round` |
18+
| | :func:`anext` | | **F** | | **M** | | |
19+
| | :func:`ascii` | | :func:`filter` | | :func:`map` | | **S** |
20+
| | | | :func:`float` | | :func:`max` | | |func-set|_ |
21+
| | **B** | | :func:`format` | | |func-memoryview|_ | | :func:`setattr` |
22+
| | :func:`bin` | | |func-frozenset|_ | | :func:`min` | | :func:`slice` |
23+
| | :func:`bool` | | | | | | :func:`sorted` |
24+
| | :func:`breakpoint` | | **G** | | **N** | | :func:`staticmethod` |
25+
| | |func-bytearray|_ | | :func:`getattr` | | :func:`next` | | |func-str|_ |
26+
| | |func-bytes|_ | | :func:`globals` | | | | :func:`sum` |
27+
| | | | | | **O** | | :func:`super` |
28+
| | **C** | | **H** | | :func:`object` | | |
29+
| | :func:`callable` | | :func:`hasattr` | | :func:`oct` | | **T** |
30+
| | :func:`chr` | | :func:`hash` | | :func:`open` | | |func-tuple|_ |
31+
| | :func:`classmethod` | | :func:`help` | | :func:`ord` | | :func:`type` |
32+
| | :func:`compile` | | :func:`hex` | | | | |
33+
| | :func:`complex` | | | | **P** | | **V** |
34+
| | | | **I** | | :func:`pow` | | :func:`vars` |
35+
| | **D** | | :func:`id` | | :func:`print` | | |
36+
| | :func:`delattr` | | :func:`input` | | :func:`property` | | **Z** |
37+
| | |func-dict|_ | | :func:`int` | | | | :func:`zip` |
38+
| | :func:`dir` | | :func:`isinstance` | | | | |
39+
| | :func:`divmod` | | :func:`issubclass` | | | | **_** |
4040
| | | | :func:`iter` | | | | :func:`__import__` |
4141
+-------------------------+-----------------------+-----------------------+-------------------------+
4242

@@ -61,6 +61,17 @@ are always available. They are listed here in alphabetical order.
6161
If the argument is a complex number, its magnitude is returned.
6262

6363

64+
.. function:: aiter(async_iterable)
65+
66+
Return an :term:`asynchronous iterator` for an :term:`asynchronous iterable`.
67+
Equivalent to calling ``x.__aiter__()``.
68+
69+
``aiter(x)`` itself has an ``__aiter__()`` method that returns ``x``,
70+
so ``aiter(aiter(x))`` is the same as ``aiter(x)``.
71+
72+
Note: Unlike :func:`iter`, :func:`aiter` has no 2-argument variant.
73+
74+
6475
.. function:: all(iterable)
6576

6677
Return ``True`` if all elements of the *iterable* are true (or if the iterable
@@ -73,6 +84,20 @@ are always available. They are listed here in alphabetical order.
7384
return True
7485

7586

87+
.. awaitablefunction:: anext(async_iterator[, default])
88+
89+
When awaited, return the next item from the given :term:`asynchronous
90+
iterator`, or *default* if given and the iterator is exhausted.
91+
92+
This is the async variant of the :func:`next` builtin, and behaves
93+
similarly.
94+
95+
This calls the :meth:`~object.__anext__` method of *async_iterator*,
96+
returning an :term:`awaitable`. Awaiting this returns the next value of the
97+
iterator. If *default* is given, it is returned if the iterator is exhausted,
98+
otherwise :exc:`StopAsyncIteration` is raised.
99+
100+
76101
.. function:: any(iterable)
77102

78103
Return ``True`` if any element of the *iterable* is true. If the iterable

Doc/whatsnew/3.10.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,11 @@ Other Language Changes
588588
``__globals__["__builtins__"]`` if it exists, else from the current builtins.
589589
(Contributed by Mark Shannon in :issue:`42990`.)
590590
591+
* Two new builtin functions -- :func:`aiter` and :func:`anext` have been added
592+
to provide asynchronous counterparts to :func:`iter` and :func:`next`,
593+
respectively.
594+
(Contributed by Joshua Bronson, Daniel Pope, and Justin Wang in :issue:`31861`.)
595+
591596
592597
New Modules
593598
===========

Include/abstract.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,11 +324,21 @@ PyAPI_FUNC(PyObject *) PyObject_Format(PyObject *obj,
324324
returns itself. */
325325
PyAPI_FUNC(PyObject *) PyObject_GetIter(PyObject *);
326326

327+
/* Takes an AsyncIterable object and returns an AsyncIterator for it.
328+
This is typically a new iterator but if the argument is an AsyncIterator,
329+
this returns itself. */
330+
PyAPI_FUNC(PyObject *) PyObject_GetAiter(PyObject *);
331+
327332
/* Returns non-zero if the object 'obj' provides iterator protocols, and 0 otherwise.
328333
329334
This function always succeeds. */
330335
PyAPI_FUNC(int) PyIter_Check(PyObject *);
331336

337+
/* Returns non-zero if the object 'obj' provides AsyncIterator protocols, and 0 otherwise.
338+
339+
This function always succeeds. */
340+
PyAPI_FUNC(int) PyAiter_Check(PyObject *);
341+
332342
/* Takes an iterator object and calls its tp_iternext slot,
333343
returning the next value.
334344

Lib/test/test_asyncgen.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,88 @@ def tearDown(self):
372372
self.loop = None
373373
asyncio.set_event_loop_policy(None)
374374

375+
def test_async_gen_anext(self):
376+
async def gen():
377+
yield 1
378+
yield 2
379+
g = gen()
380+
async def consume():
381+
results = []
382+
results.append(await anext(g))
383+
results.append(await anext(g))
384+
results.append(await anext(g, 'buckle my shoe'))
385+
return results
386+
res = self.loop.run_until_complete(consume())
387+
self.assertEqual(res, [1, 2, 'buckle my shoe'])
388+
with self.assertRaises(StopAsyncIteration):
389+
self.loop.run_until_complete(consume())
390+
391+
def test_async_gen_aiter(self):
392+
async def gen():
393+
yield 1
394+
yield 2
395+
g = gen()
396+
async def consume():
397+
return [i async for i in aiter(g)]
398+
res = self.loop.run_until_complete(consume())
399+
self.assertEqual(res, [1, 2])
400+
401+
def test_async_gen_aiter_class(self):
402+
results = []
403+
class Gen:
404+
async def __aiter__(self):
405+
yield 1
406+
yield 2
407+
g = Gen()
408+
async def consume():
409+
ait = aiter(g)
410+
while True:
411+
try:
412+
results.append(await anext(ait))
413+
except StopAsyncIteration:
414+
break
415+
self.loop.run_until_complete(consume())
416+
self.assertEqual(results, [1, 2])
417+
418+
def test_aiter_idempotent(self):
419+
async def gen():
420+
yield 1
421+
applied_once = aiter(gen())
422+
applied_twice = aiter(applied_once)
423+
self.assertIs(applied_once, applied_twice)
424+
425+
def test_anext_bad_args(self):
426+
async def gen():
427+
yield 1
428+
async def call_with_too_few_args():
429+
await anext()
430+
async def call_with_too_many_args():
431+
await anext(gen(), 1, 3)
432+
async def call_with_wrong_type_args():
433+
await anext(1, gen())
434+
with self.assertRaises(TypeError):
435+
self.loop.run_until_complete(call_with_too_few_args())
436+
with self.assertRaises(TypeError):
437+
self.loop.run_until_complete(call_with_too_many_args())
438+
with self.assertRaises(TypeError):
439+
self.loop.run_until_complete(call_with_wrong_type_args())
440+
441+
def test_aiter_bad_args(self):
442+
async def gen():
443+
yield 1
444+
async def call_with_too_few_args():
445+
await aiter()
446+
async def call_with_too_many_args():
447+
await aiter(gen(), 1)
448+
async def call_with_wrong_type_arg():
449+
await aiter(1)
450+
with self.assertRaises(TypeError):
451+
self.loop.run_until_complete(call_with_too_few_args())
452+
with self.assertRaises(TypeError):
453+
self.loop.run_until_complete(call_with_too_many_args())
454+
with self.assertRaises(TypeError):
455+
self.loop.run_until_complete(call_with_wrong_type_arg())
456+
375457
async def to_list(self, gen):
376458
res = []
377459
async for i in gen:

Lib/test/test_inspect.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3860,6 +3860,9 @@ def test_builtins_have_signatures(self):
38603860
needs_groups = {"range", "slice", "dir", "getattr",
38613861
"next", "iter", "vars"}
38623862
no_signature |= needs_groups
3863+
# These have unrepresentable parameter default values of NULL
3864+
needs_null = {"anext"}
3865+
no_signature |= needs_null
38633866
# These need PEP 457 groups or a signature change to accept None
38643867
needs_semantic_update = {"round"}
38653868
no_signature |= needs_semantic_update
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add builtins.aiter and builtins.anext.
2+
Patch by Joshua Bronson (@jab), Daniel Pope (@lordmauve), and Justin Wang (@justin39).

Objects/abstract.c

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2738,6 +2738,26 @@ PyObject_GetIter(PyObject *o)
27382738
}
27392739
}
27402740

2741+
PyObject *
2742+
PyObject_GetAiter(PyObject *o) {
2743+
PyTypeObject *t = Py_TYPE(o);
2744+
unaryfunc f;
2745+
2746+
if (t->tp_as_async == NULL || t->tp_as_async->am_aiter == NULL) {
2747+
return type_error("'%.200s' object is not an AsyncIterable", o);
2748+
}
2749+
f = t->tp_as_async->am_aiter;
2750+
PyObject *it = (*f)(o);
2751+
if (it != NULL && !PyAiter_Check(it)) {
2752+
PyErr_Format(PyExc_TypeError,
2753+
"aiter() returned non-AsyncIterator of type '%.100s'",
2754+
Py_TYPE(it)->tp_name);
2755+
Py_DECREF(it);
2756+
it = NULL;
2757+
}
2758+
return it;
2759+
}
2760+
27412761
int
27422762
PyIter_Check(PyObject *obj)
27432763
{
@@ -2746,6 +2766,17 @@ PyIter_Check(PyObject *obj)
27462766
tp->tp_iternext != &_PyObject_NextNotImplemented);
27472767
}
27482768

2769+
int
2770+
PyAiter_Check(PyObject *obj)
2771+
{
2772+
PyTypeObject *tp = Py_TYPE(obj);
2773+
return (tp->tp_as_async != NULL &&
2774+
tp->tp_as_async->am_aiter != NULL &&
2775+
tp->tp_as_async->am_aiter != &_PyObject_NextNotImplemented &&
2776+
tp->tp_as_async->am_anext != NULL &&
2777+
tp->tp_as_async->am_anext != &_PyObject_NextNotImplemented);
2778+
}
2779+
27492780
/* Return next item.
27502781
* If an error occurs, return NULL. PyErr_Occurred() will be true.
27512782
* If the iteration terminates normally, return NULL and clear the

Objects/iterobject.c

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ PyTypeObject PySeqIter_Type = {
157157
PyObject_GenericGetAttr, /* tp_getattro */
158158
0, /* tp_setattro */
159159
0, /* tp_as_buffer */
160-
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,/* tp_flags */
160+
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */
161161
0, /* tp_doc */
162162
(traverseproc)iter_traverse, /* tp_traverse */
163163
0, /* tp_clear */
@@ -276,7 +276,7 @@ PyTypeObject PyCallIter_Type = {
276276
PyObject_GenericGetAttr, /* tp_getattro */
277277
0, /* tp_setattro */
278278
0, /* tp_as_buffer */
279-
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,/* tp_flags */
279+
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */
280280
0, /* tp_doc */
281281
(traverseproc)calliter_traverse, /* tp_traverse */
282282
0, /* tp_clear */
@@ -288,3 +288,91 @@ PyTypeObject PyCallIter_Type = {
288288
};
289289

290290

291+
/* -------------------------------------- */
292+
293+
typedef struct {
294+
PyObject_HEAD
295+
PyObject *wrapped;
296+
PyObject *default_value;
297+
} anextawaitableobject;
298+
299+
static void
300+
anextawaitable_dealloc(anextawaitableobject *obj)
301+
{
302+
_PyObject_GC_UNTRACK(obj);
303+
Py_XDECREF(obj->wrapped);
304+
Py_XDECREF(obj->default_value);
305+
PyObject_GC_Del(obj);
306+
}
307+
308+
static int
309+
anextawaitable_traverse(anextawaitableobject *obj, visitproc visit, void *arg)
310+
{
311+
Py_VISIT(obj->wrapped);
312+
Py_VISIT(obj->default_value);
313+
return 0;
314+
}
315+
316+
static PyObject *
317+
anextawaitable_iternext(anextawaitableobject *obj)
318+
{
319+
PyObject *result = PyIter_Next(obj->wrapped);
320+
if (result != NULL) {
321+
return result;
322+
}
323+
if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration)) {
324+
_PyGen_SetStopIterationValue(obj->default_value);
325+
}
326+
return NULL;
327+
}
328+
329+
static PyAsyncMethods anextawaitable_as_async = {
330+
PyObject_SelfIter, /* am_await */
331+
0, /* am_aiter */
332+
0, /* am_anext */
333+
0, /* am_send */
334+
};
335+
336+
PyTypeObject PyAnextAwaitable_Type = {
337+
PyVarObject_HEAD_INIT(&PyType_Type, 0)
338+
"anext_awaitable", /* tp_name */
339+
sizeof(anextawaitableobject), /* tp_basicsize */
340+
0, /* tp_itemsize */
341+
/* methods */
342+
(destructor)anextawaitable_dealloc, /* tp_dealloc */
343+
0, /* tp_vectorcall_offset */
344+
0, /* tp_getattr */
345+
0, /* tp_setattr */
346+
&anextawaitable_as_async, /* tp_as_async */
347+
0, /* tp_repr */
348+
0, /* tp_as_number */
349+
0, /* tp_as_sequence */
350+
0, /* tp_as_mapping */
351+
0, /* tp_hash */
352+
0, /* tp_call */
353+
0, /* tp_str */
354+
PyObject_GenericGetAttr, /* tp_getattro */
355+
0, /* tp_setattro */
356+
0, /* tp_as_buffer */
357+
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */
358+
0, /* tp_doc */
359+
(traverseproc)anextawaitable_traverse, /* tp_traverse */
360+
0, /* tp_clear */
361+
0, /* tp_richcompare */
362+
0, /* tp_weaklistoffset */
363+
PyObject_SelfIter, /* tp_iter */
364+
(unaryfunc)anextawaitable_iternext, /* tp_iternext */
365+
0, /* tp_methods */
366+
};
367+
368+
PyObject *
369+
PyAnextAwaitable_New(PyObject *awaitable, PyObject *default_value)
370+
{
371+
anextawaitableobject *anext = PyObject_GC_New(anextawaitableobject, &PyAnextAwaitable_Type);
372+
Py_INCREF(awaitable);
373+
anext->wrapped = awaitable;
374+
Py_INCREF(default_value);
375+
anext->default_value = default_value;
376+
_PyObject_GC_TRACK(anext);
377+
return (PyObject *)anext;
378+
}

0 commit comments

Comments
 (0)