Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
44b2a60
bpo-31861: Add operator.aiter and operator.anext
jab Aug 24, 2018
142523b
address comments from first review
jab Sep 7, 2018
3ebce05
Address new review comments + rebase
jab Nov 30, 2020
2f8df99
improve tests
jab Dec 4, 2020
ad12116
Sketch out async iterator in C
lordmauve Dec 4, 2020
ce35092
Implement aiter() and anext() using sync methods only
lordmauve Dec 7, 2020
f9dc183
Add basic aiter built-in [WIP]
justin39 Dec 7, 2020
086bb79
Start aiter implementation
justin39 Dec 9, 2020
29ef712
Get test_asyncgen tests passing
justin39 Dec 10, 2020
331e80e
Fix awaitable iternext
justin39 Dec 10, 2020
95879d8
Add anext builtin
justin39 Dec 11, 2020
5b64589
Use stop iter functions for anext
justin39 Dec 14, 2020
6e145db
Use stop iter functions in aiter
justin39 Dec 14, 2020
2fd4be6
Note about implementing __reduce__ for aiter
justin39 Dec 16, 2020
430dd59
Refactor aiter and anext type names
justin39 Dec 16, 2020
4266e65
Add documentation for aiter()
justin39 Dec 16, 2020
ecbedc7
Update documentation for aiter and anext
justin39 Dec 18, 2020
5fa3812
Clean up docs and formatting
justin39 Dec 18, 2020
0f9c814
Remove async iterator code from operator.py
justin39 Dec 18, 2020
8160c82
Cleanup formatting
justin39 Dec 18, 2020
fa8b12a
Fix test_builtins_have_signatures + misc. cleanups
jab Dec 18, 2020
a2242d9
Use PyErr_SetNone now that we can
jab Dec 18, 2020
3ce5675
whitespace
jab Dec 18, 2020
0038cde
cosmetic fixes
jab Dec 19, 2020
06019ea
Add null check and use StopAsyncIteration when aiter is exhausted
justin39 Dec 19, 2020
0cc00f2
Fix AC definition and resulting signature
justin39 Dec 21, 2020
8b8a689
Fix comparison to NULL instead of Py_None
justin39 Dec 21, 2020
894600d
Revert None deafult for aiter/anext and add whatsnew entry
justin39 Dec 21, 2020
e687d88
Merge branch 'master' of https://github.com/python/cpython into justi…
justin39 Feb 26, 2021
259af97
Delint tests + docs (fixes CI), alphabetize names.
jab Mar 20, 2021
97d06e5
Merge branch 'master' into justin39/aiter-c
jab Mar 20, 2021
dd7c02d
Fix code style and comments.
jab Mar 20, 2021
2fbdd5b
Remove 2-arg variant of aiter.
jab Mar 22, 2021
009118c
Fix use of clinic.
jab Mar 22, 2021
71fede3
Address some feedback from Guido's review.
jab Mar 22, 2021
4a51ace
No longer need to exclude aiter from test_builtins_have_signatures.
jab Mar 22, 2021
108e4a3
Improve aiter() docs.
jab Mar 22, 2021
042f596
Fix typo.
jab Mar 22, 2021
6ee8824
Add test_aiter_idempotent().
jab Mar 23, 2021
6f50ef8
Remove public API added for anext.
jab Mar 23, 2021
ef40fb7
Slightly improve wording
gvanrossum Mar 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Doc/glossary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ Glossary
:ref:`the difference between arguments and parameters
<faq-argument-vs-parameter>`, and :pep:`362`.

asynchronous callable
Any callable that returns an :term:`awaitable`. Examples include
:term:`coroutine functions <coroutine function>` and the built-in
:func:`anext` function.

