Skip to content

Commit

Permalink
Don't duplicate ParamSpec prefixes and properly substitute Paramspecs (
Browse files Browse the repository at this point in the history
…#14677)

Fixes #12734, fixes #12909
  • Loading branch information
A5rocks authored Mar 15, 2023
1 parent d05974b commit 730ba8a
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 14 deletions.
2 changes: 1 addition & 1 deletion mypy/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,7 @@ def visit_callable_type(self, template: CallableType) -> list[Constraint]:
)

# TODO: see above "FIX" comments for param_spec is None case
# TODO: this assume positional arguments
# TODO: this assumes positional arguments
for t, a in zip(prefix.arg_types, cactual_prefix.arg_types):
res.extend(infer_constraints(t, a, neg_op(self.direction)))

Expand Down
24 changes: 22 additions & 2 deletions mypy/expandtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,10 @@ def visit_type_var(self, t: TypeVarType) -> Type:
return repl

def visit_param_spec(self, t: ParamSpecType) -> Type:
repl = get_proper_type(self.variables.get(t.id, t))
# set prefix to something empty so we don't duplicate it
repl = get_proper_type(
self.variables.get(t.id, t.copy_modified(prefix=Parameters([], [], [])))
)
if isinstance(repl, Instance):
# TODO: what does prefix mean in this case?
# TODO: why does this case even happen? Instances aren't plural.
Expand Down Expand Up @@ -369,7 +372,7 @@ def visit_callable_type(self, t: CallableType) -> CallableType:
# must expand both of them with all the argument types,
# kinds and names in the replacement. The return type in
# the replacement is ignored.
if isinstance(repl, CallableType) or isinstance(repl, Parameters):
if isinstance(repl, (CallableType, Parameters)):
# Substitute *args: P.args, **kwargs: P.kwargs
prefix = param_spec.prefix
# we need to expand the types in the prefix, so might as well
Expand All @@ -382,6 +385,23 @@ def visit_callable_type(self, t: CallableType) -> CallableType:
ret_type=t.ret_type.accept(self),
type_guard=(t.type_guard.accept(self) if t.type_guard is not None else None),
)
# TODO: Conceptually, the "len(t.arg_types) == 2" should not be here. However, this
# errors without it. Either figure out how to eliminate this or place an
# explanation for why this is necessary.
elif isinstance(repl, ParamSpecType) and len(t.arg_types) == 2:
# We're substituting one paramspec for another; this can mean that the prefix
# changes. (e.g. sub Concatenate[int, P] for Q)
prefix = repl.prefix
old_prefix = param_spec.prefix

# Check assumptions. I'm not sure what order to place new prefix vs old prefix:
assert not old_prefix.arg_types or not prefix.arg_types

t = t.copy_modified(
arg_types=prefix.arg_types + old_prefix.arg_types + t.arg_types,
arg_kinds=prefix.arg_kinds + old_prefix.arg_kinds + t.arg_kinds,
arg_names=prefix.arg_names + old_prefix.arg_names + t.arg_names,
)

var_arg = t.var_arg()
if var_arg is not None and isinstance(var_arg.typ, UnpackType):
Expand Down
3 changes: 1 addition & 2 deletions mypy/subtypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,8 +388,7 @@ def _is_subtype(self, left: Type, right: Type) -> bool:
return is_proper_subtype(left, right, subtype_context=self.subtype_context)
return is_subtype(left, right, subtype_context=self.subtype_context)

# visit_x(left) means: is left (which is an instance of X) a subtype of
# right?
# visit_x(left) means: is left (which is an instance of X) a subtype of right?

def visit_unbound_type(self, left: UnboundType) -> bool:
# This can be called if there is a bad type annotation. The result probably
Expand Down
13 changes: 4 additions & 9 deletions mypy/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ class TypeOfAny:
# Does this Any come from an error?
from_error: Final = 5
# Is this a type that can't be represented in mypy's type system? For instance, type of
# call to NewType...). Even though these types aren't real Anys, we treat them as such.
# call to NewType(...). Even though these types aren't real Anys, we treat them as such.
# Also used for variables named '_'.
special_form: Final = 6
# Does this Any come from interaction with another Any?
Expand Down Expand Up @@ -1978,20 +1978,15 @@ def param_spec(self) -> ParamSpecType | None:
arg_type = self.arg_types[-2]
if not isinstance(arg_type, ParamSpecType):
return None

# sometimes paramspectypes are analyzed in from mysterious places,
# e.g. def f(prefix..., *args: P.args, **kwargs: P.kwargs) -> ...: ...
prefix = arg_type.prefix
if not prefix.arg_types:
# TODO: confirm that all arg kinds are positional
prefix = Parameters(self.arg_types[:-2], self.arg_kinds[:-2], self.arg_names[:-2])
return ParamSpecType(
arg_type.name,
arg_type.fullname,
arg_type.id,
ParamSpecFlavor.BARE,
arg_type.upper_bound,
prefix=prefix,
)

return arg_type.copy_modified(flavor=ParamSpecFlavor.BARE, prefix=prefix)

def expand_param_spec(
self, c: CallableType | Parameters, no_prefix: bool = False
Expand Down
50 changes: 50 additions & 0 deletions test-data/unit/check-parameter-specification.test
Original file line number Diff line number Diff line change
Expand Up @@ -1471,3 +1471,53 @@ def test(f: Concat[T, ...]) -> None: ...

class Defer: ...
[builtins fixtures/paramspec.pyi]

[case testNoParamSpecDoubling]
# https://github.com/python/mypy/issues/12734
from typing import Callable, ParamSpec
from typing_extensions import Concatenate

P = ParamSpec("P")
Q = ParamSpec("Q")

def foo(f: Callable[P, int]) -> Callable[P, int]:
return f

def bar(f: Callable[Concatenate[str, Q], int]) -> Callable[Concatenate[str, Q], int]:
return foo(f)
[builtins fixtures/paramspec.pyi]

[case testAlreadyExpandedCallableWithParamSpecReplacement]
from typing import Callable, Any, overload
from typing_extensions import Concatenate, ParamSpec

P = ParamSpec("P")

@overload
def command() -> Callable[[Callable[Concatenate[object, object, P], object]], None]: # E: Overloaded function signatures 1 and 2 overlap with incompatible return types
...

@overload
def command(
cls: int = ...,
) -> Callable[[Callable[Concatenate[object, P], object]], None]:
...

def command(
cls: int = 42,
) -> Any:
...
[builtins fixtures/paramspec.pyi]

[case testCopiedParamSpecComparison]
# minimized from https://github.com/python/mypy/issues/12909
from typing import Callable
from typing_extensions import ParamSpec

P = ParamSpec("P")

def identity(func: Callable[P, None]) -> Callable[P, None]: ...

@identity
def f(f: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ...
[builtins fixtures/paramspec.pyi]

0 comments on commit 730ba8a

Please sign in to comment.