Skip to content
Closed
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
22 changes: 16 additions & 6 deletions mypy/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,15 @@ def _infer_constraints(
)
if result:
return result
elif has_recursive_types(template) and not has_recursive_types(actual):
elif (
has_recursive_types(template)
or (
any(isinstance(t, TypeVarType) for t in template.items)
and any(
not isinstance(t, TypeVarType) and has_type_vars(t) for t in template.items
)
)
) and not has_recursive_types(actual):
return handle_recursive_union(template, actual, direction)
return []

Expand Down Expand Up @@ -504,11 +512,13 @@ def merge_with_any(constraint: Constraint) -> Constraint:


def handle_recursive_union(template: UnionType, actual: Type, direction: int) -> list[Constraint]:
# This is a hack to special-case things like Union[T, Inst[T]] in recursive types. Although
# it is quite arbitrary, it is a relatively common pattern, so we should handle it well.
# This function may be called when inferring against such union resulted in different
# constraints for each item. Normally we give up in such case, but here we instead split
# the union in two parts, and try inferring sequentially.
# Special-case unions of the form ``T | Wrapper[T]``, both in recursive type aliases and
# in plain generic function signatures (e.g. ``def f[T](x: T | list[T])``).
# When any_constraints() infers against each union branch independently it may yield
# conflicting results — e.g. ``T >= list[int]`` from the bare-T branch and ``T = int``
# from the ``list[T]`` branch — and gives up, leaving T unconstrained. Instead, we
# split the union into non-TypeVar parts (tried first, since they give precise bounds
# like ``T = int``) and TypeVar parts (used as a fallback).
non_type_var_items = [t for t in template.items if not isinstance(t, TypeVarType)]
type_var_items = [t for t in template.items if isinstance(t, TypeVarType)]
return infer_constraints(
Expand Down
22 changes: 15 additions & 7 deletions test-data/unit/check-inference.test
Original file line number Diff line number Diff line change
Expand Up @@ -873,13 +873,7 @@ def g(x: Union[T, List[T]]) -> List[T]: pass
def h(x: List[str]) -> None: pass
g('a')() # E: "list[str]" not callable

# The next line is a case where there are multiple ways to satisfy a constraint
# involving a Union. Either T = list[str] or T = str would turn out to be valid,
# but mypy doesn't know how to branch on these two options (and potentially have
# to backtrack later) and defaults to T = Never. The result is an
# awkward error message. Either a better error message, or simply accepting the
# call, would be preferable here.
g(['a']) # E: Argument 1 to "g" has incompatible type "list[str]"; expected "list[Never]"
g(['a'])

h(g(['a']))

Expand All @@ -891,6 +885,20 @@ i(b, a, b)
i(a, b, b) # E: Argument 1 to "i" has incompatible type "list[int]"; expected "list[str]"
[builtins fixtures/list.pyi]

[case testUnionInferenceTypeVarWrapper]
# Regression test for https://github.com/python/mypy/issues/21615
# T | Wrapper[T] should infer T from the wrapped branch when the argument matches it.
from typing import TypeVar, Union, List, Dict
T = TypeVar('T')
def f(x: Union[T, List[T]]) -> List[T]: pass
def g(x: Union[T, Dict[str, T]]) -> T: pass

reveal_type(f(1)) # N: Revealed type is "builtins.list[builtins.int]"
reveal_type(f([1])) # N: Revealed type is "builtins.list[builtins.int]"
reveal_type(f(['a'])) # N: Revealed type is "builtins.list[builtins.str]"
reveal_type(g({'k': 1})) # N: Revealed type is "builtins.int"
[builtins fixtures/dict.pyi]

[case testCallableListJoinInference]
from typing import Any, Callable

Expand Down
Loading