Skip to content

Commit 02d3667

Browse files
authored
Fix typechecking for async generators (#17452)
Fixes #10534 This PR fixes a bug in typechecking asynchronous generators. Mypy currently typechecks a generator/comprehension as `AsyncGenerator` if the leftmost expression contains `await`, or if it contains an `async for`. However, there are other situations where we should get async generator: If there is an `await` expression in any of the conditions or in any sequence except for the leftmost one, the generator/comprehension should also be typechecked as `AsyncGenerator`. I've implemented this change in Mypy and added a test case to assert this behavior. If I enter the test cases into a regular repl, I can confirm that the runtime representation is generator/async_generator as the test case expects. According to the [language reference](https://docs.python.org/3/reference/expressions.html#grammar-token-python-grammar-comp_for): > If a comprehension contains either async for clauses or await expressions or other asynchronous comprehensions it is called an asynchronous comprehension. Confusingly, the documentation itself is actually not quite correct either, as pointed out in python/cpython#114104 Alongside this change, I've made a PR to update the docs to be more precise: python/cpython#121175 has more details.
1 parent 177c8ee commit 02d3667

File tree

2 files changed

+26
-2
lines changed

2 files changed

+26
-2
lines changed

mypy/checkexpr.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5585,8 +5585,13 @@ def visit_set_comprehension(self, e: SetComprehension) -> Type:
55855585

55865586
def visit_generator_expr(self, e: GeneratorExpr) -> Type:
55875587
# If any of the comprehensions use async for, the expression will return an async generator
5588-
# object, or if the left-side expression uses await.
5589-
if any(e.is_async) or has_await_expression(e.left_expr):
5588+
# object, or await is used anywhere but in the leftmost sequence.
5589+
if (
5590+
any(e.is_async)
5591+
or has_await_expression(e.left_expr)
5592+
or any(has_await_expression(sequence) for sequence in e.sequences[1:])
5593+
or any(has_await_expression(cond) for condlist in e.condlists for cond in condlist)
5594+
):
55905595
typ = "typing.AsyncGenerator"
55915596
# received type is always None in async generator expressions
55925597
additional_args: list[Type] = [NoneType()]

test-data/unit/check-async-await.test

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,25 @@ async def return_f() -> AsyncGenerator[int, None]:
573573
[builtins fixtures/dict.pyi]
574574
[typing fixtures/typing-async.pyi]
575575

576+
[case testImplicitAsyncGenerator]
577+
from typing import List
578+
579+
async def get_list() -> List[int]:
580+
return [1]
581+
582+
async def predicate() -> bool:
583+
return True
584+
585+
async def test_implicit_generators() -> None:
586+
reveal_type(await predicate() for _ in [1]) # N: Revealed type is "typing.AsyncGenerator[builtins.bool, None]"
587+
reveal_type(x for x in [1] if await predicate()) # N: Revealed type is "typing.AsyncGenerator[builtins.int, None]"
588+
reveal_type(x for x in await get_list()) # N: Revealed type is "typing.Generator[builtins.int, None, None]"
589+
reveal_type(x for _ in [1] for x in await get_list()) # N: Revealed type is "typing.AsyncGenerator[builtins.int, None]"
590+
591+
[builtins fixtures/dict.pyi]
592+
[typing fixtures/typing-async.pyi]
593+
594+
576595
-- The full matrix of coroutine compatibility
577596
-- ------------------------------------------
578597

0 commit comments

Comments
 (0)