Skip to content

Commit 657154b

Browse files
Metaclass conflict check improvements (#17682)
This PR fixes some points of #14033: * Give metaclass errors to their own error code (I chose `metaclass`). * Document shortcomings of and workarounds for mypy's metaclass handling. I didn't attempt to fix that mypy follows the logic for determining the metaclass as documented whereas it should follow what the interpreter is actually doing (#14033 (comment)). I think such a change is better kept as a separate PR, which is why I don't want to close the issue with this PR. Fixes: #14033 --------- Co-authored-by: hauntsaninja <hauntsaninja@gmail.com>
1 parent 9934278 commit 657154b

File tree

9 files changed

+182
-20
lines changed

9 files changed

+182
-20
lines changed

docs/source/error_code_list.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,35 @@ You can use :py:class:`~collections.abc.Callable` as the type for callable objec
215215
for x in objs:
216216
f(x)
217217
218+
.. _code-metaclass:
219+
220+
Check the validity of a class's metaclass [metaclass]
221+
-----------------------------------------------------
222+
223+
Mypy checks whether the metaclass of a class is valid. The metaclass
224+
must be a subclass of ``type``. Further, the class hierarchy must yield
225+
a consistent metaclass. For more details, see the
226+
`Python documentation <https://docs.python.org/3.13/reference/datamodel.html#determining-the-appropriate-metaclass>`_
227+
228+
Note that mypy's metaclass checking is limited and may produce false-positives.
229+
See also :ref:`limitations`.
230+
231+
Example with an error:
232+
233+
.. code-block:: python
234+
235+
class GoodMeta(type):
236+
pass
237+
238+
class BadMeta:
239+
pass
240+
241+
class A1(metaclass=GoodMeta): # OK
242+
pass
243+
244+
class A2(metaclass=BadMeta): # Error: Metaclasses not inheriting from "type" are not supported [metaclass]
245+
pass
246+
218247
.. _code-var-annotated:
219248

220249
Require annotation if variable type is unclear [var-annotated]

docs/source/metaclasses.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,28 @@ so it's better not to combine metaclasses and class hierarchies:
9090
* ``Self`` is not allowed as annotation in metaclasses as per `PEP 673`_.
9191

9292
.. _PEP 673: https://peps.python.org/pep-0673/#valid-locations-for-self
93+
94+
For some builtin types, mypy may think their metaclass is :py:class:`abc.ABCMeta`
95+
even if it is :py:class:`type` at runtime. In those cases, you can either:
96+
97+
* use :py:class:`abc.ABCMeta` instead of :py:class:`type` as the
98+
superclass of your metaclass if that works in your use-case
99+
* mute the error with ``# type: ignore[metaclass]``
100+
101+
.. code-block:: python
102+
103+
import abc
104+
105+
assert type(tuple) is type # metaclass of tuple is type at runtime
106+
107+
# The problem:
108+
class M0(type): pass
109+
class A0(tuple, metaclass=M0): pass # Mypy Error: metaclass conflict
110+
111+
# Option 1: use ABCMeta instead of type
112+
class M1(abc.ABCMeta): pass
113+
class A1(tuple, metaclass=M1): pass
114+
115+
# Option 2: mute the error
116+
class M2(type): pass
117+
class A2(tuple, metaclass=M2): pass # type: ignore[metaclass]

mypy/checker.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2959,7 +2959,11 @@ def check_metaclass_compatibility(self, typ: TypeInfo) -> None:
29592959
"Metaclass conflict: the metaclass of a derived class must be "
29602960
"a (non-strict) subclass of the metaclasses of all its bases",
29612961
typ,
2962+
code=codes.METACLASS,
29622963
)
2964+
explanation = typ.explain_metaclass_conflict()
2965+
if explanation:
2966+
self.note(explanation, typ, code=codes.METACLASS)
29632967

29642968
def visit_import_from(self, node: ImportFrom) -> None:
29652969
for name, _ in node.names:

mypy/errorcodes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ def __hash__(self) -> int:
270270
"General",
271271
default_enabled=False,
272272
)
273+
METACLASS: Final[ErrorCode] = ErrorCode("metaclass", "Ensure that metaclass is valid", "General")
273274

274275
# Syntax errors are often blocking.
275276
SYNTAX: Final[ErrorCode] = ErrorCode("syntax", "Report syntax errors", "General")

mypy/nodes.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3402,6 +3402,43 @@ def calculate_metaclass_type(self) -> mypy.types.Instance | None:
34023402

