Skip to content

Commit 5f6961b

Browse files
authored
Use upper bounds as fallback solutions for inference (#16184)
Fixes #13220 This looks a bit ad-hoc, but it is probably the least disruptive solution possible.
1 parent 4b66fa9 commit 5f6961b

File tree

2 files changed

+43
-0
lines changed

2 files changed

+43
-0
lines changed

mypy/solve.py

+35
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,13 @@ def solve_constraints(
109109
else:
110110
candidate = AnyType(TypeOfAny.special_form)
111111
res.append(candidate)
112+
113+
if not free_vars:
114+
# Most of the validation for solutions is done in applytype.py, but here we can
115+
# quickly test solutions w.r.t. to upper bounds, and use the latter (if possible),
116+
# if solutions are actually not valid (due to poor inference context).
117+
res = pre_validate_solutions(res, original_vars, constraints)
118+
112119
return res, free_vars
113120

114121

@@ -487,3 +494,31 @@ def check_linear(scc: set[TypeVarId], lowers: Bounds, uppers: Bounds) -> bool:
487494
def get_vars(target: Type, vars: list[TypeVarId]) -> set[TypeVarId]:
488495
"""Find type variables for which we are solving in a target type."""
489496
return {tv.id for tv in get_all_type_vars(target)} & set(vars)
497+
498+
499+
def pre_validate_solutions(
500+
solutions: list[Type | None],
501+
original_vars: Sequence[TypeVarLikeType],
502+
constraints: list[Constraint],
503+
) -> list[Type | None]:
504+
"""Check is each solution satisfies the upper bound of the corresponding type variable.
505+
506+
If it doesn't satisfy the bound, check if bound itself satisfies all constraints, and
507+
if yes, use it instead as a fallback solution.
508+
"""
509+
new_solutions: list[Type | None] = []
510+
for t, s in zip(original_vars, solutions):
511+
if s is not None and not is_subtype(s, t.upper_bound):
512+
bound_satisfies_all = True
513+
for c in constraints:
514+
if c.op == SUBTYPE_OF and not is_subtype(t.upper_bound, c.target):
515+
bound_satisfies_all = False
516+
break
517+
if c.op == SUPERTYPE_OF and not is_subtype(c.target, t.upper_bound):
518+
bound_satisfies_all = False
519+
break
520+
if bound_satisfies_all:
521+
new_solutions.append(t.upper_bound)
522+
continue
523+
new_solutions.append(s)
524+
return new_solutions

test-data/unit/check-inference.test

+8
Original file line numberDiff line numberDiff line change
@@ -3542,6 +3542,14 @@ T = TypeVar("T")
35423542
def type_or_callable(value: T, tp: Union[Type[T], Callable[[int], T]]) -> T: ...
35433543
reveal_type(type_or_callable(A("test"), A)) # N: Revealed type is "__main__.A"
35443544

3545+
[case testUpperBoundAsInferenceFallback]
3546+
from typing import Callable, TypeVar, Any, Mapping, Optional
3547+
T = TypeVar("T", bound=Mapping[str, Any])
3548+
def raises(opts: Optional[T]) -> T: pass
3549+
def assertRaises(cb: Callable[..., object]) -> None: pass
3550+
assertRaises(raises) # OK
3551+
[builtins fixtures/dict.pyi]
3552+
35453553
[case testJoinWithAnyFallback]
35463554
from unknown import X # type: ignore[import]
35473555

0 commit comments

Comments
 (0)