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

Make coroutine function return type more specific #5052

Merged
merged 8 commits into from
May 17, 2018
Merged

Make coroutine function return type more specific #5052

merged 8 commits into from
May 17, 2018

Conversation

AndreLouisCaron
Copy link
Contributor

@AndreLouisCaron AndreLouisCaron commented May 15, 2018

See the commit messages for details and rationale. I tried to "tell a story" with them :-)

Fixes #3569.
Fixes #4460.

Special thanks to @ilevkivskyi and @gvanrossum for their time and their infinite patience with all my questions :-)

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..
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.
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()`.
Changing the return type of coroutine functions to  `Coroutine`
introduced a regression in the expression checks.
@AndreLouisCaron AndreLouisCaron changed the title Fix coroutine function return type Make coroutine function return type more specific May 15, 2018
This fixes the type inference logic that was causing the last
failing test to fail.  Build should now be green :-)
Copy link
Member

@ilevkivskyi ilevkivskyi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! Mostly looks good, just few comments.

mypy/semanal.py Outdated
# has external return type `Awaitable[T]`.
ret_type = self.named_type_or_none('typing.Awaitable', [defn.type.ret_type])
assert ret_type is not None, "Internal error: typing.Awaitable not found"
# has external return type `Coroutine[T, Any, Any]`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update this comment to Coroutine[Any, Any, T].

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in latest commit :-)

mypy/checker.py Outdated
@@ -878,7 +890,8 @@ def is_unannotated_any(t: Type) -> bool:
if is_unannotated_any(ret_type):
self.fail(messages.RETURN_TYPE_EXPECTED, fdef)
elif (fdef.is_coroutine and isinstance(ret_type, Instance) and
is_unannotated_any(ret_type.args[0])):
is_unannotated_any(ret_type.args[2])):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use self.get_coroutine_return_type here, then the below note is not needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in latest commit :-)

mypy/checker.py Outdated
if isinstance(return_type, AnyType):
return AnyType(TypeOfAny.from_another_any, source_any=return_type)
assert isinstance(return_type, Instance), "Should only be called on coroutine functions!"
# return type is 3rd type specification in Coroutine!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would format this comment as # Note: return type is the 3rd type parameter of Coroutine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in latest commit :-)

mypy/checker.py Outdated
def get_coroutine_return_type(self, return_type: Type) -> Type:
if isinstance(return_type, AnyType):
return AnyType(TypeOfAny.from_another_any, source_any=return_type)
assert isinstance(return_type, Instance), "Should only be called on coroutine functions!"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use exclamation signs in assertion messages.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in latest commit :-)

if c:
tr = self.get_coroutine_return_type(t)
else:
tr = self.get_generator_return_type(t, c)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the symmetry between the branches here. 👍

@@ -398,8 +398,7 @@ def do_func_def(self, n: Union[ast3.FunctionDef, ast3.AsyncFunctionDef],
self.as_required_block(n.body, n.lineno),
func_type)
if is_coroutine:
# A coroutine is also a generator, mostly for internal reasons.
func_def.is_generator = func_def.is_coroutine = True
func_def.is_coroutine = True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is good you also cleaned this up.

mypy/semanal.py Outdated
assert ret_type is not None, "Internal error: typing.Awaitable not found"
# has external return type `Coroutine[T, Any, Any]`.
any_type = AnyType(TypeOfAny.special_form)
ret_type = self.named_type_or_none('typing.Coroutine', [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move the opening bracket to the next line.

@AndreLouisCaron
Copy link
Contributor Author

Thanks @ilevkivskyi for the quick feedback. I pushed a new commit with all the requested changes :-)

assert isinstance(return_type, Instance), "Should only be called on coroutine functions."
# Note: return type is the 3rd type parameter of Coroutine.
return return_type.args[2]

def get_generator_return_type(self, return_type: Type, is_coroutine: bool) -> Type:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the is_coroutine argument here still useful, or can we remove it now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure we still need it to handle asynchronous generators. In that case, they have both is_generator=True and is_coroutine=True.

Copy link
Member

@ilevkivskyi ilevkivskyi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, thanks, LGTM now!

@ilevkivskyi ilevkivskyi merged commit 6519eb6 into python:master May 17, 2018
@gvanrossum
Copy link
Member

Thank you for fixing such a sophisticated problem! Vive les sprint. :-)

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

Successfully merging this pull request may close these issues.

4 participants