From b8d656f2786912da530a2dbf5a81ccd8d598e796 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Tue, 27 Aug 2024 15:54:16 +0200 Subject: [PATCH] Fix metaclass resolution algorithm --- mypy/checker.py | 23 +++++++-------------- mypy/nodes.py | 28 +++++++++++++++++-------- test-data/unit/check-abstract.test | 2 +- test-data/unit/check-classes.test | 33 +++++++++++++++++++++++++++++- 4 files changed, 59 insertions(+), 27 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index db65660bbfbd..6f844950222c 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2819,23 +2819,14 @@ def check_metaclass_compatibility(self, typ: TypeInfo) -> None: ): return # Reasonable exceptions from this check - metaclasses = [ - entry.metaclass_type - for entry in typ.mro[1:-1] - if entry.metaclass_type - and not is_named_instance(entry.metaclass_type, "builtins.type") - ] - if not metaclasses: - return - if typ.metaclass_type is not None and all( - is_subtype(typ.metaclass_type, meta) for meta in metaclasses + if typ.metaclass_type is None and any( + base.type.metaclass_type is not None for base in typ.bases ): - return - self.fail( - "Metaclass conflict: the metaclass of a derived class must be " - "a (non-strict) subclass of the metaclasses of all its bases", - typ, - ) + self.fail( + "Metaclass conflict: the metaclass of a derived class must be " + "a (non-strict) subclass of the metaclasses of all its bases", + typ, + ) def visit_import_from(self, node: ImportFrom) -> None: self.check_import(node) diff --git a/mypy/nodes.py b/mypy/nodes.py index 4a5c7240fa83..af2678828a00 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -3246,15 +3246,25 @@ def calculate_metaclass_type(self) -> mypy.types.Instance | None: return declared if self._fullname == "builtins.type": return mypy.types.Instance(self, []) - candidates = [ - s.declared_metaclass - for s in self.mro - if s.declared_metaclass is not None and s.declared_metaclass.type is not None - ] - for c in candidates: - if all(other.type in c.type.mro for other in candidates): - return c - return None + + winner = declared + for super_class in self.mro[1:]: + super_meta = super_class.declared_metaclass + if super_meta is None or super_meta.type is None: + continue + if winner is None: + winner = super_meta + continue + if winner.type.has_base(super_meta.type.fullname): + continue + if super_meta.type.has_base(winner.type.fullname): + winner = super_meta + continue + # metaclass conflict + winner = None + break + + return winner def is_metaclass(self) -> bool: return ( diff --git a/test-data/unit/check-abstract.test b/test-data/unit/check-abstract.test index 3b0b9c520b75..d87fe3a0dc15 100644 --- a/test-data/unit/check-abstract.test +++ b/test-data/unit/check-abstract.test @@ -571,7 +571,7 @@ from abc import abstractmethod, ABCMeta import typing class A(metaclass=ABCMeta): pass -class B(object, A): pass \ +class B(object, A, metaclass=ABCMeta): pass \ # E: Cannot determine consistent method resolution order (MRO) for "B" [case testOverloadedAbstractMethod] diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 82208d27df41..5167c8d33561 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -7104,7 +7104,7 @@ class Conflict1(A1, B, E): ... # E: Metaclass conflict: the metaclass of a deri 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 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): ... class ChildOfConflict2(Conflict3, metaclass=CorrectMeta): ... class ConflictingMeta(MyMeta1, MyMeta3): ... @@ -7113,6 +7113,37 @@ class Conflict4(A1, B, E, metaclass=ConflictingMeta): ... # E: Metaclass confli 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 ... +[case testMetaClassConflictIssue14033] +class M1(type): pass +class M2(type): pass +class Mx(M1, M2): pass + +class A1(metaclass=M1): pass +class A2(A1): pass + +class B1(metaclass=M2): pass + +class C1(metaclass=Mx): pass + +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 +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 + +# should not warn again for children +class ChildOfTestABC(TestABC): pass + +# no metaclass is assumed if super class has a metaclass conflict +class ChildOfTestABCMetaMx(TestABC, metaclass=Mx): pass +class ChildOfTestABCMetaM1(TestABC, metaclass=M1): pass + +class TestABCMx(A2, B1, C1, metaclass=Mx): pass +class TestBACMx(B1, A2, C1, metaclass=Mx): pass + +class TestACB(A2, C1, B1): pass +class TestBCA(B1, C1, A2): pass + +class TestCAB(C1, A2, B1): pass +class TestCBA(C1, B1, A2): pass + [case testGenericOverride] from typing import Generic, TypeVar, Any