From e77ee3b8f268defe3937327fd08ceb712b957cfa Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Wed, 10 Aug 2022 10:07:52 +0300 Subject: [PATCH] Allow stubtest to raise errors on abstract state mismatch (#13323) Co-authored-by: Alex Waygood Co-authored-by: Shantanu <12621235+hauntsaninja@users.noreply.github.com> --- mypy/stubtest.py | 12 +++++ mypy/test/teststubtest.py | 98 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 2c530f9ab2e5..e24ad410d141 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -840,6 +840,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 bb20e335a035..b82e77386c7a 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -1268,6 +1268,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!