Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
53 changes: 9 additions & 44 deletions Doc/reference/datamodel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2520,9 +2520,8 @@ generators, coroutines do not directly support iteration.
Asynchronous Iterators
----------------------

An *asynchronous iterable* is able to call asynchronous code in its
``__aiter__`` implementation, and an *asynchronous iterator* can call
asynchronous code in its ``__anext__`` method.
An *asynchronous iterator* can call asynchronous code in
its ``__anext__`` method.

Asynchronous iterators can be used in an :keyword:`async for` statement.

Expand Down Expand Up @@ -2552,48 +2551,14 @@ An example of an asynchronous iterable object::

.. versionadded:: 3.5

.. note::
.. versionchanged:: 3.7
Prior to Python 3.7, ``__aiter__`` could return an *awaitable*
that would resolve to an
:term:`asynchronous iterator <asynchronous iterator>`.

.. versionchanged:: 3.5.2
Starting with CPython 3.5.2, ``__aiter__`` can directly return
:term:`asynchronous iterators <asynchronous iterator>`. Returning
an :term:`awaitable` object will result in a
:exc:`PendingDeprecationWarning`.

The recommended way of writing backwards compatible code in
CPython 3.5.x is to continue returning awaitables from
``__aiter__``. If you want to avoid the PendingDeprecationWarning
and keep the code backwards compatible, the following decorator
can be used::

import functools
import sys

if sys.version_info < (3, 5, 2):
def aiter_compat(func):
@functools.wraps(func)
async def wrapper(self):
return func(self)
return wrapper
else:
def aiter_compat(func):
return func

Example::

class AsyncIterator:

@aiter_compat
def __aiter__(self):
return self

async def __anext__(self):
...

Starting with CPython 3.6, the :exc:`PendingDeprecationWarning`
will be replaced with the :exc:`DeprecationWarning`.
In CPython 3.7, returning an awaitable from ``__aiter__`` will
result in a :exc:`RuntimeError`.
Starting with Python 3.7, ``__aiter__`` must return an
asynchronous iterator object. Returning anything else
will result in a :exc:`TypeError` error.


.. _async-context-managers:
Expand Down
1 change: 0 additions & 1 deletion Include/genobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ PyAPI_DATA(PyTypeObject) PyCoro_Type;
PyAPI_DATA(PyTypeObject) _PyCoroWrapper_Type;

PyAPI_DATA(PyTypeObject) _PyAIterWrapper_Type;
PyObject *_PyAIterWrapper_New(PyObject *aiter);

#define PyCoro_CheckExact(op) (Py_TYPE(op) == &PyCoro_Type)
PyObject *_PyCoro_GetAwaitableIter(PyObject *o);
Expand Down
26 changes: 9 additions & 17 deletions Lib/asyncio/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,20 +676,12 @@ def readexactly(self, n):
self._maybe_resume_transport()
return data

if compat.PY35:
@coroutine
def __aiter__(self):
return self

@coroutine
def __anext__(self):
val = yield from self.readline()
if val == b'':
raise StopAsyncIteration
return val

if compat.PY352:
# In Python 3.5.2 and greater, __aiter__ should return
# the asynchronous iterator directly.
def __aiter__(self):
return self
def __aiter__(self):
return self

@coroutine
def __anext__(self):
val = yield from self.readline()
if val == b'':
raise StopAsyncIteration
return val
4 changes: 2 additions & 2 deletions Lib/test/test_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,7 @@ def __hash__(self):

def test_AsyncIterable(self):
class AI:
async def __aiter__(self):
def __aiter__(self):
return self
self.assertTrue(isinstance(AI(), AsyncIterable))
self.assertTrue(issubclass(AI, AsyncIterable))
Expand All @@ -674,7 +674,7 @@ async def __aiter__(self):

def test_AsyncIterator(self):
class AI:
async def __aiter__(self):
def __aiter__(self):
return self
async def __anext__(self):
raise StopAsyncIteration
Expand Down
110 changes: 19 additions & 91 deletions Lib/test/test_coroutines.py
Original file line number Diff line number Diff line change
Expand Up @@ -1382,7 +1382,7 @@ class AsyncIter:
def __init__(self):
self.i = 0

