API for simple Js <-> Python communication ? #354
Replies: 5 comments
-
Thank you for your interest in anywidget and for the feature suggestion. anywidget is fundamentally a wrapper around Jupyter Widgets, which provides low-level building blocks: 1.) a data model for front-end and kernel synchronization, 2.) a DOM element for rendering. While it is possible to implement RPC mechanisms using these elements, there are multiple ways to approach such an implementation. Given the scope of anywidget, I don't intend to incorporate RPC functionality directly into the main API. This is primarily because Jupyter Widgets themselves don't offer this feature, and anywidget aims to provide an easier interface to what Jupyter Widgets already offer. With that said, it's fairly straightforward to implement a minimal RPC using the building blocks exposed by Jupyter Widgets, and as such I believe that libraries are a much more appropriate layer for such functionality. I've thought about adding a "utility" library for anywidget (similar to // @anywidget/utils/index.js
export function send(model, payload, { timeout = 3000 } = {}) {
let uuid = globalThis.crypto.randomUUID();
return new Promise((resolve, reject) => {
let timer = setTimeout(() => {
reject(new Error(`Promise timed out after ${timeout} ms`));
model.off("msg:custom", handler);
}, timeout);
function handler(msg) {
if (!(msg.uuid === uuid)) return;
clearTimeout(timer)
resolve(msg.payload);
view.model.off("msg:custom", handler);
}
model.on("msg:custom", handler)
model.send({ payload, uuid });
})
} which could be used as the basis of an RPC. For example, implementing your outlined model: import anywidget
class RPCWidget(anywidget.AnyWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.on_msg(self._handle_custom_msg)
def _handle_custom_msg(self, msg: dict, buffers: list):
payload = msg["payload"]
self.send({
"uuid": msg["uuid"],
"payload": getattr(self, payload["fn"])(*payload["args"], **payload["kwargs"])
}) class Widget(RPCWidget):
_esm = """
import { send } from "https://esm.sh/@anywidget/utils"; // Does not exist!
function call(model, fn, args, kwargs) {
return send(model, { fn, args, kwargs });
}
export async function render({ model, el }) {
el.innerText = await call(model, "my_function", [], {});
}
"""
def my_function(self, *args, **kwargs):
return "hello" |
Beta Was this translation helpful? Give feedback.
-
It's important to note that the code above supports invoking Python functions from JS and awaiting the response, but to my knowledge the opposite is not possible: |
Beta Was this translation helpful? Give feedback.
-
Thank you for the snippet and fast answer. This is helpful! Instead of |
Beta Was this translation helpful? Give feedback.
-
Certainly!
String concatenation for JS modules can quickly go awry, which is why we are sticking to ESM. Because we expect ESM, we can easily integrate with other build tools (like Vite), other Jupyter kernels (like Deno), and provide our own implementation of Hot Module Replacement. The JS module would be the most ideomatic way of exposing this behavior at the moment, but ofc you could add a helper to something like an RPCWidget to inline the static code for you: import anywidget
class RPCWidget(anywidget.AnyWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.on_msg(self._handle_custom_msg)
def _handle_custom_msg(self, msg: dict, buffers: list):
print(msg)
payload = msg["payload"]
self.send({
"uuid": msg["uuid"],
"payload": getattr(self, payload["fn"])(*payload["args"], **payload["kwargs"])
})
@staticmethod
def esm(esm):
return """
function _send_rpc(model, payload, { timeout = 3000 } = {}) {
let uuid = globalThis.crypto.randomUUID();
return new Promise((resolve, reject) => {
let timer = setTimeout(() => {
reject(new Error(`Promise timed out after ${timeout} ms`));
model.off("msg:custom", handler);
}, timeout);
function handler(msg) {
console.log({ msg })
if (!(msg.uuid === uuid)) return;
clearTimeout(timer)
resolve(msg.payload);
view.model.off("msg:custom", handler);
}
model.on("msg:custom", handler)
model.send({ payload, uuid });
})
}
function call(model, fn, { args, kwargs }) {
return _send_rpc(model, { fn, args, kwargs });
}
""" + esm class Widget(RPCWidget):
_esm = RPCWidget.esm("""
export async function render({ model, el }) {
el.innerText = await call(model, "my_function", { args: [], kwargs: {} });
}
""")
def my_function(self, *args, **kwargs):
return "hello"
Widget() |
Beta Was this translation helpful? Give feedback.
-
Some iteration on this happening in #453 |
Beta Was this translation helpful? Give feedback.
-
I'm really excited that a project like anywidget exists. I myself worked on making bidirectional JS <> Python communications works across Colab / Jupyter but was missing VS Code support.
It looks your project support all backends (VSCode, Colab,...), so I was trying to migrate my current code to anywidget. However, I didn't found a straightforward way to call Python from Javascript and get the result.
Currently, a hack is to use
model.set("some_attribute", "value")
on an attribute that is decorated with@traitlets.validate('some_attribute')
. Thevalidate
Python code is executed whensome_attribute
is changed from JS. However this is boilerplate, limited and hard to use.It would be nice if there was a native
model.call
function, for example with signature:This would make it much easier to call Python code from the widget, like:
In js:
In Python:
Input/output could be json.
This was my implementation (that only support Colab/Jupyter): https://github.com/google/etils/blob/main/etils/ecolab/pyjs_com/py_js_com.js
It would be awesome if anywidget was having something similar.
One concrete use-case for example is an interactive variable inspector: https://etils.readthedocs.io/en/latest/ecolab.html#inspect-any-python-objects
Beta Was this translation helpful? Give feedback.
All reactions