8000 Don't duplicate ParamSpec prefixes and properly substitute Paramspecs… · python/mypy@730ba8a · GitHub
[go: up one dir, main page]

Skip to content

Commit 730ba8a

Browse files
authored
Don't duplicate ParamSpec prefixes and properly substitute Paramspecs (#14677)
Fixes #12734, fixes #12909
1 parent d05974b commit 730ba8a

File tree

5 files changed

+78
-14
lines changed

5 files changed

+78
-14
lines changed

mypy/constraints.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -949,7 +949,7 @@ def visit_callable_type(self, template: CallableType) -> list[Constraint]:
949949
)
950950

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

mypy/expandtype.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,10 @@ def visit_type_var(self, t: TypeVarType) -> Type:
246246
return repl
247247

248248
def visit_param_spec(self, t: ParamSpecType) -> Type:
249-
repl = get_proper_type(self.variables.get(t.id, t))
249+
# set prefix to something empty so we don't duplicate it
250+
repl = get_proper_type(
251+
self.variables.get(t.id, t.copy_modified(prefix=Parameters([], [], [])))
252+
)
250253
if isinstance(repl, Instance):
251254
# TODO: what does prefix mean in this case?
252255
# TODO: why does this case even happen? Instances aren't plural.
@@ -369,7 +372,7 @@ def visit_callable_type(self, t: CallableType) -> CallableType:
369372
# must expand both of them with all the argument types,
370373
# kinds and names in the replacement. The return type in
371374
# the replacement is ignored.
372-
if isinstance(repl, CallableType) or isinstance(repl, Parameters):
375+
if isinstance(repl, (CallableType, Parameters)):
373376
# Substitute *args: P.args, **kwargs: P.kwargs
374377
prefix = param_spec.prefix
375378
# we need to expand the types in the prefix, so might as well
@@ -382,6 +385,23 @@ def visit_callable_type(self, t: CallableType) -> CallableType:
382385
ret_type=t.ret_type.accept(self),
383386
type_guard=(t.type_guard.accept(self) if t.type_guard is not None else None),
384387
)
388+
# TODO: Conceptually, the "len(t.arg_types) == 2" should not be here. However, this
389+
# errors without it. Either figure out how to eliminate this or place an
390+
# explanation for why this is necessary.
391+
elif isinstance(repl, ParamSpecType) and len(t.arg_types) == 2:
392+
# We're substituting one paramspec for another; this can mean that the prefix
393+
# changes. (e.g. sub Concatenate[int, P] for Q)
394+
prefix = repl.prefix
395+
old_prefix = param_spec.prefix
396+
397+
# Check assumptions. I'm not sure what order to place new prefix vs old prefix:
398+
assert not old_prefix.arg_types or not prefix.arg_types
399+
400+
t = t.copy_modified(
401+
arg_types=prefix.arg_types + old_prefix.arg_types + t.arg_types,
402+
arg_kinds=prefix.arg_kinds + old_prefix.arg_kinds + t.arg_kinds,
403+
arg_names=prefix.arg_names + old_prefix.arg_names + t.arg_names,
404+
)
385405

386406
var_arg = t.var_arg()
387407
if var_arg is not None and isinstance(var_arg.typ, UnpackType):

mypy/subtypes.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -388,8 +388,7 @@ def _is_subtype(self, left: Type, right: Type) -> bool:
388388
return is_proper_subtype(left, right, subtype_context=self.subtype_context)
389389
return is_subtype(left, right, subtype_context=self.subtype_context)
390390

391-
# visit_x(left) means: is left (which is an instance of X) a subtype of
392-
# right?
391+
# visit_x(left) means: is left (which is an instance of X) a subtype of right?
393392

394393
def visit_unbound_type(self, left: UnboundType) -> bool:
395394
# This can be called if there is a bad type annotation. The result probably

