Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix nondeterministic type checking involving noncommutative join #18402

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions mypy/join.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions test-data/unit/check-inference.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Loading