From 519b91e716661d64585a3ecc926ac6abcd2d243b Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sat, 2 Jan 2021 17:05:20 +0800 Subject: [PATCH 1/9] tupleify everything --- Lib/test/test_genericalias.py | 18 ++++++++++++ Objects/genericaliasobject.c | 53 ++++++++++++++++++++++++----------- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_genericalias.py b/Lib/test/test_genericalias.py index fd024dcec8208b..fb2c42ffe50a79 100644 --- a/Lib/test/test_genericalias.py +++ b/Lib/test/test_genericalias.py @@ -391,5 +391,23 @@ 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, str, [int, list]) + self.assertEqual(list[x], list[int, str, (int, list)]) + # Make sure the original tuple wasn't modified. + self.assertEqual(x, (int, str, [int, list])) + + if __name__ == "__main__": unittest.main() diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index 4cc82ffcdf39a3..570d831dfe21f4 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,36 @@ tuple_add(PyObject *self, Py_ssize_t len, PyObject *item) return 0; } +// Makes a deep 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 help caching in other libraries. +// Must be called on a copy of a tuple object, where the only reference is +// owned by the caller. +static inline void +tupleify_lists(PyObject *args) { + Py_ssize_t len = PyTuple_GET_SIZE(args); + for (Py_ssize_t i = 0; i < len; ++i) { + PyObject *arg = PyTuple_GET_ITEM(args, i); + if (PyList_CheckExact(arg)) { + PyObject *new_arg = PyList_AsTuple(arg); + tupleify_lists(new_arg); + PyTuple_SET_ITEM(args, i, new_arg); + Py_DECREF(arg); + } + else if (PyTuple_CheckExact(arg)) { + // In case there are lists nested inside tuples. + // This works despite interned tuples because tuples containing + // lists aren't interned. + tupleify_lists(arg); + } + } +} + static PyObject * make_parameters(PyObject *args) { @@ -306,14 +337,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 +408,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 +644,13 @@ setup_ga(gaobject *alias, PyObject *origin, PyObject *args) { else { Py_INCREF(args); } + PyObject *new_args = tuple_copy(args); + tupleify_lists(new_args); + Py_DECREF(args); Py_INCREF(origin); alias->origin = origin; - alias->args = args; + alias->args = new_args; alias->parameters = NULL; alias->weakreflist = NULL; return 1; From cb4e9af1818cb342fbb9573013e0d3fea154dbc0 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sat, 2 Jan 2021 17:42:35 +0800 Subject: [PATCH 2/9] update docs and add news --- Doc/library/stdtypes.rst | 7 +++++++ .../2021-01-02-17-37-08.bpo-41559.y2LoK8.rst | 3 +++ 2 files changed, 10 insertions(+) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2021-01-02-17-37-08.bpo-41559.y2LoK8.rst 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/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..a8e72cb7cbb68d --- /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 aid caching in other libraries and prepare for +:pep:`612` in Python 3.10. From d4c9a59c241daab5b734de6029221a4fc54064b0 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 3 Jan 2021 14:56:02 +0800 Subject: [PATCH 3/9] make tuple support less iffy --- Lib/test/test_genericalias.py | 8 ++++---- Objects/genericaliasobject.c | 15 ++++++--------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_genericalias.py b/Lib/test/test_genericalias.py index fb2c42ffe50a79..bb4bdc4bb57462 100644 --- a/Lib/test/test_genericalias.py +++ b/Lib/test/test_genericalias.py @@ -403,10 +403,10 @@ def test_tupleify(self): # Most likely to refleak due to nested list inside tuple. self.assertEqual(list[int, (str, [str, dict])], list[int, (str, (str, dict))]) - x = (int, str, [int, list]) - self.assertEqual(list[x], list[int, str, (int, list)]) - # Make sure the original tuple wasn't modified. - self.assertEqual(x, (int, str, [int, list])) + 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]))) if __name__ == "__main__": diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index 570d831dfe21f4..88c21662c0e3d2 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -226,7 +226,7 @@ tuple_add(PyObject *self, Py_ssize_t len, PyObject *item) return 0; } -// Makes a deep copy of a tuple +// Makes a shallow copy of a tuple static inline PyObject * tuple_copy(PyObject *obj) { return _PyTuple_FromArray(((PyTupleObject *)obj)->ob_item, Py_SIZE(obj)); @@ -241,18 +241,15 @@ tupleify_lists(PyObject *args) { Py_ssize_t len = PyTuple_GET_SIZE(args); for (Py_ssize_t i = 0; i < len; ++i) { PyObject *arg = PyTuple_GET_ITEM(args, i); - if (PyList_CheckExact(arg)) { - PyObject *new_arg = PyList_AsTuple(arg); + int is_list = PyList_CheckExact(arg); + int is_tuple = PyTuple_CheckExact(arg); + if (is_list || is_tuple) { + // In case there are lists nested inside tuples. + PyObject *new_arg = is_list ? PyList_AsTuple(arg) : tuple_copy(arg); tupleify_lists(new_arg); PyTuple_SET_ITEM(args, i, new_arg); Py_DECREF(arg); } - else if (PyTuple_CheckExact(arg)) { - // In case there are lists nested inside tuples. - // This works despite interned tuples because tuples containing - // lists aren't interned. - tupleify_lists(arg); - } } } From cddc54b8c5a14df6b0c82add1c7388fb833886e9 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sat, 9 Jan 2021 11:12:59 +0800 Subject: [PATCH 4/9] apply suggestions by zackery --- Lib/test/test_genericalias.py | 4 ++++ Objects/genericaliasobject.c | 44 ++++++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_genericalias.py b/Lib/test/test_genericalias.py index bb4bdc4bb57462..1249b3f2256a40 100644 --- a/Lib/test/test_genericalias.py +++ b/Lib/test/test_genericalias.py @@ -408,6 +408,10 @@ def test_tupleify(self): # 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/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index 22edc58c212a68..41cf0f7fa2c135 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -228,29 +228,49 @@ tuple_add(PyObject *self, Py_ssize_t len, PyObject *item) // Makes a shallow copy of a tuple static inline PyObject * -tuple_copy(PyObject *obj) { +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 help caching in other libraries. -// Must be called on a copy of a tuple object, where the only reference is -// owned by the caller. -static inline void +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(args, i); + PyObject *arg = PyTuple_GET_ITEM(result, i); int is_list = PyList_CheckExact(arg); int is_tuple = PyTuple_CheckExact(arg); if (is_list || is_tuple) { // In case there are lists nested inside tuples. - PyObject *new_arg = is_list ? PyList_AsTuple(arg) : tuple_copy(arg); - tupleify_lists(new_arg); - PyTuple_SET_ITEM(args, i, new_arg); - Py_DECREF(arg); + PyObject *new_arg = is_list ? PyList_AsTuple(arg) : arg; + if (new_arg == NULL) { + goto error; + } + PyObject *new_arg_tupled = tupleify_lists(new_arg); + if (new_arg_tupled == NULL) { + Py_DECREF(new_arg); + goto error; + } + Py_DECREF(new_arg); + PyTuple_SET_ITEM(result, i, new_arg_tupled); + // PyTuple_SET_ITEM doesn't DECREF existing item automatically. + if (is_list) { + Py_DECREF(arg); + } + continue; + + error: + Py_DECREF(result); + return NULL; } } + return result; } static PyObject * @@ -641,9 +661,11 @@ setup_ga(gaobject *alias, PyObject *origin, PyObject *args) { else { Py_INCREF(args); } - PyObject *new_args = tuple_copy(args); - tupleify_lists(new_args); + PyObject *new_args = tupleify_lists(args); Py_DECREF(args); + if (new_args == NULL) { + return 0; + } Py_INCREF(origin); alias->origin = origin; From b365cc1d4ef01fc93fb11a991902004983f4316c Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sat, 9 Jan 2021 11:37:15 +0800 Subject: [PATCH 5/9] fix order --- Objects/genericaliasobject.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index 41cf0f7fa2c135..20d9bb705c8211 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -252,6 +252,9 @@ tupleify_lists(PyObject *args) { if (new_arg == NULL) { goto error; } + if (is_list) { + Py_DECREF(arg); + } PyObject *new_arg_tupled = tupleify_lists(new_arg); if (new_arg_tupled == NULL) { Py_DECREF(new_arg); @@ -259,10 +262,6 @@ tupleify_lists(PyObject *args) { } Py_DECREF(new_arg); PyTuple_SET_ITEM(result, i, new_arg_tupled); - // PyTuple_SET_ITEM doesn't DECREF existing item automatically. - if (is_list) { - Py_DECREF(arg); - } continue; error: From e58597f0ef1e22ca120973c8bbf916848730691c Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 10 Jan 2021 23:36:04 +0800 Subject: [PATCH 6/9] Add recursion guards --- Objects/genericaliasobject.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index 20d9bb705c8211..4d5a8a53a2ff9c 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -253,9 +253,15 @@ tupleify_lists(PyObject *args) { goto error; } if (is_list) { + // Discard old arg since AsTuple gives a new reference. Py_DECREF(arg); } + if (Py_EnterRecursiveCall(" while converting lists to tuples in " + "GenericAlias' __args__")) { + goto error; + } PyObject *new_arg_tupled = tupleify_lists(new_arg); + Py_LeaveRecursiveCall(); if (new_arg_tupled == NULL) { Py_DECREF(new_arg); goto error; From bd57c35a7b1a804e60148e5dd7b4ebef8bedf7e0 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sun, 10 Jan 2021 23:40:10 +0800 Subject: [PATCH 7/9] remove redundant stuff --- Objects/genericaliasobject.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index 4d5a8a53a2ff9c..cd2efb510df241 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -262,11 +262,10 @@ tupleify_lists(PyObject *args) { } PyObject *new_arg_tupled = tupleify_lists(new_arg); Py_LeaveRecursiveCall(); + Py_DECREF(new_arg); if (new_arg_tupled == NULL) { - Py_DECREF(new_arg); goto error; } - Py_DECREF(new_arg); PyTuple_SET_ITEM(result, i, new_arg_tupled); continue; From 0389d960f267f7dbc9d62a3fcb33f859767f7100 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 5 Feb 2021 00:04:47 +0800 Subject: [PATCH 8/9] clean up code a little --- Objects/genericaliasobject.c | 51 +++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/Objects/genericaliasobject.c b/Objects/genericaliasobject.c index cd2efb510df241..79d7d494cd7b19 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -244,37 +244,40 @@ tupleify_lists(PyObject *args) { } for (Py_ssize_t i = 0; i < len; ++i) { PyObject *arg = PyTuple_GET_ITEM(result, i); - int is_list = PyList_CheckExact(arg); - int is_tuple = PyTuple_CheckExact(arg); - if (is_list || is_tuple) { - // In case there are lists nested inside tuples. - PyObject *new_arg = is_list ? PyList_AsTuple(arg) : arg; + if (arg == NULL) { + goto error; + } + PyObject *new_arg = NULL; + if (PyList_CheckExact(arg)) { + new_arg = PyList_AsTuple(arg); if (new_arg == NULL) { goto error; } - if (is_list) { - // Discard old arg since AsTuple gives a new reference. - Py_DECREF(arg); - } - if (Py_EnterRecursiveCall(" while converting lists to tuples in " - "GenericAlias' __args__")) { - goto error; - } - PyObject *new_arg_tupled = tupleify_lists(new_arg); - Py_LeaveRecursiveCall(); - Py_DECREF(new_arg); - if (new_arg_tupled == NULL) { - goto error; - } - PyTuple_SET_ITEM(result, i, new_arg_tupled); + Py_DECREF(arg); + } + else if (PyTuple_CheckExact(arg)) { + new_arg = arg; + } + else { continue; - - error: - Py_DECREF(result); - return NULL; } + 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 * From a97ff336e3e9e2282ad846ef8132fa3376c90159 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Sat, 13 Feb 2021 13:32:51 +0800 Subject: [PATCH 9/9] simplify code and update news --- .../2021-01-02-17-37-08.bpo-41559.y2LoK8.rst | 4 ++-- Objects/genericaliasobject.c | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) 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 index a8e72cb7cbb68d..b981ce2655266e 100644 --- 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 @@ -1,3 +1,3 @@ :ref:`Generic Alias ` objects will now convert all lists -received to tuples to aid caching in other libraries and prepare for -:pep:`612` in Python 3.10. +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 79d7d494cd7b19..19192e5cb52400 100644 --- a/Objects/genericaliasobject.c +++ b/Objects/genericaliasobject.c @@ -234,7 +234,8 @@ tuple_copy(PyObject *obj) } // This converts all nested lists in args to a tuple (args should be a tuple). -// Usually needed to help caching in other libraries. +// 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); @@ -247,7 +248,7 @@ tupleify_lists(PyObject *args) { if (arg == NULL) { goto error; } - PyObject *new_arg = NULL; + PyObject *new_arg = arg; if (PyList_CheckExact(arg)) { new_arg = PyList_AsTuple(arg); if (new_arg == NULL) { @@ -255,10 +256,7 @@ tupleify_lists(PyObject *args) { } Py_DECREF(arg); } - else if (PyTuple_CheckExact(arg)) { - new_arg = arg; - } - else { + else if (!PyTuple_CheckExact(arg)) { continue; } if (Py_EnterRecursiveCall(" while converting lists to tuples in "