Skip to content

Commit 7c14fee

Browse files
stubtest: Detect abstract properties mismatches (#13647)
Closes #13646 Co-authored-by: Shantanu <[email protected]>
1 parent 1df4ac2 commit 7c14fee

File tree

2 files changed

+28
-10
lines changed

2 files changed

+28
-10
lines changed

mypy/stubtest.py

+13-10
Original file line numberDiff line numberDiff line change
@@ -870,16 +870,8 @@ def verify_funcitem(
870870
return
871871

872872
if isinstance(stub, nodes.FuncDef):
873-
stub_abstract = stub.abstract_status == nodes.IS_ABSTRACT
874-
runtime_abstract = getattr(runtime, "__isabstractmethod__", False)
875-
# The opposite can exist: some implementations omit `@abstractmethod` decorators
876-
if runtime_abstract and not stub_abstract:
877-
yield Error(
878-
object_path,
879-
"is inconsistent, runtime method is abstract but stub is not",
880-
stub,
881-
runtime,
882-
)
873+
for error_text in _verify_abstract_status(stub, runtime):
874+
yield Error(object_path, error_text, stub, runtime)
883875

884876
for message in _verify_static_class_methods(stub, runtime, object_path):
885877
yield Error(object_path, "is inconsistent, " + message, stub, runtime)
@@ -1066,6 +1058,15 @@ def _verify_readonly_property(stub: nodes.Decorator, runtime: Any) -> Iterator[s
10661058
yield "is inconsistent, cannot reconcile @property on stub with runtime object"
10671059

10681060

1061+
def _verify_abstract_status(stub: nodes.FuncDef, runtime: Any) -> Iterator[str]:
1062+
stub_abstract = stub.abstract_status == nodes.IS_ABSTRACT
1063+
runtime_abstract = getattr(runtime, "__isabstractmethod__", False)
1064+
# The opposite can exist: some implementations omit `@abstractmethod` decorators
1065+
if runtime_abstract and not stub_abstract:
1066+
item_type = "property" if stub.is_property else "method"
1067+
yield f"is inconsistent, runtime {item_type} is abstract but stub is not"
1068+
1069+
10691070
def _resolve_funcitem_from_decorator(dec: nodes.OverloadPart) -> nodes.FuncItem | None:
10701071
"""Returns a FuncItem that corresponds to the output of the decorator.
10711072
@@ -1124,6 +1125,8 @@ def verify_decorator(
11241125
if stub.func.is_property:
11251126
for message in _verify_readonly_property(stub, runtime):
11261127
yield Error(object_path, message, stub, runtime)
1128+
for message in _verify_abstract_status(stub.func, runtime):
1129+
yield Error(object_path, message, stub, runtime)
11271130
return
11281131

11291132
func = _resolve_funcitem_from_decorator(stub)

mypy/test/teststubtest.py

+15
Original file line numberDiff line numberDiff line change
@@ -1382,6 +1382,7 @@ def some(self) -> None: ...
13821382

13831383
@collect_cases
13841384
def test_abstract_properties(self) -> Iterator[Case]:
1385+
# TODO: test abstract properties with setters
13851386
yield Case(
13861387
stub="from abc import abstractmethod",
13871388
runtime="from abc import abstractmethod",
@@ -1391,6 +1392,7 @@ def test_abstract_properties(self) -> Iterator[Case]:
13911392
yield Case(
13921393
stub="""
13931394
class AP1:
1395+
@property
13941396
def some(self) -> int: ...
13951397
""",
13961398
runtime="""
@@ -1401,6 +1403,19 @@ def some(self) -> int: ...
14011403
""",
14021404
error="AP1.some",
14031405
)
1406+
yield Case(
1407+
stub="""
1408+
class AP1_2:
1409+
def some(self) -> int: ... # missing `@property` decorator
1410+
""",
1411+
runtime="""
1412+
class AP1_2:
1413+
@property
1414+
@abstractmethod
1415+
def some(self) -> int: ...
1416+
""",
1417+
error="AP1_2.some",
1418+
)
14041419
yield Case(
14051420
stub="""
14061421
class AP2:

0 commit comments

Comments
 (0)