Skip to content

Commit

Permalink
decouple error checking and kw merging + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dg-pb committed Jan 6, 2025
1 parent e8d7521 commit 106e6bc
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 19 deletions.
5 changes: 3 additions & 2 deletions Lib/functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,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")
if keywords and Placeholder in keywords.values():
raise TypeError("Placeholder cannot be passed as a keyword argument")
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
Expand Down
11 changes: 11 additions & 0 deletions Lib/test/test_functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,12 @@ def test_placeholders(self):
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 unittest.mock.ANY isn't treated as Placeholder
p = self.partial(capture, unittest.mock.ANY)
actual_args, actual_kwds = p()
self.assertEqual(actual_args, (unittest.mock.ANY,))
self.assertEqual(actual_kwds, {})

def test_placeholders_optimization(self):
PH = self.module.Placeholder
Expand All @@ -253,6 +259,11 @@ 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=unittest.mock.ANY)
actual_args, actual_kwds = p()
self.assertEqual(actual_args, ())
self.assertEqual(actual_kwds, {'a': unittest.mock.ANY})

def test_construct_placeholder_singleton(self):
PH = self.module.Placeholder
Expand Down
45 changes: 28 additions & 17 deletions Modules/_functoolsmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -281,31 +294,29 @@ partial_new(PyTypeObject *type, PyObject *args, PyObject *kw)
}

if (pto_kw == NULL || PyDict_GET_SIZE(pto_kw) == 0) {
pto->kw = PyDict_New();
if (kw == NULL) {
pto->kw = PyDict_New();
}
else if (Py_REFCNT(kw) == 1) {
pto->kw = Py_NewRef(kw);
}
else {
pto->kw = PyDict_Copy(kw);
}
}
else {
pto->kw = PyDict_Copy(pto_kw);
}
if (pto->kw == NULL) {
Py_DECREF(pto);
return NULL;
}
if (kw != NULL) {
PyObject *key, *val;
Py_ssize_t pos = 0;
while (PyDict_Next(kw, &pos, &key, &val)) {
if (val == phold) {
Py_DECREF(pto);
PyErr_SetString(PyExc_TypeError,
"Placeholder cannot be passed as a keyword argument");
return NULL;
}
if (PyDict_SetItem(pto->kw, key, val) < 0) {
if (kw != NULL && pto->kw != NULL) {
if (PyDict_Merge(pto->kw, kw, 1) != 0) {
Py_DECREF(pto);
return NULL;
}
}
}
if (pto->kw == NULL) {
Py_DECREF(pto);
return NULL;
}

partial_setvectorcall(pto);
return (PyObject *)pto;
Expand Down

0 comments on commit 106e6bc

Please sign in to comment.