Skip to content

Commit 9fee22d

Browse files
serhiy-storchakamiss-islington
authored andcommitted
gh-99344, gh-99379, gh-99382: Fix issues in substitution of ParamSpec and TypeVarTuple (GH-99412)
* Fix substitution of TypeVarTuple and ParamSpec together in user generics. * Fix substitution of ParamSpec followed by TypeVarTuple in generic aliases. * Check the number of arguments in substitution in user generics containing a TypeVarTuple and one or more TypeVar. (cherry picked from commit 8f2fb7d) Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 5bbf8ed commit 9fee22d

5 files changed

+117
-41
lines changed

Diff for: Lib/test/test_typing.py

+81
Original file line numberDiff line numberDiff line change
@@ -769,20 +769,42 @@ class C(Generic[*Ts]): pass
769769
('generic[*Ts]', '[*Ts]', 'generic[*Ts]'),
770770
('generic[*Ts]', '[T, *Ts]', 'generic[T, *Ts]'),
771771
('generic[*Ts]', '[*Ts, T]', 'generic[*Ts, T]'),
772+
('generic[T, *Ts]', '[()]', 'TypeError'),
772773
('generic[T, *Ts]', '[int]', 'generic[int]'),
773774
('generic[T, *Ts]', '[int, str]', 'generic[int, str]'),
774775
('generic[T, *Ts]', '[int, str, bool]', 'generic[int, str, bool]'),
776+
('generic[list[T], *Ts]', '[()]', 'TypeError'),
775777
('generic[list[T], *Ts]', '[int]', 'generic[list[int]]'),
776778
('generic[list[T], *Ts]', '[int, str]', 'generic[list[int], str]'),
777779
('generic[list[T], *Ts]', '[int, str, bool]', 'generic[list[int], str, bool]'),
778780

781+
('generic[*Ts, T]', '[()]', 'TypeError'),
779782
('generic[*Ts, T]', '[int]', 'generic[int]'),
780783
('generic[*Ts, T]', '[int, str]', 'generic[int, str]'),
781784
('generic[*Ts, T]', '[int, str, bool]', 'generic[int, str, bool]'),
785+
('generic[*Ts, list[T]]', '[()]', 'TypeError'),
782786
('generic[*Ts, list[T]]', '[int]', 'generic[list[int]]'),
783787
('generic[*Ts, list[T]]', '[int, str]', 'generic[int, list[str]]'),
784788
('generic[*Ts, list[T]]', '[int, str, bool]', 'generic[int, str, list[bool]]'),
785789

790+
('generic[T1, T2, *Ts]', '[()]', 'TypeError'),
791+
('generic[T1, T2, *Ts]', '[int]', 'TypeError'),
792+
('generic[T1, T2, *Ts]', '[int, str]', 'generic[int, str]'),
793+
('generic[T1, T2, *Ts]', '[int, str, bool]', 'generic[int, str, bool]'),
794+
('generic[T1, T2, *Ts]', '[int, str, bool, bytes]', 'generic[int, str, bool, bytes]'),
795+
796+
('generic[*Ts, T1, T2]', '[()]', 'TypeError'),
797+
('generic[*Ts, T1, T2]', '[int]', 'TypeError'),
798+
('generic[*Ts, T1, T2]', '[int, str]', 'generic[int, str]'),
799+
('generic[*Ts, T1, T2]', '[int, str, bool]', 'generic[int, str, bool]'),
800+
('generic[*Ts, T1, T2]', '[int, str, bool, bytes]', 'generic[int, str, bool, bytes]'),
801+
802+
('generic[T1, *Ts, T2]', '[()]', 'TypeError'),
803+
('generic[T1, *Ts, T2]', '[int]', 'TypeError'),
804+
('generic[T1, *Ts, T2]', '[int, str]', 'generic[int, str]'),
805+
('generic[T1, *Ts, T2]', '[int, str, bool]', 'generic[int, str, bool]'),
806+
('generic[T1, *Ts, T2]', '[int, str, bool, bytes]', 'generic[int, str, bool, bytes]'),
807+
786808
('generic[T, *Ts]', '[*tuple_type[int, ...]]', 'generic[int, *tuple_type[int, ...]]'),
787809
('generic[T, *Ts]', '[str, *tuple_type[int, ...]]', 'generic[str, *tuple_type[int, ...]]'),
788810
('generic[T, *Ts]', '[*tuple_type[int, ...], str]', 'generic[int, *tuple_type[int, ...], str]'),
@@ -7190,6 +7212,65 @@ class X(Generic[P, P2]):
71907212
self.assertEqual(G1.__args__, ((int, str), (bytes,)))
71917213
self.assertEqual(G2.__args__, ((int,), (str, bytes)))
71927214

