Skip to content

Commit 853ce8a

Browse files
committed
support for async; get runtime from context
1 parent c0ad724 commit 853ce8a

File tree

3 files changed

+127
-10
lines changed

3 files changed

+127
-10
lines changed

c-sources/binding.c

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ typedef struct
4242
static PyTypeObject ContextType;
4343
static PyObject *Context_new(PyTypeObject *type, PyObject *args, PyObject *kwds);
4444
static void Context_dealloc(Context *self);
45-
static PyObject *Context_Eval(Context *self, PyObject *args);
45+
static PyObject *Context_Eval(Context *self, PyObject *args, PyObject *kwargs);
46+
static PyObject *Context_EvalSync(Context *self, PyObject *args, PyObject *kwargs);
4647
static PyObject *Context_Set(Context *self, PyObject *args);
47-
48+
static PyObject * Context_GetRuntime(Context *self, PyObject *Py_UNUSED(ignored));
4849
static JSValue py_to_js_value(JSContext *ctx, PyObject *obj);
4950
static JSValue py_callable_handler(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv, int magic, JSValue *func_data);
5051

@@ -255,7 +256,12 @@ static PyObject *js_value_to_py(JSContext *ctx, JSValue val)
255256
case JS_TAG_EXCEPTION:
256257
{
257258
JSValue exc = JS_GetException(ctx);
258-
const char *str = JS_ToCString(ctx, exc);
259+
JSValue val = JS_GetPropertyStr(ctx, exc, "stack");
260+
if (JS_IsUndefined(val))
261+
{
262+
val = JS_DupValue(ctx, exc);
263+
}
264+
const char *str = JS_ToCString(ctx, val);
259265
if (str)
260266
{
261267
PyErr_SetString(PyExc_RuntimeError, str);
@@ -265,6 +271,7 @@ static PyObject *js_value_to_py(JSContext *ctx, JSValue val)
265271
{
266272
PyErr_SetString(PyExc_RuntimeError, "Unknown QuickJS exception");
267273
}
274+
JS_FreeValue(ctx, val);
268275
JS_FreeValue(ctx, exc);
269276
return NULL;
270277
}
@@ -411,21 +418,59 @@ static PyObject *Context_Set(Context *self, PyObject *args)
411418
Py_RETURN_NONE;
412419
}
413420

414-
static PyObject *Context_Eval(Context *self, PyObject *args)
421+
static PyObject *Context_Eval(Context *self, PyObject *args, PyObject *kwargs)
422+
{
423+
static char *kwlist[] = {"code", "filename", NULL};
424+
const char *code;
425+
const char *filename = "input.js";
426+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s|s", kwlist, &code, &filename))
427+
{
428+
return NULL;
429+
}
430+
431+
JSValue val = JS_Eval(self->ctx, code, strlen(code), filename, JS_EVAL_TYPE_GLOBAL);
432+
PyObject *result = js_value_to_py(self->ctx, val);
433+
JS_FreeValue(self->ctx, val);
434+
return result;
435+
}
436+
437+
static PyObject *Context_EvalSync(Context *self, PyObject *args, PyObject *kwargs)
415438
{
439+
static char *kwlist[] = {"code", "filename", NULL};
416440
const char *code;
417441
const char *filename = "input.js";
418-
if (!PyArg_ParseTuple(args, "s|s", &code, &filename))
442+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s|s", kwlist, &code, &filename))
419443
{
420444
return NULL;
421445
}
422446

423447
JSValue val = JS_Eval(self->ctx, code, strlen(code), filename, JS_EVAL_TYPE_GLOBAL);
448+
449+
// Run the job queue until empty
450+
JSContext *ctx1;
451+
int err;
452+
while (JS_IsJobPending(self->runtime->rt))
453+
{
454+
err = JS_ExecutePendingJob(self->runtime->rt, &ctx1);
455+
if (err < 0)
456+
{
457+
JS_FreeValue(self->ctx, val);
458+
// js_value_to_py with JS_EXCEPTION will set the Python error
459+
return js_value_to_py(self->ctx, JS_EXCEPTION);
460+
}
461+
}
462+
424463
PyObject *result = js_value_to_py(self->ctx, val);
425464
JS_FreeValue(self->ctx, val);
426465
return result;
427466
}
428467

