diff --git a/mypy/join.py b/mypy/join.py index 166434f58f8d..c87ff1cf5b15 100644 --- a/mypy/join.py +++ b/mypy/join.py @@ -8,7 +8,7 @@ import mypy.typeops from mypy.expandtype import expand_type from mypy.maptype import map_instance_to_supertype -from mypy.nodes import CONTRAVARIANT, COVARIANT, INVARIANT, VARIANCE_NOT_READY +from mypy.nodes import CONTRAVARIANT, COVARIANT, INVARIANT, VARIANCE_NOT_READY, TypeInfo from mypy.state import state from mypy.subtypes import ( SubtypeContext, @@ -168,9 +168,19 @@ def join_instances_via_supertype(self, t: Instance, s: Instance) -> ProperType: # Compute the "best" supertype of t when joined with s. # The definition of "best" may evolve; for now it is the one with # the longest MRO. Ties are broken by using the earlier base. - best: ProperType | None = None + + # Go over both sets of bases in case there's an explicit Protocol base. This is important + # to ensure commutativity of join (although in cases where both classes have relevant + # Protocol bases this maybe might still not be commutative) + base_types: dict[TypeInfo, None] = {} for base in t.type.bases: - mapped = map_instance_to_supertype(t, base.type) + base_types[base.type] = None + for base in s.type.bases: + base_types[base.type] = None + + best: ProperType | None = None + for base_type in base_types: + mapped = map_instance_to_supertype(t, base_type) res = self.join_instances(mapped, s) if best is None or is_better(res, best): best = res diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 0da1c092efe8..657797ffe5cc 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -3886,3 +3886,44 @@ def a4(x: List[str], y: List[Never]) -> None: reveal_type(z2) # N: Revealed type is "builtins.list[builtins.object]" z1[1].append("asdf") # E: "object" has no attribute "append" [builtins fixtures/dict.pyi] + + +[case testNonDeterminismFromNonCommuativeJoinInvolvingProtocolBaseAndPromotableType] +# flags: --python-version 3.11 +# Regression test for https://github.com/python/mypy/issues/16979#issuecomment-1982246306 +from __future__ import annotations + +from typing import Any, Generic, Protocol, TypeVar, overload, cast +from typing_extensions import Never + +T = TypeVar("T") +U = TypeVar("U") + +class _SupportsCompare(Protocol): + def __lt__(self, other: Any, /) -> bool: + return True + +class Comparable(_SupportsCompare): + pass + +class A(Generic[T, U]): + @overload + def __init__(self: A[T, T], a: T, b: T, /) -> None: ... # type: ignore[overload-overlap] + @overload + def __init__(self: A[T, U], a: T, b: U, /) -> Never: ... + def __init__(self, *a) -> None: ... + +comparable: Comparable = Comparable() + +from typing import _promote + +class floatlike: + def __lt__(self, other: floatlike, /) -> bool: ... + +@_promote(floatlike) +class intlike: + def __lt__(self, other: intlike, /) -> bool: ... + +reveal_type(A(intlike(), comparable)) # N: Revealed type is "__main__.A[__main__._SupportsCompare, __main__._SupportsCompare]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-medium.pyi]