diff --git a/mypy/stubtest.py b/mypy/stubtest.py index ebc7fa12857d..33de28f993f2 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -825,6 +825,18 @@ def verify_funcitem( if not callable(runtime): 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 message in _verify_static_class_methods(stub, runtime, object_path): yield Error(object_path, "is inconsistent, " + message, stub, runtime) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 3de0e3fd5fc6..be953ae17615 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -1264,6 +1264,104 @@ def test_type_var(self) -> Iterator[Case]: ) yield Case(stub="C = ParamSpec('C')", runtime="C = ParamSpec('C')", error=None) + @collect_cases + def test_abstract_methods(self) -> Iterator[Case]: + yield Case( + stub="from abc import abstractmethod", + runtime="from abc import abstractmethod", + error=None, + ) + yield Case( + stub=""" + class A1: + def some(self) -> None: ... + """, + runtime=""" + class A1: + @abstractmethod + def some(self) -> None: ... + """, + error="A1.some", + ) + yield Case( + stub=""" + class A2: + @abstractmethod + def some(self) -> None: ... + """, + runtime=""" + class A2: + @abstractmethod + def some(self) -> None: ... + """, + error=None, + ) + # Runtime can miss `@abstractmethod`: + yield Case( + stub=""" + class A3: + @abstractmethod + def some(self) -> None: ... + """, + runtime=""" + class A3: + def some(self) -> None: ... + """, + error=None, + ) + + @collect_cases + def test_abstract_properties(self) -> Iterator[Case]: + yield Case( + stub="from abc import abstractmethod", + runtime="from abc import abstractmethod", + error=None, + ) + # Ensure that `@property` also can be abstract: + yield Case( + stub=""" + class AP1: + def some(self) -> int: ... + """, + runtime=""" + class AP1: + @property + @abstractmethod + def some(self) -> int: ... + """, + error="AP1.some", + ) + yield Case( + stub=""" + class AP2: + @property + @abstractmethod + def some(self) -> int: ... + """, + runtime=""" + class AP2: + @property + @abstractmethod + def some(self) -> int: ... + """, + error=None, + ) + # Runtime can miss `@abstractmethod`: + yield Case( + stub=""" + class AP3: + @property + @abstractmethod + def some(self) -> int: ... + """, + runtime=""" + class AP3: + @property + def some(self) -> int: ... + """, + error=None, + ) + def remove_color_code(s: str) -> str: return re.sub("\\x1b.*?m", "", s) # this works!