mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 05:44:56 +00:00
[ty] Add diagnostics for invalid await
expressions (#19711)
Some checks are pending
CI / mkdocs (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Some checks are pending
CI / mkdocs (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
## Summary This PR adds a new lint, `invalid-await`, for all sorts of reasons why an object may not be `await`able, as discussed in astral-sh/ty#919. Precisely, `__await__` is guarded against being missing, possibly unbound, or improperly defined (expects additional arguments or doesn't return an iterator). Of course, diagnostics need to be fine-tuned. If `__await__` cannot be called with no extra arguments, it indicates an error (or a quirk?) in the method signature, not at the call site. Without any doubt, such an object is not `Awaitable`, but I feel like talking about arguments for an *implicit* call is a bit leaky. I didn't reference any actual diagnostic messages in the lint definition, because I want to hear feedback first. Also, there's no mention of the actual required method signature for `__await__` anywhere in the docs. The only reference I had is the `typing` stub. I basically ended up linking `[Awaitable]` to ["must implement `__await__`"](https://docs.python.org/3/library/collections.abc.html#collections.abc.Awaitable), which is insufficient on its own. ## Test Plan The following code was tested: ```python import asyncio import typing class Awaitable: def __await__(self) -> typing.Generator[typing.Any, None, int]: yield None return 5 class NoDunderMethod: pass class InvalidAwaitArgs: def __await__(self, value: int) -> int: return value class InvalidAwaitReturn: def __await__(self) -> int: return 5 class InvalidAwaitReturnImplicit: def __await__(self): pass async def main() -> None: result = await Awaitable() # valid result = await NoDunderMethod() # `__await__` is missing result = await InvalidAwaitReturn() # `__await__` returns `int`, which is not a valid iterator result = await InvalidAwaitArgs() # `__await__` expects additional arguments and cannot be called implicitly result = await InvalidAwaitReturnImplicit() # `__await__` returns `Unknown`, which is not a valid iterator asyncio.run(main()) ``` --------- Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
f6093452ed
commit
957320c0f1
14 changed files with 661 additions and 84 deletions
|
@ -0,0 +1,105 @@
|
|||
# Invalid await diagnostics
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
## Basic
|
||||
|
||||
This is a test showcasing a primitive case where an object is not awaitable.
|
||||
|
||||
```py
|
||||
async def main() -> None:
|
||||
await 1 # error: [invalid-await]
|
||||
```
|
||||
|
||||
## Custom type with missing `__await__`
|
||||
|
||||
This diagnostic also points to the class definition if available.
|
||||
|
||||
```py
|
||||
class MissingAwait:
|
||||
pass
|
||||
|
||||
async def main() -> None:
|
||||
await MissingAwait() # error: [invalid-await]
|
||||
```
|
||||
|
||||
## Custom type with possibly unbound `__await__`
|
||||
|
||||
This diagnostic also points to the method definition if available.
|
||||
|
||||
```py
|
||||
from datetime import datetime
|
||||
|
||||
class PossiblyUnbound:
|
||||
if datetime.today().weekday() == 0:
|
||||
def __await__(self):
|
||||
yield
|
||||
|
||||
async def main() -> None:
|
||||
await PossiblyUnbound() # error: [invalid-await]
|
||||
```
|
||||
|
||||
## `__await__` definition with extra arguments
|
||||
|
||||
Currently, the signature of `__await__` isn't checked for conformity with the `Awaitable` protocol
|
||||
directly. Instead, individual anomalies are reported, such as the following. Here, the diagnostic
|
||||
reports that the object is not implicitly awaitable, while also pointing at the function parameters.
|
||||
|
||||
```py
|
||||
class InvalidAwaitArgs:
|
||||
def __await__(self, value: int):
|
||||
yield value
|
||||
|
||||
async def main() -> None:
|
||||
await InvalidAwaitArgs() # error: [invalid-await]
|
||||
```
|
||||
|
||||
## Non-callable `__await__`
|
||||
|
||||
This diagnostic doesn't point to the attribute definition, but complains about it being possibly not
|
||||
awaitable.
|
||||
|
||||
```py
|
||||
class NonCallableAwait:
|
||||
__await__ = 42
|
||||
|
||||
async def main() -> None:
|
||||
await NonCallableAwait() # error: [invalid-await]
|
||||
```
|
||||
|
||||
## `__await__` definition with explicit invalid return type
|
||||
|
||||
`__await__` must return a valid iterator. This diagnostic also points to the method definition if
|
||||
available.
|
||||
|
||||
```py
|
||||
class InvalidAwaitReturn:
|
||||
def __await__(self) -> int:
|
||||
return 5
|
||||
|
||||
async def main() -> None:
|
||||
await InvalidAwaitReturn() # error: [invalid-await]
|
||||
```
|
||||
|
||||
## Invalid union return type
|
||||
|
||||
When multiple potential definitions of `__await__` exist, all of them must be proper in order for an
|
||||
instance to be awaitable. In this specific case, no specific function definition is highlighted.
|
||||
|
||||
```py
|
||||
import typing
|
||||
from datetime import datetime
|
||||
|
||||
class UnawaitableUnion:
|
||||
if datetime.today().weekday() == 6:
|
||||
|
||||
def __await__(self) -> typing.Generator[typing.Any, None, None]:
|
||||
yield
|
||||
else:
|
||||
|
||||
def __await__(self) -> int:
|
||||
return 5
|
||||
|
||||
async def main() -> None:
|
||||
await UnawaitableUnion() # error: [invalid-await]
|
||||
```
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_await.md - Invalid await diagnostics - Basic
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | async def main() -> None:
|
||||
2 | await 1 # error: [invalid-await]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-await]: `Literal[1]` is not awaitable
|
||||
--> src/mdtest_snippet.py:2:11
|
||||
|
|
||||
1 | async def main() -> None:
|
||||
2 | await 1 # error: [invalid-await]
|
||||
| ^
|
||||
|
|
||||
::: stdlib/builtins.pyi:337:7
|
||||
|
|
||||
335 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed
|
||||
336 |
|
||||
337 | class int:
|
||||
| --- type defined here
|
||||
338 | """int([x]) -> integer
|
||||
339 | int(x, base=10) -> integer
|
||||
|
|
||||
info: `__await__` is missing
|
||||
info: rule `invalid-await` is enabled by default
|
||||
|
||||
```
|
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_await.md - Invalid await diagnostics - Custom type with missing `__await__`
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class MissingAwait:
|
||||
2 | pass
|
||||
3 |
|
||||
4 | async def main() -> None:
|
||||
5 | await MissingAwait() # error: [invalid-await]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-await]: `MissingAwait` is not awaitable
|
||||
--> src/mdtest_snippet.py:5:11
|
||||
|
|
||||
4 | async def main() -> None:
|
||||
5 | await MissingAwait() # error: [invalid-await]
|
||||
| ^^^^^^^^^^^^^^
|
||||
|
|
||||
::: src/mdtest_snippet.py:1:7
|
||||
|
|
||||
1 | class MissingAwait:
|
||||
| ------------ type defined here
|
||||
2 | pass
|
||||
|
|
||||
info: `__await__` is missing
|
||||
info: rule `invalid-await` is enabled by default
|
||||
|
||||
```
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_await.md - Invalid await diagnostics - Custom type with possibly unbound `__await__`
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from datetime import datetime
|
||||
2 |
|
||||
3 | class PossiblyUnbound:
|
||||
4 | if datetime.today().weekday() == 0:
|
||||
5 | def __await__(self):
|
||||
6 | yield
|
||||
7 |
|
||||
8 | async def main() -> None:
|
||||
9 | await PossiblyUnbound() # error: [invalid-await]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-await]: `PossiblyUnbound` is not awaitable
|
||||
--> src/mdtest_snippet.py:9:11
|
||||
|
|
||||
8 | async def main() -> None:
|
||||
9 | await PossiblyUnbound() # error: [invalid-await]
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
::: src/mdtest_snippet.py:5:13
|
||||
|
|
||||
3 | class PossiblyUnbound:
|
||||
4 | if datetime.today().weekday() == 0:
|
||||
5 | def __await__(self):
|
||||
| --------------- method defined here
|
||||
6 | yield
|
||||
|
|
||||
info: `__await__` is possibly unbound
|
||||
info: rule `invalid-await` is enabled by default
|
||||
|
||||
```
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_await.md - Invalid await diagnostics - Invalid union return type
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | import typing
|
||||
2 | from datetime import datetime
|
||||
3 |
|
||||
4 | class UnawaitableUnion:
|
||||
5 | if datetime.today().weekday() == 6:
|
||||
6 |
|
||||
7 | def __await__(self) -> typing.Generator[typing.Any, None, None]:
|
||||
8 | yield
|
||||
9 | else:
|
||||
10 |
|
||||
11 | def __await__(self) -> int:
|
||||
12 | return 5
|
||||
13 |
|
||||
14 | async def main() -> None:
|
||||
15 | await UnawaitableUnion() # error: [invalid-await]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-await]: `UnawaitableUnion` is not awaitable
|
||||
--> src/mdtest_snippet.py:15:11
|
||||
|
|
||||
14 | async def main() -> None:
|
||||
15 | await UnawaitableUnion() # error: [invalid-await]
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
info: `__await__` returns `Generator[Any, None, None] | int`, which is not a valid iterator
|
||||
info: rule `invalid-await` is enabled by default
|
||||
|
||||
```
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_await.md - Invalid await diagnostics - Non-callable `__await__`
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class NonCallableAwait:
|
||||
2 | __await__ = 42
|
||||
3 |
|
||||
4 | async def main() -> None:
|
||||
5 | await NonCallableAwait() # error: [invalid-await]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-await]: `NonCallableAwait` is not awaitable
|
||||
--> src/mdtest_snippet.py:5:11
|
||||
|
|
||||
4 | async def main() -> None:
|
||||
5 | await NonCallableAwait() # error: [invalid-await]
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
info: `__await__` is possibly not callable
|
||||
info: rule `invalid-await` is enabled by default
|
||||
|
||||
```
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_await.md - Invalid await diagnostics - `__await__` definition with extra arguments
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class InvalidAwaitArgs:
|
||||
2 | def __await__(self, value: int):
|
||||
3 | yield value
|
||||
4 |
|
||||
5 | async def main() -> None:
|
||||
6 | await InvalidAwaitArgs() # error: [invalid-await]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-await]: `InvalidAwaitArgs` is not awaitable
|
||||
--> src/mdtest_snippet.py:6:11
|
||||
|
|
||||
5 | async def main() -> None:
|
||||
6 | await InvalidAwaitArgs() # error: [invalid-await]
|
||||
| ^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
::: src/mdtest_snippet.py:2:18
|
||||
|
|
||||
1 | class InvalidAwaitArgs:
|
||||
2 | def __await__(self, value: int):
|
||||
| ------------------ parameters here
|
||||
3 | yield value
|
||||
|
|
||||
info: `__await__` requires arguments and cannot be called implicitly
|
||||
info: rule `invalid-await` is enabled by default
|
||||
|
||||
```
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: invalid_await.md - Invalid await diagnostics - `__await__` definition with explicit invalid return type
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | class InvalidAwaitReturn:
|
||||
2 | def __await__(self) -> int:
|
||||
3 | return 5
|
||||
4 |
|
||||
5 | async def main() -> None:
|
||||
6 | await InvalidAwaitReturn() # error: [invalid-await]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-await]: `InvalidAwaitReturn` is not awaitable
|
||||
--> src/mdtest_snippet.py:6:11
|
||||
|
|
||||
5 | async def main() -> None:
|
||||
6 | await InvalidAwaitReturn() # error: [invalid-await]
|
||||
| ^^^^^^^^^^^^^^^^^^^^
|
||||
|
|
||||
::: src/mdtest_snippet.py:2:9
|
||||
|
|
||||
1 | class InvalidAwaitReturn:
|
||||
2 | def __await__(self) -> int:
|
||||
| ---------------------- method defined here
|
||||
3 | return 5
|
||||
|
|
||||
info: `__await__` returns `int`, which is not a valid iterator
|
||||
info: rule `invalid-await` is enabled by default
|
||||
|
||||
```
|
Loading…
Add table
Add a link
Reference in a new issue