Skip to content

Commit

Permalink
stubtest: analyze metaclass of types, refs #13327 (#13331)
Browse files Browse the repository at this point in the history
  • Loading branch information
sobolevn committed Aug 25, 2022
1 parent d9750c6 commit 55d757e
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 10 deletions.
65 changes: 55 additions & 10 deletions mypy/stubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,17 +349,9 @@ def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool:
yield from verify(stub_entry, runtime_entry, object_path + [entry])


@verify.register(nodes.TypeInfo)
def verify_typeinfo(
stub: nodes.TypeInfo, runtime: MaybeMissing[type[Any]], object_path: list[str]
def _verify_final(
stub: nodes.TypeInfo, runtime: type[Any], object_path: list[str]
) -> Iterator[Error]:
if isinstance(runtime, Missing):
yield Error(object_path, "is not present at runtime", stub, runtime, stub_desc=repr(stub))
return
if not isinstance(runtime, type):
yield Error(object_path, "is not a type", stub, runtime, stub_desc=repr(stub))
return

try:

class SubClass(runtime): # type: ignore
Expand All @@ -380,6 +372,59 @@ class SubClass(runtime): # type: ignore
# Examples: ctypes.Array, ctypes._SimpleCData
pass


def _verify_metaclass(
stub: nodes.TypeInfo, runtime: type[Any], object_path: list[str]
) -> Iterator[Error]:
# We exclude protocols, because of how complex their implementation is in different versions of
# python. Enums are also hard, ignoring.
# TODO: check that metaclasses are identical?
if not stub.is_protocol and not stub.is_enum:
runtime_metaclass = type(runtime)
if runtime_metaclass is not type and stub.metaclass_type is None:
# This means that runtime has a custom metaclass, but a stub does not.
yield Error(
object_path,
"is inconsistent, metaclass differs",
stub,
runtime,
stub_desc="N/A",
runtime_desc=f"{runtime_metaclass}",
)
elif (
runtime_metaclass is type
and stub.metaclass_type is not None
# We ignore extra `ABCMeta` metaclass on stubs, this might be typing hack.
# We also ignore `builtins.type` metaclass as an implementation detail in mypy.
and not mypy.types.is_named_instance(
stub.metaclass_type, ("abc.ABCMeta", "builtins.type")
)
):
# This means that our stub has a metaclass that is not present at runtime.
yield Error(
object_path,
"metaclass mismatch",
stub,
runtime,
stub_desc=f"{stub.metaclass_type.type.fullname}",
runtime_desc="N/A",
)


@verify.register(nodes.TypeInfo)
def verify_typeinfo(
stub: nodes.TypeInfo, runtime: MaybeMissing[type[Any]], object_path: list[str]
) -> Iterator[Error]:
if isinstance(runtime, Missing):
yield Error(object_path, "is not present at runtime", stub, runtime, stub_desc=repr(stub))
return
if not isinstance(runtime, type):
yield Error(object_path, "is not a type", stub, runtime, stub_desc=repr(stub))
return

yield from _verify_final(stub, runtime, object_path)
yield from _verify_metaclass(stub, runtime, object_path)

# Check everything already defined on the stub class itself (i.e. not inherited)
to_check = set(stub.names)
# Check all public things on the runtime class
Expand Down
63 changes: 63 additions & 0 deletions mypy/test/teststubtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1271,6 +1271,69 @@ def test_type_var(self) -> Iterator[Case]:
)
yield Case(stub="C = ParamSpec('C')", runtime="C = ParamSpec('C')", error=None)

@collect_cases
def test_metaclass_match(self) -> Iterator[Case]:
yield Case(stub="class Meta(type): ...", runtime="class Meta(type): ...", error=None)
yield Case(stub="class A0: ...", runtime="class A0: ...", error=None)
yield Case(
stub="class A1(metaclass=Meta): ...",
runtime="class A1(metaclass=Meta): ...",
error=None,
)
yield Case(stub="class A2: ...", runtime="class A2(metaclass=Meta): ...", error="A2")
yield Case(stub="class A3(metaclass=Meta): ...", runtime="class A3: ...", error="A3")

# Explicit `type` metaclass can always be added in any part:
yield Case(
stub="class T1(metaclass=type): ...",
runtime="class T1(metaclass=type): ...",
error=None,
)
yield Case(stub="class T2: ...", runtime="class T2(metaclass=type): ...", error=None)
yield Case(stub="class T3(metaclass=type): ...", runtime="class T3: ...", error=None)

# Explicit check that `_protected` names are also supported:
yield Case(stub="class _P1(type): ...", runtime="class _P1(type): ...", error=None)
yield Case(stub="class P2: ...", runtime="class P2(metaclass=_P1): ...", error="P2")

# With inheritance:
yield Case(
stub="""
class I1(metaclass=Meta): ...
class S1(I1): ...
""",
runtime="""
class I1(metaclass=Meta): ...
class S1(I1): ...
""",
error=None,
)
yield Case(
stub="""
class I2(metaclass=Meta): ...
class S2: ... # missing inheritance
""",
runtime="""
class I2(metaclass=Meta): ...
class S2(I2): ...
""",
error="S2",
)

@collect_cases
def test_metaclass_abcmeta(self) -> Iterator[Case]:
# Handling abstract metaclasses is special:
yield Case(stub="from abc import ABCMeta", runtime="from abc import ABCMeta", error=None)
yield Case(
stub="class A1(metaclass=ABCMeta): ...",
runtime="class A1(metaclass=ABCMeta): ...",
error=None,
)
# Stubs cannot miss abstract metaclass:
yield Case(stub="class A2: ...", runtime="class A2(metaclass=ABCMeta): ...", error="A2")
# But, stubs can add extra abstract metaclass, this might be a typing hack:
yield Case(stub="class A3(metaclass=ABCMeta): ...", runtime="class A3: ...", error=None)

@collect_cases
def test_abstract_methods(self) -> Iterator[Case]:
yield Case(
Expand Down

0 comments on commit 55d757e

Please sign in to comment.