Skip to content

Commit e13a50d

Browse files
committed
Add logic for ParamSpec args/kwargs in expand_type
1 parent fb16e93 commit e13a50d

File tree

2 files changed

+150
-9
lines changed

2 files changed

+150
-9
lines changed

mypy/expandtype.py

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from collections.abc import Iterable, Mapping
44
from typing import Final, TypeVar, cast, overload
55

6-
from mypy.nodes import ARG_STAR, FakeInfo, Var
6+
from mypy.nodes import ARG_STAR, ArgKind, FakeInfo, Var
77
from mypy.state import state
88
from mypy.types import (
99
ANY_STRATEGY,
@@ -270,14 +270,61 @@ def visit_param_spec(self, t: ParamSpecType) -> Type:
270270
),
271271
)
272272
elif isinstance(repl, Parameters):
273-
assert t.flavor == ParamSpecFlavor.BARE
274-
return Parameters(
275-
self.expand_types(t.prefix.arg_types) + repl.arg_types,
276-
t.prefix.arg_kinds + repl.arg_kinds,
277-
t.prefix.arg_names + repl.arg_names,
278-
variables=[*t.prefix.variables, *repl.variables],
279-
imprecise_arg_kinds=repl.imprecise_arg_kinds,
280-
)
273+
assert isinstance(t.upper_bound, ProperType) and isinstance(t.upper_bound, Instance)
274+
if t.flavor == ParamSpecFlavor.BARE:
275+
return Parameters(
276+
self.expand_types(t.prefix.arg_types) + repl.arg_types,
277+
t.prefix.arg_kinds + repl.arg_kinds,
278+
t.prefix.arg_names + repl.arg_names,
279+
variables=[*t.prefix.variables, *repl.variables],
280+
imprecise_arg_kinds=repl.imprecise_arg_kinds,
281+
)
282+
elif t.flavor == ParamSpecFlavor.ARGS:
283+
assert all(k.is_positional() for k in t.prefix.arg_kinds)
284+
required_posargs = list(t.prefix.arg_types)
285+
optional_posargs: list[Type] = []
286+
for kind, name, type in zip(repl.arg_kinds, repl.arg_names, repl.arg_types):
287+
if kind.is_positional() and name is None:
288+
if optional_posargs:
289+
# May happen following Unpack expansion
290+
required_posargs += optional_posargs
291+
optional_posargs = []
292+
required_posargs.append(type)
293+
elif kind.is_positional():
294+
optional_posargs.append(type)
295+
elif kind == ArgKind.ARG_STAR:
296+
p_type = get_proper_type(type)
297+
if isinstance(p_type, UnpackType):
298+
optional_posargs.append(type)
299+
else:
300+
optional_posargs.append(
301+
UnpackType(Instance(t.upper_bound.type, [type]))
302+
)
303+
break
304+
return UnionType.make_union(
305+
[
306+
TupleType(required_posargs + optional_posargs[:i], fallback=t.upper_bound)
307+
for i in range(len(optional_posargs) + 1)
308+
]
309+
)
310+
else:
311+
assert t.flavor == ParamSpecFlavor.KWARGS
312+
kwargs = {}
313+
required_names = set()
314+
extra_items: Type = UninhabitedType()
315+
for kind, name, type in zip(repl.arg_kinds, repl.arg_names, repl.arg_types):
316+
if kind == ArgKind.ARG_NAMED and name is not None:
317+
kwargs[name] = type
318+
required_names.add(name)
319+
elif kind == ArgKind.ARG_STAR2:
320+
# Unpack[TypedDict] is normalized early, it isn't stored as Unpack
321+
extra_items = type
322+
elif not kind.is_star() and name is not None:
323+
kwargs[name] = type
324+
if not kwargs:
325+
return Instance(t.upper_bound.type, [t.upper_bound.args[0], extra_items])
326+
# TODO: when PEP 728 is implemented, pass extra_items below.
327+
return TypedDictType(kwargs, required_names, set(), fallback=t.upper_bound)
281328
else:
282329
# We could encode Any as trivial parameters etc., but it would be too verbose.
283330
# TODO: assert this is a trivial type, like Any, Never, or object.

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

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2599,3 +2599,97 @@ def run3(predicate: Callable[Concatenate[int, str, _P], None], *args: _P.args, *
25992599
# E: Argument 1 has incompatible type "*tuple[Union[int, str], ...]"; expected "str" \
26002600
# E: Argument 1 has incompatible type "*tuple[Union[int, str], ...]"; expected "_P.args"
26012601
[builtins fixtures/paramspec.pyi]
2602+
2603+
[case testRevealBoundParamSpecArgs]
2604+
from typing import Callable, Generic, ParamSpec
2605+
from typing_extensions import TypeVarTuple, Unpack
2606+
2607+
P = ParamSpec("P")
2608+
Ts = TypeVarTuple("Ts")
2609+
2610+
class Sneaky(Generic[P]):
2611+
def __init__(self, fn: Callable[P, object], *args: P.args, **kwargs: P.kwargs) -> None:
2612+
self.fn = fn
2613+
self.args = args
2614+
self.kwargs = kwargs
2615+
2616+
def f1() -> int:
2617+
return 0
2618+
def f2(x: int) -> int:
2619+
return 0
2620+
def f3(x: int, /) -> int:
2621+
return 0
2622+
def f4(*, x: int) -> int:
2623+
return 0
2624+
def f5(x: int, y: int = 0) -> int:
2625+
return 0
2626+
def f6(x: int, *args: int) -> int:
2627+
return 0
2628+
def f7(x: int, *args: Unpack[Ts]) -> int:
2629+
return 0
2630+
def f8(x: int, *args: Unpack[tuple[str, ...]]) -> int:
2631+
return 0
2632+
def f9(x: int, *args: Unpack[tuple[str, int]]) -> int:
2633+
return 0
2634+
2635+
reveal_type(Sneaky(f1).args) # N: Revealed type is "tuple[()]"
2636+
reveal_type(Sneaky(f2, 1).args) # N: Revealed type is "Union[tuple[()], tuple[builtins.int]]"
2637+
reveal_type(Sneaky(f3, 1).args) # N: Revealed type is "tuple[builtins.int]"
2638+
reveal_type(Sneaky(f4, x=1).args) # N: Revealed type is "tuple[()]"
2639+
reveal_type(Sneaky(f5, 1).args) # N: Revealed type is "Union[tuple[()], tuple[builtins.int], tuple[builtins.int, builtins.int]]"
2640+
reveal_type(Sneaky(f5, 1, 2).args) # N: Revealed type is "Union[tuple[()], tuple[builtins.int], tuple[builtins.int, builtins.int]]"
2641+
reveal_type(Sneaky(f6, 1).args) # N: Revealed type is "Union[tuple[()], tuple[builtins.int], tuple[builtins.int, Unpack[builtins.tuple[builtins.int, ...]]]]"
2642+
reveal_type(Sneaky(f6, 1, 2).args) # N: Revealed type is "Union[tuple[()], tuple[builtins.int], tuple[builtins.int, Unpack[builtins.tuple[builtins.int, ...]]]]"
2643+
reveal_type(Sneaky(f7, 1, 2).args) # N: Revealed type is "tuple[Literal[1]?, Literal[2]?]"
2644+
reveal_type(Sneaky(f8, 1, '').args) # N: Revealed type is "Union[tuple[()], tuple[builtins.int], tuple[builtins.int, Unpack[builtins.tuple[builtins.str, ...]]]]"
2645+
reveal_type(Sneaky(f9, 1, '', 0).args) # N: Revealed type is "tuple[builtins.int, builtins.str, builtins.int]"
2646+
[builtins fixtures/paramspec.pyi]
2647+
2648+
[case testRevealBoundParamSpecKwargs]
2649+
from typing import Callable, Generic, ParamSpec
2650+
from typing_extensions import Unpack, NotRequired, TypedDict
2651+
2652+
P = ParamSpec("P")
2653+
2654+
class Sneaky(Generic[P]):
2655+
def __init__(self, fn: Callable[P, object], *args: P.args, **kwargs: P.kwargs) -> None:
2656+
self.fn = fn
2657+
self.args = args
2658+
self.kwargs = kwargs
2659+
2660+
class Opt(TypedDict):
2661+
y: int
2662+
z: NotRequired[str]
2663+
2664+
def f1() -> int:
2665+
return 0
2666+
def f2(x: int) -> int:
2667+
return 0
2668+
def f3(x: int, /) -> int:
2669+
return 0
2670+
def f4(*, x: int) -> int:
2671+
return 0
2672+
def f5(x: int, y: int = 0) -> int:
2673+
return 0
2674+
def f6(**kwargs: int) -> int:
2675+
return 0
2676+
def f7(x: int, **kwargs: str) -> int:
2677+
return 0
2678+
def f8(x: int, /, **kwargs: str) -> int:
2679+
return 0
2680+
def f9(x: int, **kwargs: Unpack[Opt]) -> int:
2681+
return 0
2682+
2683+
reveal_type(Sneaky(f1).kwargs) # N: Revealed type is "builtins.dict[builtins.str, Never]"
2684+
reveal_type(Sneaky(f2, 1).kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int})"
2685+
reveal_type(Sneaky(f3, 1).kwargs) # N: Revealed type is "builtins.dict[builtins.str, Never]"
2686+
reveal_type(Sneaky(f4, x=1).kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x': builtins.int})"
2687+
reveal_type(Sneaky(f5, 1).kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int, 'y'?: builtins.int})"
2688+
reveal_type(Sneaky(f5, 1, 2).kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int, 'y'?: builtins.int})"
2689+
reveal_type(Sneaky(f6, x=1).kwargs) # N: Revealed type is "builtins.dict[builtins.str, builtins.int]"
2690+
reveal_type(Sneaky(f6, x=1, y=2).kwargs) # N: Revealed type is "builtins.dict[builtins.str, builtins.int]"
2691+
reveal_type(Sneaky(f7, 1, y='').kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int})"
2692+
reveal_type(Sneaky(f8, 1, y='').kwargs) # N: Revealed type is "builtins.dict[builtins.str, builtins.str]"
2693+
reveal_type(Sneaky(f9, 1, y=0).kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int, 'y': builtins.int, 'z'?: builtins.str})"
2694+
reveal_type(Sneaky(f9, 1, y=0, z='').kwargs) # N: Revealed type is "TypedDict('builtins.dict', {'x'?: builtins.int, 'y': builtins.int, 'z'?: builtins.str})"
2695+
[builtins fixtures/paramspec.pyi]

0 commit comments

Comments
 (0)