Skip to content

Commit

Permalink
Tests for Concatenate
Browse files Browse the repository at this point in the history
  • Loading branch information
A5rocks committed Dec 26, 2021
1 parent d202d1e commit 9ed9830
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 4 deletions.
9 changes: 8 additions & 1 deletion mypy/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
39 changes: 39 additions & 0 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
84 changes: 81 additions & 3 deletions test-data/unit/check-parameter-specification.test
Original file line number Diff line number Diff line change
Expand Up @@ -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]'
Expand Down Expand Up @@ -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]

0 comments on commit 9ed9830

Please sign in to comment.