-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Design for using async+await with asyncio #1886
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Focusing on asyncio, there are now two equivalent ways of waiting for things:
In the first two cases the function should also be decorated with There are also three kinds of things that are worth waiting:
In the last two cases the function should also be decorated with Thinking aloud, I'm going to pretend that from typing import Any, Awaitable, Generator
T = TypeVar('T')
def await_(arg: Awaitable[T]) -> T: ...
def yield_from_(arg: Generator[Any, None, T]) -> T: ... To this list we can add the current definition of class AbstractEventLoop:
def run_until_complete(self,
future: Union[Future[T], Awaitable[T], Generator[Any, Any, T]]) -> T: ... Next we look at the definitions. When we define a generator function, the return type must explicitly use the def count(n: int) -> Generator[int, None, str]:
for i in range(n):
yield i
return 'ok' Note that the string def outer() -> Generator[int, None, None]:
x = yield from count(5)
print(x) (This is just a summary of the behavior defined by PEP 380, which introduced For simple generators we can also use from typing import Iterator
def count(n: int) -> Iterator[int]:
for i in range(n):
yield i Now let's use an example with def count_repeatedly(n: int, repeats: int) -> Iterator[int]:
for i in range(repeats):
yield from count(n) On the other hand, PEP 492's native coroutines use a more concise notation: async def my_coro(a: int) -> int:
await another_coro()
return 42 This is the moral equivalent of the following more cumbersome version using generators: def my_coro_as_gen(a: int) -> Generator[Any, None, int]:
yield from another_coro_as_gen()
return 42 Now consider the types revealed by mypy: reveal_type(my_coro(0))
reveal_type(my_coro_as_gen(0)) This will reveal the types to be as follows:
IOW for generators the type written in the signature is what we get; but for native coroutines the type written is wrapped in This very long ramble is leading up to the question: What should I think we first need to define a further type: something that's acceptable by both So when And when it wraps a generator function whose return type is (written as, and seen by callers as) There's one more special case: when How to support Finally a note about the difference between As a stretch goal we might try to distinguish between the two decorators as follows: in asyncio, the first parameter to |
So, to be specific, if we had a feature to modify a Callable's return type, here's how I would define stubs for the two decorators. Prerequisite: the T = TypeVar('T')
class AwaitableGenerator(Awaitable[T], Generator[Any, Any, T], Generic[T]):
pass # Nothing here Then, AA = TypeVar('AA')
T = TypeVar('T')
def coroutine(func: Callable[AA, Generator[Any, Any, T]]
) -> Callable[AA, AwaitableGenerator[T]: ... Finally, AA = TypeVar('AA')
T = TypeVar('T')
@overload
def coroutine(func: Callable[AA, Generator[Any, Any, T]]
) -> Callable[AA, AwaitableGenerator[T]]: ...
@overload
def coroutine(func: Callable[AA, Awaitable[T]]) -> Callable[AA, Awaitable[T]]: ...
@overload
def coroutine(func: Callable[AA, T]]) -> Callable[AA, Awaitable[T]]: I suspect that the third variant of the overloading doesn't actually work, because it overlaps with the other two. More reasons to special-case this in mypy. (To be clear, the notation using |
A final question to myself: where should AwaitableGenerator be defined? The obvious answer is to add it to the |
I've got a bunch of this mostly working (https://github.com./python/mypy/tree/add-crawl2), but there's a remaining issue. I'm currently just updating the type of the decorated FuncItem to change its return type to @coroutine
def foo() -> Generator[int, None, str]:
yield 'wrong!'
return 'ok' This should give an error on the Possible (hacky) solution: give AwaitableGenerator a second type parameter, and when updating the definition's type, set it to [Update:] Or alternatively I could just use |
FWIW I've got all this sorted out now in the add-crawl2 branch. No PR yet
(I need to update PR #1808 in response to the latest code review first).
|
I'm trying something new here, pouring my thoughts into the issue tracker to get some clarity.
I hope that we'll soon have syntactic support for PEP 492, in particular
async def
andawait
(#1808). (Yes, it also supportsasync for
andasync with
, but these aren't very interesting so I won't mention them.)I've also made some improvements to the asyncio stubs (python/typeshed#373, merged already) and created a fully annotated example, crawl.py (#1878). But these do not use
async def
orawait
; they use generators annotated with@asyncio.coroutine
andyield from
to wait for coroutines and Futures.The next step is supporting asyncio code that uses
async def
andawait
, and here I am a little stuck. On the one hand, PEP 492 draws a clear distinction between generators and native coroutines (the latter being the ones defined usingasync def
). You can't use a generator withawait
, and you can't use a native coroutine withyield from
. On the other hand, PEP 492 also allows an escape clause: by using the decorator@types.coroutine
you can mark a generator as a coroutine, and thenawait
accepts it. The@asyncio.coroutine
decorator calls this decorator, in addition to doing other asyncio-specific things (some of which are only apparent in debug mode).I would like mypy to model all this as well as possible, so that mypy will give a useful error message when you try to use a generator with
await
or a coroutine withyield from
. But it should also understand that a generator decorated with@types.coroutine
or@asyncio.coroutine
is acceptable forawait
as well as foryield from
(in the latter case it would be even better if it could also keep track of whether the generator containing theyield from
is decorated with@asyncio.coroutine
).The final piece of the puzzle (or rather, contribution to the problem) is that it's not easy to specify the type for a decorator that modifies the signature of the function it wraps. What I wish I could do is to declare that the
@asyncio.coroutine
decorator takes a function with argumentsAA
and return typeGenerator[Any, Any, R]
and returns a function with argumentsAA
and return typeAwaitable[R]
(which is whatawait
requires -- the return value ofawait
is then of typeR
). And I'd like the decorator to be overloaded so that it also handles a few other cases.But neither PEP 484 nor mypy supports this "signature algebra", so the best I can think of is to special-case the two decorators (
@types.coroutine
and@asyncio.coroutine
) in mypy, in a similar fashion as how it deals with some other built-in decorators (@property
,@classmethod
and probably a few more).[To be continued]
The text was updated successfully, but these errors were encountered: