Skip to content

Commit b8d656f

Browse files
committed
Fix metaclass resolution algorithm
1 parent fe15ee6 commit b8d656f

File tree

4 files changed

+59
-27
lines changed

4 files changed

+59
-27
lines changed

mypy/checker.py

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

2822-
metaclasses = [
2823-
entry.metaclass_type
2824-
for entry in typ.mro[1:-1]
2825-
if entry.metaclass_type
2826-
and not is_named_instance(entry.metaclass_type, "builtins.type")
2827-
]
2828-
if not metaclasses:
2829-
return
2830-
if typ.metaclass_type is not None and all(
2831-
is_subtype(typ.metaclass_type, meta) for meta in metaclasses
2822+
if typ.metaclass_type is None and any(
2823+
base.type.metaclass_type is not None for base in typ.bases
28322824
):
2833-
return
2834-
self.fail(
2835-
"Metaclass conflict: the metaclass of a derived class must be "
2836-
"a (non-strict) subclass of the metaclasses of all its bases",
2837-
typ,
2838-
)
2825+
self.fail(
2826+
"Metaclass conflict: the metaclass of a derived class must be "
2827+
"a (non-strict) subclass of the metaclasses of all its bases",
2828+
typ,
2829+
)
28392830

28402831
def visit_import_from(self, node: ImportFrom) -> None:
28412832
self.check_import(node)

mypy/nodes.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3246,15 +3246,25 @@ def calculate_metaclass_type(self) -> mypy.types.Instance | None:
32463246
return declared
32473247
if self._fullname == "builtins.type":
32483248
return mypy.types.Instance(self, [])
3249-
candidates = [
3250-
s.declared_metaclass
3251-
for s in self.mro
3252-
if s.declared_metaclass is not None and s.declared_metaclass.type is not None
3253-
]
3254-
for c in candidates:
3255-
if all(other.type in c.type.mro for other in candidates):
3256-
return c
3257-
return None
3249+
3250+
winner = declared
3251+
for super_class in self.mro[1:]:
3252+
super_meta = super_class.declared_metaclass
3253+
if super_meta is None or super_meta.type is None:
3254+
continue
3255+
if winner is None:
3256+
winner = super_meta
3257+
continue
3258+
if winner.type.has_base(super_meta.type.fullname):
3259+
continue
3260+
if super_meta.type.has_base(winner.type.fullname):
3261+
winner = super_meta
3262+
continue
3263+
# metaclass conflict
3264+
winner = None
3265+
break
3266+
3267+
return winner
32583268

32593269
def is_metaclass(self) -> bool:
32603270
return (

test-data/unit/check-abstract.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,7 @@ from abc import abstractmethod, ABCMeta
571571
import typing
572572

573573
class A(metaclass=ABCMeta): pass
574-
class B(object, A): pass \
574+
class B(object, A, metaclass=ABCMeta): pass \
575575
# E: Cannot determine consistent method resolution order (MRO) for "B"
576576

577577
[case testOverloadedAbstractMethod]

test-data/unit/check-classes.test

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

7107-
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
7107+
class ChildOfConflict1(Conflict3): ...
71087108
class ChildOfConflict2(Conflict3, metaclass=CorrectMeta): ...
71097109

71107110
class ConflictingMeta(MyMeta1, MyMeta3): ...
@@ -7113,6 +7113,37 @@ class Conflict4(A1, B, E, metaclass=ConflictingMeta): ... # E: Metaclass confli
71137113
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
71147114
...
71157115

7116+
[case testMetaClassConflictIssue14033]
7117+
class M1(type): pass
7118+
class M2(type): pass
7119+
class Mx(M1, M2): pass
7120+
7121+
class A1(metaclass=M1): pass
7122+
class A2(A1): pass
7123+
7124+
class B1(metaclass=M2): pass
7125+
7126+
class C1(metaclass=Mx): pass
7127+
7128+
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
7129+
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
7130+
7131+
# should not warn again for children
7132+
class ChildOfTestABC(TestABC): pass
7133+
7134+
# no metaclass is assumed if super class has a metaclass conflict
7135+
class ChildOfTestABCMetaMx(TestABC, metaclass=Mx): pass
7136+
class ChildOfTestABCMetaM1(TestABC, metaclass=M1): pass
7137+
7138+
class TestABCMx(A2, B1, C1, metaclass=Mx): pass
7139+
class TestBACMx(B1, A2, C1, metaclass=Mx): pass
7140+
7141+
class TestACB(A2, C1, B1): pass
7142+
class TestBCA(B1, C1, A2): pass
7143+
7144+
class TestCAB(C1, A2, B1): pass
7145+
class TestCBA(C1, B1, A2): pass
7146+
71167147
[case testGenericOverride]
71177148
from typing import Generic, TypeVar, Any
71187149

0 commit comments

Comments
 (0)