Skip to content

Commit af1c425

Browse files
Merge pull request #361 from Distributive-Network/caleb/fix/variadic
fix python function arg-count mismatch frustration
2 parents 05795f1 + 4cfd707 commit af1c425

File tree

3 files changed

+206
-53
lines changed

3 files changed

+206
-53
lines changed

src/jsTypeFactory.cc

Lines changed: 85 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -294,11 +294,11 @@ JS::Value jsTypeFactorySafe(JSContext *cx, PyObject *object) {
294294
return v;
295295
}
296296

297-
void setPyException(JSContext *cx) {
297+
bool setPyException(JSContext *cx) {
298298
// Python `exit` and `sys.exit` only raise a SystemExit exception to end the program
299299
// We definitely don't want to catch it in JS
300300
if (PyErr_ExceptionMatches(PyExc_SystemExit)) {
301-
return;
301+
return false;
302302
}
303303

304304
PyObject *type, *value, *traceback;
@@ -314,52 +314,111 @@ void setPyException(JSContext *cx) {
314314
JS::RootedValue jsExceptionValue(cx, JS::ObjectValue(*jsException));
315315
JS_SetPendingException(cx, jsExceptionValue);
316316
}
317+
return true;
317318
}
318319

319320
bool callPyFunc(JSContext *cx, unsigned int argc, JS::Value *vp) {
320321
JS::CallArgs callargs = JS::CallArgsFromVp(argc, vp);
321322

322323
// get the python function from the 0th reserved slot
323-
JS::Value pyFuncVal = js::GetFunctionNativeReserved(&(callargs.callee()), 0);
324-
PyObject *pyFunc = (PyObject *)(pyFuncVal.toPrivate());
325-
326-
unsigned int callArgsLength = callargs.length();
324+
PyObject *pyFunc = (PyObject *)js::GetFunctionNativeReserved(&(callargs.callee()), 0).toPrivate();
325+
Py_INCREF(pyFunc);
326+
PyObject *pyRval = NULL;
327+
PyObject *pyArgs = NULL;
328+
Py_ssize_t nNormalArgs = 0; // number of positional non-default arguments
329+
Py_ssize_t nDefaultArgs = 0; // number of positional default arguments
330+
bool varargs = false;
331+
bool unknownNargs = false;
332+
333+
if (PyCFunction_Check(pyFunc)) {
334+
const int funcFlags = ((PyCFunctionObject *)pyFunc)->m_ml->ml_flags;
335+
if (funcFlags & METH_NOARGS) { // 0 arguments
336+
nNormalArgs = 0;
337+
}
338+
else if (funcFlags & METH_O) { // 1 argument
339+
nNormalArgs = 1;
340+
}
341+
else { // unknown number of arguments
342+
nNormalArgs = 0;
343+
unknownNargs = true;
344+
varargs = true;
345+
}
346+
}
347+
else {
348+
nNormalArgs = 1;
349+
PyObject *f = pyFunc;
350+
if (PyMethod_Check(pyFunc)) {
351+
f = PyMethod_Function(pyFunc); // borrowed reference
352+
nNormalArgs -= 1; // don't include the implicit `self` of the method as an argument
353+
}
354+
PyCodeObject *bytecode = (PyCodeObject *)PyFunction_GetCode(f); // borrowed reference
355+
PyObject *defaults = PyFunction_GetDefaults(f); // borrowed reference
356+
nDefaultArgs = defaults ? PyTuple_Size(defaults) : 0;
357+
nNormalArgs += bytecode->co_argcount - nDefaultArgs - 1;
358+
if (bytecode->co_flags & CO_VARARGS) {
359+
varargs = true;
360+
}
361+
}
327362

328-
if (!callArgsLength) {
363+
// use faster calling if no arguments are needed
364+
if (((nNormalArgs + nDefaultArgs) == 0 && !varargs)) {
329365
#if PY_VERSION_HEX >= 0x03090000
330-
PyObject *pyRval = PyObject_CallNoArgs(pyFunc);
366+
pyRval = PyObject_CallNoArgs(pyFunc);
331367
#else
332-
PyObject *pyRval = _PyObject_CallNoArg(pyFunc); // in Python 3.8, the API is only available under the name with a leading underscore
368+
pyRval = _PyObject_CallNoArg(pyFunc); // in Python 3.8, the API is only available under the name with a leading underscore
333369
#endif
334-
if (PyErr_Occurred()) { // Check if an exception has already been set in Python error stack
335-
setPyException(cx);
336-
return false;
370+
if (PyErr_Occurred() && setPyException(cx)) { // Check if an exception has already been set in Python error stack
371+
goto failure;
337372
}
338-
callargs.rval().set(jsTypeFactory(cx, pyRval));
339-
return true;
373+
goto success;
340374
}
341375

342376
// populate python args tuple
343-
PyObject *pyArgs = PyTuple_New(callArgsLength);
344-
for (size_t i = 0; i < callArgsLength; i++) {
377+
Py_ssize_t argTupleLength;
378+
if (unknownNargs) { // pass all passed arguments
379+
argTupleLength = callargs.length();
380+
}
381+
else if (varargs) { // if passed arguments is less than number of non-default positionals, rest will be set to `None`
382+
argTupleLength = std::max((Py_ssize_t)callargs.length(), nNormalArgs);
383+
}
384+
else if (nNormalArgs > callargs.length()) { // if passed arguments is less than number of non-default positionals, rest will be set to `None`
385+
argTupleLength = nNormalArgs;
386+
}
387+
else { // passed arguments greater than non-default positionals, so we may be replacing default positional arguments
388+
argTupleLength = std::min((Py_ssize_t)callargs.length(), nNormalArgs+nDefaultArgs);
389+
}
390+
pyArgs = PyTuple_New(argTupleLength);
391+
392+
for (size_t i = 0; i < callargs.length() && i < argTupleLength; i++) {
345393
JS::RootedValue jsArg(cx, callargs[i]);
346394
PyObject *pyArgObj = pyTypeFactory(cx, jsArg);
347395
if (!pyArgObj) return false; // error occurred
348396
PyTuple_SetItem(pyArgs, i, pyArgObj);
349397
}
350398

351-
PyObject *pyRval = PyObject_Call(pyFunc, pyArgs, NULL);
352-
if (PyErr_Occurred()) {
353-
setPyException(cx);
354-
return false;
399+
// set unspecified args to None, to match JS behaviour of setting unspecified args to undefined
400+
for (Py_ssize_t i = callargs.length(); i < argTupleLength; i++) {
401+
PyTuple_SetItem(pyArgs, i, Py_None);
355402
}
356-
callargs.rval().set(jsTypeFactory(cx, pyRval));
357-
if (PyErr_Occurred()) {
358-
Py_DECREF(pyRval);
359-
setPyException(cx);
360-
return false;
403+
404+
pyRval = PyObject_Call(pyFunc, pyArgs, NULL);
405+
if (PyErr_Occurred() && setPyException(cx)) {
406+
goto failure;
361407
}
408+
goto success;
362409

363-
Py_DECREF(pyRval);
410+
success:
411+
if (pyRval) { // can be NULL if SystemExit was raised
412+
callargs.rval().set(jsTypeFactory(cx, pyRval));
413+
Py_DECREF(pyRval);
414+
}
415+
Py_DECREF(pyFunc);
416+
Py_XDECREF(pyArgs);
364417
return true;
418+
419+
failure:
420+
Py_XDECREF(pyRval);
421+
Py_DECREF(pyFunc);
422+
Py_XDECREF(pyArgs);
423+
return false;
365424
}

tests/python/test_arrays.py

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -894,33 +894,6 @@ def myFunc(e, f):
894894
assert "@evaluate:1:27" in str(e)
895895

896896

897-
def test_sort_with_one_arg_keyfunc():
898-
items = ['Four', 'Three', 'One']
899-
900-
def myFunc(e):
901-
return len(e)
902-
try:
903-
pm.eval("(arr, compareFun) => {arr.sort(compareFun)}")(items, myFunc)
904-
assert (False)
905-
except Exception as e:
906-
assert str(type(e)) == "<class 'pythonmonkey.SpiderMonkeyError'>"
907-
assert "takes 1 positional argument but 2 were given" in str(e)
908-
assert "JS Stack Trace" in str(e)
909-
assert "@evaluate:1:27" in str(e)
910-
911-
912-
def test_sort_with_builtin_keyfunc():
913-
items = ['Four', 'Three', 'One']
914-
try:
915-
pm.eval("(arr, compareFun) => {arr.sort(compareFun)}")(items, len)
916-
assert (False)
917-
except Exception as e:
918-
assert str(type(e)) == "<class 'pythonmonkey.SpiderMonkeyError'>"
919-
assert "len() takes exactly one argument (2 given)" in str(e)
920-
assert "JS Stack Trace" in str(e)
921-
assert "@evaluate:1:27" in str(e)
922-
923-
924897
def test_sort_with_js_func():
925898
items = ['Four', 'Three', 'One']
926899
result = [None]

tests/python/test_functions.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import pythonmonkey as pm
2+
3+
4+
def test_func_no_args():
5+
def f():
6+
return 42
7+
assert 42 == pm.eval("(f) => f()")(f)
8+
9+
10+
def test_func_too_many_args():
11+
def f(a, b):
12+
return [a, b]
13+
assert [1, 2] == pm.eval("(f) => f(1, 2, 3)")(f)
14+
15+
16+
def test_func_equal_args():
17+
def f(a, b):
18+
return [a, b]
19+
assert [1, 2] == pm.eval("(f) => f(1, 2)")(f)
20+
21+
22+
def test_func_too_few_args():
23+
def f(a, b):
24+
return [a, b]
25+
assert [1, None] == pm.eval("(f) => f(1)")(f)
26+
27+
28+
def test_default_func_no_args():
29+
def f(a, b, c=42, d=43):
30+
return [a, b, c, d]
31+
assert [None, None, 42, 43] == pm.eval("(f) => f()")(f)
32+
33+
34+
def test_default_func_too_many_default_args():
35+
def f(a, b, c=42, d=43):
36+
return [a, b, c, d]
37+
assert [1, 2, 3, 4] == pm.eval("(f) => f(1, 2, 3, 4, 5)")(f)
38+
39+
40+
def test_default_func_equal_default_args():
41+
def f(a, b, c=42, d=43):
42+
return [a, b, c, d]
43+
assert [1, 2, 3, 4] == pm.eval("(f) => f(1, 2, 3, 4)")(f)
44+
45+
46+
def test_default_func_too_few_default_args():
47+
def f(a, b, c=42, d=43):
48+
return [a, b, c, d]
49+
assert [1, 2, 3, 43] == pm.eval("(f) => f(1, 2, 3)")(f)
50+
51+
52+
def test_default_func_equal_args():
53+
def f(a, b, c=42, d=43):
54+
return [a, b, c, d]
55+
assert [1, 2, 42, 43] == pm.eval("(f) => f(1, 2)")(f)
56+
57+
58+
def test_default_func_too_few_args():
59+
def f(a, b, c=42, d=43):
60+
return [a, b, c, d]
61+
assert [1, None, 42, 43] == pm.eval("(f) => f(1)")(f)
62+
63+
64+
def test_vararg_func_no_args():
65+
def f(a, b, *args):
66+
return [a, b, *args]
67+
assert [None, None] == pm.eval("(f) => f()")(f)
68+
69+
70+
def test_vararg_func_too_many_args():
71+
def f(a, b, *args):
72+
return [a, b, *args]
73+
assert [1, 2, 3] == pm.eval("(f) => f(1, 2, 3)")(f)
74+
75+
76+
def test_vararg_func_equal_args():
77+
def f(a, b, *args):
78+
return [a, b, *args]
79+
assert [1, 2] == pm.eval("(f) => f(1, 2)")(f)
80+
81+
82+
def test_vararg_func_too_few_args():
83+
def f(a, b, *args):
84+
return [a, b, *args]
85+
assert [1, None] == pm.eval("(f) => f(1)")(f)
86+
87+
88+
def test_default_vararg_func_no_args():
89+
def f(a, b, c=42, d=43, *args):
90+
return [a, b, c, d, *args]
91+
assert [None, None, 42, 43] == pm.eval("(f) => f()")(f)
92+
93+
94+
def test_default_vararg_func_too_many_default_args():
95+
def f(a, b, c=42, d=43, *args):
96+
return [a, b, c, d, *args]
97+
assert [1, 2, 3, 4, 5] == pm.eval("(f) => f(1, 2, 3, 4, 5)")(f)
98+
99+
100+
def test_default_vararg_func_equal_default_args():
101+
def f(a, b, c=42, d=43, *args):
102+
return [a, b, c, d, *args]
103+
assert [1, 2, 3, 4] == pm.eval("(f) => f(1, 2, 3, 4)")(f)
104+
105+
106+
def test_default_vararg_func_too_few_default_args():
107+
def f(a, b, c=42, d=43, *args):
108+
return [a, b, c, d, *args]
109+
assert [1, 2, 3, 43] == pm.eval("(f) => f(1, 2, 3)")(f)
110+
111+
112+
def test_default_vararg_func_equal_args():
113+
def f(a, b, c=42, d=43, *args):
114+
return [a, b, c, d, *args]
115+
assert [1, 2, 42, 43] == pm.eval("(f) => f(1, 2)")(f)
116+
117+
118+
def test_default_vararg_func_too_few_args():
119+
def f(a, b, c=42, d=43, *args):
120+
return [a, b, c, d, *args]
121+
assert [1, None, 42, 43] == pm.eval("(f) => f(1)")(f)

0 commit comments

Comments
 (0)