async def __aiter__(self):
def __aiter__(self):
nonlocal aiter_calls
aiter_calls += 1
return self
Expand All @@ -1401,9 +1401,8 @@ async def __anext__(self):

buffer = []
async def test1():
with self.assertWarnsRegex(DeprecationWarning, "legacy"):
async for i1, i2 in AsyncIter():
buffer.append(i1 + i2)
async for i1, i2 in AsyncIter():
buffer.append(i1 + i2)

yielded, _ = run_async(test1())
# Make sure that __aiter__ was called only once
Expand All @@ -1415,13 +1414,12 @@ async def test1():
buffer = []
async def test2():
nonlocal buffer
with self.assertWarnsRegex(DeprecationWarning, "legacy"):
async for i in AsyncIter():
buffer.append(i[0])
if i[0] == 20:
break
else:
buffer.append('what?')
async for i in AsyncIter():
buffer.append(i[0])
if i[0] == 20:
break
else:
buffer.append('what?')
buffer.append('end')

yielded, _ = run_async(test2())
Expand All @@ -1434,13 +1432,12 @@ async def test2():
buffer = []
async def test3():
nonlocal buffer
with self.assertWarnsRegex(DeprecationWarning, "legacy"):
async for i in AsyncIter():
if i[0] > 20:
continue
buffer.append(i[0])
else:
buffer.append('what?')
async for i in AsyncIter():
if i[0] > 20:
continue
buffer.append(i[0])
else:
buffer.append('what?')
buffer.append('end')

yielded, _ = run_async(test3())
Expand Down Expand Up @@ -1479,7 +1476,7 @@ async def foo():

with self.assertRaisesRegex(
TypeError,
r"async for' received an invalid object.*__aiter.*\: I"):
r"that does not implement __anext__"):

run_async(foo())

Expand Down Expand Up @@ -1508,25 +1505,6 @@ async def foo():

self.assertEqual(sys.getrefcount(aiter), refs_before)

def test_for_5(self):
class I:
async def __aiter__(self):
return self

def __anext__(self):
return 123

async def foo():
with self.assertWarnsRegex(DeprecationWarning, "legacy"):
async for i in I():
print('never going to happen')

with self.assertRaisesRegex(
TypeError,
"async for' received an invalid object.*__anext.*int"):

run_async(foo())

def test_for_6(self):
I = 0

Expand Down Expand Up @@ -1622,13 +1600,12 @@ async def main():
def test_for_7(self):
CNT = 0
class AI:
async def __aiter__(self):
def __aiter__(self):
1/0
async def foo():
nonlocal CNT
with self.assertWarnsRegex(DeprecationWarning, "legacy"):
async for i in AI():
CNT += 1
async for i in AI():
CNT += 1
CNT += 10
with self.assertRaises(ZeroDivisionError):
run_async(foo())
Expand All @@ -1652,37 +1629,6 @@ async def foo():
run_async(foo())
self.assertEqual(CNT, 0)

def test_for_9(self):
# Test that DeprecationWarning can safely be converted into
# an exception (__aiter__ should not have a chance to raise
# a ZeroDivisionError.)
class AI:
async def __aiter__(self):
1/0
async def foo():
async for i in AI():
pass

with self.assertRaises(DeprecationWarning):
with warnings.catch_warnings():
warnings.simplefilter("error")
run_async(foo())

def test_for_10(self):
# Test that DeprecationWarning can safely be converted into
# an exception.
class AI:
async def __aiter__(self):
pass
async def foo():
async for i in AI():
pass

with self.assertRaises(DeprecationWarning):
with warnings.catch_warnings():
warnings.simplefilter("error")
run_async(foo())

def test_for_11(self):
class F:
def __aiter__(self):
Expand All @@ -1703,24 +1649,6 @@ async def main():
err = c.exception
self.assertIsInstance(err.__cause__, ZeroDivisionError)

def test_for_12(self):
class F:
def __aiter__(self):
return self
def __await__(self):
1 / 0

async def main():
async for _ in F():
pass

with self.assertRaisesRegex(TypeError,
'an invalid object from __aiter__') as c:
main().send(None)

err = c.exception
self.assertIsInstance(err.__cause__, ZeroDivisionError)

def test_for_tuple(self):
class Done(Exception): pass

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Drop support of asynchronous __aiter__.
Loading