Skip to content

Commit f336726

Browse files
authored
Only consider meta variables in ambiguous "any of" constraints (#18986)
Sometimes our constraints builder needs to express "at least one of these constraints must be true". Sometimes it's trivial, but sometimes they have nothing in common - in that case we fall back to forgetting all of them and (usually) inferring `Never`. This PR extends the fallback handling logic by one more rule: before giving up, we try restricting the constraints to meta variables only. This means that when we try to express `T :> str || U :> str` in the following setup: ``` from typing import TypeVar T = TypeVar("T") U = TypeVar("U") class Foo(Generic[T]): def func(self, arg: T | U) -> None: ... ``` we won't ignore both and fall back to `Never` but will pick `U :> str` instead. The reason for this heuristic is that function-scoped typevars are definitely something we're trying to infer, while everything else is usually out of our control, any other type variable should stay intact. There are other places where constraint builder may emit restrictions on type variables it does not control, handling them consistently everywhere is left as an exercise to the reader. This isn't safe in general case - it might be that another typevar satisfies the constraints but the chosen one doesn't. However, this shouldn't make any existing inference worse: if we used to infer `Never` and it worked, then anything else should almost definitely work as well. See the added testcase for motivation: currently `mypy` fails to handle `Mapping.get` with default without return type context when `Mapping` has a type variable as the second argument. https://mypy-play.net/?mypy=1.15.0&python=3.12&flags=strict&gist=2f9493548082e66b77750655d3a90218 This is a prerequisite of #18976 - that inference change makes the problem solved here occur more often.
1 parent 61b3664 commit f336726

File tree

2 files changed

+36
-2
lines changed

2 files changed

+36
-2
lines changed

mypy/constraints.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,7 @@ def handle_recursive_union(template: UnionType, actual: Type, direction: int) ->
512512
) or infer_constraints(UnionType.make_union(type_var_items), actual, direction)
513513

514514

515-
def any_constraints(options: list[list[Constraint] | None], eager: bool) -> list[Constraint]:
515+
def any_constraints(options: list[list[Constraint] | None], *, eager: bool) -> list[Constraint]:
516516
"""Deduce what we can from a collection of constraint lists.
517517
518518
It's a given that at least one of the lists must be satisfied. A
@@ -547,14 +547,22 @@ def any_constraints(options: list[list[Constraint] | None], eager: bool) -> list
547547
if option in trivial_options:
548548
continue
549549
merged_options.append([merge_with_any(c) for c in option])
550-
return any_constraints(list(merged_options), eager)
550+
return any_constraints(list(merged_options), eager=eager)
551551

552552
# If normal logic didn't work, try excluding trivially unsatisfiable constraint (due to
553553
# upper bounds) from each option, and comparing them again.
554554
filtered_options = [filter_satisfiable(o) for o in options]
555555
if filtered_options != options:
556556
return any_constraints(filtered_options, eager=eager)
557557

558+
# Try harder: if that didn't work, try to strip typevars that aren't meta vars.
559+
# Note this is what we would always do, but unfortunately some callers may not
560+
# set the meta var status correctly (for historical reasons), so we use this as
561+
# a fallback only.
562+
filtered_options = [exclude_non_meta_vars(o) for o in options]
563+
if filtered_options != options:
564+
return any_constraints(filtered_options, eager=eager)
565+
558566
# Otherwise, there are either no valid options or multiple, inconsistent valid
559567
# options. Give up and deduce nothing.
560568
return []
@@ -569,6 +577,7 @@ def filter_satisfiable(option: list[Constraint] | None) -> list[Constraint] | No
569577
"""
570578
if not option:
571579
return option
580+
572581
satisfiable = []
573582
for c in option:
574583
if isinstance(c.origin_type_var, TypeVarType) and c.origin_type_var.values:
@@ -583,6 +592,15 @@ def filter_satisfiable(option: list[Constraint] | None) -> list[Constraint] | No
583592
return satisfiable
584593

585594

595+
def exclude_non_meta_vars(option: list[Constraint] | None) -> list[Constraint] | None:
596+
# If we had an empty list, keep it intact
597+
if not option:
598+
return option
599+
# However, if none of the options actually references meta vars, better remove
600+
# this constraint entirely.
601+
return [c for c in option if c.type_var.is_meta_var()] or None
602+
603+
586604
def is_same_constraints(x: list[Constraint], y: list[Constraint]) -> bool:
587605
for c1 in x:
588606
if not any(is_same_constraint(c1, c2) for c2 in y):

test-data/unit/check-inference.test

+16
Original file line numberDiff line numberDiff line change
@@ -3963,3 +3963,19 @@ def f() -> None:
39633963

39643964
# The type below should not be Any.
39653965
reveal_type(x) # N: Revealed type is "builtins.int"
3966+
3967+
[case testInferenceMappingTypeVarGet]
3968+
from typing import Generic, TypeVar, Union
3969+
3970+
_T = TypeVar("_T")
3971+
_K = TypeVar("_K")
3972+
_V = TypeVar("_V")
3973+
3974+
class Mapping(Generic[_K, _V]):
3975+
def get(self, key: _K, default: Union[_V, _T]) -> Union[_V, _T]: ...
3976+
3977+
def check(mapping: Mapping[str, _T]) -> None:
3978+
ok1 = mapping.get("", "")
3979+
reveal_type(ok1) # N: Revealed type is "Union[_T`-1, builtins.str]"
3980+
ok2: Union[_T, str] = mapping.get("", "")
3981+
[builtins fixtures/tuple.pyi]

0 commit comments

Comments
 (0)