Repository: python/mypy

Issue:
stubtest: metaclass is not taken into an account
Here's a simple example of this bug:

```python
# runtime implementation
class A(type):
    def method(self) -> None:
        print('a')

class B(metaclass=A): ...

# This works:
# B.method()
```

And:

```python
# type stub
class A(type):
    def method(self) -> None: ...

class B: ...  # notice that `metaclass=A` is missing, but `stubtest` does not produce any errors
```

Notice that this bug prevents using things like `B.method()` in your code.
I will send a fix asap.

Refs https://github.com/python/mypy/issues/13316


Discussion:
CC @AlexWaygood 
Also note that stubtest currently doesn't check inheritance at all. I have a patch for this, but never got around to merging it. See https://github.com/python/typeshed/issues/3968 for the patch / details

Resolving patch:
diff --git a/mypy/stubtest.py b/mypy/stubtest.py
--- a/mypy/stubtest.py
+++ b/mypy/stubtest.py
@@ -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
@@ -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


Test patch:
diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py
--- a/mypy/test/teststubtest.py
+++ b/mypy/test/teststubtest.py
@@ -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(