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.
Files changed (47) hide show
  1. solara/__init__.py +1 -1
  2. solara/__main__.py +7 -1
  3. solara/_stores.py +185 -0
  4. solara/components/component_vue.py +23 -0
  5. solara/components/echarts.py +5 -2
  6. solara/components/echarts.vue +22 -5
  7. solara/components/file_drop.py +20 -0
  8. solara/components/input.py +16 -0
  9. solara/components/markdown.py +22 -13
  10. solara/components/spinner-solara.vue +2 -2
  11. solara/components/spinner.py +17 -2
  12. solara/hooks/use_reactive.py +8 -1
  13. solara/reactive.py +9 -3
  14. solara/server/kernel.py +2 -1
  15. solara/server/qt.py +1 -1
  16. solara/server/starlette.py +2 -2
  17. solara/server/static/solara_bootstrap.py +1 -1
  18. solara/settings.py +14 -0
  19. solara/template/portal/pyproject.toml +1 -1
  20. solara/test/pytest_plugin.py +3 -0
  21. solara/toestand.py +139 -16
  22. solara/util.py +22 -0
  23. solara/website/components/markdown.py +45 -1
  24. solara/website/pages/changelog/changelog.md +9 -0
  25. solara/website/pages/documentation/api/cross_filter/cross_filter_dataframe.py +4 -5
  26. solara/website/pages/documentation/api/cross_filter/cross_filter_report.py +3 -5
  27. solara/website/pages/documentation/api/cross_filter/cross_filter_select.py +3 -5
  28. solara/website/pages/documentation/api/cross_filter/cross_filter_slider.py +3 -5
  29. solara/website/pages/documentation/api/hooks/use_cross_filter.py +3 -5
  30. solara/website/pages/documentation/api/hooks/use_exception.py +9 -11
  31. solara/website/pages/documentation/api/hooks/use_previous.py +6 -9
  32. solara/website/pages/documentation/api/hooks/use_state_or_update.py +23 -26
  33. solara/website/pages/documentation/api/hooks/use_thread.py +11 -13
  34. solara/website/pages/documentation/api/utilities/on_kernel_start.py +17 -0
  35. solara/website/pages/documentation/components/input/input.py +22 -0
  36. solara/website/pages/documentation/components/viz/echarts.py +3 -1
  37. solara/website/pages/documentation/examples/general/pokemon_search.py +3 -3
  38. solara/website/pages/documentation/examples/visualization/linked_views.py +0 -3
  39. solara/website/pages/documentation/getting_started/content/00-quickstart.md +1 -1
  40. solara/website/pages/documentation/getting_started/content/01-introduction.md +1 -1
  41. solara/website/pages/roadmap/roadmap.md +3 -0
  42. {solara_ui-1.41.0.dist-info → solara_ui-1.42.0.dist-info}/METADATA +8 -5
  43. {solara_ui-1.41.0.dist-info → solara_ui-1.42.0.dist-info}/RECORD +47 -46
  44. {solara_ui-1.41.0.dist-info → solara_ui-1.42.0.dist-info}/WHEEL +1 -1
  45. {solara_ui-1.41.0.data → solara_ui-1.42.0.data}/data/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
  46. {solara_ui-1.41.0.data → solara_ui-1.42.0.data}/data/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
  47. {solara_ui-1.41.0.dist-info → solara_ui-1.42.0.dist-info}/licenses/LICENSE +0 -0
solara/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Build webapps using IPywidgets"""
2
2
 
3
- __version__ = "1.41.0"
3
+ __version__ = "1.42.0"
4
4
  github_url = "https://github.com/widgetti/solara"
5
5
  git_branch = "master"
6
6
 
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
- reload_excludes.append(app)
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.
@@ -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
  )
@@ -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) {
@@ -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)
@@ -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
@@ -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, extra, *args, **kwargs):
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
- def highlight(src, language, *args, **kwargs):
345
- try:
346
- return _highlight(src, language, unsafe_solara_execute, *args, **kwargs)
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": highlight,
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
- md = solara.use_memo(make_markdown_object, dependencies=[unsafe_solara_execute])
392
- html = md.convert(md_text)
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="#FFCF64"></path>
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="#FF8C3E"></path>
11
+ :fill="color_front"></path>
12
12
  </svg>
13
13
  </div>
14
14
  </template>
@@ -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)
@@ -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.