Skip to content

Commit b757745

Browse files
authored
bpo-30524: Write unit tests for FASTCALL (#2022) (#2030)
Test C functions: * _PyObject_FastCall() * _PyObject_FastCallDict() * _PyObject_FastCallKeywords() (cherry picked from commit 3b5cf85)
1 parent bbeaccc commit b757745

File tree

2 files changed

+277
-0
lines changed

2 files changed

+277
-0
lines changed

Lib/test/test_call.py

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

39
# The test cases here cover several paths through the function calling
410
# code. They depend on the METH_XXX flag that is used to define a C
@@ -122,5 +128,175 @@ def test_oldargs1_2_kw(self):
122128
self.assertRaises(TypeError, [].count, x=2, y=2)
123129

124130

131+
def pyfunc(arg1, arg2):
132+
return [arg1, arg2]
133+
134+
135+
def pyfunc_noarg():
136+
return "noarg"
137+
138+
139+
class PythonClass:
140+
def method(self, arg1, arg2):
141+
return [arg1, arg2]
142+
143+
def method_noarg(self):
144+
return "noarg"
145+
146+
@classmethod
147+
def class_method(cls):
148+
return "classmethod"
149+
150+
@staticmethod
151+
def static_method():
152+
return "staticmethod"
153+
154+
155+
PYTHON_INSTANCE = PythonClass()
156+
157+
158+
IGNORE_RESULT = object()
159+
160+
161+
@cpython_only
162+
class FastCallTests(unittest.TestCase):
163+
# Test calls with positional arguments
164+
CALLS_POSARGS = (
165+
# (func, args: tuple, result)
166+
167+
# Python function with 2 arguments
168+
(pyfunc, (1, 2), [1, 2]),
169+
170+
# Python function without argument
171+
(pyfunc_noarg, (), "noarg"),
172+
173+
# Python class methods
174+
(PythonClass.class_method, (), "classmethod"),
175+
(PythonClass.static_method, (), "staticmethod"),
176+
177+
# Python instance methods
178+
(PYTHON_INSTANCE.method, (1, 2), [1, 2]),
179+
(PYTHON_INSTANCE.method_noarg, (), "noarg"),
180+
(PYTHON_INSTANCE.class_method, (), "classmethod"),
181+
(PYTHON_INSTANCE.static_method, (), "staticmethod"),
182+
183+
# C function: METH_NOARGS
184+
(globals, (), IGNORE_RESULT),
185+
186+
# C function: METH_O
187+
(id, ("hello",), IGNORE_RESULT),
188+
189+
# C function: METH_VARARGS
190+
(dir, (1,), IGNORE_RESULT),
191+
192+
# C function: METH_VARARGS | METH_KEYWORDS
193+
(min, (5, 9), 5),
194+
195+
# C function: METH_FASTCALL
196+
(divmod, (1000, 33), (30, 10)),
197+
198+
# C type static method: METH_FASTCALL | METH_CLASS
199+
(int.from_bytes, (b'\x01\x00', 'little'), 1),
200+
201+
# bpo-30524: Test that calling a C type static method with no argument
202+
# doesn't crash (ignore the result): METH_FASTCALL | METH_CLASS
203+
(datetime.datetime.now, (), IGNORE_RESULT),
204+
)
205+
206+
# Test calls with positional and keyword arguments
207+
CALLS_KWARGS = (
208+
# (func, args: tuple, kwargs: dict, result)
209+
210+
# Python function with 2 arguments
211+
(pyfunc, (1,), {'arg2': 2}, [1, 2]),
212+
(pyfunc, (), {'arg1': 1, 'arg2': 2}, [1, 2]),
213+
214+
# Python instance methods
215+
(PYTHON_INSTANCE.method, (1,), {'arg2': 2}, [1, 2]),
216+
(PYTHON_INSTANCE.method, (), {'arg1': 1, 'arg2': 2}, [1, 2]),
217+
218+
# C function: METH_VARARGS | METH_KEYWORDS
219+
(max, ([],), {'default': 9}, 9),
220+
221+
# C type static method: METH_FASTCALL | METH_CLASS
222+
(int.from_bytes, (b'\x01\x00',), {'byteorder': 'little'}, 1),
223+
(int.from_bytes, (), {'bytes': b'\x01\x00', 'byteorder': 'little'}, 1),
224+
)
225+
226+
def check_result(self, result, expected):
227+
if expected is IGNORE_RESULT:
228+
return
229+
self.assertEqual(result, expected)
230+
231+
def test_fastcall(self):
232+
# Test _PyObject_FastCall()
233+
234+
for func, args, expected in self.CALLS_POSARGS:
235+
with self.subTest(func=func, args=args):
236+
result = _testcapi.pyobject_fastcall(func, args)
237+
self.check_result(result, expected)
238+
239+
if not args:
240+
# args=NULL, nargs=0
241+
result = _testcapi.pyobject_fastcall(func, None)
242+
self.check_result(result, expected)
243+
244+
def test_fastcall_dict(self):
245+
# Test _PyObject_FastCallDict()
246+
247+
for func, args, expected in self.CALLS_POSARGS:
248+
with self.subTest(func=func, args=args):
249+
# kwargs=NULL
250+
result = _testcapi.pyobject_fastcalldict(func, args, None)
251+
self.check_result(result, expected)
252+
253+
# kwargs={}
254+
result = _testcapi.pyobject_fastcalldict(func, args, {})
255+
self.check_result(result, expected)
256+
257+
if not args:
258+
# args=NULL, nargs=0, kwargs=NULL
259+
result = _testcapi.pyobject_fastcalldict(func, None, None)
260+
self.check_result(result, expected)
261+
262+
# args=NULL, nargs=0, kwargs={}
263+
result = _testcapi.pyobject_fastcalldict(func, None, {})
264+
self.check_result(result, expected)
265+
266+
for func, args, kwargs, expected in self.CALLS_KWARGS:
267+
with self.subTest(func=func, args=args, kwargs=kwargs):
268+
result = _testcapi.pyobject_fastcalldict(func, args, kwargs)
269+
self.check_result(result, expected)
270+
271+
def test_fastcall_keywords(self):
272+
# Test _PyObject_FastCallKeywords()
273+
274+
for func, args, expected in self.CALLS_POSARGS:
275+
with self.subTest(func=func, args=args):
276+
# kwnames=NULL
277+
result = _testcapi.pyobject_fastcallkeywords(func, args, None)
278+
self.check_result(result, expected)
279+
280+
# kwnames=()
281+
result = _testcapi.pyobject_fastcallkeywords(func, args, ())
282+
self.check_result(result, expected)
283+
284+
if not args:
285+
# kwnames=NULL
286+
result = _testcapi.pyobject_fastcallkeywords(func, None, None)
287+
self.check_result(result, expected)
288+
289+
# kwnames=()
290+
result = _testcapi.pyobject_fastcallkeywords(func, None, ())
291+
self.check_result(result, expected)
292+
293+
for func, args, kwargs, expected in self.CALLS_KWARGS:
294+
with self.subTest(func=func, args=args, kwargs=kwargs):
295+
kwnames = tuple(kwargs.keys())
296+
args = args + tuple(kwargs.values())
297+
result = _testcapi.pyobject_fastcallkeywords(func, args, kwnames)
298+
self.check_result(result, expected)
299+
300+
125301
if __name__ == "__main__":
126302
unittest.main()

Modules/_testcapimodule.c

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4027,6 +4027,104 @@ dict_get_version(PyObject *self, PyObject *args)
40274027
}
40284028

