-
Notifications
You must be signed in to change notification settings - Fork 37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(experimental): Add command
system for calling Python from JS
#453
Conversation
✅ Deploy Preview for anywidget ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
This is awesome! Very nice how the |
Thanks! Yeah, I think having something opinionated but opt-in would probably be preferable. I renamed to |
I'm still curious whether the Python callback can be made async or whether that messes with the Jupyter event loop |
I've been reviewing this, and I'm realizing this doesn't really follow the redux pattern. In Redux, This PR is narrowing in on something different but very important: RPC. So I wonder about taking this one step further, and baking in a mechanism like Tauri's class Widget(anywidget.AnyWidget):
_esm = """
async function render({ model, el, experimental }) {
let [msg, buffers] = await experimental.invoke("_echo", "hello, world");
}
"""
@anywidget.command
def _echo(msg, buffers):
return msg, buffers I think this is the direction I'd like to take this PR, what do you think @kylebarron @keller-mark ? The previous API would still be possible, just register one "command" that handles many different types of messages. This would make the most simple case (a single callback) very easy. It would be nice if we could have something other than msg/buffers, I suppose we could require "msg" to be an dict and **msg when invoking the callback... but then not sure what to do about buffers. A more opinionated version would be to use something like class Widget(anywidget.AnyWidget):
_esm = """
async function render({ model, el, experimental }) {
let response = await experimental.invoke("_echo", {
text: "hello, world",
btext: new TextEncoder().encode("hello, world"),
});
}
"""
@anywidget.command
def _echo(text: str, btext: bytes):
return { "text": text, "btext": btext } Probably overkill, and something we could optionally implement ontop of the core mechanism in the future. |
I think framing it more like RPC conceptually makes sense. I do not have a super strong preference towards Is the
I don't really follow this point but i am not familiar with -let [msg] = await experimental.invoke("_echo", "hello, world");
+let msg = await experimental.invoke("_echo", "hello, world"); when the user does not care about buffers? |
Right, makes sense. I guess I should just stop bike-shedding and merge the thing :)
This decorator "marks" the methods on the class to "expose" to the front end. I guess we could try to inspect the signature of all the methods on the class and try to guess which ones implement the invoke signature: (msg: Any, buffers: list[bytes]) -> tuple[Any, list[bytes]] but this is more explicit and we can raise type errors if the wrapped method has the wrong signature. It also creates a future space where we could extend the behavior (see below).
If you wanted to return a def getitems(keys: list[str]) -> dict[str, bytes] ... You would need to extract the buffers manually, both on the JS and Python side: @anywidget.command
def _getitems(self, msg: Any, buffers: list[bytes]) -> tuple[list[str], list[bytes]]:
keys = msg["keys"]
items = self.store.getitems(keys)
return list(items.keys()), list(items.values()) If this was a traitlet, Jupyter Widgets would automatically take care of "extracting" the buffers and assembling the deconstructed object on the JS side. But for custom messages, you need to do it yourself. I guess I was wondering if this is a place we could further make developing widgets easier, so developers didn't need to worry about packing/unpacking buffers. One option would be to reuse the packing mechanism from Jupyter Widgets, or a more well defined binary protocol like msgpack. My current thinking is that we could extend @anywidget.command(serialize=None)
def _getitems(self, msg: Any, buffers: list[bytes]) -> tuple[list[str], list[bytes]]:
keys = msg["keys"]
items = self.store.getitems(keys)
return list(items.keys()), list(items.values()) @anywidget.command(serialize="msgpack") # serialize="jupyter-widgets"
def _getitems(self, keys: list[str]) -> dict[str, bytes]:
keys = msg["keys"]
return self.store.getitems(keys) Since serialization has some overhead, I think it makes sense to have the |
🦋 Changeset detectedLatest commit: 568e243 The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
I agree that an RPC-based API makes sense for this, and also +1 on exploring msgpack (at least as an option). I haven't used it myself but I'm guessing the client libraries for that are pretty stable and minimal |
Awesome. I'm going to push forward and hopefully cut a release this week! |
Yes, I'm familiar with msgpack for neovim's extension mechanism. On the Python side there is |
Thanks for the clarification, the ability to specify in the decorator whether the msgpack serialization should be used looks like a great way forward! |
b8bf0af
to
263e583
Compare
command
system for calling Python methods from JS
command
system for calling Python methods from JScommand
system for calling Python from JS
Co-authored-by: Talley Lambert <talley.lambert@gmail.com>
Cool, let's merge the thing! I will work on some docs in a separate PR, but they will be a draft likely until we stabilize the feature. cc: @mscolnick, just FYI a new API we are iterating on. It would be great to learn if this is something that you think Marimo could support. Relevant code: anywidget/packages/anywidget/src/widget.js Lines 241 to 283 in 568e243
|
@manzt very cool addition! we can support this (we have a concept of RPC/Functions too that I can adapt to work with this as well. Is there a timeline for when it will be out of experimental? Would like to avoid 2 changes if possible, but not against it. |
This PR introduces an experimental API designed to make sending custom messages easier in the front end. This is achieved through a new RPC mechanism, which allows the frontend to
invoke
acommand
registered in Python and await the response.A minimal example:
this can make implementing things with dynamic data much easier, like the
ZarrWidget
:ZarrWidget
implementationcc: @keller-mark
TODO:
invoke
(via another argument)Another option would be to move this kind of dispatching mechanism to a separate library (e.g.,
@anywidget/std
), but, since it is coupled to the Python, I prefer we implement something within anywidget that makes this pattern frictionless for applications that need it.EDIT: This PR has changed from a messaging callback to registering methods like RPC.