Skip to content

Commit e77ee3b

Browse files
sobolevnAlexWaygoodhauntsaninja
authored
Allow stubtest to raise errors on abstract state mismatch (#13323)
Co-authored-by: Alex Waygood <[email protected]> Co-authored-by: Shantanu <[email protected]>
1 parent 0ce8793 commit e77ee3b

File tree

2 files changed

+110
-0
lines changed

2 files changed

+110
-0
lines changed

mypy/stubtest.py

+12
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,18 @@ def verify_funcitem(
840840
if not callable(runtime):
841841
return
842842

843+
if isinstance(stub, nodes.FuncDef):
844+
stub_abstract = stub.abstract_status == nodes.IS_ABSTRACT
845+
runtime_abstract = getattr(runtime, "__isabstractmethod__", False)
846+
# The opposite can exist: some implementations omit `@abstractmethod` decorators
847+
if runtime_abstract and not stub_abstract:
848+
yield Error(
849+
object_path,
850+
"is inconsistent, runtime method is abstract but stub is not",
851+
stub,
852+
runtime,
853+
)
854+
843855
for message in _verify_static_class_methods(stub, runtime, object_path):
844856
yield Error(object_path, "is inconsistent, " + message, stub, runtime)
845857

mypy/test/teststubtest.py

+98
Original file line numberDiff line numberDiff line change
@@ -1268,6 +1268,104 @@ def test_type_var(self) -> Iterator[Case]:
12681268
)
12691269
yield Case(stub="C = ParamSpec('C')", runtime="C = ParamSpec('C')", error=None)
12701270

1271+
@collect_cases
1272+
def test_abstract_methods(self) -> Iterator[Case]:
1273+
yield Case(
1274+
stub="from abc import abstractmethod",
1275+
runtime="from abc import abstractmethod",
1276+
error=None,
1277+
)
1278+
yield Case(
1279+
stub="""
1280+
class A1:
1281+
def some(self) -> None: ...
1282+
""",
1283+
runtime="""
1284+
class A1:
1285+
@abstractmethod
1286+
def some(self) -> None: ...
1287+
""",
1288+
error="A1.some",
1289+
)
1290+
yield Case(
1291+
stub="""
1292+
class A2:
1293+
@abstractmethod
1294+
def some(self) -> None: ...
1295+
""",
1296+
runtime="""
1297+
class A2:
1298+
@abstractmethod
1299+
def some(self) -> None: ...
1300+
""",
1301+
error=None,
1302+
)
1303+
# Runtime can miss `@abstractmethod`:
1304+
yield Case(
1305+
stub="""
1306+
class A3:
1307+
@abstractmethod
1308+
def some(self) -> None: ...
1309+
""",
1310+
runtime="""
1311+
class A3:
1312+
def some(self) -> None: ...
1313+
""",
1314+
error=None,
1315+
)
1316+
1317+
@collect_cases
1318+
def test_abstract_properties(self) -> Iterator[Case]:
1319+
yield Case(
1320+
stub="from abc import abstractmethod",
1321+
runtime="from abc import abstractmethod",
1322+
error=None,
1323+
)
1324+
# Ensure that `@property` also can be abstract:
1325+
yield Case(
1326+
stub="""
1327+
class AP1:
1328+
def some(self) -> int: ...
1329+
""",
1330+
runtime="""
1331+
class AP1:
1332+
@property
1333+
@abstractmethod
1334+
def some(self) -> int: ...
1335+
""",
1336+
error="AP1.some",
1337+
)
1338+
yield Case(
1339+
stub="""
1340+
class AP2:
1341+
@property
1342+
@abstractmethod
1343+
def some(self) -> int: ...
1344+
""",
1345+
runtime="""
1346+
class AP2:
1347+
@property
1348+
@abstractmethod
1349+
def some(self) -> int: ...
1350+
""",
1351+
error=None,
1352+
)
1353+
# Runtime can miss `@abstractmethod`:
1354+
yield Case(
1355+
stub="""
1356+
class AP3:
1357+
@property
1358+
@abstractmethod
1359+
def some(self) -> int: ...
1360+
""",
1361+
runtime="""
1362+
class AP3:
1363+
@property
1364+
def some(self) -> int: ...
1365+
""",
1366+
error=None,
1367+
)
1368+
12711369

12721370
def remove_color_code(s: str) -> str:
12731371
return re.sub("\\x1b.*?m", "", s) # this works!

0 commit comments

Comments
 (0)