Skip to content

Commit cd1beab

Browse files
feat: allow custom serializers in vue components
1 parent a093fc8 commit cd1beab

File tree

2 files changed

+91
-8
lines changed

2 files changed

+91
-8
lines changed

solara/components/component_vue.py

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,19 @@
1212

1313
P = typing_extensions.ParamSpec("P")
1414

15-
16-
def _widget_from_signature(classname, base_class: Type[widgets.Widget], func: Callable[..., None], event_prefix: str) -> Type[widgets.Widget]:
15+
default_to_json = widgets.widget_serialization["to_json"]
16+
default_from_json = widgets.widget_serialization["from_json"]
17+
18+
19+
def _widget_from_signature(
20+
classname,
21+
base_class: Type[widgets.Widget],
22+
func: Callable[..., None],
23+
event_prefix: str,
24+
tags: Dict[str, Any],
25+
to_json: Dict[str, Callable[[Any, widgets.Widget], Any]],
26+
from_json: Dict[str, Callable[[Any, widgets.Widget], Any]],
27+
) -> Type[widgets.Widget]:
1728
classprops: Dict[str, Any] = {}
1829

1930
parameters = inspect.signature(func).parameters
@@ -38,15 +49,23 @@ def event_handler(self, data, buffers=None, event_name=event_name, param=param):
3849
trait = traitlets.Any()
3950
else:
4051
trait = traitlets.Any(default_value=param.default)
41-
classprops[name] = trait.tag(sync=True, **widgets.widget_serialization)
52+
tag = dict(sync=True, to_json=to_json.get(name, default_to_json), from_json=from_json.get(name, default_from_json))
53+
tag.update(**tags.get(name, {}))
54+
classprops[name] = trait.tag(**tag)
4255
# maps event_foo to a callable
4356
classprops["_event_callbacks"] = traitlets.Dict(default_value={})
4457

4558
widget_class = type(classname, (base_class,), classprops)
4659
return widget_class
4760

4861

49-
def _widget_vue(vue_path: str, vuetify=True) -> Callable[[Callable[P, None]], Type[v.VuetifyTemplate]]:
62+
def _widget_vue(
63+
vue_path: str,
64+
vuetify=True,
65+
to_json: Dict[str, Callable[[Any, widgets.Widget], Any]] = {},
66+
from_json: Dict[str, Callable[[Any, widgets.Widget], Any]] = {},
67+
tags: Dict[str, Any] = {},
68+
) -> Callable[[Callable[P, None]], Type[v.VuetifyTemplate]]:
5069
def decorator(func: Callable[P, None]):
5170
class VuetifyWidgetSolara(v.VuetifyTemplate):
5271
template_file = (os.path.abspath(inspect.getfile(func)), vue_path)
@@ -55,14 +74,20 @@ class VueWidgetSolara(vue.VueTemplate):
5574
template_file = (os.path.abspath(inspect.getfile(func)), vue_path)
5675

5776
base_class = VuetifyWidgetSolara if vuetify else VueWidgetSolara
58-
widget_class = _widget_from_signature("VueWidgetSolaraSub", base_class, func, "vue_")
77+
widget_class = _widget_from_signature("VueWidgetSolaraSub", base_class, func, "vue_", to_json=to_json, from_json=from_json, tags=tags)
5978

6079
return widget_class
6180

6281
return decorator
6382

6483

