[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() return AsyncIterator()
async def _(): async def _():
# revealed: @Todo(async iterables/iterators) # revealed: int
[reveal_type(x) async for x in AsyncIterable()] [reveal_type(x) async for x in AsyncIterable()]
``` ```
@ -147,6 +147,7 @@ class Iterable:
return Iterator() return Iterator()
async def _(): 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()] [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 ```py
async def test(): 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)] 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. 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 ## Basic async for loop
```py ```py
@ -35,11 +14,154 @@ async def foo():
def __aiter__(self) -> IntAsyncIterator: def __aiter__(self) -> IntAsyncIterator:
return IntAsyncIterator() return IntAsyncIterator()
# TODO(Alex): async iterables/iterators!
async for x in IntAsyncIterable(): async for x in IntAsyncIterable():
pass reveal_type(x) # revealed: int
```
# error: [possibly-unresolved-reference]
# revealed: @Todo(async iterables/iterators) ## Async for loop with unpacking
reveal_type(x)
```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 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: 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 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)" 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)} 6 | return {n: [x async for x in elements(n)] for n in range(3)}
7 | async def test(): 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"
9 | async def f(): 9 | return [[x async for x in elements(n)] async for n in range(3)]
10 | [x for x in [1]] and [x async for x in elements(1)] 10 | async def f():
11 | 11 | [x for x in [1]] and [x async for x in elements(1)]
12 | async def f(): 12 |
13 | def g(): 13 | async def f():
14 | pass 14 | def g():
15 | [x async for x in elements(1)] 15 | pass
16 | [x async for x in elements(1)]
``` ```
# Diagnostics # Diagnostics
@ -40,7 +41,23 @@ error[invalid-syntax]
6 | return {n: [x async for x in elements(n)] for n in range(3)} 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) | ^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot use an asynchronous comprehension inside of a synchronous comprehension on Python 3.10 (syntax was added in 3.11)
7 | async def test(): 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_index::{ArcUseDefMap, ExpressionsScopeMap, SemanticIndex};
use crate::semantic_model::HasTrackedScope; use crate::semantic_model::HasTrackedScope;
use crate::unpack::{Unpack, UnpackKind, UnpackPosition, UnpackValue}; use crate::unpack::{EvaluationMode, Unpack, UnpackKind, UnpackPosition, UnpackValue};
use crate::{Db, Program}; use crate::{Db, Program};
mod except_handlers; mod except_handlers;
@ -2804,9 +2804,17 @@ impl<'ast> Unpackable<'ast> {
const fn kind(&self) -> UnpackKind { const fn kind(&self) -> UnpackKind {
match self { match self {
Unpackable::Assign(_) => UnpackKind::Assign, 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 { 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; pub(crate) use crate::types::narrow::infer_narrowing_constraint;
use crate::types::signatures::{Parameter, ParameterForm, Parameters, walk_signature}; use crate::types::signatures::{Parameter, ParameterForm, Parameters, walk_signature};
use crate::types::tuple::{TupleSpec, TupleType}; use crate::types::tuple::{TupleSpec, TupleType};
use crate::unpack::EvaluationMode;
pub use crate::util::diagnostics::add_inferred_python_version_hint_to_diagnostic; pub use crate::util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
use crate::{Db, FxOrderSet, Module, Program}; use crate::{Db, FxOrderSet, Module, Program};
pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass}; pub(crate) use class::{ClassLiteral, ClassType, GenericAlias, KnownClass};
@ -4637,6 +4638,65 @@ impl<'db> Type<'db> {
/// y(*x) /// y(*x)
/// ``` /// ```
fn try_iterate(self, db: &'db dyn Db) -> Result<Cow<'db, TupleSpec<'db>>, IterationError<'db>> { 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 { match self {
Type::Tuple(tuple_type) => return Ok(Cow::Borrowed(tuple_type.tuple(db))), Type::Tuple(tuple_type) => return Ok(Cow::Borrowed(tuple_type.tuple(db))),
Type::GenericAlias(alias) if alias.origin(db).is_tuple(db) => { Type::GenericAlias(alias) if alias.origin(db).is_tuple(db) => {
@ -4693,7 +4753,8 @@ impl<'db> Type<'db> {
.map_err( .map_err(
|dunder_next_error| IterationError::IterReturnsInvalidIterator { |dunder_next_error| IterationError::IterReturnsInvalidIterator {
iterator, 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 { Err(dunder_next_error) => Err(IterationError::IterReturnsInvalidIterator {
iterator, iterator,
dunder_next_error, dunder_error: dunder_next_error,
mode,
}), }),
} }
} }
// `__iter__` is definitely bound but it can't be called with the expected arguments // `__iter__` is definitely bound but it can't be called with the expected arguments
Err(CallDunderError::CallError(kind, bindings)) => { Err(CallDunderError::CallError(kind, bindings)) => Err(IterationError::IterCallError {
Err(IterationError::IterCallError(kind, bindings)) kind,
} bindings,
mode,
}),
// There's no `__iter__` method. Try `__getitem__` instead... // There's no `__iter__` method. Try `__getitem__` instead...
Err(CallDunderError::MethodNotAvailable) => try_call_dunder_getitem() 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>> { 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* // 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 // 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`. // from the `Generator` protocol, such as `types.GeneratorType`.
let from_class_base = |base: ClassBase<'db>| { 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. /// Error returned if a type is not (or may not be) iterable.
#[derive(Debug)] #[derive(Debug)]
enum IterationError<'db> { 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. /// 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. /// with the expected types, but it returns an object that is not a valid iterator.
IterReturnsInvalidIterator { 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>, iterator: Type<'db>,
/// The error we encountered when we tried to call `__next__` on the type /// The error we encountered when we tried to call `__(a)next__` on the type
/// returned by `__iter__` /// returned by `__(a)iter__`
dunder_next_error: CallDunderError<'db>, 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 /// The object being iterated over has a bound `__iter__` method that returns a
@ -6838,6 +6908,9 @@ enum IterationError<'db> {
UnboundIterAndGetitemError { UnboundIterAndGetitemError {
dunder_getitem_error: CallDunderError<'db>, dunder_getitem_error: CallDunderError<'db>,
}, },
/// The asynchronous iterable has no `__aiter__` method.
UnboundAiterError,
} }
impl<'db> IterationError<'db> { 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. /// 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>> { 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 { match self {
Self::IterReturnsInvalidIterator { Self::IterReturnsInvalidIterator {
dunder_next_error, .. dunder_error, mode, ..
} => dunder_next_error.return_type(db), } => 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 Self::IterCallError {
.return_type(db) kind: _,
.try_call_dunder(db, "__next__", CallArguments::none()) bindings: dunder_iter_bindings,
.map(|dunder_next_outcome| Some(dunder_next_outcome.return_type(db))) mode,
.unwrap_or_else(|dunder_next_call_error| dunder_next_call_error.return_type(db)), } => {
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 { Self::PossiblyUnboundIterAndGetitemError {
dunder_next_return, dunder_next_return,
@ -6882,6 +6982,19 @@ impl<'db> IterationError<'db> {
Self::UnboundIterAndGetitemError { Self::UnboundIterAndGetitemError {
dunder_getitem_error, dunder_getitem_error,
} => dunder_getitem_error.return_type(db), } => 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, db: &'a dyn Db,
builder: LintDiagnosticGuardBuilder<'a, 'a>, builder: LintDiagnosticGuardBuilder<'a, 'a>,
iterable_type: Type<'a>, iterable_type: Type<'a>,
mode: EvaluationMode,
} }
impl<'a> Reporter<'a> { impl<'a> Reporter<'a> {
@ -6907,8 +7021,9 @@ impl<'db> IterationError<'db> {
#[expect(clippy::wrong_self_convention)] #[expect(clippy::wrong_self_convention)]
fn is_not(self, because: impl std::fmt::Display) -> LintDiagnosticGuard<'a, 'a> { fn is_not(self, because: impl std::fmt::Display) -> LintDiagnosticGuard<'a, 'a> {
let mut diag = self.builder.into_diagnostic(format_args!( 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), iterable_type = self.iterable_type.display(self.db),
maybe_async = if self.mode.is_async() { "async-" } else { "" }
)); ));
diag.info(because); diag.info(because);
diag diag
@ -6919,8 +7034,9 @@ impl<'db> IterationError<'db> {
/// `because` should explain why `iterable_type` is likely not iterable. /// `because` should explain why `iterable_type` is likely not iterable.
fn may_not(self, because: impl std::fmt::Display) -> LintDiagnosticGuard<'a, 'a> { fn may_not(self, because: impl std::fmt::Display) -> LintDiagnosticGuard<'a, 'a> {
let mut diag = self.builder.into_diagnostic(format_args!( 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), iterable_type = self.iterable_type.display(self.db),
maybe_async = if self.mode.is_async() { "async-" } else { "" }
)); ));
diag.info(because); diag.info(because);
diag diag
@ -6931,106 +7047,132 @@ impl<'db> IterationError<'db> {
return; return;
}; };
let db = context.db(); let db = context.db();
let mode = self.mode();
let reporter = Reporter { let reporter = Reporter {
db, db,
builder, builder,
iterable_type, iterable_type,
mode,
}; };
// TODO: for all of these error variants, the "explanation" for the diagnostic // TODO: for all of these error variants, the "explanation" for the diagnostic
// (everything after the "because") should really be presented as a "help:", "note", // (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. // or similar, rather than as part of the same sentence as the error message.
match self { match self {
Self::IterCallError(CallErrorKind::NotCallable, bindings) => { Self::IterCallError {
reporter.is_not(format_args!( kind,
"Its `__iter__` attribute has type `{dunder_iter_type}`, which is not callable", bindings,
dunder_iter_type = bindings.callable_type().display(db), mode,
)); } => {
} let method = if mode.is_async() {
Self::IterCallError(CallErrorKind::PossiblyNotCallable, bindings) "__aiter__"
if bindings.is_single() => } else {
{ "__iter__"
reporter.may_not(format_args!( };
"Its `__iter__` attribute (with type `{dunder_iter_type}`) \
may not be callable", match kind {
dunder_iter_type = bindings.callable_type().display(db), CallErrorKind::NotCallable => {
)); reporter.is_not(format_args!(
} "Its `{method}` attribute has type `{dunder_iter_type}`, which is not callable",
Self::IterCallError(CallErrorKind::PossiblyNotCallable, bindings) => { dunder_iter_type = bindings.callable_type().display(db),
reporter.may_not(format_args!( ));
"Its `__iter__` attribute (with type `{dunder_iter_type}`) \ }
may not be callable", CallErrorKind::PossiblyNotCallable => {
dunder_iter_type = bindings.callable_type().display(db), reporter.may_not(format_args!(
)); "Its `{method}` attribute (with type `{dunder_iter_type}`) \
} may not be callable",
Self::IterCallError(CallErrorKind::BindingError, bindings) if bindings.is_single() => { dunder_iter_type = bindings.callable_type().display(db),
reporter ));
.is_not("Its `__iter__` method has an invalid signature") }
.info("Expected signature `def __iter__(self): ...`"); CallErrorKind::BindingError => {
} if bindings.is_single() {
Self::IterCallError(CallErrorKind::BindingError, bindings) => { reporter
let mut diag = .is_not(format_args!(
reporter.may_not("Its `__iter__` method may have an invalid signature"); "Its `{method}` method has an invalid signature"
diag.info(format_args!( ))
"Type of `__iter__` is `{dunder_iter_type}`", .info(format_args!("Expected signature `def {method}(self): ...`"));
dunder_iter_type = bindings.callable_type().display(db), } else {
)); let mut diag = reporter.may_not(format_args!(
diag.info("Expected signature for `__iter__` is `def __iter__(self): ...`"); "Its `{method}` method may have an invalid signature"
));
diag.info(format_args!(
"Type of `{method}` is `{dunder_iter_type}`",
dunder_iter_type = bindings.callable_type().display(db),
));
diag.info(format_args!(
"Expected signature for `{method}` is `def {method}(self): ...`",
));
}
}
}
} }
Self::IterReturnsInvalidIterator { Self::IterReturnsInvalidIterator {
iterator, iterator,
dunder_next_error, dunder_error: dunder_next_error,
} => match dunder_next_error { mode,
CallDunderError::MethodNotAvailable => { } => {
reporter.is_not(format_args!( let dunder_iter_name = if mode.is_async() {
"Its `__iter__` method returns an object of type `{iterator_type}`, \ "__aiter__"
which has no `__next__` method", } 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 `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \
which has no `{dunder_next_name}` method",
iterator_type = iterator.display(db), iterator_type = iterator.display(db),
)); ));
} }
CallDunderError::PossiblyUnbound(_) => { CallDunderError::PossiblyUnbound(_) => {
reporter.may_not(format_args!( reporter.may_not(format_args!(
"Its `__iter__` method returns an object of type `{iterator_type}`, \ "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \
which may not have a `__next__` method", 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",
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",
iterator_type = iterator.display(db),
));
}
CallDunderError::CallError(CallErrorKind::BindingError, bindings)
if bindings.is_single() =>
{
reporter
.is_not(format_args!(
"Its `__iter__` method returns an object of type `{iterator_type}`, \
which has an invalid `__next__` method",
iterator_type = iterator.display(db), iterator_type = iterator.display(db),
)) ));
.info("Expected signature for `__next__` is `def __next__(self): ...`"); }
} CallDunderError::CallError(CallErrorKind::NotCallable, _) => {
CallDunderError::CallError(CallErrorKind::BindingError, _) => { reporter.is_not(format_args!(
reporter "Its `{dunder_iter_name}` method returns an object of type `{iterator_type}`, \
.may_not(format_args!( which has a `{dunder_next_name}` attribute that is not callable",
"Its `__iter__` method returns an object of type `{iterator_type}`, \
which may have an invalid `__next__` method",
iterator_type = iterator.display(db), iterator_type = iterator.display(db),
)) ));
.info("Expected signature for `__next__` is `def __next__(self): ...`)"); }
CallDunderError::CallError(CallErrorKind::PossiblyNotCallable, _) => {
reporter.may_not(format_args!(
"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),
));
}
CallDunderError::CallError(CallErrorKind::BindingError, bindings)
if bindings.is_single() =>
{
reporter
.is_not(format_args!(
"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(format_args!("Expected signature for `{dunder_next_name}` is `def {dunder_next_name}(self): ...`"));
}
CallDunderError::CallError(CallErrorKind::BindingError, _) => {
reporter
.may_not(format_args!(
"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(format_args!("Expected signature for `{dunder_next_name}` is `def {dunder_next_name}(self): ...`"));
}
} }
}, }
Self::PossiblyUnboundIterAndGetitemError { Self::PossiblyUnboundIterAndGetitemError {
dunder_getitem_error, 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, TypeAndQualifiers, TypeIsType, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance,
TypeVarKind, TypeVarVariance, UnionBuilder, UnionType, binding_type, todo_type, 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::diagnostics::format_enumeration;
use crate::util::subscript::{PyIndex, PySlice}; use crate::util::subscript::{PyIndex, PySlice};
use crate::{Db, FxOrderSet, Program}; use crate::{Db, FxOrderSet, Program};
@ -4560,29 +4560,28 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
let iterable = for_stmt.iterable(self.module()); let iterable = for_stmt.iterable(self.module());
let target = for_stmt.target(self.module()); let target = for_stmt.target(self.module());
let loop_var_value_type = if for_stmt.is_async() { let loop_var_value_type = match for_stmt.target_kind() {
let _iterable_type = self.infer_standalone_expression(iterable); TargetKind::Sequence(unpack_position, unpack) => {
todo_type!("async iterables/iterators") let unpacked = infer_unpack_types(self.db(), unpack);
} else { if unpack_position == UnpackPosition::First {
match for_stmt.target_kind() { self.context.extend(unpacked.diagnostics());
TargetKind::Sequence(unpack_position, unpack) => { }
let unpacked = infer_unpack_types(self.db(), unpack);
if unpack_position == UnpackPosition::First {
self.context.extend(unpacked.diagnostics());
}
unpacked.expression_type(target) unpacked.expression_type(target)
} }
TargetKind::Single => { TargetKind::Single => {
let iterable_type = self.infer_standalone_expression(iterable); let iterable_type = self.infer_standalone_expression(iterable);
iterable_type
.try_iterate(self.db()) iterable_type
.map(|tuple| tuple.homogeneous_element_type(self.db())) .try_iterate_with_mode(
.unwrap_or_else(|err| { self.db(),
err.report_diagnostic(&self.context, iterable_type, iterable.into()); EvaluationMode::from_is_async(for_stmt.is_async()),
err.fallback_element_type(self.db()) )
}) .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())
})
} }
}; };
@ -5692,30 +5691,28 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
} }
}; };
let target_type = if comprehension.is_async() { let target_type = match comprehension.target_kind() {
// TODO: async iterables/iterators! -- Alex TargetKind::Sequence(unpack_position, unpack) => {
let _iterable_type = infer_iterable_type(); let unpacked = infer_unpack_types(self.db(), unpack);
todo_type!("async iterables/iterators") if unpack_position == UnpackPosition::First {
} else { self.context.extend(unpacked.diagnostics());
match comprehension.target_kind() { }
TargetKind::Sequence(unpack_position, unpack) => {
let unpacked = infer_unpack_types(self.db(), unpack);
if unpack_position == UnpackPosition::First {
self.context.extend(unpacked.diagnostics());
}
unpacked.expression_type(target) unpacked.expression_type(target)
} }
TargetKind::Single => { TargetKind::Single => {
let iterable_type = infer_iterable_type(); let iterable_type = infer_iterable_type();
iterable_type
.try_iterate(self.db()) iterable_type
.map(|tuple| tuple.homogeneous_element_type(self.db())) .try_iterate_with_mode(
.unwrap_or_else(|err| { self.db(),
err.report_diagnostic(&self.context, iterable_type, iterable.into()); EvaluationMode::from_is_async(comprehension.is_async()),
err.fallback_element_type(self.db()) )
}) .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())
})
} }
}; };

View file

@ -64,8 +64,8 @@ impl<'db, 'ast> Unpacker<'db, 'ast> {
value_type value_type
} }
} }
UnpackKind::Iterable => value_type UnpackKind::Iterable { mode } => value_type
.try_iterate(self.db()) .try_iterate_with_mode(self.db(), mode)
.map(|tuple| tuple.homogeneous_element_type(self.db())) .map(|tuple| tuple.homogeneous_element_type(self.db()))
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
err.report_diagnostic( err.report_diagnostic(
@ -75,8 +75,8 @@ impl<'db, 'ast> Unpacker<'db, 'ast> {
); );
err.fallback_element_type(self.db()) err.fallback_element_type(self.db())
}), }),
UnpackKind::ContextManager { is_async } => { UnpackKind::ContextManager { mode } => {
if is_async { if mode.is_async() {
value_type.aenter(self.db()) value_type.aenter(self.db())
} else { } else {
value_type.try_enter(self.db()).unwrap_or_else(|err| { 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)] #[derive(Clone, Copy, Debug, Hash, salsa::Update)]
pub(crate) enum UnpackKind { pub(crate) enum UnpackKind {
/// An iterable expression like the one in a `for` loop or a comprehension. /// 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. /// 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. /// An expression that is being assigned to a target.
Assign, Assign,
} }