diff --git a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md index a32244c09a..ea7ed41cbf 100644 --- a/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md +++ b/crates/ty_python_semantic/resources/mdtest/comprehensions/basic.md @@ -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()] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md index bdd12d0b0a..84e4f362bb 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/semantic_syntax_errors.md @@ -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)] ``` diff --git a/crates/ty_python_semantic/resources/mdtest/loops/async_for.md b/crates/ty_python_semantic/resources/mdtest/loops/async_for.md index c073576876..25eee5c071 100644 --- a/crates/ty_python_semantic/resources/mdtest/loops/async_for.md +++ b/crates/ty_python_semantic/resources/mdtest/loops/async_for.md @@ -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 + + + +### 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 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_No_`__aiter__`_metho…_(4fbd80e21774cc23).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_No_`__aiter__`_metho…_(4fbd80e21774cc23).snap new file mode 100644 index 0000000000..0953c5286e --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_No_`__aiter__`_metho…_(4fbd80e21774cc23).snap @@ -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` + | + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_No_`__anext__`_metho…_(a0b186714127abee).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_No_`__anext__`_metho…_(a0b186714127abee).snap new file mode 100644 index 0000000000..6994519044 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_No_`__anext__`_metho…_(a0b186714127abee).snap @@ -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` + | + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_unbound_`__…_(42b1d61a2b7be1b5).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_unbound_`__…_(42b1d61a2b7be1b5).snap new file mode 100644 index 0000000000..c41a49507e --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_unbound_`__…_(42b1d61a2b7be1b5).snap @@ -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` + | + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_unbound_`__…_(74ad2f945cad6ed8).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_unbound_`__…_(74ad2f945cad6ed8).snap new file mode 100644 index 0000000000..4060670bd9 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Possibly_unbound_`__…_(74ad2f945cad6ed8).snap @@ -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` + | + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Synchronously_iterab…_(80fa705b1c61d982).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Synchronously_iterab…_(80fa705b1c61d982).snap new file mode 100644 index 0000000000..29e0d262df --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Synchronously_iterab…_(80fa705b1c61d982).snap @@ -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` + | + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Wrong_signature_for_…_(b614724363eec343).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Wrong_signature_for_…_(b614724363eec343).snap new file mode 100644 index 0000000000..33e475d546 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Wrong_signature_for_…_(b614724363eec343).snap @@ -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` + | + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Wrong_signature_for_…_(e1f3e9275d0a367).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Wrong_signature_for_…_(e1f3e9275d0a367).snap new file mode 100644 index 0000000000..3373f0d4ae --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/async_for.md_-_Async_-_Error_cases_-_Wrong_signature_for_…_(e1f3e9275d0a367).snap @@ -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` + | + +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__…_(c626bde8651b643a).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__…_(c626bde8651b643a).snap index 91c7d40dbd..ddeda6f7f9 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__…_(c626bde8651b643a).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/for.md_-_For_loops_-_Possibly_invalid_`__…_(c626bde8651b643a).snap @@ -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 ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/semantic_syntax_erro…_-_Semantic_syntax_erro…_-_`async`_comprehensio…_-_Python_3.10_(96aa8ec77d46553d).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/semantic_syntax_erro…_-_Semantic_syntax_erro…_-_`async`_comprehensio…_-_Python_3.10_(96aa8ec77d46553d).snap index 6fd460c56d..f25e7b1bac 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/semantic_syntax_erro…_-_Semantic_syntax_erro…_-_`async`_comprehensio…_-_Python_3.10_(96aa8ec77d46553d).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/semantic_syntax_erro…_-_Semantic_syntax_erro…_-_`async`_comprehensio…_-_Python_3.10_(96aa8ec77d46553d).snap @@ -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 + +``` diff --git a/crates/ty_python_semantic/src/semantic_index/builder.rs b/crates/ty_python_semantic/src/semantic_index/builder.rs index 5d623755ab..a18d3a21fb 100644 --- a/crates/ty_python_semantic/src/semantic_index/builder.rs +++ b/crates/ty_python_semantic/src/semantic_index/builder.rs @@ -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), }, } } diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 2d7dfa9b9d..a6f76a5550 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -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>, IterationError<'db>> { + self.try_iterate_with_mode(db, EvaluationMode::Sync) + } + + fn try_iterate_with_mode( + self, + db: &'db dyn Db, + mode: EvaluationMode, + ) -> Result>, 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> { // 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>), + IterCallError { + kind: CallErrorKind, + bindings: Box>, + 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> { + let return_type = |result: Result, 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"); + } } } } diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index facb2762e5..8c395cb31a 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -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()) + }) } }; diff --git a/crates/ty_python_semantic/src/types/unpacker.rs b/crates/ty_python_semantic/src/types/unpacker.rs index 92f3d7cbc4..f1ebc42967 100644 --- a/crates/ty_python_semantic/src/types/unpacker.rs +++ b/crates/ty_python_semantic/src/types/unpacker.rs @@ -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| { diff --git a/crates/ty_python_semantic/src/unpack.rs b/crates/ty_python_semantic/src/unpack.rs index 19d9361165..917ff71b4e 100644 --- a/crates/ty_python_semantic/src/unpack.rs +++ b/crates/ty_python_semantic/src/unpack.rs @@ -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, }