ruff/crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_await.md
Andrii Turov 957320c0f1
Some checks are pending
CI / cargo clippy (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
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 / 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 / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
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
[ty] Add diagnostics for invalid await expressions (#19711)
## 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>
2025-08-14 14:38:33 -07:00

2.5 KiB

Invalid await diagnostics

Basic

This is a test showcasing a primitive case where an object is not awaitable.

async def main() -> None:
    await 1  # error: [invalid-await]

Custom type with missing __await__

This diagnostic also points to the class definition if available.

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.

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.

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.

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.

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.

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]