65-
def component_vue(vue_path: str, vuetify=True) -> Callable[[Callable[P, None]], Callable[P, solara.Element]]:
84+
def component_vue(
85+
vue_path: str,
86+
vuetify=True,
87+
tags: Dict[str, Any] = {},
88+
to_json: Dict[str, Callable[[Any, widgets.Widget], Any]] = {},
89+
from_json: Dict[str, Callable[[Any, widgets.Widget], Any]] = {},
90+
) -> Callable[[Callable[P, None]], Callable[P, solara.Element]]:
6691
"""Decorator to create a component backed by a Vue template.
6792
6893
Although many components can be made from the Python side, sometimes it is easier to write components using Vue directly.
@@ -74,14 +99,19 @@ def component_vue(vue_path: str, vuetify=True) -> Callable[[Callable[P, None]],
7499
are assumed by refer to the same vue property, with `on_foo` being the event handler when `foo` changes from
75100
the vue template.
76101
77-
Arguments or the form `event_foo` should be callbacks that can be called from the vue template. They are
102+
Arguments of the form `event_foo` should be callbacks that can be called from the vue template. They are
78103
available as the function `foo` in the vue template.
79104
80105
[See the vue v2 api](https://v2.vuejs.org/v2/api/) for more information on how to use Vue, like `watch`,
81106
`methods` and lifecycle hooks such as `mounted` and `destroyed`.
82107
83108
See the [Vue component example](/documentation/examples/general/vue_component) for an example of how to use this decorator.
84109
110+
The underlying trait can be passed extra arguments by passing a dictionary to the `tags` argument.
111+
The most common case is to pass a custom serializer and deserializer for the trait, for which we added the
112+
strictly typed `to_json` and `from_json` arguments.
113+
Otherwise pass a dictionary to the `tags` argument, see the example below for more details.
114+
85115
86116
## Examples
87117
@@ -105,6 +135,28 @@ def MyDateComponent(month: int, event_date_clicked: Callable):
105135
pass
106136
```
107137
138+
## Example with custom serializer and deserializer
139+
140+
```python
141+
import solara
142+
143+
def to_json_datetime(value: datetime.date, widget: widgets.Widget) -> str:
144+
return value.isoformat()
145+
146+
def from_json_datetime(value: str, widget: widgets.Widget) -> datetime.date:
147+
return datetime.date.fromisoformat(value)
148+
149+
@solara.component_vue("my_date_component.vue", to_json={"month": to_json_datetime}, from_json={"month": from_json_datetime})
150+
def MyDateComponent(month: datetime.date, event_date_clicked: Callable):
151+
pass
152+
153+
# the following will be the same, except that it is less strictly typed
154+
@solara.component_vue("my_date_component.vue", tags={"month": {"to_json": to_json_datetime, "from_json": from_json_datetime}})
155+
def MyDateComponentSame(month: datetime.date, event_date_clicked: Callable):
156+
pass
157+
158+
```
159+
108160
## Arguments
109161
110162
* `vue_path`: The path to the Vue template file.
@@ -113,7 +165,7 @@ def MyDateComponent(month: int, event_date_clicked: Callable):
113165
"""
114166

115167
def decorator(func: Callable[P, None]):
116-
VueWidgetSolaraSub = _widget_vue(vue_path, vuetify=vuetify)(func)
168+
VueWidgetSolaraSub = _widget_vue(vue_path, vuetify=vuetify, to_json=to_json, from_json=from_json, tags=tags)(func)
117169

118170
def wrapper(*args, **kwargs):
119171
event_callbacks = {}

tests/unit/component_frontend_test.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import unittest.mock
22

3+
import pytest
4+
35
import solara
46

57

@@ -17,6 +19,35 @@ def ComponentVueTest(value: int, name: str = "World"):
1719
assert widget.name == "Universe"
1820

1921

22+
@pytest.mark.parametrize("use_tags", [True, False])
23+
def test_component_vue_basic_with_custom_serializer(use_tags: bool):
24+
if use_tags:
25+
26+
@solara._component_vue("component_vue_test.vue", tags={"value": {"to_json": lambda x, w: str(x), "from_json": lambda x, w: int(x)}})
27+
def ComponentVueTest(value: int, name: str = "World"):
28+
pass
29+
else:
30+
31+
@solara._component_vue("component_vue_test.vue", to_json={"value": lambda x, w: str(x)}, from_json={"value": lambda x, w: int(x)})
32+
def ComponentVueTest(value: int, name: str = "World"):
33+
pass
34+
35+
box, rc = solara.render(ComponentVueTest(value=1))
36+
widget = box.children[0]
37+
assert widget.value == 1
38+
assert widget.name == "World"
39+
40+
state = widget.get_state()
41+
assert state["value"] == "1"
42+
assert state["name"] == "World"
43+
44+
state["value"] = "2"
45+
state["name"] = "Universe"
46+
widget.set_state(state)
47+
assert widget.value == 2
48+
assert widget.name == "Universe"
49+
50+
2051
def test_component_vue_callback():
2152
mock = unittest.mock.Mock()
2253

0 commit comments

Comments
 (0)