Skip to content

Commit 5af6bc9

Browse files
committed
feat!: dataclass is ready for prime time
1 parent e964ed1 commit 5af6bc9

25 files changed

+620
-20
lines changed

README.rst

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
trame-dataclass
22
----------------------------------------
33

4-
Dataclass for trame UI binding
4+
Dataclass for trame UI binding and reactivity handling.
55

66
License
77
----------------------------------------
@@ -20,22 +20,31 @@ Install the application/library
2020
Usage example
2121
----------------------------------------
2222

23-
.. code-block:: console
23+
.. code-block:: python
24+
25+
from typing import Any
2426
25-
from trame.app import TrameApp, StateDataModel
27+
from trame.app import TrameApp
28+
from trame.app.dataclass import ClientOnly, ServerOnly, StateDataModel, Sync, watch
2629
from trame.ui.html import DivLayout
27-
from trame.widgets import html
30+
from trame.widgets import dataclass, html
2831
2932
3033
class SimpleStructure(StateDataModel):
31-
name: str = "John Doe"
32-
age: int = 1
33-
derived_value: int
34+
name = Sync(str, "John Doe") # server <=> client
35+
age = Sync(int, 1) # server <=> client
36+
derived_value = Sync(int) # server <=> client
37+
something = ServerOnly(Any) # server
38+
local_edit = ClientOnly(int) # server => client
3439
3540
@watch("age")
3641
def _update_derived(self, age):
3742
self.derived_value = 80 - age
3843
44+
@watch("local_edit")
45+
def _never_called(self, local_edit):
46+
print("local_edit changed to", local_edit)
47+
3948
4049
class GettingStarted(TrameApp):
4150
def __init__(self, server=None):
@@ -45,16 +54,26 @@ Usage example
4554
self._build_ui()
4655
4756
def print_age(self, age):
48-
print(f"{age=}")
57+
print(f"Age changed to {age=}")
58+
59+
def toggle_active_user(self):
60+
if self.state.active_user:
61+
self.state.active_user = None
62+
else:
63+
self.state.active_user = self._data._id
4964
5065
def _modify_data(self):
5166
self._data.age += 1
5267
5368
def _build_ui(self):
5469
with DivLayout(self.server) as self.ui:
70+
# Edit user on server
5571
html.Button("Server change", click=self._modify_data)
56-
html.Div("Getting started with StateDataModel")
57-
with self._data.Provider(name="user"):
72+
73+
# Provide data class instance to the UI as a variable
74+
with self._data.provide_as("user"):
75+
html.Button("Edit local", click="user.local_edit = Date.now()")
76+
5877
html.Pre("{{ JSON.stringify(user, null, 2) }}")
5978
html.Hr()
6079
html.Div(
@@ -68,6 +87,20 @@ Usage example
6887
html.Input(
6988
type="range", min=0, max=120, step=1, v_model_number="user.age"
7089
)
90+
html.Hr()
91+
92+
# Adjust dynamic user
93+
html.Button(
94+
"Toggle user ({{ active_user || 'None' }})",
95+
click=self.toggle_active_user,
96+
)
97+
98+
# Dynamically provide a dataclass to UI
99+
with dataclass.Provider(
100+
name="dynamic_user",
101+
instance=("active_user", None),
102+
):
103+
html.Pre("{{ JSON.stringify(dynamic_user, null, 2) }}")
71104
72105
73106
def main():
@@ -128,3 +161,8 @@ Professional Support
128161
* `Training <https://www.kitware.com/courses/trame/>`_: Learn how to confidently use trame from the expert developers at Kitware.
129162
* `Support <https://www.kitware.com/trame/support/>`_: Our experts can assist your team as you build your web application and establish in-house expertise.
130163
* `Custom Development <https://www.kitware.com/trame/support/>`_: Leverage Kitware’s 25+ years of experience to quickly build your web application.
164+
165+
Commit message convention
166+
----------------------------------------
167+
168+
Semantic release rely on `conventional commits <https://www.conventionalcommits.org/en/v1.0.0/>`_ to generate new releases and changelog.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env -S uv run --script
2+
#
3+
# /// script
4+
# requires-python = ">=3.10"
5+
# dependencies = [
6+
# "trame",
7+
# "trame-vuetify>=3.2",
8+
# "trame-dataclass>=2",
9+
# ]
10+
# ///
11+
from trame.app import TrameApp
12+
from trame.app.dataclass import StateDataModel, Sync, watch
13+
from trame.ui.html import DivLayout
14+
from trame.widgets import html
15+
16+
17+
class Data(StateDataModel):
18+
values_reactive = Sync(list[int], list, client_deep_reactive=True)
19+
values = Sync(list[int], list)
20+
21+
@watch("values_reactive")
22+
def _on_values_reactive(self, v):
23+
print("values_reactive", v)
24+
25+
@watch("values")
26+
def _on_values(self, _):
27+
print("values")
28+
29+
30+
class DeepReactive(TrameApp):
31+
def __init__(self, server=None):
32+
super().__init__(server)
33+
self.data = Data(self.server)
34+
self.data.values = [1, 2, 3, 4, 5]
35+
self.data.values_reactive = [1, 2, 3, 4, 5]
36+
self._build_ui()
37+
38+
def _build_ui(self):
39+
with DivLayout(self.server) as self.ui:
40+
with self.data.provide_as("data"):
41+
html.Hr()
42+
html.Div("Regular Array")
43+
html.Button("Add", click="data.values.push(1)")
44+
html.Hr()
45+
with html.Div(v_for="v, i in data.values", key="i"):
46+
html.Input(
47+
type="range",
48+
v_model_number="data.values[i]",
49+
min=0,
50+
max=10,
51+
step=1,
52+
)
53+
html.Button("+ (js)", click="data.values[i]++")
54+
html.Button("- (js)", click="data.values[i]--")
55+
html.Hr()
56+
html.Div("Deep reactive Array")
57+
html.Button("Add", click="data.values_reactive.push(1)")
58+
html.Hr()
59+
with html.Div(v_for="v, i in data.values_reactive", key="i"):
60+
html.Input(
61+
type="range",
62+
v_model_number="data.values_reactive[i]",
63+
min=0,
64+
max=10,
65+
step=1,
66+
)
67+
html.Button("+ (js)", click="data.values_reactive[i]++")
68+
html.Button("- (js)", click="data.values_reactive[i]--")
69+
html.Hr()
70+
71+
72+
def main():
73+
app = DeepReactive()
74+
app.server.start()
75+
76+
77+
if __name__ == "__main__":
78+
main()
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env -S uv run --script
2+
#
3+
# /// script
4+
# requires-python = ">=3.10"
5+
# dependencies = [
6+
# "trame",
7+
# "trame-vuetify>=3.2",
8+
# "trame-dataclass>=2",
9+
# ]
10+
# ///
11+
12+
from pathlib import Path
13+
14+
from trame.app import TrameApp
15+
from trame.app.dataclass import FieldEncoder, StateDataModel, Sync
16+
from trame.ui.html import DivLayout
17+
from trame.widgets import html
18+
19+
20+
def path_encode(p: Path) -> str:
21+
return str(p)
22+
23+
24+
def path_decode(p: str) -> Path:
25+
return Path(p)
26+
27+
28+
def list_encode(value: list[Path] | None) -> list[str] | None:
29+
if value is None:
30+
return None
31+
32+
return list(map(path_encode, value))
33+
34+
35+
def list_decode(value: list[str] | None) -> list[Path] | None:
36+
if value is None:
37+
return None
38+
39+
return list(map(path_decode, value))
40+
41+
42+
class CustomStructure(StateDataModel):
43+
path = Sync(Path, Path.cwd(), FieldEncoder(path_encode, path_decode))
44+
children = Sync(list[Path] | None, None, FieldEncoder(list_encode, list_decode))
45+
46+
47+
class CustomEncoder(TrameApp):
48+
def __init__(self, server=None):
49+
super().__init__(server)
50+
self._data = CustomStructure(self.server)
51+
self._data.watch(["path"], self._list_children, eager=True)
52+
self._build_ui()
53+
54+
def _list_children(self, file_path: Path):
55+
if file_path.exists():
56+
self._data.children = list(file_path.glob("*"))
57+
else:
58+
self._data.children = None
59+
60+
def _build_ui(self):
61+
with DivLayout(self.server) as self.ui:
62+
html.Div("Getting started with StateDataModel")
63+
with self._data.provide_as("data"):
64+
html.Input(v_model="data.path", style="width: 95%;")
65+
html.Hr()
66+
html.Pre("{{ JSON.stringify(data.children, null, 2) }}")
67+
68+
69+
def main():
70+
app = CustomEncoder()
71+
app.server.start()
72+
73+
74+
if __name__ == "__main__":
75+
main()

examples/getting_started/readme.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#!/usr/bin/env -S uv run --script
2+
#
3+
# /// script
4+
# requires-python = ">=3.10"
5+
# dependencies = [
6+
# "trame",
7+
# "trame-vuetify>=3.2",
8+
# "trame-dataclass>=2",
9+
# ]
10+
# ///
11+
12+
from typing import Any
13+
14+
from trame.app import TrameApp
15+
from trame.app.dataclass import ClientOnly, ServerOnly, StateDataModel, Sync, watch
16+
from trame.ui.html import DivLayout
17+
from trame.widgets import dataclass, html
18+
19+
20+
class SimpleStructure(StateDataModel):
21+
name = Sync(str, "John Doe") # server <=> client
22+
age = Sync(int, 1) # server <=> client
23+
derived_value = Sync(int) # server <=> client
24+
something = ServerOnly(Any) # server
25+
local_edit = ClientOnly(int) # server => client
26+
27+
@watch("age")
28+
def _update_derived(self, age):
29+
self.derived_value = 80 - age
30+
31+
@watch("local_edit")
32+
def _never_called(self, local_edit):
33+
print("local_edit changed to", local_edit)
34+
35+
36+
class GettingStarted(TrameApp):
37+
def __init__(self, server=None):
38+
super().__init__(server)
39+
self._data = SimpleStructure(self.server)
40+
self._data.watch(["age"], self.print_age)
41+
self._build_ui()
42+
43+
def print_age(self, age):
44+
print(f"Age changed to {age=}")
45+
46+
def toggle_active_user(self):
47+
if self.state.active_user:
48+
self.state.active_user = None
49+
else:
50+
self.state.active_user = self._data._id
51+
52+
def _modify_data(self):
53+
self._data.age += 1
54+
55+
def _build_ui(self):
56+
with DivLayout(self.server) as self.ui:
57+
# Edit user on server
58+
html.Button("Server change", click=self._modify_data)
59+
60+
# Provide data class instance to the UI as a variable
61+
with self._data.provide_as("user"):
62+
html.Button("Edit local", click="user.local_edit = Date.now()")
63+
64+
html.Pre("{{ JSON.stringify(user, null, 2) }}")
65+
html.Hr()
66+
html.Div(
67+
"Hello {{ user.name }} - derived value = {{ user.derived_value }}"
68+
)
69+
html.Hr()
70+
html.Span("Your name:")
71+
html.Input(type="text", v_model="user.name")
72+
html.Hr()
73+
html.Span("Your age:")
74+
html.Input(
75+
type="range", min=0, max=120, step=1, v_model_number="user.age"
76+
)
77+
html.Hr()
78+
79+
# Adjust dynamic user
80+
html.Button(
81+
"Toggle user ({{ active_user || 'None' }})",
82+
click=self.toggle_active_user,
83+
)
84+
85+
# Dynamically provide a dataclass to UI
86+
with dataclass.Provider(
87+
name="dynamic_user",
88+
instance=("active_user", None),
89+
):
90+
html.Pre("{{ JSON.stringify(dynamic_user, null, 2) }}")
91+
92+
93+
def main():
94+
app = GettingStarted()
95+
app.server.start()
96+
97+
98+
if __name__ == "__main__":
99+
main()

0 commit comments

Comments
 (0)