Skip to content
Open
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
64 changes: 57 additions & 7 deletions mypy/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,9 @@ def _infer_constraints(
# checkexpr.has_erased_component().
if not type_state.keep_unions and isinstance(template, UnionType):
template = mypy.typeops.make_simplified_union(template.items, keep_erased=True)
unsimplified_actual: UnionType | None = None
if not type_state.keep_unions and isinstance(actual, UnionType):
unsimplified_actual = actual
actual = mypy.typeops.make_simplified_union(actual.items, keep_erased=True)

# Ignore Any types from the type suggestion engine to avoid them
Expand Down Expand Up @@ -428,13 +430,16 @@ def _infer_constraints(
# We infer constraints eagerly -- try to find constraints for a type
# variable if possible. This seems to help with some real-world
# use cases.
return any_constraints(
[
infer_constraints_if_possible(template, a_item, direction)
for a_item in actual.items
],
eager=True,
)
options = [
infer_constraints_if_possible(template, a_item, direction) for a_item in actual.items
]
if unsimplified_actual is not None:
options.extend(
_options_lost_by_simplification(
template, actual.items, options, unsimplified_actual.items
)
)
return any_constraints(options, eager=True)
if direction == SUPERTYPE_OF and isinstance(template, UnionType):
# When the template is a union, we are okay with leaving some
# type variables indeterminate. This helps with some special
Expand Down Expand Up @@ -476,6 +481,51 @@ def _unwrap_type_type(tp: TypeType | UnionType) -> ProperType:
return UnionType.make_union([cast(TypeType, get_proper_type(o)).item for o in tp.items])


def _options_lost_by_simplification(
template: ProperType,
simplified_items: Sequence[Type],
simplified_options: list[list[Constraint] | None],
original_items: Sequence[Type],
) -> list[list[Constraint] | None]:
"""Constraint options for union items dropped by simplification that we still need.

Simplification drops an item X when it is a proper subtype of another item Y,
but subtyping is not always transitive through the template: ``MutableSequence[str]``
is a subtype of ``Hashable``, yet ``list[str]`` is not hashable even though it is
a subtype of ``MutableSequence[str]``. If the template can match X but none of the
items that absorbed X can, keep X as an extra option so the solver does not commit
to a wrong member (e.g. inferring ``T = bool`` from ``list[bool]`` alone).
"""
# Only recover an option when some simplified item already produced
# constraints: recovery exists to offer an alternative to a (possibly
# wrong) commitment, not to invent constraints where the fallback --
# inferring the literal's type from its items -- already applies. Without
# this, a context like ``Hashable | Sequence[Hashable] |
# dict[Hashable, Hashable]`` would gain a ``T = Hashable`` constraint for
# ``[names]`` and report a new ``list-item`` error where previously only
# an ``assignment`` error was reported (pandas/core/indexes/base.py:2143,
# reported by mypy_primer on #22381).
if not any(option is not None for option in simplified_options):
return []
simplified_ids = {id(get_proper_type(item)) for item in simplified_items}
extra: list[list[Constraint] | None] = []
for item in original_items:
proper_item = get_proper_type(item)
if id(proper_item) in simplified_ids or isinstance(proper_item, UnionType):
continue
absorbers = [
option
for simplified_item, option in zip(simplified_items, simplified_options)
if mypy.subtypes.is_proper_subtype(item, simplified_item, keep_erased_types=True)
]
if not absorbers or any(option is not None for option in absorbers):
continue
option = infer_constraints_if_possible(template, item, SUBTYPE_OF)
if option is not None:
extra.append(option)
return extra


def infer_constraints_if_possible(
template: Type, actual: Type, direction: int
) -> list[Constraint] | None:
Expand Down
28 changes: 28 additions & 0 deletions test-data/unit/check-inference-context.test
Original file line number Diff line number Diff line change
Expand Up @@ -1680,3 +1680,31 @@ x: Union[B, C]
fn: Callable[[Union[A, B]], None]
fn = make(x)
reveal_type(fn) # N: Revealed type is "def (__main__.B | __main__.C | __main__.A)"

[case testListLiteralContextWithUnhashableListAndHashableSequence]
# https://github.com/python/mypy/issues/22342
from typing import Hashable, MutableSequence

def f(k: list[bool] | Hashable | MutableSequence[str]) -> None: ...

f(["a", "b"])
f([True])
f([1.5]) # E: Argument 1 to "f" has incompatible type "list[float]"; expected "list[bool] | Hashable | MutableSequence[str]" \
# N: Following member(s) of "list[float]" have conflicts: \
# N: __hash__: expected "Callable[[], int]", got "None"
[builtins fixtures/list_hashable.pyi]
[typing fixtures/typing-full.pyi]

[case testListLiteralContextUnionWithUnhashableDictMember]
# mypy_primer regression reported on #22381 for the #22342 fix: no simplified
# union item yields constraints here, so no option may be recovered. The list
# literal must keep the previous behaviour (an `assignment` error, which the
# pandas code suppresses with `# type: ignore[assignment]`), not a new
# `list-item` error.
from typing import Hashable, Sequence

def g(names: Hashable | Sequence[Hashable] | dict[Hashable, Hashable]) -> None:
names = [names] # E: Incompatible types in assignment (expression has type "list[Hashable | Sequence[Hashable] | dict[Hashable, Hashable]]", variable has type "Hashable | Sequence[Hashable] | dict[Hashable, Hashable]")

[builtins fixtures/list_hashable.pyi]
[typing fixtures/typing-full.pyi]
47 changes: 47 additions & 0 deletions test-data/unit/fixtures/list_hashable.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Builtins stub used in list tests that need hashability information.

import types
from typing import Any, ClassVar, Generic, Iterable, Iterator, MutableSequence, TypeVar, overload

T = TypeVar('T')

class object:
def __init__(self) -> None: pass
def __eq__(self, other: object) -> bool: pass
def __hash__(self) -> int: pass

class type: pass

class list(MutableSequence[T]):
__hash__: ClassVar[None] # type: ignore[assignment]
@overload
def __init__(self) -> None: pass
@overload
def __init__(self, x: Iterable[T]) -> None: pass
def __iter__(self) -> Iterator[T]: pass
def __len__(self) -> int: pass
def __contains__(self, item: object) -> bool: pass
def __add__(self, x: list[T]) -> list[T]: pass
def __mul__(self, x: int) -> list[T]: pass
def __getitem__(self, x: Any) -> T: pass
def __setitem__(self, x: Any, v: T) -> None: pass
def append(self, x: T) -> None: pass
def extend(self, x: Iterable[T]) -> None: pass

class tuple(Generic[T]): pass
class function: pass
class int:
def __bool__(self) -> bool: pass
class float:
def __bool__(self) -> bool: pass
class str:
def __len__(self) -> bool: pass
class bool(int): pass

property = object() # Dummy definition.

K = TypeVar('K')
V = TypeVar('V')

class dict(Generic[K, V]):
__hash__: ClassVar[None] # type: ignore[assignment]
Loading