34033403
return winner
34043404

3405+
def explain_metaclass_conflict(self) -> str | None:
3406+
# Compare to logic in calculate_metaclass_type
3407+
declared = self.declared_metaclass
3408+
if declared is not None and not declared.type.has_base("builtins.type"):
3409+
return None
3410+
if self._fullname == "builtins.type":
3411+
return None
3412+
3413+
winner = declared
3414+
if declared is None:
3415+
resolution_steps = []
3416+
else:
3417+
resolution_steps = [f'"{declared.type.fullname}" (metaclass of "{self.fullname}")']
3418+
for super_class in self.mro[1:]:
3419+
super_meta = super_class.declared_metaclass
3420+
if super_meta is None or super_meta.type is None:
3421+
continue
3422+
if winner is None:
3423+
winner = super_meta
3424+
resolution_steps.append(
3425+
f'"{winner.type.fullname}" (metaclass of "{super_class.fullname}")'
3426+
)
3427+
continue
3428+
if winner.type.has_base(super_meta.type.fullname):
3429+
continue
3430+
if super_meta.type.has_base(winner.type.fullname):
3431+
winner = super_meta
3432+
resolution_steps.append(
3433+
f'"{winner.type.fullname}" (metaclass of "{super_class.fullname}")'
3434+
)
3435+
continue
3436+
# metaclass conflict
3437+
conflict = f'"{super_meta.type.fullname}" (metaclass of "{super_class.fullname}")'
3438+
return f"{' > '.join(resolution_steps)} conflicts with {conflict}"
3439+
3440+
return None
3441+
34053442
def is_metaclass(self, *, precise: bool = False) -> bool:
34063443
return (
34073444
self.has_base("builtins.type")

mypy/semanal.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2702,7 +2702,7 @@ def infer_metaclass_and_bases_from_compat_helpers(self, defn: ClassDef) -> None:
27022702
if len(metas) == 0:
27032703
return
27042704
if len(metas) > 1:
2705-
self.fail("Multiple metaclass definitions", defn)
2705+
self.fail("Multiple metaclass definitions", defn, code=codes.METACLASS)
27062706
return
27072707
defn.metaclass = metas.pop()
27082708

@@ -2758,7 +2758,11 @@ def get_declared_metaclass(
27582758
elif isinstance(metaclass_expr, MemberExpr):
27592759
metaclass_name = get_member_expr_fullname(metaclass_expr)
27602760
if metaclass_name is None:
2761-
self.fail(f'Dynamic metaclass not supported for "{name}"', metaclass_expr)
2761+
self.fail(
2762+
f'Dynamic metaclass not supported for "{name}"',
2763+
metaclass_expr,
2764+
code=codes.METACLASS,
2765+
)
27622766
return None, False, True
27632767
sym = self.lookup_qualified(metaclass_name, metaclass_expr)
27642768
if sym is None:
@@ -2769,6 +2773,7 @@ def get_declared_metaclass(
27692773
self.fail(
27702774
f'Class cannot use "{sym.node.name}" as a metaclass (has type "Any")',
27712775
metaclass_expr,
2776+
code=codes.METACLASS,
27722777
)
27732778
return None, False, True
27742779
if isinstance(sym.node, PlaceholderNode):
@@ -2786,11 +2791,15 @@ def get_declared_metaclass(
27862791
metaclass_info = target.type
27872792

27882793
if not isinstance(metaclass_info, TypeInfo) or metaclass_info.tuple_type is not None:
2789-
self.fail(f'Invalid metaclass "{metaclass_name}"', metaclass_expr)
2794+
self.fail(
2795+
f'Invalid metaclass "{metaclass_name}"', metaclass_expr, code=codes.METACLASS
2796+
)
27902797
return None, False, False
27912798
if not metaclass_info.is_metaclass():
27922799
self.fail(
2793-
'Metaclasses not inheriting from "type" are not supported', metaclass_expr
2800+
'Metaclasses not inheriting from "type" are not supported',
2801+
metaclass_expr,
2802+
code=codes.METACLASS,
27942803
)
27952804
return None, False, False
27962805
inst = fill_typevars(metaclass_info)

test-data/unit/check-classes.test

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4757,8 +4757,8 @@ class C(B):
47574757
class X(type): pass
47584758
class Y(type): pass
47594759
class A(metaclass=X): pass
4760-
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
4761-
4760+
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 \
4761+
# N: "__main__.Y" (metaclass of "__main__.B") conflicts with "__main__.X" (metaclass of "__main__.A")
47624762
[case testMetaclassNoTypeReveal]
47634763
class M:
47644764
x = 0 # type: int
@@ -5737,8 +5737,8 @@ def f() -> type: return M
57375737
class C1(six.with_metaclass(M), object): pass # E: Unsupported dynamic base class "six.with_metaclass"
57385738
class C2(C1, six.with_metaclass(M)): pass # E: Unsupported dynamic base class "six.with_metaclass"
57395739
class C3(six.with_metaclass(A)): pass # E: Metaclasses not inheriting from "type" are not supported
5740-
@six.add_metaclass(A) # E: Metaclasses not inheriting from "type" are not supported \
5741-
# E: Argument 1 to "add_metaclass" has incompatible type "type[A]"; expected "type[type]"
5740+
@six.add_metaclass(A) # E: Metaclasses not inheriting from "type" are not supported \
5741+
# E: Argument 1 to "add_metaclass" has incompatible type "type[A]"; expected "type[type]"
57425742

57435743
class D3(A): pass
57445744
class C4(six.with_metaclass(M), metaclass=M): pass # E: Multiple metaclass definitions
@@ -5754,8 +5754,10 @@ class CD(six.with_metaclass(M)): pass # E: Multiple metaclass definitions
57545754
class M1(type): pass
57555755
class Q1(metaclass=M1): pass
57565756
@six.add_metaclass(M)
5757-
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
5758-
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
5757+
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 \
5758+
# N: "__main__.M" (metaclass of "__main__.CQA") conflicts with "__main__.M1" (metaclass of "__main__.Q1")
5759+
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 \
5760+
# N: "__main__.M" (metaclass of "__main__.CQW") conflicts with "__main__.M1" (metaclass of "__main__.Q1")
57595761
[builtins fixtures/tuple.pyi]
57605762

57615763
[case testSixMetaclassAny]
@@ -5873,7 +5875,8 @@ class C5(future.utils.with_metaclass(f())): pass # E: Dynamic metaclass not sup
58735875

58745876
class M1(type): pass
58755877
class Q1(metaclass=M1): pass
5876-
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
5878+
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 \
5879+
# N: "__main__.M" (metaclass of "__main__.CQW") conflicts with "__main__.M1" (metaclass of "__main__.Q1")
58775880
[builtins fixtures/tuple.pyi]
58785881

58795882
[case testFutureMetaclassAny]
@@ -7342,17 +7345,22 @@ class ChildOfCorrectSubclass1(CorrectSubclass1): ...
73427345
class CorrectWithType1(C, A1): ...
73437346
class CorrectWithType2(B, C): ...
73447347

7345-
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
7346-
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
7347-
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
7348+
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 \
7349+
# N: "__main__.MyMeta1" (metaclass of "__main__.A") conflicts with "__main__.MyMeta2" (metaclass of "__main__.B")
7350+
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 \
7351+
# N: "__main__.MyMeta1" (metaclass of "__main__.A") conflicts with "__main__.MyMeta2" (metaclass of "__main__.B")
7352+
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 \
7353+
# N: "__main__.MyMeta2" (metaclass of "__main__.B") conflicts with "__main__.MyMeta1" (metaclass of "__main__.A")
73487354

73497355
class ChildOfConflict1(Conflict3): ...
73507356
class ChildOfConflict2(Conflict3, metaclass=CorrectMeta): ...
73517357

73527358
class ConflictingMeta(MyMeta1, MyMeta3): ...
7353-
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
7359+
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 \
7360+
# N: "__main__.ConflictingMeta" (metaclass of "__main__.Conflict4") conflicts with "__main__.MyMeta2" (metaclass of "__main__.B")
73547361

7355-
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
7362+
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 \
7363+
# N: "__main__.ConflictingMeta" (metaclass of "__main__.ChildOfCorrectButWrongMeta") conflicts with "__main__.CorrectMeta" (metaclass of "__main__.CorrectSubclass1")
73567364
...
73577365

73587366
[case testMetaClassConflictIssue14033]
@@ -7367,8 +7375,10 @@ class B1(metaclass=M2): pass
73677375

73687376
class C1(metaclass=Mx): pass
73697377

7370-
class TestABC(A2, B1, C1): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
7371-
class TestBAC(B1, A2, C1): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
7378+
class TestABC(A2, B1, C1): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases \
7379+
# N: "__main__.M1" (metaclass of "__main__.A1") conflicts with "__main__.M2" (metaclass of "__main__.B1")
7380+
class TestBAC(B1, A2, C1): pass # E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases \
7381+
# N: "__main__.M2" (metaclass of "__main__.B1") conflicts with "__main__.M1" (metaclass of "__main__.A1")
73727382

73737383
# should not warn again for children
73747384
class ChildOfTestABC(TestABC): pass

test-data/unit/check-errorcodes.test

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,6 +1239,47 @@ def f(x: str) -> TypeIs[int]: # E: Narrowed type "int" is not a subtype of inpu
12391239

12401240
[builtins fixtures/tuple.pyi]
12411241

1242+
[case testDynamicMetaclass]
1243+
class A(metaclass=type(tuple)): pass # E: Dynamic metaclass not supported for "A" [metaclass]
1244+
[builtins fixtures/tuple.pyi]
1245+
1246+
[case testMetaclassOfTypeAny]
1247+
# mypy: disallow-subclassing-any=True
1248+
from typing import Any
1249+
foo: Any = ...
1250+
class A(metaclass=foo): pass # E: Class cannot use "foo" as a metaclass (has type "Any") [metaclass]
1251+
1252+
[case testMetaclassOfWrongType]
1253+
class Foo:
1254+
bar = 1
1255+
class A2(metaclass=Foo.bar): pass # E: Invalid metaclass "Foo.bar" [metaclass]
1256+
1257+
[case testMetaclassNotTypeSubclass]
1258+
class M: pass
1259+
class A(metaclass=M): pass # E: Metaclasses not inheriting from "type" are not supported [metaclass]
1260+
1261+
[case testMultipleMetaclasses]
1262+
import six
1263+
class M1(type): pass
1264+
1265+
@six.add_metaclass(M1)
1266+
class A1(metaclass=M1): pass # E: Multiple metaclass definitions [metaclass]
1267+
1268+
class A2(six.with_metaclass(M1), metaclass=M1): pass # E: Multiple metaclass definitions [metaclass]
1269+
1270+
@six.add_metaclass(M1)
1271+
class A3(six.with_metaclass(M1)): pass # E: Multiple metaclass definitions [metaclass]
1272+
[builtins fixtures/tuple.pyi]
1273+
1274+
[case testInvalidMetaclassStructure]
1275+
class X(type): pass
1276+
class Y(type): pass
1277+
class A(metaclass=X): pass
1278+
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 [metaclass] \
1279+
# N: "__main__.Y" (metaclass of "__main__.B") conflicts with "__main__.X" (metaclass of "__main__.A")
1280+
1281+
1282+
12421283

12431284
[case testOverloadedFunctionSignature]
12441285
from typing import overload, Union

test-data/unit/fine-grained.test

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2936,10 +2936,12 @@ a.py:6: error: Argument 1 to "f" has incompatible type "type[B]"; expected "M"
29362936

29372937
[case testFineMetaclassRecalculation]
29382938
import a
2939+
29392940
[file a.py]
29402941
from b import B
29412942
class M2(type): pass
29422943
class D(B, metaclass=M2): pass
2944+
29432945
[file b.py]
29442946
import c
29452947
class B: pass
@@ -2949,27 +2951,31 @@ import c
29492951
class B(metaclass=c.M): pass
29502952

29512953
[file c.py]
2952-
class M(type):
2953-
pass
2954+
class M(type): pass
29542955
[out]
29552956
==
29562957
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
2958+
a.py:3: note: "a.M2" (metaclass of "a.D") conflicts with "c.M" (metaclass of "b.B")
29572959

29582960
[case testFineMetaclassDeclaredUpdate]
29592961
import a
2962+
29602963
[file a.py]
29612964
import b
29622965
class B(metaclass=b.M): pass
29632966
class D(B, metaclass=b.M2): pass
2967+
29642968
[file b.py]
29652969
class M(type): pass
29662970
class M2(M): pass
2971+
29672972
[file b.py.2]
29682973
class M(type): pass
29692974
class M2(type): pass
29702975
[out]
29712976
==
29722977
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
2978+
a.py:3: note: "b.M2" (metaclass of "a.D") conflicts with "b.M" (metaclass of "a.B")
29732979

29742980
[case testFineMetaclassRemoveFromClass]
29752981
import a

0 commit comments

Comments
 (0)