diff --git a/docs/source/error_code_list.rst b/docs/source/error_code_list.rst index 85c8d437a856..b197fabbdbc2 100644 --- a/docs/source/error_code_list.rst +++ b/docs/source/error_code_list.rst @@ -221,6 +221,35 @@ You can use :py:data:`~typing.Callable` as the type for callable objects: for x in objs: f(x) +.. _code-metaclass: + +Check the validity of a class's metaclass [metaclass] +----------------------------------------------------- + +Mypy checks whether the metaclass of a class is valid. The metaclass +must be a subclass of ``type``. Further, the class hierarchy must yield +a consistent metaclass. For more details, see the +`Python documentation `_ + +Note that mypy's metaclass checking is limited and may produce false-positives. +See also :ref:`limitations`. + +Example with an error: + +.. code-block:: python + + class GoodMeta(type): + pass + + class BadMeta: + pass + + class A1(metaclass=GoodMeta): # OK + pass + + class A2(metaclass=BadMeta): # Error: Metaclasses not inheriting from "type" are not supported [metaclass] + pass + .. _code-var-annotated: Require annotation if variable type is unclear [var-annotated] diff --git a/docs/source/metaclasses.rst b/docs/source/metaclasses.rst index 396d7dbb42cc..fe82399a3995 100644 --- a/docs/source/metaclasses.rst +++ b/docs/source/metaclasses.rst @@ -86,3 +86,34 @@ so it's better not to combine metaclasses and class hierarchies: such as ``class A(metaclass=f()): ...`` * Mypy does not and cannot understand arbitrary metaclass code. * Mypy only recognizes subclasses of :py:class:`type` as potential metaclasses. + +For some builtin types, mypy assumes that their metaclass is :py:class:`abc.ABCMeta` +even if it's :py:class:`type`. In those cases, you can either + +* use :py:class:`abc.ABCMeta` instead of :py:class:`type` as the + superclass of your metaclass if that works in your use case, +* mute the error with ``# type: ignore[metaclass]``, or +* compute the metaclass' superclass dynamically, which mypy doesn't understand + so it will also need to be muted. + +.. code-block:: python + + import abc + + assert type(tuple) is type # metaclass of tuple is type + + # the problem: + class M0(type): pass + class A0(tuple, metaclass=M1): pass # Mypy Error: metaclass conflict + + # option 1: use ABCMeta instead of type + class M1(abc.ABCMeta): pass + class A1(tuple, metaclass=M1): pass + + # option 2: mute the error + class M2(type): pass + class A2(tuple, metaclass=M2): pass # type: ignore[metaclass] + + # option 3: compute the metaclass dynamically + class M3(type(tuple)): pass # type: ignore[metaclass] + class A3(tuple, metaclass=M3): pass diff --git a/mypy/checker.py b/mypy/checker.py index db65660bbfbd..36a2075b79e3 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2831,10 +2831,27 @@ def check_metaclass_compatibility(self, typ: TypeInfo) -> None: is_subtype(typ.metaclass_type, meta) for meta in metaclasses ): return + if typ.declared_metaclass is None: + metaclass_names = { # using a dict as ordered set + str(meta): None for meta in metaclasses + }.keys() + conflict_info = f"found metaclasses of bases: {', '.join(metaclass_names)}" + else: + uncovered_metaclass_names = { # using a dict as ordered set + str(meta): None + for meta in metaclasses + if not is_subtype(typ.declared_metaclass, meta) + }.keys() + conflict_info = ( + f"own metaclass {typ.declared_metaclass} is not a subclass of " + f"{', '.join(uncovered_metaclass_names)}" + ) self.fail( - "Metaclass conflict: the metaclass of a derived class must be " - "a (non-strict) subclass of the metaclasses of all its bases", + "Metaclass conflict: the metaclass of a derived class must be a " + "(non-strict) subclass of the metaclasses of all its bases - " + f"{conflict_info}", typ, + code=codes.METACLASS, ) def visit_import_from(self, node: ImportFrom) -> None: diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 6e8763264ddd..dc908a70fa84 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -261,6 +261,7 @@ def __hash__(self) -> int: "General", default_enabled=False, ) +METACLASS: Final[ErrorCode] = ErrorCode("metaclass", "Ensure that metaclass is valid", "General") # Syntax errors are often blocking. SYNTAX: Final[ErrorCode] = ErrorCode("syntax", "Report syntax errors", "General") diff --git a/mypy/semanal.py b/mypy/semanal.py index 782985e3fbab..18b0b10909fa 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2592,7 +2592,7 @@ def infer_metaclass_and_bases_from_compat_helpers(self, defn: ClassDef) -> None: if len(metas) == 0: return if len(metas) > 1: - self.fail("Multiple metaclass definitions", defn) + self.fail("Multiple metaclass definitions", defn, code=codes.METACLASS) return defn.metaclass = metas.pop() @@ -2648,7 +2648,11 @@ def get_declared_metaclass( elif isinstance(metaclass_expr, MemberExpr): metaclass_name = get_member_expr_fullname(metaclass_expr) if metaclass_name is None: - self.fail(f'Dynamic metaclass not supported for "{name}"', metaclass_expr) + self.fail( + f'Dynamic metaclass not supported for "{name}"', + metaclass_expr, + code=codes.METACLASS, + ) return None, False, True sym = self.lookup_qualified(metaclass_name, metaclass_expr) if sym is None: @@ -2659,6 +2663,7 @@ def get_declared_metaclass( self.fail( f'Class cannot use "{sym.node.name}" as a metaclass (has type "Any")', metaclass_expr, + code=codes.METACLASS, ) return None, False, True if isinstance(sym.node, PlaceholderNode): @@ -2676,11 +2681,15 @@ def get_declared_metaclass( metaclass_info = sym.node if not isinstance(metaclass_info, TypeInfo) or metaclass_info.tuple_type is not None: - self.fail(f'Invalid metaclass "{metaclass_name}"', metaclass_expr) + self.fail( + f'Invalid metaclass "{metaclass_name}"', metaclass_expr, code=codes.METACLASS + ) return None, False, False if not metaclass_info.is_metaclass(): self.fail( - 'Metaclasses not inheriting from "type" are not supported', metaclass_expr + 'Metaclasses not inheriting from "type" are not supported', + metaclass_expr, + code=codes.METACLASS, ) return None, False, False inst = fill_typevars(metaclass_info) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 82208d27df41..f9469caff5e6 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -4558,7 +4558,7 @@ class C(B): class X(type): pass class Y(type): pass class A(metaclass=X): pass -class B(A, metaclass=Y): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases +class B(A, metaclass=Y): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases - own metaclass __main__.Y is not a subclass of __main__.X [case testMetaclassNoTypeReveal] class M: @@ -5552,8 +5552,8 @@ class CD(six.with_metaclass(M)): pass # E: Multiple metaclass definitions class M1(type): pass class Q1(metaclass=M1): pass @six.add_metaclass(M) -class CQA(Q1): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases -class CQW(six.with_metaclass(M, Q1)): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases +class CQA(Q1): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases - own metaclass __main__.M is not a subclass of __main__.M1 +class CQW(six.with_metaclass(M, Q1)): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases - own metaclass __main__.M is not a subclass of __main__.M1 [builtins fixtures/tuple.pyi] [case testSixMetaclassAny] @@ -5671,7 +5671,7 @@ class C5(future.utils.with_metaclass(f())): pass # E: Dynamic metaclass not sup class M1(type): pass class Q1(metaclass=M1): pass -class CQW(future.utils.with_metaclass(M, Q1)): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases +class CQW(future.utils.with_metaclass(M, Q1)): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases - own metaclass __main__.M is not a subclass of __main__.M1 [builtins fixtures/tuple.pyi] [case testFutureMetaclassAny] @@ -7100,17 +7100,17 @@ class ChildOfCorrectSubclass1(CorrectSubclass1): ... class CorrectWithType1(C, A1): ... class CorrectWithType2(B, C): ... -class Conflict1(A1, B, E): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases -class Conflict2(A, B): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases -class Conflict3(B, A): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases +class Conflict1(A1, B, E): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases - found metaclasses of bases: __main__.MyMeta1, __main__.MyMeta2 +class Conflict2(A, B): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases - found metaclasses of bases: __main__.MyMeta1, __main__.MyMeta2 +class Conflict3(B, A): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases - found metaclasses of bases: __main__.MyMeta2, __main__.MyMeta1 -class ChildOfConflict1(Conflict3): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases +class ChildOfConflict1(Conflict3): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases - found metaclasses of bases: __main__.MyMeta2, __main__.MyMeta1 class ChildOfConflict2(Conflict3, metaclass=CorrectMeta): ... class ConflictingMeta(MyMeta1, MyMeta3): ... -class Conflict4(A1, B, E, metaclass=ConflictingMeta): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases +class Conflict4(A1, B, E, metaclass=ConflictingMeta): ... # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases - own metaclass __main__.ConflictingMeta is not a subclass of __main__.MyMeta2 -class ChildOfCorrectButWrongMeta(CorrectSubclass1, metaclass=ConflictingMeta): # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases +class ChildOfCorrectButWrongMeta(CorrectSubclass1, metaclass=ConflictingMeta): # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases - own metaclass __main__.ConflictingMeta is not a subclass of __main__.CorrectMeta, __main__.MyMeta2 ... [case testGenericOverride] diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index c4d72388fba9..f90c70a57fa7 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -1196,3 +1196,35 @@ def f(x: str) -> TypeIs[int]: # E: Narrowed type "int" is not a subtype of inpu pass [builtins fixtures/tuple.pyi] + +[case testDynamicMetaclass] +class A(metaclass=type(tuple)): pass # E: Dynamic metaclass not supported for "A" [metaclass] +[builtins fixtures/tuple.pyi] + +[case testMetaclassOfTypeAny] +# mypy: disallow-subclassing-any=True +from typing import Any +foo: Any = ... +class A(metaclass=foo): pass # E: Class cannot use "foo" as a metaclass (has type "Any") [metaclass] + +[case testMetaclassOfWrongType] +class Foo: + bar = 1 +class A2(metaclass=Foo.bar): pass # E: Invalid metaclass "Foo.bar" [metaclass] + +[case testMetaclassNotTypeSubclass] +class M: pass +class A(metaclass=M): pass # E: Metaclasses not inheriting from "type" are not supported [metaclass] + +[case testMultipleMetaclasses] +import six +class M1(type): pass + +@six.add_metaclass(M1) +class A1(metaclass=M1): pass # E: Multiple metaclass definitions [metaclass] + +class A2(six.with_metaclass(M1), metaclass=M1): pass # E: Multiple metaclass definitions [metaclass] + +@six.add_metaclass(M1) +class A3(six.with_metaclass(M1)): pass # E: Multiple metaclass definitions [metaclass] +[builtins fixtures/tuple.pyi] diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test index 2ad31311a402..40ae232354e8 100644 --- a/test-data/unit/fine-grained.test +++ b/test-data/unit/fine-grained.test @@ -2936,10 +2936,12 @@ a.py:6: error: Argument 1 to "f" has incompatible type "Type[B]"; expected "M" [case testFineMetaclassRecalculation] import a + [file a.py] from b import B class M2(type): pass class D(B, metaclass=M2): pass + [file b.py] import c class B: pass @@ -2949,27 +2951,29 @@ import c class B(metaclass=c.M): pass [file c.py] -class M(type): - pass +class M(type): pass [out] == -a.py:3: error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases +a.py:3: error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases - own metaclass a.M2 is not a subclass of c.M [case testFineMetaclassDeclaredUpdate] import a + [file a.py] import b class B(metaclass=b.M): pass class D(B, metaclass=b.M2): pass + [file b.py] class M(type): pass class M2(M): pass + [file b.py.2] class M(type): pass class M2(type): pass [out] == -a.py:3: error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases +a.py:3: error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases - own metaclass b.M2 is not a subclass of b.M [case testFineMetaclassRemoveFromClass] import a