Skip to content

Commit 340d34e

Browse files
gvanrossumddfisher
authored andcommitted
Phase 2 of async/await (#1946)
This PR adds support for decorating either generators or `async def` functions with `@types.coroutine` or `@asyncio.coroutine`. This will fix #1453.
1 parent f10bfc7 commit 340d34e

File tree

12 files changed

+1200
-58
lines changed

12 files changed

+1200
-58
lines changed

misc/async_matrix.py

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
#!/usr/bin/env python3
2+
"""Test various combinations of generators/coroutines.
3+
4+
This was used to cross-check the errors in the test case
5+
testFullCoroutineMatrix in test-data/unit/check-async-await.test.
6+
"""
7+
8+
import sys
9+
from types import coroutine
10+
from typing import Any, AsyncIterator, Awaitable, Generator, Iterator
11+
12+
# The various things you might try to use in `await` or `yield from`.
13+
14+
def plain_generator() -> Generator[str, None, int]:
15+
yield 'a'
16+
return 1
17+
18+
async def plain_coroutine() -> int:
19+
return 1
20+
21+
@coroutine
22+
def decorated_generator() -> Generator[str, None, int]:
23+
yield 'a'
24+
return 1
25+
26+
@coroutine
27+
async def decorated_coroutine() -> int:
28+
return 1
29+
30+
class It(Iterator[str]):
31+
stop = False
32+
def __iter__(self) -> 'It':
33+
return self
34+
def __next__(self) -> str:
35+
if self.stop:
36+
raise StopIteration('end')
37+
else:
38+
self.stop = True
39+
return 'a'
40+
41+
def other_iterator() -> It:
42+
return It()
43+
44+
class Aw(Awaitable[int]):
45+
def __await__(self) -> Generator[str, Any, int]:
46+
yield 'a'
47+
return 1
48+
49+
def other_coroutine() -> Aw:
50+
return Aw()
51+
52+
# The various contexts in which `await` or `yield from` might occur.
53+
54+
def plain_host_generator(func) -> Generator[str, None, None]:
55+
yield 'a'
56+
x = 0
57+
f = func()
58+
try:
59+
x = yield from f
60+
finally:
61+
try:
62+
f.close()
63+
except AttributeError:
64+
pass
65+
66+
async def plain_host_coroutine(func) -> None:
67+
x = 0
68+
x = await func()
69+
70+
@coroutine
71+
def decorated_host_generator(func) -> Generator[str, None, None]:
72+
yield 'a'
73+
x = 0
74+
f = func()
75+
try:
76+
x = yield from f
77+
finally:
78+
try:
79+
f.close()
80+
except AttributeError:
81+
pass
82+
83+
@coroutine
84+
async def decorated_host_coroutine(func) -> None:
85+
x = 0
86+
x = await func()
87+
88+
# Main driver.
89+
90+
def main():
91+
verbose = ('-v' in sys.argv)
92+
for host in [plain_host_generator, plain_host_coroutine,
93+
decorated_host_generator, decorated_host_coroutine]:
94+
print()
95+
print("==== Host:", host.__name__)
96+
for func in [plain_generator, plain_coroutine,
97+
decorated_generator, decorated_coroutine,
98+
other_iterator, other_coroutine]:
99+
print(" ---- Func:", func.__name__)
100+
try:
101+
f = host(func)
102+
for i in range(10):
103+
try:
104+
x = f.send(None)
105+
if verbose:
106+
print(" yield:", x)
107+
except StopIteration as e:
108+
if verbose:
109+
print(" stop:", e.value)
110+
break
111+
else:
112+
if verbose:
113+
print(" ???? still going")
114+
except Exception as e:
115+
print(" error:", repr(e))
116+
117+
# Run main().
118+
119+
if __name__ == '__main__':
120+
main()

mypy/checker.py

+98-46
Original file line numberDiff line numberDiff line change
@@ -265,54 +265,67 @@ def check_overlapping_overloads(self, defn: OverloadedFuncDef) -> None:
265265
# in PEP 492 and only available in Python >= 3.5.
266266
#
267267
# Classic generators can be parameterized with three types:
268-
# - ty is the yield type (the type of y in `yield y`)
269-
# - ts is the type received by yield (the type of s in `s = yield`)
270-
# (it's named `ts` after `send()`, since `tr` is `return`).
271-
# - tr is the return type (the type of r in `return r`)
268+
# - ty is the Yield type (the type of y in `yield y`)
269+
# - tc is the type reCeived by yield (the type of c in `c = yield`).
270+
# - tr is the Return type (the type of r in `return r`)
272271
#
273272
# A classic generator must define a return type that's either
274-
# `Generator[ty, ts, tr]`, Iterator[ty], or Iterable[ty] (or
275-
# object or Any). If ts/tr are not given, both are Void.
273+
# `Generator[ty, tc, tr]`, Iterator[ty], or Iterable[ty] (or
274+
# object or Any). If tc/tr are not given, both are Void.
276275
#
277276
# A coroutine must define a return type corresponding to tr; the
278277
# other two are unconstrained. The "external" return type (seen
279278
# by the caller) is Awaitable[tr].
280279
#
280+
# In addition, there's the synthetic type AwaitableGenerator: it
281+
# inherits from both Awaitable and Generator and can be used both
282+
# in `yield from` and in `await`. This type is set automatically
283+
# for functions decorated with `@types.coroutine` or
284+
# `@asyncio.coroutine`. Its single parameter corresponds to tr.
285+
#
281286
# There are several useful methods, each taking a type t and a
282287
# flag c indicating whether it's for a generator or coroutine:
283288
#
284289
# - is_generator_return_type(t, c) returns whether t is a Generator,
285-
# Iterator, Iterable (if not c), or Awaitable (if c).
290+
# Iterator, Iterable (if not c), or Awaitable (if c), or
291+
# AwaitableGenerator (regardless of c).
286292
# - get_generator_yield_type(t, c) returns ty.
287-
# - get_generator_receive_type(t, c) returns ts.
293+
# - get_generator_receive_type(t, c) returns tc.
288294
# - get_generator_return_type(t, c) returns tr.
289295

290296
def is_generator_return_type(self, typ: Type, is_coroutine: bool) -> bool:
291297
"""Is `typ` a valid type for a generator/coroutine?
292298
293-
True if either Generator or Awaitable is a supertype of `typ`.
299+
True if `typ` is a *supertype* of Generator or Awaitable.
300+
Also true it it's *exactly* AwaitableGenerator (modulo type parameters).
294301
"""
295302
if is_coroutine:
303+
# This means we're in Python 3.5 or later.
296304
at = self.named_generic_type('typing.Awaitable', [AnyType()])
297-
return is_subtype(at, typ)
305+
if is_subtype(at, typ):
306+
return True
298307
else:
299308
gt = self.named_generic_type('typing.Generator', [AnyType(), AnyType(), AnyType()])
300-
return is_subtype(gt, typ)
309+
if is_subtype(gt, typ):
310+
return True
311+
return isinstance(typ, Instance) and typ.type.fullname() == 'typing.AwaitableGenerator'
301312

302313
def get_generator_yield_type(self, return_type: Type, is_coroutine: bool) -> Type:
303314
"""Given the declared return type of a generator (t), return the type it yields (ty)."""
304315
if isinstance(return_type, AnyType):
305316
return AnyType()
306317
elif not self.is_generator_return_type(return_type, is_coroutine):
307-
# If the function doesn't have a proper Generator (or superclass) return type, anything
308-
# is permissible.
318+
# If the function doesn't have a proper Generator (or
319+
# Awaitable) return type, anything is permissible.
309320
return AnyType()
310321
elif not isinstance(return_type, Instance):
311322
# Same as above, but written as a separate branch so the typechecker can understand.
312323
return AnyType()
313324
elif return_type.type.fullname() == 'typing.Awaitable':
325+
# Awaitable: ty is Any.
314326
return AnyType()
315327
elif return_type.args:
328+
# AwaitableGenerator, Generator, Iterator, or Iterable; ty is args[0].
316329
ret_type = return_type.args[0]
317330
# TODO not best fix, better have dedicated yield token
318331
if isinstance(ret_type, NoneTyp):
@@ -324,33 +337,31 @@ def get_generator_yield_type(self, return_type: Type, is_coroutine: bool) -> Typ
324337
else:
325338
# If the function's declared supertype of Generator has no type
326339
# parameters (i.e. is `object`), then the yielded values can't
327-
# be accessed so any type is acceptable.
340+
# be accessed so any type is acceptable. IOW, ty is Any.
341+
# (However, see https://github.com/python/mypy/issues/1933)
328342
return AnyType()
329343

330344
def get_generator_receive_type(self, return_type: Type, is_coroutine: bool) -> Type:
331-
"""Given a declared generator return type (t), return the type its yield receives (ts)."""
345+
"""Given a declared generator return type (t), return the type its yield receives (tc)."""
332346
if isinstance(return_type, AnyType):
333347
return AnyType()
334348
elif not self.is_generator_return_type(return_type, is_coroutine):
335-
# If the function doesn't have a proper Generator (or superclass) return type, anything
336-
# is permissible.
349+
# If the function doesn't have a proper Generator (or
350+
# Awaitable) return type, anything is permissible.
337351
return AnyType()
338352
elif not isinstance(return_type, Instance):
339353
# Same as above, but written as a separate branch so the typechecker can understand.
340354
return AnyType()
341-
elif return_type.type.fullname() == 'typing.Generator':
342-
# Generator is one of the two types which specify the type of values it can receive.
343-
if len(return_type.args) == 3:
344-
return return_type.args[1]
345-
else:
346-
return AnyType()
347355
elif return_type.type.fullname() == 'typing.Awaitable':
348-
# Awaitable is one of the two types which specify the type of values it can receive.
349-
# According to the stub this is always `Any`.
356+
# Awaitable, AwaitableGenerator: tc is Any.
350357
return AnyType()
358+
elif (return_type.type.fullname() in ('typing.Generator', 'typing.AwaitableGenerator')
359+
and len(return_type.args) >= 3):
360+
# Generator: tc is args[1].
361+
return return_type.args[1]
351362
else:
352363
# `return_type` is a supertype of Generator, so callers won't be able to send it
353-
# values.
364+
# values. IOW, tc is None.
354365
if experiments.STRICT_OPTIONAL:
355366
return NoneTyp(is_ret_type=True)
356367
else:
@@ -361,29 +372,21 @@ def get_generator_return_type(self, return_type: Type, is_coroutine: bool) -> Ty
361372
if isinstance(return_type, AnyType):
362373
return AnyType()
363374
elif not self.is_generator_return_type(return_type, is_coroutine):
364-
# If the function doesn't have a proper Generator (or superclass) return type, anything
365-
# is permissible.
375+
# If the function doesn't have a proper Generator (or
376+
# Awaitable) return type, anything is permissible.
366377
return AnyType()
367378
elif not isinstance(return_type, Instance):
368379
# Same as above, but written as a separate branch so the typechecker can understand.
369380
return AnyType()
370-
elif return_type.type.fullname() == 'typing.Generator':
371-
# Generator is one of the two types which specify the type of values it returns into
372-
# `yield from` expressions (using a `return` statement).
373-
if len(return_type.args) == 3:
374-
return return_type.args[2]
375-
else:
376-
return AnyType()
377-
elif return_type.type.fullname() == 'typing.Awaitable':
378-
# Awaitable is the other type which specifies the type of values it returns into
379-
# `yield from` expressions (using `return`).
380-
if len(return_type.args) == 1:
381-
return return_type.args[0]
382-
else:
383-
return AnyType()
381+
elif return_type.type.fullname() == 'typing.Awaitable' and len(return_type.args) == 1:
382+
# Awaitable: tr is args[0].
383+
return return_type.args[0]
384+
elif (return_type.type.fullname() in ('typing.Generator', 'typing.AwaitableGenerator')
385+
and len(return_type.args) >= 3):
386+
# AwaitableGenerator, Generator: tr is args[2].
387+
return return_type.args[2]
384388
else:
385-
# `return_type` is supertype of Generator, so callers won't be able to see the return
386-
# type when used in a `yield from` expression.
389+
# Supertype of Generator (Iterator, Iterable, object): tr is any.
387390
return AnyType()
388391

389392
def check_awaitable_expr(self, t: Type, ctx: Context, msg: str) -> Type:
@@ -540,6 +543,20 @@ def is_implicit_any(t: Type) -> bool:
540543
if not isinstance(typ.ret_type.args[2], (Void, NoneTyp, AnyType)):
541544
self.fail(messages.INVALID_GENERATOR_RETURN_ITEM_TYPE, typ)
542545

546+
# Fix the type if decorated with `@types.coroutine` or `@asyncio.coroutine`.
547+
if defn.is_awaitable_coroutine:
548+
# Update the return type to AwaitableGenerator.
549+
# (This doesn't exist in typing.py, only in typing.pyi.)
550+
t = typ.ret_type
551+
c = defn.is_coroutine
552+
ty = self.get_generator_yield_type(t, c)
553+
tc = self.get_generator_receive_type(t, c)
554+
tr = self.get_generator_return_type(t, c)
555+
ret_type = self.named_generic_type('typing.AwaitableGenerator',
556+
[ty, tc, tr, t])
557+
typ = typ.copy_modified(ret_type=ret_type)
558+
defn.type = typ
559+
543560
# Push return type.
544561
self.return_types.append(typ.ret_type)
545562

@@ -1872,6 +1889,11 @@ def visit_call_expr(self, e: CallExpr) -> Type:
18721889
return self.expr_checker.visit_call_expr(e)
18731890

18741891
def visit_yield_from_expr(self, e: YieldFromExpr) -> Type:
1892+
# NOTE: Whether `yield from` accepts an `async def` decorated
1893+
# with `@types.coroutine` (or `@asyncio.coroutine`) depends on
1894+
# whether the generator containing the `yield from` is itself
1895+
# thus decorated. But it accepts a generator regardless of
1896+
# how it's decorated.
18751897
return_type = self.return_types[-1]
18761898
subexpr_type = self.accept(e.expr, return_type)
18771899
iter_type = None # type: Type
@@ -1882,6 +1904,8 @@ def visit_yield_from_expr(self, e: YieldFromExpr) -> Type:
18821904
iter_type = AnyType()
18831905
elif (isinstance(subexpr_type, Instance) and
18841906
is_subtype(subexpr_type, self.named_type('typing.Iterable'))):
1907+
if self.is_async_def(subexpr_type) and not self.has_coroutine_decorator(return_type):
1908+
self.msg.yield_from_invalid_operand_type(subexpr_type, e)
18851909
iter_method_type = self.expr_checker.analyze_external_member_access(
18861910
'__iter__',
18871911
subexpr_type,
@@ -1892,8 +1916,12 @@ def visit_yield_from_expr(self, e: YieldFromExpr) -> Type:
18921916
iter_type, _ = self.expr_checker.check_call(iter_method_type, [], [],
18931917
context=generic_generator_type)
18941918
else:
1895-
self.msg.yield_from_invalid_operand_type(subexpr_type, e)
1896-
iter_type = AnyType()
1919+
if not (self.is_async_def(subexpr_type) and self.has_coroutine_decorator(return_type)):
1920+
self.msg.yield_from_invalid_operand_type(subexpr_type, e)
1921+
iter_type = AnyType()
1922+
else:
1923+
iter_type = self.check_awaitable_expr(subexpr_type, e,
1924+
messages.INCOMPATIBLE_TYPES_IN_YIELD_FROM)
18971925

18981926
# Check that the iterator's item type matches the type yielded by the Generator function
18991927
# containing this `yield from` expression.
@@ -1919,6 +1947,30 @@ def visit_yield_from_expr(self, e: YieldFromExpr) -> Type:
19191947
else:
19201948
return Void()
19211949

1950+
def has_coroutine_decorator(self, t: Type) -> bool:
1951+
"""Whether t came from a function decorated with `@coroutine`."""
1952+
return isinstance(t, Instance) and t.type.fullname() == 'typing.AwaitableGenerator'
1953+
1954+
def is_async_def(self, t: Type) -> bool:
1955+
"""Whether t came from a function defined using `async def`."""
1956+
# In check_func_def(), when we see a function decorated with
1957+
# `@typing.coroutine` or `@async.coroutine`, we change the
1958+
# return type to typing.AwaitableGenerator[...], so that its
1959+
# type is compatible with either Generator or Awaitable.
1960+
# But for the check here we need to know whether the original
1961+
# function (before decoration) was an `async def`. The
1962+
# AwaitableGenerator type conveniently preserves the original
1963+
# type as its 4th parameter (3rd when using 0-origin indexing
1964+
# :-), so that we can recover that information here.
1965+
# (We really need to see whether the original, undecorated
1966+
# function was an `async def`, which is orthogonal to its
1967+
# decorations.)
1968+
if (isinstance(t, Instance)
1969+
and t.type.fullname() == 'typing.AwaitableGenerator'
1970+
and len(t.args) >= 4):
1971+
t = t.args[3]
1972+
return isinstance(t, Instance) and t.type.fullname() == 'typing.Awaitable'
1973+
19221974
def visit_member_expr(self, e: MemberExpr) -> Type:
19231975
return self.expr_checker.visit_member_expr(e)
19241976

mypy/nodes.py

+1
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ class FuncItem(FuncBase):
417417
is_overload = False
418418
is_generator = False # Contains a yield statement?
419419
is_coroutine = False # Defined using 'async def' syntax?
420+
is_awaitable_coroutine = False # Decorated with '@{typing,asyncio}.coroutine'?
420421
is_static = False # Uses @staticmethod?
421422
is_class = False # Uses @classmethod?
422423
# Variants of function with type variables with values expanded

mypy/semanal.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1716,8 +1716,10 @@ def visit_decorator(self, dec: Decorator) -> None:
17161716
removed.append(i)
17171717
dec.func.is_abstract = True
17181718
self.check_decorated_function_is_method('abstractmethod', dec)
1719-
elif refers_to_fullname(d, 'asyncio.tasks.coroutine'):
1719+
elif (refers_to_fullname(d, 'asyncio.coroutines.coroutine') or
1720+
refers_to_fullname(d, 'types.coroutine')):
17201721
removed.append(i)
1722+
dec.func.is_awaitable_coroutine = True
17211723
elif refers_to_fullname(d, 'builtins.staticmethod'):
17221724
removed.append(i)
17231725
dec.func.is_static = True

0 commit comments

Comments
 (0)