8000 gh-88965: typing: fix type substitution of a list of types after ini… · python/cpython@1645a40 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1645a40

Browse files
miss-islingtonsobolevnAlexWaygood
authored
gh-88965: typing: fix type substitution of a list of types after initial ParamSpec substitution (GH-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] ``` (cherry picked from commit adb0621) Co-authored-by: Nikita Sobolev <mail@sobolevn.me> Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
1 parent 84ae50c commit 1645a40

File tree

3 files changed

+154
-7
lines changed

3 files changed

+154
-7
lines changed

Lib/test/test_typing.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7454,6 +7454,127 @@ def test_bad_var_substitution(self):
74547454
with self.assertRaises(TypeError):
74557455
collections.abc.Callable[P, T][arg, str]
74567456

7457+
def test_type_var_subst_for_other_type_vars(self):
7458+
T = TypeVar('T')
7459+
T2 = TypeVar('T2')
7460+
P = ParamSpec('P')
7461+
P2 = ParamSpec('P2')
7462+
Ts = TypeVarTuple('Ts')
7463+
7464+
class Base(Generic[P]):
7465+
pass
7466+
7467+
A1 = Base[T]
7468+
self.assertEqual(A1.__parameters__, (T,))
7469+
self.assertEqual(A1.__args__, ((T,),))
7470+
self.assertEqual(A1[int], Base[int])
7471+
7472+
A2 = Base[[T]]
7473+
self.assertEqual(A2.__parameters__, (T,))
7474+
self.assertEqual(A2.__args__, ((T,),))
7475+
self.assertEqual(A2[int], Base[int])
7476+
7477+
A3 = Base[[int, T]]
7478+
self.assertEqual(A3.__parameters__, (T,))
7479+
self.assertEqual(A3.__args__, ((int, T),))
7480+
self.assertEqual(A3[str], Base[[int, str]])
7481+
7482+
A4 = Base[[T, int, T2]]
7483+
self.assertEqual(A4.__parameters__, (T, T2))
7484+
self.assertEqual(A4.__args__, ((T, int, T2),))
7485+
self.assertEqual(A4[str, bool], Base[[str, int, bool]])
7486+
7487+
A5 = Base[[*Ts, int]]
7488+
self.assertEqual(A5.__parameters__, (Ts,))
7489+
self.assertEqual(A5.__args__, ((*Ts, int),))
7490+
self.assertEqual(A5[str, bool], Base[[str, bool, int]])
7491+
7492+
A5_2 = Base[[int, *Ts]]
7493+
self.assertEqual(A5_2.__parameters__, (Ts,))
7494+
self.assertEqual(A5_2.__args__, ((int, *Ts),))
7495+
self.assertEqual(A5_2[str, bool], Base[[int, str, bool]])
7496+
7497+
A6 = Base[[T, *Ts]]
7498+
self.assertEqual(A6.__parameters__, (T, Ts))
7499+
self.assertEqual(A6.__args__, ((T, *Ts),))
7500+
self.assertEqual(A6[int, str, bool], Base[[int, str, bool]])
7501+
7502+
A7 = Base[[T, T]]
7503+
self.assertEqual(A7.__parameters__, (T,))
7504+
self.assertEqual(A7.__args__, ((T, T),))
7505+
self.assertEqual(A7[int], Base[[int, int]])
7506+
7507+
A8 = Base[[T, list[T]]]
7508+
self.assertEqual(A8.__parameters__, (T,))
7509+
self.assertEqual(A8.__args__, ((T, list[T]),))
7510+
self.assertEqual(A8[int], Base[[int, list[int]]])
7511+
7512+
A9 = Base[[Tuple[*Ts], *Ts]]
7513+
self.assertEqual(A9.__parameters__, (Ts,))
7514+
self.assertEqual(A9.__args__, ((Tuple[*Ts], *Ts),))
7515+
self.assertEqual(A9[int, str], Base[Tuple[int, str], int, str])
7516+
7517+
A10 = Base[P2]
7518+
self.assertEqual(A10.__parameters__, (P2,))
7519+
self.assertEqual(A10.__args__, (P2,))
7520+
self.assertEqual(A10[[int, str]], Base[[int, str]])
7521+
7522+
class DoubleP(Generic[P, P2]):
7523+
pass
7524+
7525+
B1 = DoubleP[P, P2]
7526+
self.assertEqual(B1.__parameters__, (P, P2))
7527+
self.assertEqual(B1.__args__, (P, P2))
7528+
self.assertEqual(B1[[int, str], [bool]], DoubleP[[int, str], [bool]])
7529+
self.assertEqual(B1[[], []], DoubleP[[], []])
7530+
7531+
B2 = DoubleP[[int, str], P2]
7532+
self.assertEqual(B2.__parameters__, (P2,))
7533+
self.assertEqual(B2.__args__, ((int, str), P2))
7534+
self.assertEqual(B2[[bool, bool]], DoubleP[[int, str], [bool, bool]])
7535+
self.assertEqual(B2[[]], DoubleP[[int, str], []])
7536+
7537+
B3 = DoubleP[P, [bool, bool]]
7538+
self.assertEqual(B3.__parameters__, (P,))
7539+
self.assertEqual(B3.__args__, (P, (bool, bool)))
7540+
self.assertEqual(B3[[int, str]], DoubleP[[int, str], [bool, bool]])
7541+
self.assertEqual(B3[[]], DoubleP[[], [bool, bool]])
7542+
7543+
B4 = DoubleP[[T, int], [bool, T2]]
7544+
self.assertEqual(B4.__parameters__, (T, T2))
7545+
self.assertEqual(B4.__args__, ((T, int), (bool, T2)))
7546+
self.assertEqual(B4[str, float], DoubleP[[str, int], [bool, float]])
7547+
7548+
B5 = DoubleP[[*Ts, int], [bool, T2]]
7549+
self.assertEqual(B5.__parameters__, (Ts, T2))
7550+
self.assertEqual(B5.__args__, ((*Ts, int), (bool, T2)))
7551+
self.assertEqual(B5[str, bytes, float],
7552+
DoubleP[[str, bytes, int], [bool, float]])
7553+
7554+
B6 = DoubleP[[T, int], [bool, *Ts]]
7555+
self.assertEqual(B6.__parameters__, (T, Ts))
7556+
self.assertEqual(B6.__args__, ((T, int), (bool, *Ts)))
7557+
self.assertEqual(B6[str, bytes, float],
7558+
DoubleP[[str, int], [bool, bytes, float]])
7559+
7560+
class PandT(Generic[P, T]):
7561+
pass
7562+
7563+
C1 = PandT[P, T]
7564+
self.assertEqual(C1.__parameters__, (P, T))
7565+
self.assertEqual(C1.__args__, (P, T))
7566+
self.assertEqual(C1[[int, str], bool], PandT[[int, str], bool])
7567+
7568+
C2 = PandT[[int, T], T]
7569+
self.assertEqual(C2.__parameters__, (T,))
7570+
self.assertEqual(C2.__args__, ((int, T), T))
7571+
self.assertEqual(C2[str], PandT[[int, str], str])
7572+
7573+
C3 = PandT[[int, *Ts], T]
7574+
self.assertEqual(C3.__parameters__, (Ts, T))
7575+
self.assertEqual(C3.__args__, ((int, *Ts), T))
7576+
self.assertEqual(C3[str, bool, bytes], PandT[[int, str, bool], bytes])
7577+
74577578
def test_paramspec_in_nested_generics(self):
74587579
# Although ParamSpec should not be found in __parameters__ of most
74597580
# generics, they probably should be found when nested in

Lib/typing.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -250,10 +250,17 @@ def _collect_parameters(args):
250250
"""
251251
parameters = []
252252
for t in args:
253-
# We don't want __parameters__ descriptor of a bare Python class.
254253
if isinstance(t, type):
255-
continue
256-
if hasattr(t, '__typing_subst__'):
254+
# We don't want __parameters__ descriptor of a bare Python class.
255+
pass
256+
elif isinstance(t, tuple):
257+
# `t` might be a tuple, when `ParamSpec` is substituted with
258+
# `[T, int]`, or `[int, *Ts]`, etc.
259+
for x in t:
260+
for collected in _collect_parameters([x]):
261+
if collected not in parameters:
262+
parameters.append(collected)
263+
elif hasattr(t, '__typing_subst__'):
257264
if t not in parameters:
258265
parameters.append(t)
259266
else:
@@ -1416,10 +1423,12 @@ def _determine_new_args(self, args):
14161423
raise TypeError(f"Too {'many' if alen > plen else 'few'} arguments for {self};"
14171424
f" actual {alen}, expected {plen}")
14181425
new_arg_by_param = dict(zip(params, args))
1426+
return tuple(self._make_substitution(self.__args__, new_arg_by_param))
14191427

1428+
def _make_substitution(self, args, new_arg_by_param):
1429+
"""Create a list of new type arguments."""
14201430
new_args = []
1421-
for old_arg in self.__args__:
1422-
1431+
for old_arg in args:
14231432
if isinstance(old_arg, type):
14241433
new_args.append(old_arg)
14251434
continue
@@ -1463,10 +1472,20 @@ def _determine_new_args(self, args):
14631472
# should join all these types together in a flat list
14641473
# `(float, int, str)` - so again, we should `extend`.
14651474
new_args.extend(new_arg)
1475+
elif isinstance(old_arg, tuple):
1476+
# Corner case:
1477+
# P = ParamSpec('P')
1478+
# T = TypeVar('T')
1479+
# class Base(Generic[P]): ...
1480+
# Can be substituted like this:
1481+
# X = Base[[int, T]]
1482+
# In this case, `old_arg` will be a tuple:
1483+
new_args.append(
1484+
tuple(self._make_substitution(old_arg, new_arg_by_param)),
1485+
)
14661486
else:
14671487
new_args.append(new_arg)
1468-
1469-
return tuple(new_args)
1488+
return new_args
14701489

14711490
def copy_with(self, args):
14721491
return self.__class__(self.__origin__, args, name=self._name, inst=self._inst,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
typing: Fix a bug relating to substitution in custom classes generic over a
2+
:class:`~typing.ParamSpec`. Previously, if the ``ParamSpec`` was substituted
3+
with a parameters list that itself contained a :class:`~typing.TypeVar`, the
4+
``TypeVar`` in the parameters list could not be subsequently substituted. This
5+
is now fixed.
6+
7+
Patch by Nikita Sobolev.

0 commit comments

Comments
 (0)
0