From 18aba1376b9edfd83974ecb661be848614d2f15b Mon Sep 17 00:00:00 2001 From: STerliakov Date: Mon, 28 Apr 2025 02:00:06 +0200 Subject: [PATCH 1/8] Try ignoring "foreign" typevars from any_constraints if they are otherwise ambiguous --- mypy/constraints.py | 15 +++++++++++++-- test-data/unit/check-inference-context.test | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 079f6536ee20..1dcb83500bc0 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -69,6 +69,8 @@ SUBTYPE_OF: Final = 0 SUPERTYPE_OF: Final = 1 +PT = [] + class Constraint: """A representation of a type constraint. @@ -127,6 +129,9 @@ def infer_constraints_for_callable( param_spec_arg_names = [] param_spec_arg_kinds = [] + own_vars = {t.id for t in callee.variables} + PT.append(own_vars) + incomplete_star_mapping = False for i, actuals in enumerate(formal_to_actual): # TODO: isn't this `enumerate(arg_types)`? for actual in actuals: @@ -273,6 +278,7 @@ def infer_constraints_for_callable( if any(isinstance(v, ParamSpecType) for v in callee.variables): # As a perf optimization filter imprecise constraints only when we can have them. constraints = filter_imprecise_kinds(constraints) + PT.pop() return constraints @@ -512,7 +518,7 @@ def handle_recursive_union(template: UnionType, actual: Type, direction: int) -> ) or infer_constraints(UnionType.make_union(type_var_items), actual, direction) -def any_constraints(options: list[list[Constraint] | None], eager: bool) -> list[Constraint]: +def any_constraints(options: list[list[Constraint] | None], *, eager: bool) -> list[Constraint]: """Deduce what we can from a collection of constraint lists. It's a given that at least one of the lists must be satisfied. A @@ -547,7 +553,7 @@ def any_constraints(options: list[list[Constraint] | None], eager: bool) -> list if option in trivial_options: continue merged_options.append([merge_with_any(c) for c in option]) - return any_constraints(list(merged_options), eager) + return any_constraints(list(merged_options), eager=eager) # If normal logic didn't work, try excluding trivially unsatisfiable constraint (due to # upper bounds) from each option, and comparing them again. @@ -569,8 +575,13 @@ def filter_satisfiable(option: list[Constraint] | None) -> list[Constraint] | No """ if not option: return option + + own = PT[-1] if PT else None + satisfiable = [] for c in option: + if own is not None and c.op == SUPERTYPE_OF and c.type_var not in own: + continue if isinstance(c.origin_type_var, TypeVarType) and c.origin_type_var.values: if any( mypy.subtypes.is_subtype(c.target, value) for value in c.origin_type_var.values diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test index 17ae6d9934b7..28a26bf60d07 100644 --- a/test-data/unit/check-inference-context.test +++ b/test-data/unit/check-inference-context.test @@ -1495,3 +1495,18 @@ def g(b: Optional[str]) -> None: z: Callable[[], str] = lambda: reveal_type(b) # N: Revealed type is "builtins.str" f2(lambda: reveal_type(b)) # N: Revealed type is "builtins.str" lambda: reveal_type(b) # N: Revealed type is "builtins.str" + +[case testInferenceContextMappingGet] +from typing import Generic, TypeVar, Union + +_T = TypeVar("_T") +_K = TypeVar("_K") +_V = TypeVar("_V") + +class Mapping(Generic[_K, _V]): + def get(self, key: _K, default: Union[_V, _T]) -> Union[_V, _T]: ... + +def check(mapping: Mapping[str, _T]) -> None: + fail1 = mapping.get("", "") + fail2: Union[_T, str] = mapping.get("", "") +[builtins fixtures/tuple.pyi] From 9ae2a23078bdc586bc77ab2b0b6d799351d3f885 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Mon, 28 Apr 2025 02:10:31 +0200 Subject: [PATCH 2/8] Move to shared type_state --- mypy/constraints.py | 8 +++----- mypy/typestate.py | 4 ++++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 1dcb83500bc0..fa3635275f6e 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -69,8 +69,6 @@ SUBTYPE_OF: Final = 0 SUPERTYPE_OF: Final = 1 -PT = [] - class Constraint: """A representation of a type constraint. @@ -130,7 +128,7 @@ def infer_constraints_for_callable( param_spec_arg_kinds = [] own_vars = {t.id for t in callee.variables} - PT.append(own_vars) + type_state.constraints_targets.append(own_vars) incomplete_star_mapping = False for i, actuals in enumerate(formal_to_actual): # TODO: isn't this `enumerate(arg_types)`? @@ -278,7 +276,7 @@ def infer_constraints_for_callable( if any(isinstance(v, ParamSpecType) for v in callee.variables): # As a perf optimization filter imprecise constraints only when we can have them. constraints = filter_imprecise_kinds(constraints) - PT.pop() + type_state.constraints_targets.pop() return constraints @@ -576,7 +574,7 @@ def filter_satisfiable(option: list[Constraint] | None) -> list[Constraint] | No if not option: return option - own = PT[-1] if PT else None + own = type_state.constraints_targets[-1] if type_state.constraints_targets else None satisfiable = [] for c in option: diff --git a/mypy/typestate.py b/mypy/typestate.py index 574618668477..4e863f6da323 100644 --- a/mypy/typestate.py +++ b/mypy/typestate.py @@ -91,6 +91,9 @@ class TypeState: _assuming_proper: Final[list[tuple[Type, Type]]] # Ditto for inference of generic constraints against recursive type aliases. inferring: Final[list[tuple[Type, Type]]] + # When building constraints for a callable, prefer these variables when we encounter + # ambiguous set in `any_constraints` + constraints_targets: Final[list[set[TypeVarId]]] # Whether to use joins or unions when solving constraints, see checkexpr.py for details. infer_unions: bool # Whether to use new type inference algorithm that can infer polymorphic types. @@ -112,6 +115,7 @@ def __init__(self) -> None: self._assuming = [] self._assuming_proper = [] self.inferring = [] + self.constraints_targets = [] self.infer_unions = False self.infer_polymorphic = False From e8d632770de84da027d1ff80349212f4fc1b7084 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Mon, 28 Apr 2025 02:56:44 +0200 Subject: [PATCH 3/8] Direction doesn't matter --- mypy/constraints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index fa3635275f6e..826b5e658722 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -578,7 +578,7 @@ def filter_satisfiable(option: list[Constraint] | None) -> list[Constraint] | No satisfiable = [] for c in option: - if own is not None and c.op == SUPERTYPE_OF and c.type_var not in own: + if own is not None and c.type_var not in own: continue if isinstance(c.origin_type_var, TypeVarType) and c.origin_type_var.values: if any( From 7af21d50808c0427b5a9dc825a8209cc570c9988 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Mon, 28 Apr 2025 02:58:03 +0200 Subject: [PATCH 4/8] Move test case where it belongs --- test-data/unit/check-inference-context.test | 15 --------------- test-data/unit/check-inference.test | 15 +++++++++++++++ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test index 28a26bf60d07..17ae6d9934b7 100644 --- a/test-data/unit/check-inference-context.test +++ b/test-data/unit/check-inference-context.test @@ -1495,18 +1495,3 @@ def g(b: Optional[str]) -> None: z: Callable[[], str] = lambda: reveal_type(b) # N: Revealed type is "builtins.str" f2(lambda: reveal_type(b)) # N: Revealed type is "builtins.str" lambda: reveal_type(b) # N: Revealed type is "builtins.str" - -[case testInferenceContextMappingGet] -from typing import Generic, TypeVar, Union - -_T = TypeVar("_T") -_K = TypeVar("_K") -_V = TypeVar("_V") - -class Mapping(Generic[_K, _V]): - def get(self, key: _K, default: Union[_V, _T]) -> Union[_V, _T]: ... - -def check(mapping: Mapping[str, _T]) -> None: - fail1 = mapping.get("", "") - fail2: Union[_T, str] = mapping.get("", "") -[builtins fixtures/tuple.pyi] diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 42b5a05ab39a..2749d19bd79c 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -3963,3 +3963,18 @@ def f() -> None: # The type below should not be Any. reveal_type(x) # N: Revealed type is "builtins.int" + +[case testInferenceMappingTypeVarGet] +from typing import Generic, TypeVar, Union + +_T = TypeVar("_T") +_K = TypeVar("_K") +_V = TypeVar("_V") + +class Mapping(Generic[_K, _V]): + def get(self, key: _K, default: Union[_V, _T]) -> Union[_V, _T]: ... + +def check(mapping: Mapping[str, _T]) -> None: + ok1 = mapping.get("", "") + ok2: Union[_T, str] = mapping.get("", "") +[builtins fixtures/tuple.pyi] From 71e449647c6aaad9bf58885954b137b26c3b3da8 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Mon, 28 Apr 2025 03:43:53 +0200 Subject: [PATCH 5/8] Move this check to a separate state, see anyio failure --- mypy/constraints.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 826b5e658722..eb16e83c0fef 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -559,6 +559,11 @@ def any_constraints(options: list[list[Constraint] | None], *, eager: bool) -> l if filtered_options != options: return any_constraints(filtered_options, eager=eager) + # Try harder: if that didn't work, try to strip typevars not owned by current function. + filtered_options = [filter_own(o) for o in options] + if filtered_options != options: + return any_constraints(filtered_options, eager=eager) + # Otherwise, there are either no valid options or multiple, inconsistent valid # options. Give up and deduce nothing. return [] @@ -574,12 +579,8 @@ def filter_satisfiable(option: list[Constraint] | None) -> list[Constraint] | No if not option: return option - own = type_state.constraints_targets[-1] if type_state.constraints_targets else None - satisfiable = [] for c in option: - if own is not None and c.type_var not in own: - continue if isinstance(c.origin_type_var, TypeVarType) and c.origin_type_var.values: if any( mypy.subtypes.is_subtype(c.target, value) for value in c.origin_type_var.values @@ -592,6 +593,16 @@ def filter_satisfiable(option: list[Constraint] | None) -> list[Constraint] | No return satisfiable +def filter_own(option: list[Constraint] | None) -> list[Constraint] | None: + """Keep only constraints that reference type vars local to current function, if any.""" + + if not option or not type_state.constraints_targets: + return option + own_vars = type_state.constraints_targets[-1] + + return [c for c in option if c.type_var in own_vars] or None + + def is_same_constraints(x: list[Constraint], y: list[Constraint]) -> bool: for c1 in x: if not any(is_same_constraint(c1, c2) for c2 in y): From 9090192a4376e23d95269713d5f794016385df3a Mon Sep 17 00:00:00 2001 From: STerliakov Date: Mon, 5 May 2025 23:32:29 +0200 Subject: [PATCH 6/8] Remove shared state, `is_meta_var` is sufficient --- mypy/constraints.py | 21 ++++++++------------- mypy/typestate.py | 4 ---- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index eb16e83c0fef..b7ee7baba26f 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -127,9 +127,6 @@ def infer_constraints_for_callable( param_spec_arg_names = [] param_spec_arg_kinds = [] - own_vars = {t.id for t in callee.variables} - type_state.constraints_targets.append(own_vars) - incomplete_star_mapping = False for i, actuals in enumerate(formal_to_actual): # TODO: isn't this `enumerate(arg_types)`? for actual in actuals: @@ -276,7 +273,6 @@ def infer_constraints_for_callable( if any(isinstance(v, ParamSpecType) for v in callee.variables): # As a perf optimization filter imprecise constraints only when we can have them. constraints = filter_imprecise_kinds(constraints) - type_state.constraints_targets.pop() return constraints @@ -559,8 +555,8 @@ def any_constraints(options: list[list[Constraint] | None], *, eager: bool) -> l if filtered_options != options: return any_constraints(filtered_options, eager=eager) - # Try harder: if that didn't work, try to strip typevars not owned by current function. - filtered_options = [filter_own(o) for o in options] + # Try harder: if that didn't work, try to strip typevars that aren't meta vars. + filtered_options = [exclude_non_meta_vars(o) for o in options] if filtered_options != options: return any_constraints(filtered_options, eager=eager) @@ -593,14 +589,13 @@ def filter_satisfiable(option: list[Constraint] | None) -> list[Constraint] | No return satisfiable -def filter_own(option: list[Constraint] | None) -> list[Constraint] | None: - """Keep only constraints that reference type vars local to current function, if any.""" - - if not option or not type_state.constraints_targets: +def exclude_non_meta_vars(option: list[Constraint] | None) -> list[Constraint] | None: + # If we had an empty list, keep it intact + if not option: return option - own_vars = type_state.constraints_targets[-1] - - return [c for c in option if c.type_var in own_vars] or None + # However, if none of the options actually references meta vars, better remove + # this constraint entirely. + return [c for c in option if c.type_var.is_meta_var()] or None def is_same_constraints(x: list[Constraint], y: list[Constraint]) -> bool: diff --git a/mypy/typestate.py b/mypy/typestate.py index 4e863f6da323..574618668477 100644 --- a/mypy/typestate.py +++ b/mypy/typestate.py @@ -91,9 +91,6 @@ class TypeState: _assuming_proper: Final[list[tuple[Type, Type]]] # Ditto for inference of generic constraints against recursive type aliases. inferring: Final[list[tuple[Type, Type]]] - # When building constraints for a callable, prefer these variables when we encounter - # ambiguous set in `any_constraints` - constraints_targets: Final[list[set[TypeVarId]]] # Whether to use joins or unions when solving constraints, see checkexpr.py for details. infer_unions: bool # Whether to use new type inference algorithm that can infer polymorphic types. @@ -115,7 +112,6 @@ def __init__(self) -> None: self._assuming = [] self._assuming_proper = [] self.inferring = [] - self.constraints_targets = [] self.infer_unions = False self.infer_polymorphic = False From 516cef73ffa567dd0636d0bfd4afaf3d4b86be6c Mon Sep 17 00:00:00 2001 From: STerliakov Date: Tue, 6 May 2025 13:27:16 +0200 Subject: [PATCH 7/8] Add reveal_type in test --- test-data/unit/check-inference.test | 1 + 1 file changed, 1 insertion(+) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 2749d19bd79c..25565946158e 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -3976,5 +3976,6 @@ class Mapping(Generic[_K, _V]): def check(mapping: Mapping[str, _T]) -> None: ok1 = mapping.get("", "") + reveal_type(ok1) # N: Revealed type is "Union[_T`-1, builtins.str]" ok2: Union[_T, str] = mapping.get("", "") [builtins fixtures/tuple.pyi] From e4126864af181aeade1ee4ccf10b144888ce39b9 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Tue, 6 May 2025 13:27:56 +0200 Subject: [PATCH 8/8] Add comment from review --- mypy/constraints.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mypy/constraints.py b/mypy/constraints.py index b7ee7baba26f..8e7a30e05ffb 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -556,6 +556,9 @@ def any_constraints(options: list[list[Constraint] | None], *, eager: bool) -> l return any_constraints(filtered_options, eager=eager) # Try harder: if that didn't work, try to strip typevars that aren't meta vars. + # Note this is what we would always do, but unfortunately some callers may not + # set the meta var status correctly (for historical reasons), so we use this as + # a fallback only. filtered_options = [exclude_non_meta_vars(o) for o in options] if filtered_options != options: return any_constraints(filtered_options, eager=eager)