From 1f1074d0c73c6c1215a59b73cbde4c6c47bb0148 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 25 Sep 2023 23:30:28 +0100 Subject: [PATCH 1/4] Use type variable bound when it appears as actual during inference --- mypy/constraints.py | 5 +++++ test-data/unit/check-inference.test | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/mypy/constraints.py b/mypy/constraints.py index 0524e38f9643..2a547fca4d31 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -373,6 +373,11 @@ def _infer_constraints( return handle_recursive_union(template, actual, direction) return [] + if isinstance(actual, TypeVarType) and not actual.id.is_meta_var(): + # Unless template is also a type variable (that is handled above), using the upper + # bound for inference will usually give better result for actual that is a type variable. + actual = get_proper_type(actual.upper_bound) + # Remaining cases are handled by ConstraintBuilderVisitor. return template.accept(ConstraintBuilderVisitor(actual, direction, skip_neg_op)) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index f9a4d58c74af..49ada5d5e718 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -3686,3 +3686,15 @@ def g(*args: str) -> None: pass reveal_type(f(g)) # N: Revealed type is "Tuple[Never, Never]" \ # E: Argument 1 to "f" has incompatible type "Callable[[VarArg(str)], None]"; expected "Call[Never]" [builtins fixtures/list.pyi] + +[case testInferenceAgainstTypeVarActualBound] +from typing import Callable, TypeVar + +T = TypeVar("T") +S = TypeVar("S") +def test(f: Callable[[T], S]) -> Callable[[T], S]: ... + +F = TypeVar("F", bound=Callable[..., object]) +def dec(f: F) -> F: + reveal_type(test(f)) # N: Revealed type is "def (Any) -> builtins.object" + return f From 0d23e85e3a28a47d681dac95d0ccc47f392c3b2f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Tue, 26 Sep 2023 23:38:02 +0100 Subject: [PATCH 2/4] Unions require specific ordering of steps --- mypy/constraints.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index 2a547fca4d31..b61d882da3c4 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -328,6 +328,18 @@ def _infer_constraints( if isinstance(template, TypeVarType): return [Constraint(template, direction, actual)] + if ( + isinstance(actual, TypeVarType) + and not actual.id.is_meta_var() + and direction == SUPERTYPE_OF + ): + # Unless template is also a type variable (or a union that contains one), using the upper + # bound for inference will usually give better result for actual that is a type variable. + if not isinstance(template, UnionType) or not any( + isinstance(t, TypeVarType) for t in template.items + ): + actual = get_proper_type(actual.upper_bound) + # Now handle the case of either template or actual being a Union. # For a Union to be a subtype of another type, every item of the Union # must be a subtype of that type, so concatenate the constraints. @@ -373,11 +385,6 @@ def _infer_constraints( return handle_recursive_union(template, actual, direction) return [] - if isinstance(actual, TypeVarType) and not actual.id.is_meta_var(): - # Unless template is also a type variable (that is handled above), using the upper - # bound for inference will usually give better result for actual that is a type variable. - actual = get_proper_type(actual.upper_bound) - # Remaining cases are handled by ConstraintBuilderVisitor. return template.accept(ConstraintBuilderVisitor(actual, direction, skip_neg_op)) From 67d965cb1c27a58756ea50d5d888437660d8d109 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Tue, 26 Sep 2023 16:26:51 -0700 Subject: [PATCH 3/4] add regression test --- test-data/unit/check-inference.test | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index 49ada5d5e718..ea46b11727fa 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -3698,3 +3698,24 @@ F = TypeVar("F", bound=Callable[..., object]) def dec(f: F) -> F: reveal_type(test(f)) # N: Revealed type is "def (Any) -> builtins.object" return f + +[case testInferenceAgainstTypeVarActualUnionBound] +from typing import Protocol, Generic, TypeVar, Union, overload + +_T_co = TypeVar("_T_co", covariant=True) + +class SupportsFoo(Protocol[_T_co]): + def foo(self) -> _T_co: ... + +class A: + def foo(self) -> A: ... + +class B: + def foo(self) -> B: ... + +def foo(f: SupportsFoo[_T_co]) -> _T_co: ... + +ABT = TypeVar("ABT", bound=Union[A, B]) + +def simpler(k: ABT): + foo(k) From ce79dbce354823136665ed5b6e0ba43b099689f6 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 27 Sep 2023 23:33:16 +0100 Subject: [PATCH 4/4] Cleanup the test --- test-data/unit/check-inference.test | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/test-data/unit/check-inference.test b/test-data/unit/check-inference.test index d10422d1aa63..870417ca87c0 100644 --- a/test-data/unit/check-inference.test +++ b/test-data/unit/check-inference.test @@ -3700,27 +3700,23 @@ def dec(f: F) -> F: return f [case testInferenceAgainstTypeVarActualUnionBound] -from typing import Protocol, Generic, TypeVar, Union, overload +from typing import Protocol, TypeVar, Union -_T_co = TypeVar("_T_co", covariant=True) - -class SupportsFoo(Protocol[_T_co]): - def foo(self) -> _T_co: ... +T_co = TypeVar("T_co", covariant=True) +class SupportsFoo(Protocol[T_co]): + def foo(self) -> T_co: ... class A: def foo(self) -> A: ... - class B: def foo(self) -> B: ... -def foo(f: SupportsFoo[_T_co]) -> _T_co: ... +def foo(f: SupportsFoo[T_co]) -> T_co: ... ABT = TypeVar("ABT", bound=Union[A, B]) - def simpler(k: ABT): foo(k) - [case testInferenceWorksWithEmptyCollectionsNested] from typing import List, TypeVar, NoReturn T = TypeVar('T')