]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-125028: Prohibit placeholders in partial keywords (GH-126062)
authordgpb <3577712+dg-pb@users.noreply.github.com>
Thu, 8 May 2025 07:53:53 +0000 (10:53 +0300)
committerGitHub <noreply@github.com>
Thu, 8 May 2025 07:53:53 +0000 (10:53 +0300)
Doc/library/functools.rst
Lib/functools.py
Lib/test/test_functools.py
Misc/NEWS.d/next/Library/2024-10-28-06-54-22.gh-issue-125028.GEY8Ws.rst [new file with mode: 0644]
Modules/_functoolsmodule.c

index 3a933dff057bbba345eb008ab3479b47bffc658e..3e75621be6dad3ccca868e749d7ba38f13dd7f6f 100644 (file)
@@ -403,8 +403,7 @@ The :mod:`functools` module defines the following functions:
       >>> remove_first_dear(message)
       'Hello, dear world!'
 
-   :data:`!Placeholder` has no special treatment when used in a keyword
-   argument to :func:`!partial`.
+   :data:`!Placeholder` cannot be passed to :func:`!partial` as a keyword argument.
 
    .. versionchanged:: 3.14
       Added support for :data:`Placeholder` in positional arguments.
index 714070c6ac94609ed162e4ce357de8788fb6a071..7f0eac3f65020993fd6b8c0c3b106fead054322f 100644 (file)
@@ -323,6 +323,9 @@ def _partial_new(cls, func, /, *args, **keywords):
                             "or a descriptor")
     if args and args[-1] is Placeholder:
         raise TypeError("trailing Placeholders are not allowed")
+    for value in keywords.values():
+        if value is Placeholder:
+            raise TypeError("Placeholder cannot be passed as a keyword argument")
     if isinstance(func, base_cls):
         pto_phcount = func._phcount
         tot_args = func.args
index 2e794b0fc95a22d0031e51cc5e3e3641326dc6e9..f7e09fd771eaf2d9a6a4adf817f22adfec3ab1f8 100644 (file)
@@ -21,6 +21,7 @@ from weakref import proxy
 import contextlib
 from inspect import Signature
 
+from test.support import ALWAYS_EQ
 from test.support import import_helper
 from test.support import threading_helper
 from test.support import cpython_only
@@ -244,6 +245,13 @@ class TestPartial:
         actual_args, actual_kwds = p('x', 'y')
         self.assertEqual(actual_args, ('x', 0, 'y', 1))
         self.assertEqual(actual_kwds, {})
+        # Checks via `is` and not `eq`
+        # thus ALWAYS_EQ isn't treated as Placeholder
+        p = self.partial(capture, ALWAYS_EQ)
+        actual_args, actual_kwds = p()
+        self.assertEqual(len(actual_args), 1)
+        self.assertIs(actual_args[0], ALWAYS_EQ)
+        self.assertEqual(actual_kwds, {})
 
     def test_placeholders_optimization(self):
         PH = self.module.Placeholder
@@ -260,6 +268,17 @@ class TestPartial:
         self.assertEqual(p2.args, (PH, 0))
         self.assertEqual(p2(1), ((1, 0), {}))
 
+    def test_placeholders_kw_restriction(self):
+        PH = self.module.Placeholder
+        with self.assertRaisesRegex(TypeError, "Placeholder"):
+            self.partial(capture, a=PH)
+        # Passes, as checks via `is` and not `eq`
+        p = self.partial(capture, a=ALWAYS_EQ)
+        actual_args, actual_kwds = p()
+        self.assertEqual(actual_args, ())
+        self.assertEqual(len(actual_kwds), 1)
+        self.assertIs(actual_kwds['a'], ALWAYS_EQ)
+
     def test_construct_placeholder_singleton(self):
         PH = self.module.Placeholder
         tp = type(PH)
diff --git a/Misc/NEWS.d/next/Library/2024-10-28-06-54-22.gh-issue-125028.GEY8Ws.rst b/Misc/NEWS.d/next/Library/2024-10-28-06-54-22.gh-issue-125028.GEY8Ws.rst
new file mode 100644 (file)
index 0000000..09ebee4
--- /dev/null
@@ -0,0 +1 @@
+:data:`functools.Placeholder` cannot be passed to :func:`functools.partial` as a keyword argument.
index e6c454faf4b16f32927aa76fc2a2184f7e7f9114..899eef50ecc6007a922a2a484f84cf9dffc703a0 100644 (file)
@@ -196,6 +196,19 @@ partial_new(PyTypeObject *type, PyObject *args, PyObject *kw)
         return NULL;
     }
 
+    /* keyword Placeholder prohibition */
+    if (kw != NULL) {
+        PyObject *key, *val;
+        Py_ssize_t pos = 0;
+        while (PyDict_Next(kw, &pos, &key, &val)) {
+            if (val == phold) {
+                PyErr_SetString(PyExc_TypeError,
+                                "Placeholder cannot be passed as a keyword argument");
+                return NULL;
+            }
+        }
+    }
+
     /* check wrapped function / object */
     pto_args = pto_kw = NULL;
     int res = PyObject_TypeCheck(func, state->partial_type);