From bc677bbda01a2cbdd030540e39de523a23a025c7 Mon Sep 17 00:00:00 2001 From: Caleb Aikens Date: Tue, 21 Apr 2026 17:42:32 -0400 Subject: [PATCH] feat(JSFunctionProxy): implement passing kwargs as a final argument --- python/pythonmonkey/helpers.py | 7 +++---- src/JSFunctionProxy.cc | 12 ++++++++++++ tests/js/js2py/kwargs.simple | 28 ++++++++++++++++++++++++++++ tests/python/test_functions.py | 14 ++++++++++++++ 4 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 tests/js/js2py/kwargs.simple diff --git a/python/pythonmonkey/helpers.py b/python/pythonmonkey/helpers.py index 76970edc..9e98998b 100644 --- a/python/pythonmonkey/helpers.py +++ b/python/pythonmonkey/helpers.py @@ -34,13 +34,12 @@ def new(ctor): newCtor = pm.eval("""'use strict'; ( function pmNewFactory(ctor) { - return function newCtor(args) { - args = Array.from(args || []); - return new ctor(...args); + return function newCtor() { + return new ctor(...[...arguments]); }; } )""", evalOpts)(ctor) - return (lambda *args: newCtor(list(args))) + return (lambda *args, **kwargs: newCtor(*args, **kwargs)) def simpleUncaughtExceptionHandler(loop, context): diff --git a/src/JSFunctionProxy.cc b/src/JSFunctionProxy.cc index 99a32552..76d7ed94 100644 --- a/src/JSFunctionProxy.cc +++ b/src/JSFunctionProxy.cc @@ -52,6 +52,18 @@ PyObject *JSFunctionProxyMethodDefinitions::JSFunctionProxy_call(PyObject *self, } } + if (kwargs && PyDict_Size(kwargs) > 0) { + JS::Value jsValue = jsTypeFactory(cx, kwargs); + if (PyErr_Occurred()) { // Check if an exception has already been set in the flow of control + return NULL; // Fail-fast + } + if (!jsArgsVector.append(jsValue)) { + // out of memory + setSpiderMonkeyException(cx); + return NULL; + } + } + JS::HandleValueArray jsArgs(jsArgsVector); JS::RootedValue jsReturnVal(cx); if (!JS_CallFunctionValue(cx, thisObj, jsFunc, jsArgs, &jsReturnVal)) { diff --git a/tests/js/js2py/kwargs.simple b/tests/js/js2py/kwargs.simple new file mode 100644 index 00000000..e3769f2f --- /dev/null +++ b/tests/js/js2py/kwargs.simple @@ -0,0 +1,28 @@ +/** + * @file js2py/kwargs.simple + * Simple test which shows that kwargs passed from python to JS end up as an additional final dict argument + * + * @author Caleb Aikens, caikens@amd.com + * @date April 2026 + */ +'use strict'; + +function js_func() { + return [...arguments]; +} + +python.exec(` +def kwarg_test(js_func): + return js_func(1, 2, i=42, j=43) +`); +const kwarg_test = python.eval('kwarg_test'); + +const [first, second, kwargs] = kwarg_test(js_func); + +if (first !== 1 || + second !== 2 || + kwargs.i !== 42 || + kwargs.j !== 43) { + + throw new Error('args or kwargs were not passed correctly.'); +} \ No newline at end of file diff --git a/tests/python/test_functions.py b/tests/python/test_functions.py index 5b61acb8..6452a000 100644 --- a/tests/python/test_functions.py +++ b/tests/python/test_functions.py @@ -119,3 +119,17 @@ def test_default_vararg_func_too_few_args(): def f(a, b, c=42, d=43, *args): return [a, b, c, d, *args] assert [1, None, 42, 43] == pm.eval("(f) => f(1)")(f) + +def test_kwargs_in_js(): + assert [1, 2, {"i": 42, "j": 43}] == pm.eval("(first, second, kwargs) => {return [first, second, kwargs]}")(1, 2, i=42, j=43) + +def test_kwargs_in_js_arguments_array(): + # Note: arguments object is only available in non-arrow functions + assert [1, 2, {"i": 42, "j": 43}] == pm.eval("(function() {return [...arguments]})")(1, 2, i=42, j=43) + +def test_kwargs_pass_through(): + def py_func(*args, **kwargs): + return [*args, kwargs]; + + # Note: kwargs passed py1->js->py2 end up in args for py2 rather than kwargs, since JS has no equivalent to kwargs + assert [1, 2, {"i": 42, "j": 43}, {}] == pm.eval("(py_func, ...args) => { return py_func(...args)}")(py_func, 1, 2, i=42, j=43) \ No newline at end of file