asynchronous context manager
An object which controls the environment seen in an
:keyword:`async with` statement by defining :meth:`__aenter__` and
Expand Down
80 changes: 55 additions & 25 deletions Doc/library/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,31 @@ are always available. They are listed here in alphabetical order.
+=========================+=======================+=======================+=========================+
| | **A** | | **E** | | **L** | | **R** |
| | :func:`abs` | | :func:`enumerate` | | :func:`len` | | |func-range|_ |
| | :func:`all` | | :func:`eval` | | |func-list|_ | | :func:`repr` |
| | :func:`any` | | :func:`exec` | | :func:`locals` | | :func:`reversed` |
| | :func:`ascii` | | | | | | :func:`round` |
| | | | **F** | | **M** | | |
| | **B** | | :func:`filter` | | :func:`map` | | **S** |
| | :func:`bin` | | :func:`float` | | :func:`max` | | |func-set|_ |
| | :func:`bool` | | :func:`format` | | |func-memoryview|_ | | :func:`setattr` |
| | :func:`breakpoint` | | |func-frozenset|_ | | :func:`min` | | :func:`slice` |
| | |func-bytearray|_ | | | | | | :func:`sorted` |
| | |func-bytes|_ | | **G** | | **N** | | :func:`staticmethod` |
| | | | :func:`getattr` | | :func:`next` | | |func-str|_ |
| | **C** | | :func:`globals` | | | | :func:`sum` |
| | :func:`callable` | | | | **O** | | :func:`super` |
| | :func:`chr` | | **H** | | :func:`object` | | |
| | :func:`classmethod` | | :func:`hasattr` | | :func:`oct` | | **T** |
| | :func:`compile` | | :func:`hash` | | :func:`open` | | |func-tuple|_ |
| | :func:`complex` | | :func:`help` | | :func:`ord` | | :func:`type` |
| | | | :func:`hex` | | | | |
| | **D** | | | | **P** | | **V** |
| | :func:`delattr` | | **I** | | :func:`pow` | | :func:`vars` |
| | |func-dict|_ | | :func:`id` | | :func:`print` | | |
| | :func:`dir` | | :func:`input` | | :func:`property` | | **Z** |
| | :func:`divmod` | | :func:`int` | | | | :func:`zip` |
| | | | :func:`isinstance` | | | | |
| | | | :func:`issubclass` | | | | **_** |
| | :func:`aiter` | | :func:`eval` | | |func-list|_ | | :func:`repr` |
| | :func:`all` | | :func:`exec` | | :func:`locals` | | :func:`reversed` |
| | :func:`any` | | | | | | :func:`round` |
| | :func:`anext` | | **F** | | **M** | | |
| | :func:`ascii` | | :func:`filter` | | :func:`map` | | **S** |
| | | | :func:`float` | | :func:`max` | | |func-set|_ |
| | **B** | | :func:`format` | | |func-memoryview|_ | | :func:`setattr` |
| | :func:`bin` | | |func-frozenset|_ | | :func:`min` | | :func:`slice` |
| | :func:`bool` | | | | | | :func:`sorted` |
| | :func:`breakpoint` | | **G** | | **N** | | :func:`staticmethod` |
| | |func-bytearray|_ | | :func:`getattr` | | :func:`next` | | |func-str|_ |
| | |func-bytes|_ | | :func:`globals` | | | | :func:`sum` |
| | | | | | **O** | | :func:`super` |
| | **C** | | **H** | | :func:`object` | | |
| | :func:`callable` | | :func:`hasattr` | | :func:`oct` | | **T** |
| | :func:`chr` | | :func:`hash` | | :func:`open` | | |func-tuple|_ |
| | :func:`classmethod` | | :func:`help` | | :func:`ord` | | :func:`type` |
| | :func:`compile` | | :func:`hex` | | | | |
| | :func:`complex` | | | | **P** | | **V** |
| | | | **I** | | :func:`pow` | | :func:`vars` |
| | **D** | | :func:`id` | | :func:`print` | | |
| | :func:`delattr` | | :func:`input` | | :func:`property` | | **Z** |
| | |func-dict|_ | | :func:`int` | | | | :func:`zip` |
| | :func:`dir` | | :func:`isinstance` | | | | |
| | :func:`divmod` | | :func:`issubclass` | | | | **_** |
| | | | :func:`iter` | | | | :func:`__import__` |
+-------------------------+-----------------------+-----------------------+-------------------------+

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


.. function:: aiter(object, [sentinel])

Return an :term:`asynchronous iterator` object. This is the async variant
of the :func:`iter` builtin, and behaves similarly.

If sentinel is omitted, then *object* must be an
:term:`asynchronous iterable` object, and :func:`aiter` returns an iterator
for it.