mypy/types.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ class TypeOfAny:
183183
# Does this Any come from an error?
184184
from_error: Final = 5
185185
# Is this a type that can't be represented in mypy's type system? For instance, type of
186-
# call to NewType...). Even though these types aren't real Anys, we treat them as such.
186+
# call to NewType(...). Even though these types aren't real Anys, we treat them as such.
187187
# Also used for variables named '_'.
188188
special_form: Final = 6
189189
# Does this Any come from interaction with another Any?
@@ -1978,20 +1978,15 @@ def param_spec(self) -> ParamSpecType | None:
19781978
arg_type = self.arg_types[-2]
19791979
if not isinstance(arg_type, ParamSpecType):
19801980
return None
1981+
19811982
# sometimes paramspectypes are analyzed in from mysterious places,
19821983
# e.g. def f(prefix..., *args: P.args, **kwargs: P.kwargs) -> ...: ...
19831984
prefix = arg_type.prefix
19841985
if not prefix.arg_types:
19851986
# TODO: confirm that all arg kinds are positional
19861987
prefix = Parameters(self.arg_types[:-2], self.arg_kinds[:-2], self.arg_names[:-2])
1987-
return ParamSpecType(
1988-
arg_type.name,
1989-
arg_type.fullname,
1990-
arg_type.id,
1991-
ParamSpecFlavor.BARE,
1992-
arg_type.upper_bound,
1993-
prefix=prefix,
1994-
)
1988+
1989+
return arg_type.copy_modified(flavor=ParamSpecFlavor.BARE, prefix=prefix)
19951990

19961991
def expand_param_spec(
19971992
self, c: CallableType | Parameters, no_prefix: bool = False

test-data/unit/check-parameter-specification.test

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,3 +1471,53 @@ def test(f: Concat[T, ...]) -> None: ...
14711471

14721472
class Defer: ...
14731473
[builtins fixtures/paramspec.pyi]
1474+
1475+
[case testNoParamSpecDoubling]
1476+
# https://github.com/python/mypy/issues/12734
1477+
from typing import Callable, ParamSpec
1478+
from typing_extensions import Concatenate
1479+
1480+
P = ParamSpec("P")
1481+
Q = ParamSpec("Q")
1482+
1483+
def foo(f: Callable[P, int]) -> Callable[P, int]:
1484+
return f
1485+
1486+
def bar(f: Callable[Concatenate[str, Q], int]) -> Callable[Concatenate[str, Q], int]:
1487+
return foo(f)
1488+
[builtins fixtures/paramspec.pyi]
1489+
1490+
[case testAlreadyExpandedCallableWithParamSpecReplacement]
1491+
from typing import Callable, Any, overload
1492+
from typing_extensions import Concatenate, ParamSpec
1493+
1494+
P = ParamSpec("P")
1495+
1496+
@overload
1497+
def command() -> Callable[[Callable[Concatenate[object, object, P], object]], None]: # E: Overloaded function signatures 1 and 2 overlap with incompatible return types
1498+
...
1499+
1500+
@overload
1501+
def command(
1502+
cls: int = ...,
1503+
) -> Callable[[Callable[Concatenate[object, P], object]], None]:
1504+
...
1505+
1506+
def command(
1507+
cls: int = 42,
1508+
) -> Any:
1509+
...
1510+
[builtins fixtures/paramspec.pyi]
1511+
1512+
[case testCopiedParamSpecComparison]
1513+
# minimized from https://github.com/python/mypy/issues/12909
1514+
from typing import Callable
1515+
from typing_extensions import ParamSpec
1516+
1517+
P = ParamSpec("P")
1518+
1519+
def identity(func: Callable[P, None]) -> Callable[P, None]: ...
1520+
1521+
@identity
1522+
def f(f: Callable[P, None], *args: P.args, **kwargs: P.kwargs) -> None: ...
1523+
[builtins fixtures/paramspec.pyi]

0 commit comments

Comments
 (0)
0