Skip to content
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

Coroutine functions should return Coroutine, not Awaitable #3569

Closed
jpochyla opened this issue Jun 19, 2017 · 10 comments
Closed

Coroutine functions should return Coroutine, not Awaitable #3569

jpochyla opened this issue Jun 19, 2017 · 10 comments

Comments

@jpochyla
Copy link

async def coro_func() -> None:
    pass
c = coro_func()
reveal_type(c)  # prints typing.Awaitable[builtins.None]

c above has all fields of typing.Coroutine, you can send and throw on it, but the type doesn't reflect that.

@gvanrossum
Copy link
Member

Would you like to see if you can submit a PR to fix this?

@ilevkivskyi
Copy link
Member

This would be simple to fix, by just changing the return type in semanal.py:

                    defn.type = defn.type.copy_modified(
                        ret_type = self.named_type_or_none('typing.Awaitable',
                                                           [defn.type.ret_type]))

To something like Coroutine[ret_type, None, NoReturn]. But this will also require many tedious changes in tests (because of error messages).

@jpochyla
Copy link
Author

jpochyla commented Jun 20, 2017

Thanks @ilevkivskyi! I think I can do the PR, with the test & doc changes. One thing I am not sure about is whether the return type of defs annotated with @types.coroutine should also be Coroutine, #2907 seems to indicate otherwise. Why wouldn't the type be the same? I thought they are basically equivalent.

@ilevkivskyi
Copy link
Member

@jpochyla
The coroutines made by @types.coroutine (they are called generator based coroutines) are special in the sense that they provide compatibility between older versions of Python and Python 3.5+. Namely, you can do both await test() and yield from test() if it is defined as

@types.coroutine
def test():
    yield 1

You can't do await on normal generators, and you can't do yield from on native coroutines (made by async def). IIRC, there is even a special internal type in typeshed and mypy to denote generator based coroutines AwaitableGenerator.

@scottbelden
Copy link

Hi, just seeing if any progress was being made on this (as I stumbled on it today).

@ilevkivskyi
Copy link
Member

@scottbelden
Unfortunately, it seems that no-one is working on this now. If you have time and enthusiasm, then you can try to fix it yourself.

@scottbelden
Copy link

I started trying to make an attempt to resolve this, but I'm afraid that without some help I don't think I'll be able to do it and might just have to wait for someone else to get to it. I started with making the changes in semanal.py that you mentioned in one of the previous messages and the tests did blow up as you expected, but I think in an unexpected way (at least unexpected to me). The errors were complaining about typing.Coroutine not found:

    assert ret_type is not None, "Internal error: typing.Coroutine not found"
AssertionError: Internal error: typing.Coroutine not found

I'm not sure why typing.Awaitable would be found but not typing.Coroutine.

@JelleZijlstra
Copy link
Member

You may have to add it to one of the stub test files (typing.pyi somewhere in the test-data/ directory).

@AndreLouisCaron
Copy link
Contributor

AndreLouisCaron commented May 15, 2018

I also hit this issue yesteday. Here is one case where having Coroutine  rather than Awaitable would really help:

from asyncio import (
    AbstractEventLoop,
    get_event_loop,
    sleep,
    Task,
)
from typing import Coroutine, Optional


# NOTE: `loop.create_task()` only accepts coroutine objects.  It cannot accept
#       futures, nor tasks so we should not use `typing.Awaitable`.
def spawn(coro: Coroutine,
          loop: Optional[AbstractEventLoop]=None) -> Task:
    loop = loop or get_event_loop()
    return loop.create_task(coro)


# Any regular coroutine :-)
#
# I would expect `mypy` to deduce `Coroutine[Any, Any, Any]`, but it detects
# `Awaitable[None]`.
async def background_work() -> None:
    await sleep(5.0)
    print('DONE!')


# Demonstrate the type conflict :-)
async def main() -> None:
    task = spawn(background_work())  # error: Argument 1 to "spawn" has incompatible type "Awaitable[None]"; expected "Coroutine[Any, Any, Any]"
    print('waiting...')
    await task


if __name__ == '__main__':
    get_event_loop().run_until_complete(main())

@gvanrossum
Copy link
Member

As a reminder to myself:

  • Awaitable is a Protocol that just defines __await__, and Coroutine subclasses that and adds send(), throw() and close() methods.
  • AbstractEventLoop.create_task() is defined to take a union of Coroutine and Generator.

ilevkivskyi pushed a commit that referenced this issue May 17, 2018
* Change the return type of coroutine functions to coroutine object

Previously, the return type was `Awaitable`, which is correct, but
not specific enough for some use cases.  For example, if you have
a function parameter that should be a coroutine object (but not a
`Future`, `Task` or other awaitable), then `mypy` is unable to
detect incorrect invocations of the function.

This change is (deliberately) imcomplete as is.  It seems like
this breaks quite a few tests in `mypy`.  The first symptom is that
coroutine objects are now (incorrectly) detected as generators..

* Prevent coroutine functions from being classified as generators

This change removes the undesired "return type of a generator
function should be `Generator` or one of its subtypes for coroutine
function definitions.

However, it introduces a new error where the return type of coroutine
functions is expected to be `Coroutine[T, Any, Any]` instead of the
desired `T`.  It looks like we were hijacking a generator-specific
path for checking the return type of coroutine functinos.  I added
an explicit path for coroutine functions, allowing our test to pass.

However, lots of tests now fail.  A few of them were simply places
that were incidentally relying on coroutine functions to have type
`Awaitable`.  I fix them.  The remaining failures all seem to be
about coroutine functions with return type `None` without an explicit
return statement.  Seems like this is also something for which we
were relying on implicit classification as generators.

* Allow implicit return for coroutine functions that return `None`

Most of the tests are fixed, but two tests still fail.  One about
not detecting invalid `yield from` on `AwaitableGenerator`.  The
other about types being erased in call to `asyncio.gather()`.

* Fix return type for coroutine functions decorated with @coroutine

* Fix detection of await expression on direct coroutine function call

Changing the return type of coroutine functions to  `Coroutine`
introduced a regression in the expression checks.

* Fix regression after change of coroutine function return type

* Fix position of return type in `Coroutine`

This fixes the type inference logic that was causing the last
failing test to fail.  Build should now be green :-)

* Fix issues raised in code review

Fixes #3569.
Fixes #4460.

Special thanks to @ilevkivskyi and @gvanrossum for their time
and their infinite patience with all my questions :-)
facebook-github-bot pushed a commit to facebook/pyre-check that referenced this issue Jan 30, 2019
Summary:
Came across this because we were wrapping functions returning `AsyncGenerator` inside an `Awaitable`, which was throwing false positive errors later on when we tried to call `__aiter__` on it, for instance.

Turns out functions prefixed with `async` wrap the return type in a `Coroutine`, not an `Awaitable` (see: python/mypy#3569), and functions that are actually generators (contain a yield) just take the return annotation of `AsyncGenerator` at face value - otherwise, the function signature is understood as asynchronously returning a generator object just like any other async function (see: python/mypy#5070)

Reviewed By: dkgi

Differential Revision: D13864544

fbshipit-source-id: 0d201735252b77688a5491428cfb5818d000754b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants