diff --git a/docs/source/error_code_list.rst b/docs/source/error_code_list.rst index 9c8e3871950e..b197fabbdbc2 100644 --- a/docs/source/error_code_list.rst +++ b/docs/source/error_code_list.rst @@ -231,6 +231,9 @@ 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 diff --git a/docs/source/metaclasses.rst b/docs/source/metaclasses.rst index 396d7dbb42cc..cb73c1d13e61 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.ABCMetaclass` 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 b7a16ccdbbd0..36a2075b79e3 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2809,7 +2809,7 @@ class C(B, A[int]): ... # this is unsafe because... self.msg.base_class_definitions_incompatible(name, base1, base2, ctx) def check_metaclass_compatibility(self, typ: TypeInfo) -> None: - """Ensure that metaclasses of all parent types are compatible.""" + """Ensures that metaclasses of all parent types are compatible.""" if ( typ.is_metaclass() or typ.is_protocol diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 2c46a3f7dd55..dc908a70fa84 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -261,11 +261,7 @@ def __hash__(self) -> int: "General", default_enabled=False, ) -METACLASS: Final[ErrorCode] = ErrorCode( - "metaclass", - "Ensure that metaclass is valid", - "General", -) +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 edd6bfd02a57..18b0b10909fa 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -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, code=codes.METACLASS) + 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: @@ -2677,7 +2681,9 @@ 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, code=codes.METACLASS) + self.fail( + f'Invalid metaclass "{metaclass_name}"', metaclass_expr, code=codes.METACLASS + ) return None, False, False if not metaclass_info.is_metaclass(): self.fail(