diff --git a/mypy/checker.py b/mypy/checker.py index c1c31538b7de..83675fc294f6 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4194,7 +4194,7 @@ def type_requires_usage(self, typ: Type) -> tuple[str, ErrorCode] | None: # Coroutines are on by default, whereas generic awaitables are not. if proper_type.type.fullname == "typing.Coroutine": return ("Are you missing an await?", UNUSED_COROUTINE) - if proper_type.type.get("__await__") is not None: + if proper_type.is_awaitable: return ("Are you missing an await?", UNUSED_AWAITABLE) return None diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 4e038cfd75d1..a144ff4c4460 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -982,14 +982,34 @@ def verify_funcitem( # That results in false positives. # See https://github.com/python/typeshed/issues/7344 if runtime_is_coroutine and not stub.is_coroutine: - yield Error( - object_path, - 'is an "async def" function at runtime, but not in the stub', - stub, - runtime, - stub_desc=stub_desc, - runtime_desc=runtime_sig_desc, - ) + if runtime_is_abstract(runtime) or ( + isinstance(stub, nodes.FuncDef) and stub_is_abstract(stub) + ): + error_msg = ( + 'is an "async def" function at runtime, ' + "but doesn't return an awaitable in the stub" + ) + # Be more permissive if the method is an abstractmethod: + # Only error if the return type of the stub isn't awaitable + if isinstance(stub.type, mypy.types.CallableType): + ret_type = mypy.types.get_proper_type(stub.type.ret_type) + should_error = ( + isinstance(ret_type, mypy.types.Instance) and not ret_type.is_awaitable + ) + else: + should_error = False + else: + error_msg = 'is an "async def" function at runtime, but not in the stub' + should_error = True + if should_error: + yield Error( + object_path, + error_msg, + stub, + runtime, + stub_desc=stub_desc, + runtime_desc=runtime_sig_desc, + ) if not signature: return @@ -1168,10 +1188,8 @@ def _verify_readonly_property(stub: nodes.Decorator, runtime: Any) -> Iterator[s def _verify_abstract_status(stub: nodes.FuncDef, runtime: Any) -> Iterator[str]: - stub_abstract = stub.abstract_status == nodes.IS_ABSTRACT - runtime_abstract = getattr(runtime, "__isabstractmethod__", False) # The opposite can exist: some implementations omit `@abstractmethod` decorators - if runtime_abstract and not stub_abstract: + if runtime_is_abstract(runtime) and not stub_is_abstract(stub): item_type = "property" if stub.is_property else "method" yield f"is inconsistent, runtime {item_type} is abstract but stub is not" @@ -1424,6 +1442,14 @@ def is_read_only_property(runtime: object) -> bool: return isinstance(runtime, property) and runtime.fset is None +def stub_is_abstract(stub: nodes.FuncDef) -> bool: + return stub.abstract_status == nodes.IS_ABSTRACT + + +def runtime_is_abstract(runtime: object) -> bool: + return getattr(runtime, "__isabstractmethod__", False) + + def safe_inspect_signature(runtime: Any) -> inspect.Signature | None: try: return inspect.signature(runtime) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 275b09c3a240..feb5f00edc3a 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -62,8 +62,11 @@ def __init__(self, name: str) -> None: ... _S = TypeVar("_S", contravariant=True) _R = TypeVar("_R", covariant=True) -class Coroutine(Generic[_T_co, _S, _R]): ... class Iterable(Generic[_T_co]): ... +class Generator(Iterable[_T_co], Generic[_T_co, _S, _R]): ... +class Awaitable(Generic[_T_co]): + def __await__(self) -> Generator[Any, None, _T_co]: ... +class Coroutine(Awaitable[_T_co], Generic[_T_co, _S, _R]): ... class Mapping(Generic[_K, _V]): ... class Match(Generic[AnyStr]): ... class Sequence(Iterable[_T_co]): ... @@ -224,6 +227,65 @@ def test_coroutines(self) -> Iterator[Case]: yield Case( stub="async def bingo() -> int: ...", runtime="async def bingo(): return 5", error=None ) + # Be more permissive if it's an abstractmethod in the stub... + yield Case( + stub=""" + from abc import abstractmethod + from typing import Awaitable + class Bar: + @abstractmethod + def abstract(self) -> Awaitable[int]: ... + """, + runtime=""" + class Bar: + async def abstract(self): return 5 + """, + error=None, + ) + # ...and/or an abstractmethod at runtime... + yield Case( + stub=""" + from abc import abstractmethod + from typing import Generator, Any, Coroutine, Awaitable + class _CustomAwaitable: + def __await__(self) -> Generator[Any, Any, Any]: ... + class _CustomAwaitable2(Awaitable[Any]): ... + class Foo: + @abstractmethod + def abstract(self) -> _CustomAwaitable: ... + @abstractmethod + def abstract2(self) -> _CustomAwaitable2: ... + @abstractmethod + def abstract3(self) -> Coroutine[Any, Any, Any]: ... + """, + runtime=""" + from abc import abstractmethod + class Foo: + @abstractmethod + async def abstract(self): return 5 + @abstractmethod + async def abstract2(self): return 4 + @abstractmethod + async def abstract3(self): return 5 + """, + error=None, + ) + # ...but still error if the stub's return type isn't awaitable + yield Case( + stub=""" + from abc import abstractmethod + class Baz: + @abstractmethod + def abstract(self) -> int: ... + """, + runtime=""" + from abc import abstractmethod + class Baz: + @abstractmethod + async def abstract(self): return 5 + """, + error="Baz.abstract", + ) @collect_cases def test_arg_name(self) -> Iterator[Case]: diff --git a/mypy/types.py b/mypy/types.py index 53f21e8c0222..36a88846e307 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1482,6 +1482,10 @@ def copy_with_extra_attr(self, name: str, typ: Type) -> Instance: new.extra_attrs = existing_attrs return new + @property + def is_awaitable(self) -> bool: + return self.type.get("__await__") is not None + def is_singleton_type(self) -> bool: # TODO: # Also make this return True if the type corresponds to NotImplemented?