[ty] Async for loops and async iterables (#19634)

## Summary

Add support for `async for` loops and async iterables.

part of https://github.com/astral-sh/ty/issues/151

## Ecosystem impact

```diff
- boostedblob/listing.py:445:54: warning[unused-ignore-comment] Unused blanket `type: ignore` directive
```

This is correct. We now find a true positive in the `# type: ignore`'d
code.

All of the other ecosystem hits are of the type

```diff
trio (https://github.com/python-trio/trio)
+ src/trio/_core/_tests/test_guest_mode.py:532:24: error[not-iterable] Object of type `MemorySendChannel[int] | MemoryReceiveChannel[int]` may not be iterable
```

The message is correct, because only `MemoryReceiveChannel` has an
`__aiter__` method, but `MemorySendChannel` does not. What's not correct
is our inferred type here. It should be `MemoryReceiveChannel[int]`, not
the union of the two. This is due to missing unpacking support for tuple
subclasses, which @AlexWaygood is working on. I don't think this should
block merging this PR, because those wrong types are already there,
without this PR.

## Test Plan

New Markdown tests and snapshot tests for diagnostics.
This commit is contained in:
David Peter 2025-07-30 17:40:24 +02:00 committed by GitHub
parent e593761232
commit eb02aa5676
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 908 additions and 197 deletions

View file

@ -2,27 +2,6 @@
Async `for` loops do not work according to the synchronous iteration protocol.
## Invalid async for loop
```py
async def foo():
class Iterator:
def __next__(self) -> int:
return 42
class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
async for x in Iterator():
pass
# TODO: should reveal `Unknown` because `__aiter__` is not defined
# revealed: @Todo(async iterables/iterators)
# error: [possibly-unresolved-reference]
reveal_type(x)
```
## Basic async for loop
```py
@ -35,11 +14,154 @@ async def foo():
def __aiter__(self) -> IntAsyncIterator:
return IntAsyncIterator()
# TODO(Alex): async iterables/iterators!
async for x in IntAsyncIterable():
pass
# error: [possibly-unresolved-reference]
# revealed: @Todo(async iterables/iterators)
reveal_type(x)
reveal_type(x) # revealed: int
```
## Async for loop with unpacking
```py
async def foo():
class AsyncIterator:
async def __anext__(self) -> tuple[int, str]:
return 42, "hello"
class AsyncIterable:
def __aiter__(self) -> AsyncIterator:
return AsyncIterator()
async for x, y in AsyncIterable():
reveal_type(x) # revealed: int
reveal_type(y) # revealed: str
```
## Error cases
<!-- snapshot-diagnostics -->
### No `__aiter__` method
```py
from typing_extensions import reveal_type
class NotAsyncIterable: ...
async def foo():
# error: [not-iterable] "Object of type `NotAsyncIterable` is not async-iterable"
async for x in NotAsyncIterable():
reveal_type(x) # revealed: Unknown
```
### Synchronously iterable, but not asynchronously iterable
```py
from typing_extensions import reveal_type
async def foo():
class Iterator:
def __next__(self) -> int:
return 42
class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
# error: [not-iterable] "Object of type `Iterator` is not async-iterable"
async for x in Iterator():
reveal_type(x) # revealed: Unknown
```
### No `__anext__` method
```py
from typing_extensions import reveal_type
class NoAnext: ...
class AsyncIterable:
def __aiter__(self) -> NoAnext:
return NoAnext()
async def foo():
# error: [not-iterable] "Object of type `AsyncIterable` is not async-iterable"
async for x in AsyncIterable():
reveal_type(x) # revealed: Unknown
```
### Possibly unbound `__anext__` method
```py
from typing_extensions import reveal_type
async def foo(flag: bool):
class PossiblyUnboundAnext:
if flag:
async def __anext__(self) -> int:
return 42
class AsyncIterable:
def __aiter__(self) -> PossiblyUnboundAnext:
return PossiblyUnboundAnext()
# error: [not-iterable] "Object of type `AsyncIterable` may not be async-iterable"
async for x in AsyncIterable():
reveal_type(x) # revealed: int
```
### Possibly unbound `__aiter__` method
```py
from typing_extensions import reveal_type
async def foo(flag: bool):
class AsyncIterable:
async def __anext__(self) -> int:
return 42
class PossiblyUnboundAiter:
if flag:
def __aiter__(self) -> AsyncIterable:
return AsyncIterable()
# error: "Object of type `PossiblyUnboundAiter` may not be async-iterable"
async for x in PossiblyUnboundAiter():
reveal_type(x) # revealed: int
```
### Wrong signature for `__aiter__`
```py
from typing_extensions import reveal_type
class AsyncIterator:
async def __anext__(self) -> int:
return 42
class AsyncIterable:
def __aiter__(self, arg: int) -> AsyncIterator: # wrong
return AsyncIterator()
async def foo():
# error: [not-iterable] "Object of type `AsyncIterable` is not async-iterable"
async for x in AsyncIterable():
reveal_type(x) # revealed: int
```
### Wrong signature for `__anext__`
```py
from typing_extensions import reveal_type
class AsyncIterator:
async def __anext__(self, arg: int) -> int: # wrong
return 42
class AsyncIterable:
def __aiter__(self) -> AsyncIterator:
return AsyncIterator()
async def foo():
# error: [not-iterable] "Object of type `AsyncIterable` is not async-iterable"
async for x in AsyncIterable():
reveal_type(x) # revealed: int
```