diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 2331849c02e982..a3bd02fb12e077 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -4946,6 +4946,13 @@ All parameterized generics implement special read-only attributes. >>> dict[str, list[int]].__args__ (, 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__ diff --git a/Lib/test/test_genericalias.py b/Lib/test/test_genericalias.py index fd024dcec8208b..1249b3f2256a40 100644 --- a/Lib/test/test_genericalias.py +++ b/Lib/test/test_genericalias.py @@ -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() diff --git a/Misc/NEWS.d/next/Core and Builtins/2021-01-02-17-37-08.bpo-41559.y2LoK8.rst b/Misc/NEWS.d/next/Core and Builtins/2021-01-02-17-37-08.bpo-41559.y2LoK8.rst new file mode 100644 index 00000000000000..b981ce2655266e --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2021-01-02-17-37-08.bpo-41559.y2LoK8.rst @@ -0,0 +1,3 @@ +:ref:`Generic Alias ` 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. diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index 8fae83b27297d0..19192e5cb52400 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -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 @@ -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) { @@ -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); } @@ -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); @@ -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;