diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 718a6cfa6254..4b3175e8649f 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -870,16 +870,8 @@ def verify_funcitem( return if isinstance(stub, nodes.FuncDef): - 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: - yield Error( - object_path, - "is inconsistent, runtime method is abstract but stub is not", - stub, - runtime, - ) + for error_text in _verify_abstract_status(stub, runtime): + yield Error(object_path, error_text, stub, runtime) for message in _verify_static_class_methods(stub, runtime, object_path): yield Error(object_path, "is inconsistent, " + message, stub, runtime) @@ -1066,6 +1058,15 @@ def _verify_readonly_property(stub: nodes.Decorator, runtime: Any) -> Iterator[s yield "is inconsistent, cannot reconcile @property on stub with runtime object" +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: + item_type = "property" if stub.is_property else "method" + yield f"is inconsistent, runtime {item_type} is abstract but stub is not" + + def _resolve_funcitem_from_decorator(dec: nodes.OverloadPart) -> nodes.FuncItem | None: """Returns a FuncItem that corresponds to the output of the decorator. @@ -1124,6 +1125,8 @@ def verify_decorator( if stub.func.is_property: for message in _verify_readonly_property(stub, runtime): yield Error(object_path, message, stub, runtime) + for message in _verify_abstract_status(stub.func, runtime): + yield Error(object_path, message, stub, runtime) return func = _resolve_funcitem_from_decorator(stub) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 8e78966363d4..d74949fde783 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -1382,6 +1382,7 @@ def some(self) -> None: ... @collect_cases def test_abstract_properties(self) -> Iterator[Case]: + # TODO: test abstract properties with setters yield Case( stub="from abc import abstractmethod", runtime="from abc import abstractmethod", @@ -1391,6 +1392,7 @@ def test_abstract_properties(self) -> Iterator[Case]: yield Case( stub=""" class AP1: + @property def some(self) -> int: ... """, runtime=""" @@ -1401,6 +1403,19 @@ def some(self) -> int: ... """, error="AP1.some", ) + yield Case( + stub=""" + class AP1_2: + def some(self) -> int: ... # missing `@property` decorator + """, + runtime=""" + class AP1_2: + @property + @abstractmethod + def some(self) -> int: ... + """, + error="AP1_2.some", + ) yield Case( stub=""" class AP2: