diff --git a/Lib/test/test_call.py b/Lib/test/test_call.py index 0992a0e3612..d929cfc0887 100644 --- a/Lib/test/test_call.py +++ b/Lib/test/test_call.py @@ -1,5 +1,10 @@ +import datetime import unittest from test.support import cpython_only +try: + import _testcapi +except ImportError: + _testcapi = None # The test cases here cover several paths through the function calling # code. They depend on the METH_XXX flag that is used to define a C @@ -176,5 +181,175 @@ class CFunctionCallsErrorMessages(unittest.TestCase): self.assertRaisesRegex(TypeError, msg, [].count, x=2, y=2) +def pyfunc(arg1, arg2): + return [arg1, arg2] + + +def pyfunc_noarg(): + return "noarg" + + +class PythonClass: + def method(self, arg1, arg2): + return [arg1, arg2] + + def method_noarg(self): + return "noarg" + + @classmethod + def class_method(cls): + return "classmethod" + + @staticmethod + def static_method(): + return "staticmethod" + + +PYTHON_INSTANCE = PythonClass() + + +IGNORE_RESULT = object() + + +@cpython_only +class FastCallTests(unittest.TestCase): + # Test calls with positional arguments + CALLS_POSARGS = ( + # (func, args: tuple, result) + + # Python function with 2 arguments + (pyfunc, (1, 2), [1, 2]), + + # Python function without argument + (pyfunc_noarg, (), "noarg"), + + # Python class methods + (PythonClass.class_method, (), "classmethod"), + (PythonClass.static_method, (), "staticmethod"), + + # Python instance methods + (PYTHON_INSTANCE.method, (1, 2), [1, 2]), + (PYTHON_INSTANCE.method_noarg, (), "noarg"), + (PYTHON_INSTANCE.class_method, (), "classmethod"), + (PYTHON_INSTANCE.static_method, (), "staticmethod"), + + # C function: METH_NOARGS + (globals, (), IGNORE_RESULT), + + # C function: METH_O + (id, ("hello",), IGNORE_RESULT), + + # C function: METH_VARARGS + (dir, (1,), IGNORE_RESULT), + + # C function: METH_VARARGS | METH_KEYWORDS + (min, (5, 9), 5), + + # C function: METH_FASTCALL + (divmod, (1000, 33), (30, 10)), + + # C type static method: METH_FASTCALL | METH_CLASS + (int.from_bytes, (b'\x01\x00', 'little'), 1), + + # bpo-30524: Test that calling a C type static method with no argument + # doesn't crash (ignore the result): METH_FASTCALL | METH_CLASS + (datetime.datetime.now, (), IGNORE_RESULT), + ) + + # Test calls with positional and keyword arguments + CALLS_KWARGS = ( + # (func, args: tuple, kwargs: dict, result) + + # Python function with 2 arguments + (pyfunc, (1,), {'arg2': 2}, [1, 2]), + (pyfunc, (), {'arg1': 1, 'arg2': 2}, [1, 2]), + + # Python instance methods + (PYTHON_INSTANCE.method, (1,), {'arg2': 2}, [1, 2]), + (PYTHON_INSTANCE.method, (), {'arg1': 1, 'arg2': 2}, [1, 2]), + + # C function: METH_VARARGS | METH_KEYWORDS + (max, ([],), {'default': 9}, 9), + + # C type static method: METH_FASTCALL | METH_CLASS + (int.from_bytes, (b'\x01\x00',), {'byteorder': 'little'}, 1), + (int.from_bytes, (), {'bytes': b'\x01\x00', 'byteorder': 'little'}, 1), + ) + + def check_result(self, result, expected): + if expected is IGNORE_RESULT: + return + self.assertEqual(result, expected) + + def test_fastcall(self): + # Test _PyObject_FastCall() + + for func, args, expected in self.CALLS_POSARGS: + with self.subTest(func=func, args=args): + result = _testcapi.pyobject_fastcall(func, args) + self.check_result(result, expected) + + if not args: + # args=NULL, nargs=0 + result = _testcapi.pyobject_fastcall(func, None) + self.check_result(result, expected) + + def test_fastcall_dict(self): + # Test _PyObject_FastCallDict() + + for func, args, expected in self.CALLS_POSARGS: + with self.subTest(func=func, args=args): + # kwargs=NULL + result = _testcapi.pyobject_fastcalldict(func, args, None) + self.check_result(result, expected) + + # kwargs={} + result = _testcapi.pyobject_fastcalldict(func, args, {}) + self.check_result(result, expected) + + if not args: + # args=NULL, nargs=0, kwargs=NULL + result = _testcapi.pyobject_fastcalldict(func, None, None) + self.check_result(result, expected) + + # args=NULL, nargs=0, kwargs={} + result = _testcapi.pyobject_fastcalldict(func, None, {}) + self.check_result(result, expected) + + for func, args, kwargs, expected in self.CALLS_KWARGS: + with self.subTest(func=func, args=args, kwargs=kwargs): + result = _testcapi.pyobject_fastcalldict(func, args, kwargs) + self.check_result(result, expected) + + def test_fastcall_keywords(self): + # Test _PyObject_FastCallKeywords() + + for func, args, expected in self.CALLS_POSARGS: + with self.subTest(func=func, args=args): + # kwnames=NULL + result = _testcapi.pyobject_fastcallkeywords(func, args, None) + self.check_result(result, expected) + + # kwnames=() + result = _testcapi.pyobject_fastcallkeywords(func, args, ()) + self.check_result(result, expected) + + if not args: + # kwnames=NULL + result = _testcapi.pyobject_fastcallkeywords(func, None, None) + self.check_result(result, expected) + + # kwnames=() + result = _testcapi.pyobject_fastcallkeywords(func, None, ()) + self.check_result(result, expected) + + for func, args, kwargs, expected in self.CALLS_KWARGS: + with self.subTest(func=func, args=args, kwargs=kwargs): + kwnames = tuple(kwargs.keys()) + args = args + tuple(kwargs.values()) + result = _testcapi.pyobject_fastcallkeywords(func, args, kwnames) + self.check_result(result, expected) + + if __name__ == "__main__": unittest.main() diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 7f7a13ee51d..8c44ad205bf 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -4051,6 +4051,104 @@ raise_SIGINT_then_send_None(PyObject *self, PyObject *args) } +static int +fastcall_args(PyObject *args, PyObject ***stack, Py_ssize_t *nargs) +{ + if (args == Py_None) { + *stack = NULL; + *nargs = 0; + } + else if (PyTuple_Check(args)) { + *stack = &PyTuple_GET_ITEM(args, 0); + *nargs = PyTuple_GET_SIZE(args); + } + else { + PyErr_SetString(PyExc_TypeError, "args must be None or a tuple"); + return -1; + } + return 0; +} + + +static PyObject * +test_pyobject_fastcall(PyObject *self, PyObject *args) +{ + PyObject *func, *func_args; + PyObject **stack; + Py_ssize_t nargs; + + if (!PyArg_ParseTuple(args, "OO", &func, &func_args)) { + return NULL; + } + + if (fastcall_args(func_args, &stack, &nargs) < 0) { + return NULL; + } + return _PyObject_FastCall(func, stack, nargs); +} + + +static PyObject * +test_pyobject_fastcalldict(PyObject *self, PyObject *args) +{ + PyObject *func, *func_args, *kwargs; + PyObject **stack; + Py_ssize_t nargs; + + if (!PyArg_ParseTuple(args, "OOO", &func, &func_args, &kwargs)) { + return NULL; + } + + if (fastcall_args(func_args, &stack, &nargs) < 0) { + return NULL; + } + + if (kwargs == Py_None) { + kwargs = NULL; + } + else if (!PyDict_Check(kwargs)) { + PyErr_SetString(PyExc_TypeError, "kwnames must be None or a dict"); + return NULL; + } + + return _PyObject_FastCallDict(func, stack, nargs, kwargs); +} + + +static PyObject * +test_pyobject_fastcallkeywords(PyObject *self, PyObject *args) +{ + PyObject *func, *func_args, *kwnames = NULL; + PyObject **stack; + Py_ssize_t nargs, nkw; + + if (!PyArg_ParseTuple(args, "OOO", &func, &func_args, &kwnames)) { + return NULL; + } + + if (fastcall_args(func_args, &stack, &nargs) < 0) { + return NULL; + } + + if (kwnames == Py_None) { + kwnames = NULL; + } + else if (PyTuple_Check(kwnames)) { + nkw = PyTuple_GET_SIZE(kwnames); + if (nargs < nkw) { + PyErr_SetString(PyExc_ValueError, "kwnames longer than args"); + return NULL; + } + nargs -= nkw; + } + else { + PyErr_SetString(PyExc_TypeError, "kwnames must be None or a tuple"); + return NULL; + } + return _PyObject_FastCallKeywords(func, stack, nargs, kwnames); +} + + static PyMethodDef TestMethods[] = { {"raise_exception", raise_exception, METH_VARARGS}, {"raise_memoryerror", (PyCFunction)raise_memoryerror, METH_NOARGS}, @@ -4256,6 +4354,9 @@ static PyMethodDef TestMethods[] = { {"tracemalloc_get_traceback", tracemalloc_get_traceback, METH_VARARGS}, {"dict_get_version", dict_get_version, METH_VARARGS}, {"raise_SIGINT_then_send_None", raise_SIGINT_then_send_None, METH_VARARGS}, + {"pyobject_fastcall", test_pyobject_fastcall, METH_VARARGS}, + {"pyobject_fastcalldict", test_pyobject_fastcalldict, METH_VARARGS}, + {"pyobject_fastcallkeywords", test_pyobject_fastcallkeywords, METH_VARARGS}, {NULL, NULL} /* sentinel */ };