Skip to content

Commit 106e6bc

Browse files
committed
decouple error checking and kw merging + tests
1 parent e8d7521 commit 106e6bc

File tree

3 files changed

+42
-19
lines changed

3 files changed

+42
-19
lines changed

Lib/functools.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,8 +326,9 @@ def _partial_new(cls, func, /, *args, **keywords):
326326
"or a descriptor")
327327
if args and args[-1] is Placeholder:
328328
raise TypeError("trailing Placeholders are not allowed")
329-
if keywords and Placeholder in keywords.values():
330-
raise TypeError("Placeholder cannot be passed as a keyword argument")
329+
for value in keywords.values():
330+
if value is Placeholder:
331+
raise TypeError("Placeholder cannot be passed as a keyword argument")
331332
if isinstance(func, base_cls):
332333
pto_phcount = func._phcount
333334
tot_args = func.args

Lib/test/test_functools.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,12 @@ def test_placeholders(self):
233233
actual_args, actual_kwds = p('x', 'y')
234234
self.assertEqual(actual_args, ('x', 0, 'y', 1))
235235
self.assertEqual(actual_kwds, {})
236+
# Checks via `is` and not `eq`
237+
# thus unittest.mock.ANY isn't treated as Placeholder
238+
p = self.partial(capture, unittest.mock.ANY)
239+
actual_args, actual_kwds = p()
240+
self.assertEqual(actual_args, (unittest.mock.ANY,))
241+
self.assertEqual(actual_kwds, {})
236242

237243
def test_placeholders_optimization(self):
238244
PH = self.module.Placeholder
@@ -253,6 +259,11 @@ def test_placeholders_kw_restriction(self):
253259
PH = self.module.Placeholder
254260
with self.assertRaisesRegex(TypeError, "Placeholder"):
255261
self.partial(capture, a=PH)
262+
# Passes, as checks via `is` and not `eq`
263+
p = self.partial(capture, a=unittest.mock.ANY)
264+
actual_args, actual_kwds = p()
265+
self.assertEqual(actual_args, ())
266+
self.assertEqual(actual_kwds, {'a': unittest.mock.ANY})
256267

257268
def test_construct_placeholder_singleton(self):
258269
PH = self.module.Placeholder

Modules/_functoolsmodule.c

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,19 @@ partial_new(PyTypeObject *type, PyObject *args, PyObject *kw)
196196
return NULL;
197197
}
198198

199+
/* keyword Placeholder prohibition */
200+
if (kw != NULL) {
201+
PyObject *key, *val;
202+
Py_ssize_t pos = 0;
203+
while (PyDict_Next(kw, &pos, &key, &val)) {
204+
if (val == phold) {
205+
PyErr_SetString(PyExc_TypeError,
206+
"Placeholder cannot be passed as a keyword argument");
207+
return NULL;
208+
}
209+
}
210+
}
211+
199212
/* check wrapped function / object */
200213
pto_args = pto_kw = NULL;
201214
int res = PyObject_TypeCheck(func, state->partial_type);
@@ -281,31 +294,29 @@ partial_new(PyTypeObject *type, PyObject *args, PyObject *kw)
281294
}
282295

283296
if (pto_kw == NULL || PyDict_GET_SIZE(pto_kw) == 0) {
284-
pto->kw = PyDict_New();
297+
if (kw == NULL) {
298+
pto->kw = PyDict_New();
299+
}
300+
else if (Py_REFCNT(kw) == 1) {
301+
pto->kw = Py_NewRef(kw);
302+
}
303+
else {
304+
pto->kw = PyDict_Copy(kw);
305+
}
285306
}
286307
else {
287308
pto->kw = PyDict_Copy(pto_kw);
288-
}
289-
if (pto->kw == NULL) {
290-
Py_DECREF(pto);
291-
return NULL;
292-
}
293-
if (kw != NULL) {
294-
PyObject *key, *val;
295-
Py_ssize_t pos = 0;
296-
while (PyDict_Next(kw, &pos, &key, &val)) {
297-
if (val == phold) {
298-
Py_DECREF(pto);
299-
PyErr_SetString(PyExc_TypeError,
300-
"Placeholder cannot be passed as a keyword argument");
301-
return NULL;
302-
}
303-
if (PyDict_SetItem(pto->kw, key, val) < 0) {
309+
if (kw != NULL && pto->kw != NULL) {
310+
if (PyDict_Merge(pto->kw, kw, 1) != 0) {
304311
Py_DECREF(pto);
305312
return NULL;
306313
}
307314
}
308315
}
316+
if (pto->kw == NULL) {
317+
Py_DECREF(pto);
318+
return NULL;
319+
}
309320

310321
partial_setvectorcall(pto);
311322
return (PyObject *)pto;

0 commit comments

Comments
 (0)