Skip to content

Commit 8b06a60

Browse files
authored
feat: set python exception if an error happens in a js callback (#83)
* feat: set python exception if an error happens in a js callback * remove extra log * change pyobject -> null, gives slightly better error message
1 parent ce8bac0 commit 8b06a60

File tree

3 files changed

+61
-7
lines changed

3 files changed

+61
-7
lines changed

src/python.ts

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,38 @@ export class Callback {
154154
args: Deno.PointerValue,
155155
kwargs: Deno.PointerValue,
156156
) => {
157-
return PyObject.from(callback(
158-
kwargs === null ? {} : Object.fromEntries(
159-
new PyObject(kwargs).asDict()
160-
.entries(),
161-
),
162-
...(args === null ? [] : new PyObject(args).valueOf()),
163-
)).handle;
157+
let result: PythonConvertible;
158+
// Prepare arguments for the JS callback
159+
try {
160+
// Prepare arguments for the JS callback
161+
const jsKwargs = kwargs === null
162+
? {}
163+
: Object.fromEntries(new PyObject(kwargs).asDict().entries());
164+
const jsArgs = args === null ? [] : new PyObject(args).valueOf();
165+
166+
// Call the actual JS function
167+
result = callback(jsKwargs, ...jsArgs);
168+
169+
// Convert the JS return value back to a Python object
170+
return PyObject.from(result).handle;
171+
} catch (e) {
172+
// An error occurred in the JS callback.
173+
// We need to set a Python exception and return NULL.
174+
175+
// Prepare the error message for Python
176+
const errorMessage = e instanceof Error
177+
? `${e.name}: ${e.message}` // Include JS error type and message
178+
: String(e); // Fallback for non-Error throws
179+
const cErrorMessage = cstr(`JS Callback Error: ${errorMessage}`);
180+
181+
const errorTypeHandle =
182+
python.builtins.RuntimeError[ProxiedPyObject].handle;
183+
184+
// Set the Python exception (type and message)
185+
py.PyErr_SetString(errorTypeHandle, cErrorMessage);
186+
187+
return null;
188+
}
164189
},
165190
);
166191
}

src/symbols.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ export const SYMBOLS = {
3939
result: "void",
4040
},
4141

42+
PyErr_SetString: {
43+
parameters: ["pointer", "buffer"], // type, message
44+
result: "void",
45+
},
46+
4247
PyDict_New: {
4348
parameters: [],
4449
result: "pointer",

test/test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,3 +360,27 @@ Deno.test("callbacks have signature", async (t) => {
360360
fn.destroy();
361361
});
362362
});
363+
364+
Deno.test("js exception inside python callback returns python exception", () => {
365+
const pyCallback = python.callback(() => {
366+
throw new Error("This is an intentional error from JS!");
367+
});
368+
369+
const pyModule = python.runModule(
370+
`
371+
def call_the_callback(cb):
372+
result = cb()
373+
return result
374+
`,
375+
"test_module",
376+
);
377+
378+
try {
379+
pyModule.call_the_callback(pyCallback);
380+
} catch (e) {
381+
// deno-lint-ignore no-explicit-any
382+
assertEquals((e as any).name, "PythonError");
383+
} finally {
384+
pyCallback.destroy();
385+
}
386+
});

0 commit comments

Comments
 (0)