solara-ui 1.40.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 (66) hide show
  1. solara/__init__.py +1 -1
  2. solara/__main__.py +30 -11
  3. solara/_stores.py +185 -0
  4. solara/components/component_vue.py +26 -2
  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/assets/style.css +2 -0
  15. solara/server/kernel.py +2 -1
  16. solara/server/qt.py +113 -0
  17. solara/server/settings.py +1 -0
  18. solara/server/starlette.py +2 -2
  19. solara/server/static/main-vuetify.js +10 -0
  20. solara/server/static/solara_bootstrap.py +1 -1
  21. solara/server/templates/loader-solara.html +1 -1
  22. solara/server/templates/solara.html.j2 +6 -1
  23. solara/settings.py +14 -0
  24. solara/template/portal/pyproject.toml +1 -1
  25. solara/test/pytest_plugin.py +3 -0
  26. solara/toestand.py +139 -16
  27. solara/util.py +22 -0
  28. solara/website/components/markdown.py +45 -1
  29. solara/website/components/sidebar.py +3 -1
  30. solara/website/pages/__init__.py +13 -7
  31. solara/website/pages/changelog/changelog.md +9 -0
  32. solara/website/pages/documentation/advanced/content/20-understanding/40-routing.md +17 -1
  33. solara/website/pages/documentation/api/cross_filter/cross_filter_dataframe.py +4 -5
  34. solara/website/pages/documentation/api/cross_filter/cross_filter_report.py +3 -5
  35. solara/website/pages/documentation/api/cross_filter/cross_filter_select.py +3 -5
  36. solara/website/pages/documentation/api/cross_filter/cross_filter_slider.py +3 -5
  37. solara/website/pages/documentation/api/hooks/use_cross_filter.py +3 -5
  38. solara/website/pages/documentation/api/hooks/use_exception.py +9 -11
  39. solara/website/pages/documentation/api/hooks/use_previous.py +6 -9
  40. solara/website/pages/documentation/api/hooks/use_state_or_update.py +23 -26
  41. solara/website/pages/documentation/api/hooks/use_thread.py +11 -13
  42. solara/website/pages/documentation/api/utilities/on_kernel_start.py +17 -0
  43. solara/website/pages/documentation/components/input/input.py +22 -0
  44. solara/website/pages/documentation/components/viz/echarts.py +3 -1
  45. solara/website/pages/documentation/examples/__init__.py +13 -21
  46. solara/website/pages/documentation/examples/ai/chatbot.py +1 -1
  47. solara/website/pages/documentation/examples/general/pokemon_search.py +3 -3
  48. solara/website/pages/documentation/examples/general/vue_component.py +1 -1
  49. solara/website/pages/documentation/examples/libraries/altair.py +1 -0
  50. solara/website/pages/documentation/examples/libraries/bqplot.py +1 -1
  51. solara/website/pages/documentation/examples/libraries/ipyleaflet.py +1 -1
  52. solara/website/pages/documentation/examples/libraries/ipyleaflet_advanced.py +1 -1
  53. solara/website/pages/documentation/examples/utilities/countdown_timer.py +18 -20
  54. solara/website/pages/documentation/examples/visualization/annotator.py +1 -3
  55. solara/website/pages/documentation/examples/visualization/linked_views.py +3 -6
  56. solara/website/pages/documentation/getting_started/content/00-quickstart.md +19 -1
  57. solara/website/pages/documentation/getting_started/content/01-introduction.md +1 -1
  58. solara/website/pages/roadmap/roadmap.md +3 -0
  59. solara/widgets/vue/navigator.vue +46 -16
  60. solara/widgets/vue/vegalite.vue +18 -0
  61. {solara_ui-1.40.0.dist-info → solara_ui-1.42.0.dist-info}/METADATA +8 -5
  62. {solara_ui-1.40.0.dist-info → solara_ui-1.42.0.dist-info}/RECORD +66 -64
  63. {solara_ui-1.40.0.dist-info → solara_ui-1.42.0.dist-info}/WHEEL +1 -1
  64. {solara_ui-1.40.0.data → solara_ui-1.42.0.data}/data/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
  65. {solara_ui-1.40.0.data → solara_ui-1.42.0.data}/data/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
  66. {solara_ui-1.40.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.40.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
@@ -261,6 +261,12 @@ if "SOLARA_MODE" in os.environ:
261
261
  default=True,
262
262
  help="Check installed version again pypi version.",
263
263
  )
