Skip to content

Commit 536bac0

Browse files
authored
Error on unused awaitable expressions (#12279)
Generates an error when an expression has a type which has a defined __await__ in its MRO but is not used. This includes all builtin awaitable objects (e.g. futures, coroutines, and tasks) but also anything a user might define which is also awaitable. A hint is attached to suggest awaiting. This can be extended in the future to other types of values that may lead to a resource leak if needed, or even exposed as a plugin. We test simple and complex cases (coroutines and user defined classes). We also test that __getattr__ does not create false positives for awaitables. Some tests require fixes, either because they were deliberately not awaiting an awaitable to verify some other logic in mypy, or because reveal_type returns the object, so it was generating an error we would rather simply silence,
1 parent 3a5b2a9 commit 536bac0

File tree

6 files changed

+66
-5
lines changed

6 files changed

+66
-5
lines changed

mypy/checker.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
from mypy.scope import Scope
8686
from mypy import state, errorcodes as codes
8787
from mypy.traverser import has_return_statement, all_return_statements
88-
from mypy.errorcodes import ErrorCode
88+
from mypy.errorcodes import ErrorCode, UNUSED_AWAITABLE, UNUSED_COROUTINE
8989
from mypy.util import is_typeshed_file, is_dunder, is_sunder
9090

9191
T = TypeVar('T')
@@ -3432,8 +3432,32 @@ def try_infer_partial_type_from_indexed_assignment(
34323432
[key_type, value_type])
34333433
del partial_types[var]
34343434

3435+
def type_requires_usage(self, typ: Type) -> Optional[Tuple[str, ErrorCode]]:
3436+
"""Some types require usage in all cases. The classic example is
3437+
an unused coroutine.
3438+
3439+
In the case that it does require usage, returns a note to attach
3440+
to the error message.
3441+
"""
3442+
proper_type = get_proper_type(typ)
3443+
if isinstance(proper_type, Instance):
3444+
# We use different error codes for generic awaitable vs coroutine.
3445+
# Coroutines are on by default, whereas generic awaitables are not.
3446+
if proper_type.type.fullname == "typing.Coroutine":
3447+
return ("Are you missing an await?", UNUSED_COROUTINE)
3448+
if proper_type.type.get("__await__") is not None:
3449+
return ("Are you missing an await?", UNUSED_AWAITABLE)
3450+
return None
3451+
34353452
def visit_expression_stmt(self, s: ExpressionStmt) -> None:
3436-
self.expr_checker.accept(s.expr, allow_none_return=True, always_allow_any=True)
3453+
expr_type = self.expr_checker.accept(s.expr, allow_none_return=True, always_allow_any=True)
3454+
error_note_and_code = self.type_requires_usage(expr_type)
3455+
if error_note_and_code:
3456+
error_note, code = error_note_and_code
3457+
self.fail(
3458+
message_registry.TYPE_MUST_BE_USED.format(format_type(expr_type)), s, code=code
3459+
)
3460+
self.note(error_note, s, code=code)
34373461

34383462
def visit_return_stmt(self, s: ReturnStmt) -> None:
34393463
"""Type check a return statement."""

mypy/errorcodes.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ def __str__(self) -> str:
9797
LITERAL_REQ: Final = ErrorCode(
9898
"literal-required", "Check that value is a literal", 'General'
9999
)
100+
UNUSED_COROUTINE: Final = ErrorCode(
101+
"unused-coroutine", "Ensure that all coroutines are used", "General"
102+
)
100103

101104
# These error codes aren't enabled by default.
102105
NO_UNTYPED_DEF: Final[ErrorCode] = ErrorCode(
@@ -147,6 +150,12 @@ def __str__(self) -> str:
147150
"General",
148151
default_enabled=False,
149152
)
153+
UNUSED_AWAITABLE: Final = ErrorCode(
154+
"unused-awaitable",
155+
"Ensure that all awaitable values are used",
156+
"General",
157+
default_enabled=False,
158+
)
150159

151160

152161
# Syntax errors are often blocking.

mypy/message_registry.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ def format(self, *args: object, **kwargs: object) -> "ErrorMessage":
141141
PYTHON2_PRINT_FILE_TYPE: Final = (
142142
'Argument "file" to "print" has incompatible type "{}"; expected "{}"'
143143
)
144+
TYPE_MUST_BE_USED: Final = 'Value of type {} must be used'
144145

145146
# Generic
146147
GENERIC_INSTANCE_VAR_CLASS_ACCESS: Final = (

test-data/unit/check-async-await.test

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ async def f() -> int:
1212

1313
async def f() -> int:
1414
return 0
15-
reveal_type(f()) # N: Revealed type is "typing.Coroutine[Any, Any, builtins.int]"
15+
_ = reveal_type(f()) # N: Revealed type is "typing.Coroutine[Any, Any, builtins.int]"
1616
[builtins fixtures/async_await.pyi]
1717
[typing fixtures/typing-async.pyi]
1818

@@ -799,3 +799,29 @@ async def precise2(futures: Iterable[Awaitable[int]]) -> None:
799799

800800
[builtins fixtures/async_await.pyi]
801801
[typing fixtures/typing-async.pyi]
802+
803+
[case testUnusedAwaitable]
804+
# flags: --show-error-codes --enable-error-code unused-awaitable
805+
from typing import Iterable
806+
807+
async def foo() -> None:
808+
pass
809+
810+
class A:
811+
def __await__(self) -> Iterable[int]:
812+
yield 5
813+
814+
# Things with __getattr__ should not simply be considered awaitable.
815+
class B:
816+
def __getattr__(self, attr) -> object:
817+
return 0
818+
819+
def bar() -> None:
820+
A() # E: Value of type "A" must be used [unused-awaitable] \
821+
# N: Are you missing an await?
822+
foo() # E: Value of type "Coroutine[Any, Any, None]" must be used [unused-coroutine] \
823+
# N: Are you missing an await?
824+
B()
825+
826+
[builtins fixtures/async_await.pyi]
827+
[typing fixtures/typing-async.pyi]

test-data/unit/check-class-namedtuple.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ class XRepr(NamedTuple):
538538
return 0
539539

540540
reveal_type(XMeth(1).double()) # N: Revealed type is "builtins.int"
541-
reveal_type(XMeth(1).asyncdouble()) # N: Revealed type is "typing.Coroutine[Any, Any, builtins.int]"
541+
_ = reveal_type(XMeth(1).asyncdouble()) # N: Revealed type is "typing.Coroutine[Any, Any, builtins.int]"
542542
reveal_type(XMeth(42).x) # N: Revealed type is "builtins.int"
543543
reveal_type(XRepr(42).__str__()) # N: Revealed type is "builtins.str"
544544
reveal_type(XRepr(1, 2).__sub__(XRepr(3))) # N: Revealed type is "builtins.int"

test-data/unit/check-flags.test

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,8 @@ async def g() -> NoReturn:
405405
await f()
406406

407407
async def h() -> NoReturn: # E: Implicit return in function which does not return
408-
f()
408+
# Purposely not evaluating coroutine
409+
_ = f()
409410
[builtins fixtures/dict.pyi]
410411
[typing fixtures/typing-async.pyi]
411412

0 commit comments

Comments
 (0)