Skip to content

gh-88959: Add __parameters__ and __getitem__ in TypeVar and ParamSpec #27511

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 0 additions & 1 deletion Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,6 @@ struct _Py_global_strings {
STRUCT_FOR_ID(__trunc__)
STRUCT_FOR_ID(__typing_is_unpacked_typevartuple__)
STRUCT_FOR_ID(__typing_prepare_subst__)
STRUCT_FOR_ID(__typing_subst__)
STRUCT_FOR_ID(__typing_unpacked_tuple_args__)
STRUCT_FOR_ID(__warningregistry__)
STRUCT_FOR_ID(__weaklistoffset__)
Expand Down
1 change: 0 additions & 1 deletion Include/internal/pycore_runtime_init.h
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,6 @@ extern "C" {
INIT_ID(__trunc__), \
INIT_ID(__typing_is_unpacked_typevartuple__), \
INIT_ID(__typing_prepare_subst__), \
INIT_ID(__typing_subst__), \
INIT_ID(__typing_unpacked_tuple_args__), \
INIT_ID(__warningregistry__), \
INIT_ID(__weaklistoffset__), \
Expand Down
41 changes: 22 additions & 19 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,16 +448,18 @@ def test_no_bivariant(self):

def test_var_substitution(self):
T = TypeVar('T')
subst = T.__typing_subst__
self.assertIs(subst(int), int)
self.assertEqual(subst(list[int]), list[int])
self.assertEqual(subst(List[int]), List[int])
self.assertEqual(subst(List), List)
self.assertIs(subst(Any), Any)
self.assertIs(subst(None), type(None))
self.assertIs(subst(T), T)
self.assertEqual(subst(int|str), int|str)
self.assertEqual(subst(Union[int, str]), Union[int, str])
self.assertEqual(T.__parameters__, (T,))
self.assertIs(T.__parameters__[0], T)
self.assertIs(T[int,], int)
self.assertEqual(T[list[int],], list[int])
self.assertEqual(T[List[int],], List[int])
self.assertEqual(T[List,], List)
self.assertIs(T[Any,], Any)
self.assertIs(T[None,], type(None))
self.assertIs(T[T,], T)
self.assertIs(T[(int,)], int)
self.assertEqual(T[int|str,], int|str)
self.assertEqual(T[Union[int, str],], Union[int, str])

def test_bad_var_substitution(self):
T = TypeVar('T')
Expand All @@ -470,7 +472,7 @@ def test_bad_var_substitution(self):
for arg in bad_args:
with self.subTest(arg=arg):
with self.assertRaises(TypeError):
T.__typing_subst__(arg)
T[arg,]
with self.assertRaises(TypeError):
List[T][arg]
with self.assertRaises(TypeError):
Expand Down Expand Up @@ -6819,13 +6821,14 @@ class X(Generic[P, P2]):
def test_var_substitution(self):
T = TypeVar("T")
P = ParamSpec("P")
subst = P.__typing_subst__
self.assertEqual(subst((int, str)), (int, str))
self.assertEqual(subst([int, str]), (int, str))
self.assertEqual(subst([None]), (type(None),))
self.assertIs(subst(...), ...)
self.assertIs(subst(P), P)
self.assertEqual(subst(Concatenate[int, P]), Concatenate[int, P])
self.assertEqual(P.__parameters__, (P,))
self.assertIs(P.__parameters__[0], P)
self.assertEqual(P[(int, str),], (int, str))
self.assertEqual(P[[int, str],], (int, str))
self.assertEqual(P[[None],], (type(None),))
self.assertIs(P[...,], ...)
self.assertIs(P[P,], P)
self.assertEqual(P[Concatenate[int, P],], Concatenate[int, P])

def test_bad_var_substitution(self):
T = TypeVar('T')
Expand All @@ -6834,7 +6837,7 @@ def test_bad_var_substitution(self):
for arg in bad_args:
with self.subTest(arg=arg):
with self.assertRaises(TypeError):
P.__typing_subst__(arg)
P[arg,]
with self.assertRaises(TypeError):
typing.Callable[P, T][arg, str]
with self.assertRaises(TypeError):
Expand Down
51 changes: 27 additions & 24 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,9 @@ def _collect_parameters(args):
"""
parameters = []
for t in args:
if hasattr(t, '__typing_subst__'):
if t not in parameters:
parameters.append(t)
else:
for x in getattr(t, '__parameters__', ()):
if x not in parameters:
parameters.append(x)
for x in getattr(t, '__parameters__', ()):
if x not in parameters:
parameters.append(x)
return tuple(parameters)


Expand Down Expand Up @@ -954,6 +950,9 @@ def __repr__(self):
prefix = '~'
return prefix + self.__name__

@property
def __parameters__(self):
return (self,)

class TypeVar(_Final, _Immutable, _BoundVarianceMixin, _PickleUsingNameMixin,
_root=True):
Expand Down Expand Up @@ -1014,7 +1013,9 @@ def __init__(self, name, *constraints, bound=None,
if def_mod != 'typing':
self.__module__ = def_mod

def __typing_subst__(self, arg):
def __getitem__(self, arg):
if isinstance(arg, tuple) and len(arg) == 1:
arg, = arg
msg = "Parameters to generic types must be types."
arg = _type_check(arg, msg, is_argument=True)
if ((isinstance(arg, _GenericAlias) and arg.__origin__ is Unpack) or
Expand Down Expand Up @@ -1062,9 +1063,13 @@ def __iter__(self):
def __repr__(self):
return self.__name__

def __typing_subst__(self, arg):
def __getitem__(self, arg):
raise TypeError("Substitution of bare TypeVarTuple is not supported")

@property
def __parameters__(self):
return (self,)

def __typing_prepare_subst__(self, alias, args):
params = alias.__parameters__
typevartuple_index = params.index(self)
Expand Down Expand Up @@ -1212,7 +1217,9 @@ def __init__(self, name, *, bound=None, covariant=False, contravariant=False):
if def_mod != 'typing':
self.__module__ = def_mod

def __typing_subst__(self, arg):
def __getitem__(self, arg):
if isinstance(arg, tuple) and len(arg) == 1:
arg, = arg
if isinstance(arg, (list, tuple)):
arg = tuple(_type_check(a, "Expected a type.") for a in arg)
elif not _is_param_expr(arg):
Expand Down Expand Up @@ -1420,21 +1427,17 @@ def _determine_new_args(self, args):
new_args = []
for old_arg in self.__args__:

substfunc = getattr(old_arg, '__typing_subst__', None)
if substfunc:
new_arg = substfunc(new_arg_by_param[old_arg])
subparams = getattr(old_arg, '__parameters__', ())
if not subparams:
new_arg = old_arg
else:
subparams = getattr(old_arg, '__parameters__', ())
if not subparams:
new_arg = old_arg
else:
subargs = []
for x in subparams:
if isinstance(x, TypeVarTuple):
subargs.extend(new_arg_by_param[x])
else:
subargs.append(new_arg_by_param[x])
new_arg = old_arg[tuple(subargs)]
subargs = []
for x in subparams:
if isinstance(x, TypeVarTuple):
subargs.extend(new_arg_by_param[x])
else:
subargs.append(new_arg_by_param[x])
new_arg = old_arg[tuple(subargs)]

if self.__origin__ == collections.abc.Callable and isinstance(new_arg, tuple):
# Consider the following `Callable`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add ``__parameters__`` and ``__getitem__`` in :class:`~typing.TypeVar` and
:class:`~ParamSpec`.
61 changes: 18 additions & 43 deletions Objects/genericaliasobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -218,40 +218,29 @@ _Py_make_parameters(PyObject *args)
Py_ssize_t iparam = 0;
for (Py_ssize_t iarg = 0; iarg < nargs; iarg++) {
PyObject *t = PyTuple_GET_ITEM(args, iarg);
PyObject *subst;
if (_PyObject_LookupAttr(t, &_Py_ID(__typing_subst__), &subst) < 0) {
PyObject *subparams;
if (_PyObject_LookupAttr(t, &_Py_ID(__parameters__),
&subparams) < 0) {
Py_DECREF(parameters);
return NULL;
}
if (subst) {
iparam += tuple_add(parameters, iparam, t);
Py_DECREF(subst);
}
else {
PyObject *subparams;
if (_PyObject_LookupAttr(t, &_Py_ID(__parameters__),
&subparams) < 0) {
Py_DECREF(parameters);
return NULL;
}
if (subparams && PyTuple_Check(subparams)) {
Py_ssize_t len2 = PyTuple_GET_SIZE(subparams);
Py_ssize_t needed = len2 - 1 - (iarg - iparam);
if (needed > 0) {
len += needed;
if (_PyTuple_Resize(&parameters, len) < 0) {
Py_DECREF(subparams);
Py_DECREF(parameters);
return NULL;
}
}
for (Py_ssize_t j = 0; j < len2; j++) {
PyObject *t2 = PyTuple_GET_ITEM(subparams, j);
iparam += tuple_add(parameters, iparam, t2);
if (subparams && PyTuple_Check(subparams)) {
Py_ssize_t len2 = PyTuple_GET_SIZE(subparams);
Py_ssize_t needed = len2 - 1 - (iarg - iparam);
if (needed > 0) {
len += needed;
if (_PyTuple_Resize(&parameters, len) < 0) {
Py_DECREF(subparams);
Py_DECREF(parameters);
return NULL;
}
}
Py_XDECREF(subparams);
for (Py_ssize_t j = 0; j < len2; j++) {
PyObject *t2 = PyTuple_GET_ITEM(subparams, j);
iparam += tuple_add(parameters, iparam, t2);
}
}
Py_XDECREF(subparams);
}
if (iparam < len) {
if (_PyTuple_Resize(&parameters, iparam) < 0) {
Expand Down Expand Up @@ -460,21 +449,7 @@ _Py_subs_parameters(PyObject *self, PyObject *args, PyObject *parameters, PyObje
Py_DECREF(item);
return NULL;
}
PyObject *subst;
if (_PyObject_LookupAttr(arg, &_Py_ID(__typing_subst__), &subst) < 0) {
Py_DECREF(newargs);
Py_DECREF(item);
return NULL;
}
if (subst) {
Py_ssize_t iparam = tuple_index(parameters, nparams, arg);
assert(iparam >= 0);
arg = PyObject_CallOneArg(subst, argitems[iparam]);
Py_DECREF(subst);
}
else {
arg = subs_tvars(arg, parameters, argitems, nitems);
}
arg = subs_tvars(arg, parameters, argitems, nitems);
if (arg == NULL) {
Py_DECREF(newargs);
Py_DECREF(item);
Expand Down