Skip to content

Commit be6a3c3

Browse files
committed
gh-140596: Substitute earlier type parameters into PEP 696 defaults
The default of a type parameter may refer to type parameters that appear earlier in the same type parameter list. Those references are now replaced by the values supplied for the earlier type parameters when the default is filled in, so ``class Bar[T, S = T]`` gives ``Bar[int, int]`` for ``Bar[int]`` instead of leaving ``S`` bound to the unsubstituted ``T``. Leaving it unsubstituted leaked ``T`` into the specialization, which made ``class Baz[U](Bar[U])`` fail with "Some type variables (T) are not listed in Generic[U]". The substitution happens in ``__typing_prepare_subst__``, where the values of all earlier type parameters are already known, so it applies to ``TypeVar``, ``ParamSpec`` and ``TypeVarTuple`` defaults and to both generic classes and generic aliases. A default that refers to a type parameter which is not declared before it, whether a self-reference (``class A[T = T]``) or a cycle (``class A[T = S, S = T]``), now raises TypeError when the default is used.
1 parent bc31217 commit be6a3c3

6 files changed

Lines changed: 229 additions & 4 deletions

File tree

Doc/reference/compound_stmts.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1845,6 +1845,18 @@ is accessed. To this end, the default value is evaluated in a separate
18451845
for a type parameter, the ``__default__`` attribute is set to the special
18461846
sentinel object :data:`typing.NoDefault`.
18471847

1848+
A default value may refer to type parameters that appear earlier in the same
1849+
type parameter list. Such a reference is replaced by the value that was
1850+
supplied for that type parameter::
1851+
1852+
class Bar[T, S = list[T]]: ...
1853+
1854+
Bar[int] # equivalent to Bar[int, list[int]]
1855+
1856+
Referring to a type parameter that does not appear earlier in the same type
1857+
parameter list, including the type parameter itself, raises :exc:`TypeError`
1858+
when the default is used.
1859+
18481860
The following example indicates the full set of allowed type parameter declarations::
18491861

