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

Skip to content

Commit adb0621

Browse files
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>
1 parent baf4eb0 commit adb0621

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
@@ -7679,6 +7679,127 @@ def test_bad_var_substitution(self):
76797679
with self.assertRaises(TypeError):
76807680
collections.abc.Callable[P, T][arg, str]
76817681

7682+
def test_type_var_subst_for_other_type_vars(self):
7683+
T = TypeVar('T')
7684+
T2 = TypeVar('T2')
7685+
P = ParamSpec('P')
7686+
P2 = ParamSpec('P2')
7687+
Ts = TypeVarTuple('Ts')
7688+
7689+
class Base(Generic[P]):
7690+
pass
7691+
7692+
A1 = Base[T]
7693+
self.assertEqual(A1.__parameters__, (T,))
7694+
self.assertEqual(A1.__args__, ((T,),))
7695+
self.assertEqual(A1[int], Base[int])
7696+
7697+
A2 = Base[[T]]
7698+
self.assertEqual(A2.__parameters__, (T,))
7699+
self.assertEqual(A2.__args__, ((T,),))
7700+
self.assertEqual(A2[int], Base[int])
7701+
7702+
A3 = Base[[int, T]]
7703+
self.assertEqual(A3.__parameters__, (T,))
7704+
self.assertEqual(A3.__args__, ((int, T),))
7705+
self.assertEqual(A3[str], Base[[int, str]])
7706+
7707+
A4 = Base[[T, int, T2]]
7708+
self.assertEqual(A4.__parameters__, (T, T2))
7709+
self.assertEqual(A4.__args__, ((T, int, T2),))
7710+
self.assertEqual(A4[str, bool], Base[[str, int, bool]])
7711+
7712+
A5 = Base[[*Ts, int]]
7713+
self.assertEqual(A5.__parameters__, (Ts,))
7714+
self.assertEqual(A5.__args__, ((*Ts, int),))
7715+
self.assertEqual(A5[str, bool], Base[[str, bool, int]])
7716+
7717+
A5_2 = Base[[int, *Ts]]
7718+
self.assertEqual(A5_2.__parameters__, (Ts,))
7719+
self.assertEqual(A5_2.__args__, ((int, *Ts),))
7720+
self.assertEqual(A5_2[str, bool], Base[[int, str, bool]])
7721+
7722+
A6 = Base[[T, *Ts]]
7723+
self.assertEqual(A6.__parameters__, (T, Ts))
7724+
self.assertEqual(A6.__args__, ((T, *Ts),))
7725+
self.assertEqual(A6[int, str, bool], Base[[int, str, bool]])
7726+
7727+
A7 = Base[[T, T]]
7728+
self.assertEqual(A7.__parameters__, (T,))
7729+
self.assertEqual(A7.__args__, ((T, T),))
7730+
self.assertEqual(A7[int], Base[[int, int]])
7731+
7732+
A8 = Base[[T, list[T]]]
7733+
self.assertEqual(A8.__parameters__, (T,))
7734+
self.assertEqual(A8.__args__, ((T, list[T]),))
7735+
self.assertEqual(A8[int], Base[[int, list[int]]])
7736+
7737+
A9 = Base[[Tuple[*Ts], *Ts]]
7738+
self.assertEqual(A9.__parameters__, (Ts,))
7739+
self.assertEqual(A9.__args__, ((Tuple[*Ts], *Ts),))
7740+
self.assertEqual(A9[int, str], Base[Tuple[int, str], int, str])
7741+
7742+
A10 = Base[P2]
7743+
self.assertEqual(A10.__parameters__, (P2,))
7744+
self.assertEqual(A10.__args__, (P2,))
7745+
self.assertEqual(A10[[int, str]], Base[[int, str]])
7746+
7747+
class DoubleP(Generic[P, P2]):
7748+
pass
7749+
7750+
B1 = DoubleP[P, P2]
7751+
self.assertEqual(B1.__parameters__, (P, P2))
7752+
self.assertEqual(B1.__args__, (P, P2))
7753+
self.assertEqual(B1[[int, str], [bool]], DoubleP[[int, str], [bool]])
7754+
self.assertEqual(B1[[], []], DoubleP[[], []])
7755+
7756+
B2 = DoubleP[[int, str], P2]
7757+
self.assertEqual(B2.__parameters__, (P2,))
7758+
self.assertEqual(B2.__args__, ((int, str), P2))
7759+
self.assertEqual(B2[[bool, bool]], DoubleP[[int, str], [bool, bool]])
7760+
self.assertEqual(B2[[]], DoubleP[[int, str], []])
7761+
7762+
B3 = DoubleP[P, [bool, bool]]
7763+
self.assertEqual(B3.__parameters__, (P,))
7764+
self.assertEqual(B3.__args__, (P, (bool, bool)))
7765+
self.assertEqual(B3[[int, str]], DoubleP[[int, str], [bool, bool]])
7766+
self.assertEqual(B3[[]], DoubleP[[], [bool, bool]])
7767+
7768+
B4 = DoubleP[[T, int], [bool, T2]]
7769+
self.assertEqual(B4.__parameters__, (T, T2))
7770+
self.assertEqual(B4.__args__, ((T, int), (bool, T2)))
7771+
self.assertEqual(B4[str, float], DoubleP[[str, int], [bool, float]])
7772+
7773+
B5 = DoubleP[[*Ts, int], [bool, T2]]
7774+
self.assertEqual(B5.__parameters__, (Ts, T2))
7775+
self.assertEqual(B5.__args__, ((*Ts, int), (bool, T2)))
7776+
self.assertEqual(B5[str, bytes, float],
7777+
DoubleP[[str, bytes, int], [bool, float]])
7778+
7779+
B6 = DoubleP[[T, int], [bool, *Ts]]
7780+
self.assertEqual(B6.__parameters__, (T, Ts))
7781+
self.assertEqual(B6.__args__, ((T, int), (bool, *Ts)))
7782+
self.assertEqual(B6[str, bytes, float],
7783+
DoubleP[[str, int], [bool, bytes, float]])
7784+
7785+
class PandT(Generic[P, T]):
7786+
pass
7787+
7788+
C1 = PandT[P, T]
7789+
self.assertEqual(C1.__parameters__, (P, T))
7790+
self.assertEqual(C1.__args__, (P, T))
7791+
self.assertEqual(C1[[int, str], bool], PandT[[int, str], bool])
7792+
7793+
C2 = PandT[[int, T], T]
7794+
self.assertEqual(C2.__parameters__, (T,))
7795+
self.assertEqual(C2.__args__, ((int, T), T))
7796+
self.assertEqual(C2[str], PandT[[int, str], str])
7797+
7798+
C3 = PandT[[int, *Ts], T]
7799+
self.assertEqual(C3.__parameters__, (Ts, T))
7800+
self.assertEqual(C3.__args__, ((int, *Ts), T))
7801+
self.assertEqual(C3[str, bool, bytes], PandT[[int, str, bool], bytes])
7802+
76827803
def test_paramspec_in_nested_generics(self):
76837804
# Although ParamSpec should not be found in __parameters__ of most
76847805
# 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
@@ -255,10 +255,17 @@ def _collect_parameters(args):
255255
"""
256256
parameters = []
257257
for t in args:
258-
# We don't want __parameters__ descriptor of a bare Python class.
259258
if isinstance(t, type):
260-
continue
261-
if hasattr(t, '__typing_subst__'):
259+
# We don't want __parameters__ descriptor of a bare Python class.
260+
pass
261+
elif isinstance(t, tuple):
262+
# `t` might be a tuple, when `ParamSpec` is substituted with
263+
# `[T, int]`, or `[int, *Ts]`, etc.
264+
for x in t:
265+
for collected in _collect_parameters([x]):
266+
if collected not in parameters:
267+
parameters.append(collected)
268+
elif hasattr(t, '__typing_subst__'):
262269
if t not in parameters:
263270
parameters.append(t)
264271
else:
@@ -1441,10 +1448,12 @@ def _determine_new_args(self, args):
14411448
raise TypeError(f"Too {'many' if alen > plen else 'few'} arguments for {self};"
14421449
f" actual {alen}, expected {plen}")
14431450
new_arg_by_param = dict(zip(params, args))
1451+
return tuple(self._make_substitution(self.__args__, new_arg_by_param))
14441452

1453+
def _make_substitution(self, args, new_arg_by_param):
1454+
"""Create a list of new type arguments."""
14451455
new_args = []
1446-
for old_arg in self.__args__:
1447-
1456+
for old_arg in args:
14481457
if isinstance(old_arg, type):
14491458
new_args.append(old_arg)
14501459
continue
@@ -1488,10 +1497,20 @@ def _determine_new_args(self, args):
14881497
# should join all these types together in a flat list
14891498
# `(float, int, str)` - so again, we should `extend`.
14901499
new_args.extend(new_arg)
1500+
elif isinstance(old_arg, tuple):
1501+
# Corner case:
1502+
# P = ParamSpec('P')
1503+
# T = TypeVar('T')
1504+
# class Base(Generic[P]): ...
1505+
# Can be substituted like this:
1506+
# X = Base[[int, T]]
1507+
# In this case, `old_arg` will be a tuple:
1508+
new_args.append(
1509+
tuple(self._make_substitution(old_arg, new_arg_by_param)),
1510+
)
14911511
else:
14921512
new_args.append(new_arg)
1493-
1494-
return tuple(new_args)
1513+
return new_args
14951514

14961515
def copy_with(self, args):
14971516
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