solara-ui 1.42.0__py2.py3-none-any.whl → 1.44.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 (71) hide show
  1. solara/__init__.py +1 -1
  2. solara/__main__.py +12 -7
  3. solara/_stores.py +128 -16
  4. solara/cache.py +6 -4
  5. solara/checks.py +1 -1
  6. solara/components/__init__.py +18 -1
  7. solara/components/datatable.py +4 -4
  8. solara/components/input.py +5 -1
  9. solara/components/markdown.py +46 -10
  10. solara/components/misc.py +2 -2
  11. solara/components/select.py +1 -1
  12. solara/components/style.py +1 -1
  13. solara/hooks/use_reactive.py +16 -1
  14. solara/lab/components/__init__.py +1 -0
  15. solara/lab/components/chat.py +15 -9
  16. solara/lab/components/input_time.py +133 -0
  17. solara/lab/hooks/dataframe.py +1 -0
  18. solara/lab/utils/dataframe.py +11 -1
  19. solara/server/app.py +66 -30
  20. solara/server/flask.py +12 -2
  21. solara/server/jupyter/server_extension.py +1 -0
  22. solara/server/kernel.py +50 -3
  23. solara/server/kernel_context.py +68 -9
  24. solara/server/patch.py +28 -30
  25. solara/server/server.py +16 -6
  26. solara/server/settings.py +11 -0
  27. solara/server/shell.py +19 -1
  28. solara/server/starlette.py +72 -14
  29. solara/server/static/solara_bootstrap.py +1 -1
  30. solara/settings.py +3 -0
  31. solara/tasks.py +30 -9
  32. solara/test/pytest_plugin.py +4 -2
  33. solara/toestand.py +119 -28
  34. solara/util.py +18 -0
  35. solara/website/components/docs.py +24 -1
  36. solara/website/components/markdown.py +17 -3
  37. solara/website/pages/changelog/changelog.md +26 -1
  38. solara/website/pages/documentation/advanced/content/10-howto/20-layout.md +1 -1
  39. solara/website/pages/documentation/advanced/content/20-understanding/50-solara-server.md +10 -0
  40. solara/website/pages/documentation/advanced/content/30-enterprise/10-oauth.md +4 -2
  41. solara/website/pages/documentation/api/routing/route.py +10 -12
  42. solara/website/pages/documentation/api/routing/use_route.py +26 -30
  43. solara/website/pages/documentation/components/advanced/link.py +6 -8
  44. solara/website/pages/documentation/components/advanced/meta.py +6 -9
  45. solara/website/pages/documentation/components/advanced/style.py +7 -9
  46. solara/website/pages/documentation/components/input/file_browser.py +12 -14
  47. solara/website/pages/documentation/components/lab/input_time.py +15 -0
  48. solara/website/pages/documentation/components/lab/theming.py +6 -4
  49. solara/website/pages/documentation/components/layout/columns_responsive.py +37 -39
  50. solara/website/pages/documentation/components/layout/gridfixed.py +4 -6
  51. solara/website/pages/documentation/components/output/html.py +1 -3
  52. solara/website/pages/documentation/components/output/sql_code.py +23 -25
  53. solara/website/pages/documentation/components/page/head.py +4 -7
  54. solara/website/pages/documentation/components/page/title.py +12 -14
  55. solara/website/pages/documentation/components/status/error.py +17 -18
  56. solara/website/pages/documentation/components/status/info.py +17 -18
  57. solara/website/pages/documentation/examples/__init__.py +10 -0
  58. solara/website/pages/documentation/examples/ai/chatbot.py +62 -44
  59. solara/website/pages/documentation/examples/general/live_update.py +22 -28
  60. solara/website/pages/documentation/examples/general/pokemon_search.py +1 -1
  61. solara/website/pages/documentation/faq/content/99-faq.md +9 -0
  62. solara/website/pages/documentation/getting_started/content/00-quickstart.md +1 -1
  63. solara/website/pages/documentation/getting_started/content/04-tutorials/_jupyter_dashboard_1.ipynb +23 -19
  64. solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md +2 -2
  65. solara/website/pages/roadmap/roadmap.md +3 -0
  66. {solara_ui-1.42.0.dist-info → solara_ui-1.44.0.dist-info}/METADATA +2 -2
  67. {solara_ui-1.42.0.dist-info → solara_ui-1.44.0.dist-info}/RECORD +71 -69
  68. {solara_ui-1.42.0.data → solara_ui-1.44.0.data}/data/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
  69. {solara_ui-1.42.0.data → solara_ui-1.44.0.data}/data/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
  70. {solara_ui-1.42.0.dist-info → solara_ui-1.44.0.dist-info}/WHEEL +0 -0
  71. {solara_ui-1.42.0.dist-info → solara_ui-1.44.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.42.0"
3
+ __version__ = "1.44.0"
4
4
  github_url = "https://github.com/widgetti/solara"
5
5
  git_branch = "master"
6
6
 
solara/__main__.py CHANGED
@@ -21,6 +21,8 @@ import solara.server.threaded
21
21
 
22
22
  from .server import telemetry
23
23
 
24
+ print_mutex = threading.Lock()
25
+
24
26
  try:
25
27
  from solara_enterprise.ssg import ssg_crawl
26
28
  except ImportError:
@@ -75,13 +77,15 @@ def _check_version():
75
77
  import requests
76
78
 
77
79
  try:
78
- response = requests.get("https://pypi.org/pypi/solara/json")
80
+ response = requests.get("https://pypi.org/pypi/solara/json", timeout=0.5)
79
81
  latest_version = response.json()["info"]["version"]
80
82
  except: # noqa: E722
83
+ # in case of a firewall, or timeout, we just abort
81
84
  return
82
85
  if latest_version != solara.__version__:
83
- print(f"New version of Solara available: {latest_version}. You have {solara.__version__}. Please upgrade using:") # noqa: T201
84
- print(f'\t$ pip install "solara=={latest_version}"') # noqa: T201
86
+ with print_mutex:
87
+ print(f"New version of Solara available: {latest_version}. You have {solara.__version__}. Please upgrade using:") # noqa: T201
88
+ print(f'\t$ pip install "solara=={latest_version}"') # noqa: T201
85
89
 
86
90
 
87
91
  def find_all_packages_paths():
@@ -152,7 +156,7 @@ if "SOLARA_MODE" in os.environ:
152
156
  "--restart-dir",
153
157
  "restart_dirs",
154
158
  multiple=True,
155
- help="Set restart directories explicitly, instead of using the current working" " directory.",
159
+ help="Set restart directories explicitly, instead of using the current working directory.",
156
160
  type=click.Path(exists=True),
157
161
  )
