8000 bpo-41559: Change PEP 612 implementation to pure Python (#25449) · python/cpython@859577c · GitHub
[go: up one dir, main page]

Skip to content

Commit 859577c

Browse files
bpo-41559: Change PEP 612 implementation to pure Python (#25449)
1 parent c1a9535 commit 859577c

File tree

4 files changed

+92
-71
lines changed

4 files changed

+92
-71
lines changed

Lib/_collections_abc.py

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,18 @@ def __create_ga(cls, origin, args):
443443
ga_args = args
444444
return super().__new__(cls, origin, ga_args)
445445

446+
@property
447+
def __parameters__(self):
448+
params = []
449+
for arg in self.__args__:
450+
# Looks like a genericalias
451+
if hasattr(arg, "__parameters__") and isinstance(arg.__parameters__, tuple):
452+
params.extend(arg.__parameters__)
453+
else:
454+
if _is_typevarlike(arg):
455+
params.append(arg)
456+
return tuple(dict.fromkeys(params))
457+
446458
def __repr__(self):
447459
if _has_special_args(self.__args__):
448460
return super().__repr__()
@@ -458,16 +470,50 @@ def __reduce__(self):
458470

459471
def __getitem__(self, item):
460472
# Called during TypeVar substitution, returns the custom subclass
461-
# rather than the default types.GenericAlias object.
462-
ga = super().__getitem__(item)
463-
args = ga.__args__
464-
# args[0] occurs due to things like Z[[int, str, bool]] from PEP 612
465-
if not isinstance(ga.__args__[0], tuple):
466-
t_result = ga.__args__[-1]
467-
t_args = ga.__args__[:-1]
468-
args = (t_args, t_result)
469-
return _CallableGenericAlias(Callable, args)
473+
# rather than the default types.GenericAlias object. Most of the
474+
# code is copied from typing's _GenericAlias and the builtin
475+
# types.GenericAlias.
476+
477+
# A special case in PEP 612 where if X = Callable[P, int],
478+
# then X[int, str] == X[[int, str]].
479+
param_len = len(self.__parameters__)
480+
if param_len == 0:
481+
raise TypeError(f'There are no type or parameter specification'
482+
f'variables left in {self}')
483+
if (param_len == 1
484+
and isinstance(item, (tuple, list))
485+
and len(item) > 1) or not isinstance(item, tuple):
486+
item = (item,)
487+
item_len = len(item)
488+
if item_len != param_len:
489+
raise TypeError(f'Too {"many" if item_len > param_len else "few"}'
490+
f' arguments for {self};'
491+
f' actual {item_len}, expected {param_len}')
492+
subst = dict(zip(self.__parameters__, item))
493+
new_args = []
494+
for arg in self.__args__:
495+
if _is_typevarlike(arg):
496+
arg = subst[arg]
497+
# Looks like a GenericAlias
498+
elif hasattr(arg, '__parameters__') and isinstance(arg.__parameters__, tuple):
499+
subparams = arg.__parameters__
500+
if subparams:
501+
subargs = tuple(subst[x] for x in subparams)
502+
arg = arg[subargs]
503+
new_args.append(arg)
470504

505+
# args[0] occurs due to things like Z[[int, str, bool]] from PEP 612
506+
if not isinstance(new_args[0], (tuple, list)):
507+
t_result = new_args[-1]
508+
t_args = new_args[:-1]
509+
new_args = (t_args, t_result)
510+
return _CallableGenericAlias(Callable, tuple(new_args))
511+
512+
def _is_typevarlike(arg):
513+
obj = type(arg)
514+
# looks like a TypeVar/ParamSpec
515+
return (obj.__module__ == 'typing'
516+
and obj.__name__ in {'ParamSpec', 'TypeVar'})
471517

472518
def _has_special_args(args):
473519
"""Checks if args[0] matches either ``...``, ``ParamSpec`` or

Lib/test/test_genericalias.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,12 @@ def test_abc_callable(self):
353353
self.assertEqual(repr(C4[dict]).split(".")[-1], "Callable[[int, dict], str]")
354354
self.assertEqual(C4[dict], Callable[[int, dict], str])
355355

356+
# substitute a nested GenericAlias (both typing and the builtin
357+
# version)
358+
C5 = Callable[[typing.List[T], tuple[K, T], V], int]
359+
self.assertEqual(C5[int, str, float],
360+
Callable[[typing.List[int], tuple[str, int], float], int])
361+
356362
with self.subTest("Testing type erasure"):
357363
class C1(Callable):
358364
def __call__(self):
@@ -391,5 +397,16 @@ def __call__(self):
391397
self.assertEqual(repr(C1), "collections.abc.Callable"
392398
"[typing.Concatenate[int, ~P], int]")
393399

400+
with self.subTest("Testing TypeErrors"):
401+
with self.assertRaisesRegex(TypeError, "variables left in"):
402+
alias[int]
403+
P = typing.ParamSpec('P')
404+
C1 = Callable[P, T]
405+
with self.assertRaisesRegex(TypeError, "many arguments for"):
406+
C1[int, str, str]
407+
with self.assertRaisesRegex(TypeError, "few arguments for"):
408+
C1[int]
409+
410+
394411
if __name__ == "__main__":
395412
unittest.main()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
:pep:`612` is now implemented purely in Python; builtin ``types.GenericAlias``
2+
objects no longer include ``typing.ParamSpec`` in ``__parameters__``
3+
(with the exception of ``collections.abc.Callable``\ 's ``GenericAlias``).
4+
This means previously invalid uses of ``ParamSpec`` (such as
5+
``list[P]``) which worked in earlier versions of Python 3.10 alpha,
6+
will now raise ``TypeError`` during substitution.

Objects/genericaliasobject.c

Lines changed: 14 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -156,25 +156,13 @@ ga_repr(PyObject *self)
156156
return NULL;
157157
}
158158

159-
/* Checks if a variable number of names are from typing.py.
160-
* If any one of the names are found, return 1, else 0.
161-
**/
162-
static inline int
163-
is_typing_name(PyObject *obj, int num, ...)
159+
// isinstance(obj, TypeVar) without importing typing.py.
160+
// Returns -1 for errors.
161+
static int
162+
is_typevar(PyObject *obj)
164163
{
165-
va_list names;
166-
va_start(names, num);
167-
168164
PyTypeObject *type = Py_TYPE(obj);
169-
int hit = 0;
170-
for (int i = 0; i < num; ++i) {
171-
if (!strcmp(type->tp_name, va_arg(names, const char *))) {
172-
hit = 1;
173-
break;
174-
}
175-
}
176-
va_end(names);
177-
if (!hit) {
165+
if (strcmp(type->tp_name, "TypeVar") != 0) {
178166
return 0;
179167
}
180168
PyObject *module = PyObject_GetAttrString((PyObject *)type, "__module__");
@@ -184,24 +172,9 @@ is_typing_name(PyObject *obj, int num, ...)
184172
int res = PyUnicode_Check(module)
185173
&& _PyUnicode_EqualToASCIIString(module, "typing");
186174
Py_DECREF(module);
187-
188175
return res;
189176
}
190177

191-
// isinstance(obj, (TypeVar, ParamSpec)) without importing typing.py.
192-
// Returns -1 for errors.
193-
static inline int
194-
is_typevarlike(PyObject *obj)
195-
{
196-
return is_typing_name(obj, 2, "TypeVar", "ParamSpec");
197-
}
198-
199-
static inline int
200-
is_paramspec(PyObject *obj)
201-
{
202-
return is_typing_name(obj, 1, "ParamSpec");
203-
}
204-
205178
// Index of item in self[:len], or -1 if not found (self is a tuple)
206179
static Py_ssize_t
207180
tuple_index(PyObject *self, Py_ssize_t len, PyObject *item)
@@ -236,7 +209,7 @@ make_parameters(PyObject *args)
236209
Py_ssize_t iparam = 0;
237210
for (Py_ssize_t iarg = 0; iarg < nargs; iarg++) {
238211
PyObject *t = PyTuple_GET_ITEM(args, iarg);
239-
int typevar = is_typevarlike(t);
212+
int typevar = is_typevar(t);
240213
if (typevar < 0) {
241214
Py_DECREF(parameters);
242215
return NULL;
@@ -306,14 +279,7 @@ subs_tvars(PyObject *obj, PyObject *params, PyObject **argitems)
306279
if (iparam >= 0) {
307280
arg = argitems[iparam];
308281
}
309-
// convert all the lists inside args to tuples to help
310-
// with caching in other libaries
311-
if (PyList_CheckExact(arg)) {
312-
arg = PyList_AsTuple(arg);
313-
}
314-
else {
315-
Py_INCREF(arg);
316-
}
282+
Py_INCREF(arg);
317283
PyTuple_SET_ITEM(subargs, i, arg);
318284
}
319285

@@ -348,19 +314,11 @@ ga_getitem(PyObject *self, PyObject *item)
348314
int is_tuple = PyTuple_Check(item);
349315
Py_ssize_t nitems = is_tuple ? PyTuple_GET_SIZE(item) : 1;
350316
PyObject **argitems = is_tuple ? &PyTuple_GET_ITEM(item, 0) : &item;
351-
// A special case in PEP 612 where if X = Callable[P, int],
352-
// then X[int, str] == X[[int, str]].
353-
if (nparams == 1 && nitems > 1 && is_tuple &&
354-
is_paramspec(PyTuple_GET_ITEM(alias->parameters, 0))) {
355-
argitems = &item;
356-
}
357-
else {
358-
if (nitems != nparams) {
359-
return PyErr_Format(PyExc_TypeError,
360-
"Too %s arguments for %R",
361-
nitems > nparams ? "many" : "few",
362-
self);
363-
}
317+
if (nitems != nparams) {
318+
return PyErr_Format(PyExc_TypeError,
319+
"Too %s arguments for %R",
320+
nitems > nparams ? "many" : "few",
321+
self);
364322
}
365323
/* Replace all type variables (specified by alias->parameters)
366324
with corresponding values specified by argitems.
@@ -375,7 +333,7 @@ ga_getitem(PyObject *self, PyObject *item)
375333
}
376334
for (Py_ssize_t iarg = 0; iarg < nargs; iarg++) {
377335
PyObject *arg = PyTuple_GET_ITEM(alias->args, iarg);
378-
int typevar = is_typevarlike(arg);
336+
int typevar = is_typevar(arg);
379337
if (typevar < 0) {
380338
Py_DECREF(newargs);
381339
return NULL;
@@ -384,13 +342,7 @@ ga_getitem(PyObject *self, PyObject *item)
384342
Py_ssize_t iparam = tuple_index(alias->parameters, nparams, arg);
385343
assert(iparam >= 0);
386344
arg = argitems[iparam];
387-
// convert lists to tuples to help with caching in other libaries.
388-
if (PyList_CheckExact(arg)) {
389-
arg = PyList_AsTuple(arg);
390-
}
391-
else {
392-
Py_INCREF(arg);
393-
}
345+
Py_INCREF(arg);
394346
}
395347
else {
396348
arg = subs_tvars(arg, alias->parameters, argitems);

0 commit comments

Comments
 (0)
0