diff --git a/Lib/functools.py b/Lib/functools.py index 62f9349abbf62c..bb3e3e0a78e3b9 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -310,15 +310,7 @@ def _partial_prepare_merger(args): else: order.append(i) phcount = j - nargs - if phcount: - if nargs == 1: - i = order[0] - def merger(all_args): - return (all_args[i],) - else: - merger = itemgetter(*order) - else: - merger = None + merger = itemgetter(*order) if phcount else None return phcount, merger def _partial_repr(self): @@ -342,6 +334,8 @@ class partial: def __new__(cls, func, /, *args, **keywords): if not callable(func): raise TypeError("the first argument must be callable") + if args and args[-1] is Placeholder: + raise TypeError("trailing Placeholders are not allowed") if isinstance(func, partial): pto_phcount = func._phcount tot_args = func.args @@ -409,6 +403,8 @@ def __setstate__(self, state): (namespace is not None and not isinstance(namespace, dict))): raise TypeError("invalid partial state") + if args and args[-1] is Placeholder: + raise TypeError("trailing Placeholders are not allowed") phcount, merger = _partial_prepare_merger(args) args = tuple(args) # just in case it's a subclass diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 71112b889541f7..bdaa9a7ec4f020 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -211,16 +211,11 @@ def foo(bar): p2.new_attr = 'spam' self.assertEqual(p2.new_attr, 'spam') - def test_trailing_placeholders(self): + def test_placeholders_trailing_raise(self): PH = self.module.Placeholder - for args, call_args, expected_args in [ - [(PH,), (1,), (1,)], - [(0, PH), (1,), (0, 1)], - [(0, PH, 2, PH, PH), (1, 3, 4), (0, 1, 2, 3, 4)] - ]: - actual_args, actual_kwds = self.partial(capture, *args)(*call_args) - self.assertEqual(actual_args, expected_args) - self.assertEqual(actual_kwds, {}) + for args in [(PH,), (0, PH), (0, PH, 1, PH, PH, PH)]: + with self.assertRaises(TypeError): + self.partial(capture, *args) def test_placeholders(self): PH = self.module.Placeholder @@ -373,6 +368,12 @@ def test_setstate(self): f() self.assertEqual(f(2), ((2, 1), dict(a=10))) + # Trailing Placeholder error + f = self.partial(signature) + msg_regex = re.escape("trailing Placeholders are not allowed") + with self.assertRaisesRegex(TypeError, f'^{msg_regex}$') as cm: + f.__setstate__((capture, (1, PH), dict(a=10), dict(attr=[]))) + def test_setstate_errors(self): f = self.partial(signature) self.assertRaises(TypeError, f.__setstate__, (capture, (), {})) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 6f61e00b28b53b..894d7b10f8cbf8 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -3714,6 +3714,11 @@ def test(self: 'anno', x): ((('self', ..., 'anno', 'positional_only'),), ...)) + def test_signature_on_fake_partialmethod(self): + def foo(a): pass + foo.__partialmethod__ = 'spam' + self.assertEqual(str(inspect.signature(foo)), '(a)') + def test_signature_on_decorated(self): def decorator(func): @functools.wraps(func) diff --git a/Misc/NEWS.d/next/Library/2024-10-17-00-50-32.gh-issue-124652.AK3PDp.rst b/Misc/NEWS.d/next/Library/2024-10-17-00-50-32.gh-issue-124652.AK3PDp.rst index 260c0dcc8b314b..43eb2e90285ba7 100644 --- a/Misc/NEWS.d/next/Library/2024-10-17-00-50-32.gh-issue-124652.AK3PDp.rst +++ b/Misc/NEWS.d/next/Library/2024-10-17-00-50-32.gh-issue-124652.AK3PDp.rst @@ -1,2 +1 @@ -:func:`functools.partial` now allows trailing placeholders, which are converted to positional-only arguments. :func:`functools.partialmethod` is simplified by making use of new :func:`functools.partial` placeholder functionality. diff --git a/Modules/_functoolsmodule.c b/Modules/_functoolsmodule.c index 9ec90ceb19324a..339e2dd6b00d7b 100644 --- a/Modules/_functoolsmodule.c +++ b/Modules/_functoolsmodule.c @@ -187,6 +187,14 @@ partial_new(PyTypeObject *type, PyObject *args, PyObject *kw) if (state == NULL) { return NULL; } + phold = state->placeholder; + + /* Placeholder restrictions */ + if (new_nargs && PyTuple_GET_ITEM(args, new_nargs) == phold) { + PyErr_SetString(PyExc_TypeError, + "trailing Placeholders are not allowed"); + return NULL; + } /* check wrapped function / object */ pto_args = pto_kw = NULL; @@ -212,86 +220,66 @@ partial_new(PyTypeObject *type, PyObject *args, PyObject *kw) if (pto == NULL) return NULL; - phold = state->placeholder; pto->fn = Py_NewRef(func); pto->placeholder = phold; - /* process args */ - if (new_nargs == 0) { - if (pto_args == NULL) { - pto->args = PyTuple_New(0); - pto->phcount = 0; - } - else { - pto->args = pto_args; - pto->phcount = pto_phcount; - Py_INCREF(pto_args); - assert(PyTuple_Check(pto->args)); - } + new_args = PyTuple_GetSlice(args, 1, new_nargs + 1); + if (new_args == NULL) { + Py_DECREF(pto); + return NULL; } - else { - /* Count placeholders */ - Py_ssize_t phcount = 0; - for (Py_ssize_t i = 0; i < new_nargs; i++) { - if (PyTuple_GET_ITEM(args, i + 1) == phold) { - phcount++; - } + + /* Count placeholders */ + Py_ssize_t phcount = 0; + for (Py_ssize_t i = 0; i < new_nargs - 1; i++) { + if (PyTuple_GET_ITEM(new_args, i) == phold) { + phcount++; } - if (pto_args == NULL) { - new_args = PyTuple_GetSlice(args, 1, new_nargs + 1); - if (new_args == NULL) { - Py_DECREF(pto); - return NULL; - } - pto->args = new_args; - pto->phcount = phcount; + } + /* merge args with args of `func` which is `partial` */ + if (pto_phcount > 0 && new_nargs > 0) { + Py_ssize_t npargs = PyTuple_GET_SIZE(pto_args); + Py_ssize_t tot_nargs = npargs; + if (new_nargs > pto_phcount) { + tot_nargs += new_nargs - pto_phcount; } - else { - /* merge args with args of `func` which is `partial` */ - Py_ssize_t npargs = PyTuple_GET_SIZE(pto_args); - Py_ssize_t tot_nargs = npargs; - if (new_nargs > pto_phcount) { - tot_nargs += new_nargs - pto_phcount; - } - PyObject *tot_args = PyTuple_New(tot_nargs); - PyObject *item; - if (pto_phcount > 0) { - for (Py_ssize_t i = 0, j = 0; i < tot_nargs; ++i) { - if (i < npargs) { - item = PyTuple_GET_ITEM(pto_args, i); - if (j < new_nargs && item == phold) { - item = PyTuple_GET_ITEM(args, j + 1); - j++; - pto_phcount--; - } - } - else { - item = PyTuple_GET_ITEM(args, j + 1); - j++; - } - Py_INCREF(item); - PyTuple_SET_ITEM(tot_args, i, item); + PyObject *item; + PyObject *tot_args = PyTuple_New(tot_nargs); + for (Py_ssize_t i = 0, j = 0; i < tot_nargs; i++) { + if (i < npargs) { + item = PyTuple_GET_ITEM(pto_args, i); + if (j < new_nargs && item == phold) { + item = PyTuple_GET_ITEM(new_args, j); + j++; + pto_phcount--; } } else { - for (Py_ssize_t i = 0; i < npargs; ++i) { - item = PyTuple_GET_ITEM(pto_args, i); - Py_INCREF(item); - PyTuple_SET_ITEM(tot_args, i, item); - } - for (Py_ssize_t i = 0; i < new_nargs; ++i) { - item = PyTuple_GET_ITEM(args, i + 1); - Py_INCREF(item); - PyTuple_SET_ITEM(tot_args, npargs + i, item); - } + item = PyTuple_GET_ITEM(new_args, j); + j++; } - pto->args = tot_args; - pto->phcount = pto_phcount + phcount; - assert(PyTuple_Check(pto->args)); + Py_INCREF(item); + PyTuple_SET_ITEM(tot_args, i, item); } + pto->args = tot_args; + pto->phcount = pto_phcount + phcount; + Py_DECREF(new_args); + } + else if (pto_args == NULL) { + pto->args = new_args; + pto->phcount = phcount; + } + else { + pto->args = PySequence_Concat(pto_args, new_args); + pto->phcount = pto_phcount + phcount; + Py_DECREF(new_args); + if (pto->args == NULL) { + Py_DECREF(pto); + return NULL; + } + assert(PyTuple_Check(pto->args)); } - /* process keywords */ if (pto_kw == NULL || PyDict_GET_SIZE(pto_kw) == 0) { if (kw == NULL) { pto->kw = PyDict_New(); @@ -414,12 +402,6 @@ partial_vectorcall(PyObject *self, PyObject *const *args, pto_args, pto_nargs, NULL); } - /* Fast path if all Placeholders */ - if (pto_nargs == pto_phcount) { - return _PyObject_VectorcallTstate(tstate, pto->fn, - args, nargs, kwnames); - } - /* Fast path using PY_VECTORCALL_ARGUMENTS_OFFSET to prepend a single * positional argument */ if (pto_nargs == 1 && (nargsf & PY_VECTORCALL_ARGUMENTS_OFFSET)) { @@ -711,8 +693,14 @@ partial_setstate(PyObject *self, PyObject *state) return NULL; } - /* Count placeholders */ Py_ssize_t nargs = PyTuple_GET_SIZE(fnargs); + if (nargs && PyTuple_GET_ITEM(fnargs, nargs - 1) == pto->placeholder) { + PyErr_SetString(PyExc_TypeError, + "trailing Placeholders are not allowed"); + return NULL; + } + + /* Count placeholders */ Py_ssize_t phcount = 0; for (Py_ssize_t i = 0; i < nargs - 1; i++) { if (PyTuple_GET_ITEM(fnargs, i) == pto->placeholder) {