Skip to content

Commit

Permalink
Fix attribute type resolution with multiple inheritance (#18415)
Browse files Browse the repository at this point in the history
Fixes #18268. Fixes #9319. Fixes #14279. Fixes #9031.

Supersedes #18270 as requested by @ilevkivskyi.

This PR introduces two changes:

* Add missing `map_type_from_supertype` when checking generic attributes
* Only compare the first base defining a name to all following in MRO -
others are not necessarily pairwise compatible.

---------

Co-authored-by: Shantanu <[email protected]>
  • Loading branch information
sterliakov and hauntsaninja authored Jan 11, 2025
1 parent 106f714 commit d86b1e5
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 11 deletions.
25 changes: 14 additions & 11 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2733,19 +2733,20 @@ 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 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.
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:
Expand Down Expand Up @@ -2826,8 +2827,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 = 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 = 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:
Expand Down
35 changes: 35 additions & 0 deletions test-data/unit/check-generic-subtyping.test
Original file line number Diff line number Diff line change
Expand Up @@ -1065,3 +1065,38 @@ 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 "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 "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 "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 "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]
26 changes: 26 additions & 0 deletions test-data/unit/check-multiple-inheritance.test
Original file line number Diff line number Diff line change
Expand Up @@ -706,3 +706,29 @@ class C34(B3, B4): ...
class C41(B4, B1): ...
class C42(B4, B2): ...
class C43(B4, B3): ...

[case testMultipleInheritanceExplicitDiamondResolution]
# 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

0 comments on commit d86b1e5

Please sign in to comment.