solara-ui 1.41.0__py2.py3-none-any.whl → 1.42.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/__main__.py +7 -1
- solara/_stores.py +185 -0
- solara/components/component_vue.py +23 -0
- solara/components/echarts.py +5 -2
- solara/components/echarts.vue +22 -5
- solara/components/file_drop.py +20 -0
- solara/components/input.py +16 -0
- solara/components/markdown.py +22 -13
- solara/components/spinner-solara.vue +2 -2
- solara/components/spinner.py +17 -2
- solara/hooks/use_reactive.py +8 -1
- solara/reactive.py +9 -3
- solara/server/kernel.py +2 -1
- solara/server/qt.py +1 -1
- solara/server/starlette.py +2 -2
- solara/server/static/solara_bootstrap.py +1 -1
- solara/settings.py +14 -0
- solara/template/portal/pyproject.toml +1 -1
- solara/test/pytest_plugin.py +3 -0
- solara/toestand.py +139 -16
- solara/util.py +22 -0
- solara/website/components/markdown.py +45 -1
- solara/website/pages/changelog/changelog.md +9 -0
- solara/website/pages/documentation/api/cross_filter/cross_filter_dataframe.py +4 -5
- solara/website/pages/documentation/api/cross_filter/cross_filter_report.py +3 -5
- solara/website/pages/documentation/api/cross_filter/cross_filter_select.py +3 -5
- solara/website/pages/documentation/api/cross_filter/cross_filter_slider.py +3 -5
- solara/website/pages/documentation/api/hooks/use_cross_filter.py +3 -5
- solara/website/pages/documentation/api/hooks/use_exception.py +9 -11
- solara/website/pages/documentation/api/hooks/use_previous.py +6 -9
- solara/website/pages/documentation/api/hooks/use_state_or_update.py +23 -26
- solara/website/pages/documentation/api/hooks/use_thread.py +11 -13
- solara/website/pages/documentation/api/utilities/on_kernel_start.py +17 -0
- solara/website/pages/documentation/components/input/input.py +22 -0
- solara/website/pages/documentation/components/viz/echarts.py +3 -1
- solara/website/pages/documentation/examples/general/pokemon_search.py +3 -3
- solara/website/pages/documentation/examples/visualization/linked_views.py +0 -3
- solara/website/pages/documentation/getting_started/content/00-quickstart.md +1 -1
- solara/website/pages/documentation/getting_started/content/01-introduction.md +1 -1
- solara/website/pages/roadmap/roadmap.md +3 -0
- {solara_ui-1.41.0.dist-info → solara_ui-1.42.0.dist-info}/METADATA +8 -5
- {solara_ui-1.41.0.dist-info → solara_ui-1.42.0.dist-info}/RECORD +47 -46
- {solara_ui-1.41.0.dist-info → solara_ui-1.42.0.dist-info}/WHEEL +1 -1
- {solara_ui-1.41.0.data → solara_ui-1.42.0.data}/data/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
- {solara_ui-1.41.0.data → solara_ui-1.42.0.data}/data/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
- {solara_ui-1.41.0.dist-info → solara_ui-1.42.0.dist-info}/licenses/LICENSE +0 -0
solara/__init__.py
CHANGED
solara/__main__.py
CHANGED
|
@@ -341,7 +341,13 @@ def run(
|
|
|
341
341
|
reload_excludes = restart_excludes if restart_excludes else []
|
|
342
342
|
del restart_excludes
|
|
343
343
|
reload_excludes = [str(solara_root / "website"), str(solara_root / "template")]
|
|
344
|
-
|
|
344
|
+
app_path = Path(app)
|
|
345
|
+
if app_path.exists():
|
|
346
|
+
# if app is not a child of the current working directory
|
|
347
|
+
# uvicorn crashes
|
|
348
|
+
if not str(app_path.resolve()).startswith(str(Path.cwd().resolve())):
|
|
349
|
+
reload_excludes.append(str(app_path.resolve()))
|
|
350
|
+
del app_path
|
|
345
351
|
del solara_root
|
|
346
352
|
reload = True
|
|
347
353
|
# avoid sending many restarts
|
solara/_stores.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import dataclasses
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import Callable, ContextManager, Generic, Optional, Union, cast
|
|
5
|
+
import warnings
|
|
6
|
+
from .toestand import ValueBase, KernelStore, S, _find_outside_solara_frame
|
|
7
|
+
import solara.util
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _PublicValueNotSet:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclasses.dataclass
|
|
15
|
+
class StoreValue(Generic[S]):
|
|
16
|
+
private: S # the internal private value, should never be mutated
|
|
17
|
+
public: Union[S, _PublicValueNotSet] # this is the value that is exposed in .get(), it is a deep copy of private
|
|
18
|
+
get_traceback: Optional[inspect.Traceback]
|
|
19
|
+
set_value: Optional[S] # the value that was set using .set(..), we deepcopy this to set private
|
|
20
|
+
set_traceback: Optional[inspect.Traceback]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MutateDetectorStore(ValueBase[S]):
|
|
24
|
+
def __init__(self, store: KernelStore[StoreValue[S]], equals=solara.util.equals_extra):
|
|
25
|
+
self._storage = store
|
|
26
|
+
self._enabled = True
|
|
27
|
+
super().__init__(equals=equals)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def lock(self):
|
|
31
|
+
return self._storage.lock
|
|
32
|
+
|
|
33
|
+
def get(self) -> S:
|
|
34
|
+
self.check_mutations()
|
|
35
|
+
self._ensure_public_exists()
|
|
36
|
+
value = self._storage.get()
|
|
37
|
+
# value.public is of type Optional[S], so it's tempting to check for None here,
|
|
38
|
+
# but S could include None as a valid value, so best we can do is cast
|
|
39
|
+
public_value = cast(S, value.public)
|
|
40
|
+
return public_value
|
|
41
|
+
|
|
42
|
+
def peek(self) -> S:
|
|
43
|
+
"""Return the value without automatically subscribing to listeners."""
|
|
44
|
+
self.check_mutations()
|
|
45
|
+
store_value = self._storage.peek()
|
|
46
|
+
self._ensure_public_exists()
|
|
47
|
+
public_value = cast(S, store_value.public)
|
|
48
|
+
return public_value
|
|
49
|
+
|
|
50
|
+
def set(self, value: S):
|
|
51
|
+
self.check_mutations()
|
|
52
|
+
self._ensure_public_exists()
|
|
53
|
+
private = copy.deepcopy(value)
|
|
54
|
+
self._check_equals(private, value)
|
|
55
|
+
frame = _find_outside_solara_frame()
|
|
56
|
+
if frame is not None:
|
|
57
|
+
frame_info = inspect.getframeinfo(frame)
|
|
58
|
+
else:
|
|
59
|
+
frame_info = None
|
|
60
|
+
store_value = StoreValue(private=private, public=_PublicValueNotSet(), get_traceback=None, set_value=value, set_traceback=frame_info)
|
|
61
|
+
self._storage.set(store_value)
|
|
62
|
+
|
|
63
|
+
def check_mutations(self):
|
|
64
|
+
self._storage._check_mutation()
|
|
65
|
+
if not self._enabled:
|
|
66
|
+
return
|
|
67
|
+
store_value = self._storage.peek()
|
|
68
|
+
if not isinstance(store_value.public, _PublicValueNotSet) and not self.equals(store_value.public, store_value.private):
|
|
69
|
+
tb = store_value.get_traceback
|
|
70
|
+
# TODO: make the error message as elaborate as below
|
|
71
|
+
msg = (
|
|
72
|
+
f"Reactive variable was read when it had the value of {store_value.private!r}, but was later mutated to {store_value.public!r}.\n"
|
|
73
|
+
"Mutation should not be done on the value of a reactive variable, as in production mode we would be unable to track changes.\n"
|
|
74
|
+
)
|
|
75
|
+
if tb:
|
|
76
|
+
if tb.code_context:
|
|
77
|
+
code = tb.code_context[0]
|
|
78
|
+
else:
|
|
79
|
+
code = "<No code context available>"
|
|
80
|
+
msg += f"The last value was read in the following code:\n" f"{tb.filename}:{tb.lineno}\n" f"{code}"
|
|
81
|
+
raise ValueError(msg)
|
|
82
|
+
elif store_value.set_value is not None and not self.equals(store_value.set_value, store_value.private):
|
|
83
|
+
tb = store_value.set_traceback
|
|
84
|
+
msg = f"""Reactive variable was set with a value of {store_value.private!r}, but was later mutated mutated to {store_value.set_value!r}.
|
|
85
|
+
|
|
86
|
+
Mutation should not be done on the value of a reactive variable, as in production mode we would be unable to track changes.
|
|
87
|
+
|
|
88
|
+
Bad:
|
|
89
|
+
mylist = reactive([]]
|
|
90
|
+
some_values = [1, 2, 3]
|
|
91
|
+
mylist.value = some_values # you give solara a reference to your list
|
|
92
|
+
some_values.append(4) # but later mutate it (solara cannot detect this change, so a render will not be triggered)
|
|
93
|
+
# if later on a re-render happens for a different reason, you will read of the mutated list.
|
|
94
|
+
|
|
95
|
+
Good (if you want the reactive variable to be updated):
|
|
96
|
+
mylist = reactive([]]
|
|
97
|
+
some_values = [1, 2, 3]
|
|
98
|
+
mylist.value = some_values
|
|
99
|
+
mylist.value = some_values + [4]
|
|
100
|
+
|
|
101
|
+
Good (if you want to keep mutating your own list):
|
|
102
|
+
mylist = reactive([]]
|
|
103
|
+
some_values = [1, 2, 3]
|
|
104
|
+
mylist.value = some_values.copy() # this gives solara a copy of the list
|
|
105
|
+
some_values.append(4) # you are free to mutate your own list, solara will not see this
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
if tb:
|
|
109
|
+
if tb.code_context:
|
|
110
|
+
code = tb.code_context[0]
|
|
111
|
+
else:
|
|
112
|
+
code = "<No code context available>"
|
|
113
|
+
msg += "The last time the value was set was at:\n" f"{tb.filename}:{tb.lineno}\n" f"{code}"
|
|
114
|
+
raise ValueError(msg)
|
|
115
|
+
|
|
116
|
+
def _ensure_public_exists(self):
|
|
117
|
+
store_value = self._storage.peek()
|
|
118
|
+
if isinstance(store_value.public, _PublicValueNotSet):
|
|
119
|
+
with self.lock:
|
|
120
|
+
if isinstance(store_value.public, _PublicValueNotSet):
|
|
121
|
+
frame = _find_outside_solara_frame()
|
|
122
|
+
if frame is not None:
|
|
123
|
+
frame_info = inspect.getframeinfo(frame)
|
|
124
|
+
else:
|
|
125
|
+
frame_info = None
|
|
126
|
+
store_value.public = copy.deepcopy(store_value.private)
|
|
127
|
+
self._check_equals(store_value.public, store_value.private)
|
|
128
|
+
store_value.get_traceback = frame_info
|
|
129
|
+
|
|
130
|
+
def _check_equals(self, a: S, b: S):
|
|
131
|
+
if not self._enabled:
|
|
132
|
+
return
|
|
133
|
+
if not self.equals(a, b):
|
|
134
|
+
frame = _find_outside_solara_frame()
|
|
135
|
+
if frame is not None:
|
|
136
|
+
frame_info = inspect.getframeinfo(frame)
|
|
137
|
+
else:
|
|
138
|
+
frame_info = None
|
|
139
|
+
|
|
140
|
+
warn = """The equals function for this reactive value returned False when comparing a deepcopy to itself.
|
|
141
|
+
|
|
142
|
+
This reactive variable will not be able to detect mutations correctly, and is therefore disabled.
|
|
143
|
+
|
|
144
|
+
To avoid this warning, and to ensure that mutation detection works correctly, please provide a better equals function to the reactive variable.
|
|
145
|
+
A good choice for dataframes and numpy arrays might be solara.util.equals_pickle, which will also attempt to compare the pickled values of the objects.
|
|
146
|
+
|
|
147
|
+
Example:
|
|
148
|
+
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
|
|
149
|
+
reactive_df = solara.reactive(df, equals=solara.util.equals_pickle)
|
|
150
|
+
"""
|
|
151
|
+
tb = frame_info
|
|
152
|
+
if tb:
|
|
153
|
+
if tb.code_context:
|
|
154
|
+
code = tb.code_context[0]
|
|
155
|
+
else:
|
|
156
|
+
code = "<No code context available>"
|
|
157
|
+
warn += "This warning was triggered from:\n" f"{tb.filename}:{tb.lineno}\n" f"{code}"
|
|
158
|
+
warnings.warn(warn)
|
|
159
|
+
self._enabled = False
|
|
160
|
+
|
|
161
|
+
def subscribe(self, listener: Callable[[S], None], scope: Optional[ContextManager] = None):
|
|
162
|
+
def listener_wrapper(new: StoreValue[S], previous: StoreValue[S]):
|
|
163
|
+
self._ensure_public_exists()
|
|
164
|
+
assert new.public is not None
|
|
165
|
+
assert previous.public is not None
|
|
166
|
+
previous_value = previous.set_value if previous.set_value is not None else previous.private
|
|
167
|
+
new_value = new.set_value
|
|
168
|
+
assert new_value is not None
|
|
169
|
+
if not self.equals(new_value, previous_value):
|
|
170
|
+
listener(new_value)
|
|
171
|
+
|
|
172
|
+
return self._storage.subscribe_change(listener_wrapper, scope=scope)
|
|
173
|
+
|
|
174
|
+
def subscribe_change(self, listener: Callable[[S, S], None], scope: Optional[ContextManager] = None):
|
|
175
|
+
def listener_wrapper(new: StoreValue[S], previous: StoreValue[S]):
|
|
176
|
+
self._ensure_public_exists()
|
|
177
|
+
assert new.public is not None
|
|
178
|
+
assert previous.public is not None
|
|
179
|
+
previous_value = previous.set_value if previous.set_value is not None else previous.private
|
|
180
|
+
new_value = new.set_value
|
|
181
|
+
assert new_value is not None
|
|
182
|
+
if not self.equals(new_value, previous_value):
|
|
183
|
+
listener(new_value, previous_value)
|
|
184
|
+
|
|
185
|
+
return self._storage.subscribe_change(listener_wrapper, scope=scope)
|
|
@@ -82,6 +82,29 @@ def component_vue(vue_path: str, vuetify=True) -> Callable[[Callable[P, None]],
|
|
|
82
82
|
|
|
83
83
|
See the [Vue component example](/documentation/examples/general/vue_component) for an example of how to use this decorator.
|
|
84
84
|
|
|
85
|
+
|
|
86
|
+
## Examples
|
|
87
|
+
|
|
88
|
+
A component that takes a `foo` argument and an `on_foo` callback that gets called when `foo` changes (from the frontend).
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
import solara
|
|
92
|
+
|
|
93
|
+
@solara.component_vue("my_foo_component.vue")
|
|
94
|
+
def MyFooComponent(foo: int, on_foo: Callable[[int], None]):
|
|
95
|
+
pass
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The following component only takes in a month argument and an event_date_clicked callback that gets called from
|
|
99
|
+
the vue template using `this.date_clicked({'extra-data': 42, 'day': this.day})`.
|
|
100
|
+
```python
|
|
101
|
+
import solara
|
|
102
|
+
|
|
103
|
+
@solara.component_vue("my_date_component.vue")
|
|
104
|
+
def MyDateComponent(month: int, event_date_clicked: Callable):
|
|
105
|
+
pass
|
|
106
|
+
```
|
|
107
|
+
|
|
85
108
|
## Arguments
|
|
86
109
|
|
|
87
110
|
* `vue_path`: The path to the Vue template file.
|
solara/components/echarts.py
CHANGED
|
@@ -10,7 +10,7 @@ class EchartsWidget(ipyvuetify.VuetifyTemplate):
|
|
|
10
10
|
template_file = (__file__, "echarts.vue")
|
|
11
11
|
|
|
12
12
|
attributes = traitlets.Dict(default_value=None, allow_none=True).tag(sync=True)
|
|
13
|
-
|
|
13
|
+
responsive = traitlets.Bool(False).tag(sync=True)
|
|
14
14
|
maps = traitlets.Any({}).tag(sync=True)
|
|
15
15
|
option = traitlets.Any({}).tag(sync=True)
|
|
16
16
|
on_click = traitlets.Callable(None, allow_none=True)
|
|
@@ -49,7 +49,8 @@ def FigureEcharts(
|
|
|
49
49
|
on_mouseover: Callable[[Any], Any] = None,
|
|
50
50
|
on_mouseout: Callable[[Any], Any] = None,
|
|
51
51
|
maps: dict = {},
|
|
52
|
-
attributes={"style": "height: 400px"},
|
|
52
|
+
attributes={"style": "height: 400px;"},
|
|
53
|
+
responsive: bool = False,
|
|
53
54
|
):
|
|
54
55
|
"""Create a Echarts figure.
|
|
55
56
|
|
|
@@ -68,6 +69,7 @@ def FigureEcharts(
|
|
|
68
69
|
* on_mouseout: Callable, a function that will be called when the user moves the mouse out of a certain component.
|
|
69
70
|
* maps: dict, a dictionary of maps to be used in the figure.
|
|
70
71
|
* attributes: dict, a dictionary of attributes to be passed to the container (like style, class).
|
|
72
|
+
* responsive: bool, whether the chart should resize when the container changes size.
|
|
71
73
|
|
|
72
74
|
|
|
73
75
|
"""
|
|
@@ -80,4 +82,5 @@ def FigureEcharts(
|
|
|
80
82
|
on_mouseout=on_mouseout,
|
|
81
83
|
on_mouseout_enabled=on_mouseout is not None,
|
|
82
84
|
attributes=attributes,
|
|
85
|
+
responsive=responsive,
|
|
83
86
|
)
|
solara/components/echarts.vue
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
3
|
-
<div ref="echarts" class="solara-echarts" v-bind="attributes"></div>
|
|
4
|
-
</div>
|
|
2
|
+
<div ref="echarts" class="solara-echarts" v-bind="attributes"></div>
|
|
5
3
|
</template>
|
|
6
4
|
<script>
|
|
7
5
|
module.exports = {
|
|
@@ -14,6 +12,22 @@ module.exports = {
|
|
|
14
12
|
this.echarts = echarts;
|
|
15
13
|
this.create();
|
|
16
14
|
})();
|
|
15
|
+
if(this.responsive){
|
|
16
|
+
this.resizeObserver = new ResizeObserver(entries => {
|
|
17
|
+
for (let entry of entries) {
|
|
18
|
+
if (entry.target === this.$refs.echarts) {
|
|
19
|
+
this.handleContainerResize();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
this.resizeObserver.observe(this.$refs.echarts);
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
beforeDestroy() {
|
|
27
|
+
if (this.resizeObserver) {
|
|
28
|
+
this.resizeObserver.unobserve(this.$refs.echarts);
|
|
29
|
+
this.resizeObserver.disconnect();
|
|
30
|
+
}
|
|
17
31
|
},
|
|
18
32
|
watch: {
|
|
19
33
|
option() {
|
|
@@ -25,9 +39,7 @@ module.exports = {
|
|
|
25
39
|
methods: {
|
|
26
40
|
create() {
|
|
27
41
|
this.chart = this.echarts.init(this.$refs.echarts);
|
|
28
|
-
console.log(this.maps);
|
|
29
42
|
Object.keys(this.maps).forEach((mapName) => {
|
|
30
|
-
console.log(mapName);
|
|
31
43
|
this.echarts.registerMap(mapName, this.maps[mapName]);
|
|
32
44
|
});
|
|
33
45
|
|
|
@@ -66,6 +78,11 @@ module.exports = {
|
|
|
66
78
|
if (this.on_mouseout_enabled) this.on_mouseout(eventData);
|
|
67
79
|
});
|
|
68
80
|
},
|
|
81
|
+
handleContainerResize() {
|
|
82
|
+
if (this.chart) {
|
|
83
|
+
this.chart.resize();
|
|
84
|
+
}
|
|
85
|
+
},
|
|
69
86
|
import(deps) {
|
|
70
87
|
return this.loadRequire().then(() => {
|
|
71
88
|
if (window.jupyterVue) {
|
solara/components/file_drop.py
CHANGED
|
@@ -111,6 +111,26 @@ def FileDrop(
|
|
|
111
111
|
* `lazy`: Whether to load the file contents into memory or not. If `False`,
|
|
112
112
|
the file contents will be loaded into memory via the `.data` attribute of file object(s).
|
|
113
113
|
|
|
114
|
+
## Load into Pandas
|
|
115
|
+
To load the data into a Pandas DF, set `lazy=False` and use `file['file_obj']` (be careful of memory)<br>
|
|
116
|
+
You can run this directly in your Jupyter notebook
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import io
|
|
120
|
+
import pandas as pd
|
|
121
|
+
import solara
|
|
122
|
+
|
|
123
|
+
@solara.component
|
|
124
|
+
def Page():
|
|
125
|
+
def load_file_df(file):
|
|
126
|
+
df = pd.read_csv(file["file_obj"])
|
|
127
|
+
print("Loaded dataframe:")
|
|
128
|
+
print(df)
|
|
129
|
+
|
|
130
|
+
solara.FileDrop(label="Drop file to see dataframe!", on_file=load_file_df)
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
|
|
114
134
|
"""
|
|
115
135
|
|
|
116
136
|
return _FileDrop(label=label, on_total_progress=on_total_progress, on_file=on_file, lazy=lazy, multiple=False)
|
solara/components/input.py
CHANGED
|
@@ -49,6 +49,7 @@ def InputText(
|
|
|
49
49
|
message: Optional[str] = None,
|
|
50
50
|
classes: List[str] = [],
|
|
51
51
|
style: Optional[Union[str, Dict[str, str]]] = None,
|
|
52
|
+
autofocus: bool = False,
|
|
52
53
|
):
|
|
53
54
|
"""Free form text input.
|
|
54
55
|
|
|
@@ -105,6 +106,7 @@ def InputText(
|
|
|
105
106
|
* `message`: Message to show below the input. If `error` is a string, this will be ignored.
|
|
106
107
|
* `classes`: List of CSS classes to apply to the input.
|
|
107
108
|
* `style`: CSS style to apply to the input.
|
|
109
|
+
* `autofocus`: Determines if a component is to be autofocused or not (Default is False). Autofocus will occur during page load and only one component per page can have autofocus active.
|
|
108
110
|
"""
|
|
109
111
|
reactive_value = solara.use_reactive(value, on_value)
|
|
110
112
|
del value, on_value
|
|
@@ -133,6 +135,7 @@ def InputText(
|
|
|
133
135
|
messages=messages,
|
|
134
136
|
class_=classes_flat,
|
|
135
137
|
style_=style_flat,
|
|
138
|
+
autofocus=autofocus,
|
|
136
139
|
)
|
|
137
140
|
use_change(text_field, set_value_cast, enabled=not continuous_update, update_events=update_events)
|
|
138
141
|
return text_field
|
|
@@ -150,6 +153,7 @@ def InputFloat(
|
|
|
150
153
|
clearable: bool = ...,
|
|
151
154
|
classes: List[str] = ...,
|
|
152
155
|
style: Optional[Union[str, Dict[str, str]]] = ...,
|
|
156
|
+
autofocus: bool = False,
|
|
153
157
|
) -> reacton.core.ValueElement[vw.TextField, Any]: ...
|
|
154
158
|
|
|
155
159
|
|
|
@@ -165,6 +169,7 @@ def InputFloat(
|
|
|
165
169
|
clearable: bool = ...,
|
|
166
170
|
classes: List[str] = ...,
|
|
167
171
|
style: Optional[Union[str, Dict[str, str]]] = ...,
|
|
172
|
+
autofocus: bool = False,
|
|
168
173
|
) -> reacton.core.ValueElement[vw.TextField, Any]: ...
|
|
169
174
|
|
|
170
175
|
|
|
@@ -179,6 +184,7 @@ def InputFloat(
|
|
|
179
184
|
clearable: bool = False,
|
|
180
185
|
classes: List[str] = [],
|
|
181
186
|
style: Optional[Union[str, Dict[str, str]]] = None,
|
|
187
|
+
autofocus: bool = False,
|
|
182
188
|
):
|
|
183
189
|
"""Numeric input (floats).
|
|
184
190
|
|
|
@@ -211,6 +217,7 @@ def InputFloat(
|
|
|
211
217
|
* `clearable`: Whether the input can be cleared.
|
|
212
218
|
* `classes`: List of CSS classes to apply to the input.
|
|
213
219
|
* `style`: CSS style to apply to the input.
|
|
220
|
+
* `autofocus`: Determines if a component is to be autofocused or not (Default is False). Autofocus will occur either during page load, or when the component becomes visible (for example, dialog being opened). Only one component per page should have autofocus on each such event.
|
|
214
221
|
|
|
215
222
|
"""
|
|
216
223
|
|
|
@@ -237,6 +244,7 @@ def InputFloat(
|
|
|
237
244
|
clearable=clearable,
|
|
238
245
|
classes=classes,
|
|
239
246
|
style=style,
|
|
247
|
+
autofocus=autofocus,
|
|
240
248
|
)
|
|
241
249
|
|
|
242
250
|
|
|
@@ -252,6 +260,7 @@ def InputInt(
|
|
|
252
260
|
clearable: bool = ...,
|
|
253
261
|
classes: List[str] = ...,
|
|
254
262
|
style: Optional[Union[str, Dict[str, str]]] = ...,
|
|
263
|
+
autofocus: bool = False,
|
|
255
264
|
) -> reacton.core.ValueElement[vw.TextField, Any]: ...
|
|
256
265
|
|
|
257
266
|
|
|
@@ -267,6 +276,7 @@ def InputInt(
|
|
|
267
276
|
clearable: bool = ...,
|
|
268
277
|
classes: List[str] = ...,
|
|
269
278
|
style: Optional[Union[str, Dict[str, str]]] = ...,
|
|
279
|
+
autofocus: bool = False,
|
|
270
280
|
) -> reacton.core.ValueElement[vw.TextField, Any]: ...
|
|
271
281
|
|
|
272
282
|
|
|
@@ -281,6 +291,7 @@ def InputInt(
|
|
|
281
291
|
clearable: bool = False,
|
|
282
292
|
classes: List[str] = [],
|
|
283
293
|
style: Optional[Union[str, Dict[str, str]]] = None,
|
|
294
|
+
autofocus: bool = False,
|
|
284
295
|
):
|
|
285
296
|
"""Numeric input (integers).
|
|
286
297
|
|
|
@@ -312,6 +323,7 @@ def InputInt(
|
|
|
312
323
|
* `clearable`: Whether the input can be cleared.
|
|
313
324
|
* `classes`: List of CSS classes to apply to the input.
|
|
314
325
|
* `style`: CSS style to apply to the input.
|
|
326
|
+
* `autofocus`: Determines if a component is to be autofocused or not (Default is False). Autofocus will occur either during page load, or when the component becomes visible (for example, dialog being opened). Only one component per page should have autofocus on each such event.
|
|
315
327
|
"""
|
|
316
328
|
|
|
317
329
|
def str_to_int(value: Optional[str]):
|
|
@@ -336,6 +348,7 @@ def InputInt(
|
|
|
336
348
|
clearable=clearable,
|
|
337
349
|
classes=classes,
|
|
338
350
|
style=style,
|
|
351
|
+
autofocus=autofocus,
|
|
339
352
|
)
|
|
340
353
|
|
|
341
354
|
|
|
@@ -389,6 +402,7 @@ def _InputNumeric(
|
|
|
389
402
|
clearable: bool = False,
|
|
390
403
|
classes: List[str] = [],
|
|
391
404
|
style: Optional[Union[str, Dict[str, str]]] = None,
|
|
405
|
+
autofocus: bool = False,
|
|
392
406
|
):
|
|
393
407
|
"""Numeric input.
|
|
394
408
|
|
|
@@ -401,6 +415,7 @@ def _InputNumeric(
|
|
|
401
415
|
* `continuous_update`: Whether to call the `on_value` callback on every change or only when the input loses focus or the enter key is pressed.
|
|
402
416
|
* `classes`: List of CSS classes to apply to the input.
|
|
403
417
|
* `style`: CSS style to apply to the input.
|
|
418
|
+
* `autofocus`: Determines if a component is to be autofocused or not (Default is False). Autofocus will occur either during page load, or when the component becomes visible (for example, dialog being opened). Only one component per page should have autofocus on each such event.
|
|
404
419
|
"""
|
|
405
420
|
style_flat = solara.util._flatten_style(style)
|
|
406
421
|
classes_flat = solara.util._combine_classes(classes)
|
|
@@ -431,6 +446,7 @@ def _InputNumeric(
|
|
|
431
446
|
error=bool(error),
|
|
432
447
|
class_=classes_flat,
|
|
433
448
|
style_=style_flat,
|
|
449
|
+
autofocus=autofocus,
|
|
434
450
|
)
|
|
435
451
|
use_change(text_field, set_value_cast, enabled=not continuous_update)
|
|
436
452
|
return text_field
|
solara/components/markdown.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import functools
|
|
1
2
|
import hashlib
|
|
2
3
|
import html
|
|
3
4
|
import logging
|
|
4
5
|
import textwrap
|
|
5
6
|
import traceback
|
|
6
7
|
import warnings
|
|
7
|
-
from typing import Any, Dict, List, Union, cast
|
|
8
|
+
from typing import Any, Dict, List, Optional, Union, cast
|
|
9
|
+
import typing
|
|
8
10
|
|
|
9
11
|
import ipyvuetify as v
|
|
10
12
|
|
|
@@ -30,6 +32,9 @@ else:
|
|
|
30
32
|
from pygments.formatters import HtmlFormatter
|
|
31
33
|
from pygments.lexers import get_lexer_by_name
|
|
32
34
|
|
|
35
|
+
if typing.TYPE_CHECKING:
|
|
36
|
+
import markdown
|
|
37
|
+
|
|
33
38
|
logger = logging.getLogger(__name__)
|
|
34
39
|
|
|
35
40
|
html_no_execute_enabled = "<div><i>Solara execution is not enabled</i></div>"
|
|
@@ -231,7 +236,7 @@ module.exports = {
|
|
|
231
236
|
return template
|
|
232
237
|
|
|
233
238
|
|
|
234
|
-
def _highlight(src, language, unsafe_solara_execute,
|
|
239
|
+
def _highlight(src, language, unsafe_solara_execute, *args, **kwargs):
|
|
235
240
|
"""Highlight a block of code"""
|
|
236
241
|
|
|
237
242
|
if not has_pygments:
|
|
@@ -258,6 +263,10 @@ def _highlight(src, language, unsafe_solara_execute, extra, *args, **kwargs):
|
|
|
258
263
|
return src_html
|
|
259
264
|
|
|
260
265
|
|
|
266
|
+
def formatter(unsafe_solara_execute: bool):
|
|
267
|
+
return functools.partial(_highlight, unsafe_solara_execute=unsafe_solara_execute)
|
|
268
|
+
|
|
269
|
+
|
|
261
270
|
@solara.component
|
|
262
271
|
def MarkdownIt(md_text: str, highlight: List[int] = [], unsafe_solara_execute: bool = False):
|
|
263
272
|
md_text = textwrap.dedent(md_text)
|
|
@@ -293,7 +302,7 @@ def _no_deep_copy_emojione(options, md):
|
|
|
293
302
|
|
|
294
303
|
|
|
295
304
|
@solara.component
|
|
296
|
-
def Markdown(md_text: str, unsafe_solara_execute=False, style: Union[str, Dict, None] = None):
|
|
305
|
+
def Markdown(md_text: str, unsafe_solara_execute=False, style: Union[str, Dict, None] = None, md_parser: Optional["markdown.Markdown"] = None):
|
|
297
306
|
"""Renders markdown text
|
|
298
307
|
|
|
299
308
|
Renders markdown using https://python-markdown.github.io/
|
|
@@ -333,6 +342,7 @@ def Markdown(md_text: str, unsafe_solara_execute=False, style: Union[str, Dict,
|
|
|
333
342
|
* `unsafe_solara_execute`: If True, code marked with language "solara" will be executed. This is potentially unsafe
|
|
334
343
|
if the markdown text can come from user input and should only be used for trusted markdown.
|
|
335
344
|
* `style`: A string or dict of css styles to apply to the rendered markdown.
|
|
345
|
+
* `md_parser`: A markdown object to use for rendering. If not provided, a markdown object will be created.
|
|
336
346
|
|
|
337
347
|
"""
|
|
338
348
|
import markdown
|
|
@@ -341,13 +351,9 @@ def Markdown(md_text: str, unsafe_solara_execute=False, style: Union[str, Dict,
|
|
|
341
351
|
style = solara.util._flatten_style(style)
|
|
342
352
|
|
|
343
353
|
def make_markdown_object():
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
except Exception as e:
|
|
348
|
-
logger.exception("Error highlighting code: %s", src)
|
|
349
|
-
return repr(e)
|
|
350
|
-
|
|
354
|
+
if md_parser is not None:
|
|
355
|
+
# we won't use the use_memo
|
|
356
|
+
return None
|
|
351
357
|
if has_pymdownx:
|
|
352
358
|
return markdown.Markdown( # type: ignore
|
|
353
359
|
extensions=[
|
|
@@ -371,7 +377,7 @@ def Markdown(md_text: str, unsafe_solara_execute=False, style: Union[str, Dict,
|
|
|
371
377
|
{
|
|
372
378
|
"name": "solara",
|
|
373
379
|
"class": "",
|
|
374
|
-
"format":
|
|
380
|
+
"format": formatter(unsafe_solara_execute),
|
|
375
381
|
},
|
|
376
382
|
],
|
|
377
383
|
},
|
|
@@ -388,8 +394,11 @@ def Markdown(md_text: str, unsafe_solara_execute=False, style: Union[str, Dict,
|
|
|
388
394
|
],
|
|
389
395
|
)
|
|
390
396
|
|
|
391
|
-
|
|
392
|
-
|
|
397
|
+
md_self = solara.use_memo(make_markdown_object, dependencies=[unsafe_solara_execute])
|
|
398
|
+
if md_parser is None:
|
|
399
|
+
assert md_self is not None
|
|
400
|
+
md_parser = md_self
|
|
401
|
+
html = md_parser.convert(md_text)
|
|
393
402
|
# if we update the template value, the whole vue tree will rerender (ipvue/ipyvuetify issue)
|
|
394
403
|
# however, using the hash we simply generate a new widget each time
|
|
395
404
|
hash = hashlib.sha256((html + str(unsafe_solara_execute)).encode("utf-8")).hexdigest()
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
xmlns:svgjs="http://svgjs.com/svgjs" :width="size" :height="size" viewBox="0 0 65 65" fill="none">
|
|
6
6
|
<path id="sun-spinner1" style="transform-origin: center;"
|
|
7
7
|
d="M57.11 30.64L61.47 17.87L48.7 13.51L42.76 1.39999L30.65 7.34999L17.87 2.97999L13.51 15.75L1.40002 21.7L7.35002 33.81L2.99002 46.58L15.76 50.94L21.71 63.06L33.82 57.11L46.59 61.47L50.95 48.7L63.06 42.75L57.11 30.64ZM54.26 34.39L34.39 54.26C33.19 55.46 31.25 55.46 30.05 54.26L10.2 34.4C9.00002 33.2 9.00002 31.26 10.2 30.07L30.06 10.2C31.26 8.99999 33.2 8.99999 34.4 10.2L54.27 30.07C55.47 31.27 55.47 33.21 54.27 34.4L54.26 34.39Z"
|
|
8
|
-
fill="
|
|
8
|
+
:fill="color_back"></path>
|
|
9
9
|
<path id="sun-spinner2" style="transform-origin: center;"
|
|
10
10
|
d="M53.62 19.42L51.65 6.07L38.3 8.04L27.46 0L19.42 10.84L6.07 12.82L8.04 26.17L0 37L10.84 45.04L12.81 58.39L26.16 56.42L37 64.46L45.04 53.62L58.39 51.64L56.42 38.29L64.46 27.45L53.62 19.4V19.42ZM52.8 24.06L44.24 50.82C43.72 52.43 42 53.32 40.39 52.81L13.63 44.25C12.02 43.74 11.13 42.01 11.64 40.4L20.21 13.64C20.72 12.03 22.45 11.14 24.06 11.65L50.82 20.21C52.43 20.72 53.32 22.45 52.81 24.06H52.8Z"
|
|
11
|
-
fill="
|
|
11
|
+
:fill="color_front"></path>
|
|
12
12
|
</svg>
|
|
13
13
|
</div>
|
|
14
14
|
</template>
|
solara/components/spinner.py
CHANGED
|
@@ -8,12 +8,15 @@ class SpinnerSolaraWidget(ipyvue.VueTemplate):
|
|
|
8
8
|
template_file = (__file__, "spinner-solara.vue")
|
|
9
9
|
|
|
10
10
|
size = traitlets.Unicode("64px").tag(sync=True)
|
|
11
|
+
color_back = traitlets.Unicode("#FFCF64").tag(sync=True)
|
|
12
|
+
color_front = traitlets.Unicode("#FF8C3E").tag(sync=True)
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
@solara.component
|
|
14
|
-
def SpinnerSolara(size="64px"):
|
|
16
|
+
def SpinnerSolara(size="64px", color_back="#FFCF64", color_front="#FF8C3E"):
|
|
15
17
|
"""Spinner component with the Solara logo to indicate the app is busy.
|
|
16
18
|
|
|
19
|
+
## Examples
|
|
17
20
|
### Basic example
|
|
18
21
|
|
|
19
22
|
```solara
|
|
@@ -24,7 +27,19 @@ def SpinnerSolara(size="64px"):
|
|
|
24
27
|
solara.SpinnerSolara(size="100px")
|
|
25
28
|
```
|
|
26
29
|
|
|
30
|
+
## Changing the colors
|
|
31
|
+
```solara
|
|
32
|
+
import solara
|
|
33
|
+
|
|
34
|
+
@solara.component
|
|
35
|
+
def Page():
|
|
36
|
+
solara.SpinnerSolara(size="100px", color_back="Grey", color_front="Lime")
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
|
|
27
40
|
## Arguments
|
|
28
41
|
* `size`: Size of the spinner.
|
|
42
|
+
* `color_back`: Color of the spinner in the background.
|
|
43
|
+
* `color_front`: Color of the spinner in the foreground.
|
|
29
44
|
"""
|
|
30
|
-
return SpinnerSolaraWidget.element(size=size)
|
|
45
|
+
return SpinnerSolaraWidget.element(size=size, color_back=color_back, color_front=color_front)
|
solara/hooks/use_reactive.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Callable, Optional, TypeVar, Union
|
|
1
|
+
from typing import Any, Callable, Optional, TypeVar, Union
|
|
2
2
|
|
|
3
3
|
import solara
|
|
4
4
|
|
|
@@ -8,6 +8,7 @@ T = TypeVar("T")
|
|
|
8
8
|
def use_reactive(
|
|
9
9
|
value: Union[T, solara.Reactive[T]],
|
|
10
10
|
on_change: Optional[Callable[[T], None]] = None,
|
|
11
|
+
equals: Callable[[Any, Any], bool] = solara.util.equals_extra,
|
|
11
12
|
) -> solara.Reactive[T]:
|
|
12
13
|
"""Creates a reactive variable with the a local component scope.
|
|
13
14
|
|
|
@@ -44,6 +45,12 @@ def use_reactive(
|
|
|
44
45
|
* on_change (Optional[Callable[[T], None]]): An optional callback function
|
|
45
46
|
that will be called when the reactive variable's value changes.
|
|
46
47
|
|
|
48
|
+
* equals: A function that returns True if two values are considered equal, and False otherwise.
|
|
49
|
+
The default function is `solara.util.equals`, which performs a deep comparison of the two values
|
|
50
|
+
and is more forgiving than the default `==` operator.
|
|
51
|
+
You can provide a custom function if you need to define a different notion of equality.
|
|
52
|
+
|
|
53
|
+
|
|
47
54
|
Returns:
|
|
48
55
|
solara.Reactive[T]: A reactive variable with the specified initial value
|
|
49
56
|
or the provided reactive variable.
|