18501862
def overly_generic[

Lib/test/test_type_params.py

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import weakref
88
from test.support import check_syntax_error, run_code, run_no_yield_async_fn
99

10-
from typing import Generic, NoDefault, Sequence, TypeAliasType, TypeVar, TypeVarTuple, ParamSpec, get_args
10+
from typing import (Callable, Generic, NoDefault, Sequence, TypeAliasType,
11+
TypeVar, TypeVarTuple, ParamSpec, get_args)
1112

1213

1314
class TypeParamsInvalidTest(unittest.TestCase):
@@ -1418,6 +1419,111 @@ def test_symtable_key_regression_name(self):
14181419
self.assertEqual(ns["X1"].__type_params__[0].__default__, "A")
14191420
self.assertEqual(ns["X2"].__type_params__[0].__default__, "B")
14201421

1422+
def test_default_refers_to_earlier_type_param(self):
1423+
class A[T1, T2=T1]: ...
1424+
1425+
self.assertEqual(A[int].__args__, (int, int))
1426+
self.assertEqual(A[int, str].__args__, (int, str))
1427+
1428+
def test_default_refers_to_earlier_type_param_chain(self):
1429+
class A[T1, T2=T1, T3=T2]: ...
1430+
1431+
self.assertEqual(A[int].__args__, (int, int, int))
1432+
self.assertEqual(A[int, str].__args__, (int, str, str))
1433+
self.assertEqual(A[int, str, bool].__args__, (int, str, bool))
1434+
1435+
def test_default_refers_to_earlier_type_param_nested(self):
1436+
class A[T1, T2=list[T1]]: ...
1437+
1438+
self.assertEqual(A[int].__args__, (int, list[int]))
1439+
1440+
class B[T1, T2, T3=dict[T1, T2]]: ...
1441+
1442+
self.assertEqual(B[int, str].__args__, (int, str, dict[int, str]))
1443+
1444+
class C[T1, T2, T3=T1 | T2]: ...
1445+
1446+
self.assertEqual(C[int, str].__args__, (int, str, int | str))
1447+
1448+
class D[T1, T2=Callable[[T1], T1]]: ...
1449+
1450+
self.assertEqual(D[int].__args__, (int, Callable[[int], int]))
1451+
1452+
def test_default_refers_to_earlier_type_param_typevartuple(self):
1453+
class A[T1, *Ts=*tuple[T1, ...]]: ...
1454+
1455+
self.assertEqual(A[int].__args__, (int, *tuple[int, ...]))
1456+
1457+
class B[T1, T2, *Ts=*tuple[T1, T2]]: ...
1458+
1459+
self.assertEqual(B[int, str].__args__, (int, str, int, str))
1460+
1461+
def test_default_refers_to_earlier_type_param_paramspec(self):
1462+
class A[T1, **P=[T1, int]]: ...
1463+
1464+
self.assertEqual(A[str].__args__, (str, (str, int)))
1465+
1466+
class B[**P, T=int]: ...
1467+
1468+
self.assertEqual(B[[int, str]].__args__, ((int, str), int))
1469+
1470+
def test_default_refers_to_earlier_type_param_in_base_class(self):
1471+
# gh-140596: omitting a type parameter with a default when
1472+
# subclassing used to leave the default unsubstituted, which made
1473+
# the type parameter it refers to leak into the subclass.
1474+
class Bar[T, S=T]: ...
1475+
class Baz[U](Bar[U]): ...
1476+
1477+
U, = Baz.__type_params__
1478+
self.assertEqual(Baz.__orig_bases__[0].__args__, (U, U))
1479+
self.assertEqual(Baz.__parameters__, (U,))
1480+
self.assertEqual(Baz[int].__args__, (int,))
1481+
1482+
def test_default_refers_to_type_param_from_enclosing_scope(self):
1483+
# A default that refers to a type variable which is not a type
1484+
# parameter of the class itself is left untouched.
1485+
T = TypeVar('T')
1486+
S = TypeVar('S', default=T)
1487+
class A(Generic[S]): ...
1488+
1489+
self.assertEqual(A[()].__args__, (T,))
1490+
1491+
def test_default_refers_to_type_param_supplied_by_the_user(self):
1492+
T = TypeVar('T')
1493+
class A[T1, T2=T1]: ...
1494+
1495+
self.assertEqual(A[T].__args__, (T, T))
1496+
1497+
def test_default_refers_to_itself(self):
1498+
class A[T1=T1]: ...
1499+
1500+
with self.assertRaisesRegex(
1501+
TypeError,
1502+
r"The default of type parameter T1 refers to type parameter T1, "
1503+
r"which is not declared before it",
1504+
):
1505+
A[()]
1506+
1507+
def test_default_refers_to_later_type_param(self):
1508+
class A[T1=T2, T2=int]: ...
1509+
1510+
with self.assertRaisesRegex(
1511+
TypeError,
1512+
r"The default of type parameter T1 refers to type parameter T2, "
1513+
r"which is not declared before it",
1514+
):
1515+
A[()]
1516+
1517+
def test_defaults_refer_to_each_other(self):
1518+
class A[T1=T2, T2=T1]: ...
1519+
1520+
with self.assertRaisesRegex(
1521+
TypeError,
1522+
r"The default of type parameter T1 refers to type parameter T2, "
1523+
r"which is not declared before it",
1524+
):
1525+
A[()]
1526+
14211527

14221528
class TestEvaluateFunctions(unittest.TestCase):
14231529
def test_general(self):

Lib/test/test_typing.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,36 @@ def test_pickle(self):
826826
self.assertEqual(z.__bound__, typevar.__bound__)
827827
self.assertEqual(z.__default__, typevar.__default__)
828828

829+
def test_default_referring_to_earlier_type_param(self):
830+
T = TypeVar('T')
831+
U = TypeVar('U', default=T)
832+
V = TypeVar('V', default=List[U])
833+
834+
class A(Generic[T, U, V]): ...
835+
836+
self.assertEqual(A[int].__args__, (int, int, List[int]))
837+
self.assertEqual(A[int, str].__args__, (int, str, List[str]))
838+
self.assertEqual(A[int, str, bool].__args__, (int, str, bool))
839+
840+
def test_default_referring_to_earlier_type_param_alias(self):
841+
T = TypeVar('T')
842+
U = TypeVar('U', default=T)
843+
Alias = Union[T, U]
844+
845+
self.assertEqual(Alias[int], int)
846+
self.assertEqual(Alias[int, str], Union[int, str])
847+
848+
def test_default_referring_to_earlier_paramspec_and_typevartuple(self):
849+
T = TypeVar('T')
850+
Ts = TypeVarTuple('Ts', default=Unpack[Tuple[T, int]])
851+
P = ParamSpec('P', default=[T, int])
852+
853+
class A(Generic[T, Unpack[Ts]]): ...
854+
self.assertEqual(A[str].__args__, (str, str, int))
855+
856+
class B(Generic[T, P]): ...
857+
self.assertEqual(B[str].__args__, (str, (str, int)))
858+
829859

830860
def template_replace(templates: list[str], replacements: dict[str, list[str]]) -> list[tuple[str]]:
831861
"""Renders templates with possible combinations of replacements.

Lib/typing.py

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,62 @@ def _typevar_subst(self, arg):
10901090
return arg
10911091

10921092

1093+
def _resolve_type_param_default(param, default, params, resolved):
1094+
"""Substitute already-bound type parameters into a type parameter default.
1095+
1096+
PEP 696 allows the default of a type parameter to refer to type parameters
1097+
that appear earlier in the same type parameter list, for example::
1098+
1099+
class A[T, S = list[T]]: ...
1100+
1101+
`param` is the type parameter whose `default` is being filled in, `params`
1102+
is the full list of type parameters of the object being subscripted, and
1103+
`resolved` holds the values that have already been determined for the
1104+
leading ``len(resolved)`` of them, so that ``params[len(resolved)]`` is
1105+
`param` itself.
1106+
1107+
Type parameters that come from an enclosing scope (they do not appear in
1108+
`params`) are left untouched. Referring to a type parameter that is not
1109+
bound yet is an error; that covers both self-references such as
1110+
``class A[T = T]`` and cycles such as ``class A[T = S, S = T]``.
1111+
"""
1112+
bound = dict(zip(params, resolved))
1113+
unbound = frozenset(params[len(resolved):])
1114+
1115+
def resolve(value):
1116+
if isinstance(value, (TypeVar, ParamSpec, TypeVarTuple)):
1117+
if value in bound:
1118+
return bound[value]
1119+
if value in unbound:
1120+
raise TypeError(
1121+
f"The default of type parameter {param} refers to type "
1122+
f"parameter {value}, which is not declared before it"
1123+
)
1124+
return value
1125+
if isinstance(value, list):
1126+
return [resolve(v) for v in value]
1127+
if isinstance(value, tuple):
1128+
return tuple(resolve(v) for v in value)
1129+
subparams = getattr(value, '__parameters__', ())
1130+
if not subparams:
1131+
return value
1132+
subargs = []
1133+
changed = False
1134+
for subparam in subparams:
1135+
new_subarg = resolve(subparam)
1136+
changed |= new_subarg is not subparam
1137+
if (isinstance(subparam, TypeVarTuple)
1138+
and isinstance(new_subarg, tuple)):
1139+
subargs.extend(new_subarg)
1140+
else:
1141+
subargs.append(new_subarg)
1142+
if not changed:
1143+
return value
1144+
return value[tuple(subargs)]
1145+
1146+
return resolve(default)
1147+
1148+
10931149
def _typevartuple_prepare_subst(self, alias, args):
10941150
params = alias.__parameters__
10951151
typevartuple_index = params.index(self)
@@ -1118,7 +1174,9 @@ def _typevartuple_prepare_subst(self, alias, args):
11181174
raise TypeError(f"Too few arguments for {alias};"
11191175
f" actual {alen}, expected at least {plen-1}")
11201176
if left == alen - right and self.has_default():
1121-
replacement = _unpack_args(self.__default__)
1177+
default = _resolve_type_param_default(self, self.__default__, params,
1178+
args[:left])
1179+
replacement = _unpack_args(default)
11221180
else:
11231181
replacement = args[left: alen - right]
11241182

@@ -1144,7 +1202,9 @@ def _paramspec_prepare_subst(self, alias, args):
11441202
params = alias.__parameters__
11451203
i = params.index(self)
11461204
if i == len(args) and self.has_default():
1147-
args = (*args, self.__default__)
1205+
default = _resolve_type_param_default(self, self.__default__, params,
1206+
args)
1207+
args = (*args, default)
11481208
if i >= len(args):
11491209
raise TypeError(f"Too few arguments for {alias}")
11501210
# Special case where Z[[int, str, bool]] == Z[int, str, bool] in PEP 612.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Fix substitution of :pep:`696` type parameter defaults that refer to earlier
2+
type parameters in the same type parameter list. ``class Bar[T, S = T]`` now
3+
resolves ``Bar[int]`` to ``Bar[int, int]`` instead of leaving ``S`` bound to
4+
the unsubstituted ``T``, which previously made ``class Baz[U](Bar[U])`` raise
5+
:exc:`TypeError`. A default that refers to a type parameter which is not
6+
declared before it now raises :exc:`TypeError` when it is used.

Objects/typevarobject.c

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -807,8 +807,19 @@ typevar_typing_prepare_subst_impl(typevarobject *self, PyObject *alias,
807807
return NULL;
808808
}
809809
if (dflt != &_Py_NoDefaultStruct) {
810-
PyObject *new_args = PyTuple_Pack(1, dflt);
810+
// The default may refer to type parameters that appear earlier in
811+
// the same type parameter list; those are already resolved in
812+
// "args", so substitute them in.
813+
PyObject *resolve_args[4] = {(PyObject *)self, dflt, params, args};
814+
PyObject *resolved = call_typing_func_object(
815+
"_resolve_type_param_default", resolve_args, 4);
811816
Py_DECREF(dflt);
817+
if (resolved == NULL) {
818+
Py_DECREF(params);
819+
return NULL;
820+
}
821+
PyObject *new_args = PyTuple_Pack(1, resolved);
822+
Py_DECREF(resolved);
812823
if (new_args == NULL) {
813824
Py_DECREF(params);
814825
return NULL;

0 commit comments

Comments
 (0)