mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-21 07:41:53 +00:00
[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:
parent
e593761232
commit
eb02aa5676
17 changed files with 908 additions and 197 deletions
|
@ -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
|
||||
```
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue