solara-ui 1.39.0__py2.py3-none-any.whl → 1.40.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- solara/__init__.py +1 -1
- solara/components/__init__.py +1 -0
- solara/components/input_text_area.py +86 -0
- solara/components/markdown.py +1 -1
- solara/hooks/use_thread.py +4 -4
- solara/lab/components/chat.py +8 -2
- solara/server/assets/style.css +2 -1
- solara/server/flask.py +1 -1
- solara/server/jupyter/server_extension.py +11 -1
- solara/server/jupyter/solara.py +91 -0
- solara/server/patch.py +1 -0
- solara/server/pyinstaller/__init__.py +9 -0
- solara/server/pyinstaller/hook-ipyreact.py +5 -0
- solara/server/pyinstaller/hook-ipyvuetify.py +5 -0
- solara/server/pyinstaller/hook-solara.py +9 -0
- solara/server/server.py +6 -1
- solara/server/starlette.py +18 -6
- solara/server/static/highlight-dark.css +1 -1
- solara/server/static/main-vuetify.js +1 -1
- solara/server/static/solara_bootstrap.py +1 -1
- solara/server/templates/solara.html.j2 +30 -6
- solara/website/assets/custom.css +20 -57
- solara/website/components/__init__.py +2 -2
- solara/website/components/algolia_api.vue +23 -6
- solara/website/components/breadcrumbs.py +28 -0
- solara/website/components/contact.py +144 -0
- solara/website/components/docs.py +11 -9
- solara/website/components/header.py +31 -20
- solara/website/components/markdown.py +12 -1
- solara/website/components/markdown_nav.vue +34 -0
- solara/website/components/sidebar.py +7 -1
- solara/website/pages/__init__.py +87 -254
- solara/website/pages/about/__init__.py +9 -0
- solara/website/pages/about/about.md +3 -0
- solara/website/pages/careers/__init__.py +27 -0
- solara/website/pages/changelog/__init__.py +2 -2
- solara/website/pages/changelog/changelog.md +12 -0
- solara/website/pages/contact/__init__.py +30 -6
- solara/website/pages/documentation/__init__.py +25 -33
- solara/website/pages/documentation/advanced/content/10-howto/40-embed.md +2 -1
- solara/website/pages/documentation/advanced/content/15-reference/41-asset-files.md +1 -1
- solara/website/pages/documentation/advanced/content/20-understanding/50-solara-server.md +2 -1
- solara/website/pages/documentation/advanced/content/30-enterprise/00-overview.md +1 -1
- solara/website/pages/documentation/advanced/content/30-enterprise/10-oauth.md +5 -2
- solara/website/pages/documentation/api/hooks/use_thread.md +6 -0
- solara/website/pages/documentation/components/data/pivot_table.py +2 -2
- solara/website/pages/documentation/components/input/input.py +2 -0
- solara/website/pages/documentation/components/output/sql_code.py +3 -3
- solara/website/pages/documentation/examples/__init__.py +2 -2
- solara/website/pages/documentation/getting_started/content/04-tutorials/_jupyter_dashboard_1.ipynb +2 -2
- solara/website/pages/documentation/getting_started/content/05-fundamentals/10-components.md +19 -14
- solara/website/pages/documentation/getting_started/content/05-fundamentals/50-state-management.md +205 -15
- solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md +3 -1
- solara/website/pages/home.vue +1199 -0
- solara/website/pages/our_team/__init__.py +83 -0
- solara/website/pages/pricing/__init__.py +31 -0
- solara/website/pages/roadmap/__init__.py +11 -0
- solara/website/pages/roadmap/roadmap.md +41 -0
- solara/website/pages/scale_ipywidgets.py +45 -0
- {solara_ui-1.39.0.dist-info → solara_ui-1.40.0.dist-info}/METADATA +2 -2
- {solara_ui-1.39.0.dist-info → solara_ui-1.40.0.dist-info}/RECORD +65 -49
- solara/website/components/hero.py +0 -15
- solara/website/pages/contact/contact.md +0 -17
- {solara_ui-1.39.0.data → solara_ui-1.40.0.data}/data/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
- {solara_ui-1.39.0.data → solara_ui-1.40.0.data}/data/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
- {solara_ui-1.39.0.dist-info → solara_ui-1.40.0.dist-info}/WHEEL +0 -0
- {solara_ui-1.39.0.dist-info → solara_ui-1.40.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Solara enterprise is a not Open Source, but the source code is visible, is free for non-commercial use and installable from pypi.
|
|
4
4
|
|
|
5
|
-
For commercial use, you require a license. Please [
|
|
5
|
+
For commercial use, you require a license. Please see [pricing](/pricing) for more information.
|
|
6
6
|
|
|
7
7
|
Solara-enterprise will stay free for non-commercial use and features that are in solara will **not be moved to solara-enterprise**.
|
|
@@ -44,12 +44,15 @@ from solara_enterprise import auth
|
|
|
44
44
|
@solara.component
|
|
45
45
|
def Page():
|
|
46
46
|
if not auth.user.value:
|
|
47
|
-
|
|
47
|
+
# `get_login_url` should automatically redirect the user back to the current page.
|
|
48
|
+
# The URL argument is only used here as an example.
|
|
49
|
+
solara.Button("Login", icon_name="mdi-login", href=auth.get_login_url("documentation/advanced/enterprise/oauth"))
|
|
48
50
|
else:
|
|
49
51
|
userinfo = auth.user.value['userinfo']
|
|
50
52
|
if 'name' in userinfo:
|
|
51
53
|
solara.Markdown(f"### Welcome {userinfo['name']}")
|
|
52
|
-
|
|
54
|
+
# See above comment
|
|
55
|
+
solara.Button("Logout", icon_name="mdi-logout", href=auth.get_logout_url("documentation/advanced/enterprise/oauth"))
|
|
53
56
|
```
|
|
54
57
|
## How to configure OAuth
|
|
55
58
|
|
|
@@ -56,3 +56,9 @@ graph TD;
|
|
|
56
56
|
RUNNING-- "work finished" -->FINISHED
|
|
57
57
|
RUNNING-- "cancel()" -->CANCELLED
|
|
58
58
|
```
|
|
59
|
+
|
|
60
|
+
## Future deprecation
|
|
61
|
+
|
|
62
|
+
Note that [`use_task`](/https://solara.dev/documentation/components/lab/use_task) is an alternative to `use_thread` that will most likely
|
|
63
|
+
replace `use_thread` in the future. Compared to `use_thread`, `use_task` has no intrusive cancel, which turned out to be too large of
|
|
64
|
+
a performance overhead to be useful. Also, `use_task` can run async functions, which is not possible with `use_thread`.
|
|
@@ -54,10 +54,10 @@ def Page():
|
|
|
54
54
|
return solara.Markdown("This example requires vaex, please install it with `pip install vaex`")
|
|
55
55
|
with solara.Div() as main:
|
|
56
56
|
solara.Markdown("# Titanic")
|
|
57
|
-
selected, on_selected = solara.use_state({"x": [0, 0]})
|
|
57
|
+
selected, on_selected = solara.use_state({"x": [0, 0]}) # noqa: SH101
|
|
58
58
|
solara.provide_cross_filter()
|
|
59
59
|
with solara.VBox():
|
|
60
|
-
type, set_type = solara.use_state("view")
|
|
60
|
+
type, set_type = solara.use_state("view") # noqa: SH101
|
|
61
61
|
with solara.ToggleButtonsSingle(type, on_value=set_type):
|
|
62
62
|
solara.Button("PivotTableView", value="view")
|
|
63
63
|
solara.Button("PivotTable", value="df")
|
|
@@ -13,6 +13,8 @@ Page = NoPage
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
__doc__ += apidoc(solara.InputText.f) # type: ignore
|
|
16
|
+
__doc__ += "# InputTextArea"
|
|
17
|
+
__doc__ += apidoc(solara.InputTextArea.f) # type: ignore
|
|
16
18
|
__doc__ += "# InputFloat"
|
|
17
19
|
__doc__ += apidoc(solara.InputFloat.f) # type: ignore
|
|
18
20
|
__doc__ += "# InputInt"
|
|
@@ -42,8 +42,8 @@ if vaex is not None:
|
|
|
42
42
|
def Page():
|
|
43
43
|
if vaex is None:
|
|
44
44
|
return solara.Error("Vaex is not installed, run pip install vaex-core vaex-hdf5")
|
|
45
|
-
query, set_query = solara.use_state("SELECT * from titanic")
|
|
46
|
-
query_executed, set_query_executed = solara.use_state(cast(Optional[str], None))
|
|
45
|
+
query, set_query = solara.use_state("SELECT * from titanic") # noqa: SH101
|
|
46
|
+
query_executed, set_query_executed = solara.use_state(cast(Optional[str], None)) # noqa: SH101
|
|
47
47
|
|
|
48
48
|
def run_query(cancel: threading.Event) -> pd.DataFrame:
|
|
49
49
|
if not query_executed:
|
|
@@ -55,7 +55,7 @@ def Page():
|
|
|
55
55
|
df = pd.DataFrame(cursor.fetchall(), columns=[k[0] for k in cursor.description])
|
|
56
56
|
return df
|
|
57
57
|
|
|
58
|
-
result: solara.Result[pd.DataFrame] = solara.use_thread(run_query, dependencies=[query_executed])
|
|
58
|
+
result: solara.Result[pd.DataFrame] = solara.use_thread(run_query, dependencies=[query_executed]) # noqa: SH101
|
|
59
59
|
with solara.VBox() as main:
|
|
60
60
|
solara.SqlCode(query=query, tables=table_hints, on_query=set_query)
|
|
61
61
|
enable_execute = (query != query_executed) or result.error is not None
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import solara
|
|
2
|
-
from solara.website.components import Gallery
|
|
2
|
+
from solara.website.components import Gallery, MarkdownWithMetadata
|
|
3
3
|
|
|
4
4
|
title = "Examples"
|
|
5
5
|
|
|
@@ -34,7 +34,7 @@ def Layout(children):
|
|
|
34
34
|
doc = module.__doc__
|
|
35
35
|
if doc:
|
|
36
36
|
with solara.VBox(grow=True):
|
|
37
|
-
|
|
37
|
+
MarkdownWithMetadata(doc)
|
|
38
38
|
with solara.HBox():
|
|
39
39
|
if route_current.path != "/":
|
|
40
40
|
solara.Button("View source code on GitHub", icon_name="mdi-github-circle", href=github_url, class_="ma-2", target="_blank", text=True)
|
solara/website/pages/documentation/getting_started/content/04-tutorials/_jupyter_dashboard_1.ipynb
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"\n",
|
|
36
36
|
"We will use a subsample of the [San Fanfrisco crime dataset](https://www.kaggle.com/competitions/sf-crime/data) which contains information on types of crimes and where they were committed.\n",
|
|
37
37
|
"\n",
|
|
38
|
-
"[Download the CSV file](https://
|
|
38
|
+
"[Download the CSV file](https://github.com/widgetti/solara/raw/master/solara/website/pages/documentation/getting_started/content/04-tutorials/SF_crime_sample.csv.gz) if you want to run this locally, or let the code below sort it out."
|
|
39
39
|
]
|
|
40
40
|
},
|
|
41
41
|
{
|
|
@@ -350,7 +350,7 @@
|
|
|
350
350
|
"\n",
|
|
351
351
|
"ROOT = Path(solara.__file__).parent / \"website\" / \"pages\" / \"docs\" / \"content\" / \"04-tutorial\"\n",
|
|
352
352
|
"path = ROOT / Path(\"SF_crime_sample.csv.gz\")\n",
|
|
353
|
-
"url = \"https://
|
|
353
|
+
"url = \"https://github.com/widgetti/solara/raw/master/solara/website/pages/documentation/getting_started/content/04-tutorials/SF_crime_sample.csv.gz\"\n",
|
|
354
354
|
"\n",
|
|
355
355
|
"if path.exists():\n",
|
|
356
356
|
" df_crime = pd.read_csv(path)\n",
|
|
@@ -41,7 +41,7 @@ By utilizing both Widget Components and Function Components, you can create flex
|
|
|
41
41
|
In Solara, users can create their own custom components without any special distinction from the built-in components provided by the framework, they are all components. These user-defined components have the same capabilities and can be composed seamlessly alongside Solara's components, allowing for the creation of highly customized and reusable user interfaces.
|
|
42
42
|
|
|
43
43
|
# Defining Components
|
|
44
|
-
To create a component in Solara, you'll start by defining a Python function decorated with
|
|
44
|
+
To create a component in Solara, you'll start by defining a Python function decorated with `@solara.component`. Inside the function, you can create the component's structure by calling Solara's built-in components or creating custom components to suit your specific needs. If a single element is created, it's taken as the component's main element. If multiple elements are created, they are automatically wrapped in a [Column component](/documentation/components/layout/column).
|
|
45
45
|
|
|
46
46
|
Here's an example of a simple Solara component that displays a button:
|
|
47
47
|
|
|
@@ -69,7 +69,7 @@ def MyApp():
|
|
|
69
69
|
MyButton()
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
In this example, we create a `MyApp` function decorated with `@solara.component`. The function
|
|
72
|
+
In this example, we create a `MyApp` function decorated with `@solara.component`. The function creates two MyButton elements, resulting in two buttons (or two component instances).
|
|
73
73
|
|
|
74
74
|
## Handling User Interactions
|
|
75
75
|
Components in Solara can capture user input and respond to events, such as button clicks or form submissions. To handle user interactions, you'll define callback functions and connect them to your components using Solara's event handling system.
|
|
@@ -87,7 +87,7 @@ def MyInteractiveButton():
|
|
|
87
87
|
solara.Button("Click me!", on_click=on_button_click)
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
In this example, we define a function called on_button_click that will be executed when the button is clicked. In the MyInteractiveButton function, we create a Button component and set the on_click argument to the on_button_click function.
|
|
90
|
+
In this example, we define a function called `on_button_click` that will be executed when the button is clicked. In the `MyInteractiveButton` function, we create a Button component and set the `on_click` argument to the `on_button_click` function.
|
|
91
91
|
|
|
92
92
|
By following these steps, you can create and use components to build rich, interactive applications with Solara.
|
|
93
93
|
|
|
@@ -111,7 +111,7 @@ def MyButton(text):
|
|
|
111
111
|
In this example, we define a function called MyButton that takes a single argument, text. The render function creates a Button component from Solara with the specified text.
|
|
112
112
|
|
|
113
113
|
### Using Application state in Components
|
|
114
|
-
To manage the state of a component in Solara, you can use the solara.reactive() function to create reactive variables. Reactive variables are used to store values that can change over time and automatically trigger component updates when their values change. This allows you to create components that respond to changes in data and user interactions.
|
|
114
|
+
To manage the state of a component in Solara, you can use the [`solara.reactive()`](/documentation/api/utilities/reactive) function to create reactive variables. Reactive variables are used to store values that can change over time and automatically trigger component updates when their values change. This allows you to create components that respond to changes in data and user interactions.
|
|
115
115
|
|
|
116
116
|
Here's an example that demonstrates the use of reactive variables in Solara components:
|
|
117
117
|
```solara
|
|
@@ -119,22 +119,25 @@ import solara
|
|
|
119
119
|
|
|
120
120
|
counter = solara.reactive(0)
|
|
121
121
|
|
|
122
|
+
|
|
122
123
|
def increment():
|
|
123
124
|
counter.value += 1
|
|
124
125
|
|
|
126
|
+
|
|
125
127
|
@solara.component
|
|
126
128
|
def CounterDisplay():
|
|
127
129
|
solara.Info(f"Counter: {counter.value}")
|
|
128
130
|
|
|
131
|
+
|
|
129
132
|
@solara.component
|
|
130
133
|
def IncrementButton():
|
|
131
134
|
solara.Button("Increment", on_click=increment)
|
|
132
135
|
|
|
136
|
+
|
|
133
137
|
@solara.component
|
|
134
138
|
def Page():
|
|
135
139
|
IncrementButton()
|
|
136
140
|
CounterDisplay()
|
|
137
|
-
|
|
138
141
|
```
|
|
139
142
|
|
|
140
143
|
In this example, we create a reactive variable counter with an initial value of 0. We define two components: `CounterDisplay` and `IncrementButton`. `CounterDisplay` renders the current value of counter, while `IncrementButton` increments the value of counter when clicked. Whenever the counter value changes, `CounterDisplay` automatically updates to display the new value.
|
|
@@ -143,33 +146,35 @@ By using arguments and state in your Solara components, you can create more dyna
|
|
|
143
146
|
|
|
144
147
|
### Internal State in Components
|
|
145
148
|
|
|
146
|
-
In addition to using reactive variables for global or application-wide state, you can also manage internal or component-specific state using the
|
|
149
|
+
In addition to using reactive variables for global or application-wide state, you can also manage internal or component-specific state using the [`use_reactive`](/documentation/api/hooks/use_reactive) hook in Solara. The `use_reactive` hook allows you to define reactive variables which are local to a component, and automatically trigger updates when their values change.
|
|
147
150
|
|
|
148
|
-
To use the
|
|
151
|
+
To use the `use_reactive` hook, call the solara.use_reactive() function inside your component function. This function takes an initial value as an argument and returns a the same reactive variable on each render.
|
|
149
152
|
|
|
150
|
-
Here's an example that demonstrates the use of the
|
|
153
|
+
Here's an example that demonstrates the use of the use_reactive hook to manage internal state in a Solara component:
|
|
151
154
|
|
|
152
155
|
```solara
|
|
153
156
|
import solara
|
|
154
157
|
|
|
158
|
+
|
|
155
159
|
@solara.component
|
|
156
160
|
def Counter():
|
|
157
|
-
count
|
|
161
|
+
count = solara.use_reactive(0)
|
|
158
162
|
|
|
159
163
|
def increment():
|
|
160
|
-
|
|
164
|
+
count.value += 1
|
|
161
165
|
|
|
162
166
|
solara.Button("Increment", on_click=increment)
|
|
163
|
-
solara.Info(f"Counter: {count}")
|
|
167
|
+
solara.Info(f"Counter: {count.value}")
|
|
168
|
+
|
|
164
169
|
|
|
165
170
|
@solara.component
|
|
166
171
|
def Page():
|
|
167
172
|
Counter()
|
|
168
173
|
```
|
|
169
174
|
|
|
170
|
-
In this example, we define a Counter component that uses the
|
|
175
|
+
In this example, we define a Counter component that uses the `use_reactive` hook to manage its internal state. We create a reactive variable called `count` with an initial value of 0 and a function `increment` to update the state. The increment function increments the value of count when the button is clicked. Whenever the count value changes, the component automatically updates to display the new value.
|
|
171
176
|
|
|
172
|
-
By using the
|
|
177
|
+
By using the `use_reactive` hook, you can manage the internal state of your components and create more dynamic and interactive applications that respond to user input and changes in data.
|
|
173
178
|
|
|
174
179
|
## Lazy Rendering in Solara Components
|
|
175
180
|
|
|
@@ -216,7 +221,7 @@ This example demonstrates Solara's lazy rendering, where only the relevant compo
|
|
|
216
221
|
|
|
217
222
|
|
|
218
223
|
## Conclusions
|
|
219
|
-
In conclusion, understanding components, their arguments, and how to manage their internal state is crucial for building Solara applications. To create more advanced components, you need to have a deeper understanding of hooks, such as the
|
|
224
|
+
In conclusion, understanding components, their arguments, and how to manage their internal state is crucial for building Solara applications. To create more advanced components, you need to have a deeper understanding of hooks, such as the `use_reactive` hook we have already discussed.
|
|
220
225
|
|
|
221
226
|
In the next fundamentals article, we will explore more hooks available in Solara, which will enable you to build more sophisticated components that cater to a wide range of use cases. By learning about hooks, you can create powerful components that can manage state, interact with other components, and respond to user input.
|
|
222
227
|
|
solara/website/pages/documentation/getting_started/content/05-fundamentals/50-state-management.md
CHANGED
|
@@ -33,21 +33,26 @@ def Page():
|
|
|
33
33
|
|
|
34
34
|
In this case, the `SomeAppSpecificComponent` is not reusable in the sense that a second component has a different state. The `color` variable is global and shared across all components. This component is meant to be used only once, and mainly helps to organize the code.
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
You may have heard that globals are considered a bad practice. As with many things, it depends on the context. A possible downside of using a global is that it does not
|
|
37
|
+
allow you to create multiple instances of the same component with different states. But if the state reflects application state, there is by definition only one instance
|
|
38
|
+
of it needed.
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
### Local component state using solara.use_reactive
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
If you do need state that is specific to a component, you should use [`solara.use_reactive`](/documentation/api/hooks/use_reactive) hook. This hook allow you to create local state variables that are scoped to a specific component. This approach is useful when you want to encapsulate state within a component, making it self-contained and modular. Local state management is suitable for situations where state changes only affect the component and do not need to be shared across the application.
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
```solara hl_lines="6 8"
|
|
42
46
|
import solara
|
|
43
47
|
|
|
48
|
+
|
|
44
49
|
@solara.component
|
|
45
50
|
def ReusableComponent():
|
|
46
|
-
|
|
47
|
-
color, set_color = solara.use_state("red") # local state
|
|
51
|
+
color = solara.use_reactive("red") # local state (instead of top level solara.reactive)
|
|
48
52
|
solara.Select(label="Color",values=["red", "green", "blue", "orange"],
|
|
49
|
-
|
|
50
|
-
solara.Markdown("### Solara is awesome", style={"color": color})
|
|
53
|
+
value=color)
|
|
54
|
+
solara.Markdown("### Solara is awesome", style={"color": color.value})
|
|
55
|
+
|
|
51
56
|
|
|
52
57
|
@solara.component
|
|
53
58
|
def Page():
|
|
@@ -57,22 +62,27 @@ def Page():
|
|
|
57
62
|
|
|
58
63
|
```
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
### Local component state using solara.use_state (not recommended)
|
|
61
66
|
|
|
67
|
+
[`solara.use_state`](/documentation/api/hooks/use_state) is a hook that might be a bit more familiar to React developers. It also allows you to create local state variables that are scoped to a specific component, however, instead of using reactive variables, it uses a tuple of a value and a setter function.
|
|
62
68
|
|
|
63
|
-
|
|
69
|
+
We generally recommend using `use_reactive` over `use_state` as it is more easy to refactor between global application state and local component state by switching between `use_reactive` and `reactive`. There is no equivalent for `use_state` at the global level.
|
|
64
70
|
|
|
71
|
+
If we take the previous example and replace `use_reactive` by `use_state`, we get:
|
|
65
72
|
|
|
66
|
-
|
|
67
|
-
```solara
|
|
73
|
+
Example:
|
|
74
|
+
```solara hl_lines="6 9"
|
|
68
75
|
import solara
|
|
69
76
|
|
|
77
|
+
|
|
70
78
|
@solara.component
|
|
71
79
|
def ReusableComponent():
|
|
72
|
-
color = solara.use_reactive("red") #
|
|
80
|
+
# color = solara.use_reactive("red") # instead of use_reactive (not recommended)
|
|
81
|
+
color, set_color = solara.use_state("red") # local state
|
|
73
82
|
solara.Select(label="Color",values=["red", "green", "blue", "orange"],
|
|
74
|
-
|
|
75
|
-
solara.Markdown("### Solara is awesome", style={"color": color
|
|
83
|
+
value=color, on_value=set_color)
|
|
84
|
+
solara.Markdown("### Solara is awesome", style={"color": color})
|
|
85
|
+
|
|
76
86
|
|
|
77
87
|
@solara.component
|
|
78
88
|
def Page():
|
|
@@ -82,6 +92,186 @@ def Page():
|
|
|
82
92
|
|
|
83
93
|
```
|
|
84
94
|
|
|
95
|
+
## Mutation pittfalls
|
|
96
|
+
|
|
97
|
+
In Python, strings, numbers, and tuples are immutable. This means that you cannot change the value of a string, number, or tuple in place.
|
|
98
|
+
Instead, you need to create a new object and assign that to a variable.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
a = 1
|
|
102
|
+
b = a
|
|
103
|
+
# a.value = 2 # ERROR: numbers are immutable
|
|
104
|
+
a = 2 # instead, re-assign a new value, the number 1 will not change
|
|
105
|
+
assert b == 1 # b is still 1
|
|
106
|
+
```
|
|
107
|
+
However, many objects in Python are mutable, including lists and dictionaries or potentially user defined classes. This means that you can change the value of a list, dictionary, or user defined class in place without creating a new object.
|
|
108
|
+
```python
|
|
109
|
+
a = [1, 2, 3]
|
|
110
|
+
b = a # b points to the same list as a
|
|
111
|
+
a.append(4) # a is now [1, 2, 3, 4]
|
|
112
|
+
assert b == [1, 2, 3, 4] # b is also [1, 2, 3, 4]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Not mutating lists
|
|
116
|
+
|
|
117
|
+
However, mutations in Python are not observable. **This means that if you change the value of a list, dictionary, or user defined class, Solara does not know that the value has changed and does not know it needs to re-render a component that uses that value.**
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
import solara
|
|
121
|
+
|
|
122
|
+
reactive_list = solara.reactive([1, 2, 3])
|
|
123
|
+
# The next line will not trigger a re-render of a component
|
|
124
|
+
reactive_list.value.append(4) # ERROR: mutating a list is not observable in Python
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Although Solara could potentially track mutations of lists and dictionaries, that would be difficult to do for user defined classes, since we would need to know which methods mutate the object and which do not. Therefore, we have chosen not to include
|
|
128
|
+
any magic tracking of mutations in Solara, and instead require you to re-assign a new value to a reactive variable if you want to trigger a re-render.
|
|
129
|
+
|
|
130
|
+
```python hl_lines="5"
|
|
131
|
+
import solara
|
|
132
|
+
|
|
133
|
+
reactive_list = solara.reactive([1, 2, 3])
|
|
134
|
+
# Instead, re-assign a new value
|
|
135
|
+
reactive_list.value = [*reactive_list.value, 4] # GOOD: re-assign a new list
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Not mutating dictionaries
|
|
139
|
+
|
|
140
|
+
A similar pattern applies to dictionaries.
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
import solara
|
|
144
|
+
|
|
145
|
+
reactive_dict = solara.reactive({"a": 1, "b": 2})
|
|
146
|
+
reactive_dict.value = {**reactive_dict.value, "c": 3} # GOOD: re-assign a new dictionary
|
|
147
|
+
# deleting a key
|
|
148
|
+
reactive_dict.value = {k: v for k, v in reactive_dict.value.items() if k != "a"} # GOOD: re-assign a new dictionary
|
|
149
|
+
# deleting a key (method 2)
|
|
150
|
+
dict_copy = reactive_dict.value.copy()
|
|
151
|
+
del dict_copy["b"]
|
|
152
|
+
reactive_dict.value = dict_copy # GOOD: re-assign a new dictionary
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Not mutating user defined classes
|
|
156
|
+
|
|
157
|
+
Or user defined classes, like a Pandas DataFrame.
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
import solara
|
|
161
|
+
import pandas as pd
|
|
162
|
+
|
|
163
|
+
reactive_df = solara.reactive(pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]}))
|
|
164
|
+
# reactive_df.value.append({"a": 4, "b": 7}) # BAD: mutating a DataFrame is not observable in Python
|
|
165
|
+
df_copy = reactive_df.value.copy(deep=True) # for Pandas 3, deep=True is not necessary
|
|
166
|
+
df_copy = df_copy.append({"a": 4, "b": 7}, ignore_index=True)
|
|
167
|
+
reactive_df.value = df_copy # GOOD: re-assign a new DataFrame
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
## Creating a store
|
|
172
|
+
|
|
173
|
+
Using reactive variables is a powerful way to manage state in your Solara applications. However, as your application grows, you may find that you need a more structured way to manage your state.
|
|
174
|
+
|
|
175
|
+
In larger applications, you may want to create a store to manage your application's state. A store is a regular Python class where all attributes are reactive variables.
|
|
176
|
+
|
|
177
|
+
In your Python class you are free to expose the reactive variables as attributes, or you can create properties to make certain attributes read-only or to add additional logic when setting an attribute.
|
|
178
|
+
|
|
179
|
+
A complete TODO application demonstrates this below.
|
|
180
|
+
|
|
181
|
+
```solara
|
|
182
|
+
import uuid
|
|
183
|
+
from typing import Callable
|
|
184
|
+
|
|
185
|
+
import solara
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# this todo item is only a collection of reactive values
|
|
189
|
+
class TodoItem:
|
|
190
|
+
def __init__(self, text: str, done: bool = False):
|
|
191
|
+
self.text = solara.reactive(text)
|
|
192
|
+
self.done = solara.reactive(done)
|
|
193
|
+
self._uuid = solara.reactive(str(uuid.uuid4()))
|
|
194
|
+
self._dirty = solara.reactive(True)
|
|
195
|
+
|
|
196
|
+
def __str__(self) -> str:
|
|
197
|
+
return f"{self.text.value} ({'done' if self.done else 'not done'})"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# However, this class really adds some logic to the todo items
|
|
201
|
+
class TodoStore:
|
|
202
|
+
def __init__(self, items: list[TodoItem]):
|
|
203
|
+
# we keep the items as a protected attribute
|
|
204
|
+
self._items = solara.reactive(items)
|
|
205
|
+
self.add_item_text = solara.reactive("")
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def items(self):
|
|
209
|
+
# and make the items read only for a property
|
|
210
|
+
return self._items.value
|
|
211
|
+
|
|
212
|
+
def add_item(self, item):
|
|
213
|
+
self._items.value = [*self._items.value, item]
|
|
214
|
+
# reset the new text after adding a new item
|
|
215
|
+
self.add_item_text.value = ""
|
|
216
|
+
|
|
217
|
+
def add(self):
|
|
218
|
+
self.add_item(TodoItem(text=self.add_item_text.value))
|
|
219
|
+
|
|
220
|
+
def remove(self, item: TodoItem):
|
|
221
|
+
self._items.value = [k for k in self.items if k._uuid.value != item._uuid.value]
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def done_count(self):
|
|
225
|
+
return len([k for k in self.items if k.done.value])
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def done_percentage(self):
|
|
229
|
+
if len(self.items) == 0:
|
|
230
|
+
return 0
|
|
231
|
+
else:
|
|
232
|
+
return self.done_count / len(self.items) * 100
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@solara.component
|
|
236
|
+
def TodoItemCard(item: TodoItem, on_remove: Callable[[TodoItem], None]):
|
|
237
|
+
with solara.Card():
|
|
238
|
+
solara.InputText("", value=item.text)
|
|
239
|
+
solara.Switch(label="Done", value=item.done)
|
|
240
|
+
solara.Button("Remove", on_click=lambda: on_remove(item))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# The TodoApp component is reusable, so in the future
|
|
244
|
+
# we could have multiple TodoApp components if needed
|
|
245
|
+
# (e.g. multiple lists of todos)
|
|
246
|
+
|
|
247
|
+
default_store = TodoStore(
|
|
248
|
+
[
|
|
249
|
+
TodoItem(text="Write a blog post", done=False),
|
|
250
|
+
TodoItem(text="Take out the trash", done=True),
|
|
251
|
+
TodoItem(text="Do the laundry", done=False),
|
|
252
|
+
]
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@solara.component
|
|
257
|
+
def TodoApp(store: TodoStore = default_store):
|
|
258
|
+
for item in store.items:
|
|
259
|
+
TodoItemCard(item, on_remove=store.remove)
|
|
260
|
+
|
|
261
|
+
with solara.Card("New item"):
|
|
262
|
+
solara.InputText(label="Text", value=store.add_item_text)
|
|
263
|
+
solara.Button("Add new", on_click=store.add)
|
|
264
|
+
solara.ProgressLinear(value=store.done_percentage)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@solara.component
|
|
268
|
+
def Page():
|
|
269
|
+
TodoApp()
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
|
|
85
275
|
## Conclusion
|
|
86
276
|
Understanding the advantages and disadvantages of reusable components and application-specific code can help you strike the right balance between modularity and simplicity when building your Solara applications.
|
|
87
277
|
|
|
@@ -65,7 +65,7 @@ A typical command would be:
|
|
|
65
65
|
$ SOLARA_APP=sol.py gunicorn --workers 4 --threads=20 -b 0.0.0.0:8765 solara.server.flask:app
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
Note that we need at least 1 thread per user due to the use of a websocket.
|
|
68
|
+
Note that we need at least 1 thread per user due to the use of a websocket. *Make sure you understand the implications of using multiple workers by reading [about handling multiple workers with solara server](https://solara.dev/documentation/advanced/understanding/solara-server#handling-multiple-workers)*
|
|
69
69
|
|
|
70
70
|
### Embedding in an existing Flask application
|
|
71
71
|
|
|
@@ -98,6 +98,8 @@ For [Starlette](https://www.starlette.io/) we will assume [uvicorn](http://www.u
|
|
|
98
98
|
$ SOLARA_APP=sol.py uvicorn --workers 4 --host 0.0.0.0 --port 8765 solara.server.starlette:app
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
*Make sure you understand the implications of using multiple workers by reading [about handling multiple workers with solara server](https://solara.dev/documentation/advanced/understanding/solara-server#handling-multiple-workers)*
|
|
102
|
+
|
|
101
103
|
### Embedding in an existing Starlette application
|
|
102
104
|
|
|
103
105
|
If you already have a Starlette app and want to add your Solara app behind a prefix, say `'/solara/'`, you can mount the Starlette routes for Solara to your existing app as follows.
|