Skip to content

Commit 77679f8

Browse files
committed
Fix nondeterministic type checking involving noncommutative join
See python#16979 (comment)
1 parent 9bf5169 commit 77679f8

File tree

2 files changed

+54
-3
lines changed

2 files changed

+54
-3
lines changed

mypy/join.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import mypy.typeops
99
from mypy.expandtype import expand_type
1010
from mypy.maptype import map_instance_to_supertype
11-
from mypy.nodes import CONTRAVARIANT, COVARIANT, INVARIANT, VARIANCE_NOT_READY
11+
from mypy.nodes import CONTRAVARIANT, COVARIANT, INVARIANT, VARIANCE_NOT_READY, TypeInfo
1212
from mypy.state import state
1313
from mypy.subtypes import (
1414
SubtypeContext,
@@ -168,9 +168,19 @@ def join_instances_via_supertype(self, t: Instance, s: Instance) -> ProperType:
168168
# Compute the "best" supertype of t when joined with s.
169169
# The definition of "best" may evolve; for now it is the one with
170170
# the longest MRO. Ties are broken by using the earlier base.
171-
best: ProperType | None = None
171+
172+
# Go over both sets of bases in case there's an explicit Protocol base. This is important
173+
# to ensure commutativity of join (although in cases where both classes have relevant
174+
# Protocol bases this maybe might still not be commutative)
175+
base_types: dict[TypeInfo, None] = {}
172176
for base in t.type.bases:
173-
mapped = map_instance_to_supertype(t, base.type)
177+
base_types[base.type] = None
178+
for base in s.type.bases:
179+
base_types[base.type] = None
180+
181+
best: ProperType | None = None
182+
for base_type in base_types:
183+
mapped = map_instance_to_supertype(t, base_type)
174184
res = self.join_instances(mapped, s)
175185
if best is None or is_better(res, best):
176186
best = res

test-data/unit/check-inference.test

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3886,3 +3886,44 @@ def a4(x: List[str], y: List[Never]) -> None:
38863886
reveal_type(z2) # N: Revealed type is "builtins.list[builtins.object]"
38873887
z1[1].append("asdf") # E: "object" has no attribute "append"
38883888
[builtins fixtures/dict.pyi]
3889+
3890+
3891+
[case testNonDeterminismFromNonCommuativeJoinInvolvingProtocolBaseAndPromotableType]
3892+
# flags: --python-version 3.11
3893+
# Regression test for https://github.com/python/mypy/issues/16979#issuecomment-1982246306
3894+
from __future__ import annotations
3895+
3896+
from typing import Any, Generic, Protocol, TypeVar, overload, cast
3897+
from typing_extensions import Never
3898+
3899+
T = TypeVar("T")
3900+
U = TypeVar("U")
3901+
3902+
class _SupportsCompare(Protocol):
3903+
def __lt__(self, other: Any, /) -> bool:
3904+
return True
3905+
3906+
class Comparable(_SupportsCompare):
3907+
pass
3908+
3909+
class A(Generic[T, U]):
3910+
@overload
3911+
def __init__(self: A[T, T], a: T, b: T, /) -> None: ... # type: ignore[overload-overlap]
3912+
@overload
3913+
def __init__(self: A[T, U], a: T, b: U, /) -> Never: ...
3914+
def __init__(self, *a) -> None: ...
3915+
3916+
comparable: Comparable = Comparable()
3917+
3918+
from typing import _promote
3919+
3920+
class floatlike:
3921+
def __lt__(self, other: floatlike, /) -> bool: ...
3922+
3923+
@_promote(floatlike)
3924+
class intlike:
3925+
def __lt__(self, other: intlike, /) -> bool: ...
3926+
3927+
reveal_type(A(intlike(), comparable)) # N: Revealed type is "__main__.A[__main__._SupportsCompare, __main__._SupportsCompare]"
3928+
[builtins fixtures/tuple.pyi]
3929+
[typing fixtures/typing-medium.pyi]

0 commit comments

Comments
 (0)