From 87e670560d2325488ca2466acf7a883325694c73 Mon Sep 17 00:00:00 2001 From: pytype authors Date: Fri, 30 May 2025 21:34:58 -0700 Subject: [PATCH] Add a pytype error unused-coroutine. PiperOrigin-RevId: 765456902 --- pytype/errors/errors.py | 6 ++++ pytype/tests/test_coroutine.py | 65 +++++++++++++++++++++++++++++++--- pytype/tests/test_stdlib2.py | 7 ++-- pytype/vm.py | 9 +++++ 4 files changed, 81 insertions(+), 6 deletions(-) diff --git a/pytype/errors/errors.py b/pytype/errors/errors.py index 986f955a5..62ffae534 100644 --- a/pytype/errors/errors.py +++ b/pytype/errors/errors.py @@ -1365,6 +1365,12 @@ def typed_dict_error(self, stack, obj, name): ) self.error(stack, err_msg) + @_error_name("unused-coroutine") + def unused_coroutine(self, stack): + self.error( + stack, "Coroutine result was not used. Did you forget to await it?" + ) + @_error_name("final-error") def _overriding_final(self, stack, cls, base, name, *, is_method, details): desc = "method" if is_method else "class attribute" diff --git a/pytype/tests/test_coroutine.py b/pytype/tests/test_coroutine.py index 4143e7279..81fbb83f9 100644 --- a/pytype/tests/test_coroutine.py +++ b/pytype/tests/test_coroutine.py @@ -500,9 +500,9 @@ async def func2(x: Coroutine[Any, Any, str]): var.func() return res - func1(foo.f1()) - func1(foo.f2()) - func2(foo.f1()) + coro1 = func1(foo.f1()) + coro2 = func1(foo.f2()) + coro3 = func2(foo.f1()) """, pythonpath=[d.path], ) @@ -512,6 +512,10 @@ async def func2(x: Coroutine[Any, Any, str]): import foo from typing import Any, Awaitable, Coroutine, List + coro1: Coroutine[Any, Any, list[str]] + coro2: Coroutine[Any, Any, list[str]] + coro3: Coroutine[Any, Any, list[str]] + def func1(x: Awaitable[str]) -> Coroutine[Any, Any, List[str]]: ... def func2(x: Coroutine[Any, Any, str]) -> Coroutine[Any, Any, List[str]]: ... """, @@ -570,7 +574,7 @@ async def worker(queue): async def main(): queue = asyncio.Queue() - worker(queue) + await worker(queue) """) self.assertTypesMatchPytd( ty, @@ -646,5 +650,58 @@ async def call_with_retry( """) +class UnusedCoroutineTest(test_base.BaseTest): + """Tests for the unused-coroutine error.""" + + def test_trigger_unused_coroutine(self): + self.CheckWithErrors(""" + async def sample_coro(): + return 1 + sample_coro() # unused-coroutine + """) + + def test_correct_coroutine_usage(self): + self.Check(""" + import asyncio + + async def sample_coro(): + return 1 + + async def main(): + await sample_coro() + x = sample_coro() + asyncio.create_task(sample_coro()) + my_list = [sample_coro()] + my_dict = {"a": sample_coro()} + return x, my_list, my_dict + + def regular_func(): return 1 + regular_func() + """) + + def test_wrapper_returns_unused_coroutine(self): + self.CheckWithErrors(""" + async def sample_coro(): + return 1 + def wrapper(): + return sample_coro() + wrapper() # unused-coroutine + """) + + def test_assign_to_underscore(self): + self.Check(""" + async def sample_coro(): + return 1 + _ = sample_coro() + """) + + def test_coroutine_in_used_expression(self): + self.CheckWithErrors(""" + async def sample_coro(): + return 1 + y = 1 + sample_coro() # unsupported-operands + """) + + if __name__ == "__main__": test_base.main() diff --git a/pytype/tests/test_stdlib2.py b/pytype/tests/test_stdlib2.py index eb1794103..5281ca2f2 100644 --- a/pytype/tests/test_stdlib2.py +++ b/pytype/tests/test_stdlib2.py @@ -144,7 +144,7 @@ def test_collections_smoke_test(self): collections.AsyncIterator collections.AsyncGenerator collections.Awaitable - collections.Coroutine + collections.Coroutine # pytype: disable=unused-coroutine """) def test_collections_bytestring(self): @@ -453,7 +453,7 @@ async def iterate(x): pass else: pass - iterate(AsyncIterable()) + iterate_coro = iterate(AsyncIterable()) """) self.assertTypesMatchPytd( ty, @@ -461,6 +461,9 @@ async def iterate(x): import asyncio from typing import Any, Coroutine, TypeVar _TAsyncIterable = TypeVar('_TAsyncIterable', bound=AsyncIterable) + + iterate_coro: Coroutine[Any, Any, None] + class AsyncIterable: def __aiter__(self: _TAsyncIterable) -> _TAsyncIterable: ... def __anext__(self) -> Coroutine[Any, Any, int]: ... diff --git a/pytype/vm.py b/pytype/vm.py index 3510505ea..2ea560d8d 100644 --- a/pytype/vm.py +++ b/pytype/vm.py @@ -1638,6 +1638,15 @@ def byte_SETUP_EXCEPT_311(self, state, op): return self._setup_except(state, op) def byte_POP_TOP(self, state, op): + """Pops and discards, checking for an unused coroutine.""" + tos = None + try: + tos = state.top() + except IndexError: + pass + if tos is not None: + if any(b.data.full_name == "typing.Coroutine" for b in tos.bindings): + self.ctx.errorlog.unused_coroutine(self.frames) return state.pop_and_discard() def byte_DUP_TOP(self, state, op):