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
12 changes: 12 additions & 0 deletions Doc/reference/compound_stmts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1845,6 +1845,18 @@ is accessed. To this end, the default value is evaluated in a separate
for a type parameter, the ``__default__`` attribute is set to the special
sentinel object :data:`typing.NoDefault`.

A default value may refer to type parameters that appear earlier in the same
type parameter list. Such a reference is replaced by the value that was
supplied for that type parameter::

class Bar[T, S = list[T]]: ...

Bar[int] # equivalent to Bar[int, list[int]]

Referring to a type parameter that does not appear earlier in the same type
parameter list, including the type parameter itself, raises :exc:`TypeError`
when the default is used.

The following example indicates the full set of allowed type parameter declarations::

def overly_generic[
Expand Down
108 changes: 107 additions & 1 deletion Lib/test/test_type_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import weakref
from test.support import check_syntax_error, run_code, run_no_yield_async_fn

from typing import Generic, NoDefault, Sequence, TypeAliasType, TypeVar, TypeVarTuple, ParamSpec, get_args
from typing import (Callable, Generic, NoDefault, Sequence, TypeAliasType,
TypeVar, TypeVarTuple, ParamSpec, get_args)


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

def test_default_refers_to_earlier_type_param(self):
class A[T1, T2=T1]: ...

self.assertEqual(A[int].__args__, (int, int))
self.assertEqual(A[int, str].__args__, (int, str))

def test_default_refers_to_earlier_type_param_chain(self):
class A[T1, T2=T1, T3=T2]: ...

self.assertEqual(A[int].__args__, (int, int, int))
self.assertEqual(A[int, str].__args__, (int, str, str))
self.assertEqual(A[int, str, bool].__args__, (int, str, bool))

def test_default_refers_to_earlier_type_param_nested(self):
class A[T1, T2=list[T1]]: ...

self.assertEqual(A[int].__args__, (int, list[int]))

class B[T1, T2, T3=dict[T1, T2]]: ...

self.assertEqual(B[int, str].__args__, (int, str, dict[int, str]))

class C[T1, T2, T3=T1 | T2]: ...

self.assertEqual(C[int, str].__args__, (int, str, int | str))

class D[T1, T2=Callable[[T1], T1]]: ...

self.assertEqual(D[int].__args__, (int, Callable[[int], int]))

def test_default_refers_to_earlier_type_param_typevartuple(self):
class A[T1, *Ts=*tuple[T1, ...]]: ...

self.assertEqual(A[int].__args__, (int, *tuple[int, ...]))

class B[T1, T2, *Ts=*tuple[T1, T2]]: ...

self.assertEqual(B[int, str].__args__, (int, str, int, str))

def test_default_refers_to_earlier_type_param_paramspec(self):
class A[T1, **P=[T1, int]]: ...

self.assertEqual(A[str].__args__, (str, (str, int)))

class B[**P, T=int]: ...

self.assertEqual(B[[int, str]].__args__, ((int, str), int))

def test_default_refers_to_earlier_type_param_in_base_class(self):
# gh-140596: omitting a type parameter with a default when
# subclassing used to leave the default unsubstituted, which made
# the type parameter it refers to leak into the subclass.
class Bar[T, S=T]: ...
class Baz[U](Bar[U]): ...

U, = Baz.__type_params__
self.assertEqual(Baz.__orig_bases__[0].__args__, (U, U))
self.assertEqual(Baz.__parameters__, (U,))
self.assertEqual(Baz[int].__args__, (int,))

def test_default_refers_to_type_param_from_enclosing_scope(self):
# A default that refers to a type variable which is not a type
# parameter of the class itself is left untouched.
T = TypeVar('T')
S = TypeVar('S', default=T)
class A(Generic[S]): ...

self.assertEqual(A[()].__args__, (T,))

def test_default_refers_to_type_param_supplied_by_the_user(self):
T = TypeVar('T')
class A[T1, T2=T1]: ...

self.assertEqual(A[T].__args__, (T, T))

def test_default_refers_to_itself(self):
class A[T1=T1]: ...

with self.assertRaisesRegex(
TypeError,
r"The default of type parameter T1 refers to type parameter T1, "
r"which is not declared before it",
):
A[()]

def test_default_refers_to_later_type_param(self):
class A[T1=T2, T2=int]: ...

with self.assertRaisesRegex(
TypeError,
r"The default of type parameter T1 refers to type parameter T2, "
r"which is not declared before it",
):
A[()]

def test_defaults_refer_to_each_other(self):
class A[T1=T2, T2=T1]: ...

with self.assertRaisesRegex(
TypeError,
r"The default of type parameter T1 refers to type parameter T2, "
r"which is not declared before it",
):
A[()]


class TestEvaluateFunctions(unittest.TestCase):
def test_general(self):
Expand Down
30 changes: 30 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,36 @@ def test_pickle(self):
self.assertEqual(z.__bound__, typevar.__bound__)
self.assertEqual(z.__default__, typevar.__default__)

def test_default_referring_to_earlier_type_param(self):
T = TypeVar('T')
U = TypeVar('U', default=T)
V = TypeVar('V', default=List[U])

class A(Generic[T, U, V]): ...

self.assertEqual(A[int].__args__, (int, int, List[int]))
self.assertEqual(A[int, str].__args__, (int, str, List[str]))
self.assertEqual(A[int, str, bool].__args__, (int, str, bool))

def test_default_referring_to_earlier_type_param_alias(self):
T = TypeVar('T')
U = TypeVar('U', default=T)
Alias = Union[T, U]

self.assertEqual(Alias[int], int)
self.assertEqual(Alias[int, str], Union[int, str])

def test_default_referring_to_earlier_paramspec_and_typevartuple(self):
T = TypeVar('T')
Ts = TypeVarTuple('Ts', default=Unpack[Tuple[T, int]])
P = ParamSpec('P', default=[T, int])

class A(Generic[T, Unpack[Ts]]): ...
self.assertEqual(A[str].__args__, (str, str, int))

class B(Generic[T, P]): ...
self.assertEqual(B[str].__args__, (str, (str, int)))


def template_replace(templates: list[str], replacements: dict[str, list[str]]) -> list[tuple[str]]:
"""Renders templates with possible combinations of replacements.
Expand Down
64 changes: 62 additions & 2 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1090,6 +1090,62 @@ def _typevar_subst(self, arg):
return arg


def _resolve_type_param_default(param, default, params, resolved):
"""Substitute already-bound type parameters into a type parameter default.

PEP 696 allows the default of a type parameter to refer to type parameters
that appear earlier in the same type parameter list, for example::

class A[T, S = list[T]]: ...

`param` is the type parameter whose `default` is being filled in, `params`
is the full list of type parameters of the object being subscripted, and
`resolved` holds the values that have already been determined for the
leading ``len(resolved)`` of them, so that ``params[len(resolved)]`` is
`param` itself.

Type parameters that come from an enclosing scope (they do not appear in
`params`) are left untouched. Referring to a type parameter that is not
bound yet is an error; that covers both self-references such as
``class A[T = T]`` and cycles such as ``class A[T = S, S = T]``.
"""
bound = dict(zip(params, resolved))
unbound = frozenset(params[len(resolved):])

def resolve(value):
if isinstance(value, (TypeVar, ParamSpec, TypeVarTuple)):
if value in bound:
return bound[value]
if value in unbound:
raise TypeError(
f"The default of type parameter {param} refers to type "
f"parameter {value}, which is not declared before it"
)
return value
if isinstance(value, list):
return [resolve(v) for v in value]
if isinstance(value, tuple):
return tuple(resolve(v) for v in value)
subparams = getattr(value, '__parameters__', ())
if not subparams:
return value
subargs = []
changed = False
for subparam in subparams:
new_subarg = resolve(subparam)
changed |= new_subarg is not subparam
if (isinstance(subparam, TypeVarTuple)
and isinstance(new_subarg, tuple)):
subargs.extend(new_subarg)
else:
subargs.append(new_subarg)
if not changed:
return value
return value[tuple(subargs)]

return resolve(default)


def _typevartuple_prepare_subst(self, alias, args):
params = alias.__parameters__
typevartuple_index = params.index(self)
Expand Down Expand Up @@ -1118,7 +1174,9 @@ def _typevartuple_prepare_subst(self, alias, args):
raise TypeError(f"Too few arguments for {alias};"
f" actual {alen}, expected at least {plen-1}")
if left == alen - right and self.has_default():
replacement = _unpack_args(self.__default__)
default = _resolve_type_param_default(self, self.__default__, params,
args[:left])
replacement = _unpack_args(default)
else:
replacement = args[left: alen - right]

Expand All @@ -1144,7 +1202,9 @@ def _paramspec_prepare_subst(self, alias, args):
params = alias.__parameters__
i = params.index(self)
if i == len(args) and self.has_default():
args = (*args, self.__default__)
default = _resolve_type_param_default(self, self.__default__, params,
args)
args = (*args, default)
if i >= len(args):
raise TypeError(f"Too few arguments for {alias}")
# Special case where Z[[int, str, bool]] == Z[int, str, bool] in PEP 612.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fix substitution of :pep:`696` type parameter defaults that refer to earlier
type parameters in the same type parameter list. ``class Bar[T, S = T]`` now
resolves ``Bar[int]`` to ``Bar[int, int]`` instead of leaving ``S`` bound to
the unsubstituted ``T``, which previously made ``class Baz[U](Bar[U])`` raise
:exc:`TypeError`. A default that refers to a type parameter which is not
declared before it now raises :exc:`TypeError` when it is used.
13 changes: 12 additions & 1 deletion Objects/typevarobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -807,8 +807,19 @@ typevar_typing_prepare_subst_impl(typevarobject *self, PyObject *alias,
return NULL;
}
if (dflt != &_Py_NoDefaultStruct) {
PyObject *new_args = PyTuple_Pack(1, dflt);
// The default may refer to type parameters that appear earlier in
// the same type parameter list; those are already resolved in
// "args", so substitute them in.
PyObject *resolve_args[4] = {(PyObject *)self, dflt, params, args};
PyObject *resolved = call_typing_func_object(
"_resolve_type_param_default", resolve_args, 4);
Py_DECREF(dflt);
if (resolved == NULL) {
Py_DECREF(params);
return NULL;
}
PyObject *new_args = PyTuple_Pack(1, resolved);
Py_DECREF(resolved);
if (new_args == NULL) {
Py_DECREF(params);
return NULL;
Expand Down
Loading