Skip to content

Commit 9d21615

Browse files
authored
Detect metaclass conflicts (#13598)
Recreate of #13565 Closes #13563
1 parent 130e1a4 commit 9d21615

File tree

5 files changed

+79
-21
lines changed

5 files changed

+79
-21
lines changed

docs/source/metaclasses.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,15 @@ so it's better not to combine metaclasses and class hierarchies:
7272
class A1(metaclass=M1): pass
7373
class A2(metaclass=M2): pass
7474
75-
class B1(A1, metaclass=M2): pass # Mypy Error: Inconsistent metaclass structure for "B1"
75+
class B1(A1, metaclass=M2): pass # Mypy Error: metaclass conflict
7676
# At runtime the above definition raises an exception
7777
# TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
7878
79-
# Same runtime error as in B1, but mypy does not catch it yet
80-
class B12(A1, A2): pass
79+
class B12(A1, A2): pass # Mypy Error: metaclass conflict
80+
81+
# This can be solved via a common metaclass subtype:
82+
class CorrectMeta(M1, M2): pass
83+
class B2(A1, A2, metaclass=CorrectMeta): pass # OK, runtime is also OK
8184
8285
* Mypy does not understand dynamically-computed metaclasses,
8386
such as ``class A(metaclass=f()): ...``

mypy/checker.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2044,6 +2044,7 @@ def visit_class_def(self, defn: ClassDef) -> None:
20442044
if not defn.has_incompatible_baseclass:
20452045
# Otherwise we've already found errors; more errors are not useful
20462046
self.check_multiple_inheritance(typ)
2047+
self.check_metaclass_compatibility(typ)
20472048
self.check_final_deletable(typ)
20482049

20492050
if defn.decorators:
@@ -2383,6 +2384,35 @@ class C(B, A[int]): ... # this is unsafe because...
23832384
if not ok:
23842385
self.msg.base_class_definitions_incompatible(name, base1, base2, ctx)
23852386

2387+
def check_metaclass_compatibility(self, typ: TypeInfo) -> None:
2388+
"""Ensures that metaclasses of all parent types are compatible."""
2389+
if (
2390+
typ.is_metaclass()
2391+
or typ.is_protocol
2392+
or typ.is_named_tuple
2393+
or typ.is_enum
2394+
or typ.typeddict_type is not None
2395+
):
2396+
return # Reasonable exceptions from this check
2397+
2398+
metaclasses = [
2399+
entry.metaclass_type
2400+
for entry in typ.mro[1:-1]
2401+
if entry.metaclass_type
2402+
and not is_named_instance(entry.metaclass_type, "builtins.type")
2403+
]
2404+
if not metaclasses:
2405+
return
2406+
if typ.metaclass_type is not None and all(
2407+
is_subtype(typ.metaclass_type, meta) for meta in metaclasses
2408+
):
2409+
return
2410+
self.fail(
2411+
"Metaclass conflict: the metaclass of a derived class must be "
2412+
"a (non-strict) subclass of the metaclasses of all its bases",
2413+
typ,
2414+
)
2415+
23862416
def visit_import_from(self, node: ImportFrom) -> None:
23872417
self.check_import(node)
23882418

mypy/semanal.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2120,18 +2120,10 @@ def recalculate_metaclass(self, defn: ClassDef, declared_metaclass: Instance | N
21202120
abc_meta = self.named_type_or_none("abc.ABCMeta", [])
21212121
if abc_meta is not None: # May be None in tests with incomplete lib-stub.
21222122
defn.info.metaclass_type = abc_meta
2123-
if declared_metaclass is not None and defn.info.metaclass_type is None:
2124-
# Inconsistency may happen due to multiple baseclasses even in classes that
2125-
# do not declare explicit metaclass, but it's harder to catch at this stage
2126-
if defn.metaclass is not None:
2127-
self.fail(f'Inconsistent metaclass structure for "{defn.name}"', defn)
2128-
else:
2129-
if defn.info.metaclass_type and defn.info.metaclass_type.type.has_base(
2130-
"enum.EnumMeta"
2131-
):
2132-
defn.info.is_enum = True
2133-
if defn.type_vars:
2134-
self.fail("Enum class cannot be generic", defn)
2123+
if defn.info.metaclass_type and defn.info.metaclass_type.type.has_base("enum.EnumMeta"):
2124+
defn.info.is_enum = True
2125+
if defn.type_vars:
2126+
self.fail("Enum class cannot be generic", defn)
21352127

21362128
#
21372129
# Imports

test-data/unit/check-classes.test

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4351,7 +4351,7 @@ class C(B):
43514351
class X(type): pass
43524352
class Y(type): pass
43534353
class A(metaclass=X): pass
4354-
class B(A, metaclass=Y): pass # E: Inconsistent metaclass structure for "B"
4354+
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
43554355

43564356
[case testMetaclassNoTypeReveal]
43574357
class M:
@@ -5213,8 +5213,8 @@ class CD(six.with_metaclass(M)): pass # E: Multiple metaclass definitions
52135213
class M1(type): pass
52145214
class Q1(metaclass=M1): pass
52155215
@six.add_metaclass(M)
5216-
class CQA(Q1): pass # E: Inconsistent metaclass structure for "CQA"
5217-
class CQW(six.with_metaclass(M, Q1)): pass # E: Inconsistent metaclass structure for "CQW"
5216+
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
5217+
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
52185218
[builtins fixtures/tuple.pyi]
52195219

52205220
[case testSixMetaclassAny]
@@ -5319,7 +5319,7 @@ class C5(future.utils.with_metaclass(f())): pass # E: Dynamic metaclass not sup
53195319

53205320
class M1(type): pass
53215321
class Q1(metaclass=M1): pass
5322-
class CQW(future.utils.with_metaclass(M, Q1)): pass # E: Inconsistent metaclass structure for "CQW"
5322+
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
53235323
[builtins fixtures/tuple.pyi]
53245324

53255325
[case testFutureMetaclassAny]
@@ -6718,6 +6718,39 @@ class Meta(A): pass
67186718
from m import Meta
67196719
class A(metaclass=Meta): pass
67206720

6721+
[case testMetaclassConflict]
6722+
class MyMeta1(type): ...
6723+
class MyMeta2(type): ...
6724+
class MyMeta3(type): ...
6725+
class A(metaclass=MyMeta1): ...
6726+
class B(metaclass=MyMeta2): ...
6727+
class C(metaclass=type): ...
6728+
class A1(A): ...
6729+
class E: ...
6730+
6731+
class CorrectMeta(MyMeta1, MyMeta2): ...
6732+
class CorrectSubclass1(A1, B, E, metaclass=CorrectMeta): ...
6733+
class CorrectSubclass2(A, B, E, metaclass=CorrectMeta): ...
6734+
class CorrectSubclass3(B, A, metaclass=CorrectMeta): ...
6735+
6736+
class ChildOfCorrectSubclass1(CorrectSubclass1): ...
6737+
6738+
class CorrectWithType1(C, A1): ...
6739+
class CorrectWithType2(B, C): ...
6740+
6741+
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
6742+
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
6743+
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
6744+
6745+
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
6746+
class ChildOfConflict2(Conflict3, metaclass=CorrectMeta): ...
6747+
6748+
class ConflictingMeta(MyMeta1, MyMeta3): ...
6749+
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
6750+
6751+
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
6752+
...
6753+
67216754
[case testGenericOverride]
67226755
from typing import Generic, TypeVar, Any
67236756

test-data/unit/fine-grained.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2968,7 +2968,7 @@ class M(type):
29682968
pass
29692969
[out]
29702970
==
2971-
a.py:3: error: Inconsistent metaclass structure for "D"
2971+
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
29722972

29732973
[case testFineMetaclassDeclaredUpdate]
29742974
import a
@@ -2984,7 +2984,7 @@ class M(type): pass
29842984
class M2(type): pass
29852985
[out]
29862986
==
2987-
a.py:3: error: Inconsistent metaclass structure for "D"
2987+
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
29882988

29892989
[case testFineMetaclassRemoveFromClass]
29902990
import a

0 commit comments

Comments
 (0)