From e8ccf89ec17a34cf04df26e520f23d0ec68a7d53 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sat, 4 Jan 2025 23:27:05 +0100 Subject: [PATCH 1/5] Map TypeVar-valued attributes to the checked base --- mypy/checker.py | 2 ++ test-data/unit/check-generic-subtyping.test | 36 +++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/mypy/checker.py b/mypy/checker.py index 3d0f40283606..60855a8cfc2f 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2826,8 +2826,10 @@ class C(B, A[int]): ... # this is unsafe because... ok = is_subtype(first_sig, second_sig, ignore_pos_arg_names=True) elif first_type and second_type: if isinstance(first.node, Var): + first_type = map_type_from_supertype(first_type, ctx, base1) first_type = expand_self_type(first.node, first_type, fill_typevars(ctx)) if isinstance(second.node, Var): + second_type = map_type_from_supertype(second_type, ctx, base2) second_type = expand_self_type(second.node, second_type, fill_typevars(ctx)) ok = is_equivalent(first_type, second_type) if not ok: diff --git a/test-data/unit/check-generic-subtyping.test b/test-data/unit/check-generic-subtyping.test index 90180e0f83f6..f23db34cf9f0 100644 --- a/test-data/unit/check-generic-subtyping.test +++ b/test-data/unit/check-generic-subtyping.test @@ -1065,3 +1065,39 @@ class F(E[T_co], Generic[T_co]): ... # E: Variance of TypeVar "T_co" incompatib class G(Generic[T]): ... class H(G[T_contra], Generic[T_contra]): ... # E: Variance of TypeVar "T_contra" incompatible with variance in parent type + +[case testMultipleInheritanceCompatibleTypeVar] +from typing import Generic, TypeVar + +T = TypeVar("T") +U = TypeVar("U") + +class A(Generic[T]): + x: T + def fn(self, t: T) -> None: ... + +class A2(A[T]): + y: str + z: str + +class B(Generic[T]): + x: T + def fn(self, t: T) -> None: ... + +class C1(A2[str], B[str]): pass +class C2(A2[str], B[int]): pass # E: Definition of "x" in base class "A" is incompatible with definition in base class "B" \ + # E: Definition of "fn" in base class "A" is incompatible with definition in base class "B" +class C3(A2[T], B[T]): pass +class C4(A2[U], B[U]): pass +class C5(A2[U], B[T]): pass # E: Definition of "x" in base class "A" is incompatible with definition in base class "B" \ + # E: Definition of "fn" in base class "A" is incompatible with definition in base class "B" + +class D1(A[str], B[str]): pass +class D2(A[str], B[int]): pass # E: Definition of "x" in base class "A" is incompatible with definition in base class "B" \ + # E: Definition of "fn" in base class "A" is incompatible with definition in base class "B" +class D3(A[T], B[T]): pass +class D4(A[U], B[U]): pass +class D5(A[U], B[T]): pass # E: Definition of "x" in base class "A" is incompatible with definition in base class "B" \ + # E: Definition of "fn" in base class "A" is incompatible with definition in base class "B" + +[builtins fixtures/tuple.pyi] From f04fd933566f964071e5cf3eff811153aaf53437 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sat, 4 Jan 2025 23:43:05 +0100 Subject: [PATCH 2/5] Only compare the most derived base defining name with all others - following bases needn't be compatible --- mypy/checker.py | 22 ++++++++-------- .../unit/check-multiple-inheritance.test | 26 +++++++++++++++++++ 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 60855a8cfc2f..102ada3bfd90 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2733,19 +2733,19 @@ def check_multiple_inheritance(self, typ: TypeInfo) -> None: return # Verify that inherited attributes are compatible. mro = typ.mro[1:] - for i, base in enumerate(mro): + all_names = {name for base in mro for name in base.names} + for name in all_names - typ.names.keys(): # Attributes defined in both the type and base are skipped. # Normal checks for attribute compatibility should catch any problems elsewhere. - non_overridden_attrs = base.names.keys() - typ.names.keys() - for name in non_overridden_attrs: - if is_private(name): - continue - for base2 in mro[i + 1 :]: - # We only need to check compatibility of attributes from classes not - # in a subclass relationship. For subclasses, normal (single inheritance) - # checks suffice (these are implemented elsewhere). - if name in base2.names and base2 not in base.mro: - self.check_compatibility(name, base, base2, typ) + if is_private(name): + continue + # Compare the first base defining a name with the rest. + # Remaining bases may not be pairwise compatible as the first base provides + # the used definition. + i, base = next((i, base) for i, base in enumerate(mro) if name in base.names) + for base2 in mro[i + 1 :]: + if name in base2.names and base2 not in base.mro: + self.check_compatibility(name, base, base2, typ) def determine_type_of_member(self, sym: SymbolTableNode) -> Type | None: if sym.type is not None: diff --git a/test-data/unit/check-multiple-inheritance.test b/test-data/unit/check-multiple-inheritance.test index d03f2e35e1c4..f315e31b4b20 100644 --- a/test-data/unit/check-multiple-inheritance.test +++ b/test-data/unit/check-multiple-inheritance.test @@ -706,3 +706,29 @@ class C34(B3, B4): ... class C41(B4, B1): ... class C42(B4, B2): ... class C43(B4, B3): ... + +[case testMultipleInheritanceExplcitDiamondResolution] +# Adapted from #14279 +class A: + class M: + pass + +class B0(A): + class M(A.M): + pass + +class B1(A): + class M(A.M): + pass + +class C(B0,B1): + class M(B0.M, B1.M): + pass + +class D0(B0): + pass +class D1(B1): + pass + +class D(D0,D1,C): + pass From 4d6bdf44e226efd6c0865deab4de24a6b95c6acf Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sat, 4 Jan 2025 23:59:23 +0100 Subject: [PATCH 3/5] Fix own typing --- mypy/checker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 102ada3bfd90..727ef5ead0fb 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2826,10 +2826,10 @@ class C(B, A[int]): ... # this is unsafe because... ok = is_subtype(first_sig, second_sig, ignore_pos_arg_names=True) elif first_type and second_type: if isinstance(first.node, Var): - first_type = map_type_from_supertype(first_type, ctx, base1) + first_type = get_proper_type(map_type_from_supertype(first_type, ctx, base1)) first_type = expand_self_type(first.node, first_type, fill_typevars(ctx)) if isinstance(second.node, Var): - second_type = map_type_from_supertype(second_type, ctx, base2) + second_type = get_proper_type(map_type_from_supertype(second_type, ctx, base2)) second_type = expand_self_type(second.node, second_type, fill_typevars(ctx)) ok = is_equivalent(first_type, second_type) if not ok: From 503c1459d6d68593989c38361f30ee87caf94c2f Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sun, 5 Jan 2025 00:24:07 +0100 Subject: [PATCH 4/5] Sort names for consistent diagnostics order --- mypy/checker.py | 3 ++- test-data/unit/check-generic-subtyping.test | 17 ++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 727ef5ead0fb..80de4254766b 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -2734,7 +2734,8 @@ def check_multiple_inheritance(self, typ: TypeInfo) -> None: # Verify that inherited attributes are compatible. mro = typ.mro[1:] all_names = {name for base in mro for name in base.names} - for name in all_names - typ.names.keys(): + for name in sorted(all_names - typ.names.keys()): + # Sort for reproducible message order. # Attributes defined in both the type and base are skipped. # Normal checks for attribute compatibility should catch any problems elsewhere. if is_private(name): diff --git a/test-data/unit/check-generic-subtyping.test b/test-data/unit/check-generic-subtyping.test index f23db34cf9f0..03a0654520fd 100644 --- a/test-data/unit/check-generic-subtyping.test +++ b/test-data/unit/check-generic-subtyping.test @@ -1085,19 +1085,18 @@ class B(Generic[T]): def fn(self, t: T) -> None: ... class C1(A2[str], B[str]): pass -class C2(A2[str], B[int]): pass # E: Definition of "x" in base class "A" is incompatible with definition in base class "B" \ - # E: Definition of "fn" in base class "A" is incompatible with definition in base class "B" +class C2(A2[str], B[int]): pass # E: Definition of "fn" in base class "A" is incompatible with definition in base class "B" \ + # E: Definition of "x" in base class "A" is incompatible with definition in base class "B" class C3(A2[T], B[T]): pass class C4(A2[U], B[U]): pass -class C5(A2[U], B[T]): pass # E: Definition of "x" in base class "A" is incompatible with definition in base class "B" \ - # E: Definition of "fn" in base class "A" is incompatible with definition in base class "B" +class C5(A2[U], B[T]): pass # E: Definition of "fn" in base class "A" is incompatible with definition in base class "B" \ + # E: Definition of "x" in base class "A" is incompatible with definition in base class "B" class D1(A[str], B[str]): pass -class D2(A[str], B[int]): pass # E: Definition of "x" in base class "A" is incompatible with definition in base class "B" \ - # E: Definition of "fn" in base class "A" is incompatible with definition in base class "B" +class D2(A[str], B[int]): pass # E: Definition of "fn" in base class "A" is incompatible with definition in base class "B" \ + # E: Definition of "x" in base class "A" is incompatible with definition in base class "B" class D3(A[T], B[T]): pass class D4(A[U], B[U]): pass -class D5(A[U], B[T]): pass # E: Definition of "x" in base class "A" is incompatible with definition in base class "B" \ - # E: Definition of "fn" in base class "A" is incompatible with definition in base class "B" - +class D5(A[U], B[T]): pass # E: Definition of "fn" in base class "A" is incompatible with definition in base class "B" \ + # E: Definition of "x" in base class "A" is incompatible with definition in base class "B" [builtins fixtures/tuple.pyi] From 44ccc383e85f142e3420c12b93bc57a431a95bcc Mon Sep 17 00:00:00 2001 From: Stanislav Terliakov <50529348+sterliakov@users.noreply.github.com> Date: Fri, 10 Jan 2025 16:57:58 +0100 Subject: [PATCH 5/5] FIx a typo in testcase name Co-authored-by: Shantanu <12621235+hauntsaninja@users.noreply.github.com> --- test-data/unit/check-multiple-inheritance.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-multiple-inheritance.test b/test-data/unit/check-multiple-inheritance.test index f315e31b4b20..9cb3bd2e7ca2 100644 --- a/test-data/unit/check-multiple-inheritance.test +++ b/test-data/unit/check-multiple-inheritance.test @@ -707,7 +707,7 @@ class C41(B4, B1): ... class C42(B4, B2): ... class C43(B4, B3): ... -[case testMultipleInheritanceExplcitDiamondResolution] +[case testMultipleInheritanceExplicitDiamondResolution] # Adapted from #14279 class A: class M: