Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,36 @@ class A(Generic[T, U, Unpack[Ts]]): ...
self.assertEqual(A[int, str, range].__args__, (int, str, range))
self.assertEqual(A[int, str, *tuple[int, ...]].__args__, (int, str, *tuple[int, ...]))

def test_typevar_default_referencing_other_typevar(self):
# gh-140665: a type parameter default that references an earlier type
# parameter is substituted when the generic is parameterized.
T = TypeVar("T")
S = TypeVar("S", default=T)
class A(Generic[T, S]): ...
self.assertEqual(A[int].__args__, (int, int))
self.assertEqual(A[str].__args__, (str, str))
# an explicit argument is not overridden by the default
self.assertEqual(A[int, str].__args__, (int, str))

# PEP 695 syntax
class B[T, S = T]: ...
self.assertEqual(B[int].__args__, (int, int))
self.assertEqual(B[int, str].__args__, (int, str))

# a default that contains an earlier type parameter
class C[T, S = list[T]]: ...
self.assertEqual(C[int].__args__, (int, list[int]))
self.assertEqual(C[int, str].__args__, (int, str))

# chained defaults
class D[T, S = T, U = S]: ...
self.assertEqual(D[int].__args__, (int, int, int))
self.assertEqual(D[int, str].__args__, (int, str, str))

# a concrete default is unaffected
class E[T, S = int]: ...
self.assertEqual(E[str].__args__, (str, int))

def test_no_default_after_typevar_tuple(self):
T = TypeVar("T", default=int)
Ts = TypeVarTuple("Ts")
Expand Down
48 changes: 48 additions & 0 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,40 @@ def _paramspec_prepare_subst(self, alias, args):
return args


def _resolve_parameter_defaults(parameters, args, defaulted):
"""Substitute now-bound parameters into PEP 696 defaults (gh-140665).

A type parameter's default may reference earlier parameters in the same
scope, e.g. ``class C[T, S = T]`` or ``class C[T, S = list[T]]``. Such a
default is stored unsubstituted, so once every parameter is bound to an
argument we replace those references here. *defaulted* is the set of
parameters whose argument came from their default (not from an explicit
argument). *args* is positional with *parameters*; a new tuple is returned.
"""
arg_by_param = dict(zip(parameters, args))
for param in parameters:
if param not in defaulted:
continue
default = arg_by_param[param]
if isinstance(default, type):
continue
if getattr(default, '__typing_subst__', None) is not None:
# The default is itself a parameter, e.g. ``S = T``.
arg_by_param[param] = arg_by_param.get(default, default)
else:
subparams = getattr(default, '__parameters__', ())
if subparams:
# The default contains parameters, e.g. ``S = list[T]``.
subargs = []
for x in subparams:
if isinstance(x, TypeVarTuple):
subargs.extend(arg_by_param[x])
else:
subargs.append(arg_by_param.get(x, x))
arg_by_param[param] = default[tuple(subargs)]
return tuple(arg_by_param[p] for p in parameters)


@_tp_cache
def _generic_class_getitem(cls, args):
"""Parameterizes a generic class.
Expand Down Expand Up @@ -1182,11 +1216,18 @@ def _generic_class_getitem(cls, args):
f"calling 'super().__init_subclass__()'"
)
raise
defaulted = set()
for param in parameters:
prepare = getattr(param, '__typing_prepare_subst__', None)
if prepare is not None:
prev_len = len(args)
args = prepare(cls, args)
if (len(args) > prev_len and isinstance(param, TypeVar)
and param.has_default()):
defaulted.add(param)
_check_generic_specialization(cls, args)
if defaulted:
args = _resolve_parameter_defaults(parameters, args, defaulted)

new_args = []
for param, new_arg in zip(parameters, args):
Expand Down Expand Up @@ -1450,15 +1491,22 @@ class A(Generic[T1, T2]): pass
"""
params = self.__parameters__
# In the example above, this would be {T3: str}
defaulted = set()
for param in params:
prepare = getattr(param, '__typing_prepare_subst__', None)
if prepare is not None:
prev_len = len(args)
args = prepare(self, args)
if (len(args) > prev_len and isinstance(param, TypeVar)
and param.has_default()):
defaulted.add(param)
alen = len(args)
plen = len(params)
if alen != plen:
raise TypeError(f"Too {'many' if alen > plen else 'few'} arguments for {self};"
f" actual {alen}, expected {plen}")
if defaulted:
args = _resolve_parameter_defaults(params, args, defaulted)
new_arg_by_param = dict(zip(params, args))
return tuple(self._make_substitution(self.__args__, new_arg_by_param))

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fix runtime substitution of :pep:`696` type parameter defaults that reference
earlier type parameters. Parameterizing, for example, ``class C[T, S = T]``
as ``C[int]`` now yields ``C[int, int]`` (and ``S = list[T]`` yields
``list[int]``) instead of leaving the default unsubstituted. Patch by Olayinka
Vaughan.
Loading