widgetti / solara

A Pure Python, React-style Framework for Scaling Your Jupyter and Web Apps
https://solara.dev
MIT License
1.75k stars 131 forks source link

Help trying to port my widget to work in Solara #506

Open paddymul opened 5 months ago

paddymul commented 5 months ago

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:

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 commented 5 months ago

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 commented 5 months ago

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