468+
static PyObject * Context_GetRuntime(Context *self, PyObject *Py_UNUSED(ignored))
469+
{
470+
Py_INCREF(self->runtime);
471+
return (PyObject *)self->runtime;
472+
}
473+
429474
static PyObject *Context_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
430475
{
431476
PyErr_SetString(PyExc_RuntimeError, "Cannot create Context directly; use Runtime.new_context()");
@@ -441,8 +486,10 @@ static void Context_dealloc(Context *self)
441486
}
442487

443488
static PyMethodDef Context_methods[] = {
444-
{"eval", (PyCFunction)Context_Eval, METH_VARARGS, "Evaluate JavaScript code"},
489+
{"eval", (PyCFunction)Context_Eval, METH_VARARGS | METH_KEYWORDS, "Evaluate JavaScript code"},
490+
{"eval_sync", (PyCFunction)Context_EvalSync, METH_VARARGS | METH_KEYWORDS, "Evaluate JavaScript code and run pending jobs"},
445491
{"set", (PyCFunction)Context_Set, METH_VARARGS, "Set a global value"},
492+
{"get_runtime", (PyCFunction)Context_GetRuntime, METH_NOARGS, "Get the Runtime associated with this Context"},
446493
{NULL} /* Sentinel */
447494
};
448495

src/quickjs_runtime/__init__.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,24 @@ def run_gc(self) -> None:
4343
def new_context(self) -> "IContext":
4444
...
4545

46-
class IContext(ABC):
46+
class Context(ABC):
47+
4748
@abstractmethod
4849
def eval(self, code: str, filename: str = "input.js") -> any:
4950
...
5051

52+
@abstractmethod
53+
def eval_sync(self, code: str, filename: str = "input.js") -> any:
54+
...
55+
5156
@abstractmethod
5257
def set(self, name: str, value: any) -> None:
5358
...
5459

60+
@abstractmethod
61+
def get_runtime(self) -> IRuntime:
62+
...
63+
5564

5665
class Runtime(IRuntime, _Runtime):
5766

@@ -83,8 +92,9 @@ def run_gc(self) -> None:
8392
return _Runtime.run_gc(self)
8493

8594
@override
86-
def new_context(self) -> "IContext":
87-
return _Runtime.new_context(self)
95+
def new_context(self) -> Context:
96+
ctx = _Runtime.new_context(self)
97+
return ctx
8898

8999

90-
__all__ = ["Runtime"]
100+
__all__ = ["Runtime", "Context"]

tests/test_runtime.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,63 @@ def test_nested_collections():
144144

145145
res = ctx.eval("({a: [1, 2], b: {c: 3}})")
146146
assert res == {"a": [1, 2], "b": {"c": 3}}
147+
148+
149+
def test_eval_sync():
150+
rt = Runtime()
151+
ctx = rt.new_context()
152+
153+
# Test resolving a promise
154+
script = """
155+
var out = 0;
156+
async function test() {
157+
out = await Promise.resolve(100);
158+
}
159+
test();
160+
out;
161+
"""
162+
# eval() will return 0 because it doesn't wait for the promise
163+
assert ctx.eval(script) == 0
164+
165+
# eval_sync() will wait for the job queue to be empty
166+
script = """
167+
var out_async = 0;
168+
async function test_async() {
169+
out_async = await Promise.resolve(200);
170+
}
171+
test_async();
172+
out_async;
173+
"""
174+
# Note: the return value of the script is still eval'd before the loop,
175+
# but we want to check if the side effect (out_async = 200) happened.
176+
ctx.eval_sync(script)
177+
assert ctx.eval("out_async") == 200
178+
179+
180+
def test_eval_filename():
181+
rt = Runtime()
182+
ctx = rt.new_context()
183+
184+
# Verify positional filename
185+
try:
186+
ctx.eval("syntax error", "custom_pos.js")
187+
except RuntimeError as e:
188+
assert "custom_pos.js" in str(e)
189+
190+
# Verify keyword filename
191+
try:
192+
ctx.eval(code="syntax error", filename="custom_kw.js")
193+
except RuntimeError as e:
194+
assert "custom_kw.js" in str(e)
195+
196+
# Verify eval_sync keyword filename
197+
try:
198+
ctx.eval_sync(code="syntax error", filename="custom_sync_kw.js")
199+
except RuntimeError as e:
200+
assert "custom_sync_kw.js" in str(e)
201+
202+
def test_runtime_from_context():
203+
rt = Runtime()
204+
ctx = rt.new_context()
205+
rt_from_ctx = ctx.get_runtime()
206+
assert rt_from_ctx is rt

0 commit comments

Comments
 (0)