Skip to content

stubtest: analyze metaclass of types, refs #13327 #13331

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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