Skip to content

Commit 01a088b

Browse files
authored
Stubtest: verify stub methods or properties are decorated with @final if they are decorated with @final at runtime (#14951)
This implements most of #14924. The only thing it _doesn't_ implement is verification for overloaded methods decorated with `@final` -- I tried working on that, but hit #14950.
1 parent 2c6e43e commit 01a088b

File tree

2 files changed

+260
-14
lines changed

2 files changed

+260
-14
lines changed

mypy/stubtest.py

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -529,8 +529,21 @@ def verify_typeinfo(
529529
yield from verify(stub_to_verify, runtime_attr, object_path + [entry])
530530

531531

532+
def _static_lookup_runtime(object_path: list[str]) -> MaybeMissing[Any]:
533+
static_runtime = importlib.import_module(object_path[0])
534+
for entry in object_path[1:]:
535+
try:
536+
static_runtime = inspect.getattr_static(static_runtime, entry)
537+
except AttributeError:
538+
# This can happen with mangled names, ignore for now.
539+
# TODO: pass more information about ancestors of nodes/objects to verify, so we don't
540+
# have to do this hacky lookup. Would be useful in several places.
541+
return MISSING
542+
return static_runtime
543+
544+
532545
def _verify_static_class_methods(
533-
stub: nodes.FuncBase, runtime: Any, object_path: list[str]
546+
stub: nodes.FuncBase, runtime: Any, static_runtime: MaybeMissing[Any], object_path: list[str]
534547
) -> Iterator[str]:
535548
if stub.name in ("__new__", "__init_subclass__", "__class_getitem__"):
536549
# Special cased by Python, so don't bother checking
@@ -545,16 +558,8 @@ def _verify_static_class_methods(
545558
yield "stub is a classmethod but runtime is not"
546559
return
547560

548-
# Look the object up statically, to avoid binding by the descriptor protocol
549-
static_runtime = importlib.import_module(object_path[0])
550-
for entry in object_path[1:]:
551-
try:
552-
static_runtime = inspect.getattr_static(static_runtime, entry)
553-
except AttributeError:
554-
# This can happen with mangled names, ignore for now.
555-
# TODO: pass more information about ancestors of nodes/objects to verify, so we don't
556-
# have to do this hacky lookup. Would be useful in a couple other places too.
557-
return
561+
if static_runtime is MISSING:
562+
return
558563

559564
if isinstance(static_runtime, classmethod) and not stub.is_class:
560565
yield "runtime is a classmethod but stub is not"
@@ -945,11 +950,16 @@ def verify_funcitem(
945950
if not callable(runtime):
946951
return
947952

953+
# Look the object up statically, to avoid binding by the descriptor protocol
954+
static_runtime = _static_lookup_runtime(object_path)
955+
948956
if isinstance(stub, nodes.FuncDef):
949957
for error_text in _verify_abstract_status(stub, runtime):
950958
yield Error(object_path, error_text, stub, runtime)
959+
for error_text in _verify_final_method(stub, runtime, static_runtime):
960+
yield Error(object_path, error_text, stub, runtime)
951961

952-
for message in _verify_static_class_methods(stub, runtime, object_path):
962+
for message in _verify_static_class_methods(stub, runtime, static_runtime, object_path):
953963
yield Error(object_path, "is inconsistent, " + message, stub, runtime)
954964

955965
signature = safe_inspect_signature(runtime)
@@ -1063,9 +1073,15 @@ def verify_overloadedfuncdef(
10631073
for msg in _verify_abstract_status(first_part.func, runtime):
10641074
yield Error(object_path, msg, stub, runtime)
10651075

1066-
for message in _verify_static_class_methods(stub, runtime, object_path):
1076+
# Look the object up statically, to avoid binding by the descriptor protocol
1077+
static_runtime = _static_lookup_runtime(object_path)
1078+
1079+
for message in _verify_static_class_methods(stub, runtime, static_runtime, object_path):
10671080
yield Error(object_path, "is inconsistent, " + message, stub, runtime)
10681081

1082+
# TODO: Should call _verify_final_method here,
1083+
# but overloaded final methods in stubs cause a stubtest crash: see #14950
1084+
10691085
signature = safe_inspect_signature(runtime)
10701086
if not signature:
10711087
return
@@ -1126,6 +1142,7 @@ def verify_paramspecexpr(
11261142
def _verify_readonly_property(stub: nodes.Decorator, runtime: Any) -> Iterator[str]:
11271143
assert stub.func.is_property
11281144
if isinstance(runtime, property):
1145+
yield from _verify_final_method(stub.func, runtime.fget, MISSING)
11291146
return
11301147
if inspect.isdatadescriptor(runtime):
11311148
# It's enough like a property...
@@ -1154,6 +1171,17 @@ def _verify_abstract_status(stub: nodes.FuncDef, runtime: Any) -> Iterator[str]:
11541171
yield f"is inconsistent, runtime {item_type} is abstract but stub is not"
11551172

11561173

1174+
def _verify_final_method(
1175+
stub: nodes.FuncDef, runtime: Any, static_runtime: MaybeMissing[Any]
1176+
) -> Iterator[str]:
1177+
if stub.is_final:
1178+
return
1179+
if getattr(runtime, "__final__", False) or (
1180+
static_runtime is not MISSING and getattr(static_runtime, "__final__", False)
1181+
):
1182+
yield "is decorated with @final at runtime, but not in the stub"
1183+
1184+
11571185
def _resolve_funcitem_from_decorator(dec: nodes.OverloadPart) -> nodes.FuncItem | None:
11581186
"""Returns a FuncItem that corresponds to the output of the decorator.
11591187

mypy/test/teststubtest.py

Lines changed: 219 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1143,7 +1143,10 @@ def test_not_subclassable(self) -> Iterator[Case]:
11431143
def test_has_runtime_final_decorator(self) -> Iterator[Case]:
11441144
yield Case(
11451145
stub="from typing_extensions import final",
1146-
runtime="from typing_extensions import final",
1146+
runtime="""
1147+
import functools
1148+
from typing_extensions import final
1149+
""",
11471150
error=None,
11481151
)
11491152
yield Case(
@@ -1177,6 +1180,221 @@ class C: ...
11771180
""",
11781181
error="C",
11791182
)
1183+
yield Case(
1184+
stub="""
1185+
class D:
1186+
@final
1187+
def foo(self) -> None: ...
1188+
@final
1189+
@staticmethod
1190+
def bar() -> None: ...
1191+
@staticmethod
1192+
@final
1193+
def bar2() -> None: ...
1194+
@final
1195+
@classmethod
1196+
def baz(cls) -> None: ...
1197+
@classmethod
1198+
@final
1199+
def baz2(cls) -> None: ...
1200+
@property
1201+
@final
1202+
def eggs(self) -> int: ...
1203+
@final
1204+
@property
1205+
def eggs2(self) -> int: ...
1206+
@final
1207+
def ham(self, obj: int) -> int: ...
1208+
""",
1209+
runtime="""
1210+
class D:
1211+
@final
1212+
def foo(self): pass
1213+
@final
1214+
@staticmethod
1215+
def bar(): pass
1216+
@staticmethod
1217+
@final
1218+
def bar2(): pass
1219+
@final
1220+
@classmethod
1221+
def baz(cls): pass
1222+
@classmethod
1223+
@final
1224+
def baz2(cls): pass
1225+
@property
1226+
@final
1227+
def eggs(self): return 42
1228+
@final
1229+
@property
1230+
def eggs2(self): pass
1231+
@final
1232+
@functools.lru_cache()
1233+
def ham(self, obj): return obj * 2
1234+
""",
1235+
error=None,
1236+
)
1237+
# Stub methods are allowed to have @final even if the runtime doesn't...
1238+
yield Case(
1239+
stub="""
1240+
class E:
1241+
@final
1242+
def foo(self) -> None: ...
1243+
@final
1244+
@staticmethod
1245+
def bar() -> None: ...
1246+
@staticmethod
1247+
@final
1248+
def bar2() -> None: ...
1249+
@final
1250+
@classmethod
1251+
def baz(cls) -> None: ...
1252+
@classmethod
1253+
@final
1254+
def baz2(cls) -> None: ...
1255+
@property
1256+
@final
1257+
def eggs(self) -> int: ...
1258+
@final
1259+
@property
1260+
def eggs2(self) -> int: ...
1261+
@final
1262+
def ham(self, obj: int) -> int: ...
1263+
""",
1264+
runtime="""
1265+
class E:
1266+
def foo(self): pass
1267+
@staticmethod
1268+
def bar(): pass
1269+
@staticmethod
1270+
def bar2(): pass
1271+
@classmethod
1272+
def baz(cls): pass
1273+
@classmethod
1274+
def baz2(cls): pass
1275+
@property
1276+
def eggs(self): return 42
1277+
@property
1278+
def eggs2(self): return 42
1279+
@functools.lru_cache()
1280+
def ham(self, obj): return obj * 2
1281+
""",
1282+
error=None,
1283+
)
1284+
# ...But if the runtime has @final, the stub must have it as well
1285+
yield Case(
1286+
stub="""
1287+
class F:
1288+
def foo(self) -> None: ...
1289+
""",
1290+
runtime="""
1291+
class F:
1292+
@final
1293+
def foo(self): pass
1294+
""",
1295+
error="F.foo",
1296+
)
1297+
yield Case(
1298+
stub="""
1299+
class G:
1300+
@staticmethod
1301+
def foo() -> None: ...
1302+
""",
1303+
runtime="""
1304+
class G:
1305+
@final
1306+
@staticmethod
1307+
def foo(): pass
1308+
""",
1309+
error="G.foo",
1310+
)
1311+
yield Case(
1312+
stub="""
1313+
class H:
1314+
@staticmethod
1315+
def foo() -> None: ...
1316+
""",
1317+
runtime="""
1318+
class H:
1319+
@staticmethod
1320+
@final
1321+
def foo(): pass
1322+
""",
1323+
error="H.foo",
1324+
)
1325+
yield Case(
1326+
stub="""
1327+
class I:
1328+
@classmethod
1329+
def foo(cls) -> None: ...
1330+
""",
1331+
runtime="""
1332+
class I:
1333+
@final
1334+
@classmethod
1335+
def foo(cls): pass
1336+
""",
1337+
error="I.foo",
1338+
)
1339+
yield Case(
1340+
stub="""
1341+
class J:
1342+
@classmethod
1343+
def foo(cls) -> None: ...
1344+
""",
1345+
runtime="""
1346+
class J:
1347+
@classmethod
1348+
@final
1349+
def foo(cls): pass
1350+
""",
1351+
error="J.foo",
1352+
)
1353+
yield Case(
1354+
stub="""
1355+
class K:
1356+
@property
1357+
def foo(self) -> int: ...
1358+
""",
1359+
runtime="""
1360+
class K:
1361+
@property
1362+
@final
1363+
def foo(self): return 42
1364+
""",
1365+
error="K.foo",
1366+
)
1367+
# This test wouldn't pass,
1368+
# because the runtime can't set __final__ on instances of builtins.property,
1369+
# so stubtest has non way of knowing that the runtime was decorated with @final:
1370+
#
1371+
# yield Case(
1372+
# stub="""
1373+
# class K2:
1374+
# @property
1375+
# def foo(self) -> int: ...
1376+
# """,
1377+
# runtime="""
1378+
# class K2:
1379+
# @final
1380+
# @property
1381+
# def foo(self): return 42
1382+
# """,
1383+
# error="K2.foo",
1384+
# )
1385+
yield Case(
1386+
stub="""
1387+
class L:
1388+
def foo(self, obj: int) -> int: ...
1389+
""",
1390+
runtime="""
1391+
class L:
1392+
@final
1393+
@functools.lru_cache()
1394+
def foo(self, obj): return obj * 2
1395+
""",
1396+
error="L.foo",
1397+
)
11801398

11811399
@collect_cases
11821400
def test_name_mangling(self) -> Iterator[Case]:

0 commit comments

Comments
 (0)