## 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>
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]