264
+ @click.option(
265
+ "--qt",
266
+ is_flag=True,
267
+ default=False,
268
+ help="Instead of opening a browser, open a Qt window. Will also stop the server when the window is closed. (experimental)",
269
+ )
264
270
  def run(
265
271
  app,
266
272
  host,
@@ -290,6 +296,7 @@ def run(
290
296
  ssg: bool,
291
297
  search: bool,
292
298
  check_version: bool = True,
299
+ qt=False,
293
300
  ):
294
301
  """Run a Solara app."""
295
302
  if dev is not None:
@@ -334,7 +341,13 @@ def run(
334
341
  reload_excludes = restart_excludes if restart_excludes else []
335
342
  del restart_excludes
336
343
  reload_excludes = [str(solara_root / "website"), str(solara_root / "template")]
337
- 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
338
351
  del solara_root
339
352
  reload = True
340
353
  # avoid sending many restarts
@@ -365,9 +378,16 @@ def run(
365
378
  while not failed and (server is None or not server.started):
366
379
  time.sleep(0.1)
367
380
  if not failed:
368
- webbrowser.open(url)
381
+ if qt:
382
+ from .server.qt import run_qt
369
383
 
370
- if open:
384
+ run_qt(url)
385
+ else:
386
+ webbrowser.open(url)
387
+
388
+ # with qt, we open the browser in the main thread (qt wants that)
389
+ # otherwise, we open the browser in a separate thread
390
+ if open and not qt:
371
391
  threading.Thread(target=open_browser, daemon=True).start()
372
392
 
373
393
  rich.print(f"Solara server is starting at {url}")
@@ -397,7 +417,7 @@ def run(
397
417
  settings.main.timing = timing
398
418
  items = (
399
419
  "theme_variant_user_selectable dark theme_variant theme_loader use_pdb server open_browser open url failed dev tracer"
400
- " timing ssg search check_version production".split()
420
+ " timing ssg search check_version production qt".split()
401
421
  )
402
422
  for item in items:
403
423
  del kwargs[item]
@@ -451,14 +471,13 @@ def run(
451
471
 
452
472
  build_index("")
453
473
 
454
- start_server()
455
-
456
474
  # TODO: if we want to use webview, it should be sth like this
457
- # server_thread = threading.Thread(target=start_server)
458
- # server_thread.start()
459
- # if open:
460
- # # open_webview()
461
- # open_browser()
475
+ if qt:
476
+ server_thread = threading.Thread(target=start_server, daemon=True)
477
+ server_thread.start()
478
+ open_browser()
479
+ else:
480
+ start_server()
462
481
  # server_thread.join()
463
482
 
464
483
 
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)
@@ -1,4 +1,5 @@
1
1
  import inspect
2
+ import os
2
3
  from typing import Any, Callable, Dict, Type
3
4
 
4
5
  import ipyvue as vue
@@ -48,10 +49,10 @@ def _widget_from_signature(classname, base_class: Type[widgets.Widget], func: Ca
48
49
  def _widget_vue(vue_path: str, vuetify=True) -> Callable[[Callable[P, None]], Type[v.VuetifyTemplate]]:
49
50
  def decorator(func: Callable[P, None]):
50
51
  class VuetifyWidgetSolara(v.VuetifyTemplate):
51
- template_file = (inspect.getfile(func), vue_path)
52
+ template_file = (os.path.abspath(inspect.getfile(func)), vue_path)
52
53
 
53
54
  class VueWidgetSolara(vue.VueTemplate):
54
- template_file = (inspect.getfile(func), vue_path)
55
+ template_file = (os.path.abspath(inspect.getfile(func)), vue_path)
55
56
 
56
57
  base_class = VuetifyWidgetSolara if vuetify else VueWidgetSolara
57
58
  widget_class = _widget_from_signature("VueWidgetSolaraSub", base_class, func, "vue_")
@@ -81,6 +82,29 @@ def component_vue(vue_path: str, vuetify=True) -> Callable[[Callable[P, None]],
81
82
 
82
83
  See the [Vue component example](/documentation/examples/general/vue_component) for an example of how to use this decorator.
83
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
+
84
108
  ## Arguments
85
109
 
86
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>