From 56b7af60873cd7a1f3841f6af5f08f652fa0f8cf Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Mon, 14 Nov 2022 13:03:01 +0100 Subject: [PATCH 1/6] Improve error message of metaclass conflict --- mypy/checker.py | 22 +++++++++++++++++++--- test-data/unit/check-classes.test | 20 ++++++++++---------- test-data/unit/fine-grained.test | 12 ++++++++---- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index db65660bbfbd..68b27182cb07 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: - """Ensures that metaclasses of all parent types are compatible.""" + """Ensure that metaclasses of all parent types are compatible.""" if ( typ.is_metaclass() or typ.is_protocol @@ -2831,9 +2831,25 @@ 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, ) 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/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 From be3a64604f46e471a44af41a303ec5f9ca203ad2 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Mon, 14 Nov 2022 14:50:08 +0100 Subject: [PATCH 2/6] Introduce new error code `metaclass` --- docs/source/error_code_list.rst | 26 ++++++++++++++++++++++++ mypy/checker.py | 1 + mypy/errorcodes.py | 5 +++++ mypy/semanal.py | 11 ++++++---- test-data/unit/check-errorcodes.test | 30 ++++++++++++++++++++++++++++ 5 files changed, 69 insertions(+), 4 deletions(-) diff --git a/docs/source/error_code_list.rst b/docs/source/error_code_list.rst index 85c8d437a856..9c8e3871950e 100644 --- a/docs/source/error_code_list.rst +++ b/docs/source/error_code_list.rst @@ -221,6 +221,32 @@ 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 `_ + +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/mypy/checker.py b/mypy/checker.py index 68b27182cb07..b7a16ccdbbd0 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2851,6 +2851,7 @@ def check_metaclass_compatibility(self, typ: TypeInfo) -> None: "(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..2c46a3f7dd55 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -261,6 +261,11 @@ 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..edd6bfd02a57 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,7 @@ 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 +2659,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 +2677,13 @@ 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-errorcodes.test b/test-data/unit/check-errorcodes.test index c4d72388fba9..17ebb91e4684 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -1195,4 +1195,34 @@ from typing_extensions import TypeIs def f(x: str) -> TypeIs[int]: # E: Narrowed type "int" is not a subtype of input type "str" [narrowed-type-not-subtype] pass +[case testDynamicMetaclass] +class A(metaclass=type(tuple)): pass # E: Dynamic metaclass not supported for "A" [metaclass] + +[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] From 0756cbc5fc144bb498819fdcafde1865027e511d Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Fri, 16 Aug 2024 12:06:08 +0200 Subject: [PATCH 3/6] Docs: how to work around metaclass limitation --- docs/source/error_code_list.rst | 3 +++ docs/source/metaclasses.rst | 31 +++++++++++++++++++++++++++++++ mypy/checker.py | 2 +- mypy/errorcodes.py | 6 +----- mypy/semanal.py | 10 ++++++++-- 5 files changed, 44 insertions(+), 8 deletions(-) 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( From 0d2fd528be22d0a500972329b0e26fcc87530d44 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Fri, 16 Aug 2024 15:46:24 +0200 Subject: [PATCH 4/6] Docs: fix reference --- docs/source/metaclasses.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/metaclasses.rst b/docs/source/metaclasses.rst index cb73c1d13e61..fe82399a3995 100644 --- a/docs/source/metaclasses.rst +++ b/docs/source/metaclasses.rst @@ -90,7 +90,7 @@ so it's better not to combine metaclasses and class hierarchies: 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 +* 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 From 969562df6918b99bc7e30530c2ac7ea7e8705dc1 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Fri, 16 Aug 2024 15:49:09 +0200 Subject: [PATCH 5/6] Tests: fix missing fixtures --- test-data/unit/check-errorcodes.test | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 17ebb91e4684..f5daceaf7322 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -1195,8 +1195,11 @@ from typing_extensions import TypeIs def f(x: str) -> TypeIs[int]: # E: Narrowed type "int" is not a subtype of input type "str" [narrowed-type-not-subtype] 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 @@ -1224,5 +1227,3 @@ class A2(six.with_metaclass(M1), metaclass=M1): pass # E: Multiple metaclass de @six.add_metaclass(M1) class A3(six.with_metaclass(M1)): pass # E: Multiple metaclass definitions [metaclass] - -[builtins fixtures/tuple.pyi] From b6c8eb92086aba1a3f83b7acb0adf027b3b216db Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Mon, 26 Aug 2024 15:14:06 +0200 Subject: [PATCH 6/6] Fix tests: missing tuple type fixture --- test-data/unit/check-errorcodes.test | 1 + 1 file changed, 1 insertion(+) diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index f5daceaf7322..f90c70a57fa7 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -1227,3 +1227,4 @@ class A2(six.with_metaclass(M1), metaclass=M1): pass # E: Multiple metaclass de @six.add_metaclass(M1) class A3(six.with_metaclass(M1)): pass # E: Multiple metaclass definitions [metaclass] +[builtins fixtures/tuple.pyi]