Skip to content

Commit a6f6edb

Browse files
committed
Issue python#27243: Fix __aiter__ protocol
1 parent ebe95fd commit a6f6edb

File tree

13 files changed

+291
-32
lines changed

13 files changed

+291
-32
lines changed

Doc/glossary.rst

+3-4
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,12 @@ Glossary
7676

7777
asynchronous iterable
7878
An object, that can be used in an :keyword:`async for` statement.
79-
Must return an :term:`awaitable` from its :meth:`__aiter__` method,
80-
which should in turn be resolved in an :term:`asynchronous iterator`
81-
object. Introduced by :pep:`492`.
79+
Must return an :term:`asyncronous iterator` from its
80+
:meth:`__aiter__` method. Introduced by :pep:`492`.
8281

8382
asynchronous iterator
8483
An object that implements :meth:`__aiter__` and :meth:`__anext__`
85-
methods, that must return :term:`awaitable` objects.
84+
methods. ``__anext__`` must return an :term:`awaitable` object.
8685
:keyword:`async for` resolves awaitable returned from asynchronous
8786
iterator's :meth:`__anext__` method until it raises
8887
:exc:`StopAsyncIteration` exception. Introduced by :pep:`492`.

Doc/reference/compound_stmts.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,7 @@ The following code::
726726
Is semantically equivalent to::
727727

728728
iter = (ITER)
729-
iter = await type(iter).__aiter__(iter)
729+
iter = type(iter).__aiter__(iter)
730730
running = True
731731
while running:
732732
try:

Doc/reference/datamodel.rst

+46-2
Original file line numberDiff line numberDiff line change
@@ -2359,6 +2359,7 @@ generators, coroutines do not directly support iteration.
23592359
Coroutine objects are automatically closed using the above process when
23602360
they are about to be destroyed.
23612361

2362+
.. _async-iterators:
23622363

23632364
Asynchronous Iterators
23642365
----------------------
@@ -2371,7 +2372,7 @@ Asynchronous iterators can be used in an :keyword:`async for` statement.
23712372

23722373
.. method:: object.__aiter__(self)
23732374

2374-
Must return an *awaitable* resulting in an *asynchronous iterator* object.
2375+
Must return an *asynchronous iterator* object.
23752376

23762377
.. method:: object.__anext__(self)
23772378

@@ -2384,7 +2385,7 @@ An example of an asynchronous iterable object::
23842385
async def readline(self):
23852386
...
23862387

2387-
async def __aiter__(self):
2388+
def __aiter__(self):
23882389
return self
23892390

23902391
async def __anext__(self):
@@ -2395,6 +2396,49 @@ An example of an asynchronous iterable object::
23952396

23962397
.. versionadded:: 3.5
23972398

2399+
.. note::
2400+
2401+
.. versionchanged:: 3.5.2
2402+
Starting with CPython 3.5.2, ``__aiter__`` can directly return
2403+
:term:`asynchronous iterators <asynchronous iterator>`. Returning
2404+
an :term:`awaitable` object will result in a
2405+
:exc:`PendingDeprecationWarning`.
2406+
2407+
The recommended way of writing backwards compatible code in
2408+
CPython 3.5.x is to continue returning awaitables from
2409+
``__aiter__``. If you want to avoid the PendingDeprecationWarning
2410+
and keep the code backwards compatible, the following decorator
2411+
can be used::
2412+
2413+
import functools
2414+
import sys
2415+
2416+
if sys.version_info < (3, 5, 2):
2417+
def aiter_compat(func):
2418+
@functools.wraps(func)
2419+
async def wrapper(self):
2420+
return func(self)
2421+
return wrapper
2422+
else:
2423+
def aiter_compat(func):
2424+
return func
2425+
2426+
Example::
2427+
2428+
class AsyncIterator:
2429+
2430+
@aiter_compat
2431+
def __aiter__(self):
2432+
return self
2433+
2434+
async def __anext__(self):
2435+
...
2436+
2437+
Starting with CPython 3.6, the :exc:`PendingDeprecationWarning`
2438+
will be replaced with the :exc:`DeprecationWarning`.
2439+
In CPython 3.7, returning an awaitable from ``__aiter__`` will
2440+
result in a :exc:`RuntimeError`.
2441+
23982442

23992443
Asynchronous Context Managers
24002444
-----------------------------

Doc/whatsnew/3.5.rst

+13
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,19 @@ be used inside a coroutine function declared with :keyword:`async def`.
247247
Coroutine functions are intended to be run inside a compatible event loop,
248248
such as the :ref:`asyncio loop <asyncio-event-loop>`.
249249

