[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

@ -128,7 +128,7 @@ class AsyncIterable:
return AsyncIterator()
async def _():
# revealed: @Todo(async iterables/iterators)
# revealed: int
[reveal_type(x) async for x in AsyncIterable()]
```
@ -147,6 +147,7 @@ class Iterable:
return Iterator()
async def _():
# revealed: @Todo(async iterables/iterators)
# error: [not-iterable] "Object of type `Iterable` is not async-iterable"
# revealed: Unknown
[reveal_type(x) async for x in Iterable()]
```

View file

@ -27,6 +27,7 @@ If all of the comprehensions are `async`, on the other hand, the code was still
```py
async def test():
# error: [not-iterable] "Object of type `range` is not async-iterable"
return [[x async for x in elements(n)] async for n in range(3)]
```

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
```

View file

@ -0,0 +1,52 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: async_for.md - Async - Error cases - No `__aiter__` method
mdtest path: crates/ty_python_semantic/resources/mdtest/loops/async_for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class NotAsyncIterable: ...
4 |
5 | async def foo():
6 | # error: [not-iterable] "Object of type `NotAsyncIterable` is not async-iterable"
7 | async for x in NotAsyncIterable():
8 | reveal_type(x) # revealed: Unknown
```
# Diagnostics
```
error[not-iterable]: Object of type `NotAsyncIterable` is not async-iterable
--> src/mdtest_snippet.py:7:20
|
5 | async def foo():
6 | # error: [not-iterable] "Object of type `NotAsyncIterable` is not async-iterable"
7 | async for x in NotAsyncIterable():
| ^^^^^^^^^^^^^^^^^^
8 | reveal_type(x) # revealed: Unknown
|
info: It has no `__aiter__` method
info: rule `not-iterable` is enabled by default
```
```
info[revealed-type]: Revealed type
--> src/mdtest_snippet.py:8:21
|
6 | # error: [not-iterable] "Object of type `NotAsyncIterable` is not async-iterable"
7 | async for x in NotAsyncIterable():
8 | reveal_type(x) # revealed: Unknown
| ^ `Unknown`
|
```

View file

@ -0,0 +1,56 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: async_for.md - Async - Error cases - No `__anext__` method
mdtest path: crates/ty_python_semantic/resources/mdtest/loops/async_for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class NoAnext: ...
4 |
5 | class AsyncIterable:
6 | def __aiter__(self) -> NoAnext:
7 | return NoAnext()
8 |
9 | async def foo():
10 | # error: [not-iterable] "Object of type `AsyncIterable` is not async-iterable"
11 | async for x in AsyncIterable():
12 | reveal_type(x) # revealed: Unknown
```
# Diagnostics
```
error[not-iterable]: Object of type `AsyncIterable` is not async-iterable
--> src/mdtest_snippet.py:11:20
|
9 | async def foo():
10 | # error: [not-iterable] "Object of type `AsyncIterable` is not async-iterable"
11 | async for x in AsyncIterable():
| ^^^^^^^^^^^^^^^
12 | reveal_type(x) # revealed: Unknown
|
info: Its `__aiter__` method returns an object of type `NoAnext`, which has no `__anext__` method
info: rule `not-iterable` is enabled by default
```
```
info[revealed-type]: Revealed type
--> src/mdtest_snippet.py:12:21
|
10 | # error: [not-iterable] "Object of type `AsyncIterable` is not async-iterable"
11 | async for x in AsyncIterable():
12 | reveal_type(x) # revealed: Unknown
| ^ `Unknown`
|
```

View file

@ -0,0 +1,58 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: async_for.md - Async - Error cases - Possibly unbound `__aiter__` method
mdtest path: crates/ty_python_semantic/resources/mdtest/loops/async_for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | async def foo(flag: bool):
4 | class AsyncIterable:
5 | async def __anext__(self) -> int:
6 | return 42
7 |
8 | class PossiblyUnboundAiter:
9 | if flag:
10 | def __aiter__(self) -> AsyncIterable:
11 | return AsyncIterable()
12 |
13 | # error: "Object of type `PossiblyUnboundAiter` may not be async-iterable"
14 | async for x in PossiblyUnboundAiter():
15 | reveal_type(x) # revealed: int
```
# Diagnostics
```
error[not-iterable]: Object of type `PossiblyUnboundAiter` may not be async-iterable
--> src/mdtest_snippet.py:14:20
|
13 | # error: "Object of type `PossiblyUnboundAiter` may not be async-iterable"
14 | async for x in PossiblyUnboundAiter():
| ^^^^^^^^^^^^^^^^^^^^^^
15 | reveal_type(x) # revealed: int
|
info: Its `__aiter__` attribute (with type `bound method PossiblyUnboundAiter.__aiter__() -> AsyncIterable`) may not be callable
info: rule `not-iterable` is enabled by default
```
```
info[revealed-type]: Revealed type
--> src/mdtest_snippet.py:15:21
|
13 | # error: "Object of type `PossiblyUnboundAiter` may not be async-iterable"
14 | async for x in PossiblyUnboundAiter():
15 | reveal_type(x) # revealed: int
| ^ `int`
|
```

View file

@ -0,0 +1,58 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: async_for.md - Async - Error cases - Possibly unbound `__anext__` method
mdtest path: crates/ty_python_semantic/resources/mdtest/loops/async_for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | async def foo(flag: bool):
4 | class PossiblyUnboundAnext:
5 | if flag:
6 | async def __anext__(self) -> int:
7 | return 42
8 |
9 | class AsyncIterable:
10 | def __aiter__(self) -> PossiblyUnboundAnext:
11 | return PossiblyUnboundAnext()
12 |
13 | # error: [not-iterable] "Object of type `AsyncIterable` may not be async-iterable"
14 | async for x in AsyncIterable():
15 | reveal_type(x) # revealed: int
```
# Diagnostics
```
error[not-iterable]: Object of type `AsyncIterable` may not be async-iterable
--> src/mdtest_snippet.py:14:20
|
13 | # error: [not-iterable] "Object of type `AsyncIterable` may not be async-iterable"
14 | async for x in AsyncIterable():
| ^^^^^^^^^^^^^^^
15 | reveal_type(x) # revealed: int
|
info: Its `__aiter__` method returns an object of type `PossiblyUnboundAnext`, which may not have a `__anext__` method
info: rule `not-iterable` is enabled by default
```
```
info[revealed-type]: Revealed type
--> src/mdtest_snippet.py:15:21
|
13 | # error: [not-iterable] "Object of type `AsyncIterable` may not be async-iterable"
14 | async for x in AsyncIterable():
15 | reveal_type(x) # revealed: int
| ^ `int`
|
```

View file

@ -0,0 +1,57 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: async_for.md - Async - Error cases - Synchronously iterable, but not asynchronously iterable
mdtest path: crates/ty_python_semantic/resources/mdtest/loops/async_for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | async def foo():
4 | class Iterator:
5 | def __next__(self) -> int:
6 | return 42
7 |
8 | class Iterable:
9 | def __iter__(self) -> Iterator:
10 | return Iterator()
11 |
12 | # error: [not-iterable] "Object of type `Iterator` is not async-iterable"
13 | async for x in Iterator():
14 | reveal_type(x) # revealed: Unknown
```
# Diagnostics
```
error[not-iterable]: Object of type `Iterator` is not async-iterable
--> src/mdtest_snippet.py:13:20
|
12 | # error: [not-iterable] "Object of type `Iterator` is not async-iterable"
13 | async for x in Iterator():
| ^^^^^^^^^^
14 | reveal_type(x) # revealed: Unknown
|
info: It has no `__aiter__` method
info: rule `not-iterable` is enabled by default
```
```
info[revealed-type]: Revealed type
--> src/mdtest_snippet.py:14:21
|
12 | # error: [not-iterable] "Object of type `Iterator` is not async-iterable"
13 | async for x in Iterator():
14 | reveal_type(x) # revealed: Unknown
| ^ `Unknown`
|
```

View file

@ -0,0 +1,59 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: async_for.md - Async - Error cases - Wrong signature for `__anext__`
mdtest path: crates/ty_python_semantic/resources/mdtest/loops/async_for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class AsyncIterator:
4 | async def __anext__(self, arg: int) -> int: # wrong
5 | return 42
6 |
7 | class AsyncIterable:
8 | def __aiter__(self) -> AsyncIterator:
9 | return AsyncIterator()
10 |
11 | async def foo():
12 | # error: [not-iterable] "Object of type `AsyncIterable` is not async-iterable"
13 | async for x in AsyncIterable():
14 | reveal_type(x) # revealed: int
```
# Diagnostics
```
error[not-iterable]: Object of type `AsyncIterable` is not async-iterable
--> src/mdtest_snippet.py:13:20
|
11 | async def foo():
12 | # error: [not-iterable] "Object of type `AsyncIterable` is not async-iterable"
13 | async for x in AsyncIterable():
| ^^^^^^^^^^^^^^^
14 | reveal_type(x) # revealed: int
|
info: Its `__aiter__` method returns an object of type `AsyncIterator`, which has an invalid `__anext__` method
info: Expected signature for `__anext__` is `def __anext__(self): ...`
info: rule `not-iterable` is enabled by default
```
```
info[revealed-type]: Revealed type
--> src/mdtest_snippet.py:14:21
|
12 | # error: [not-iterable] "Object of type `AsyncIterable` is not async-iterable"
13 | async for x in AsyncIterable():
14 | reveal_type(x) # revealed: int
| ^ `int`
|
```

View file

@ -0,0 +1,59 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: async_for.md - Async - Error cases - Wrong signature for `__aiter__`
mdtest path: crates/ty_python_semantic/resources/mdtest/loops/async_for.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing_extensions import reveal_type
2 |
3 | class AsyncIterator:
4 | async def __anext__(self) -> int:
5 | return 42
6 |
7 | class AsyncIterable:
8 | def __aiter__(self, arg: int) -> AsyncIterator: # wrong
9 | return AsyncIterator()
10 |
11 | async def foo():
12 | # error: [not-iterable] "Object of type `AsyncIterable` is not async-iterable"
13 | async for x in AsyncIterable():
14 | reveal_type(x) # revealed: int
```
# Diagnostics
```
error[not-iterable]: Object of type `AsyncIterable` is not async-iterable
--> src/mdtest_snippet.py:13:20
|
11 | async def foo():
12 | # error: [not-iterable] "Object of type `AsyncIterable` is not async-iterable"
13 | async for x in AsyncIterable():
| ^^^^^^^^^^^^^^^
14 | reveal_type(x) # revealed: int
|
info: Its `__aiter__` method has an invalid signature
info: Expected signature `def __aiter__(self): ...`
info: rule `not-iterable` is enabled by default
```
```
info[revealed-type]: Revealed type
--> src/mdtest_snippet.py:14:21
|
12 | # error: [not-iterable] "Object of type `AsyncIterable` is not async-iterable"
13 | async for x in AsyncIterable():
14 | reveal_type(x) # revealed: int
| ^ `int`
|
```

View file

@ -60,7 +60,7 @@ error[not-iterable]: Object of type `Iterable1` may not be iterable
29 | reveal_type(x) # revealed: int | str
|
info: Its `__iter__` method returns an object of type `Iterator1`, which may have an invalid `__next__` method
info: Expected signature for `__next__` is `def __next__(self): ...`)
info: Expected signature for `__next__` is `def __next__(self): ...`
info: rule `not-iterable` is enabled by default
```

View file

@ -19,14 +19,15 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syn
5 | # error: 19 [invalid-syntax] "cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)"
6 | return {n: [x async for x in elements(n)] for n in range(3)}
7 | async def test():
8 | return [[x async for x in elements(n)] async for n in range(3)]
9 | async def f():
10 | [x for x in [1]] and [x async for x in elements(1)]
11 |
12 | async def f():
13 | def g():
14 | pass
15 | [x async for x in elements(1)]
8 | # error: [not-iterable] "Object of type `range` is not async-iterable"
9 | return [[x async for x in elements(n)] async for n in range(3)]
10 | async def f():
11 | [x for x in [1]] and [x async for x in elements(1)]
12 |
13 | async def f():
14 | def g():
15 | pass
16 | [x async for x in elements(1)]
```
# Diagnostics
@ -40,7 +41,23 @@ error[invalid-syntax]
6 | return {n: [x async for x in elements(n)] for n in range(3)}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
7 | async def test():
8 | return [[x async for x in elements(n)] async for n in range(3)]
8 | # error: [not-iterable] "Object of type `range` is not async-iterable"
|
```
```
error[not-iterable]: Object of type `range` is not async-iterable
--> src/mdtest_snippet.py:9:59
|
7 | async def test():
8 | # error: [not-iterable] "Object of type `range` is not async-iterable"
9 | return [[x async for x in elements(n)] async for n in range(3)]
| ^^^^^^^^
10 | async def f():
11 | [x for x in [1]] and [x async for x in elements(1)]
|
info: It has no `__aiter__` method
info: rule `not-iterable` is enabled by default
```

View file

@ -49,7 +49,7 @@ use crate::semantic_index::use_def::{
};
use crate::semantic_index::{ArcUseDefMap, ExpressionsScopeMap, SemanticIndex};
use crate::semantic_model::HasTrackedScope;
use crate::unpack::{Unpack, UnpackKind, UnpackPosition, UnpackValue};
use crate::unpack::{EvaluationMode, Unpack, UnpackKind, UnpackPosition, UnpackValue};
use crate::{Db, Program};
mod except_handlers;
@ -2804,9 +2804,17 @@ impl<'ast> Unpackable<'ast> {
const fn kind(&self) -> UnpackKind {
match self {
Unpackable::Assign(_) => UnpackKind::Assign,
Unpackable::For(_) | Unpackable::Comprehension { .. } => UnpackKind::Iterable,
Unpackable::For(ast::StmtFor { is_async, .. }) => UnpackKind::Iterable {
mode: EvaluationMode::from_is_async(*is_async),
},
Unpackable::Comprehension {
node: ast::Comprehension { is_async, .. },
..
} => UnpackKind::Iterable {
mode: EvaluationMode::from_is_async(*is_async),
},
Unpackable::WithItem { is_async, .. } => UnpackKind::ContextManager {
is_async: *is_async,
mode: EvaluationMode::from_is_async(*is_async),
},
}
}

View file

@ -59,6 +59,7 @@ use crate::types::mro::{Mro, MroError, MroIterator};
pub(crate) use crate::types::narrow::infer_narrowing_constraint;
use crate::types::signatures::{Parameter, ParameterForm, Parameters, walk_signature};
use crate::types::tuple::{TupleSpec, TupleType};
use crate::unpack::EvaluationMode;
pub use crate::util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
use crate::{Db, FxOrderSet, Module, Program};
pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass};
@ -4637,6 +4638,65 @@ impl<'db> Type<'db> {
/// y(*x)
/// ```
fn try_iterate(self, db: &'db dyn Db) -> Result<Cow<'db, TupleSpec<'db>>, IterationError<'db>> {
self.try_iterate_with_mode(db, EvaluationMode::Sync)
}
fn try_iterate_with_mode(
self,
db: &'db dyn Db,
mode: EvaluationMode,
) -> Result<Cow<'db, TupleSpec<'db>>, IterationError<'db>> {
if mode.is_async() {
let try_call_dunder_anext_on_iterator = |iterator: Type<'db>| {
iterator
.try_call_dunder(db, "__anext__", CallArguments::none())
.map(|dunder_anext_outcome| {
dunder_anext_outcome.return_type(db).resolve_await(db)
})
};
return match self.try_call_dunder(db, "__aiter__", CallArguments::none()) {
Ok(dunder_aiter_bindings) => {
let iterator = dunder_aiter_bindings.return_type(db);
match try_call_dunder_anext_on_iterator(iterator) {
Ok(result) => Ok(Cow::Owned(TupleSpec::homogeneous(result))),
Err(dunder_anext_error) => {
Err(IterationError::IterReturnsInvalidIterator {
iterator,
dunder_error: dunder_anext_error,
mode,
})
}
}
}
Err(CallDunderError::PossiblyUnbound(dunder_aiter_bindings)) => {
let iterator = dunder_aiter_bindings.return_type(db);
match try_call_dunder_anext_on_iterator(iterator) {
Ok(_) => Err(IterationError::IterCallError {
kind: CallErrorKind::PossiblyNotCallable,
bindings: dunder_aiter_bindings,
mode,
}),
Err(dunder_anext_error) => {
Err(IterationError::IterReturnsInvalidIterator {
iterator,
dunder_error: dunder_anext_error,
mode,
})
}
}
}
Err(CallDunderError::CallError(kind, bindings)) => {
Err(IterationError::IterCallError {
kind,
bindings,
mode,
})
}
Err(CallDunderError::MethodNotAvailable) => Err(IterationError::UnboundAiterError),
};
}
match self {
Type::Tuple(tuple_type) => return Ok(Cow::Borrowed(tuple_type.tuple(db))),
Type::GenericAlias(alias) if alias.origin(db).is_tuple(db) => {
@ -4693,7 +4753,8 @@ impl<'db> Type<'db> {
.map_err(
|dunder_next_error| IterationError::IterReturnsInvalidIterator {
iterator,
dunder_next_error,
dunder_error: dunder_next_error,
mode,
},
)
}
@ -4728,15 +4789,18 @@ impl<'db> Type<'db> {
Err(dunder_next_error) => Err(IterationError::IterReturnsInvalidIterator {
iterator,
dunder_next_error,
dunder_error: dunder_next_error,
mode,
}),
}
}
// `__iter__` is definitely bound but it can't be called with the expected arguments
Err(CallDunderError::CallError(kind, bindings)) => {
Err(IterationError::IterCallError(kind, bindings))
}
Err(CallDunderError::CallError(kind, bindings)) => Err(IterationError::IterCallError {
kind,
bindings,
mode,
}),
// There's no `__iter__` method. Try `__getitem__` instead...
Err(CallDunderError::MethodNotAvailable) => try_call_dunder_getitem()
@ -4818,7 +4882,7 @@ impl<'db> Type<'db> {
fn generator_return_type(self, db: &'db dyn Db) -> Option<Type<'db>> {
// TODO: Ideally, we would first try to upcast `self` to an instance of `Generator` and *then*
// match on the protocol instance to get the `ReturnType` type parameter. For now, implement
// an ad-hoc solution that works for protocols and instances of classes that directly inherit
// an ad-hoc solution that works for protocols and instances of classes that explicitly inherit
// from the `Generator` protocol, such as `types.GeneratorType`.
let from_class_base = |base: ClassBase<'db>| {
@ -6806,18 +6870,24 @@ impl<'db> ContextManagerError<'db> {
/// Error returned if a type is not (or may not be) iterable.
#[derive(Debug)]
enum IterationError<'db> {
/// The object being iterated over has a bound `__iter__` method,
/// The object being iterated over has a bound `__(a)iter__` method,
/// but calling it with the expected arguments results in an error.
IterCallError(CallErrorKind, Box<Bindings<'db>>),
IterCallError {
kind: CallErrorKind,
bindings: Box<Bindings<'db>>,
mode: EvaluationMode,
},
/// The object being iterated over has a bound `__iter__` method that can be called
/// The object being iterated over has a bound `__(a)iter__` method that can be called
/// with the expected types, but it returns an object that is not a valid iterator.
IterReturnsInvalidIterator {
/// The type of the object returned by the `__iter__` method.
/// The type of the object returned by the `__(a)iter__` method.
iterator: Type<'db>,
/// The error we encountered when we tried to call `__next__` on the type
/// returned by `__iter__`
dunder_next_error: CallDunderError<'db>,
/// The error we encountered when we tried to call `__(a)next__` on the type
/// returned by `__(a)iter__`
dunder_error: CallDunderError<'db>,
/// Whether this is a synchronous or an asynchronous iterator.
mode: EvaluationMode,
},
/// The object being iterated over has a bound `__iter__` method that returns a
@ -6838,6 +6908,9 @@ enum IterationError<'db> {
UnboundIterAndGetitemError {
dunder_getitem_error: CallDunderError<'db>,
},
/// The asynchronous iterable has no `__aiter__` method.
UnboundAiterError,
}
impl<'db> IterationError<'db> {
@ -6847,16 +6920,43 @@ impl<'db> IterationError<'db> {
/// Returns the element type if it is known, or `None` if the type is never iterable.
fn element_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
let return_type = |result: Result<Bindings<'db>, CallDunderError<'db>>| {
result
.map(|outcome| Some(outcome.return_type(db)))
.unwrap_or_else(|call_error| call_error.return_type(db))
};
match self {
Self::IterReturnsInvalidIterator {
dunder_next_error, ..
} => dunder_next_error.return_type(db),
dunder_error, mode, ..
} => dunder_error.return_type(db).map(|ty| {
if mode.is_async() {
ty.resolve_await(db)
} else {
ty
}
}),
Self::IterCallError(_, dunder_iter_bindings) => dunder_iter_bindings
.return_type(db)
.try_call_dunder(db, "__next__", CallArguments::none())
.map(|dunder_next_outcome| Some(dunder_next_outcome.return_type(db)))
.unwrap_or_else(|dunder_next_call_error| dunder_next_call_error.return_type(db)),
Self::IterCallError {
kind: _,
bindings: dunder_iter_bindings,
mode,
} => {
if mode.is_async() {
return_type(dunder_iter_bindings.return_type(db).try_call_dunder(
db,
"__anext__",
CallArguments::none(),
))
.map(|ty| ty.resolve_await(db))
} else {
return_type(dunder_iter_bindings.return_type(db).try_call_dunder(
db,
"__next__",
CallArguments::none(),
))
}
}
Self::PossiblyUnboundIterAndGetitemError {
dunder_next_return,
@ -6882,6 +6982,19 @@ impl<'db> IterationError<'db> {
Self::UnboundIterAndGetitemError {
dunder_getitem_error,
} => dunder_getitem_error.return_type(db),
Self::UnboundAiterError => None,
}
}
/// Does this error concern a synchronous or asynchronous iterable?
fn mode(&self) -> EvaluationMode {
match self {
Self::IterCallError { mode, .. } => *mode,
Self::IterReturnsInvalidIterator { mode, .. } => *mode,
Self::PossiblyUnboundIterAndGetitemError { .. }
| Self::UnboundIterAndGetitemError { .. } => EvaluationMode::Sync,
Self::UnboundAiterError => EvaluationMode::Async,
}
}
@ -6898,6 +7011,7 @@ impl<'db> IterationError<'db> {
db: &'a dyn Db,
builder: LintDiagnosticGuardBuilder<'a, 'a>,
iterable_type: Type<'a>,
mode: EvaluationMode,
}
impl<'a> Reporter<'a> {
@ -6907,8 +7021,9 @@ impl<'db> IterationError<'db> {
#[expect(clippy::wrong_self_convention)]
fn is_not(self, because: impl std::fmt::Display) -> LintDiagnosticGuard<'a, 'a> {
let mut diag = self.builder.into_diagnostic(format_args!(
"Object of type `{iterable_type}` is not iterable",
"Object of type `{iterable_type}` is not {maybe_async}iterable",
iterable_type = self.iterable_type.display(self.db),
maybe_async = if self.mode.is_async() { "async-" } else { "" }
));
diag.info(because);
diag
@ -6919,8 +7034,9 @@ impl<'db> IterationError<'db> {
/// `because` should explain why `iterable_type` is likely not iterable.
fn may_not(self, because: impl std::fmt::Display) -> LintDiagnosticGuard<'a, 'a> {
let mut diag = self.builder.into_diagnostic(format_args!(
"Object of type `{iterable_type}` may not be iterable",
"Object of type `{iterable_type}` may not be {maybe_async}iterable",
iterable_type = self.iterable_type.display(self.db),
maybe_async = if self.mode.is_async() { "async-" } else { "" }
));
diag.info(because);
diag
@ -6931,82 +7047,107 @@ impl<'db> IterationError<'db> {
return;
};
let db = context.db();
let mode = self.mode();
let reporter = Reporter {
db,
builder,
iterable_type,
mode,
};
// TODO: for all of these error variants, the "explanation" for the diagnostic
// (everything after the "because") should really be presented as a "help:", "note",
// or similar, rather than as part of the same sentence as the error message.
match self {
Self::IterCallError(CallErrorKind::NotCallable, bindings) => {
Self::IterCallError {
kind,
bindings,
mode,
} => {
let method = if mode.is_async() {
"__aiter__"
} else {
"__iter__"
};
match kind {
CallErrorKind::NotCallable => {
reporter.is_not(format_args!(
"Its `__iter__` attribute has type `{dunder_iter_type}`, which is not callable",
"Its `{method}` attribute has type `{dunder_iter_type}`, which is not callable",
dunder_iter_type = bindings.callable_type().display(db),
));
}
Self::IterCallError(CallErrorKind::PossiblyNotCallable, bindings)
if bindings.is_single() =>
{
CallErrorKind::PossiblyNotCallable => {
reporter.may_not(format_args!(
"Its `__iter__` attribute (with type `{dunder_iter_type}`) \
"Its `{method}` attribute (with type `{dunder_iter_type}`) \
may not be callable",
dunder_iter_type = bindings.callable_type().display(db),
));
}
Self::IterCallError(CallErrorKind::PossiblyNotCallable, bindings) => {
reporter.may_not(format_args!(
"Its `__iter__` attribute (with type `{dunder_iter_type}`) \
may not be callable",
dunder_iter_type = bindings.callable_type().display(db),
));
}
Self::IterCallError(CallErrorKind::BindingError, bindings) if bindings.is_single() => {
CallErrorKind::BindingError => {
if bindings.is_single() {
reporter
.is_not("Its `__iter__` method has an invalid signature")
.info("Expected signature `def __iter__(self): ...`");
}
Self::IterCallError(CallErrorKind::BindingError, bindings) => {
let mut diag =
reporter.may_not("Its `__iter__` method may have an invalid signature");
.is_not(format_args!(
"Its `{method}` method has an invalid signature"
))
.info(format_args!("Expected signature `def {method}(self): ...`"));
} else {
let mut diag = reporter.may_not(format_args!(
"Its `{method}` method may have an invalid signature"
));
diag.info(format_args!(
"Type of `__iter__` is `{dunder_iter_type}`",
"Type of `{method}` is `{dunder_iter_type}`",
dunder_iter_type = bindings.callable_type().display(db),
));
diag.info("Expected signature for `__iter__` is `def __iter__(self): ...`");
diag.info(format_args!(
"Expected signature for `{method}` is `def {method}(self): ...`",
));
}
}
}
}
Self::IterReturnsInvalidIterator {
iterator,
dunder_next_error,
} => match dunder_next_error {
dunder_error: dunder_next_error,
mode,
} => {
let dunder_iter_name = if mode.is_async() {
"__aiter__"
} else {
"__iter__"
};
let dunder_next_name = if mode.is_async() {
"__anext__"
} else {
"__next__"
};
match dunder_next_error {
CallDunderError::MethodNotAvailable => {
reporter.is_not(format_args!(
"Its `__iter__` method returns an object of type `{iterator_type}`, \
which has no `__next__` method",
"Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \
which has no `{dunder_next_name}` method",
iterator_type = iterator.display(db),
));
}
CallDunderError::PossiblyUnbound(_) => {
reporter.may_not(format_args!(
"Its `__iter__` method returns an object of type `{iterator_type}`, \
which may not have a `__next__` method",
"Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \
which may not have a `{dunder_next_name}` method",
iterator_type = iterator.display(db),
));
}
CallDunderError::CallError(CallErrorKind::NotCallable, _) => {
reporter.is_not(format_args!(
"Its `__iter__` method returns an object of type `{iterator_type}`, \
which has a `__next__` attribute that is not callable",
"Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \
which has a `{dunder_next_name}` attribute that is not callable",
iterator_type = iterator.display(db),
));
}
CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _) => {
reporter.may_not(format_args!(
"Its `__iter__` method returns an object of type `{iterator_type}`, \
which has a `__next__` attribute that may not be callable",
"Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \
which has a `{dunder_next_name}` attribute that may not be callable",
iterator_type = iterator.display(db),
));
}
@ -7015,22 +7156,23 @@ impl<'db> IterationError<'db> {
{
reporter
.is_not(format_args!(
"Its `__iter__` method returns an object of type `{iterator_type}`, \
which has an invalid `__next__` method",
"Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \
which has an invalid `{dunder_next_name}` method",
iterator_type = iterator.display(db),
))
.info("Expected signature for `__next__` is `def __next__(self): ...`");
.info(format_args!("Expected signature for `{dunder_next_name}` is `def {dunder_next_name}(self): ...`"));
}
CallDunderError::CallError(CallErrorKind::BindingError, _) => {
reporter
.may_not(format_args!(
"Its `__iter__` method returns an object of type `{iterator_type}`, \
which may have an invalid `__next__` method",
"Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \
which may have an invalid `{dunder_next_name}` method",
iterator_type = iterator.display(db),
))
.info("Expected signature for `__next__` is `def __next__(self): ...`)");
.info(format_args!("Expected signature for `{dunder_next_name}` is `def {dunder_next_name}(self): ...`"));
}
}
}
},
Self::PossiblyUnboundIterAndGetitemError {
dunder_getitem_error,
@ -7167,6 +7309,10 @@ impl<'db> IterationError<'db> {
);
}
},
IterationError::UnboundAiterError => {
reporter.is_not("It has no `__aiter__` method");
}
}
}
}

View file

@ -123,7 +123,7 @@ use crate::types::{
TypeAndQualifiers, TypeIsType, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance,
TypeVarKind, TypeVarVariance, UnionBuilder, UnionType, binding_type, todo_type,
};
use crate::unpack::{Unpack, UnpackPosition};
use crate::unpack::{EvaluationMode, Unpack, UnpackPosition};
use crate::util::diagnostics::format_enumeration;
use crate::util::subscript::{PyIndex, PySlice};
use crate::{Db, FxOrderSet, Program};
@ -4560,11 +4560,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let iterable = for_stmt.iterable(self.module());
let target = for_stmt.target(self.module());
let loop_var_value_type = if for_stmt.is_async() {
let _iterable_type = self.infer_standalone_expression(iterable);
todo_type!("async iterables/iterators")
} else {
match for_stmt.target_kind() {
let loop_var_value_type = match for_stmt.target_kind() {
TargetKind::Sequence(unpack_position, unpack) => {
let unpacked = infer_unpack_types(self.db(), unpack);
if unpack_position == UnpackPosition::First {
@ -4575,15 +4571,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
TargetKind::Single => {
let iterable_type = self.infer_standalone_expression(iterable);
iterable_type
.try_iterate(self.db())
.try_iterate_with_mode(
self.db(),
EvaluationMode::from_is_async(for_stmt.is_async()),
)
.map(|tuple| tuple.homogeneous_element_type(self.db()))
.unwrap_or_else(|err| {
err.report_diagnostic(&self.context, iterable_type, iterable.into());
err.fallback_element_type(self.db())
})
}
}
};
self.store_expression_type(target, loop_var_value_type);
@ -5692,12 +5691,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
};
let target_type = if comprehension.is_async() {
// TODO: async iterables/iterators! -- Alex
let _iterable_type = infer_iterable_type();
todo_type!("async iterables/iterators")
} else {
match comprehension.target_kind() {
let target_type = match comprehension.target_kind() {
TargetKind::Sequence(unpack_position, unpack) => {
let unpacked = infer_unpack_types(self.db(), unpack);
if unpack_position == UnpackPosition::First {
@ -5708,15 +5702,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
TargetKind::Single => {
let iterable_type = infer_iterable_type();
iterable_type
.try_iterate(self.db())
.try_iterate_with_mode(
self.db(),
EvaluationMode::from_is_async(comprehension.is_async()),
)
.map(|tuple| tuple.homogeneous_element_type(self.db()))
.unwrap_or_else(|err| {
err.report_diagnostic(&self.context, iterable_type, iterable.into());
err.fallback_element_type(self.db())
})
}
}
};
self.expressions.insert(target.into(), target_type);

View file

@ -64,8 +64,8 @@ impl<'db, 'ast> Unpacker<'db, 'ast> {
value_type
}
}
UnpackKind::Iterable => value_type
.try_iterate(self.db())
UnpackKind::Iterable { mode } => value_type
.try_iterate_with_mode(self.db(), mode)
.map(|tuple| tuple.homogeneous_element_type(self.db()))
.unwrap_or_else(|err| {
err.report_diagnostic(
@ -75,8 +75,8 @@ impl<'db, 'ast> Unpacker<'db, 'ast> {
);
err.fallback_element_type(self.db())
}),
UnpackKind::ContextManager { is_async } => {
if is_async {
UnpackKind::ContextManager { mode } => {
if mode.is_async() {
value_type.aenter(self.db())
} else {
value_type.try_enter(self.db()).unwrap_or_else(|err| {

View file

@ -99,12 +99,32 @@ impl<'db> UnpackValue<'db> {
}
}
#[derive(Clone, Copy, Debug, Hash, salsa::Update)]
pub(crate) enum EvaluationMode {
Sync,
Async,
}
impl EvaluationMode {
pub(crate) const fn from_is_async(is_async: bool) -> Self {
if is_async {
EvaluationMode::Async
} else {
EvaluationMode::Sync
}
}
pub(crate) const fn is_async(self) -> bool {
matches!(self, EvaluationMode::Async)
}
}
#[derive(Clone, Copy, Debug, Hash, salsa::Update)]
pub(crate) enum UnpackKind {
/// An iterable expression like the one in a `for` loop or a comprehension.
Iterable,
Iterable { mode: EvaluationMode },
/// An context manager expression like the one in a `with` statement.
ContextManager { is_async: bool },
ContextManager { mode: EvaluationMode },
/// An expression that is being assigned to a target.
Assign,
}