Otherwise, *object* must be an :term:`asynchronous callable`. When the
resulting async iterator is async iterated, the passed callable is
called and awaited and the values returned become the values produced
by the iterator. When the awaited return value is equal to sentinel,
the async iterator terminates with :exc:`StopAsyncIteration`.


.. function:: all(iterable)

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


.. awaitablefunction:: anext(async_iterator[, default])

When awaited, return the next item from the given :term:`asynchronous
iterator`, or *default* if given and the iterator is exhausted.

This is the async variant of the :func:`next` builtin, and behaves
similarly.

Immediately, call the :meth:`~object.__anext__` method of *async_iterator*,
then return an :term:`awaitable`. Awaiting this returns the next value of the
iterator. If *default* is given, it is returned if the iterator is exhausted,
otherwise :exc:`StopAsyncIteration` is raised.


.. function:: any(iterable)

Return ``True`` if any element of the *iterable* is true. If the iterable
Expand Down
5 changes: 5 additions & 0 deletions Include/abstract.h
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,11 @@ PyAPI_FUNC(PyObject *) PyObject_Format(PyObject *obj,
returns itself. */
PyAPI_FUNC(PyObject *) PyObject_GetIter(PyObject *);

/* Takes an object and returns an async iterator for it.
This is typically a new iterator but if the argument is an async iterator,
this returns itself. */
PyAPI_FUNC(PyObject *) PyObject_GetAiter(PyObject *);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to add these to the public C API? Just because PyObject_GetIter() is public I'm not sure that the Aiter variant needs to be. @vstinner tends to push back on adding new things to the C API. @1st1 what do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind adding this function -- while somewhat trivial, it's something that projects like Cython (and potentially our own modules like _asynciomodule.c) have to reimplement.


/* Returns 1 if the object 'obj' provides iterator protocols, and 0 otherwise.

This function always succeeds. */
Expand Down
7 changes: 7 additions & 0 deletions Include/cpython/abstract.h
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,13 @@ PyAPI_FUNC(void) PyBuffer_Release(Py_buffer *view);
(Py_TYPE(obj)->tp_iternext != NULL && \
Py_TYPE(obj)->tp_iternext != &_PyObject_NextNotImplemented)

#define PyAiter_Check(obj) \
(Py_TYPE(obj)->tp_as_async != NULL && \
Py_TYPE(obj)->tp_as_async->am_aiter != NULL && \
Py_TYPE(obj)->tp_as_async->am_aiter != &_PyObject_NextNotImplemented && \
Py_TYPE(obj)->tp_as_async->am_anext != NULL && \
Py_TYPE(obj)->tp_as_async->am_anext != &_PyObject_NextNotImplemented)

/* === Sequence protocol ================================================ */

/* Assume tp_as_sequence and sq_item exist and that 'i' does not
Expand Down
5 changes: 5 additions & 0 deletions Include/iterobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ extern "C" {

PyAPI_DATA(PyTypeObject) PySeqIter_Type;
PyAPI_DATA(PyTypeObject) PyCallIter_Type;
PyAPI_DATA(PyTypeObject) PyCallAsyncIter_Type;
PyAPI_DATA(PyTypeObject) PyAsyncCallAwaitable_Type;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe these types and the function below should remain CPython implementation details? Just because they're returned by builtins doesn't mean all the implementation types need to be in the C-level API.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. I'd make them internal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, thanks. Done in the latest revision. Another look?

PyAPI_DATA(PyTypeObject) PyAnextAwaitable_Type;

#define PySeqIter_Check(op) Py_IS_TYPE(op, &PySeqIter_Type)

Expand All @@ -16,6 +19,8 @@ PyAPI_FUNC(PyObject *) PySeqIter_New(PyObject *);
#define PyCallIter_Check(op) Py_IS_TYPE(op, &PyCallIter_Type)

PyAPI_FUNC(PyObject *) PyCallIter_New(PyObject *, PyObject *);
PyAPI_FUNC(PyObject *) PyCallAsyncIter_New(PyObject *, PyObject *);
PyAPI_FUNC(PyObject *) PyAnextAwaitable_New(PyObject *, PyObject *);

#ifdef __cplusplus
}
Expand Down
91 changes: 91 additions & 0 deletions Lib/test/test_asyncgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,97 @@ def tearDown(self):
self.loop = None
asyncio.set_event_loop_policy(None)

def test_async_gen_anext(self):
async def gen():
yield 1
yield 2
g = gen()
async def consume():
results = []
results.append(await anext(g))
results.append(await anext(g))
results.append(await anext(g, 'buckle my shoe'))
return results
res = self.loop.run_until_complete(consume())
self.assertEqual(res, [1, 2, 'buckle my shoe'])
with self.assertRaises(StopAsyncIteration):
self.loop.run_until_complete(consume())

def test_async_gen_aiter(self):
async def gen():
yield 1
yield 2
g = gen()
async def consume():
return [i async for i in aiter(g)]
res = self.loop.run_until_complete(consume())
self.assertEqual(res, [1, 2])

def test_async_gen_aiter_class(self):
results = []
loop = self.loop
class Gen:
async def __aiter__(self):
yield 1
await asyncio.sleep(0.01)
yield 2
g = Gen()
async def consume():
ait = aiter(g)
while True:
try:
results.append(await anext(ait))
except StopAsyncIteration:
break
self.loop.run_until_complete(consume())
self.assertEqual(results, [1, 2])

def test_async_gen_aiter_2_arg(self):
async def gen():
yield 1
yield 2
yield 3
g = gen()
async def foo():
return await anext(g)
async def consume():
return [i async for i in aiter(foo, 3)]
res = self.loop.run_until_complete(consume())
self.assertEqual(res, [1, 2])

def test_async_aiter_callable(self):
v = 0
async def foo():
nonlocal v
v += 1
return v
async def consume():
return [i async for i in aiter(foo, 3)]
res = self.loop.run_until_complete(consume())
self.assertEqual(res, [1, 2])

def test_anext_bad_args(self):
self._test_bad_args(anext)

def test_aiter_bad_args(self):
self._test_bad_args(aiter)

def _test_bad_args(self, afn):
async def gen():
yield 1
async def call_with_no_args():
await afn()
async def call_with_3_args():
await afn(gen(), 1, 2)
async def call_with_bad_args():
await afn(1, gen())
with self.assertRaises(TypeError):
self.loop.run_until_complete(call_with_no_args())
with self.assertRaises(TypeError):
self.loop.run_until_complete(call_with_3_args())
with self.assertRaises(TypeError):
self.loop.run_until_complete(call_with_bad_args())

async def to_list(self, gen):
res = []
async for i in gen:
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -3812,7 +3812,7 @@ def test_builtins_have_signatures(self):
no_signature = set()
# These need PEP 457 groups
needs_groups = {"range", "slice", "dir", "getattr",
"next", "iter", "vars"}
"next", "iter", "anext", "aiter", "vars"}
no_signature |= needs_groups
# These need PEP 457 groups or a signature change to accept None
needs_semantic_update = {"round"}
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_posix.py
Original file line number Diff line number Diff line change
Expand Up @@ -1929,7 +1929,7 @@ def test_posix_spawnp(self):
class TestPosixWeaklinking(unittest.TestCase):
# These test cases verify that weak linking support on macOS works
# as expected. These cases only test new behaviour introduced by weak linking,
# regular behaviour is tested by the normal test cases.
# regular behaviour is tested by the normal test cases.
#
# See the section on Weak Linking in Mac/README.txt for more information.
def setUp(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add builtins.aiter and builtins.anext.
Patch by Daniel Pope (@lordmauve), Joshua Bronson (@jab), and Justin Wang (@justin39).
24 changes: 24 additions & 0 deletions Objects/abstract.c
Original file line number Diff line number Diff line change
Expand Up @@ -2638,6 +2638,30 @@ PyObject_GetIter(PyObject *o)
}
}


PyObject *
PyObject_GetAiter(PyObject *o) {
PyTypeObject *t = Py_TYPE(o);
unaryfunc f;

f = t->tp_as_async->am_aiter;
if (f == NULL) {
return type_error("'%.200s' object is not async iterable", o);
}
else {
PyObject *it = (*f)(o);
if (it != NULL && !PyAiter_Check(it)) {
PyErr_Format(PyExc_TypeError,
"aiter() returned non-async-iterator "
"of type '%.100s'",
Py_TYPE(it)->tp_name);
Py_DECREF(it);
it = NULL;
}
return it;
}
}

#undef PyIter_Check

int PyIter_Check(PyObject *obj)
Expand Down
Loading