Skip to content

Commit b18d3f8

Browse files
authored
Fix metaclass resolution algorithm (#17713)
This PR fixes the algorithm for determining a classes metaclass. Fixes #14033
1 parent 4322d4f commit b18d3f8

File tree

4 files changed

+64
-28
lines changed

4 files changed

+64
-28
lines changed

mypy/checker.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2933,23 +2933,14 @@ def check_metaclass_compatibility(self, typ: TypeInfo) -> None:
29332933
):
29342934
return # Reasonable exceptions from this check
29352935

2936-
metaclasses = [
2937-
entry.metaclass_type
2938-
for entry in typ.mro[1:-1]
2939-
if entry.metaclass_type
2940-
and not is_named_instance(entry.metaclass_type, "builtins.type")
2941-
]
2942-
if not metaclasses:
2943-
return
2944-
if typ.metaclass_type is not None and all(
2945-
is_subtype(typ.metaclass_type, meta) for meta in metaclasses
2936+
if typ.metaclass_type is None and any(
2937+
base.type.metaclass_type is not None for base in typ.bases
29462938
):
2947-
return
2948-
self.fail(
2949-
"Metaclass conflict: the metaclass of a derived class must be "
2950-
"a (non-strict) subclass of the metaclasses of all its bases",
2951-
typ,
2952-
)
2939+
self.fail(
2940+
"Metaclass conflict: the metaclass of a derived class must be "
2941+
"a (non-strict) subclass of the metaclasses of all its bases",
2942+
typ,
2943+
)
29532944

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

mypy/nodes.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3382,15 +3382,25 @@ def calculate_metaclass_type(self) -> mypy.types.Instance | None:
33823382
return declared
33833383
if self._fullname == "builtins.type":
33843384
return mypy.types.Instance(self, [])
3385-
candidates = [
3386-
s.declared_metaclass
3387-
for s in self.mro
3388-
if s.declared_metaclass is not None and s.declared_metaclass.type is not None
3389-
]
3390-
for c in candidates:
3391-
if all(other.type in c.type.mro for other in candidates):
3392-
return c
3393-
return None
3385+
3386+
winner = declared
3387+
for super_class in self.mro[1:]:
3388+
super_meta = super_class.declared_metaclass
3389+
if super_meta is None or super_meta.type is None:
3390+
continue
3391+
if winner is None:
3392+
winner = super_meta
3393+
continue
3394+
if winner.type.has_base(super_meta.type.fullname):
3395+
continue
3396+
if super_meta.type.has_base(winner.type.fullname):
3397+
winner = super_meta
3398+
continue
3399+
# metaclass conflict
3400+
winner = None
3401+
break
3402+
3403+
return winner
33943404

33953405
def is_metaclass(self, *, precise: bool = False) -> bool:
33963406
return (

test-data/unit/check-abstract.test

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -571,8 +571,12 @@ from abc import abstractmethod, ABCMeta
571571
import typing
572572

573573
class A(metaclass=ABCMeta): pass
574-
class B(object, A): pass \
575-
# E: Cannot determine consistent method resolution order (MRO) for "B"
574+
class B(object, A, metaclass=ABCMeta): # E: Cannot determine consistent method resolution order (MRO) for "B"
575+
pass
576+
577+
class C(object, A): # E: Cannot determine consistent method resolution order (MRO) for "C" \
578+
# E: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
579+
pass
576580

577581
[case testOverloadedAbstractMethod]
578582
from foo import *

test-data/unit/check-classes.test

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7292,7 +7292,7 @@ class Conflict1(A1, B, E): ... # E: Metaclass conflict: the metaclass of a deri
72927292
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
72937293
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
72947294

7295-
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
7295+
class ChildOfConflict1(Conflict3): ...
72967296
class ChildOfConflict2(Conflict3, metaclass=CorrectMeta): ...
72977297

72987298
class ConflictingMeta(MyMeta1, MyMeta3): ...
@@ -7301,6 +7301,37 @@ class Conflict4(A1, B, E, metaclass=ConflictingMeta): ... # E: Metaclass confli
73017301
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
73027302
...
73037303

7304+
[case testMetaClassConflictIssue14033]
7305+
class M1(type): pass
7306+
class M2(type): pass
7307+
class Mx(M1, M2): pass
7308+
7309+
class A1(metaclass=M1): pass
7310+
class A2(A1): pass
7311+
7312+
class B1(metaclass=M2): pass
7313+
7314+
class C1(metaclass=Mx): pass
7315+
7316+
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
7317+
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
7318+
7319+
# should not warn again for children
7320+
class ChildOfTestABC(TestABC): pass
7321+
7322+
# no metaclass is assumed if super class has a metaclass conflict
7323+
class ChildOfTestABCMetaMx(TestABC, metaclass=Mx): pass
7324+
class ChildOfTestABCMetaM1(TestABC, metaclass=M1): pass
7325+
7326+
class TestABCMx(A2, B1, C1, metaclass=Mx): pass
7327+
class TestBACMx(B1, A2, C1, metaclass=Mx): pass
7328+
7329+
class TestACB(A2, C1, B1): pass
7330+
class TestBCA(B1, C1, A2): pass
7331+
7332+
class TestCAB(C1, A2, B1): pass
7333+
class TestCBA(C1, B1, A2): pass
7334+
73047335
[case testGenericOverride]
73057336
from typing import Generic, TypeVar, Any
73067337

0 commit comments

Comments
 (0)