Skip to content

Commit bdca65a

Browse files
committed
Try the "typed MRO" approach instead
1 parent 6b09c52 commit bdca65a

File tree

3 files changed

+69
-54
lines changed

3 files changed

+69
-54
lines changed

mypy/checker.py

Lines changed: 33 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2744,23 +2744,44 @@ def check_multiple_inheritance(self, typ: TypeInfo) -> None:
27442744
if len(typ.bases) <= 1:
27452745
# No multiple inheritance.
27462746
return
2747+
27472748
# Verify that inherited attributes are compatible.
2748-
bases = typ.bases
2749-
all_names = [{n for p in b.type.mro for n in p.names} for b in bases]
2750-
for i, base in enumerate(bases):
2749+
# Construct a "typed" MRO that follows regular MRO order, but includes instances
2750+
# parametrized with their generic args.
2751+
# This detects e.g. `class A(Mapping[int, str], Iterable[str])` correctly.
2752+
# For each MRO entry, include it parametrized according to each base inheriting
2753+
# from it.
2754+
typed_mro = [
2755+
map_instance_to_supertype(base, parent)
2756+
for parent in typ.mro[1:]
2757+
for base in typ.bases
2758+
if parent in base.type.mro
2759+
]
2760+
# If the first MRO entry is compatible with everything following, we don't need
2761+
# (and shouldn't) compare further pairs
2762+
# (see testMultipleInheritanceExplcitDiamondResolution)
2763+
seen_names = set()
2764+
for i, base in enumerate(typed_mro):
27512765
# Attributes defined in both the type and base are skipped.
27522766
# Normal checks for attribute compatibility should catch any problems elsewhere.
27532767
# Sort for consistent messages order.
2754-
non_overridden_attrs = sorted(all_names[i] - typ.names.keys())
2768+
non_overridden_attrs = sorted(typed_mro[i].type.names - typ.names.keys())
27552769
for name in non_overridden_attrs:
27562770
if is_private(name):
27572771
continue
2758-
for j, base2 in enumerate(bases[i + 1 :], i + 1):
2772+
if name in seen_names:
2773+
continue
2774+
for j, base2 in enumerate(typed_mro[i + 1 :], i + 1):
27592775
# We only need to check compatibility of attributes from classes not
27602776
# in a subclass relationship. For subclasses, normal (single inheritance)
27612777
# checks suffice (these are implemented elsewhere).
2762-
if name in all_names[j] and base.type != base2.type:
2778+
if name in base2.type.names and not is_subtype(
2779+
base, base2, ignore_promotions=True
2780+
):
2781+
# If base1 already inherits from base2 with correct type args,
2782+
# we have reported errors if any. Avoid reporting them again.
27632783
self.check_compatibility(name, base, base2, typ)
2784+
seen_names.add(name)
27642785

27652786
def determine_type_of_member(self, node: SymbolNode) -> Type | None:
27662787
if isinstance(node, FuncBase):
@@ -2810,48 +2831,17 @@ class C(B, A[int]): ... # this is unsafe because...
28102831
# __init__ and friends can be incompatible -- it's a special case.
28112832
return
28122833

2813-
if is_subtype(base1, base2, ignore_promotions=True):
2814-
# If base1 already inherits from base2 with correct type args,
2815-
# we have reported errors if any. Avoid reporting them again.
2816-
return
2817-
28182834
first_type = first_node = None
28192835
second_type = second_node = None
28202836
orig_var = ctx.get(name)
28212837

28222838
if orig_var is not None and orig_var.node is not None:
2823-
if (b1type := base1.type.get_containing_type_info(name)) is not None:
2824-
base1 = map_instance_to_supertype(base1, b1type)
2825-
first_type, first_node = self.attribute_type_from_base(
2826-
orig_var.node, base1.type, base1
2827-
)
2828-
2829-
if (b2type := base2.type.get_containing_type_info(name)) is not None:
2830-
base2 = map_instance_to_supertype(base2, b2type)
2831-
second_type, second_node = self.attribute_type_from_base(
2832-
orig_var.node, base2.type, base2
2833-
)
2834-
2835-
# Fix the order. We iterate over the explicit bases, which means we may
2836-
# end up with the following structure:
2837-
# class A:
2838-
# def fn(self, x: int) -> None: ...
2839-
# class B(A): ...
2840-
# class C(A):
2841-
# def fn(self, x: int|str) -> None: ...
2842-
# class D(B, C): ...
2843-
# Here D.fn will actually be dispatched to C.fn which is assignable to A.fn,
2844-
# but without this fixup we'd check A.fn against C.fn instead.
2845-
# See testMultipleInheritanceTransitive in check-multiple-inheritance.test
2846-
if (
2847-
b1type is not None
2848-
and b2type is not None
2849-
and ctx.mro.index(b1type) > ctx.mro.index(b2type)
2850-
):
2851-
b1type, b2type = b2type, b1type
2852-
base1, base2 = base2, base1
2853-
first_type, second_type = second_type, first_type
2854-
first_node, second_node = second_node, first_node
2839+
first_type, first_node = self.attribute_type_from_base(
2840+
orig_var.node, base1.type, base1
2841+
)
2842+
second_type, second_node = self.attribute_type_from_base(
2843+
orig_var.node, base2.type, base2
2844+
)
28552845

28562846
# TODO: use more principled logic to decide is_subtype() vs is_equivalent().
28572847
# We should rely on mutability of superclass node, not on types being Callable.

test-data/unit/check-generic-subtyping.test

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,14 +1090,3 @@ class C5(A2[U], B[T]): pass # E: Definition of "fn" in base class "A" is incomp
10901090
# E: Definition of "x" in base class "A" is incompatible with definition in base class "B"
10911091

10921092
[builtins fixtures/tuple.pyi]
1093-
1094-
[case testMultipleInheritanceCompatErrorPropagation]
1095-
class A:
1096-
foo: bytes
1097-
class B(A):
1098-
foo: str # type: ignore[assignment]
1099-
1100-
class Ok(B, A): pass
1101-
1102-
class C(A): pass
1103-
class Ok2(B, C): pass

test-data/unit/check-multiple-inheritance.test

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,3 +713,39 @@ class B(A): ...
713713
class C(A):
714714
def fn(self, x: "int | str") -> None: ...
715715
class D(B, C): ...
716+
717+
[case testMultipleInheritanceCompatErrorPropagation]
718+
class A:
719+
foo: bytes
720+
class B(A):
721+
foo: str # type: ignore[assignment]
722+
723+
class Ok(B, A): pass
724+
725+
class C(A): pass
726+
class Ok2(B, C): pass
727+
728+
[case testMultipleInheritanceExplcitDiamondResolution]
729+
class A:
730+
class M:
731+
pass
732+
733+
class B0(A):
734+
class M(A.M):
735+
pass
736+
737+
class B1(A):
738+
class M(A.M):
739+
pass
740+
741+
class C(B0,B1):
742+
class M(B0.M, B1.M):
743+
pass
744+
745+
class D0(B0):
746+
pass
747+
class D1(B1):
748+
pass
749+
750+
class D(D0,D1,C):
751+
pass

0 commit comments

Comments
 (0)