diff --git a/mypy/constraints.py b/mypy/constraints.py index c84447e209b0..48dacdb51fd8 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -586,9 +586,16 @@ def visit_callable_type(self, template: CallableType) -> List[Constraint]: else: # TODO: Direction # TODO: Deal with arguments that come before param spec ones? + # TODO: check the prefixes match + prefix = param_spec.prefix + prefix_len = len(prefix.arg_types) res.append(Constraint(param_spec.id, SUBTYPE_OF, - cactual.copy_modified(ret_type=NoneType()))) + cactual.copy_modified( + arg_types=cactual.arg_types[prefix_len:], + arg_kinds=cactual.arg_kinds[prefix_len:], + arg_names=cactual.arg_names[prefix_len:], + ret_type=NoneType()))) template_ret_type, cactual_ret_type = template.ret_type, cactual.ret_type if template.type_guard is not None: diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 23498e6957a3..4b26ad784885 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -797,6 +797,41 @@ def analyze_callable_args_for_paramspec( fallback=fallback, ) + def analyze_callable_args_for_concatenate( + self, + callable_args: Type, + ret_type: Type, + fallback: Instance, + ) -> Optional[CallableType]: + """Construct a 'Callable[C, RET]', where C is Concatenate[..., P], return None if we cannot.""" + if not isinstance(callable_args, UnboundType): + return None + sym = self.lookup_qualified(callable_args.name, callable_args) + if sym is None: + return None + if sym.node.fullname not in ("typing_extensions.Concatenate", "typing.Concatenate"): + return None + + tvar_def = self.anal_type(callable_args, allow_param_spec=True) + if not isinstance(tvar_def, ParamSpecType): + return None + + # TODO: Use tuple[...] or Mapping[..] instead? + obj = self.named_type('builtins.object') + # ick, CallableType should take ParamSpecType + prefix = tvar_def.prefix + return CallableType( + [*prefix.arg_types, + ParamSpecType(tvar_def.name, tvar_def.fullname, tvar_def.id, ParamSpecFlavor.ARGS, + upper_bound=obj, prefix=tvar_def.prefix), + ParamSpecType(tvar_def.name, tvar_def.fullname, tvar_def.id, ParamSpecFlavor.KWARGS, + upper_bound=obj)], + [*prefix.arg_kinds, nodes.ARG_STAR, nodes.ARG_STAR2], + [*prefix.arg_names, None, None], + ret_type=ret_type, + fallback=fallback, + ) + def analyze_callable_type(self, t: UnboundType) -> Type: fallback = self.named_type('builtins.function') if len(t.args) == 0: @@ -828,6 +863,10 @@ def analyze_callable_type(self, t: UnboundType) -> Type: callable_args, ret_type, fallback + ) or self.analyze_callable_args_for_concatenate( + callable_args, + ret_type, + fallback ) if maybe_ret is None: # Callable[?, RET] (where ? is something invalid) diff --git a/test-data/unit/check-parameter-specification.test b/test-data/unit/check-parameter-specification.test index fb6dd4e7728f..6e4010132ff0 100644 --- a/test-data/unit/check-parameter-specification.test +++ b/test-data/unit/check-parameter-specification.test @@ -15,8 +15,9 @@ def foo1(x: Callable[P, int]) -> Callable[P, str]: ... def foo2(x: P) -> P: ... # E: Invalid location for ParamSpec "P" \ # N: You can use ParamSpec as the first argument to Callable, e.g., 'Callable[P, int]' -# TODO(PEP612): uncomment once we have support for Concatenate -# def foo3(x: Concatenate[int, P]) -> int: ... $ E: Invalid location for Concatenate +# TODO: Better error message +def foo3(x: Concatenate[int, P]) -> int: ... # E: Invalid location for ParamSpec "P" \ + # N: You can use ParamSpec as the first argument to Callable, e.g., 'Callable[P, int]' def foo4(x: List[P]) -> None: ... # E: Invalid location for ParamSpec "P" \ # N: You can use ParamSpec as the first argument to Callable, e.g., 'Callable[P, int]' @@ -455,5 +456,82 @@ reveal_type(kb(n)) # N: Revealed type is "__main__.Z[[builtins.str]]" \ # TODO(PEP612): fancy "aesthetic" syntax defined in PEP # n2: Z[bytes] # -# reveal_type(kb(n2)) # N: Revealed type is "__main__.Z[[builtins.str]]" +# reveal_type(kb(n2)) $ N: Revealed type is "__main__.Z[[builtins.str]]" [builtins fixtures/tuple.pyi] + +[case testParamSpecConcatenateFromPep] +from typing_extensions import ParamSpec, Concatenate +from typing import Callable, TypeVar, Generic + +P = ParamSpec("P") +R = TypeVar("R") + +# CASE 1 +class Request: + ... + +def with_request(f: Callable[Concatenate[Request, P], R]) -> Callable[P, R]: + def inner(*args: P.args, **kwargs: P.kwargs) -> R: + return f(Request(), *args, **kwargs) + return inner + +@with_request +def takes_int_str(request: Request, x: int, y: str) -> int: + # use request + return x + 7 + +reveal_type(takes_int_str) # N: Revealed type is "def (x: builtins.int, y: builtins.str) -> builtins.int*" + +takes_int_str(1, "A") # Accepted +takes_int_str("B", 2) # E: Argument 1 to "takes_int_str" has incompatible type "str"; expected "int" \ + # E: Argument 2 to "takes_int_str" has incompatible type "int"; expected "str" + +# CASE 2 +T = TypeVar("T") +P_2 = ParamSpec("P_2") + +class X(Generic[T, P]): + f: Callable[P, int] + x: T + +def f1(x: X[int, P_2]) -> str: ... # Accepted +def f2(x: X[int, Concatenate[int, P_2]]) -> str: ... # Accepted +def f3(x: X[int, [int, bool]]) -> str: ... # Accepted +# Is ellipsis allowed by PEP? This shows up: +# def f4(x: X[int, ...]) -> str: ... # Accepted +# TODO: this is not rejected: +# def f5(x: X[int, int]) -> str: ... # Rejected + +# CASE 3 +def bar(x: int, *args: bool) -> int: ... +def add(x: Callable[P, int]) -> Callable[Concatenate[str, P], bool]: ... + +reveal_type(add(bar)) # N: Revealed type is "def (builtins.str, x: builtins.int, *args: builtins.bool) -> builtins.bool" + +def remove(x: Callable[Concatenate[int, P], int]) -> Callable[P, bool]: ... + +reveal_type(remove(bar)) # N: Revealed type is "def (*args: builtins.bool) -> builtins.bool" + +def transform( + x: Callable[Concatenate[int, P], int] +) -> Callable[Concatenate[str, P], bool]: ... + +# In the PEP, "__a" appears. What is that? Autogenerated names? To what spec? +reveal_type(transform(bar)) # N: Revealed type is "def (builtins.str, *args: builtins.bool) -> builtins.bool" + +# CASE 4 +def expects_int_first(x: Callable[Concatenate[int, P], int]) -> None: ... + +@expects_int_first # E: Argument 1 to "expects_int_first" has incompatible type "Callable[[str], int]"; expected "Callable[[int], int]" +def one(x: str) -> int: ... + +@expects_int_first # E: Argument 1 to "expects_int_first" has incompatible type "Callable[[NamedArg(int, 'x')], int]"; expected "Callable[[int], int]" +def two(*, x: int) -> int: ... + +@expects_int_first # E: Argument 1 to "expects_int_first" has incompatible type "Callable[[KwArg(int)], int]"; expected "Callable[[int], int]" +def three(**kwargs: int) -> int: ... + +@expects_int_first # Accepted +def four(*args: int) -> int: ... +[builtins fixtures/tuple.pyi] +[builtins fixtures/dict.pyi]