158
162
  @click.option(
@@ -168,7 +172,7 @@ if "SOLARA_MODE" in os.environ:
168
172
  "--workers",
169
173
  default=None,
170
174
  type=int,
171
- help="Number of worker processes. Defaults to the $WEB_CONCURRENCY environment" " variable if available, or 1. Not valid with --auto-restart/-a.",
175
+ help="Number of worker processes. Defaults to the $WEB_CONCURRENCY environment variable if available, or 1. Not valid with --auto-restart/-a.",
172
176
  )
173
177
  @click.option(
174
178
  "--env-file",
@@ -306,7 +310,7 @@ def run(
306
310
  print("solara: --reload is deprecated, use --auto-restart/-a instead", file=sys.stderr) # noqa: T201
307
311
  auto_restart = reload
308
312
  if check_version:
309
- _check_version()
313
+ threading.Thread(target=_check_version, daemon=True).run()
310
314
 
311
315
  # uvicorn calls it reload, we call it auto restart
312
316
  reload = auto_restart
@@ -390,7 +394,8 @@ def run(
390
394
  if open and not qt:
391
395
  threading.Thread(target=open_browser, daemon=True).start()
392
396
 
393
- rich.print(f"Solara server is starting at {url}")
397
+ with print_mutex:
398
+ rich.print(f"Solara server is starting at {url}")
394
399
 
395
400
  if log_level is not None:
396
401
  LOGGING_CONFIG["loggers"]["solara"]["level"] = log_level.upper()
solara/_stores.py CHANGED
@@ -1,27 +1,37 @@
1
1
  import copy
2
2
  import dataclasses
3
3
  import inspect
4
- from typing import Callable, ContextManager, Generic, Optional, Union, cast
4
+ import sys
5
+ import threading
6
+ from typing import Callable, ContextManager, Generic, Optional, Union, cast, Any
5
7
  import warnings
6
- from .toestand import ValueBase, KernelStore, S, _find_outside_solara_frame
8
+
9
+
10
+ from .toestand import ValueBase, S, _find_outside_solara_frame, _DEBUG
11
+
7
12
  import solara.util
13
+ import solara.settings
8
14
 
9
15
 
10
16
  class _PublicValueNotSet:
11
17
  pass
12
18
 
13
19
 
20
+ class _SetValueNotSet:
21
+ pass
22
+
23
+
14
24
  @dataclasses.dataclass
15
25
  class StoreValue(Generic[S]):
16
26
  private: S # the internal private value, should never be mutated
17
27
  public: Union[S, _PublicValueNotSet] # this is the value that is exposed in .get(), it is a deep copy of private
18
28
  get_traceback: Optional[inspect.Traceback]
19
- set_value: Optional[S] # the value that was set using .set(..), we deepcopy this to set private
29
+ set_value: Union[S, _SetValueNotSet] # the value that was set using .set(..), we deepcopy this to set private
20
30
  set_traceback: Optional[inspect.Traceback]
21
31
 
22
32
 
23
33
  class MutateDetectorStore(ValueBase[S]):
24
- def __init__(self, store: KernelStore[StoreValue[S]], equals=solara.util.equals_extra):
34
+ def __init__(self, store: ValueBase[StoreValue[S]], equals=solara.util.equals_extra):
25
35
  self._storage = store
26
36
  self._enabled = True
27
37
  super().__init__(equals=equals)
@@ -77,9 +87,9 @@ class MutateDetectorStore(ValueBase[S]):
77
87
  code = tb.code_context[0]
78
88
  else:
79
89
  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}"
90
+ msg += f"The last value was read in the following code:\n{tb.filename}:{tb.lineno}\n{code}"
81
91
  raise ValueError(msg)
82
- elif store_value.set_value is not None and not self.equals(store_value.set_value, store_value.private):
92
+ elif not isinstance(store_value.set_value, _SetValueNotSet) and not self.equals(store_value.set_value, store_value.private):
83
93
  tb = store_value.set_traceback
84
94
  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
95
 
@@ -110,7 +120,7 @@ Good (if you want to keep mutating your own list):
110
120
  code = tb.code_context[0]
111
121
  else:
112
122
  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}"
123
+ msg += f"The last time the value was set was at:\n{tb.filename}:{tb.lineno}\n{code}"
114
124
  raise ValueError(msg)
115
125
 
116
126
  def _ensure_public_exists(self):
@@ -154,18 +164,18 @@ reactive_df = solara.reactive(df, equals=solara.util.equals_pickle)
154
164
  code = tb.code_context[0]
155
165
  else:
156
166
  code = "<No code context available>"
157
- warn += "This warning was triggered from:\n" f"{tb.filename}:{tb.lineno}\n" f"{code}"
167
+ warn += f"This warning was triggered from:\n{tb.filename}:{tb.lineno}\n{code}"
158
168
  warnings.warn(warn)
159
169
  self._enabled = False
160
170
 
161
171
  def subscribe(self, listener: Callable[[S], None], scope: Optional[ContextManager] = None):
162
172
  def listener_wrapper(new: StoreValue[S], previous: StoreValue[S]):
163
173
  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
174
+ assert not isinstance(new.public, _PublicValueNotSet)
175
+ assert not isinstance(previous.public, _PublicValueNotSet)
176
+ previous_value = previous.set_value if not isinstance(previous.set_value, _SetValueNotSet) else previous.private
167
177
  new_value = new.set_value
168
- assert new_value is not None
178
+ assert not isinstance(new_value, _SetValueNotSet)
169
179
  if not self.equals(new_value, previous_value):
170
180
  listener(new_value)
171
181
 
@@ -174,12 +184,114 @@ reactive_df = solara.reactive(df, equals=solara.util.equals_pickle)
174
184
  def subscribe_change(self, listener: Callable[[S, S], None], scope: Optional[ContextManager] = None):
175
185
  def listener_wrapper(new: StoreValue[S], previous: StoreValue[S]):
176
186
  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
187
+ assert not isinstance(new.public, _PublicValueNotSet)
188
+ assert not isinstance(previous.public, _PublicValueNotSet)
189
+ previous_value = previous.set_value if not isinstance(previous.set_value, _SetValueNotSet) else previous.private
180
190
  new_value = new.set_value
181
- assert new_value is not None
191
+ assert not isinstance(new_value, _SetValueNotSet)
182
192
  if not self.equals(new_value, previous_value):
183
193
  listener(new_value, previous_value)
184
194
 
185
195
  return self._storage.subscribe_change(listener_wrapper, scope=scope)
196
+
197
+
198
+ class SharedStore(ValueBase[S]):
199
+ """Stores a single value, not kernel scoped."""
200
+
201
+ _traceback: Optional[inspect.Traceback]
202
+ _original_ref: Optional[S]
203
+ _original_ref_copy: Optional[S]
204
+
205
+ def __init__(self, value: S, equals: Callable[[Any, Any], bool] = solara.util.equals_extra, unwrap=lambda x: x):
206
+ # since a set can trigger events, which can trigger new updates, we need a recursive lock
207
+ self._lock = threading.RLock()
208
+ self.local = threading.local()
209
+ self.equals = equals
210
+
211
+ self._value = value
212
+ self._original_ref = None
213
+ self._original_ref_copy = None
214
+ self._unwrap = unwrap
215
+ self._mutation_detection = solara.settings.storage.mutation_detection
216
+ if self._mutation_detection:
217
+ frame = _find_outside_solara_frame()
218
+ if frame is not None:
219
+ self._traceback = inspect.getframeinfo(frame)
220
+ else:
221
+ self._traceback = None
222
+ self._original_ref = value
223
+ self._original_ref_copy = copy.deepcopy(self._original_ref)
224
+ if not self.equals(self._unwrap(self._original_ref), self._unwrap(self._original_ref_copy)):
225
+ msg = """The equals function for this reactive value returned False when comparing a deepcopy to itself.
226
+
227
+ This reactive variable will not be able to detect mutations correctly, and is therefore disabled.
228
+
229
+ To avoid this warning, and to ensure that mutation detection works correctly, please provide a better equals function to the reactive variable.
230
+ 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.
231
+
232
+ Example:
233
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
234
+ reactive_df = solara.reactive(df, equals=solara.util.equals_pickle)
235
+ """
236
+ tb = self._traceback
237
+ if tb:
238
+ if tb.code_context:
239
+ code = tb.code_context[0]
240
+ else:
241
+ code = "<No code context available>"
242
+ msg += f"This warning was triggered from:\n{tb.filename}:{tb.lineno}\n{code}"
243
+ warnings.warn(msg)
244
+ self._mutation_detection = False
245
+ super().__init__(equals=equals)
246
+
247
+ def _check_mutation(self):
248
+ if not self._mutation_detection:
249
+ return
250
+ current = self._unwrap(self._original_ref)
251
+ initial = self._unwrap(self._original_ref_copy)
252
+ if not self.equals(initial, current):
253
+ tb = self._traceback
254
+ if tb:
255
+ if tb.code_context:
256
+ code = tb.code_context[0].strip()
257
+ else:
258
+ code = "No code context available"
259
+ msg = f"Reactive variable was initialized at {tb.filename}:{tb.lineno} with {initial!r}, but was mutated to {current!r}.\n{code}"
260
+ else:
261
+ msg = f"Reactive variable was initialized with a value of {initial!r}, but was mutated to {current!r} (unable to report the location in the source code)."
262
+ raise ValueError(msg)
263
+
264
+ @property
265
+ def lock(self):
266
+ return self._lock
267
+
268
+ def peek(self):
269
+ self._check_mutation()
270
+ return self._value
271
+
272
+ def get(self):
273
+ self._check_mutation()
274
+ return self._value
275
+
276
+ def clear(self):
277
+ pass
278
+
279
+ def _get_scope_key(self):
280
+ return "GLOBAL"
281
+
282
+ def set(self, value: S):
283
+ self._check_mutation()
284
+ old = self.get()
285
+ if self.equals(old, value):
286
+ return
287
+ self._value = value
288
+
289
+ if _DEBUG:
290
+ import traceback
291
+
292
+ traceback.print_stack(limit=17, file=sys.stdout)
293
+
294
+ print("change old", old) # noqa
295
+ print("change new", value) # noqa
296
+
297
+ self.fire(value, old)
solara/cache.py CHANGED
@@ -1,3 +1,4 @@
1
+ from collections.abc import Hashable
1
2
  import hashlib
2
3
  import inspect
3
4
  import logging
@@ -50,6 +51,7 @@ if has_cachetools:
50
51
  class Memory(cachetools.LRUCache):
51
52
  def __init__(self, max_items=solara.settings.cache.memory_max_items):
52
53
  super().__init__(maxsize=max_items)
54
+
53
55
  else:
54
56
 
55
57
  class Memory(dict): # type: ignore
@@ -64,7 +66,7 @@ def _default_key(*args, **kwargs):
64
66
 
65
67
 
66
68
  class MemoizedFunction(Generic[P, R]):
67
- def __init__(self, function: Callable[P, R], key: Callable[P, R], storage: Optional[Storage], allow_nonlocals=False):
69
+ def __init__(self, function: Callable[P, R], key: Callable[P, Hashable], storage: Optional[Storage], allow_nonlocals=False):
68
70
  self.function = function
69
71
  f: Callable = self.function
70
72
  if not allow_nonlocals:
@@ -170,7 +172,7 @@ def memoize(
170
172
  @overload
171
173
  def memoize(
172
174
  function: None = None,
173
- key: Callable[P, R] = ...,
175
+ key: Callable[P, Hashable] = ...,
174
176
  storage: Optional[Storage] = None,
175
177
  allow_nonlocals=False,
176
178
  ) -> Callable[[Callable[P, R]], MemoizedFunction[P, R]]: ...
@@ -187,7 +189,7 @@ def memoize(
187
189
 
188
190
  def memoize(
189
191
  function: Union[None, Callable[P, R]] = None,
190
- key: Union[None, Callable[P, R]] = None,
192
+ key: Union[None, Callable[P, Hashable]] = None,
191
193
  storage: Optional[Storage] = None,
192
194
  allow_nonlocals: bool = False,
193
195
  ) -> Union[Callable[[Callable[P, R]], MemoizedFunction[P, R]], MemoizedFunction[P, R]]:
@@ -249,7 +251,7 @@ def memoize(
249
251
  def wrapper(func: Callable[P, R]) -> MemoizedFunction[P, R]:
250
252
  return MemoizedFunction[P, R](
251
253
  func,
252
- cast(Callable[P, R], key or _default_key),
254
+ cast(Callable[P, Hashable], key or _default_key),
253
255
  storage,
254
256
  allow_nonlocals,
255
257
  )
solara/checks.py CHANGED
@@ -164,7 +164,7 @@ def get_server_python_executable(silent: bool = False):
164
164
  else:
165
165
  python = pythons[0]
166
166
  if not silent:
167
- warnings.warn("Found multiple find servers:\n%s\n" "We are assuming the server is running under Python executable: %s" % (info, python))
167
+ warnings.warn(f"Found multiple find servers:\n{info}\nWe are assuming the server is running under Python executable: {python}")
168
168
  else:
169
169
  python = pythons[0]
170
170
  return python
@@ -57,4 +57,21 @@ from .progress import ProgressLinear # noqa: F401 F403
57
57
  from .component_vue import _component_vue, component_vue # noqa: F401 F403
58
58
  import reacton.core
59
59
 
60
- reacton.core._default_container = Column # noqa: F405
60
+ try:
61
+ from reacton import Fragment as Fragment # type: ignore
62
+ except ImportError:
63
+ pass
64
+
65
+ import logging
66
+ from ..settings import main
67
+
68
+ _container = None
69
+
70
+ if main.default_container in globals():
71
+ _container = globals()[main.default_container]
72
+ else:
73
+ logger = logging.getLogger("solara.components")
74
+ logger.warning(f"Default container {main.default_container} not found in solara.components. Defaulting to Column.")
75
+
76
+ # TODO: When Solara 2.0 releases Column should be replaced with Fragment
77
+ reacton.core._default_container = _container or Column # noqa: F405
@@ -10,14 +10,14 @@ import solara
10
10
  import solara.hooks.dataframe
11
11
  import solara.lab
12
12
  import traitlets
13
- from solara.lab.hooks.dataframe import use_df_column_names
13
+ from solara.lab.hooks.dataframe import use_df_column_names, df_row_names
14
14
  from solara.lab.utils.dataframe import df_len, df_records, df_slice
15
15
 
16
16
  from .. import CellAction, ColumnAction
17
17
 
18
18
 
19
19
  def _ensure_dict(d):
20
- if dataclasses.is_dataclass(d):
20
+ if dataclasses.is_dataclass(d) and not isinstance(d, type): # is_dataclass also returns True for dataclass type, rather than instance
21
21
  return dataclasses.asdict(d)
22
22
  return d
23
23
 
@@ -100,12 +100,12 @@ def DataTable(
100
100
  i2 = min(total_length, (page + 1) * items_per_page)
101
101
 
102
102
  columns = use_df_column_names(df)
103
-
103
+ rows = df_row_names(df)
104
104
  items = []
105
105
  dfs = df_slice(df, i1, i2)
106
106
  records = df_records(dfs)
107
107
  for i in range(i2 - i1):
108
- item = {"__row__": i + i1} # special key for the row number
108
+ item = {"__row__": format(dfs, columns, i + 1, rows[i + i1])} # special key for the row number
109
109
  for column in columns:
110
110
  item[column] = format(dfs, column, i + i1, records[i][column])
111
111
  items.append(item)
@@ -373,6 +373,10 @@ def _use_input_type(
373
373
  error_message = str(e.args[0])
374
374
 
375
375
  def sync_back_input_value():
376
+ # Make sure we update string_value when the effect is rerun,
377
+ # Since the parsing & stringigying functions might have changed
378
+ set_string_value(stringify(reactive_value.value) if reactive_value.value is not None else None)
379
+
376
380
  def on_external_value_change(new_value: Optional[T]):
377
381
  new_string_value = stringify(new_value)
378
382
  try:
@@ -386,7 +390,7 @@ def _use_input_type(
386
390
 
387
391
  return reactive_value.subscribe(on_external_value_change)
388
392
 
389
- solara.use_effect(sync_back_input_value, [reactive_value])
393
+ solara.use_effect(sync_back_input_value, [reactive_value, parse, stringify])
390
394
 
391
395
  return string_value, error_message, set_string_value
392
396
 
@@ -1,11 +1,10 @@
1
- import functools
2
1
  import hashlib
3
2
  import html
4
3
  import logging
5
4
  import textwrap
6
5
  import traceback
7
6
  import warnings
8
- from typing import Any, Dict, List, Optional, Union, cast
7
+ from typing import Any, Callable, Dict, List, Optional, Union, cast
9
8
  import typing
10
9
 
11
10
  import ipyvuetify as v
@@ -18,6 +17,7 @@ try:
18
17
  has_pymdownx = True
19
18
  except ModuleNotFoundError:
20
19
  has_pymdownx = False
20
+ import reacton.core
21
21
 
22
22
  import solara
23
23
  import solara.components.applayout
@@ -55,7 +55,7 @@ def ExceptionGuard(children=[]):
55
55
  solara.Column(children=children)
56
56
 
57
57
 
58
- def _run_solara(code):
58
+ def _run_solara(code, cleanups):
59
59
  ast = compile(code, "markdown", "exec")
60
60
  local_scope: Dict[Any, Any] = {}
61
61
  exec(ast, local_scope)
@@ -68,6 +68,13 @@ def _run_solara(code):
68
68
  else:
69
69
  raise NameError("No Page or app defined")
70
70
  box = v.Html(tag="div")
71
+
72
+ rc: reacton.core.RenderContext
73
+
74
+ def cleanup():
75
+ rc.close()
76
+
77
+ cleanups.append(cleanup)
71
78
  box, rc = solara.render(cast(solara.Element, app), container=box) # type: ignore
72
79
  widget_id = box._model_id
73
80
  return (
@@ -236,9 +243,8 @@ module.exports = {
236
243
  return template
237
244
 
238
245
 
239
- def _highlight(src, language, unsafe_solara_execute, *args, **kwargs):
246
+ def _highlight(src, language, class_name=None, options=None, md=None, unsafe_solara_execute=False, cleanups=None, **kwargs):
240
247
  """Highlight a block of code"""
241
-
242
248
  if not has_pygments:
243
249
  warnings.warn("Pygments is not installed, code highlighting will not work, use pip install pygments to install it.")
244
250
  src_safe = html.escape(src)
@@ -255,7 +261,7 @@ def _highlight(src, language, unsafe_solara_execute, *args, **kwargs):
255
261
 
256
262
  if run_src_with_solara:
257
263
  if unsafe_solara_execute:
258
- html_widget = _run_solara(src)
264
+ html_widget = _run_solara(src, cleanups)
259
265
  return src_html + html_widget
260
266
  else:
261
267
  return src_html + html_no_execute_enabled
@@ -263,8 +269,17 @@ def _highlight(src, language, unsafe_solara_execute, *args, **kwargs):
263
269
  return src_html
264
270
 
265
271
 
266
- def formatter(unsafe_solara_execute: bool):
267
- return functools.partial(_highlight, unsafe_solara_execute=unsafe_solara_execute)
272
+ def formatter(unsafe_solara_execute: bool, cleanups: List[Callable[[], None]]):
273
+ def wrapper(*args, **kwargs):
274
+ try:
275
+ kwargs["unsafe_solara_execute"] = unsafe_solara_execute
276
+ kwargs["cleanups"] = cleanups
277
+ return _highlight(*args, **kwargs)
278
+ except Exception as e:
279
+ logger.exception("Error while highlighting code")
280
+ raise e
281
+
282
+ return wrapper
268
283
 
269
284
 
270
285
  @solara.component
@@ -276,8 +291,10 @@ def MarkdownIt(md_text: str, highlight: List[int] = [], unsafe_solara_execute: b
276
291
  from mdit_py_plugins.footnote import footnote_plugin # noqa: F401
277
292
  from mdit_py_plugins.front_matter import front_matter_plugin # noqa: F401
278
293
 
294
+ cleanups = solara.use_ref(cast(List[Callable[[], None]], []))
295
+
279
296
  def highlight_code(code, name, attrs):
280
- return _highlight(code, name, unsafe_solara_execute, attrs)
297
+ return _highlight(cleanups.current, code, name, unsafe_solara_execute, attrs)
281
298
 
282
299
  md = MarkdownItMod(
283
300
  "js-default",
@@ -290,6 +307,15 @@ def MarkdownIt(md_text: str, highlight: List[int] = [], unsafe_solara_execute: b
290
307
  md = md.use(container.container_plugin, name="note")
291
308
  html = md.render(md_text)
292
309
  hash = hashlib.sha256((html + str(unsafe_solara_execute) + repr(highlight)).encode("utf-8")).hexdigest()
310
+
311
+ def cleanup_wrapper():
312
+ def cleanup():
313
+ for cleanup in cleanups.current:
314
+ cleanup()
315
+
316
+ return cleanup
317
+
318
+ solara.use_effect(cleanup_wrapper)
293
319
  return v.VuetifyTemplate.element(template=_markdown_template(html)).key(hash)
294
320
 
295
321
 
@@ -349,6 +375,7 @@ def Markdown(md_text: str, unsafe_solara_execute=False, style: Union[str, Dict,
349
375
 
350
376
  md_text = textwrap.dedent(md_text)
351
377
  style = solara.util._flatten_style(style)
378
+ cleanups = solara.use_ref(cast(List[Callable[[], None]], []))
352
379
 
353
380
  def make_markdown_object():
354
381
  if md_parser is not None:
@@ -377,7 +404,7 @@ def Markdown(md_text: str, unsafe_solara_execute=False, style: Union[str, Dict,
377
404
  {
378
405
  "name": "solara",
379
406
  "class": "",
380
- "format": formatter(unsafe_solara_execute),
407
+ "format": formatter(unsafe_solara_execute, cleanups=cleanups.current),
381
408
  },
382
409
  ],
383
410
  },
@@ -399,6 +426,15 @@ def Markdown(md_text: str, unsafe_solara_execute=False, style: Union[str, Dict,
399
426
  assert md_self is not None
400
427
  md_parser = md_self
401
428
  html = md_parser.convert(md_text)
429
+
430
+ def cleanup_wrapper():
431
+ def cleanup():
432
+ for cleanup in cleanups.current:
433
+ cleanup()
434
+
435
+ return cleanup
436
+
437
+ solara.use_effect(cleanup_wrapper, [])
402
438
  # if we update the template value, the whole vue tree will rerender (ipvue/ipyvuetify issue)
403
439
  # however, using the hash we simply generate a new widget each time
404
440
  hash = hashlib.sha256((html + str(unsafe_solara_execute)).encode("utf-8")).hexdigest()
solara/components/misc.py CHANGED
@@ -136,7 +136,7 @@ def HTML(tag="div", unsafe_innerHTML=None, style: str = None, classes: List[str]
136
136
 
137
137
  @solara.component
138
138
  def VBox(children=[], grow=True, align_items="stretch", classes: List[str] = []):
139
- """Deprecated. Use `Row` instead."""
139
+ """Deprecated. Use `Column` instead."""
140
140
  style = f"flex-direction: column; align-items: {align_items};"
141
141
  if grow:
142
142
  style += "flex-grow: 1;"
@@ -146,7 +146,7 @@ def VBox(children=[], grow=True, align_items="stretch", classes: List[str] = [])
146
146
 
147
147
  @solara.component
148
148
  def HBox(children=[], grow=True, align_items="stretch", classes: List[str] = []):
149
- """Deprecated. Use `Column` instead."""
149
+ """Deprecated. Use `Row` instead."""
150
150
  style = f"flex-direction: row; align-items: {align_items}; "
151
151
  if grow:
152
152
  style += "flex-grow: 1;"
@@ -174,7 +174,7 @@ def SelectMultiple(
174
174
  items=all_values,
175
175
  label=label,
176
176
  multiple=True,
177
- dense=False,
177
+ dense=dense,
178
178
  disabled=disabled,
179
179
  class_=class_,
180
180
  style_=style_flat,
@@ -101,5 +101,5 @@ module.exports = {
101
101
  {css_content}
102
102
  </style>
103
103
  """
104
- # using .key avoids re-using the template, which causes a flicker (due to ipyvue)
104
+ # using .key avoids reusing the template, which causes a flicker (due to ipyvue)
105
105
  return v.VuetifyTemplate.element(template=template).key(key)
@@ -1,6 +1,7 @@
1
1
  from typing import Any, Callable, Optional, TypeVar, Union
2
2
 
3
3
  import solara
4
+ import solara.settings
4
5
 
5
6
  T = TypeVar("T")
6
7
 
@@ -105,7 +106,21 @@ def use_reactive(
105
106
 
106
107
  def create():
107
108
  if not isinstance(value, solara.Reactive):
108
- return solara.reactive(value)
109
+ from solara._stores import SharedStore, MutateDetectorStore, StoreValue, _PublicValueNotSet, _SetValueNotSet
110
+ from solara.toestand import ValueBase
111
+
112
+ store: ValueBase[T]
113
+
114
+ if solara.settings.storage.mutation_detection is True:
115
+ shared_store = SharedStore[StoreValue[T]](
116
+ StoreValue[T](private=value, public=_PublicValueNotSet(), get_traceback=None, set_value=_SetValueNotSet(), set_traceback=None),
117
+ unwrap=lambda x: x.private,
118
+ )
119
+ store = MutateDetectorStore[T](shared_store, equals=equals)
120
+ else:
121
+ store = SharedStore(value, equals=equals)
122
+
123
+ return solara.Reactive(store)
109
124
 
110
125
  reactive_value = solara.use_memo(create, dependencies=[])
111
126
  if isinstance(value, solara.Reactive):
@@ -1,6 +1,7 @@
1
1
  from .chat import ChatBox, ChatInput, ChatMessage # noqa: F401
2
2
  from .confirmation_dialog import ConfirmationDialog # noqa: F401
3
3
  from .input_date import InputDate, InputDateRange # noqa: F401
4
+ from .input_time import InputTime as InputTime
4
5
  from .menu import ClickMenu, ContextMenu, Menu # noqa: F401 F403
5
6
  from .tabs import Tab, Tabs # noqa: F401
6
7
  from .theming import ThemeToggle, theme, use_dark_effective # noqa: F401