Skip to content

Implement async generators (PEP 525) #2711

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

Merged
merged 11 commits into from
Mar 18, 2017
Merged

Conversation

JelleZijlstra
Copy link
Member

@JelleZijlstra JelleZijlstra commented Jan 19, 2017

This turned out to be pretty straightforward.

Two issues that may need more discussion:

  • 3.6.0 was released without typing.AsyncGenerator; it was added in add AsyncGenerator typing#346 and should be in 3.6.1. But this patch assumes that typing.AsyncGenerator exists, so if you run mypy in 3.6.0 on a file with an async generator, it says that typing.AsyncGenerator doesn't exist. I originally put AsyncGenerator in mypy_extensions to work around this, but maybe we need some other solution.
  • PEP 525 says that it's a RuntimeError to yield inside a finally block in an async generator, but that doesn't appear to be true for 3.6.0 as implemented, so I didn't add a check in mypy for this.

This closes #2616

@JelleZijlstra JelleZijlstra changed the title Implement async generators (PEP 525) [WIP] Implement async generators (PEP 525) Jan 19, 2017
@JelleZijlstra JelleZijlstra changed the title [WIP] Implement async generators (PEP 525) Implement async generators (PEP 525) Jan 19, 2017
@gvanrossum
Copy link
Member

  • Re: the absence of typing.AsyncGenerator in Python 3.6.0, I don't understand why that would cause an internal error in mypy, since mypy doesn't look up types needed for type checking the target program in the runtime typing.py module -- it looks in typeshed. Assuming that what you're really seeing is what happens when typeshed doesn't have typing.AsyncGenerator (assuming mypy --python-version 3.6 is used), we could just guard for that by syncing the typeshed submodule past the commit that has it. Or you could check whether the lookup failed and report an error instead.

  • I don't quite follow your second bullet (maybe you can show a complete test program with expected and actual behavior?) but @1st1 should be able to help you understand whatever it is you are seeing.

@1st1
Copy link
Member

1st1 commented Jan 19, 2017

PEP 525 says that it's a RuntimeError to yield inside a finally block in an async generator, but that doesn't appear to be true for 3.6.0 as implemented, so I didn't add a check in mypy for this.

It's a RuntimeError when you yield inside a finally block during AG finalization, i.e. when its .aclose() method is called or when it is GCed. An exact same rule exists for regular synchronous generators.

@1st1
Copy link
Member

1st1 commented Jan 19, 2017

IOW it's OK to have yield expressions in finally blocks in asynchronous generators, and I don't think mypy should care about that.

@JelleZijlstra
Copy link
Member Author

Thanks for clarifying the yield/finally issue! An earlier version of my patch did have an internal error when typing.AsyncGenerator didn't exist, but that is fixed now (I amended my message above yesterday to reflect that). People using 3.6.0 will just have to guard their from typing import AsyncGenerator in if TYPE_CHECKING:.

In other words, both of the issues I flagged should be resolved.

@ilevkivskyi
Copy link
Member

@JelleZijlstra What is the status of this PR? I am asking just to see whether it would be possible to have full Python 3.6 support in 0.480 (that should be released in around one week if I understand correctly the current schedule).

@JelleZijlstra
Copy link
Member Author

@ilevkivskyi I think the code is ready, but it hasn't been reviewed yet. Also, there is a merge conflict now—I'll fix that.

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.

This mostly looks good, here are some minor comments.

def get_generator_yield_type(self, return_type: Type, is_coroutine: bool) -> Type:
"""Given the declared return type of a generator (t), return the type it yields (ty)."""
if isinstance(return_type, AnyType):
return AnyType()
elif not self.is_generator_return_type(return_type, is_coroutine):
elif (not self.is_generator_return_type(return_type, is_coroutine)
and not self.is_async_generator_return_type(return_type)):
Copy link
Member

Choose a reason for hiding this comment

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

I can't comment there, but you might update the comment after elif return_type.args: below (add AsyncGenerator).

mypy/checker.py Outdated
@@ -1705,7 +1733,7 @@ def check_return_stmt(self, s: ReturnStmt) -> None:
self.warn(messages.RETURN_ANY.format(return_type), s)
return

if self.is_unusable_type(return_type):
if self.is_unusable_type(return_type) or defn.is_async_generator:
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 prefer a separate (more clear) error message for async generators, like: "return statement with value in async generator is prohibited". Maybe it could be even blocking (since formally speaking this is a SyntaxError).

Also, I would probably move this check to semanal.py.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think putting this in semanal.py would be hard because we only discover that the function is a generator during semantic analysis. I suppose we could do it in ThirdPass, but there are some code comments suggesting that ThirdPass may be removed in the future.

Copy link
Member Author

Choose a reason for hiding this comment

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

(But I'll change the error message.)


async def gen() -> AsyncGenerator[int, None]:
yield 1
return 42 # E: No return value expected
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 add more tests here, both return None and return f() (for f that returns None) should fail (both with and without --strict-optional), since these are syntax errors.

-- ---------------------------------------------------------------------

[case testAsyncGenerator]
# flags: --fast-parser --python-version 3.6
Copy link
Member

Choose a reason for hiding this comment

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

--fast-parser flag is not necessary anymore.

self.fail("'yield' in async function", expr, True, blocker=True)
else:
self.function_stack[-1].is_generator = True
self.function_stack[-1].is_async_generator = True
Copy link
Member

Choose a reason for hiding this comment

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

This is not important, but it looks a bit strange that you need to set both flags here.

async def f() -> AsyncGenerator[int, None]:
pass

async def gen() -> AsyncGenerator[int, None]:
Copy link
Member

Choose a reason for hiding this comment

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

Maybe add some tests with AsyncIterator[int]?


async def genfunc() -> AsyncGenerator[int, None]:
yield 1
yield 2
Copy link
Member

Choose a reason for hiding this comment

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

Maybe also add a test where you yield a wrong type?

@JelleZijlstra
Copy link
Member Author

Thanks for the comments! I will have to rebase the PR because it now relies on an obsolete version of typed_ast.

Copy link
Collaborator

@JukkaL JukkaL left a comment

Choose a reason for hiding this comment

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

LGTM -- only one idea for an additional test case.

@@ -34,6 +34,9 @@
INVALID_EXCEPTION_TYPE = 'Exception type must be derived from BaseException'
INVALID_RETURN_TYPE_FOR_GENERATOR = \
'The return type of a generator function should be "Generator" or one of its supertypes'
INVALID_RETURN_TYPE_FOR_ASYNC_GENERATOR = \
Copy link
Collaborator

Choose a reason for hiding this comment

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

There doesn't seem to be a test for this error message. Can you add one?

@JukkaL JukkaL merged commit baadb04 into python:master Mar 18, 2017
@JukkaL
Copy link
Collaborator

JukkaL commented Mar 18, 2017

🎉

@JelleZijlstra JelleZijlstra deleted the asyncgen branch May 4, 2017 04:07
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.

Add support for PEP 525 -- Asynchronous Generators
5 participants