Skip to content

Commit 3b5cf85

Browse files
authored
bpo-30524: Write unit tests for FASTCALL (#2022)
Test C functions: * _PyObject_FastCall() * _PyObject_FastCallDict() * _PyObject_FastCallKeywords()
1 parent 5eb7075 commit 3b5cf85

File tree

2 files changed

+276
-0
lines changed

2 files changed

+276
-0
lines changed

Lib/test/test_call.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import datetime
12
import unittest
23
from test.support import cpython_only
4+
try:
5+
import _testcapi
6+
except ImportError:
7+
_testcapi = None
38

49
# The test cases here cover several paths through the function calling
510
# code. They depend on the METH_XXX flag that is used to define a C
@@ -176,5 +181,175 @@ def test_oldargs1_2_kw(self):
176181
self.assertRaisesRegex(TypeError, msg, [].count, x=2, y=2)
177182

178183

184+
def pyfunc(arg1, arg2):
185+
return [arg1, arg2]
186+
187+
188+
def pyfunc_noarg():
189+
return "noarg"
190+
191+
192+
class PythonClass:
193+
def method(self, arg1, arg2):
194+
return [arg1, arg2]
195+
196+
def method_noarg(self):
197+
return "noarg"
198+
199+
@classmethod
200+
def class_method(cls):
201+
return "classmethod"
202+
203+
@staticmethod
204+
def static_method():
205+
return "staticmethod"
206+
207+
208+
PYTHON_INSTANCE = PythonClass()
209+
210+
211+
IGNORE_RESULT = object()
212+
213+
214+
@cpython_only
215+
class FastCallTests(unittest.TestCase):
216+
# Test calls with positional arguments
217+
CALLS_POSARGS = (
218+
# (func, args: tuple, result)
219+
220+
# Python function with 2 arguments
221+
(pyfunc, (1, 2), [1, 2]),
222+
223+
# Python function without argument
224+
(pyfunc_noarg, (), "noarg"),
225+
226+
# Python class methods
227+
(PythonClass.class_method, (), "classmethod"),
228+
(PythonClass.static_method, (), "staticmethod"),
229+
230+
# Python instance methods
231+
(PYTHON_INSTANCE.method, (1, 2), [1, 2]),
232+
(PYTHON_INSTANCE.method_noarg, (), "noarg"),
233+
(PYTHON_INSTANCE.class_method, (), "classmethod"),
234+
(PYTHON_INSTANCE.static_method, (), "staticmethod"),
235+
236+
# C function: METH_NOARGS
237+
(globals, (), IGNORE_RESULT),
238+
239+
# C function: METH_O
240+
(id, ("hello",), IGNORE_RESULT),
241+
242+
# C function: METH_VARARGS
243+
(dir, (1,), IGNORE_RESULT),
244+
245+
# C function: METH_VARARGS | METH_KEYWORDS
246+
(min, (5, 9), 5),
247+
248+
# C function: METH_FASTCALL
249+
(divmod, (1000, 33), (30, 10)),
250+
251+
# C type static method: METH_FASTCALL | METH_CLASS
252+
(int.from_bytes, (b'\x01\x00', 'little'), 1),
253+
254+
# bpo-30524: Test that calling a C type static method with no argument
255+
# doesn't crash (ignore the result): METH_FASTCALL | METH_CLASS
256+
(datetime.datetime.now, (), IGNORE_RESULT),
257+
)
258+
259+
# Test calls with positional and keyword arguments
260+
CALLS_KWARGS = (
261+
# (func, args: tuple, kwargs: dict, result)
262+
263+
# Python function with 2 arguments
264+
(pyfunc, (1,), {'arg2': 2}, [1, 2]),
265+
(pyfunc, (), {'arg1': 1, 'arg2': 2}, [1, 2]),
266+
267+
# Python instance methods
268+
(PYTHON_INSTANCE.method, (1,), {'arg2': 2}, [1, 2]),
269+
(PYTHON_INSTANCE.method, (), {'arg1': 1, 'arg2': 2}, [1, 2]),
270+
271+
# C function: METH_VARARGS | METH_KEYWORDS
272+
(max, ([],), {'default': 9}, 9),
273+
274+
# C type static method: METH_FASTCALL | METH_CLASS
275+
(int.from_bytes, (b'\x01\x00',), {'byteorder': 'little'}, 1),
276+
(int.from_bytes, (), {'bytes': b'\x01\x00', 'byteorder': 'little'}, 1),
277+
)
278+
279+
def check_result(self, result, expected):
280+
if expected is IGNORE_RESULT:
281+
return
282+
self.assertEqual(result, expected)
283+
284+
def test_fastcall(self):
285+
# Test _PyObject_FastCall()
286+
287+
for func, args, expected in self.CALLS_POSARGS:
288+
with self.subTest(func=func, args=args):
289+
result = _testcapi.pyobject_fastcall(func, args)
290+
self.check_result(result, expected)
291+
292+
if not args:
293+
# args=NULL, nargs=0
294+
result = _testcapi.pyobject_fastcall(func, None)
295+
self.check_result(result, expected)
296+
297+
def test_fastcall_dict(self):
298+
# Test _PyObject_FastCallDict()
299+
300+
for func, args, expected in self.CALLS_POSARGS:
301+
with self.subTest(func=func, args=args):
302+
# kwargs=NULL
303+
result = _testcapi.pyobject_fastcalldict(func, args, None)
304+
self.check_result(result, expected)
305+
306+
# kwargs={}
307+
result = _testcapi.pyobject_fastcalldict(func, args, {})
308+
self.check_result(result, expected)
309+
310+
if not args:
311+
# args=NULL, nargs=0, kwargs=NULL
312+
result = _testcapi.pyobject_fastcalldict(func, None, None)
313+
self.check_result(result, expected)
314+
315+
# args=NULL, nargs=0, kwargs={}
316+
result = _testcapi.pyobject_fastcalldict(func, None, {})
317+
self.check_result(result, expected)
318+
319+
for func, args, kwargs, expected in self.CALLS_KWARGS:
320+
with self.subTest(func=func, args=args, kwargs=kwargs):
321+
result = _testcapi.pyobject_fastcalldict(func, args, kwargs)
322+
self.check_result(result, expected)
323+
324+
def test_fastcall_keywords(self):
325+
# Test _PyObject_FastCallKeywords()
326+
327+
for func, args, expected in self.CALLS_POSARGS:
328+
with self.subTest(func=func, args=args):
329+
# kwnames=NULL
330+
result = _testcapi.pyobject_fastcallkeywords(func, args, None)
331+
self.check_result(result, expected)
332+
333+
# kwnames=()
334+
result = _testcapi.pyobject_fastcallkeywords(func, args, ())
335+
self.check_result(result, expected)
336+
337+
if not args:
338+
# kwnames=NULL
339+
result = _testcapi.pyobject_fastcallkeywords(func, None, None)
340+
self.check_result(result, expected)
341+
342+
# kwnames=()
343+
result = _testcapi.pyobject_fastcallkeywords(func, None, ())
344+
self.check_result(result, expected)
345+
346+
for func, args, kwargs, expected in self.CALLS_KWARGS:
347+
with self.subTest(func=func, args=args, kwargs=kwargs):
348+
kwnames = tuple(kwargs.keys())
349+
args = args + tuple(kwargs.values())
350+
result = _testcapi.pyobject_fastcallkeywords(func, args, kwnames)
351+
self.check_result(result, expected)
352+
353+
179354
if __name__ == "__main__":
180355
unittest.main()

Modules/_testcapimodule.c

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4051,6 +4051,104 @@ raise_SIGINT_then_send_None(PyObject *self, PyObject *args)
40514051
}
40524052

