8000 bpo-41559: Change PEP 612 implementation to pure Python by Fidget-Spinner · Pull Request #25449 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

bpo-41559: Change PEP 612 implementation to pure Python #25449

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

Merged
merged 9 commits into from
Apr 28, 2021
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;
645D 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