From cc73f1689e9ba6340693b5fabec9d707ab59c844 Mon Sep 17 00:00:00 2001 From: Matpi Date: Fri, 8 Apr 2022 23:51:30 +0200 Subject: [PATCH 1/2] Implement Object.(get|set), make Object.set support callables, deprecate Context.(get|set|add_callable). --- module.c | 277 ++++++++++++++++++++++++++++-------------------- test_quickjs.py | 19 ++++ 2 files changed, 181 insertions(+), 115 deletions(-) diff --git a/module.c b/module.c index 86de17c..3f9a449 100644 --- a/module.c +++ b/module.c @@ -45,6 +45,15 @@ static PyObject *StackOverflow = NULL; // // Takes ownership of the JSValue and will deallocate it (refcount reduced by 1). static PyObject *quickjs_to_python(ContextData *context_obj, JSValue value); +// Whether converting item to QuickJS would be possible. +static int python_to_quickjs_possible(ContextData *context, PyObject *item); +// Converts item to QuickJS. +// +// If the Python object is not possible to convert to JS, undefined will be returned. This fallback +// will not be used if python_to_quickjs_possible returns 1. +static JSValueConst python_to_quickjs(ContextData *context, PyObject *item); + +static PyTypeObject Object; // Returns nonzero if we should stop due to a time limit. static int js_interrupt_handler(JSRuntime *rt, void *opaque) { @@ -129,6 +138,128 @@ static void object_dealloc(ObjectData *self) { PyObject_GC_Del(self); } +// _quickjs.Object.get +// +// Gets a Javascript property of the object. +static PyObject *object_get(ObjectData *self, PyObject *args) { + const char *name; + if (!PyArg_ParseTuple(args, "s", &name)) { + return NULL; + } + JSValue value = JS_GetPropertyStr(self->context->context, self->object, name); + return quickjs_to_python(self->context, value); +} + +static JSValue js_c_function( + JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) { + ContextData *context = (ContextData *)JS_GetContextOpaque(ctx); + if (context->has_time_limit) { + return JS_ThrowInternalError(ctx, "Can not call into Python with a time limit set."); + } + PythonCallableNode *node = context->python_callables; + while (node && node->magic != magic) { + node = node->next; + } + if (!node) { + return JS_ThrowInternalError(ctx, "Internal error."); + } + prepare_call_python(context); + + PyObject *args = PyTuple_New(argc); + if (!args) { + end_call_python(context); + return JS_ThrowOutOfMemory(ctx); + } + int tuple_success = 1; + for (int i = 0; i < argc; ++i) { + PyObject *arg = quickjs_to_python(context, JS_DupValue(ctx, argv[i])); + if (!arg) { + tuple_success = 0; + break; + } + PyTuple_SET_ITEM(args, i, arg); + } + if (!tuple_success) { + Py_DECREF(args); + end_call_python(context); + return JS_ThrowInternalError(ctx, "Internal error: could not convert args."); + } + + PyObject *result = PyObject_CallObject(node->obj, args); + Py_DECREF(args); + if (!result) { + end_call_python(context); + return JS_ThrowInternalError(ctx, "Python call failed."); + } + JSValue js_result = JS_NULL; + if (python_to_quickjs_possible(context, result)) { + js_result = python_to_quickjs(context, result); + } else { + PyErr_Clear(); + js_result = JS_ThrowInternalError(ctx, "Can not convert Python result to JS."); + } + Py_DECREF(result); + + end_call_python(context); + return js_result; +} + +// _quickjs.Object.set +// +// Sets a Javascript property to the object. Callables are supported. +static PyObject *object_set(ObjectData *self, PyObject *args) { + const char *name; + PyObject *item; + if (!PyArg_ParseTuple(args, "sO", &name, &item)) { + return NULL; + } + int ret = 0; + if (PyCallable_Check(item) && (!PyObject_IsInstance(item, (PyObject *)&Object) || JS_IsFunction( + self->context->context, ((ObjectData *)item)->object))) { + PythonCallableNode *node = PyMem_Malloc(sizeof(PythonCallableNode)); + if (!node) { + return NULL; + } + Py_INCREF(item); + node->magic = 0; + if (self->context->python_callables) { + node->magic = self->context->python_callables->magic + 1; + } + node->obj = item; + node->next = self->context->python_callables; + self->context->python_callables = node; + + JSValue function = JS_NewCFunctionMagic( + self->context->context, + js_c_function, + name, + 0, // TODO: Should we allow setting the .length of the function to something other than 0? + JS_CFUNC_generic_magic, + node->magic); + // If this fails we don't notify the caller of this function. + ret = JS_SetPropertyStr(self->context->context, self->object, name, function); + if (ret != 1) { + PyErr_SetString(PyExc_TypeError, "Failed setting the variable as a callable."); + return NULL; + } else { + Py_RETURN_NONE; + } + } else { + if (python_to_quickjs_possible(self->context, item)) { + ret = JS_SetPropertyStr(self->context->context, self->object, name, + python_to_quickjs(self->context, item)); + if (ret != 1) { + PyErr_SetString(PyExc_TypeError, "Failed setting the variable."); + } + } + if (ret == 1) { + Py_RETURN_NONE; + } else { + return NULL; + } + } +} + // _quickjs.Object.__call__ static PyObject *object_call(ObjectData *self, PyObject *args, PyObject *kwds); @@ -143,6 +274,8 @@ static PyObject *object_json(ObjectData *self) { // All methods of the _quickjs.Object class. static PyMethodDef object_methods[] = { + {"get", (PyCFunction)object_get, METH_VARARGS, "Gets a Javascript property of the object."}, + {"set", (PyCFunction)object_set, METH_VARARGS, "Sets a Javascript property to the object."}, {"json", (PyCFunction)object_json, METH_NOARGS, "Converts to a JSON string."}, {NULL} /* Sentinel */ }; @@ -461,43 +594,41 @@ static PyObject *context_parse_json(ContextData *self, PyObject *args) { return quickjs_to_python(self, value); } +// _quickjs.Context.get_global +// +// Retrieves the global object of the JS context. +static PyObject *context_get_global(ContextData *self) { + return quickjs_to_python(self, JS_GetGlobalObject(self->context)); +} + // _quickjs.Context.get // // Retrieves a global variable from the JS context. static PyObject *context_get(ContextData *self, PyObject *args) { - const char *name; - if (!PyArg_ParseTuple(args, "s", &name)) { + PyErr_WarnEx(PyExc_DeprecationWarning, + "Context.get is deprecated, use Context.get_global().get instead.", 1); + PyObject *global = context_get_global(self); + if (global == NULL) { return NULL; } - JSValue global = JS_GetGlobalObject(self->context); - JSValue value = JS_GetPropertyStr(self->context, global, name); - JS_FreeValue(self->context, global); - return quickjs_to_python(self, value); + PyObject *ret = object_get((ObjectData *)global, args); + Py_DECREF(global); + return ret; } // _quickjs.Context.set // // Sets a global variable to the JS context. static PyObject *context_set(ContextData *self, PyObject *args) { - const char *name; - PyObject *item; - if (!PyArg_ParseTuple(args, "sO", &name, &item)) { - return NULL; - } - JSValue global = JS_GetGlobalObject(self->context); - int ret = 0; - if (python_to_quickjs_possible(self, item)) { - ret = JS_SetPropertyStr(self->context, global, name, python_to_quickjs(self, item)); - if (ret != 1) { - PyErr_SetString(PyExc_TypeError, "Failed setting the variable."); - } - } - JS_FreeValue(self->context, global); - if (ret == 1) { - Py_RETURN_NONE; - } else { + PyErr_WarnEx(PyExc_DeprecationWarning, + "Context.set is deprecated, use Context.get_global().set instead.", 1); + PyObject *global = context_get_global(self); + if (global == NULL) { return NULL; } + PyObject *ret = object_set((ObjectData *)global, args); + Py_DECREF(global); + return ret; } // _quickjs.Context.set_memory_limit @@ -596,101 +727,16 @@ static PyObject *context_gc(ContextData *self) { Py_RETURN_NONE; } -static JSValue js_c_function( - JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic) { - ContextData *context = (ContextData *)JS_GetContextOpaque(ctx); - if (context->has_time_limit) { - return JS_ThrowInternalError(ctx, "Can not call into Python with a time limit set."); - } - PythonCallableNode *node = context->python_callables; - while (node && node->magic != magic) { - node = node->next; - } - if (!node) { - return JS_ThrowInternalError(ctx, "Internal error."); - } - prepare_call_python(context); - - PyObject *args = PyTuple_New(argc); - if (!args) { - end_call_python(context); - return JS_ThrowOutOfMemory(ctx); - } - int tuple_success = 1; - for (int i = 0; i < argc; ++i) { - PyObject *arg = quickjs_to_python(context, JS_DupValue(ctx, argv[i])); - if (!arg) { - tuple_success = 0; - break; - } - PyTuple_SET_ITEM(args, i, arg); - } - if (!tuple_success) { - Py_DECREF(args); - end_call_python(context); - return JS_ThrowInternalError(ctx, "Internal error: could not convert args."); - } - - PyObject *result = PyObject_CallObject(node->obj, args); - Py_DECREF(args); - if (!result) { - end_call_python(context); - return JS_ThrowInternalError(ctx, "Python call failed."); - } - JSValue js_result = JS_NULL; - if (python_to_quickjs_possible(context, result)) { - js_result = python_to_quickjs(context, result); - } else { - PyErr_Clear(); - js_result = JS_ThrowInternalError(ctx, "Can not convert Python result to JS."); - } - Py_DECREF(result); - - end_call_python(context); - return js_result; -} - static PyObject *context_add_callable(ContextData *self, PyObject *args) { - const char *name; - PyObject *callable; - if (!PyArg_ParseTuple(args, "sO", &name, &callable)) { + PyErr_WarnEx(PyExc_DeprecationWarning, + "Context.add_callable is deprecated, use Context.get_global().set instead.", 1); + PyObject *global = context_get_global(self); + if (global == NULL) { return NULL; } - if (!PyCallable_Check(callable)) { - PyErr_SetString(PyExc_TypeError, "Argument must be callable."); - return NULL; - } - - PythonCallableNode *node = PyMem_Malloc(sizeof(PythonCallableNode)); - if (!node) { - return NULL; - } - Py_INCREF(callable); - node->magic = 0; - if (self->python_callables) { - node->magic = self->python_callables->magic + 1; - } - node->obj = callable; - node->next = self->python_callables; - self->python_callables = node; - - JSValue function = JS_NewCFunctionMagic( - self->context, - js_c_function, - name, - 0, // TODO: Should we allow setting the .length of the function to something other than 0? - JS_CFUNC_generic_magic, - node->magic); - JSValue global = JS_GetGlobalObject(self->context); - // If this fails we don't notify the caller of this function. - int ret = JS_SetPropertyStr(self->context, global, name, function); - JS_FreeValue(self->context, global); - if (ret != 1) { - PyErr_SetString(PyExc_TypeError, "Failed adding the callable."); - return NULL; - } else { - Py_RETURN_NONE; - } + PyObject *ret = object_set((ObjectData *)global, args); + Py_DECREF(global); + return ret; } // All methods of the _quickjs.Context class. @@ -702,6 +748,7 @@ static PyMethodDef context_methods[] = { "Evaluates a Javascript string as a module."}, {"execute_pending_job", (PyCFunction)context_execute_pending_job, METH_NOARGS, "Executes a pending job."}, {"parse_json", (PyCFunction)context_parse_json, METH_VARARGS, "Parses a JSON string."}, + {"get_global", (PyCFunction)context_get_global, METH_NOARGS, "Gets the Javascript global object."}, {"get", (PyCFunction)context_get, METH_VARARGS, "Gets a Javascript global variable."}, {"set", (PyCFunction)context_set, METH_VARARGS, "Sets a Javascript global variable."}, {"set_memory_limit", diff --git a/test_quickjs.py b/test_quickjs.py index 75b2a50..04b0b1a 100644 --- a/test_quickjs.py +++ b/test_quickjs.py @@ -346,6 +346,25 @@ def test_wrong_context(self): with self.assertRaisesRegex(ValueError, "Can not mix JS objects from different contexts."): f(d) + def test_get(self): + self.context.eval("a = {x: 42, y: 'foo'};") + a = self.context.get_global().get("a") + self.assertEqual(a.get("x"), 42) + self.assertEqual(a.get("y"), "foo") + self.assertEqual(a.get("z"), None) + + def test_set(self): + self.context.eval("a = {x: 'overridden'}") + a = self.context.get_global().get("a") + a.set("x", 42) + a.set("y", "foo") + self.assertTrue(self.context.eval("a.x == 42")) + self.assertTrue(self.context.eval("a.y == 'foo'")) + + def test_make_function(self): + self.context.get_global().set("f", lambda x: x + 2) + self.assertEqual(self.context.eval("f(40)"), 42) + class FunctionTest(unittest.TestCase): def test_adder(self): From dc3a9f1cf31a7866fc0b166748bccbe6ccddb298 Mon Sep 17 00:00:00 2001 From: Matpi Date: Sat, 9 Apr 2022 12:07:16 +0200 Subject: [PATCH 2/2] Honor warnings-as-error setting in deprecation warnings. --- module.c | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/module.c b/module.c index 3f9a449..7ea6db8 100644 --- a/module.c +++ b/module.c @@ -605,8 +605,11 @@ static PyObject *context_get_global(ContextData *self) { // // Retrieves a global variable from the JS context. static PyObject *context_get(ContextData *self, PyObject *args) { - PyErr_WarnEx(PyExc_DeprecationWarning, - "Context.get is deprecated, use Context.get_global().get instead.", 1); + int err = PyErr_WarnEx(PyExc_DeprecationWarning, + "Context.get is deprecated, use Context.get_global().get instead.", 1); + if (err == -1) { + return NULL; + } PyObject *global = context_get_global(self); if (global == NULL) { return NULL; @@ -620,8 +623,11 @@ static PyObject *context_get(ContextData *self, PyObject *args) { // // Sets a global variable to the JS context. static PyObject *context_set(ContextData *self, PyObject *args) { - PyErr_WarnEx(PyExc_DeprecationWarning, - "Context.set is deprecated, use Context.get_global().set instead.", 1); + int err = PyErr_WarnEx(PyExc_DeprecationWarning, + "Context.set is deprecated, use Context.get_global().set instead.", 1); + if (err == -1) { + return NULL; + } PyObject *global = context_get_global(self); if (global == NULL) { return NULL; @@ -728,8 +734,11 @@ static PyObject *context_gc(ContextData *self) { } static PyObject *context_add_callable(ContextData *self, PyObject *args) { - PyErr_WarnEx(PyExc_DeprecationWarning, - "Context.add_callable is deprecated, use Context.get_global().set instead.", 1); + int err = PyErr_WarnEx(PyExc_DeprecationWarning, + "Context.add_callable is deprecated, use Context.get_global().set instead.", 1); + if (err == -1) { + return NULL; + } PyObject *global = context_get_global(self); if (global == NULL) { return NULL;