mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34:57 +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
|
@ -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()]
|
||||
```
|
||||
|
|
|
@ -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)]
|
||||
```
|
||||
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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`
|
||||
|
|
||||
|
||||
```
|
|
@ -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`
|
||||
|
|
||||
|
||||
```
|
|
@ -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`
|
||||
|
|
||||
|
||||
```
|
|
@ -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`
|
||||
|
|
||||
|
||||
```
|
|
@ -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`
|
||||
|
|
||||
|
||||
```
|
|
@ -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`
|
||||
|
|
||||
|
||||
```
|
|
@ -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`
|
||||
|
|
||||
|
||||
```
|
|
@ -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
|
||||
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
||||
```
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,106 +7047,132 @@ 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) => {
|
||||
reporter.is_not(format_args!(
|
||||
"Its `__iter__` 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() =>
|
||||
{
|
||||
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::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() => {
|
||||
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");
|
||||
diag.info(format_args!(
|
||||
"Type of `__iter__` is `{dunder_iter_type}`",
|
||||
dunder_iter_type = bindings.callable_type().display(db),
|
||||
));
|
||||
diag.info("Expected signature for `__iter__` is `def __iter__(self): ...`");
|
||||
Self::IterCallError {
|
||||
kind,
|
||||
bindings,
|
||||
mode,
|
||||
} => {
|
||||
let method = if mode.is_async() {
|
||||
"__aiter__"
|
||||
} else {
|
||||
"__iter__"
|
||||
};
|
||||
|
||||
match kind {
|
||||
CallErrorKind::NotCallable => {
|
||||
reporter.is_not(format_args!(
|
||||
"Its `{method}` attribute has type `{dunder_iter_type}`, which is not callable",
|
||||
dunder_iter_type = bindings.callable_type().display(db),
|
||||
));
|
||||
}
|
||||
CallErrorKind::PossiblyNotCallable => {
|
||||
reporter.may_not(format_args!(
|
||||
"Its `{method}` attribute (with type `{dunder_iter_type}`) \
|
||||
may not be callable",
|
||||
dunder_iter_type = bindings.callable_type().display(db),
|
||||
));
|
||||
}
|
||||
CallErrorKind::BindingError => {
|
||||
if bindings.is_single() {
|
||||
reporter
|
||||
.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 `{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 {
|
||||
iterator,
|
||||
dunder_next_error,
|
||||
} => 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",
|
||||
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 `{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",
|
||||
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",
|
||||
}
|
||||
CallDunderError::PossiblyUnbound(_) => {
|
||||
reporter.may_not(format_args!(
|
||||
"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),
|
||||
))
|
||||
.info("Expected signature for `__next__` is `def __next__(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",
|
||||
));
|
||||
}
|
||||
CallDunderError::CallError(CallErrorKind::NotCallable, _) => {
|
||||
reporter.is_not(format_args!(
|
||||
"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),
|
||||
))
|
||||
.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 {
|
||||
dunder_getitem_error,
|
||||
|
@ -7167,6 +7309,10 @@ impl<'db> IterationError<'db> {
|
|||
);
|
||||
}
|
||||
},
|
||||
|
||||
IterationError::UnboundAiterError => {
|
||||
reporter.is_not("It has no `__aiter__` method");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,29 +4560,28 @@ 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() {
|
||||
TargetKind::Sequence(unpack_position, unpack) => {
|
||||
let unpacked = infer_unpack_types(self.db(), unpack);
|
||||
if unpack_position == UnpackPosition::First {
|
||||
self.context.extend(unpacked.diagnostics());
|
||||
}
|
||||
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 {
|
||||
self.context.extend(unpacked.diagnostics());
|
||||
}
|
||||
|
||||
unpacked.expression_type(target)
|
||||
}
|
||||
TargetKind::Single => {
|
||||
let iterable_type = self.infer_standalone_expression(iterable);
|
||||
iterable_type
|
||||
.try_iterate(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())
|
||||
})
|
||||
}
|
||||
unpacked.expression_type(target)
|
||||
}
|
||||
TargetKind::Single => {
|
||||
let iterable_type = self.infer_standalone_expression(iterable);
|
||||
|
||||
iterable_type
|
||||
.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())
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -5692,30 +5691,28 @@ 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() {
|
||||
TargetKind::Sequence(unpack_position, unpack) => {
|
||||
let unpacked = infer_unpack_types(self.db(), unpack);
|
||||
if unpack_position == UnpackPosition::First {
|
||||
self.context.extend(unpacked.diagnostics());
|
||||
}
|
||||
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 {
|
||||
self.context.extend(unpacked.diagnostics());
|
||||
}
|
||||
|
||||
unpacked.expression_type(target)
|
||||
}
|
||||
TargetKind::Single => {
|
||||
let iterable_type = infer_iterable_type();
|
||||
iterable_type
|
||||
.try_iterate(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())
|
||||
})
|
||||
}
|
||||
unpacked.expression_type(target)
|
||||
}
|
||||
TargetKind::Single => {
|
||||
let iterable_type = infer_iterable_type();
|
||||
|
||||
iterable_type
|
||||
.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())
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue