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

feat: event support in component_vue for calling python callbacks. #312

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions solara/components/component_vue.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import inspect
from typing import Callable, Type
from typing import Any, Callable, Dict, Type

import ipyvue as vue
import ipyvuetify as v
Expand All @@ -12,20 +12,35 @@
P = typing_extensions.ParamSpec("P")


def _widget_from_signature(name, base_class: Type[widgets.Widget], func: Callable[..., None]) -> Type[widgets.Widget]:
traits = {}
def _widget_from_signature(classname, base_class: Type[widgets.Widget], func: Callable[..., None], event_prefix: str) -> Type[widgets.Widget]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the change from name to classname a part of adding event functionality, or for general refactoring purposes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was an unrelated change actually. The name was overridden in the for loop, causing the class name to be useless when errors occurred. Should have been a separate commit.

classprops: Dict[str, Any] = {}

parameters = inspect.signature(func).parameters
for name, param in parameters.items():
if name.startswith("event_"):
event_name = name[6:]

def event_handler(self, data, buffers=None, event_name=event_name):
callback = self._event_callbacks.get(event_name)
if callback:
if buffers:
callback(data, buffers)
else:
callback(data)

classprops[f"vue_{event_name}"] = event_handler
if name.startswith("on_") and name[3:] in parameters:
# callback, will be handled by reacton
continue
if param.default == inspect.Parameter.empty:
trait = traitlets.Any()
else:
trait = traitlets.Any(default_value=param.default)
traits[name] = trait.tag(sync=True, **widgets.widget_serialization)
widget_class = type(name, (base_class,), traits)
classprops[name] = trait.tag(sync=True, **widgets.widget_serialization)
# maps event_foo to a callable
classprops["_event_callbacks"] = traitlets.Dict(default_value={})

widget_class = type(classname, (base_class,), classprops)
return widget_class


Expand All @@ -38,7 +53,7 @@ class VueWidgetSolara(vue.VueTemplate):
template_file = (inspect.getfile(func), vue_path)

base_class = VuetifyWidgetSolara if vuetify else VueWidgetSolara
widget_class = _widget_from_signature("VueWidgetSolaraSub", base_class, func)
widget_class = _widget_from_signature("VueWidgetSolaraSub", base_class, func, "vue_")

return widget_class

Expand All @@ -57,6 +72,9 @@ def component_vue(vue_path: str, vuetify=True) -> Callable[[Callable[P, None]],
are assumed by refer to the same vue property, with `on_foo` being the event handler when `foo` changes from
the vue template.

Arguments or the form `event_foo` should be callbacks that can be called from the vue template. They are
available as the function `foo` in the vue template.

[See the vue v2 api](https://v2.vuejs.org/v2/api/) for more information on how to use Vue, like `watch`,
`methods` and lifecycle hooks such as `mounted` and `destroyed`.

Expand All @@ -73,6 +91,14 @@ def decorator(func: Callable[P, None]):
VueWidgetSolaraSub = _widget_vue(vue_path, vuetify=vuetify)(func)

def wrapper(*args, **kwargs):
event_callbacks = {}
kwargs = kwargs.copy()
# take out all events named like event_foo and put them in a separate dict
for name in list(kwargs):
if name.startswith("event_"):
event_callbacks[name[6:]] = kwargs.pop(name)
if event_callbacks:
kwargs["_event_callbacks"] = event_callbacks
return VueWidgetSolaraSub.element(*args, **kwargs) # type: ignore

return wrapper
Expand Down
2 changes: 1 addition & 1 deletion solara/website/pages/examples/general/mycard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<v-divider></v-divider>

<v-card-actions class="justify-center">
<v-btn block text>
<v-btn block text @click="goto_report">
Go to Report
</v-btn>
</v-card-actions>
Expand Down
18 changes: 14 additions & 4 deletions solara/website/pages/examples/general/vue_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

"""

from typing import Callable

import numpy as np

import solara
Expand All @@ -20,6 +22,7 @@

@solara.component_vue("mycard.vue")
def MyCard(
event_goto_report: Callable[[dict], None],
value=[1, 10, 30, 20, 3],
caption="My Card",
color="red",
Expand All @@ -31,11 +34,18 @@ def MyCard(
def Page():
gen = np.random.RandomState(seed=seed.value)
sales_data = np.floor(np.cumsum(gen.random(7) - 0.5) * 100 + 100)
show_report = solara.use_reactive(False)

with solara.Column(style={"min-width": "600px"}):
if show_report.value:
with solara.Card("Report"):
solara.Markdown("Lorum ipsum dolor sit amet")
solara.Button("Go back", on_click=lambda: show_report.set(False))
else:

def new_seed():
seed.value = np.random.randint(0, 100)
def new_seed():
seed.value = np.random.randint(0, 100)

solara.Button("Generate new data", on_click=new_seed)
solara.Button("Generate new data", on_click=new_seed)

MyCard(value=sales_data.tolist(), color="green", caption="Sales Last 7 Days")
MyCard(value=sales_data.tolist(), color="green", caption="Sales Last 7 Days", event_goto_report=lambda data: show_report.set(True))
16 changes: 16 additions & 0 deletions tests/unit/component_frontend_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,19 @@ def ComponentVueTest(value: int, on_value=None):
mock.assert_called_once_with(2)
widget.value = 3
mock.assert_called_with(3)


def test_component_vue_event():
mock = unittest.mock.Mock()

@solara._component_vue("component_vue_test.vue")
def ComponentVueTest(event_foo=None):
pass

box, rc = solara.render(ComponentVueTest(event_foo=mock), handle_error=False)
widget = box.children[0]
mock.assert_not_called()
widget._handle_event(None, {"event": "foo", "data": 42}, None)
mock.assert_called_once_with(42)
widget._handle_event(None, {"event": "foo", "data": 42}, [b"bar"])
mock.assert_called_with(42, [b"bar"])