-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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
Changes from all commits
c741a97
4dc62b6
c64e7fc
0117379
01b50de
feaf051
2273ac8
c255d84
facc5ed
f193e42
dc0206c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 = \ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
'The return type of an async generator function should be "AsyncGenerator" or one of its ' \ | ||
'supertypes' | ||
INVALID_GENERATOR_RETURN_ITEM_TYPE = \ | ||
'The return type of a generator function must be None in its third type parameter in Python 2' | ||
YIELD_VALUE_EXPECTED = 'Yield value expected' | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -324,11 +324,15 @@ def visit_func_def(self, defn: FuncDef) -> None: | |
self.errors.push_function(defn.name()) | ||
self.analyze_function(defn) | ||
if defn.is_coroutine and isinstance(defn.type, CallableType): | ||
# A coroutine defined as `async def foo(...) -> T: ...` | ||
# has external return type `Awaitable[T]`. | ||
defn.type = defn.type.copy_modified( | ||
ret_type = self.named_type_or_none('typing.Awaitable', | ||
[defn.type.ret_type])) | ||
if defn.is_async_generator: | ||
# Async generator types are handled elsewhere | ||
pass | ||
else: | ||
# A coroutine defined as `async def foo(...) -> T: ...` | ||
# has external return type `Awaitable[T]`. | ||
defn.type = defn.type.copy_modified( | ||
ret_type = self.named_type_or_none('typing.Awaitable', | ||
[defn.type.ret_type])) | ||
self.errors.pop_function() | ||
|
||
def prepare_method_signature(self, func: FuncDef) -> None: | ||
|
@@ -2842,7 +2846,11 @@ def visit_yield_expr(self, expr: YieldExpr) -> None: | |
self.fail("'yield' outside function", expr, True, blocker=True) | ||
else: | ||
if self.function_stack[-1].is_coroutine: | ||
self.fail("'yield' in async function", expr, True, blocker=True) | ||
if self.options.python_version < (3, 6): | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
else: | ||
self.function_stack[-1].is_generator = True | ||
if expr.expr: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -326,18 +326,15 @@ async def f() -> None: | |
[builtins fixtures/async_await.pyi] | ||
|
||
[case testNoYieldInAsyncDef] | ||
# flags: --python-version 3.5 | ||
|
||
async def f(): | ||
yield None | ||
yield None # E: 'yield' in async function | ||
async def g(): | ||
yield | ||
yield # E: 'yield' in async function | ||
async def h(): | ||
x = yield | ||
x = yield # E: 'yield' in async function | ||
[builtins fixtures/async_await.pyi] | ||
[out] | ||
main:3: error: 'yield' in async function | ||
main:5: error: 'yield' in async function | ||
main:7: error: 'yield' in async function | ||
|
||
[case testNoYieldFromInAsyncDef] | ||
|
||
|
@@ -410,6 +407,156 @@ def f() -> Generator[int, str, int]: | |
[builtins fixtures/async_await.pyi] | ||
[out] | ||
|
||
-- Async generators (PEP 525), some test cases adapted from the PEP text | ||
-- --------------------------------------------------------------------- | ||
|
||
[case testAsyncGenerator] | ||
# flags: --python-version 3.6 | ||
from typing import AsyncGenerator, Generator | ||
|
||
async def f() -> int: | ||
return 42 | ||
|
||
async def g() -> AsyncGenerator[int, None]: | ||
value = await f() | ||
reveal_type(value) # E: Revealed type is 'builtins.int*' | ||
yield value | ||
|
||
yield 'not an int' # E: Incompatible types in yield (actual type "str", expected type "int") | ||
# return without a value is fine | ||
return | ||
reveal_type(g) # E: Revealed type is 'def () -> typing.AsyncGenerator[builtins.int, void]' | ||
reveal_type(g()) # E: Revealed type is 'typing.AsyncGenerator[builtins.int, void]' | ||
|
||
async def h() -> None: | ||
async for item in g(): | ||
reveal_type(item) # E: Revealed type is 'builtins.int*' | ||
|
||
async def wrong_return() -> Generator[int, None, None]: # E: The return type of an async generator function should be "AsyncGenerator" or one of its supertypes | ||
yield 3 | ||
|
||
[builtins fixtures/dict.pyi] | ||
|
||
[case testAsyncGeneratorReturnIterator] | ||
# flags: --python-version 3.6 | ||
from typing import AsyncIterator | ||
|
||
async def gen() -> AsyncIterator[int]: | ||
yield 3 | ||
|
||
yield 'not an int' # E: Incompatible types in yield (actual type "str", expected type "int") | ||
|
||
async def use_gen() -> None: | ||
async for item in gen(): | ||
reveal_type(item) # E: Revealed type is 'builtins.int*' | ||
|
||
[builtins fixtures/dict.pyi] | ||
|
||
[case testAsyncGeneratorManualIter] | ||
# flags: --python-version 3.6 | ||
from typing import AsyncGenerator | ||
|
||
async def genfunc() -> AsyncGenerator[int, None]: | ||
yield 1 | ||
yield 2 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe also add a test where you |
||
|
||
async def user() -> None: | ||
gen = genfunc() | ||
|
||
reveal_type(gen.__aiter__()) # E: Revealed type is 'typing.AsyncGenerator[builtins.int*, void]' | ||
|
||
reveal_type(await gen.__anext__()) # E: Revealed type is 'builtins.int*' | ||
|
||
[builtins fixtures/dict.pyi] | ||
|
||
[case testAsyncGeneratorAsend] | ||
# flags: --fast-parser --python-version 3.6 | ||
from typing import AsyncGenerator | ||
|
||
async def f() -> None: | ||
pass | ||
|
||
async def gen() -> AsyncGenerator[int, str]: | ||
await f() | ||
v = yield 42 | ||
reveal_type(v) # E: Revealed type is 'builtins.str' | ||
await f() | ||
|
||
async def h() -> None: | ||
g = gen() | ||
await g.asend(()) # E: Argument 1 to "asend" of "AsyncGenerator" has incompatible type "Tuple[]"; expected "str" | ||
reveal_type(await g.asend('hello')) # E: Revealed type is 'builtins.int*' | ||
|
||
[builtins fixtures/dict.pyi] | ||
|
||
[case testAsyncGeneratorAthrow] | ||
# flags: --fast-parser --python-version 3.6 | ||
from typing import AsyncGenerator | ||
|
||
async def gen() -> AsyncGenerator[str, int]: | ||
try: | ||
yield 'hello' | ||
except BaseException: | ||
yield 'world' | ||
|
||
async def h() -> None: | ||
g = gen() | ||
v = await g.asend(1) | ||
reveal_type(v) # E: Revealed type is 'builtins.str*' | ||
reveal_type(await g.athrow(BaseException)) # E: Revealed type is 'builtins.str*' | ||
|
||
[builtins fixtures/dict.pyi] | ||
|
||
[case testAsyncGeneratorNoSyncIteration] | ||
# flags: --fast-parser --python-version 3.6 | ||
from typing import AsyncGenerator | ||
|
||
async def gen() -> AsyncGenerator[int, None]: | ||
for i in (1, 2, 3): | ||
yield i | ||
|
||
def h() -> None: | ||
for i in gen(): | ||
pass | ||
|
||
[builtins fixtures/dict.pyi] | ||
|
||
[out] | ||
main:9: error: Iterable expected | ||
main:9: error: AsyncGenerator[int, None] has no attribute "__iter__"; maybe "__aiter__"? | ||
|
||
[case testAsyncGeneratorNoYieldFrom] | ||
# flags: --fast-parser --python-version 3.6 | ||
from typing import AsyncGenerator | ||
|
||
async def f() -> AsyncGenerator[int, None]: | ||
pass | ||
|
||
async def gen() -> AsyncGenerator[int, None]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe add some tests with |
||
yield from f() # E: 'yield from' in async function | ||
|
||
[builtins fixtures/dict.pyi] | ||
|
||
[case testAsyncGeneratorNoReturnWithValue] | ||
# flags: --fast-parser --python-version 3.6 | ||
from typing import AsyncGenerator | ||
|
||
async def return_int() -> AsyncGenerator[int, None]: | ||
yield 1 | ||
return 42 # E: 'return' with value in async generator is not allowed | ||
|
||
async def return_none() -> AsyncGenerator[int, None]: | ||
yield 1 | ||
return None # E: 'return' with value in async generator is not allowed | ||
|
||
def f() -> None: | ||
return | ||
|
||
async def return_f() -> AsyncGenerator[int, None]: | ||
yield 1 | ||
return f() # E: 'return' with value in async generator is not allowed | ||
|
||
[builtins fixtures/dict.pyi] | ||
|
||
-- The full matrix of coroutine compatibility | ||
-- ------------------------------------------ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,3 +38,5 @@ class tuple: pass | |
class function: pass | ||
class float: pass | ||
class bool: pass | ||
|
||
class BaseException: pass |
There was a problem hiding this comment.
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 (addAsyncGenerator
).