diff --git a/mypy/constraints.py b/mypy/constraints.py index 48cc23f74222..1aa6b4200a85 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -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 [] @@ -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( diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index a5b3ae7238a5..47d6f9ad9ff2 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -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'])) @@ -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