From b5266bc1a28cd67e9bdbb0992cccd0065b9bb64d Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 18 Aug 2023 12:29:25 +0100 Subject: [PATCH 1/2] Support self-types containing ParamSpec --- mypy/typeops.py | 32 +++++++++-------------- test-data/unit/check-selftype.test | 42 ++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/mypy/typeops.py b/mypy/typeops.py index d746ea701fde..1400c9d12b70 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -303,7 +303,7 @@ class B(A): pass return cast(F, func) self_param_type = get_proper_type(func.arg_types[0]) - variables: Sequence[TypeVarLikeType] = [] + variables: Sequence[TypeVarLikeType] if func.variables and supported_self_type(self_param_type): from mypy.infer import infer_type_arguments @@ -312,46 +312,40 @@ class B(A): pass original_type = erase_to_bound(self_param_type) original_type = get_proper_type(original_type) - all_ids = func.type_var_ids() + # Find which of method type variables appear in the type of "self". + self_ids = {tv.id for tv in get_all_type_vars(self_param_type)} + self_vars = [tv for tv in func.variables if tv.id in self_ids] + + # Solve for these type arguments using the actual class or instance type. typeargs = infer_type_arguments( - func.variables, self_param_type, original_type, is_supertype=True + self_vars, self_param_type, original_type, is_supertype=True ) if ( is_classmethod - # TODO: why do we need the extra guards here? and any(isinstance(get_proper_type(t), UninhabitedType) for t in typeargs) and isinstance(original_type, (Instance, TypeVarType, TupleType)) ): - # In case we call a classmethod through an instance x, fallback to type(x) + # In case we call a classmethod through an instance x, fallback to type(x). typeargs = infer_type_arguments( - func.variables, self_param_type, TypeType(original_type), is_supertype=True + self_vars, self_param_type, TypeType(original_type), is_supertype=True ) - ids = [tid for tid in all_ids if any(tid == t.id for t in get_type_vars(self_param_type))] - + # Update the method signature with the solutions found. # Technically, some constrains might be unsolvable, make them . to_apply = [t if t is not None else UninhabitedType() for t in typeargs] - - def expand(target: Type) -> Type: - return expand_type(target, {id: to_apply[all_ids.index(id)] for id in ids}) - - arg_types = [expand(x) for x in func.arg_types[1:]] - ret_type = expand(func.ret_type) - variables = [v for v in func.variables if v.id not in ids] + func = expand_type(func, {tv.id: arg for tv, arg in zip(self_vars, to_apply)}) + variables = [v for v in func.variables if v not in self_vars] else: - arg_types = func.arg_types[1:] - ret_type = func.ret_type variables = func.variables original_type = get_proper_type(original_type) if isinstance(original_type, CallableType) and original_type.is_type_obj(): original_type = TypeType.make_normalized(original_type.ret_type) res = func.copy_modified( - arg_types=arg_types, + arg_types=func.arg_types[1:], arg_kinds=func.arg_kinds[1:], arg_names=func.arg_names[1:], variables=variables, - ret_type=ret_type, bound_args=[original_type], ) return cast(F, res) diff --git a/test-data/unit/check-selftype.test b/test-data/unit/check-selftype.test index d366e7c33799..77d2d519214a 100644 --- a/test-data/unit/check-selftype.test +++ b/test-data/unit/check-selftype.test @@ -1973,3 +1973,45 @@ class B(A): reveal_type(self.x.extra) # N: Revealed type is "builtins.int" reveal_type(self.xs[0].extra) # N: Revealed type is "builtins.int" [builtins fixtures/list.pyi] + +[case testSelfTypesWithParamSpecExtract] +from typing import Any, Callable, Generic, TypeVar +from typing_extensions import ParamSpec + +P = ParamSpec("P") +F = TypeVar("F", bound=Callable[..., Any]) +class Example(Generic[F]): + def __init__(self, fn: F) -> None: + ... + def __call__(self: Example[Callable[P, Any]], *args: P.args, **kwargs: P.kwargs) -> None: + ... + +def test_fn(a: int, b: str) -> None: + ... + +example = Example(test_fn) +example() # E: Missing positional arguments "a", "b" in call to "__call__" of "Example" +example(1, "b") # OK +[builtins fixtures/list.pyi] + +[case testSelfTypesWithParamSpecInfer] +from typing import TypeVar, Protocol, Type, Callable +from typing_extensions import ParamSpec + +R = TypeVar("R", covariant=True) +P = ParamSpec("P") +class AsyncP(Protocol[P]): + def meth(self, *args: P.args, **kwargs: P.kwargs) -> None: + ... + +class Async: + @classmethod + def async_func(cls: Type[AsyncP[P]]) -> Callable[P, int]: + ... + +class Add(Async): + def meth(self, x: int, y: int) -> None: ... + +reveal_type(Add.async_func()) # N: Revealed type is "def (x: builtins.int, y: builtins.int) -> builtins.int" +reveal_type(Add().async_func()) # N: Revealed type is "def (x: builtins.int, y: builtins.int) -> builtins.int" +[builtins fixtures/classmethod.pyi] From 1094efa3da17680fcbc305bb8f77797fb0dae78e Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 18 Aug 2023 13:20:36 +0100 Subject: [PATCH 2/2] Update mypy/typeops.py Co-authored-by: Alex Waygood --- mypy/typeops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/typeops.py b/mypy/typeops.py index 1400c9d12b70..f9bf474bdb70 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -331,7 +331,7 @@ class B(A): pass ) # Update the method signature with the solutions found. - # Technically, some constrains might be unsolvable, make them . + # Technically, some constraints might be unsolvable, make them . to_apply = [t if t is not None else UninhabitedType() for t in typeargs] func = expand_type(func, {tv.id: arg for tv, arg in zip(self_vars, to_apply)}) variables = [v for v in func.variables if v not in self_vars]