Skip to content

Commit 536e35a

Browse files
bpo-44801: Check arguments in substitution of ParamSpec in Callable (GH-27585)
(cherry picked from commit 3875a69) Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent c2593b4 commit 536e35a

File tree

4 files changed

+89
-37
lines changed

4 files changed

+89
-37
lines changed

Lib/_collections_abc.py

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -426,22 +426,16 @@ class _CallableGenericAlias(GenericAlias):
426426
__slots__ = ()
427427

428428
def __new__(cls, origin, args):
429-
return cls.__create_ga(origin, args)
430-
431-
@classmethod
432-
def __create_ga(cls, origin, args):
433-
if not isinstance(args, tuple) or len(args) != 2:
429+
if not (isinstance(args, tuple) and len(args) == 2):
434430
raise TypeError(
435431
"Callable must be used as Callable[[arg, ...], result].")
436432
t_args, t_result = args
437-
if isinstance(t_args, (list, tuple)):
438-
ga_args = tuple(t_args) + (t_result,)
439-
# This relaxes what t_args can be on purpose to allow things like
440-
# PEP 612 ParamSpec. Responsibility for whether a user is using
441-
# Callable[...] properly is deferred to static type checkers.
442-
else:
443-
ga_args = args
444-
return super().__new__(cls, origin, ga_args)
433+
if isinstance(t_args, list):
434+
args = (*t_args, t_result)
435+
elif not _is_param_expr(t_args):
436+
raise TypeError(f"Expected a list of types, an ellipsis, "
437+
f"ParamSpec, or Concatenate. Got {t_args}")
438+
return super().__new__(cls, origin, args)
445439

446440
@property
447441
def __parameters__(self):
@@ -456,15 +450,15 @@ def __parameters__(self):
456450
return tuple(dict.fromkeys(params))
457451

458452
def __repr__(self):
459-
if _has_special_args(self.__args__):
453+
if len(self.__args__) == 2 and _is_param_expr(self.__args__[0]):
460454
return super().__repr__()
461455
return (f'collections.abc.Callable'
462456
f'[[{", ".join([_type_repr(a) for a in self.__args__[:-1]])}], '
463457
f'{_type_repr(self.__args__[-1])}]')
464458

465459
def __reduce__(self):
466460
args = self.__args__
467-
if not _has_special_args(args):
461+
if not (len(args) == 2 and _is_param_expr(args[0])):
468462
args = list(args[:-1]), args[-1]
469463
return _CallableGenericAlias, (Callable, args)
470464

