solara icon indicating copy to clipboard operation
solara copied to clipboard

Help trying to port my widget to work in Solara

Open paddymul opened this issue 1 year ago • 2 comments

I am the creator of Buckaroo a full featured dataframe viewer that wraps ag-grid. I am trying to figure out how to expose buckaroo as a component for Solara apps.

A couple of points:

  • Buckaroo extends ipywidgets.DOMWidget
  • Buckaroo generally functions as its own miniapp configured with optionally user provided mutation functions, users don't wire in any events themselves.
  • Inside the frontend of Buckaroo there is the core DFViewer component that takes a serialized dataframe and Buckaroo styling config. There is also extra UI that swaps clientside styling configs, and sets properties on the parent widget that communicates back with the python widget (which transform function to apply, some other things)

It might be easier and more straightforward to integrate just the DFViewer frontend component. I don't have this as a separate DOMWidget, but I could work on it. This would probably be easier for solara users since it is less opinionated than the full BuckarooWidget.

Where I'm getting stuck

I am having trouble understanding the wrapping stage. I'm a bit lost as to how to make Buckaroo work there. I looked at the other examples (IPYWidgets, BQPlot, IPYVeutify).

The codegen in particular is confusing. what is the generated code accomplishing?

Are you inserting actual python code into an existing file in site-packages? are you only using it to run mypy on the generated code? are you using it to get completions in an IDE?


Ahhh after looking at the code in my sitepackages, I see that you are indeed writing to the file.

Documenting this would help.

paddymul avatar Feb 17 '24 16:02 paddymul

The code of


class ButtonElement(reacton.core.Element):
    def _add_widget_event_listener(self, widget: widgets.Widget, name: str, callback: Callable):
        if name == "on_click":
            callback_exception_safe = _event_handler_exception_wrapper(callback)

            def on_click(change):
                callback_exception_safe()

            key = (widget.model_id, name, callback)
            self._callback_wrappers[key] = on_click
            widget.on_click(on_click)

        else:
            super()._add_widget_event_listener(widget, name, callback)

    def _remove_widget_event_listener(self, widget: widgets.Widget, name: str, callback: Callable):
        if name == "on_click":
            key = (widget.model_id, name, callback)
            on_click = self._callback_wrappers[key]
            del self._callback_wrappers[key]
            widget.on_click(on_click, remove=True)

        else:
            super()._remove_widget_event_listener(widget, name, callback)


if __name__ == "__main__":
    from . import generate

    class CodeGen(generate.CodeGen):
        element_classes = {ipywidgets.Button: ButtonElement}

        def get_extra_argument(self, cls):
            return {ipywidgets.Button: [("on_click", None, typing.Callable[[], Any])]}.get(cls, [])

    current_module = __import__(__name__)

    CodeGen([widgets, ipywidgets.widgets.widget_description, ipywidgets.widgets.widget_int]).generate(__file__)

when run through CodeGen becomes

def _Button(
    button_style: str = "",
    description: str = "",
    disabled: bool = False,
    icon: str = "",
    layout: Union[Dict[str, Any], Element[ipywidgets.widgets.widget_layout.Layout]] = {},
    style: Union[Dict[str, Any], Element[ipywidgets.widgets.widget_button.ButtonStyle]] = {},
    tooltip: str = "",
    on_button_style: typing.Callable[[str], Any] = None,
    on_description: typing.Callable[[str], Any] = None,
    on_disabled: typing.Callable[[bool], Any] = None,
    on_icon: typing.Callable[[str], Any] = None,
    on_layout: typing.Callable[[Union[Dict[str, Any], Element[ipywidgets.widgets.widget_layout.Layout]]], Any] = None,
    on_style: typing.Callable[[Union[Dict[str, Any], Element[ipywidgets.widgets.widget_button.ButtonStyle]]], Any] = None,
    on_tooltip: typing.Callable[[str], Any] = None,
    on_click: typing.Callable[[], typing.Any] = None,
) -> Element[ipywidgets.widgets.widget_button.Button]:
    """Button widget.

    This widget has an `on_click` method that allows you to listen for the
    user clicking on the button.  The click event itself is stateless.

    Parameters
    ----------
    description: str
       description displayed next to the button
    tooltip: str
       tooltip caption of the toggle button
    icon: str
       font-awesome icon name
    disabled: bool
       whether user interaction is enabled

    :param button_style: Use a predefined styling for the button.
    :param description: Button label.
    :param disabled: Enable or disable user changes.
    :param icon: Font-awesome icon name, without the 'fa-' prefix.
    :param tooltip: Tooltip caption of the button.
    """
    ...


@implements(_Button)
def Button(**kwargs):
    if isinstance(kwargs.get("layout"), dict):
        kwargs["layout"] = Layout(**kwargs["layout"])
    if isinstance(kwargs.get("style"), dict):
        kwargs["style"] = ButtonStyle(**kwargs["style"])
    widget_cls = ipywidgets.widgets.widget_button.Button
    comp = reacton.core.ComponentWidget(widget=widget_cls)
    return ButtonElement(comp, kwargs=kwargs)


del _Button

paddymul avatar Feb 17 '24 16:02 paddymul

Ok, I got this to work.

My own codegen

import reacton
from buckaroo.buckaroo_widget import BuckarooWidget

def reacton_buckaroo(**kwargs):

    widget_cls = BuckarooWidget
    comp = reacton.core.ComponentWidget(widget=widget_cls)
    return reacton.core.Element(comp, kwargs=kwargs)

then invoking it in a simple solara app

import pandas as pd
import solara

df = pd.DataFrame({'a':[10,20]})
@solara.component
def Page():
    bw = reacton_buckaroo(df=df)
Page()
Screenshot 2024-02-17 at 11 35 43 AM

paddymul avatar Feb 17 '24 16:02 paddymul