Skip to content

stubtest: relax async checking for abstract methods #12343

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 15 commits into from
2 changes: 1 addition & 1 deletion mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
48 changes: 37 additions & 11 deletions mypy/stubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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)
Expand Down
64 changes: 63 additions & 1 deletion mypy/test/teststubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]): ...
Expand Down Expand Up @@ -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: ...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want to allow this? Doesn't seem particularly useful.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't remember, and I'm not sure I care anymore. I guess I'll just close this PR :)

@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]:
Expand Down
4 changes: 4 additions & 0 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down