mirror of https://github.com/python/cpython
gh-88965: typing: fix type substitution of a list of types after initial `ParamSpec` substitution (#102808)
Previously, this used to fail: ```py from typing import * T = TypeVar("T") P = ParamSpec("P") class X(Generic[P]): f: Callable[P, int] Y = X[[int, T]] Z = Y[str] ``` Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
parent
baf4eb083c
commit
adb0621652
|
@ -7679,6 +7679,127 @@ class ParamSpecTests(BaseTestCase):
|
|||
with self.assertRaises(TypeError):
|
||||
collections.abc.Callable[P, T][arg, str]
|
||||
|
||||
def test_type_var_subst_for_other_type_vars(self):
|
||||
T = TypeVar('T')
|
||||
T2 = TypeVar('T2')
|
||||
P = ParamSpec('P')
|
||||
P2 = ParamSpec('P2')
|
||||
Ts = TypeVarTuple('Ts')
|
||||
|
||||
class Base(Generic[P]):
|
||||
pass
|
||||
|
||||
A1 = Base[T]
|
||||
self.assertEqual(A1.__parameters__, (T,))
|
||||
self.assertEqual(A1.__args__, ((T,),))
|
||||
self.assertEqual(A1[int], Base[int])
|
||||
|
||||
A2 = Base[[T]]
|
||||
self.assertEqual(A2.__parameters__, (T,))
|
||||
self.assertEqual(A2.__args__, ((T,),))
|
||||
self.assertEqual(A2[int], Base[int])
|
||||
|
||||
A3 = Base[[int, T]]
|
||||
self.assertEqual(A3.__parameters__, (T,))
|
||||
self.assertEqual(A3.__args__, ((int, T),))
|
||||
self.assertEqual(A3[str], Base[[int, str]])
|
||||
|
||||
A4 = Base[[T, int, T2]]
|
||||
self.assertEqual(A4.__parameters__, (T, T2))
|
||||
self.assertEqual(A4.__args__, ((T, int, T2),))
|
||||
self.assertEqual(A4[str, bool], Base[[str, int, bool]])
|
||||
|
||||
A5 = Base[[*Ts, int]]
|
||||
self.assertEqual(A5.__parameters__, (Ts,))
|
||||
self.assertEqual(A5.__args__, ((*Ts, int),))
|
||||
self.assertEqual(A5[str, bool], Base[[str, bool, int]])
|
||||
|
||||
A5_2 = Base[[int, *Ts]]
|
||||
self.assertEqual(A5_2.__parameters__, (Ts,))
|
||||
self.assertEqual(A5_2.__args__, ((int, *Ts),))
|
||||
self.assertEqual(A5_2[str, bool], Base[[int, str, bool]])
|
||||
|
||||
A6 = Base[[T, *Ts]]
|
||||
self.assertEqual(A6.__parameters__, (T, Ts))
|
||||
self.assertEqual(A6.__args__, ((T, *Ts),))
|
||||
self.assertEqual(A6[int, str, bool], Base[[int, str, bool]])
|
||||
|
||||
A7 = Base[[T, T]]
|
||||
self.assertEqual(A7.__parameters__, (T,))
|
||||
self.assertEqual(A7.__args__, ((T, T),))
|
||||
self.assertEqual(A7[int], Base[[int, int]])
|
||||
|
||||
A8 = Base[[T, list[T]]]
|
||||
self.assertEqual(A8.__parameters__, (T,))
|
||||
self.assertEqual(A8.__args__, ((T, list[T]),))
|
||||
self.assertEqual(A8[int], Base[[int, list[int]]])
|
||||
|
||||
A9 = Base[[Tuple[*Ts], *Ts]]
|
||||
self.assertEqual(A9.__parameters__, (Ts,))
|
||||
self.assertEqual(A9.__args__, ((Tuple[*Ts], *Ts),))
|
||||
self.assertEqual(A9[int, str], Base[Tuple[int, str], int, str])
|
||||
|
||||
A10 = Base[P2]
|
||||
self.assertEqual(A10.__parameters__, (P2,))
|
||||
self.assertEqual(A10.__args__, (P2,))
|
||||
self.assertEqual(A10[[int, str]], Base[[int, str]])
|
||||
|
||||
class DoubleP(Generic[P, P2]):
|
||||
pass
|
||||
|
||||
B1 = DoubleP[P, P2]
|
||||
self.assertEqual(B1.__parameters__, (P, P2))
|
||||
self.assertEqual(B1.__args__, (P, P2))
|
||||
self.assertEqual(B1[[int, str], [bool]], DoubleP[[int, str], [bool]])
|
||||
self.assertEqual(B1[[], []], DoubleP[[], []])
|
||||
|
||||
B2 = DoubleP[[int, str], P2]
|
||||
self.assertEqual(B2.__parameters__, (P2,))
|
||||
self.assertEqual(B2.__args__, ((int, str), P2))
|
||||
self.assertEqual(B2[[bool, bool]], DoubleP[[int, str], [bool, bool]])
|
||||
self.assertEqual(B2[[]], DoubleP[[int, str], []])
|
||||
|
||||
B3 = DoubleP[P, [bool, bool]]
|
||||
self.assertEqual(B3.__parameters__, (P,))
|
||||
self.assertEqual(B3.__args__, (P, (bool, bool)))
|
||||
self.assertEqual(B3[[int, str]], DoubleP[[int, str], [bool, bool]])
|
||||
self.assertEqual(B3[[]], DoubleP[[], [bool, bool]])
|
||||
|
||||
B4 = DoubleP[[T, int], [bool, T2]]
|
||||
self.assertEqual(B4.__parameters__, (T, T2))
|
||||
self.assertEqual(B4.__args__, ((T, int), (bool, T2)))
|
||||
self.assertEqual(B4[str, float], DoubleP[[str, int], [bool, float]])
|
||||
|
||||
B5 = DoubleP[[*Ts, int], [bool, T2]]
|
||||
self.assertEqual(B5.__parameters__, (Ts, T2))
|
||||
self.assertEqual(B5.__args__, ((*Ts, int), (bool, T2)))
|
||||
self.assertEqual(B5[str, bytes, float],
|
||||
DoubleP[[str, bytes, int], [bool, float]])
|
||||
|
||||
B6 = DoubleP[[T, int], [bool, *Ts]]
|
||||
self.assertEqual(B6.__parameters__, (T, Ts))
|
||||
self.assertEqual(B6.__args__, ((T, int), (bool, *Ts)))
|
||||
self.assertEqual(B6[str, bytes, float],
|
||||
DoubleP[[str, int], [bool, bytes, float]])
|
||||
|
||||
class PandT(Generic[P, T]):
|
||||
pass
|
||||
|
||||
C1 = PandT[P, T]
|
||||
self.assertEqual(C1.__parameters__, (P, T))
|
||||
self.assertEqual(C1.__args__, (P, T))
|
||||
self.assertEqual(C1[[int, str], bool], PandT[[int, str], bool])
|
||||
|
||||
C2 = PandT[[int, T], T]
|
||||
self.assertEqual(C2.__parameters__, (T,))
|
||||
self.assertEqual(C2.__args__, ((int, T), T))
|
||||
self.assertEqual(C2[str], PandT[[int, str], str])
|
||||
|
||||
C3 = PandT[[int, *Ts], T]
|
||||
self.assertEqual(C3.__parameters__, (Ts, T))
|
||||
self.assertEqual(C3.__args__, ((int, *Ts), T))
|
||||
self.assertEqual(C3[str, bool, bytes], PandT[[int, str, bool], bytes])
|
||||
|
||||
def test_paramspec_in_nested_generics(self):
|
||||
# Although ParamSpec should not be found in __parameters__ of most
|
||||
# generics, they probably should be found when nested in
|
||||
|
|
|
@ -255,10 +255,17 @@ def _collect_parameters(args):
|
|||
"""
|
||||
parameters = []
|
||||
for t in args:
|
||||
# We don't want __parameters__ descriptor of a bare Python class.
|
||||
if isinstance(t, type):
|
||||
continue
|
||||
if hasattr(t, '__typing_subst__'):
|
||||
# We don't want __parameters__ descriptor of a bare Python class.
|
||||
pass
|
||||
elif isinstance(t, tuple):
|
||||
# `t` might be a tuple, when `ParamSpec` is substituted with
|
||||
# `[T, int]`, or `[int, *Ts]`, etc.
|
||||
for x in t:
|
||||
for collected in _collect_parameters([x]):
|
||||
if collected not in parameters:
|
||||
parameters.append(collected)
|
||||
elif hasattr(t, '__typing_subst__'):
|
||||
if t not in parameters:
|
||||
parameters.append(t)
|
||||
else:
|
||||
|
@ -1441,10 +1448,12 @@ class _GenericAlias(_BaseGenericAlias, _root=True):
|
|||
raise TypeError(f"Too {'many' if alen > plen else 'few'} arguments for {self};"
|
||||
f" actual {alen}, expected {plen}")
|
||||
new_arg_by_param = dict(zip(params, args))
|
||||
return tuple(self._make_substitution(self.__args__, new_arg_by_param))
|
||||
|
||||
def _make_substitution(self, args, new_arg_by_param):
|
||||
"""Create a list of new type arguments."""
|
||||
new_args = []
|
||||
for old_arg in self.__args__:
|
||||
|
||||
for old_arg in args:
|
||||
if isinstance(old_arg, type):
|
||||
new_args.append(old_arg)
|
||||
continue
|
||||
|
@ -1488,10 +1497,20 @@ class _GenericAlias(_BaseGenericAlias, _root=True):
|
|||
# should join all these types together in a flat list
|
||||
# `(float, int, str)` - so again, we should `extend`.
|
||||
new_args.extend(new_arg)
|
||||
elif isinstance(old_arg, tuple):
|
||||
# Corner case:
|
||||
# P = ParamSpec('P')
|
||||
# T = TypeVar('T')
|
||||
# class Base(Generic[P]): ...
|
||||
# Can be substituted like this:
|
||||
# X = Base[[int, T]]
|
||||
# In this case, `old_arg` will be a tuple:
|
||||
new_args.append(
|
||||
tuple(self._make_substitution(old_arg, new_arg_by_param)),
|
||||
)
|
||||
else:
|
||||
new_args.append(new_arg)
|
||||
|
||||
return tuple(new_args)
|
||||
return new_args
|
||||
|
||||
def copy_with(self, args):
|
||||
return self.__class__(self.__origin__, args, name=self._name, inst=self._inst,
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
typing: Fix a bug relating to substitution in custom classes generic over a
|
||||
:class:`~typing.ParamSpec`. Previously, if the ``ParamSpec`` was substituted
|
||||
with a parameters list that itself contained a :class:`~typing.TypeVar`, the
|
||||
``TypeVar`` in the parameters list could not be subsequently substituted. This
|
||||
is now fixed.
|
||||
|
||||
Patch by Nikita Sobolev.
|
Loading…
Reference in New Issue