250+
251+
.. note::
252+
253+
.. versionchanged:: 3.5.2
254+
Starting with CPython 3.5.2, ``__aiter__`` can directly return
255+
:term:`asynchronous iterators <asynchronous iterator>`. Returning
256+
an :term:`awaitable` object will result in a
257+
:exc:`PendingDeprecationWarning`.
258+
259+
See more details in the :ref:`async-iterators` documentation
260+
section.
261+
262+
250263
.. seealso::
251264

252265
:pep:`492` -- Coroutines with async and await syntax

Include/genobject.h

+3
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ typedef struct {
5454
PyAPI_DATA(PyTypeObject) PyCoro_Type;
5555
PyAPI_DATA(PyTypeObject) _PyCoroWrapper_Type;
5656

57+
PyAPI_DATA(PyTypeObject) _PyAIterWrapper_Type;
58+
PyObject *_PyAIterWrapper_New(PyObject *aiter);
59+
5760
#define PyCoro_CheckExact(op) (Py_TYPE(op) == &PyCoro_Type)
5861
PyObject *_PyCoro_GetAwaitableIter(PyObject *o);
5962
PyAPI_FUNC(PyObject *) PyCoro_New(struct _frame *,

Lib/_collections_abc.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ class AsyncIterable(metaclass=ABCMeta):
156156
__slots__ = ()
157157

158158
@abstractmethod
159-
async def __aiter__(self):
159+
def __aiter__(self):
160160
return AsyncIterator()
161161

162162
@classmethod
@@ -176,7 +176,7 @@ async def __anext__(self):
176176
"""Return the next item or raise StopAsyncIteration when exhausted."""
177177
raise StopAsyncIteration
178178

179-
async def __aiter__(self):
179+
def __aiter__(self):
180180
return self
181181

182182
@classmethod

Lib/asyncio/compat.py

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
PY34 = sys.version_info >= (3, 4)
66
PY35 = sys.version_info >= (3, 5)
7+
PY352 = sys.version_info >= (3, 5, 2)
78

89

910
def flatten_list_bytes(list_of_data):

Lib/asyncio/streams.py

+6
Original file line numberDiff line numberDiff line change
@@ -689,3 +689,9 @@ def __anext__(self):
689689
if val == b'':
690690
raise StopAsyncIteration
691691
return val
692+
693+
if compat.PY352:
694+
# In Python 3.5.2 and greater, __aiter__ should return
695+
# the asynchronous iterator directly.
696+
def __aiter__(self):
697+
return self

Lib/test/test_coroutines.py

+78-20
Original file line numberDiff line numberDiff line change
@@ -1255,8 +1255,9 @@ async def __anext__(self):
12551255

12561256
buffer = []
12571257
async def test1():
1258-
async for i1, i2 in AsyncIter():
1259-
buffer.append(i1 + i2)
1258+
with self.assertWarnsRegex(PendingDeprecationWarning, "legacy"):
1259+
async for i1, i2 in AsyncIter():
1260+
buffer.append(i1 + i2)
12601261

12611262
yielded, _ = run_async(test1())
12621263
# Make sure that __aiter__ was called only once
@@ -1268,12 +1269,13 @@ async def test1():
12681269
buffer = []
12691270
async def test2():
12701271
nonlocal buffer
1271-
async for i in AsyncIter():
1272-
buffer.append(i[0])
1273-
if i[0] == 20:
1274-
break
1275-
else:
1276-
buffer.append('what?')
1272+
with self.assertWarnsRegex(PendingDeprecationWarning, "legacy"):
1273+
async for i in AsyncIter():
1274+
buffer.append(i[0])
1275+
if i[0] == 20:
1276+
break
1277+
else:
1278+
buffer.append('what?')
12771279
buffer.append('end')
12781280

12791281
yielded, _ = run_async(test2())
@@ -1286,12 +1288,13 @@ async def test2():
12861288
buffer = []
12871289
async def test3():
12881290
nonlocal buffer
1289-
async for i in AsyncIter():
1290-
if i[0] > 20:
1291-
continue
1292-
buffer.append(i[0])
1293-
else:
1294-
buffer.append('what?')
1291+
with self.assertWarnsRegex(PendingDeprecationWarning, "legacy"):
1292+
async for i in AsyncIter():
1293+
if i[0] > 20:
1294+
continue
1295+
buffer.append(i[0])
1296+
else:
1297+
buffer.append('what?')
12951298
buffer.append('end')
12961299

12971300
yielded, _ = run_async(test3())
@@ -1338,7 +1341,7 @@ async def foo():
13381341

13391342
def test_for_4(self):
13401343
class I:
1341-
async def __aiter__(self):
1344+
def __aiter__(self):
13421345
return self
13431346

13441347
def __anext__(self):
@@ -1368,8 +1371,9 @@ def __anext__(self):
13681371
return 123
13691372

13701373
async def foo():
1371-
async for i in I():
1372-
print('never going to happen')
1374+
with self.assertWarnsRegex(PendingDeprecationWarning, "legacy"):
1375+
async for i in I():
1376+
print('never going to happen')
13731377

13741378
with self.assertRaisesRegex(
13751379
TypeError,
@@ -1393,7 +1397,7 @@ class Iterable:
13931397
def __init__(self):
13941398
self.i = 0
13951399

1396-
async def __aiter__(self):
1400+
def __aiter__(self):
13971401
return self
13981402

13991403
async def __anext__(self):
@@ -1417,7 +1421,11 @@ async def main():
14171421
I += 1
14181422
I += 1000
14191423

1420-
run_async(main())
1424+
with warnings.catch_warnings():
1425+
warnings.simplefilter("error")
1426+
# Test that __aiter__ that returns an asyncronous iterator
1427+
# directly does not throw any warnings.
1428+
run_async(main())
14211429
self.assertEqual(I, 111011)
14221430

14231431
self.assertEqual(sys.getrefcount(manager), mrefs_before)
@@ -1470,15 +1478,65 @@ def test_for_7(self):
14701478
class AI:
14711479
async def __aiter__(self):
14721480
1/0
1481+
async def foo():
1482+
nonlocal CNT
1483+
with self.assertWarnsRegex(PendingDeprecationWarning, "legacy"):
1484+
async for i in AI():
1485+
CNT += 1
1486+
CNT += 10
1487+
with self.assertRaises(ZeroDivisionError):
1488+
run_async(foo())
1489+
self.assertEqual(CNT, 0)
1490+
1491+
def test_for_8(self):
1492+
CNT = 0
1493+
class AI:
1494+
def __aiter__(self):
1495+
1/0
14731496
async def foo():
14741497
nonlocal CNT
14751498
async for i in AI():
14761499
CNT += 1
14771500
CNT += 10
14781501
with self.assertRaises(ZeroDivisionError):
1479-
run_async(foo())
1502+
with warnings.catch_warnings():
1503+
warnings.simplefilter("error")
1504+
# Test that if __aiter__ raises an exception it propagates
1505+
# without any kind of warning.
1506+
run_async(foo())
14801507
self.assertEqual(CNT, 0)
14811508

1509+
def test_for_9(self):
1510+
# Test that PendingDeprecationWarning can safely be converted into
1511+
# an exception (__aiter__ should not have a chance to raise
1512+
# a ZeroDivisionError.)
1513+
class AI:
1514+
async def __aiter__(self):
1515+
1/0
1516+
async def foo():
1517+
async for i in AI():
1518+
pass
1519+
1520+
with self.assertRaises(PendingDeprecationWarning):
1521+
with warnings.catch_warnings():
1522+
warnings.simplefilter("error")
1523+
run_async(foo())
1524+
1525+
def test_for_10(self):
1526+
# Test that PendingDeprecationWarning can safely be converted into
1527+
# an exception.
1528+
class AI:
1529+
async def __aiter__(self):
1530+
pass
1531+
async def foo():
1532+
async for i in AI():
1533+
pass
1534+
1535+
with self.assertRaises(PendingDeprecationWarning):
1536+
with warnings.catch_warnings():
1537+
warnings.simplefilter("error")
1538+
run_async(foo())
1539+
14821540
def test_copy(self):
14831541
async def func(): pass
14841542
coro = func()

Lib/test/test_grammar.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1076,7 +1076,7 @@ def test_async_for(self):
10761076
class Done(Exception): pass
10771077

10781078
class AIter:
1079-
async def __aiter__(self):
1079+
def __aiter__(self):
10801080
return self
10811081
async def __anext__(self):
10821082
raise StopAsyncIteration

Misc/NEWS

+5
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ Core and Builtins
130130
- Issue #25887: Raise a RuntimeError when a coroutine object is awaited
131131
more than once.
132132

133+
- Issue #27243: Update the __aiter__ protocol: instead of returning
134+
an awaitable that resolves to an asynchronous iterator, the asynchronous
135+
iterator should be returned directly. Doing the former will trigger a
136+
PendingDeprecationWarning.
137+
133138

134139
Library
135140
-------

0 commit comments

Comments
 (0)