40534053

4054+
static int
4055+
fastcall_args(PyObject *args, PyObject ***stack, Py_ssize_t *nargs)
4056+
{
4057+
if (args == Py_None) {
4058+
*stack = NULL;
4059+
*nargs = 0;
4060+
}
4061+
else if (PyTuple_Check(args)) {
4062+
*stack = &PyTuple_GET_ITEM(args, 0);
4063+
*nargs = PyTuple_GET_SIZE(args);
4064+
}
4065+
else {
4066+
PyErr_SetString(PyExc_TypeError, "args must be None or a tuple");
4067+
return -1;
4068+
}
4069+
return 0;
4070+
}
4071+
4072+
4073+
static PyObject *
4074+
test_pyobject_fastcall(PyObject *self, PyObject *args)
4075+
{
4076+
PyObject *func, *func_args;
4077+
PyObject **stack;
4078+
Py_ssize_t nargs;
4079+
4080+
if (!PyArg_ParseTuple(args, "OO", &func, &func_args)) {
4081+
return NULL;
4082+
}
4083+
4084+
if (fastcall_args(func_args, &stack, &nargs) < 0) {
4085+
return NULL;
4086+
}
4087+
return _PyObject_FastCall(func, stack, nargs);
4088+
}
4089+
4090+
4091+
static PyObject *
4092+
test_pyobject_fastcalldict(PyObject *self, PyObject *args)
4093+
{
4094+
PyObject *func, *func_args, *kwargs;
4095+
PyObject **stack;
4096+
Py_ssize_t nargs;
4097+
4098+
if (!PyArg_ParseTuple(args, "OOO", &func, &func_args, &kwargs)) {
4099+
return NULL;
4100+
}
4101+
4102+
if (fastcall_args(func_args, &stack, &nargs) < 0) {
4103+
return NULL;
4104+
}
4105+
4106+
if (kwargs == Py_None) {
4107+
kwargs = NULL;
4108+
}
4109+
else if (!PyDict_Check(kwargs)) {
4110+
PyErr_SetString(PyExc_TypeError, "kwnames must be None or a dict");
4111+
return NULL;
4112+
}
4113+
4114+
return _PyObject_FastCallDict(func, stack, nargs, kwargs);
4115+
}
4116+
4117+
4118+
static PyObject *
4119+
test_pyobject_fastcallkeywords(PyObject *self, PyObject *args)
4120+
{
4121+
PyObject *func, *func_args, *kwnames = NULL;
4122+
PyObject **stack;
4123+
Py_ssize_t nargs, nkw;
4124+
4125+
if (!PyArg_ParseTuple(args, "OOO", &func, &func_args, &kwnames)) {
4126+
return NULL;
4127+
}
4128+
4129+
if (fastcall_args(func_args, &stack, &nargs) < 0) {
4130+
return NULL;
4131+
}
4132+
4133+
if (kwnames == Py_None) {
4134+
kwnames = NULL;
4135+
}
4136+
else if (PyTuple_Check(kwnames)) {
4137+
nkw = PyTuple_GET_SIZE(kwnames);
4138+
if (nargs < nkw) {
4139+
PyErr_SetString(PyExc_ValueError, "kwnames longer than args");
4140+
return NULL;
4141+
}
4142+
nargs -= nkw;
4143+
}
4144+
else {
4145+
PyErr_SetString(PyExc_TypeError, "kwnames must be None or a tuple");
4146+
return NULL;
4147+
}
4148+
return _PyObject_FastCallKeywords(func, stack, nargs, kwnames);
4149+
}
4150+
4151+
40544152
static PyMethodDef TestMethods[] = {
40554153
{"raise_exception", raise_exception, METH_VARARGS},
40564154
{"raise_memoryerror", (PyCFunction)raise_memoryerror, METH_NOARGS},
@@ -4256,6 +4354,9 @@ static PyMethodDef TestMethods[] = {
42564354
{"tracemalloc_get_traceback", tracemalloc_get_traceback, METH_VARARGS},
42574355
{"dict_get_version", dict_get_version, METH_VARARGS},
42584356
{"raise_SIGINT_then_send_None", raise_SIGINT_then_send_None, METH_VARARGS},
4357+
{"pyobject_fastcall", test_pyobject_fastcall, METH_VARARGS},
4358+
{"pyobject_fastcalldict", test_pyobject_fastcalldict, METH_VARARGS},
4359+
{"pyobject_fastcallkeywords", test_pyobject_fastcallkeywords, METH_VARARGS},
42594360
{NULL, NULL} /* sentinel */
42604361
};
42614362

0 commit comments

Comments
 (0)