40294029

4030+
static int
4031+
fastcall_args(PyObject *args, PyObject ***stack, Py_ssize_t *nargs)
4032+
{
4033+
if (args == Py_None) {
4034+
*stack = NULL;
4035+
*nargs = 0;
4036+
}
4037+
else if (PyTuple_Check(args)) {
4038+
*stack = &PyTuple_GET_ITEM(args, 0);
4039+
*nargs = PyTuple_GET_SIZE(args);
4040+
}
4041+
else {
4042+
PyErr_SetString(PyExc_TypeError, "args must be None or a tuple");
4043+
return -1;
4044+
}
4045+
return 0;
4046+
}
4047+
4048+
4049+
static PyObject *
4050+
test_pyobject_fastcall(PyObject *self, PyObject *args)
4051+
{
4052+
PyObject *func, *func_args;
4053+
PyObject **stack;
4054+
Py_ssize_t nargs;
4055+
4056+
if (!PyArg_ParseTuple(args, "OO", &func, &func_args)) {
4057+
return NULL;
4058+
}
4059+
4060+
if (fastcall_args(func_args, &stack, &nargs) < 0) {
4061+
return NULL;
4062+
}
4063+
return _PyObject_FastCall(func, stack, nargs);
4064+
}
4065+
4066+
4067+
static PyObject *
4068+
test_pyobject_fastcalldict(PyObject *self, PyObject *args)
4069+
{
4070+
PyObject *func, *func_args, *kwargs;
4071+
PyObject **stack;
4072+
Py_ssize_t nargs;
4073+
4074+
if (!PyArg_ParseTuple(args, "OOO", &func, &func_args, &kwargs)) {
4075+
return NULL;
4076+
}
4077+
4078+
if (fastcall_args(func_args, &stack, &nargs) < 0) {
4079+
return NULL;
4080+
}
4081+
4082+
if (kwargs == Py_None) {
4083+
kwargs = NULL;
4084+
}
4085+
else if (!PyDict_Check(kwargs)) {
4086+
PyErr_SetString(PyExc_TypeError, "kwnames must be None or a dict");
4087+
return NULL;
4088+
}
4089+
4090+
return _PyObject_FastCallDict(func, stack, nargs, kwargs);
4091+
}
4092+
4093+
4094+
static PyObject *
4095+
test_pyobject_fastcallkeywords(PyObject *self, PyObject *args)
4096+
{
4097+
PyObject *func, *func_args, *kwnames = NULL;
4098+
PyObject **stack;
4099+
Py_ssize_t nargs, nkw;
4100+
4101+
if (!PyArg_ParseTuple(args, "OOO", &func, &func_args, &kwnames)) {
4102+
return NULL;
4103+
}
4104+
4105+
if (fastcall_args(func_args, &stack, &nargs) < 0) {
4106+
return NULL;
4107+
}
4108+
4109+
if (kwnames == Py_None) {
4110+
kwnames = NULL;
4111+
}
4112+
else if (PyTuple_Check(kwnames)) {
4113+
nkw = PyTuple_GET_SIZE(kwnames);
4114+
if (nargs < nkw) {
4115+
PyErr_SetString(PyExc_ValueError, "kwnames longer than args");
4116+
return NULL;
4117+
}
4118+
nargs -= nkw;
4119+
}
4120+
else {
4121+
PyErr_SetString(PyExc_TypeError, "kwnames must be None or a tuple");
4122+
return NULL;
4123+
}
4124+
return _PyObject_FastCallKeywords(func, stack, nargs, kwnames);
4125+
}
4126+
4127+
40304128
static PyMethodDef TestMethods[] = {
40314129
{"raise_exception", raise_exception, METH_VARARGS},
40324130
{"raise_memoryerror", (PyCFunction)raise_memoryerror, METH_NOARGS},
@@ -4230,6 +4328,9 @@ static PyMethodDef TestMethods[] = {
42304328
{"tracemalloc_untrack", tracemalloc_untrack, METH_VARARGS},
42314329
{"tracemalloc_get_traceback", tracemalloc_get_traceback, METH_VARARGS},
42324330
{"dict_get_version", dict_get_version, METH_VARARGS},
4331+
{"pyobject_fastcall", test_pyobject_fastcall, METH_VARARGS},
4332+
{"pyobject_fastcalldict", test_pyobject_fastcalldict, METH_VARARGS},
4333+
{"pyobject_fastcallkeywords", test_pyobject_fastcallkeywords, METH_VARARGS},
42334334
{NULL, NULL} /* sentinel */
42344335
};
42354336

0 commit comments

Comments
 (0)