Skip to content

Commit 55d757e

Browse files
authored
stubtest: analyze metaclass of types, refs #13327 (#13331)
1 parent d9750c6 commit 55d757e

File tree

2 files changed

+118
-10
lines changed

2 files changed

+118
-10
lines changed

mypy/stubtest.py

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -349,17 +349,9 @@ def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool:
349349
yield from verify(stub_entry, runtime_entry, object_path + [entry])
350350

351351

352-
@verify.register(nodes.TypeInfo)
353-
def verify_typeinfo(
354-
stub: nodes.TypeInfo, runtime: MaybeMissing[type[Any]], object_path: list[str]
352+
def _verify_final(
353+
stub: nodes.TypeInfo, runtime: type[Any], object_path: list[str]
355354
) -> Iterator[Error]:
356-
if isinstance(runtime, Missing):
357-
yield Error(object_path, "is not present at runtime", stub, runtime, stub_desc=repr(stub))
358-
return
359-
if not isinstance(runtime, type):
360-
yield Error(object_path, "is not a type", stub, runtime, stub_desc=repr(stub))
361-
return
362-
363355
try:
364356

365357
class SubClass(runtime): # type: ignore
@@ -380,6 +372,59 @@ class SubClass(runtime): # type: ignore
380372
# Examples: ctypes.Array, ctypes._SimpleCData
381373
pass
382374

375+
376+
def _verify_metaclass(
377+
stub: nodes.TypeInfo, runtime: type[Any], object_path: list[str]
378+
) -> Iterator[Error]:
379+
# We exclude protocols, because of how complex their implementation is in different versions of
380+
# python. Enums are also hard, ignoring.
381+
# TODO: check that metaclasses are identical?
382+
if not stub.is_protocol and not stub.is_enum:
383+
runtime_metaclass = type(runtime)
384+
if runtime_metaclass is not type and stub.metaclass_type is None:
385+
# This means that runtime has a custom metaclass, but a stub does not.
386+
yield Error(
387+
object_path,
388+
"is inconsistent, metaclass differs",
389+
stub,
390+
runtime,
391+
stub_desc="N/A",
392+
runtime_desc=f"{runtime_metaclass}",
393+
)
394+
elif (
395+
runtime_metaclass is type
396+
and stub.metaclass_type is not None
397+
# We ignore extra `ABCMeta` metaclass on stubs, this might be typing hack.
398+
# We also ignore `builtins.type` metaclass as an implementation detail in mypy.
399+
and not mypy.types.is_named_instance(
400+
stub.metaclass_type, ("abc.ABCMeta", "builtins.type")
401+
)
402+
):
403+
# This means that our stub has a metaclass that is not present at runtime.
404+
yield Error(
405+
object_path,
406+
"metaclass mismatch",
407+
stub,
408+
runtime,
409+
stub_desc=f"{stub.metaclass_type.type.fullname}",
410+
runtime_desc="N/A",
411+
)
412+
413+
414+
@verify.register(nodes.TypeInfo)
415+
def verify_typeinfo(
416+
stub: nodes.TypeInfo, runtime: MaybeMissing[type[Any]], object_path: list[str]
417+
) -> Iterator[Error]:
418+
if isinstance(runtime, Missing):
419+
yield Error(object_path, "is not present at runtime", stub, runtime, stub_desc=repr(stub))
420+
return
421+
if not isinstance(runtime, type):
422+
yield Error(object_path, "is not a type", stub, runtime, stub_desc=repr(stub))
423+
return
424+
425+
yield from _verify_final(stub, runtime, object_path)
426+
yield from _verify_metaclass(stub, runtime, object_path)
427+
383428
# Check everything already defined on the stub class itself (i.e. not inherited)
384429
to_check = set(stub.names)
385430
# Check all public things on the runtime class

mypy/test/teststubtest.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,6 +1271,69 @@ def test_type_var(self) -> Iterator[Case]:
12711271
)
12721272
yield Case(stub="C = ParamSpec('C')", runtime="C = ParamSpec('C')", error=None)
12731273

1274+
@collect_cases
1275+
def test_metaclass_match(self) -> Iterator[Case]:
1276+
yield Case(stub="class Meta(type): ...", runtime="class Meta(type): ...", error=None)
1277+
yield Case(stub="class A0: ...", runtime="class A0: ...", error=None)
1278+
yield Case(
1279+
stub="class A1(metaclass=Meta): ...",
1280+
runtime="class A1(metaclass=Meta): ...",
1281+
error=None,
1282+
)
1283+
yield Case(stub="class A2: ...", runtime="class A2(metaclass=Meta): ...", error="A2")
1284+
yield Case(stub="class A3(metaclass=Meta): ...", runtime="class A3: ...", error="A3")
1285+
1286+
# Explicit `type` metaclass can always be added in any part:
1287+
yield Case(
1288+
stub="class T1(metaclass=type): ...",
1289+
runtime="class T1(metaclass=type): ...",
1290+
error=None,
1291+
)
1292+
yield Case(stub="class T2: ...", runtime="class T2(metaclass=type): ...", error=None)
1293+
yield Case(stub="class T3(metaclass=type): ...", runtime="class T3: ...", error=None)
1294+
1295+
# Explicit check that `_protected` names are also supported:
1296+
yield Case(stub="class _P1(type): ...", runtime="class _P1(type): ...", error=None)
1297+
yield Case(stub="class P2: ...", runtime="class P2(metaclass=_P1): ...", error="P2")
1298+
1299+
# With inheritance:
1300+
yield Case(
1301+
stub="""
1302+
class I1(metaclass=Meta): ...
1303+
class S1(I1): ...
1304+
""",
1305+
runtime="""
1306+
class I1(metaclass=Meta): ...
1307+
class S1(I1): ...
1308+
""",
1309+
error=None,
1310+
)
1311+
yield Case(
1312+
stub="""
1313+
class I2(metaclass=Meta): ...
1314+
class S2: ... # missing inheritance
1315+
""",
1316+
runtime="""
1317+
class I2(metaclass=Meta): ...
1318+
class S2(I2): ...
1319+
""",
1320+
error="S2",
1321+
)
1322+
1323+
@collect_cases
1324+
def test_metaclass_abcmeta(self) -> Iterator[Case]:
1325+
# Handling abstract metaclasses is special:
1326+
yield Case(stub="from abc import ABCMeta", runtime="from abc import ABCMeta", error=None)
1327+
yield Case(
1328+
stub="class A1(metaclass=ABCMeta): ...",
1329+
runtime="class A1(metaclass=ABCMeta): ...",
1330+
error=None,
1331+
)
1332+
# Stubs cannot miss abstract metaclass:
1333+
yield Case(stub="class A2: ...", runtime="class A2(metaclass=ABCMeta): ...", error="A2")
1334+
# But, stubs can add extra abstract metaclass, this might be a typing hack:
1335+
yield Case(stub="class A3(metaclass=ABCMeta): ...", runtime="class A3: ...", error=None)
1336+
12741337
@collect_cases
12751338
def test_abstract_methods(self) -> Iterator[Case]:
12761339
yield Case(

0 commit comments

Comments
 (0)