7215+
def test_typevartuple_and_paramspecs_in_user_generics(self):
7216+
Ts = TypeVarTuple("Ts")
7217+
P = ParamSpec("P")
7218+
7219+
class X(Generic[*Ts, P]):
7220+
f: Callable[P, int]
7221+
g: Tuple[*Ts]
7222+
7223+
G1 = X[int, [bytes]]
7224+
self.assertEqual(G1.__args__, (int, (bytes,)))
7225+
G2 = X[int, str, [bytes]]
7226+
self.assertEqual(G2.__args__, (int, str, (bytes,)))
7227+
G3 = X[[bytes]]
7228+
self.assertEqual(G3.__args__, ((bytes,),))
7229+
G4 = X[[]]
7230+
self.assertEqual(G4.__args__, ((),))
7231+
with self.assertRaises(TypeError):
7232+
X[()]
7233+
7234+
class Y(Generic[P, *Ts]):
7235+
f: Callable[P, int]
7236+
g: Tuple[*Ts]
7237+
7238+
G1 = Y[[bytes], int]
7239+
self.assertEqual(G1.__args__, ((bytes,), int))
7240+
G2 = Y[[bytes], int, str]
7241+
self.assertEqual(G2.__args__, ((bytes,), int, str))
7242+
G3 = Y[[bytes]]
7243+
self.assertEqual(G3.__args__, ((bytes,),))
7244+
G4 = Y[[]]
7245+
self.assertEqual(G4.__args__, ((),))
7246+
with self.assertRaises(TypeError):
7247+
Y[()]
7248+
7249+
def test_typevartuple_and_paramspecs_in_generic_aliases(self):
7250+
P = ParamSpec('P')
7251+
T = TypeVar('T')
7252+
Ts = TypeVarTuple('Ts')
7253+
7254+
for C in Callable, collections.abc.Callable:
7255+
with self.subTest(generic=C):
7256+
A = C[P, Tuple[*Ts]]
7257+
B = A[[int, str], bytes, float]
7258+
self.assertEqual(B.__args__, (int, str, Tuple[bytes, float]))
7259+
7260+
class X(Generic[T, P]):
7261+
pass
7262+
7263+
A = X[Tuple[*Ts], P]
7264+
B = A[bytes, float, [int, str]]
7265+
self.assertEqual(B.__args__, (Tuple[bytes, float], (int, str,)))
7266+
7267+
class Y(Generic[P, T]):
7268+
pass
7269+
7270+
A = Y[P, Tuple[*Ts]]
7271+
B = A[[int, str], bytes, float]
7272+
self.assertEqual(B.__args__, ((int, str,), Tuple[bytes, float]))
7273+
71937274
def test_var_substitution(self):
71947275
T = TypeVar("T")
71957276
P = ParamSpec("P")

Diff for: Lib/typing.py

+30-41
Original file line numberDiff line numberDiff line change
@@ -284,25 +284,6 @@ def _unpack_args(args):
284284
newargs.append(arg)
285285
return newargs
286286

287-
def _prepare_paramspec_params(cls, params):
288-
"""Prepares the parameters for a Generic containing ParamSpec
289-
variables (internal helper).
290-
"""
291-
# Special case where Z[[int, str, bool]] == Z[int, str, bool] in PEP 612.
292-
if (len(cls.__parameters__) == 1
293-
and params and not _is_param_expr(params[0])):
294-
assert isinstance(cls.__parameters__[0], ParamSpec)
295-
return (params,)
296-
else:
297-
_check_generic(cls, params, len(cls.__parameters__))
298-
_params = []
299-
# Convert lists to tuples to help other libraries cache the results.
300-
for p, tvar in zip(params, cls.__parameters__):
301-
if isinstance(tvar, ParamSpec) and isinstance(p, list):
302-
p = tuple(p)
303-
_params.append(p)
304-
return tuple(_params)
305-
306287
def _deduplicate(params):
307288
# Weed out strict duplicates, preserving the first of each occurrence.
308289
all_params = set(params)
@@ -1226,7 +1207,18 @@ def __typing_subst__(self, arg):
12261207
return arg
12271208