@@ -479,10 +473,11 @@ def __getitem__(self, item):
479473
param_len = len(self.__parameters__)
480474
if param_len == 0:
481475
raise TypeError(f'{self} is not a generic class')
482-
if (param_len == 1
483-
and isinstance(item, (tuple, list))
484-
and len(item) > 1) or not isinstance(item, tuple):
476+
if not isinstance(item, tuple):
485477
item = (item,)
478+
if (param_len == 1 and _is_param_expr(self.__parameters__[0])
479+
and item and not _is_param_expr(item[0])):
480+
item = (list(item),)
486481
item_len = len(item)
487482
if item_len != param_len:
488483
raise TypeError(f'Too {"many" if item_len > param_len else "few"}'
@@ -492,7 +487,13 @@ def __getitem__(self, item):
492487
new_args = []
493488
for arg in self.__args__:
494489
if _is_typevarlike(arg):
495-
arg = subst[arg]
490+
if _is_param_expr(arg):
491+
arg = subst[arg]
492+
if not _is_param_expr(arg):
493+
raise TypeError(f"Expected a list of types, an ellipsis, "
494+
f"ParamSpec, or Concatenate. Got {arg}")
495+
else:
496+
arg = subst[arg]
496497
# Looks like a GenericAlias
497498
elif hasattr(arg, '__parameters__') and isinstance(arg.__parameters__, tuple):
498499
subparams = arg.__parameters__
@@ -502,32 +503,31 @@ def __getitem__(self, item):
502503
new_args.append(arg)
503504

504505
# args[0] occurs due to things like Z[[int, str, bool]] from PEP 612
505-
if not isinstance(new_args[0], (tuple, list)):
506+
if not isinstance(new_args[0], list):
506507
t_result = new_args[-1]
507508
t_args = new_args[:-1]
508509
new_args = (t_args, t_result)
509510
return _CallableGenericAlias(Callable, tuple(new_args))
510511

512+
511513
def _is_typevarlike(arg):
512514
obj = type(arg)
513515
# looks like a TypeVar/ParamSpec
514516
return (obj.__module__ == 'typing'
515517
and obj.__name__ in {'ParamSpec', 'TypeVar'})
516518

517-
def _has_special_args(args):
518-
"""Checks if args[0] matches either ``...``, ``ParamSpec`` or
519+
def _is_param_expr(obj):
520+
"""Checks if obj matches either a list of types, ``...``, ``ParamSpec`` or
519521
``_ConcatenateGenericAlias`` from typing.py
520522
"""
521-
if len(args) != 2:
522-
return False
523-
obj = args[0]
524523
if obj is Ellipsis:
525524
return True
525+
if isinstance(obj, list):
526+
return True
526527
obj = type(obj)
527528
names = ('ParamSpec', '_ConcatenateGenericAlias')
528529
return obj.__module__ == 'typing' and any(obj.__name__ == name for name in names)
529530

530-
531531
def _type_repr(obj):
532532
"""Return the repr() of an object, special-casing types (internal helper).
533533

Lib/test/test_typing.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -575,17 +575,33 @@ def test_paramspec(self):
575575
Callable = self.Callable
576576
fullname = f"{Callable.__module__}.Callable"
577577
P = ParamSpec('P')
578+
P2 = ParamSpec('P2')
578579
C1 = Callable[P, T]
579580
# substitution
580-
self.assertEqual(C1[int, str], Callable[[int], str])
581+
self.assertEqual(C1[[int], str], Callable[[int], str])
581582
self.assertEqual(C1[[int, str], str], Callable[[int, str], str])
583+
self.assertEqual(C1[[], str], Callable[[], str])
584+
self.assertEqual(C1[..., str], Callable[..., str])
585+
self.assertEqual(C1[P2, str], Callable[P2, str])
586+
self.assertEqual(C1[Concatenate[int, P2], str],
587+
Callable[Concatenate[int, P2], str])
582588
self.assertEqual(repr(C1), f"{fullname}[~P, ~T]")
583-
self.assertEqual(repr(C1[int, str]), f"{fullname}[[int], str]")
589+
self.assertEqual(repr(C1[[int, str], str]), f"{fullname}[[int, str], str]")
590+
with self.assertRaises(TypeError):
591+
C1[int, str]
584592

585593
C2 = Callable[P, int]
594+
self.assertEqual(C2[[int]], Callable[[int], int])
595+
self.assertEqual(C2[[int, str]], Callable[[int, str], int])
596+
self.assertEqual(C2[[]], Callable[[], int])
597+
self.assertEqual(C2[...], Callable[..., int])
598+
self.assertEqual(C2[P2], Callable[P2, int])
599+
self.assertEqual(C2[Concatenate[int, P2]],
600+
Callable[Concatenate[int, P2], int])
586601
# special case in PEP 612 where
587602
# X[int, str, float] == X[[int, str, float]]
588-
self.assertEqual(C2[int, str, float], C2[[int, str, float]])
603+
self.assertEqual(C2[int], Callable[[int], int])
604+
self.assertEqual(C2[int, str], Callable[[int, str], int])
589605
self.assertEqual(repr(C2), f"{fullname}[~P, int]")
590606
self.assertEqual(repr(C2[int, str]), f"{fullname}[[int, str], int]")
591607

@@ -4616,6 +4632,29 @@ class Z(Generic[P]):
46164632
self.assertEqual(G5.__parameters__, G6.__parameters__)
46174633
self.assertEqual(G5, G6)
46184634

4635+
G7 = Z[int]
4636+
self.assertEqual(G7.__args__, ((int,),))
4637+
self.assertEqual(G7.__parameters__, ())
4638+
4639+
with self.assertRaisesRegex(TypeError, "many arguments for"):
4640+
Z[[int, str], bool]
4641+
with self.assertRaisesRegex(TypeError, "many arguments for"):
4642+
Z[P_2, bool]
4643+
4644+
def test_multiple_paramspecs_in_user_generics(self):
4645+
P = ParamSpec("P")
4646+
P2 = ParamSpec("P2")
4647+
4648+
class X(Generic[P, P2]):
4649+
f: Callable[P, int]
4650+
g: Callable[P2, str]
4651+
4652+
G1 = X[[int, str], [bytes]]
4653+
G2 = X[[int], [str, bytes]]
4654+
self.assertNotEqual(G1, G2)
4655+
self.assertEqual(G1.__args__, ((int, str), (bytes,)))
4656+
self.assertEqual(G2.__args__, ((int,), (str, bytes)))
4657+
46194658
def test_no_paramspec_in__parameters__(self):
46204659
# ParamSpec should not be found in __parameters__
46214660
# of generics. Usages outside Callable, Concatenate

Lib/typing.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,11 @@ def _type_check(arg, msg, is_argument=True, module=None):
174174
return arg
175175

176176

177+
def _is_param_expr(arg):
178+
return arg is ... or isinstance(arg,
179+
(tuple, list, ParamSpec, _ConcatenateGenericAlias))
180+
181+
177182
def _type_repr(obj):
178183
"""Return the repr() of an object, special-casing types (internal helper).
179184
@@ -228,7 +233,9 @@ def _prepare_paramspec_params(cls, params):
228233
variables (internal helper).
229234
"""
230235
# Special case where Z[[int, str, bool]] == Z[int, str, bool] in PEP 612.
231-
if len(cls.__parameters__) == 1 and len(params) > 1:
236+
if (len(cls.__parameters__) == 1
237+
and params and not _is_param_expr(params[0])):
238+
assert isinstance(cls.__parameters__[0], ParamSpec)
232239
return (params,)
233240
else:
234241
_check_generic(cls, params, len(cls.__parameters__))
@@ -1031,7 +1038,13 @@ def __getitem__(self, params):
10311038
new_args = []
10321039
for arg in self.__args__:
10331040
if isinstance(arg, self._typevar_types):
1034-
arg = subst[arg]
1041+
if isinstance(arg, ParamSpec):
1042+
arg = subst[arg]
1043+
if not _is_param_expr(arg):
1044+
raise TypeError(f"Expected a list of types, an ellipsis, "
1045+
f"ParamSpec, or Concatenate. Got {arg}")
1046+
else:
1047+
arg = subst[arg]
10351048
elif isinstance(arg, (_GenericAlias, GenericAlias, types.UnionType)):
10361049
subparams = arg.__parameters__
10371050
if subparams:
@@ -1129,17 +1142,15 @@ class _CallableGenericAlias(_GenericAlias, _root=True):
11291142
def __repr__(self):
11301143
assert self._name == 'Callable'
11311144
args = self.__args__
1132-
if len(args) == 2 and (args[0] is Ellipsis
1133-
or isinstance(args[0], (ParamSpec, _ConcatenateGenericAlias))):
1145+
if len(args) == 2 and _is_param_expr(args[0]):
11341146
return super().__repr__()
11351147
return (f'typing.Callable'
11361148
f'[[{", ".join([_type_repr(a) for a in args[:-1]])}], '
11371149
f'{_type_repr(args[-1])}]')
11381150

11391151
def __reduce__(self):
11401152
args = self.__args__
1141-
if not (len(args) == 2 and (args[0] is Ellipsis
1142-
or isinstance(args[0], (ParamSpec, _ConcatenateGenericAlias)))):
1153+
if not (len(args) == 2 and _is_param_expr(args[0])):
11431154
args = list(args[:-1]), args[-1]
11441155
return operator.getitem, (Callable, args)
11451156

@@ -1865,8 +1876,7 @@ def get_args(tp):
18651876
if isinstance(tp, (_GenericAlias, GenericAlias)):
18661877
res = tp.__args__
18671878
if (tp.__origin__ is collections.abc.Callable
1868-
and not (res[0] is Ellipsis
1869-
or isinstance(res[0], (ParamSpec, _ConcatenateGenericAlias)))):
1879+
and not (len(res) == 2 and _is_param_expr(res[0]))):
18701880
res = (list(res[:-1]), res[-1])
18711881
return res
18721882
if isinstance(tp, types.UnionType):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Ensure that the :class:`~typing.ParamSpec` variable in Callable
2+
can only be substituted with a parameters expression (a list of types,
3+
an ellipsis, ParamSpec or Concatenate).

0 commit comments

Comments
 (0)