Skip to content

Commit baadb04

Browse files
JelleZijlstraJukkaL
authored andcommitted
Implement async generators (PEP 525) (#2711)
This closes #2616
1 parent c3b3177 commit baadb04

File tree

7 files changed

+230
-20
lines changed

7 files changed

+230
-20
lines changed

mypy/checker.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import itertools
44
import fnmatch
55
from contextlib import contextmanager
6+
import sys
67

78
from typing import (
89
Dict, Set, List, cast, Tuple, TypeVar, Union, Optional, NamedTuple, Iterator
@@ -311,12 +312,19 @@ def check_overlapping_overloads(self, defn: OverloadedFuncDef) -> None:
311312
# for functions decorated with `@types.coroutine` or
312313
# `@asyncio.coroutine`. Its single parameter corresponds to tr.
313314
#
315+
# PEP 525 adds a new type, the asynchronous generator, which was
316+
# first released in Python 3.6. Async generators are `async def`
317+
# functions that can also `yield` values. They can be parameterized
318+
# with two types, ty and tc, because they cannot return a value.
319+
#
314320
# There are several useful methods, each taking a type t and a
315321
# flag c indicating whether it's for a generator or coroutine:
316322
#
317323
# - is_generator_return_type(t, c) returns whether t is a Generator,
318324
# Iterator, Iterable (if not c), or Awaitable (if c), or
319325
# AwaitableGenerator (regardless of c).
326+
# - is_async_generator_return_type(t) returns whether t is an
327+
# AsyncGenerator.
320328
# - get_generator_yield_type(t, c) returns ty.
321329
# - get_generator_receive_type(t, c) returns tc.
322330
# - get_generator_return_type(t, c) returns tr.
@@ -338,11 +346,24 @@ def is_generator_return_type(self, typ: Type, is_coroutine: bool) -> bool:
338346
return True
339347
return isinstance(typ, Instance) and typ.type.fullname() == 'typing.AwaitableGenerator'
340348

349+
def is_async_generator_return_type(self, typ: Type) -> bool:
350+
"""Is `typ` a valid type for an async generator?
351+
352+
True if `typ` is a supertype of AsyncGenerator.
353+
"""
354+
try:
355+
agt = self.named_generic_type('typing.AsyncGenerator', [AnyType(), AnyType()])
356+
except KeyError:
357+
# we're running on a version of typing that doesn't have AsyncGenerator yet
358+
return False
359+
return is_subtype(agt, typ)
360+
341361
def get_generator_yield_type(self, return_type: Type, is_coroutine: bool) -> Type:
342362
"""Given the declared return type of a generator (t), return the type it yields (ty)."""
343363
if isinstance(return_type, AnyType):
344364
return AnyType()
345-
elif not self.is_generator_return_type(return_type, is_coroutine):
365+
elif (not self.is_generator_return_type(return_type, is_coroutine)
366+
and not self.is_async_generator_return_type(return_type)):
346367
# If the function doesn't have a proper Generator (or
347368
# Awaitable) return type, anything is permissible.
348369
return AnyType()
@@ -353,7 +374,7 @@ def get_generator_yield_type(self, return_type: Type, is_coroutine: bool) -> Typ
353374
# Awaitable: ty is Any.
354375
return AnyType()
355376
elif return_type.args:
356-
# AwaitableGenerator, Generator, Iterator, or Iterable; ty is args[0].
377+
# AwaitableGenerator, Generator, AsyncGenerator, Iterator, or Iterable; ty is args[0].
357378
ret_type = return_type.args[0]
358379
# TODO not best fix, better have dedicated yield token
359380
if isinstance(ret_type, NoneTyp):
@@ -373,7 +394,8 @@ def get_generator_receive_type(self, return_type: Type, is_coroutine: bool) -> T
373394
"""Given a declared generator return type (t), return the type its yield receives (tc)."""
374395
if isinstance(return_type, AnyType):
375396
return AnyType()
376-
elif not self.is_generator_return_type(return_type, is_coroutine):
397+
elif (not self.is_generator_return_type(return_type, is_coroutine)
398+
and not self.is_async_generator_return_type(return_type)):
377399
# If the function doesn't have a proper Generator (or
378400
# Awaitable) return type, anything is permissible.
379401
return AnyType()
@@ -387,6 +409,8 @@ def get_generator_receive_type(self, return_type: Type, is_coroutine: bool) -> T
387409
and len(return_type.args) >= 3):
388410
# Generator: tc is args[1].
389411
return return_type.args[1]
412+
elif return_type.type.fullname() == 'typing.AsyncGenerator' and len(return_type.args) >= 2:
413+
return return_type.args[1]
390414
else:
391415
# `return_type` is a supertype of Generator, so callers won't be able to send it
392416
# values. IOW, tc is None.
@@ -537,8 +561,12 @@ def is_implicit_any(t: Type) -> bool:
537561

538562
# Check that Generator functions have the appropriate return type.
539563
if defn.is_generator:
540-
if not self.is_generator_return_type(typ.ret_type, defn.is_coroutine):
541-
self.fail(messages.INVALID_RETURN_TYPE_FOR_GENERATOR, typ)
564+
if defn.is_async_generator:
565+
if not self.is_async_generator_return_type(typ.ret_type):
566+
self.fail(messages.INVALID_RETURN_TYPE_FOR_ASYNC_GENERATOR, typ)
567+
else:
568+
if not self.is_generator_return_type(typ.ret_type, defn.is_coroutine):
569+
self.fail(messages.INVALID_RETURN_TYPE_FOR_GENERATOR, typ)
542570

543571
# Python 2 generators aren't allowed to return values.
544572
if (self.options.python_version[0] == 2 and
@@ -1743,6 +1771,11 @@ def check_return_stmt(self, s: ReturnStmt) -> None:
17431771
if s.expr:
17441772
# Return with a value.
17451773
typ = self.expr_checker.accept(s.expr, return_type)
1774+
1775+
if defn.is_async_generator:
1776+
self.fail("'return' with value in async generator is not allowed", s)
1777+
return
1778+
17461779
# Returning a value of type Any is always fine.
17471780
if isinstance(typ, AnyType):
17481781
# (Unless you asked to be warned in that case, and the

mypy/messages.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
INVALID_EXCEPTION_TYPE = 'Exception type must be derived from BaseException'
3535
INVALID_RETURN_TYPE_FOR_GENERATOR = \
3636
'The return type of a generator function should be "Generator" or one of its supertypes'
37+
INVALID_RETURN_TYPE_FOR_ASYNC_GENERATOR = \
38+
'The return type of an async generator function should be "AsyncGenerator" or one of its ' \
39+
'supertypes'
3740
INVALID_GENERATOR_RETURN_ITEM_TYPE = \
3841
'The return type of a generator function must be None in its third type parameter in Python 2'
3942
YIELD_VALUE_EXPECTED = 'Yield value expected'

mypy/nodes.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -481,15 +481,16 @@ class FuncItem(FuncBase):
481481
is_overload = False
482482
is_generator = False # Contains a yield statement?
483483
is_coroutine = False # Defined using 'async def' syntax?
484+
is_async_generator = False # Is an async def generator?
484485
is_awaitable_coroutine = False # Decorated with '@{typing,asyncio}.coroutine'?
485486
is_static = False # Uses @staticmethod?
486487
is_class = False # Uses @classmethod?
487488
# Variants of function with type variables with values expanded
488489
expanded = None # type: List[FuncItem]
489490

490491
FLAGS = [
491-
'is_overload', 'is_generator', 'is_coroutine', 'is_awaitable_coroutine',
492-
'is_static', 'is_class',
492+
'is_overload', 'is_generator', 'is_coroutine', 'is_async_generator',
493+
'is_awaitable_coroutine', 'is_static', 'is_class',
493494
]
494495

495496
def __init__(self, arguments: List[Argument], body: 'Block',

mypy/semanal.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -324,11 +324,15 @@ def visit_func_def(self, defn: FuncDef) -> None:
324324
self.errors.push_function(defn.name())
325325
self.analyze_function(defn)
326326
if defn.is_coroutine and isinstance(defn.type, CallableType):
327-
# A coroutine defined as `async def foo(...) -> T: ...`
328-
# has external return type `Awaitable[T]`.
329-
defn.type = defn.type.copy_modified(
330-
ret_type = self.named_type_or_none('typing.Awaitable',
331-
[defn.type.ret_type]))
327+
if defn.is_async_generator:
328+
# Async generator types are handled elsewhere
329+
pass
330+
else:
331+
# A coroutine defined as `async def foo(...) -> T: ...`
332+
# has external return type `Awaitable[T]`.
333+
defn.type = defn.type.copy_modified(
334+
ret_type = self.named_type_or_none('typing.Awaitable',
335+
[defn.type.ret_type]))
332336
self.errors.pop_function()
333337

334338
def prepare_method_signature(self, func: FuncDef) -> None:
@@ -2861,7 +2865,11 @@ def visit_yield_expr(self, expr: YieldExpr) -> None:
28612865
self.fail("'yield' outside function", expr, True, blocker=True)
28622866
else:
28632867
if self.function_stack[-1].is_coroutine:
2864-
self.fail("'yield' in async function", expr, True, blocker=True)
2868+
if self.options.python_version < (3, 6):
2869+
self.fail("'yield' in async function", expr, True, blocker=True)
2870+
else:
2871+
self.function_stack[-1].is_generator = True
2872+
self.function_stack[-1].is_async_generator = True
28652873
else:
28662874
self.function_stack[-1].is_generator = True
28672875
if expr.expr:

test-data/unit/check-async-await.test

Lines changed: 154 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -326,18 +326,15 @@ async def f() -> None:
326326
[builtins fixtures/async_await.pyi]
327327

328328
[case testNoYieldInAsyncDef]
329+
# flags: --python-version 3.5
329330

330331
async def f():
331-
yield None
332+
yield None # E: 'yield' in async function
332333
async def g():
333-
yield
334+
yield # E: 'yield' in async function
334335
async def h():
335-
x = yield
336+
x = yield # E: 'yield' in async function
336337
[builtins fixtures/async_await.pyi]
337-
[out]
338-
main:3: error: 'yield' in async function
339-
main:5: error: 'yield' in async function
340-
main:7: error: 'yield' in async function
341338

342339
[case testNoYieldFromInAsyncDef]
343340

@@ -410,6 +407,156 @@ def f() -> Generator[int, str, int]:
410407
[builtins fixtures/async_await.pyi]
411408
[out]
412409

410+
-- Async generators (PEP 525), some test cases adapted from the PEP text
411+
-- ---------------------------------------------------------------------
412+
413+
[case testAsyncGenerator]
414+
# flags: --python-version 3.6
415+
from typing import AsyncGenerator, Generator
416+
417+
async def f() -> int:
418+
return 42
419+
420+
async def g() -> AsyncGenerator[int, None]:
421+
value = await f()
422+
reveal_type(value) # E: Revealed type is 'builtins.int*'
423+
yield value
424+
425+
yield 'not an int' # E: Incompatible types in yield (actual type "str", expected type "int")
426+
# return without a value is fine
427+
return
428+
reveal_type(g) # E: Revealed type is 'def () -> typing.AsyncGenerator[builtins.int, void]'
429+
reveal_type(g()) # E: Revealed type is 'typing.AsyncGenerator[builtins.int, void]'
430+
431+
async def h() -> None:
432+
async for item in g():
433+
reveal_type(item) # E: Revealed type is 'builtins.int*'
434+
435+
async def wrong_return() -> Generator[int, None, None]: # E: The return type of an async generator function should be "AsyncGenerator" or one of its supertypes
436+
yield 3
437+
438+
[builtins fixtures/dict.pyi]
439+
440+
[case testAsyncGeneratorReturnIterator]
441+
# flags: --python-version 3.6
442+
from typing import AsyncIterator
443+
444+
async def gen() -> AsyncIterator[int]:
445+
yield 3
446+
447+
yield 'not an int' # E: Incompatible types in yield (actual type "str", expected type "int")
448+
449+
async def use_gen() -> None:
450+
async for item in gen():
451+
reveal_type(item) # E: Revealed type is 'builtins.int*'
452+
453+
[builtins fixtures/dict.pyi]
454+
455+
[case testAsyncGeneratorManualIter]
456+
# flags: --python-version 3.6
457+
from typing import AsyncGenerator
458+
459+
async def genfunc() -> AsyncGenerator[int, None]:
460+
yield 1
461+
yield 2
462+
463+
async def user() -> None:
464+
gen = genfunc()
465+
466+
reveal_type(gen.__aiter__()) # E: Revealed type is 'typing.AsyncGenerator[builtins.int*, void]'
467+
468+
reveal_type(await gen.__anext__()) # E: Revealed type is 'builtins.int*'
469+
470+
[builtins fixtures/dict.pyi]
471+
472+
[case testAsyncGeneratorAsend]
473+
# flags: --fast-parser --python-version 3.6
474+
from typing import AsyncGenerator
475+
476+
async def f() -> None:
477+
pass
478+
479+
async def gen() -> AsyncGenerator[int, str]:
480+
await f()
481+
v = yield 42
482+
reveal_type(v) # E: Revealed type is 'builtins.str'
483+
await f()
484+
485+
async def h() -> None:
486+
g = gen()
487+
await g.asend(()) # E: Argument 1 to "asend" of "AsyncGenerator" has incompatible type "Tuple[]"; expected "str"
488+
reveal_type(await g.asend('hello')) # E: Revealed type is 'builtins.int*'
489+
490+
[builtins fixtures/dict.pyi]
491+
492+
[case testAsyncGeneratorAthrow]
493+
# flags: --fast-parser --python-version 3.6
494+
from typing import AsyncGenerator
495+
496+
async def gen() -> AsyncGenerator[str, int]:
497+
try:
498+
yield 'hello'
499+
except BaseException:
500+
yield 'world'
501+
502+
async def h() -> None:
503+
g = gen()
504+
v = await g.asend(1)
505+
reveal_type(v) # E: Revealed type is 'builtins.str*'
506+
reveal_type(await g.athrow(BaseException)) # E: Revealed type is 'builtins.str*'
507+
508+
[builtins fixtures/dict.pyi]
509+
510+
[case testAsyncGeneratorNoSyncIteration]
511+
# flags: --fast-parser --python-version 3.6
512+
from typing import AsyncGenerator
513+
514+
async def gen() -> AsyncGenerator[int, None]:
515+
for i in (1, 2, 3):
516+
yield i
517+
518+
def h() -> None:
519+
for i in gen():
520+
pass
521+
522+
[builtins fixtures/dict.pyi]
523+
524+
[out]
525+
main:9: error: Iterable expected
526+
main:9: error: AsyncGenerator[int, None] has no attribute "__iter__"; maybe "__aiter__"?
527+
528+
[case testAsyncGeneratorNoYieldFrom]
529+
# flags: --fast-parser --python-version 3.6
530+
from typing import AsyncGenerator
531+
532+
async def f() -> AsyncGenerator[int, None]:
533+
pass
534+
535+
async def gen() -> AsyncGenerator[int, None]:
536+
yield from f() # E: 'yield from' in async function
537+
538+
[builtins fixtures/dict.pyi]
539+
540+
[case testAsyncGeneratorNoReturnWithValue]
541+
# flags: --fast-parser --python-version 3.6
542+
from typing import AsyncGenerator
543+
544+
async def return_int() -> AsyncGenerator[int, None]:
545+
yield 1
546+
return 42 # E: 'return' with value in async generator is not allowed
547+
548+
async def return_none() -> AsyncGenerator[int, None]:
549+
yield 1
550+
return None # E: 'return' with value in async generator is not allowed
551+
552+
def f() -> None:
553+
return
554+
555+
async def return_f() -> AsyncGenerator[int, None]:
556+
yield 1
557+
return f() # E: 'return' with value in async generator is not allowed
558+
559+
[builtins fixtures/dict.pyi]
413560

414561
-- The full matrix of coroutine compatibility
415562
-- ------------------------------------------

test-data/unit/fixtures/dict.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,5 @@ class tuple: pass
3838
class function: pass
3939
class float: pass
4040
class bool: pass
41+
42+
class BaseException: pass

test-data/unit/lib-stub/typing.pyi

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,22 @@ class Generator(Iterator[T], Generic[T, U, V]):
5959
@abstractmethod
6060
def __iter__(self) -> 'Generator[T, U, V]': pass
6161

62+
class AsyncGenerator(AsyncIterator[T], Generic[T, U]):
63+
@abstractmethod
64+
def __anext__(self) -> Awaitable[T]: pass
65+
66+
@abstractmethod
67+
def asend(self, value: U) -> Awaitable[T]: pass
68+
69+
@abstractmethod
70+
def athrow(self, typ: Any, val: Any=None, tb: Any=None) -> Awaitable[T]: pass
71+
72+
@abstractmethod
73+
def aclose(self) -> Awaitable[T]: pass
74+
75+
@abstractmethod
76+
def __aiter__(self) -> 'AsyncGenerator[T, U]': pass
77+
6278
class Awaitable(Generic[T]):
6379
@abstractmethod
6480
def __await__(self) -> Generator[Any, Any, T]: pass

0 commit comments

Comments
 (0)