12281209
def __typing_prepare_subst__(self, alias, args):
1229-
return _prepare_paramspec_params(alias, args)
1210+
params = alias.__parameters__
1211+
i = params.index(self)
1212+
if i >= len(args):
1213+
raise TypeError(f"Too few arguments for {alias}")
1214+
# Special case where Z[[int, str, bool]] == Z[int, str, bool] in PEP 612.
1215+
if len(params) == 1 and not _is_param_expr(args[0]):
1216+
assert i == 0
1217+
args = (args,)
1218+
# Convert lists to tuples to help other libraries cache the results.
1219+
elif isinstance(args[i], list):
1220+
args = (*args[:i], tuple(args[i]), *args[i+1:])
1221+
return args
12301222

12311223
def _is_dunder(attr):
12321224
return attr.startswith('__') and attr.endswith('__')
@@ -1789,23 +1781,13 @@ def __class_getitem__(cls, params):
17891781
if not isinstance(params, tuple):
17901782
params = (params,)
17911783

1792-
if not params:
1793-
# We're only ok with `params` being empty if the class's only type
1794-
# parameter is a `TypeVarTuple` (which can contain zero types).
1795-
class_params = getattr(cls, "__parameters__", None)
1796-
only_class_parameter_is_typevartuple = (
1797-
class_params is not None
1798-
and len(class_params) == 1
1799-
and isinstance(class_params[0], TypeVarTuple)
1800-
)
1801-
if not only_class_parameter_is_typevartuple:
1802-
raise TypeError(
1803-
f"Parameter list to {cls.__qualname__}[...] cannot be empty"
1804-
)
1805-
18061784
params = tuple(_type_convert(p) for p in params)
18071785
if cls in (Generic, Protocol):
18081786
# Generic and Protocol can only be subscripted with unique type variables.
1787+
if not params:
1788+
raise TypeError(
1789+
f"Parameter list to {cls.__qualname__}[...] cannot be empty"
1790+
)
18091791
if not all(_is_typevar_like(p) for p in params):
18101792
raise TypeError(
18111793
f"Parameters to {cls.__name__}[...] must all be type variables "
@@ -1815,13 +1797,20 @@ def __class_getitem__(cls, params):
18151797
f"Parameters to {cls.__name__}[...] must all be unique")
18161798
else:
18171799
# Subscripting a regular Generic subclass.
1818-
if any(isinstance(t, ParamSpec) for t in cls.__parameters__):
1819-
params = _prepare_paramspec_params(cls, params)
1820-
elif not any(isinstance(p, TypeVarTuple) for p in cls.__parameters__):
1821-
# We only run this if there are no TypeVarTuples, because we
1822-
# don't check variadic generic arity at runtime (to reduce
1823-
# complexity of typing.py).
1824-
_check_generic(cls, params, len(cls.__parameters__))
1800+
for param in cls.__parameters__:
1801+
prepare = getattr(param, '__typing_prepare_subst__', None)
1802+
if prepare is not None:
1803+
params = prepare(cls, params)
1804+
_check_generic(cls, params, len(cls.__parameters__))
1805+
1806+
new_args = []
1807+
for param, new_arg in zip(cls.__parameters__, params):
1808+
if isinstance(param, TypeVarTuple):
1809+
new_args.extend(new_arg)
1810+
else:
1811+
new_args.append(new_arg)
1812+
params = tuple(new_args)
1813+
18251814
return _GenericAlias(cls, params,
18261815
_paramspec_tvars=True)
18271816

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix substitution of :class:`~typing.TypeVarTuple` and
2+
:class:`~typing.ParamSpec` together in user generics.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix substitution of :class:`~typing.ParamSpec` followed by
2+
:class:`~typing.TypeVarTuple` in generic aliases.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Check the number of arguments in substitution in user generics containing a
2+
:class:`~typing.TypeVarTuple` and one or more :class:`~typing.TypeVar`.

0 commit comments

Comments
 (0)