Skip to content

Commit a4a6976

Browse files
committed
gh-140665: Substitute PEP 696 defaults referencing other parameters
A type parameter's default may reference an earlier parameter in the same scope (e.g. class C[T, S = T], or S = list[T]), but the default was inserted unsubstituted when a trailing parameter was filled from it. Parametrizing C[int] produced C[int, T] instead of C[int, int], leaking the unbound type variable; nested defaults (S = list[T]) and chained defaults (S = T, U = S) were left unbound too. Add _resolve_parameter_defaults, called from _generic_class_getitem and _GenericAlias._determine_new_args, which substitutes the now-bound parameters into any argument supplied by a default. Detection is gated on the argument tuple growing during __typing_prepare_subst__, so explicitly-passed arguments such as C[int, T] are left untouched.
1 parent e51b616 commit a4a6976

3 files changed

Lines changed: 83 additions & 0 deletions

File tree

Lib/test/test_typing.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,36 @@ class A(Generic[T, U, Unpack[Ts]]): ...
706706
self.assertEqual(A[int, str, range].__args__, (int, str, range))
707707
self.assertEqual(A[int, str, *tuple[int, ...]].__args__, (int, str, *tuple[int, ...]))
708708

709+
def test_typevar_default_referencing_other_typevar(self):
710+
# gh-140665: a type parameter default that references an earlier type
711+
# parameter is substituted when the generic is parameterized.
712+
T = TypeVar("T")
713+
S = TypeVar("S", default=T)
714+
class A(Generic[T, S]): ...
715+
self.assertEqual(A[int].__args__, (int, int))
716+
self.assertEqual(A[str].__args__, (str, str))
717+
# an explicit argument is not overridden by the default
718+
self.assertEqual(A[int, str].__args__, (int, str))
719+
720+
# PEP 695 syntax
721+
class B[T, S = T]: ...
722+
self.assertEqual(B[int].__args__, (int, int))
723+
self.assertEqual(B[int, str].__args__, (int, str))
724+
725+
# a default that contains an earlier type parameter
726+
class C[T, S = list[T]]: ...
727+
self.assertEqual(C[int].__args__, (int, list[int]))
728+
self.assertEqual(C[int, str].__args__, (int, str))
729+
730+
# chained defaults
731+
class D[T, S = T, U = S]: ...
732+
self.assertEqual(D[int].__args__, (int, int, int))
733+
self.assertEqual(D[int, str].__args__, (int, str, str))
734+
735+
# a concrete default is unaffected
736+
class E[T, S = int]: ...
737+
self.assertEqual(E[str].__args__, (str, int))
738+
709739
def test_no_default_after_typevar_tuple(self):
710740
T = TypeVar("T", default=int)
711741
Ts = TypeVarTuple("Ts")

Lib/typing.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,40 @@ def _paramspec_prepare_subst(self, alias, args):
11381138
return args
11391139

11401140

1141+
def _resolve_parameter_defaults(parameters, args, defaulted):
1142+
"""Substitute now-bound parameters into PEP 696 defaults (gh-140665).
1143+
1144+
A type parameter's default may reference earlier parameters in the same
1145+
scope, e.g. ``class C[T, S = T]`` or ``class C[T, S = list[T]]``. Such a
1146+
default is stored unsubstituted, so once every parameter is bound to an
1147+
argument we replace those references here. *defaulted* is the set of
1148+
parameters whose argument came from their default (not from an explicit
1149+
argument). *args* is positional with *parameters*; a new tuple is returned.
1150+
"""
1151+
arg_by_param = dict(zip(parameters, args))
1152+
for param in parameters:
1153+
if param not in defaulted:
1154+
continue
1155+
default = arg_by_param[param]
1156+
if isinstance(default, type):
1157+
continue
1158+
if getattr(default, '__typing_subst__', None) is not None:
1159+
# The default is itself a parameter, e.g. ``S = T``.
1160+
arg_by_param[param] = arg_by_param.get(default, default)
1161+
else:
1162+
subparams = getattr(default, '__parameters__', ())
1163+
if subparams:
1164+
# The default contains parameters, e.g. ``S = list[T]``.
1165+
subargs = []
1166+
for x in subparams:
1167+
if isinstance(x, TypeVarTuple):
1168+
subargs.extend(arg_by_param[x])
1169+
else:
1170+
subargs.append(arg_by_param.get(x, x))
1171+
arg_by_param[param] = default[tuple(subargs)]
1172+
return tuple(arg_by_param[p] for p in parameters)
1173+
1174+
11411175
@_tp_cache
11421176
def _generic_class_getitem(cls, args):
11431177
"""Parameterizes a generic class.
@@ -1182,11 +1216,18 @@ def _generic_class_getitem(cls, args):
11821216
f"calling 'super().__init_subclass__()'"
11831217
)
11841218
raise
1219+
defaulted = set()
11851220
for param in parameters:
11861221
prepare = getattr(param, '__typing_prepare_subst__', None)
11871222
if prepare is not None:
1223+
prev_len = len(args)
11881224
args = prepare(cls, args)
1225+
if (len(args) > prev_len and isinstance(param, TypeVar)
1226+
and param.has_default()):
1227+
defaulted.add(param)
11891228
_check_generic_specialization(cls, args)
1229+
if defaulted:
1230+
args = _resolve_parameter_defaults(parameters, args, defaulted)
11901231

11911232
new_args = []
11921233
for param, new_arg in zip(parameters, args):
@@ -1450,15 +1491,22 @@ class A(Generic[T1, T2]): pass
14501491
"""
14511492
params = self.__parameters__
14521493
# In the example above, this would be {T3: str}
1494+
defaulted = set()
14531495
for param in params:
14541496
prepare = getattr(param, '__typing_prepare_subst__', None)
14551497
if prepare is not None:
1498+
prev_len = len(args)
14561499
args = prepare(self, args)
1500+
if (len(args) > prev_len and isinstance(param, TypeVar)
1501+
and param.has_default()):
1502+
defaulted.add(param)
14571503
alen = len(args)
14581504
plen = len(params)
14591505
if alen != plen:
14601506
raise TypeError(f"Too {'many' if alen > plen else 'few'} arguments for {self};"
14611507
f" actual {alen}, expected {plen}")
1508+
if defaulted:
1509+
args = _resolve_parameter_defaults(params, args, defaulted)
14621510
new_arg_by_param = dict(zip(params, args))
14631511
return tuple(self._make_substitution(self.__args__, new_arg_by_param))
14641512

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix runtime substitution of :pep:`696` type parameter defaults that reference
2+
earlier type parameters. Parameterizing, for example, ``class C[T, S = T]``
3+
as ``C[int]`` now yields ``C[int, int]`` (and ``S = list[T]`` yields
4+
``list[int]``) instead of leaving the default unsubstituted. Patch by Olayinka
5+
Vaughan.

0 commit comments

Comments
 (0)