Skip to content

Commit

Permalink
bpo-41559: Change PEP 612 implementation to pure Python (#25449)
Browse files Browse the repository at this point in the history
  • Loading branch information
Fidget-Spinner authored Apr 28, 2021
1 parent c1a9535 commit 859577c
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 71 deletions.
64 changes: 55 additions & 9 deletions Lib/_collections_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,18 @@ def __create_ga(cls, origin, args):
ga_args = args
return super().__new__(cls, origin, ga_args)

@property
def __parameters__(self):
params = []
for arg in self.__args__:
# Looks like a genericalias
if hasattr(arg, "__parameters__") and isinstance(arg.__parameters__, tuple):
params.extend(arg.__parameters__)
else:
if _is_typevarlike(arg):
params.append(arg)
return tuple(dict.fromkeys(params))

def __repr__(self):
if _has_special_args(self.__args__):
return super().__repr__()
Expand All @@ -458,16 +470,50 @@ def __reduce__(self):

def __getitem__(self, item):
# Called during TypeVar substitution, returns the custom subclass
# rather than the default types.GenericAlias object.
ga = super().__getitem__(item)
args = ga.__args__
# args[0] occurs due to things like Z[[int, str, bool]] from PEP 612
if not isinstance(ga.__args__[0], tuple):
t_result = ga.__args__[-1]
t_args = ga.__args__[:-1]
args = (t_args, t_result)
return _CallableGenericAlias(Callable, args)
# rather than the default types.GenericAlias object. Most of the
# code is copied from typing's _GenericAlias and the builtin
# types.GenericAlias.

# A special case in PEP 612 where if X = Callable[P, int],
# then X[int, str] == X[[int, str]].
param_len = len(self.__parameters__)
if param_len == 0:
raise TypeError(f'There are no type or parameter specification'
f'variables left in {self}')
if (param_len == 1
and isinstance(item, (tuple, list))
and len(item) > 1) or not isinstance(item, tuple):
item = (item,)
item_len = len(item)
if item_len != param_len:
raise TypeError(f'Too {"many" if item_len > param_len else "few"}'
f' arguments for {self};'
f' actual {item_len}, expected {param_len}')
subst = dict(zip(self.__parameters__, item))
new_args = []
for arg in self.__args__:
if _is_typevarlike(arg):
arg = subst[arg]
# Looks like a GenericAlias
elif hasattr(arg, '__parameters__') and isinstance(arg.__parameters__, tuple):
subparams = arg.__parameters__
if subparams:
subargs = tuple(subst[x] for x in subparams)
arg = arg[subargs]
new_args.append(arg)

# args[0] occurs due to things like Z[[int, str, bool]] from PEP 612
if not isinstance(new_args[0], (tuple, list)):
t_result = new_args[-1]
t_args = new_args[:-1]
new_args = (t_args, t_result)
return _CallableGenericAlias(Callable, tuple(new_args))

def _is_typevarlike(arg):
obj = type(arg)
# looks like a TypeVar/ParamSpec
return (obj.__module__ == 'typing'
and obj.__name__ in {'ParamSpec', 'TypeVar'})

def _has_special_args(args):
"""Checks if args[0] matches either ``...``, ``ParamSpec`` or
Expand Down
17 changes: 17 additions & 0 deletions Lib/test/test_genericalias.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,12 @@ def test_abc_callable(self):
self.assertEqual(repr(C4[dict]).split(".")[-1], "Callable[[int, dict], str]")
self.assertEqual(C4[dict], Callable[[int, dict], str])

# substitute a nested GenericAlias (both typing and the builtin
# version)
C5 = Callable[[typing.List[T], tuple[K, T], V], int]
self.assertEqual(C5[int, str, float],
Callable[[typing.List[int], tuple[str, int], float], int])

with self.subTest("Testing type erasure"):
class C1(Callable):
def __call__(self):
Expand Down Expand Up @@ -391,5 +397,16 @@ def __call__(self):
self.assertEqual(repr(C1), "collections.abc.Callable"
"[typing.Concatenate[int, ~P], int]")

with self.subTest("Testing TypeErrors"):
with self.assertRaisesRegex(TypeError, "variables left in"):
alias[int]
P = typing.ParamSpec('P')
C1 = Callable[P, T]
with self.assertRaisesRegex(TypeError, "many arguments for"):
C1[int, str, str]
with self.assertRaisesRegex(TypeError, "few arguments for"):
C1[int]


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
:pep:`612` is now implemented purely in Python; builtin ``types.GenericAlias``
objects no longer include ``typing.ParamSpec`` in ``__parameters__``
(with the exception of ``collections.abc.Callable``\ 's ``GenericAlias``).
This means previously invalid uses of ``ParamSpec`` (such as
``list[P]``) which worked in earlier versions of Python 3.10 alpha,
will now raise ``TypeError`` during substitution.
76 changes: 14 additions & 62 deletions Objects/genericaliasobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -156,25 +156,13 @@ ga_repr(PyObject *self)
return NULL;
}

/* Checks if a variable number of names are from typing.py.
* If any one of the names are found, return 1, else 0.
**/
static inline int
is_typing_name(PyObject *obj, int num, ...)
// isinstance(obj, TypeVar) without importing typing.py.
// Returns -1 for errors.
static int
is_typevar(PyObject *obj)
{
va_list names;
va_start(names, num);

PyTypeObject *type = Py_TYPE(obj);
int hit = 0;
for (int i = 0; i < num; ++i) {
if (!strcmp(type->tp_name, va_arg(names, const char *))) {
hit = 1;
break;
}
}
va_end(names);
if (!hit) {
if (strcmp(type->tp_name, "TypeVar") != 0) {
return 0;
}
PyObject *module = PyObject_GetAttrString((PyObject *)type, "__module__");
Expand All @@ -184,24 +172,9 @@ is_typing_name(PyObject *obj, int num, ...)
int res = PyUnicode_Check(module)
&& _PyUnicode_EqualToASCIIString(module, "typing");
Py_DECREF(module);

return res;
}

// isinstance(obj, (TypeVar, ParamSpec)) without importing typing.py.
// Returns -1 for errors.
static inline int
is_typevarlike(PyObject *obj)
{
return is_typing_name(obj, 2, "TypeVar", "ParamSpec");
}

static inline int
is_paramspec(PyObject *obj)
{
return is_typing_name(obj, 1, "ParamSpec");
}

// Index of item in self[:len], or -1 if not found (self is a tuple)
static Py_ssize_t
tuple_index(PyObject *self, Py_ssize_t len, PyObject *item)
Expand Down Expand Up @@ -236,7 +209,7 @@ 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);
int typevar = is_typevarlike(t);
int typevar = is_typevar(t);
if (typevar < 0) {
Py_DECREF(parameters);
return NULL;
Expand Down Expand Up @@ -306,14 +279,7 @@ subs_tvars(PyObject *obj, PyObject *params, PyObject **argitems)
if (iparam >= 0) {
arg = argitems[iparam];
}
// convert all the lists inside args to tuples to help
// with caching in other libaries
if (PyList_CheckExact(arg)) {
arg = PyList_AsTuple(arg);
}
else {
Py_INCREF(arg);
}
Py_INCREF(arg);
PyTuple_SET_ITEM(subargs, i, arg);
}

Expand Down Expand Up @@ -348,19 +314,11 @@ ga_getitem(PyObject *self, PyObject *item)
int is_tuple = PyTuple_Check(item);
Py_ssize_t nitems = is_tuple ? PyTuple_GET_SIZE(item) : 1;
PyObject **argitems = is_tuple ? &PyTuple_GET_ITEM(item, 0) : &item;
// A special case in PEP 612 where if X = Callable[P, int],
// then X[int, str] == X[[int, str]].
if (nparams == 1 && nitems > 1 && is_tuple &&
is_paramspec(PyTuple_GET_ITEM(alias->parameters, 0))) {
argitems = &item;
}
else {
if (nitems != nparams) {
return PyErr_Format(PyExc_TypeError,
"Too %s arguments for %R",
nitems > nparams ? "many" : "few",
self);
}
if (nitems != nparams) {
return PyErr_Format(PyExc_TypeError,
"Too %s arguments for %R",
nitems > nparams ? "many" : "few",
self);
}
/* Replace all type variables (specified by alias->parameters)
with corresponding values specified by argitems.
Expand All @@ -375,7 +333,7 @@ ga_getitem(PyObject *self, PyObject *item)
}
for (Py_ssize_t iarg = 0; iarg < nargs; iarg++) {
PyObject *arg = PyTuple_GET_ITEM(alias->args, iarg);
int typevar = is_typevarlike(arg);
int typevar = is_typevar(arg);
if (typevar < 0) {
Py_DECREF(newargs);
return NULL;
Expand All @@ -384,13 +342,7 @@ ga_getitem(PyObject *self, PyObject *item)
Py_ssize_t iparam = tuple_index(alias->parameters, nparams, arg);
assert(iparam >= 0);
arg = argitems[iparam];
// convert lists to tuples to help with caching in other libaries.
if (PyList_CheckExact(arg)) {
arg = PyList_AsTuple(arg);
}
else {
Py_INCREF(arg);
}
Py_INCREF(arg);
}
else {
arg = subs_tvars(arg, alias->parameters, argitems);
Expand Down

0 comments on commit 859577c

Please sign in to comment.