From c14955c1ee5abf94db58f1b26728f040d5302c46 Mon Sep 17 00:00:00 2001 From: Carl Friedrich Bolz-Tereick Date: Wed, 14 Dec 2022 15:48:49 +0100 Subject: [PATCH 1/3] bring functools.py partial implementation more in line with C code in partial.__new__, before checking for the existence of the attribute 'func', first check whether the argument is an instance of partial. --- Lib/functools.py | 2 +- Lib/test/test_functools.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Lib/functools.py b/Lib/functools.py index 43ead512e1ea4e..7101d4982e153b 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -284,7 +284,7 @@ def __new__(cls, func, /, *args, **keywords): if not callable(func): raise TypeError("the first argument must be callable") - if hasattr(func, "func"): + if isinstance(func, partial) and hasattr(func, "func"): args = func.args + args keywords = {**func.keywords, **keywords} func = func.func diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 730ab1f595f22c..147d09b6048196 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -183,6 +183,19 @@ def test_nested_optimization(self): flat = partial(signature, 'asdf', bar=True) self.assertEqual(signature(nested), signature(flat)) + def test_nested_optimization_bug(self): + partial = self.partial + class Builder: + def __call__(self, tag, *children, **attrib): + return (tag, children, attrib) + + def __getattr__(self, tag): + return partial(self, tag) + + B = Builder() + m = B.m + assert m(1, 2, a=2) == ('m', (1, 2), dict(a=2)) + def test_nested_partial_with_attribute(self): # see issue 25137 partial = self.partial From 30557a1fa5a22ae3e9f61d45462ae628366d50f4 Mon Sep 17 00:00:00 2001 From: Carl Friedrich Bolz-Tereick Date: Wed, 14 Dec 2022 15:53:42 +0100 Subject: [PATCH 2/3] blurb --- .../Library/2022-12-14-15-53-38.gh-issue-100242.Ny7VUO.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2022-12-14-15-53-38.gh-issue-100242.Ny7VUO.rst diff --git a/Misc/NEWS.d/next/Library/2022-12-14-15-53-38.gh-issue-100242.Ny7VUO.rst b/Misc/NEWS.d/next/Library/2022-12-14-15-53-38.gh-issue-100242.Ny7VUO.rst new file mode 100644 index 00000000000000..17f0d07f3f584b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-12-14-15-53-38.gh-issue-100242.Ny7VUO.rst @@ -0,0 +1,3 @@ +Bring pure Python implementation ``functools.partial.__new__`` more in line +with the C-implementation by not just always checking for the presence of +the attribute ``'func'`` on the first argument of ``partial``. From 97b9966bc7bbb6a21e70a318dbce9b3a2f63373f Mon Sep 17 00:00:00 2001 From: Carl Friedrich Bolz-Tereick Date: Wed, 14 Dec 2022 17:30:45 +0100 Subject: [PATCH 3/3] use PyObject_TypeCheck in the C implementation of partial.__new__ --- Lib/functools.py | 2 +- ...2022-12-14-15-53-38.gh-issue-100242.Ny7VUO.rst | 4 +++- Modules/_functoolsmodule.c | 15 +++++++++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Lib/functools.py b/Lib/functools.py index 7101d4982e153b..f125cff46fa428 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -284,7 +284,7 @@ def __new__(cls, func, /, *args, **keywords): if not callable(func): raise TypeError("the first argument must be callable") - if isinstance(func, partial) and hasattr(func, "func"): + if isinstance(func, partial): args = func.args + args keywords = {**func.keywords, **keywords} func = func.func diff --git a/Misc/NEWS.d/next/Library/2022-12-14-15-53-38.gh-issue-100242.Ny7VUO.rst b/Misc/NEWS.d/next/Library/2022-12-14-15-53-38.gh-issue-100242.Ny7VUO.rst index 17f0d07f3f584b..e86059942d52db 100644 --- a/Misc/NEWS.d/next/Library/2022-12-14-15-53-38.gh-issue-100242.Ny7VUO.rst +++ b/Misc/NEWS.d/next/Library/2022-12-14-15-53-38.gh-issue-100242.Ny7VUO.rst @@ -1,3 +1,5 @@ Bring pure Python implementation ``functools.partial.__new__`` more in line with the C-implementation by not just always checking for the presence of -the attribute ``'func'`` on the first argument of ``partial``. +the attribute ``'func'`` on the first argument of ``partial``. Instead, both +the Python version and the C version perform an ``isinstance(func, partial)`` +check on the first argument of ``partial``. diff --git a/Modules/_functoolsmodule.c b/Modules/_functoolsmodule.c index 4032ba79374fa4..af6962f3967ea5 100644 --- a/Modules/_functoolsmodule.c +++ b/Modules/_functoolsmodule.c @@ -79,12 +79,19 @@ partial_new(PyTypeObject *type, PyObject *args, PyObject *kw) return NULL; } + _functools_state *state = get_functools_state_by_type(type); + if (state == NULL) { + return NULL; + } + pargs = pkw = NULL; func = PyTuple_GET_ITEM(args, 0); - if (Py_TYPE(func)->tp_call == (ternaryfunc)partial_call) { - // The type of "func" might not be exactly the same type object - // as "type", but if it is called using partial_call, it must have the - // same memory layout (fn, args and kw members). + + int res = PyObject_TypeCheck(func, state->partial_type); + if (res == -1) { + return NULL; + } + if (res == 1) { // We can use its underlying function directly and merge the arguments. partialobject *part = (partialobject *)func; if (part->dict == NULL) {