8000 bpo-41559: Convert lists to tuples in genericalias to prepare for PEP 612 by Fidget-Spinner · Pull Request #24056 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content
Closed
7 changes: 7 additions & 0 deletions Doc/library/stdtypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4946,6 +4946,13 @@ All parameterized generics implement special read-only attributes.
>>> dict[str, list[int]].__args__
(<class 'str'>, list[int])

.. versionchanged:: 3.9.2
``__args__`` will convert all lists passed to the generic alias to tuples.
This aligns with the :mod:`typing` module which tries to ensure that
most types will be hashable for caching purposes. Passing a list is valid
in certain scenarios from Python 3.10 and higher due to :pep:`612`. An
exception to this behavior is the generic alias for
``collections.abc.Callable``.

.. attribute:: genericalias.__parameters__

Expand Down
22 changes: 22 additions & 0 deletions Lib/test/test_genericalias.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,5 +391,27 @@ def __call__(self):
self.assertEqual(repr(C1), "collections.abc.Callable"
"[typing.Concatenate[int, ~P], int]")

def test_tupleify(self):
# Converts all lists inside __args__ to tuple.
# For consistency with typing module which ensures
# most types are hashable. Required since PEP 612
# introduces lists during substitution.

self.assertEqual(tuple[[]], tuple[(),])
self.assertEqual(tuple[int, [str, dict]], tuple[int, (str, dict)])
self.assertEqual(list[int, [str, [str, dict]]], list[int, (str, (str, dict))])
# Most likely to refleak due to nested list inside tuple.
self.assertEqual(list[int, (str, [str, dict])], list[int, (str, (str, dict))])

x = (int, (int, [dict, float]))
self.assertEqual(list[x], list[int, (int, (dict, float))])
# Make sure the original tuple's list wasn't modified.
self.assertEqual(x, (int, (int, [dict, float])))

y = [int, str]
self.assertEqual(list[y], list[[int, str]])
self.assertEqual(y, [int, str])


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:ref:`Generic Alias <types-genericalias>` objects will now convert all lists
received to tuples to 1. work with ``typing.Union`` and ``typing.Optional``,
2. aid caching in other libraries and 3. prepare for :pep:`612` in Python 3.10.
77 changes: 61 additions & 16 deletions Objects/genericaliasobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "pycore_object.h"
#include "pycore_unionobject.h" // _Py_union_as_number
#include "structmember.h" // PyMemberDef
#include "pycore_tuple.h" // _PyTuple_FromArray()

typedef struct {
PyObject_HEAD
Expand Down Expand Up @@ -225,6 +226,58 @@ tuple_add(PyObject *self, Py_ssize_t len, PyObject *item)
return 0;
}

// Makes a shallow copy of a tuple
static inline PyObject *
tuple_copy(PyObject *obj)
{
return _PyTuple_FromArray(((PyTupleObject *)obj)->ob_item, Py_SIZE(obj));
}

// This converts all nested lists in args to a tuple (args should be a tuple).
// Usually needed to work with typing.py's Union and Optional and help
// caching in other libraries.
static inline PyObject *
tupleify_lists(PyObject *args) {
Py_ssize_t len = PyTuple_GET_SIZE(args);
PyObject *result = tuple_copy(args);
if (result == NULL) {
return NULL;
}
for (Py_ssize_t i = 0; i < len; ++i) {
PyObject *arg = PyTuple_GET_ITEM(result, i);
if (arg == NULL) {
goto error;
}
PyObject *new_arg = arg;
if (PyList_CheckExact(arg)) {
new_arg = PyList_AsTuple(arg);
if (new_arg == NULL) {
goto error;
}
Py_DECREF(arg);
}
else if (!PyTuple_CheckExact(arg)) {
continue;
}
if (Py_EnterRecursiveCall(" while converting lists to tuples in "
"GenericAlias' __args__")) {
goto error;
}
PyObject *new_arg_tupled = tupleify_lists(new_arg);
Py_DECREF(new_arg);
Py_LeaveRecursiveCall();
if (new_arg_tupled == NULL) {
goto error;
}
PyTuple_SET_ITEM(result, i, new_arg_tupled);
}
return result;

error:
Py_DECREF(result);
return NULL;
}

static PyObject *
make_parameters(PyObject *args)
{
Expand Down Expand Up @@ -306,14 +359,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 @@ -384,13 +430,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 Expand Up @@ -626,10 +666,15 @@ setup_ga(gaobject *alias, PyObject *origin, PyObject *args) {
else {
Py_INCREF(args);
}
PyObject *new_args = tupleify_lists(args);
Py_DECREF(args);
if (new_args == NULL) {
return 0;
}

Py_INCREF(origin);
alias->origin = origin;
alias->args = args;
alias->args = new_args;
alias->parameters = NULL;
alias->weakreflist = NULL;
return 1;
Expand Down
0