From 317acb80387674db8c94f48bb9823ae516d05f5c Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Sat, 5 Nov 2022 20:08:47 +0300 Subject: [PATCH] gh-94808: add tests covering `PyFunction_GetKwDefaults` and `PyFunction_SetKwDefaults` (GH-98809) --- Lib/test/test_capi.py | 98 +++++++++++++++++++++++++++++++++++++-- Modules/_testcapimodule.c | 29 ++++++++++++ 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_capi.py b/Lib/test/test_capi.py index 0ecc6481cfc..213b6d4feb6 100644 --- a/Lib/test/test_capi.py +++ b/Lib/test/test_capi.py @@ -1038,7 +1038,14 @@ class CAPITest(unittest.TestCase): _testcapi.function_get_module(None) # not a function def test_function_get_defaults(self): - def some(pos_only='p', zero=0, optional=None): + def some( + pos_only1, pos_only2='p', + /, + zero=0, optional=None, + *, + kw1, + kw2=True, + ): pass defaults = _testcapi.function_get_defaults(some) @@ -1046,10 +1053,17 @@ class CAPITest(unittest.TestCase): self.assertEqual(defaults, some.__defaults__) with self.assertRaises(SystemError): - _testcapi.function_get_module(None) # not a function + _testcapi.function_get_defaults(None) # not a function def test_function_set_defaults(self): - def some(pos_only='p', zero=0, optional=None): + def some( + pos_only1, pos_only2='p', + /, + zero=0, optional=None, + *, + kw1, + kw2=True, + ): pass old_defaults = ('p', 0, None) @@ -1061,11 +1075,22 @@ class CAPITest(unittest.TestCase): self.assertEqual(_testcapi.function_get_defaults(some), old_defaults) self.assertEqual(some.__defaults__, old_defaults) + with self.assertRaises(SystemError): + _testcapi.function_set_defaults(1, ()) # not a function + self.assertEqual(_testcapi.function_get_defaults(some), old_defaults) + self.assertEqual(some.__defaults__, old_defaults) + new_defaults = ('q', 1, None) _testcapi.function_set_defaults(some, new_defaults) self.assertEqual(_testcapi.function_get_defaults(some), new_defaults) self.assertEqual(some.__defaults__, new_defaults) + # Empty tuple is fine: + new_defaults = () + _testcapi.function_set_defaults(some, new_defaults) + self.assertEqual(_testcapi.function_get_defaults(some), new_defaults) + self.assertEqual(some.__defaults__, new_defaults) + class tuplesub(tuple): ... # tuple subclasses must work new_defaults = tuplesub(((1, 2), ['a', 'b'], None)) @@ -1079,6 +1104,73 @@ class CAPITest(unittest.TestCase): self.assertEqual(_testcapi.function_get_defaults(some), None) self.assertEqual(some.__defaults__, None) + def test_function_get_kw_defaults(self): + def some( + pos_only1, pos_only2='p', + /, + zero=0, optional=None, + *, + kw1, + kw2=True, + ): + pass + + defaults = _testcapi.function_get_kw_defaults(some) + self.assertEqual(defaults, {'kw2': True}) + self.assertEqual(defaults, some.__kwdefaults__) + + with self.assertRaises(SystemError): + _testcapi.function_get_kw_defaults(None) # not a function + + def test_function_set_kw_defaults(self): + def some( + pos_only1, pos_only2='p', + /, + zero=0, optional=None, + *, + kw1, + kw2=True, + ): + pass + + old_defaults = {'kw2': True} + self.assertEqual(_testcapi.function_get_kw_defaults(some), old_defaults) + self.assertEqual(some.__kwdefaults__, old_defaults) + + with self.assertRaises(SystemError): + _testcapi.function_set_kw_defaults(some, 1) # not dict or None + self.assertEqual(_testcapi.function_get_kw_defaults(some), old_defaults) + self.assertEqual(some.__kwdefaults__, old_defaults) + + with self.assertRaises(SystemError): + _testcapi.function_set_kw_defaults(1, {}) # not a function + self.assertEqual(_testcapi.function_get_kw_defaults(some), old_defaults) + self.assertEqual(some.__kwdefaults__, old_defaults) + + new_defaults = {'kw2': (1, 2, 3)} + _testcapi.function_set_kw_defaults(some, new_defaults) + self.assertEqual(_testcapi.function_get_kw_defaults(some), new_defaults) + self.assertEqual(some.__kwdefaults__, new_defaults) + + # Empty dict is fine: + new_defaults = {} + _testcapi.function_set_kw_defaults(some, new_defaults) + self.assertEqual(_testcapi.function_get_kw_defaults(some), new_defaults) + self.assertEqual(some.__kwdefaults__, new_defaults) + + class dictsub(dict): ... # dict subclasses must work + + new_defaults = dictsub({'kw2': None}) + _testcapi.function_set_kw_defaults(some, new_defaults) + self.assertEqual(_testcapi.function_get_kw_defaults(some), new_defaults) + self.assertEqual(some.__kwdefaults__, new_defaults) + + # `None` is special, it sets `kwdefaults` to `NULL`, + # it needs special handling in `_testcapi`: + _testcapi.function_set_kw_defaults(some, None) + self.assertEqual(_testcapi.function_get_kw_defaults(some), None) + self.assertEqual(some.__kwdefaults__, None) + class TestPendingCalls(unittest.TestCase): diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index f0f6d809cc8..66d1d476328 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -5862,6 +5862,33 @@ function_set_defaults(PyObject *self, PyObject *args) Py_RETURN_NONE; } +static PyObject * +function_get_kw_defaults(PyObject *self, PyObject *func) +{ + PyObject *defaults = PyFunction_GetKwDefaults(func); + if (defaults != NULL) { + Py_INCREF(defaults); + return defaults; + } else if (PyErr_Occurred()) { + return NULL; + } else { + Py_RETURN_NONE; // This can happen when `kwdefaults` are set to `None` + } +} + +static PyObject * +function_set_kw_defaults(PyObject *self, PyObject *args) +{ + PyObject *func = NULL, *defaults = NULL; + if (!PyArg_ParseTuple(args, "OO", &func, &defaults)) { + return NULL; + } + int result = PyFunction_SetKwDefaults(func, defaults); + if (result == -1) + return NULL; + Py_RETURN_NONE; +} + // type watchers @@ -6281,6 +6308,8 @@ static PyMethodDef TestMethods[] = { {"function_get_module", function_get_module, METH_O, NULL}, {"function_get_defaults", function_get_defaults, METH_O, NULL}, {"function_set_defaults", function_set_defaults, METH_VARARGS, NULL}, + {"function_get_kw_defaults", function_get_kw_defaults, METH_O, NULL}, + {"function_set_kw_defaults", function_set_kw_defaults, METH_VARARGS, NULL}, {"add_type_watcher", add_type_watcher, METH_O, NULL}, {"clear_type_watcher", clear_type_watcher, METH_O, NULL}, {"watch_type", watch_type, METH_VARARGS, NULL},