From c4baf4ed23b0648198f6e3e32a3d591d850d081c Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sat, 26 Apr 2025 19:09:47 +0200 Subject: [PATCH 1/7] Extend special-case for context-based typevar inference in return position to typevar unions --- mypy/checkexpr.py | 6 +++++- test-data/unit/check-inference-context.test | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index e7c2cba3fc55..1ce21a8b7a1a 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2013,7 +2013,11 @@ def infer_function_type_arguments_using_context( # variables in an expression are inferred at the same time. # (And this is hard, also we need to be careful with lambdas that require # two passes.) - if isinstance(ret_type, TypeVarType): + if ( + isinstance(ret_type, TypeVarType) + or isinstance(ret_type, UnionType) + and all(isinstance(u, TypeVarType) for u in ret_type.items) + ): # Another special case: the return type is a type variable. If it's unrestricted, # we could infer a too general type for the type variable if we use context, # and this could result in confusing and spurious type errors elsewhere. diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test index 17ae6d9934b7..20f534d60978 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 testInferenceContextReturningTypeVarUnion] +from collections.abc import Callable, Iterable +from typing import TypeVar, Union + +_T1 = TypeVar("_T1") +_T2 = TypeVar("_T2") + +def mymin( + iterable: Iterable[_T1], /, *, key: Callable[[_T1], int], default: _T2 +) -> Union[_T1, _T2]: ... + +def check(paths: Iterable[str], key: Callable[[str], int]) -> Union[str, None]: + return mymin(paths, key=key, default=None) +[builtins fixtures/tuple.pyi] From 83315466d5114cd61cbe993c9a17389a91dcaef2 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sat, 26 Apr 2025 19:20:05 +0200 Subject: [PATCH 2/7] Add missing get_proper_type --- mypy/checkexpr.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 1ce21a8b7a1a..fc60cf1befb1 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2013,10 +2013,11 @@ def infer_function_type_arguments_using_context( # variables in an expression are inferred at the same time. # (And this is hard, also we need to be careful with lambdas that require # two passes.) + proper_ret = get_proper_type(ret_type) if ( - isinstance(ret_type, TypeVarType) - or isinstance(ret_type, UnionType) - and all(isinstance(u, TypeVarType) for u in ret_type.items) + isinstance(proper_ret, TypeVarType) + or isinstance(proper_ret, UnionType) + and all(isinstance(get_proper_type(u), TypeVarType) for u in proper_ret.items) ): # Another special case: the return type is a type variable. If it's unrestricted, # we could infer a too general type for the type variable if we use context, From 7fcae0f3ed168eb8044bfb58d6276ebd3b82e385 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sat, 26 Apr 2025 20:28:52 +0200 Subject: [PATCH 3/7] Fix a regression with `mapping.get(key, default_singleton)` --- mypy/checkexpr.py | 3 ++- mypy/typeops.py | 21 +++++++++++++++++++++ test-data/unit/check-inference-context.test | 16 ++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index fc60cf1befb1..09a0518ab837 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -139,6 +139,7 @@ get_all_type_vars, get_type_vars, is_literal_type_like, + is_literal_type_like_or_singleton, make_simplified_union, simple_literal_type, true_only, @@ -2042,7 +2043,7 @@ def infer_function_type_arguments_using_context( # expects_literal(identity(3)) # Should type-check # TODO: we may want to add similar exception if all arguments are lambdas, since # in this case external context is almost everything we have. - if not is_generic_instance(ctx) and not is_literal_type_like(ctx): + if not is_generic_instance(ctx) and not is_literal_type_like_or_singleton(ctx): return callable.copy_modified() args = infer_type_arguments( callable.variables, ret_type, erased_ctx, skip_unsatisfied=True diff --git a/mypy/typeops.py b/mypy/typeops.py index bcf946900563..a98df7c3f3f0 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -1005,6 +1005,27 @@ def is_literal_type_like(t: Type | None) -> bool: return False +def is_literal_type_like_or_singleton(t: Type | None) -> bool: + """Returns 'true' if the given type context is potentially either a LiteralType, + a Union of LiteralType, a singleton (or a union thereof), or something similar. + """ + t = get_proper_type(t) + if t is None: + return False + elif isinstance(t, LiteralType): + return True + elif t.is_singleton_type(): + return True + elif isinstance(t, UnionType): + return any(is_literal_type_like_or_singleton(item) for item in t.items) + elif isinstance(t, TypeVarType): + return is_literal_type_like_or_singleton(t.upper_bound) or any( + is_literal_type_like_or_singleton(item) for item in t.values + ) + else: + return False + + def is_singleton_type(typ: Type) -> bool: """Returns 'true' if this type is a "singleton type" -- if there exists exactly only one runtime value associated with this type. diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test index 20f534d60978..de45c45c87a1 100644 --- a/test-data/unit/check-inference-context.test +++ b/test-data/unit/check-inference-context.test @@ -1510,3 +1510,19 @@ def mymin( def check(paths: Iterable[str], key: Callable[[str], int]) -> Union[str, None]: return mymin(paths, key=key, default=None) [builtins fixtures/tuple.pyi] + +[case testInferenceContextLiteralInstance] +from collections.abc import Callable, Iterable, Mapping +from enum import Enum +from typing import Final, Generic, Literal, TypeVar, Union + +class Opt(Enum): + MISSING = "MISSING" + +MISSING: Final[Literal[Opt.MISSING]] = Opt.MISSING +_T1 = TypeVar("_T1") + +def check(mapping: Mapping[str, _T1]) -> None: + res: Union[_T1, Opt] = mapping.get("", MISSING) +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] From 43c097c4e8ee7889ba256a0d161368cc0cf6ab61 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sat, 26 Apr 2025 20:29:36 +0200 Subject: [PATCH 4/7] Switch to detecting *any* typevar in union in return position - `T | str` is no less problematic --- mypy/checkexpr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 09a0518ab837..f8ebb9fa7f7f 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2018,7 +2018,7 @@ def infer_function_type_arguments_using_context( if ( isinstance(proper_ret, TypeVarType) or isinstance(proper_ret, UnionType) - and all(isinstance(get_proper_type(u), TypeVarType) for u in proper_ret.items) + and any(isinstance(get_proper_type(u), TypeVarType) for u in proper_ret.items) ): # Another special case: the return type is a type variable. If it's unrestricted, # we could infer a too general type for the type variable if we use context, From e3771159078378eb1a2cfc047c959305e0ef40d9 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sat, 26 Apr 2025 21:34:31 +0200 Subject: [PATCH 5/7] Revert attempt to fix Mapping.get --- mypy/checkexpr.py | 3 +-- mypy/typeops.py | 21 --------------------- test-data/unit/check-inference-context.test | 14 +++++--------- 3 files changed, 6 insertions(+), 32 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index f8ebb9fa7f7f..b525eaeb988c 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -139,7 +139,6 @@ get_all_type_vars, get_type_vars, is_literal_type_like, - is_literal_type_like_or_singleton, make_simplified_union, simple_literal_type, true_only, @@ -2043,7 +2042,7 @@ def infer_function_type_arguments_using_context( # expects_literal(identity(3)) # Should type-check # TODO: we may want to add similar exception if all arguments are lambdas, since # in this case external context is almost everything we have. - if not is_generic_instance(ctx) and not is_literal_type_like_or_singleton(ctx): + if not is_generic_instance(ctx) and not is_literal_type_like(ctx): return callable.copy_modified() args = infer_type_arguments( callable.variables, ret_type, erased_ctx, skip_unsatisfied=True diff --git a/mypy/typeops.py b/mypy/typeops.py index a98df7c3f3f0..bcf946900563 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -1005,27 +1005,6 @@ def is_literal_type_like(t: Type | None) -> bool: return False -def is_literal_type_like_or_singleton(t: Type | None) -> bool: - """Returns 'true' if the given type context is potentially either a LiteralType, - a Union of LiteralType, a singleton (or a union thereof), or something similar. - """ - t = get_proper_type(t) - if t is None: - return False - elif isinstance(t, LiteralType): - return True - elif t.is_singleton_type(): - return True - elif isinstance(t, UnionType): - return any(is_literal_type_like_or_singleton(item) for item in t.items) - elif isinstance(t, TypeVarType): - return is_literal_type_like_or_singleton(t.upper_bound) or any( - is_literal_type_like_or_singleton(item) for item in t.values - ) - else: - return False - - def is_singleton_type(typ: Type) -> bool: """Returns 'true' if this type is a "singleton type" -- if there exists exactly only one runtime value associated with this type. diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test index de45c45c87a1..0c068f551e11 100644 --- a/test-data/unit/check-inference-context.test +++ b/test-data/unit/check-inference-context.test @@ -1511,18 +1511,14 @@ def check(paths: Iterable[str], key: Callable[[str], int]) -> Union[str, None]: return mymin(paths, key=key, default=None) [builtins fixtures/tuple.pyi] -[case testInferenceContextLiteralInstance] -from collections.abc import Callable, Iterable, Mapping -from enum import Enum -from typing import Final, Generic, Literal, TypeVar, Union - -class Opt(Enum): - MISSING = "MISSING" +[case testInferenceContextMappingGet-xfail] +from collections.abc import Mapping +from typing import TypeVar, Union -MISSING: Final[Literal[Opt.MISSING]] = Opt.MISSING _T1 = TypeVar("_T1") def check(mapping: Mapping[str, _T1]) -> None: - res: Union[_T1, Opt] = mapping.get("", MISSING) + fail1 = mapping.get("", "") + fail2: Union[_T1, str] = mapping.get("", "") [builtins fixtures/tuple.pyi] [typing fixtures/typing-full.pyi] From 3f04d8ee0ad7807867ea952036930d65165df5e2 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Sun, 27 Apr 2025 04:26:16 +0200 Subject: [PATCH 6/7] Revert to `all()` - inferring for unions of typevar and something seem to be helpful --- mypy/checkexpr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index b525eaeb988c..fc60cf1befb1 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -2017,7 +2017,7 @@ def infer_function_type_arguments_using_context( if ( isinstance(proper_ret, TypeVarType) or isinstance(proper_ret, UnionType) - and any(isinstance(get_proper_type(u), TypeVarType) for u in proper_ret.items) + and all(isinstance(get_proper_type(u), TypeVarType) for u in proper_ret.items) ): # Another special case: the return type is a type variable. If it's unrestricted, # we could infer a too general type for the type variable if we use context, From f1e4dfc60f60ecd64d073dcc519939d5fea023e7 Mon Sep 17 00:00:00 2001 From: STerliakov Date: Wed, 7 May 2025 00:33:10 +0200 Subject: [PATCH 7/7] Remove xfail test - fixed in previous PR --- test-data/unit/check-inference-context.test | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test index 0c068f551e11..20f534d60978 100644 --- a/test-data/unit/check-inference-context.test +++ b/test-data/unit/check-inference-context.test @@ -1510,15 +1510,3 @@ def mymin( def check(paths: Iterable[str], key: Callable[[str], int]) -> Union[str, None]: return mymin(paths, key=key, default=None) [builtins fixtures/tuple.pyi] - -[case testInferenceContextMappingGet-xfail] -from collections.abc import Mapping -from typing import TypeVar, Union - -_T1 = TypeVar("_T1") - -def check(mapping: Mapping[str, _T1]) -> None: - fail1 = mapping.get("", "") - fail2: Union[_T1, str] = mapping.get("", "") -[builtins fixtures/tuple.pyi] -[typing fixtures/typing-full.pyi]