Skip to content
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

Support ipyreact.define_module #160

Open
machow opened this issue Sep 20, 2024 · 6 comments
Open

Support ipyreact.define_module #160

machow opened this issue Sep 20, 2024 · 6 comments

Comments

@machow
Copy link

machow commented Sep 20, 2024

  • shinywidgets version: 0.3.3
  • Python version: 3.9
  • Operating System: mac osx

Description

Currently, I'm using ipyreact to create interactive tables in reactable-py. ipyreact is built on Anywidget, and I noticed there's quak support here, but I was unable to get ipyreact react to work 😓 .

What I Did

Note that in the express app below, I could see a div prepared for the widget, but no content:

from shiny.express import ui, render
from ipyreact import Widget
from shinywidgets import render_widget


@render_widget
def out():
    return Widget(_type="button", children=["click me"], events={"onClick": print})

I double checked some of the shinywidgets._dependency pieces, and it looks like it's finding / loading the necessary javascript. However, I'm not what might be different about ipyreact here 😬

edit: updated example to import ipyreact first. importing after shiny widgets was causing an error (see #163)

@cpsievert
Copy link
Collaborator

cpsievert commented Oct 1, 2024

After investigating this with @machow, we discovered ipyreact has it's own way of importing dependencies that more or less you're targetting a Jupyter environment (ipyreact.define_module). As long as that is avoided, ipyreact widgets do seem to render OK with shinywidgets.

@machow
Copy link
Author

machow commented Oct 1, 2024

I did some more digging, and wonder if this issue is related to #163.

It seems like the challenge is this:

  • ipyreact defines Modules to represent dependencies.
  • it doesn't make sense to render these in shiny, but if they're not rendered, they won't do their work (of registering dependencies)

Here's an example app, where explicitly rendering a Module representing reactable's javascript code allows it to be rendered.

from shiny.express import ui
from reactable import Reactable
from reactable.data import cars_93
from shinywidgets import render_widget
import ipyreact

# setup ----

from pathlib import Path
from importlib_resources import files

STATIC_FILES = files("reactable.static")
ui.include_css(Path(str(files("reactable.static") / "reactable-py.esm.css")))

# render the module in order to create the dependency -----
# normally this is just defined inside reactable-py, and picked up automatically
# but explicitly rendering it registers dependency (but has side-effect of outputting UI)

@render_widget
def out2():
    mod = ipyreact.define_module("reactable", Path(str(STATIC_FILES / "reactable-py.esm.js")))
    return mod

# table we want to render ----

@render_widget
def out():
    from shinywidgets._dependencies import jupyter_extension_destination, jupyter_extension_path

    Reactable(cars_93).to_widget()

I wonder if part of the issue with #163 is that ipyreact is instantiating widgets on import, which jupyter (and by extension quarto) end up using (these seem sort of like "shadow widgets" or something?).

@machow
Copy link
Author

machow commented Oct 3, 2024

@maartenbreddels is there any chance you might be able to help give a sense for how ipyreact handles the Module widgets created by ipyreact.define_module?

I'm trying to figure out what triggers their "rendering". I noticed that ipyreact.Widget has all the dependency Module names as the default for ipyreact.Widget._dependencies, but I'm not sure how those get picked up and rendered.

I'm loving ipyreact, and super close to getting reactable-py out the door! It works really nicely in solara, so I'm guessing there's some small piece I've overlooked here 😓.

@maartenbreddels
Copy link

I’m without a keyboard, but I can give some short hints. Solara avoids the global widgets by monkey patching define_module:
https://github.com/widgetti/solara/blob/master/solara/server/esm.py
https://github.com/widgetti/solara/blob/master/solara/server/patch.py (see patch_ipyreact)
https://github.com/widgetti/solara/blob/4cfc3a4adb95271db80249ddcfaaf78491cc093b/solara/server/app.py#L375

Maybe using the same code in shiny will also work. Good luck, I can elaborate more next week when I’m behind a keyboard again.

Regards,

Maarten

@machow
Copy link
Author

machow commented Oct 3, 2024

Ah, thanks -- this is helpful to see! Looking closer, I'm realizing that maybe the key here is what ipywidgets.Widget does on initialization. It looks like...

  • ipywidget.Widget sends a message about its state on initialization (via .open())
  • This is how ipyreact is able to create the js blob for each ipyreact.Module initialized (and add it to the import shim)

The issue comes then, because this is never run by py-shinywidgets, since...

  • shinywidgets comm runs via the ipywidget.Widget.on_widget_constructed hook, BUT
  • shinywidgets no-ops this during import, since the shiny app starts up after app code is run
  • and because there's no kernel, ipywidget.Widget.open() no-ops

It sounds like, once it's ready, shinywidgets should run through all the widgets it no-op'd and run their on_widget_constructed hook (or something?).

I.e.

  • record the widget being skipped over on init in shinywidgets (here in this no-op code)
  • once ready, run init_shiny_widget() on each one.

@machow
Copy link
Author

machow commented Oct 3, 2024

Here's a minimal example using shiny's @reactive.calc to ensure the Module reactable-py needs runs its on construction hook, but without rendering it into the shiny app. Sorry this is so chaotic, but this was super helpful for wrapping my head around the ipywidgets lifecycle 😁 :

from shiny.express import ui
from shiny import reactive
from reactable import Reactable
from reactable.data import cars_93
from shinywidgets import render_widget
import ipyreact

from pathlib import Path
from importlib_resources import files

STATIC_FILES = files("reactable.static")

ui.include_css(Path(STATIC_FILES / "reactable-py.esm.css"))


# Reactive calc: define the reactable Module -----


@reactive.calc
def out2():
    # Module needs to be created here, because shiny won't run the on construction hook, until the
    # shiny app is running, and that is after all the code is initially run...
    mod = ipyreact.define_module("reactable", Path(str(STATIC_FILES / "reactable-py.esm.js")))
    return mod


# Uses the calc before rendering, to ensure the Module is loaded ----


@render_widget
def out():
    out2()
    return Reactable(cars_93).to_widget()

@cpsievert cpsievert changed the title Add support for ipyreact Add awareness ipyreact.define_module Nov 14, 2024
@cpsievert cpsievert changed the title Add awareness ipyreact.define_module Support ipyreact.define_module Nov 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants