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/tasks.py CHANGED
@@ -1,3 +1,4 @@
1
+ import sys
1
2
  import abc
2
3
  import asyncio
3
4
  import dataclasses
@@ -27,6 +28,12 @@ from solara.toestand import Singleton
27
28
 
28
29
  from .toestand import Ref as ref
29
30
 
31
+ if sys.version_info >= (3, 8):
32
+ from typing import Literal
33
+ else:
34
+ from typing_extensions import Literal
35
+
36
+
30
37
  R = TypeVar("R")
31
38
  T = TypeVar("T")
32
39
  P = typing_extensions.ParamSpec("P")
@@ -97,6 +104,9 @@ class Task(Generic[P, R], abc.ABC):
97
104
  self._progress = ref(self._result.fields.progress)
98
105
  self._exception = ref(self._result.fields.exception)
99
106
  self._state_ = ref(self._result.fields._state)
107
+ # used for tests only
108
+ self._start_event = threading.Event()
109
+ self._start_event.set()
100
110
 
101
111
  @property
102
112
  def result(self) -> TaskResult[R]:
@@ -246,6 +256,8 @@ class TaskAsyncio(Task[P, R]):
246
256
  return (self.current_task == asyncio.current_task()) and not running_task.cancelled()
247
257
 
248
258
  async def _async_run(self, call_event_loop: asyncio.AbstractEventLoop, future: asyncio.Future, args, kwargs) -> None:
259
+ self._start_event.wait()
260
+
249
261
  task_for_this_call = asyncio.current_task()
250
262
  assert task_for_this_call is not None
251
263
 
@@ -297,6 +309,7 @@ class TaskThreaded(Task[P, R]):
297
309
  self.__qualname__ = function.__qualname__
298
310
  self.function = function
299
311
  self.lock = threading.Lock()
312
+ self._local = threading.local()
300
313
 
301
314
  def cancel(self) -> None:
302
315
  if self._cancel:
@@ -336,12 +349,17 @@ class TaskThreaded(Task[P, R]):
336
349
  current_thread.start()
337
350
 
338
351
  def is_current(self):
352
+ cancel_event = getattr(self._local, "cancel_event", None)
353
+ if cancel_event is not None and cancel_event.is_set():
354
+ return False
339
355
  return self._current_thread == threading.current_thread()
340
356
 
341
357
  def _run(self, _last_finished_event, previous_thread: Optional[threading.Thread], cancel_event, args, kwargs) -> None:
342
358
  # use_thread has this as default, which can make code run 10x slower
359
+ self._start_event.wait()
343
360
  intrusive_cancel = False
344
361
  wait_on_previous = False
362
+ self._local.cancel_event = cancel_event
345
363
 
346
364
  def runner():
347
365
  if wait_on_previous:
@@ -398,7 +416,7 @@ class TaskThreaded(Task[P, R]):
398
416
  # this means this thread is cancelled not be request, but because
399
417
  # a new thread is running, we can ignore this
400
418
  finally:
401
- if self.is_current():
419
+ if self._current_thread == threading.current_thread():
402
420
  self.running_thread = None
403
421
  logger.info("thread done!")
404
422
  if cancel_event.is_set():
@@ -686,24 +704,27 @@ def task(
686
704
  return wrapper(f)
687
705
 
688
706
 
707
+ # Quotes around Task[...] are needed in Python <= 3.9, since ParamSpec doesn't properly support non-type arguments
708
+ # i.e. [] is taken as a value instead of a type
709
+ # See https://github.com/python/typing_extensions/issues/126 and related issues
689
710
  @overload
690
711
  def use_task(
691
712
  f: None = None,
692
713
  *,
693
- dependencies: None = ...,
714
+ dependencies: Literal[None] = ...,
694
715
  raise_error=...,
695
716
  prefer_threaded=...,
696
- ) -> Callable[[Callable[P, R]], Task[P, R]]: ...
717
+ ) -> Callable[[Callable[[], R]], "Task[[], R]"]: ...
697
718
 
698
719
 
699
720
  @overload
700
721
  def use_task(
701
- f: Callable[P, R],
722
+ f: Callable[[], R],
702
723
  *,
703
- dependencies: None = ...,
724
+ dependencies: Literal[None] = ...,
704
725
  raise_error=...,
705
726
  prefer_threaded=...,
706
- ) -> Task[P, R]: ...
727
+ ) -> "Task[[], R]": ...
707
728
 
708
729
 
709
730
  @overload
@@ -727,12 +748,12 @@ def use_task(
727
748
 
728
749
 
729
750
  def use_task(
730
- f: Union[None, Callable[P, R]] = None,
751
+ f: Union[None, Callable[[], R]] = None,
731
752
  *,
732
753
  dependencies: Union[None, List] = [],
733
754
  raise_error=True,
734
755
  prefer_threaded=True,
735
- ) -> Union[Callable[[Callable[P, R]], Task[P, R]], Task[P, R]]:
756
+ ) -> Union[Callable[[Callable[[], R]], "Task[[], R]"], "Task[[], R]"]:
736
757
  """A hook that runs a function or coroutine function as a task and returns the result.
737
758
 
738
759
  Allows you to run code in the background, with the UI available to the user. This is useful for long running tasks,
@@ -811,7 +832,7 @@ def use_task(
811
832
  """
812
833
 
813
834
  def wrapper(f):
814
- def create_task() -> Task[P, R]:
835
+ def create_task() -> "Task[[], R]":
815
836
  return task(f, prefer_threaded=prefer_threaded)
816
837
 
817
838
  task_instance = solara.use_memo(create_task, dependencies=[])
@@ -101,7 +101,7 @@ def context_session(
101
101
  if capture_screenshot:
102
102
  for index, page in enumerate(pages):
103
103
  human_readable_status = "failed" if failed else "finished"
104
- screenshot_path = _build_artifact_test_folder(pytestconfig, request, f"test-{human_readable_status}-{index+1}.png")
104
+ screenshot_path = _build_artifact_test_folder(pytestconfig, request, f"test-{human_readable_status}-{index + 1}.png")
105
105
  try:
106
106
  page.screenshot(timeout=5000, path=screenshot_path)
107
107
  except Error:
@@ -144,12 +144,14 @@ def solara_app(solara_server):
144
144
  used_app = None
145
145
 
146
146
  @contextlib.contextmanager
147
- def run(app: Union[solara.server.app.AppScript, str]):
147
+ def run(app: Union[solara.server.app.AppScript, str], init=True):
148
148
  nonlocal used_app
149
149
  if "__default__" in solara.server.app.apps:
150
150
  solara.server.app.apps["__default__"].close()
151
151
  if isinstance(app, str):
152
152
  app = solara.server.app.AppScript(app)
153
+ if init:
154
+ app.init()
153
155
  used_app = app
154
156
  solara.server.app.apps["__default__"] = app
155
157
  try:
solara/toestand.py CHANGED
@@ -1,4 +1,3 @@
1
- import contextlib
2
1
  import dataclasses
3
2
  import inspect
4
3
  import logging
@@ -33,6 +32,7 @@ from solara.util import equals_extra
33
32
  import solara
34
33
  import solara.settings
35
34
  from solara import _using_solara_server
35
+ from solara.util import nullcontext
36
36
 
37
37
  T = TypeVar("T")
38
38
  TS = TypeVar("TS")
@@ -98,6 +98,37 @@ class ValueBase(Generic[T]):
98
98
  self.listeners: Dict[str, Set[Tuple[Callable[[T], None], Optional[ContextManager]]]] = defaultdict(set)
99
99
  self.listeners2: Dict[str, Set[Tuple[Callable[[T, T], None], Optional[ContextManager]]]] = defaultdict(set)
100
100
 
101
+ # make sure all boolean operations give type errors
102
+ if not solara.settings.main.allow_reactive_boolean:
103
+
104
+ def __bool__(self):
105
+ raise TypeError("Reactive vars are not allowed in boolean expressions, did you mean to use .value?")
106
+
107
+ def __eq__(self, other):
108
+ raise TypeError(f"'==' not supported between a Reactive and {other.__class__.__name__}, did you mean to use .value?")
109
+
110
+ def __ne__(self, other):
111
+ raise TypeError(f"'!=' not supported between a Reactive and {other.__class__.__name__}, did you mean to use .value?")
112
+
113
+ # If we explicitly define __eq__, we need to explicitly define __hash__ as well
114
+ # Otherwise our class is marked unhashable
115
+ __hash__ = object.__hash__
116
+
117
+ def __lt__(self, other):
118
+ raise TypeError(f"'<' not supported between a Reactive and {other.__class__.__name__}, did you mean to use .value?")
119
+
120
+ def __le__(self, other):
121
+ raise TypeError(f"'<=' not supported between a Reactive and {other.__class__.__name__}, did you mean to use .value?")
122
+
123
+ def __gt__(self, other):
124
+ raise TypeError(f"'>' not supported between a Reactive and {other.__class__.__name__}, did you mean to use .value?")
125
+
126
+ def __ge__(self, other):
127
+ raise TypeError(f"'>=' not supported between a Reactive and {other.__class__.__name__}, did you mean to use .value?")
128
+
129
+ def __len__(self):
130
+ raise TypeError("'len(...)' is not supported for a Reactive, did you mean to use .value?")
131
+
101
132
  @property
102
133
  def lock(self):
103
134
  raise NotImplementedError
@@ -123,40 +154,63 @@ class ValueBase(Generic[T]):
123
154
  raise NotImplementedError
124
155
 
125
156
  def subscribe(self, listener: Callable[[T], None], scope: Optional[ContextManager] = None):
157
+ if scope is not None:
158
+ warnings.warn("scope argument should not be used, it was only for internal use")
159
+ del scope
126
160
  scope_id = self._get_scope_key()
127
- self.listeners[scope_id].add((listener, scope))
161
+ rc = reacton.core.get_render_context(required=False)
162
+ if _using_solara_server():
163
+ import solara.server.kernel_context
164
+
165
+ kernel = solara.server.kernel_context.get_current_context() if solara.server.kernel_context.has_current_context() else nullcontext()
166
+ else:
167
+ kernel = nullcontext()
168
+ context = Context(rc, kernel)
169
+
170
+ self.listeners[scope_id].add((listener, context))
128
171
 
129
172
  def cleanup():
130
- self.listeners[scope_id].remove((listener, scope))
173
+ self.listeners[scope_id].remove((listener, context))
131
174
 
132
175
  return cleanup
133
176
 
134
177
  def subscribe_change(self, listener: Callable[[T, T], None], scope: Optional[ContextManager] = None):
178
+ if scope is not None:
179
+ warnings.warn("scope argument should not be used, it was only for internal use")
180
+ del scope
135
181
  scope_id = self._get_scope_key()
136
- self.listeners2[scope_id].add((listener, scope))
182
+ rc = reacton.core.get_render_context(required=False)
183
+ if _using_solara_server():
184
+ import solara.server.kernel_context
185
+
186
+ kernel = solara.server.kernel_context.get_current_context() if solara.server.kernel_context.has_current_context() else nullcontext()
187
+ else:
188
+ kernel = nullcontext()
189
+ context = Context(rc, kernel)
190
+ self.listeners2[scope_id].add((listener, context))
137
191
 
138
192
  def cleanup():
139
- self.listeners2[scope_id].remove((listener, scope))
193
+ self.listeners2[scope_id].remove((listener, context))
140
194
 
141
195
  return cleanup
142
196
 
143
197
  def fire(self, new: T, old: T):
144
198
  logger.info("value change from %s to %s, will fire events", old, new)
145
199
  scope_id = self._get_scope_key()
146
- scopes = set()
147
- for listener, scope in self.listeners[scope_id].copy():
148
- if scope is not None:
149
- scopes.add(scope)
150
- for listener2, scope in self.listeners2[scope_id].copy():
151
- if scope is not None:
152
- scopes.add(scope)
153
- with contextlib.ExitStack() as stack:
154
- for scope in scopes:
155
- stack.enter_context(scope)
156
- for listener, scope in self.listeners[scope_id].copy():
157
- listener(new)
158
- for listener2, scope in self.listeners2[scope_id].copy():
159
- listener2(new, old)
200
+ contexts = set()
201
+ for listener, context in self.listeners[scope_id].copy():
202
+ contexts.add(context)
203
+ for listener2, context in self.listeners2[scope_id].copy():
204
+ contexts.add(context)
205
+ if contexts:
206
+ for context in contexts:
207
+ with context or nullcontext():
208
+ for listener, context_listener in self.listeners[scope_id].copy():
209
+ if context == context_listener:
210
+ listener(new)
211
+ for listener2, context_listener in self.listeners2[scope_id].copy():
212
+ if context == context_listener:
213
+ listener2(new, old)
160
214
 
161
215
  def update(self, _f=None, **kwargs):
162
216
  if _f is not None:
@@ -194,6 +248,9 @@ class ValueBase(Generic[T]):
194
248
 
195
249
  return cast(Callable[[TS], None], setter)
196
250
 
251
+ def _check_mutation(self):
252
+ pass
253
+
197
254
 
198
255
  # the default store for now, stores in a global dict, or when in a solara
199
256
  # context, in the solara user context
@@ -205,7 +262,7 @@ class KernelStore(ValueBase[S], ABC):
205
262
  _type_counter: Dict[Any, int] = defaultdict(int)
206
263
  scope_lock = threading.RLock()
207
264
 
208
- def __init__(self, key=None, equals: Callable[[Any, Any], bool] = equals_extra):
265
+ def __init__(self, key: str, equals: Callable[[Any, Any], bool] = equals_extra):
209
266
  super().__init__(equals=equals)
210
267
  self.storage_key = key
211
268
  self._global_dict = {}
@@ -285,7 +342,8 @@ def _is_internal_module(file_name: str):
285
342
  return (
286
343
  file_name_parts[-2:] == ["solara", "toestand.py"]
287
344
  or file_name_parts[-2:] == ["solara", "reactive.py"]
288
- or file_name_parts[-2:] == ["solara", "use_reactive.py"]
345
+ or file_name_parts[-2:] == ["solara", "_stores.py"]
346
+ or file_name_parts[-3:] == ["solara", "hooks", "use_reactive.py"]
289
347
  or file_name_parts[-2:] == ["reacton", "core.py"]
290
348
  # If we use SomeClass[K](...) we go via the typing module, so we need to skip that as well
291
349
  or (file_name_parts[-2].startswith("python") and file_name_parts[-1] == "typing.py")
@@ -350,7 +408,7 @@ reactive_df = solara.reactive(df, equals=solara.util.equals_pickle)
350
408
  code = tb.code_context[0]
351
409
  else:
352
410
  code = "<No code context available>"
353
- msg += "This warning was triggered from:\n" f"{tb.filename}:{tb.lineno}\n" f"{code}"
411
+ msg += f"This warning was triggered from:\n{tb.filename}:{tb.lineno}\n{code}"
354
412
  warnings.warn(msg)
355
413
  self._mutation_detection = False
356
414
  cls = type(default_value)
@@ -377,7 +435,7 @@ reactive_df = solara.reactive(df, equals=solara.util.equals_pickle)
377
435
  code = tb.code_context[0].strip()
378
436
  else:
379
437
  code = "No code context available"
380
- msg = f"Reactive variable was initialized at {tb.filename}:{tb.lineno} with {initial!r}, but was mutated to {current!r}.\n" f"{code}"
438
+ msg = f"Reactive variable was initialized at {tb.filename}:{tb.lineno} with {initial!r}, but was mutated to {current!r}.\n{code}"
381
439
  else:
382
440
  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)."
383
441
  raise ValueError(msg)
@@ -410,10 +468,10 @@ class KernelStoreFactory(KernelStore[S]):
410
468
 
411
469
  def mutation_detection_storage(default_value: S, key=None, equals=None) -> ValueBase[S]:
412
470
  from solara.util import equals_pickle as default_equals
413
- from ._stores import MutateDetectorStore, StoreValue, _PublicValueNotSet
471
+ from ._stores import MutateDetectorStore, StoreValue, _PublicValueNotSet, _SetValueNotSet
414
472
 
415
473
  kernel_store = KernelStoreValue[StoreValue[S]](
416
- StoreValue[S](private=default_value, public=_PublicValueNotSet(), get_traceback=None, set_value=None, set_traceback=None),
474
+ StoreValue[S](private=default_value, public=_PublicValueNotSet(), get_traceback=None, set_value=_SetValueNotSet(), set_traceback=None),
417
475
  key=key,
418
476
  unwrap=lambda x: x.private,
419
477
  )
@@ -812,7 +870,7 @@ class AutoSubscribeContextManagerBase:
812
870
  def __init__(self):
813
871
  self.subscribed = {}
814
872
 
815
- def update_subscribers(self, change_handler, scope=None):
873
+ def update_subscribers(self, change_handler):
816
874
  assert self.reactive_used is not None
817
875
  reactive_used = self.reactive_used
818
876
  # remove subfields for which we already listen to it's root reactive value
@@ -828,7 +886,7 @@ class AutoSubscribeContextManagerBase:
828
886
 
829
887
  for reactive in added:
830
888
  if reactive not in self.subscribed:
831
- unsubscribe = reactive.subscribe_change(change_handler, scope=scope)
889
+ unsubscribe = reactive.subscribe_change(change_handler)
832
890
  self.subscribed[reactive] = unsubscribe
833
891
  for reactive in removed:
834
892
  unsubscribe = self.subscribed[reactive]
@@ -850,6 +908,39 @@ class AutoSubscribeContextManagerBase:
850
908
  thread_local.reactive_used = self.reactive_used_before
851
909
 
852
910
 
911
+ class Context:
912
+ def __init__(self, render_context, kernel_context):
913
+ # combine the render context *and* the kernel context into one context
914
+ self.render_context = render_context
915
+ self.kernel_context = kernel_context
916
+
917
+ def __enter__(self):
918
+ if self.render_context is not None:
919
+ self.render_context.__enter__()
920
+ self.kernel_context.__enter__()
921
+
922
+ def __exit__(self, exc_type, exc_val, exc_tb):
923
+ if self.render_context is not None:
924
+ # this will trigger a render
925
+ res1 = self.render_context.__exit__(exc_type, exc_val, exc_tb)
926
+ else:
927
+ res1 = None
928
+ # pop the current context from the stack
929
+ res2 = self.kernel_context.__exit__(exc_type, exc_val, exc_tb)
930
+ return res1 or res2
931
+
932
+ def __eq__(self, value: object) -> bool:
933
+ if not isinstance(value, Context):
934
+ return False
935
+ return self.render_context == value.render_context and self.kernel_context == value.kernel_context
936
+
937
+ def __hash__(self) -> int:
938
+ return hash(id(self.render_context)) ^ hash(id(self.kernel_context))
939
+
940
+ def __repr__(self) -> str:
941
+ return f"Context(render_context={self.render_context}, kernel_context={self.kernel_context})"
942
+
943
+
853
944
  class AutoSubscribeContextManagerReacton(AutoSubscribeContextManagerBase):
854
945
  def __init__(self, element: solara.Element):
855
946
  self.element = element
@@ -865,7 +956,7 @@ class AutoSubscribeContextManagerReacton(AutoSubscribeContextManagerBase):
865
956
  super().__enter__()
866
957
 
867
958
  def update_subscribers():
868
- self.update_subscribers(force_update, scope=reacton.core.get_render_context(required=True))
959
+ self.update_subscribers(force_update)
869
960
 
870
961
  solara.use_effect(update_subscribers, None)
871
962
 
solara/util.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import base64
2
2
  import contextlib
3
+ import functools
3
4
  import gzip
4
5
  import hashlib
5
6
  import json
@@ -328,3 +329,20 @@ def is_running_in_vscode():
328
329
 
329
330
  def is_running_in_voila():
330
331
  return os.environ.get("SERVER_SOFTWARE", "").startswith("voila")
332
+
333
+
334
+ def once(f):
335
+ called = False
336
+ return_value = None
337
+
338
+ @functools.wraps(f)
339
+ def wrapper():
340
+ nonlocal called
341
+ nonlocal return_value
342
+ if called:
343
+ return return_value
344
+ called = True
345
+ return_value = f()
346
+ return return_value
347
+
348
+ return wrapper
@@ -5,6 +5,8 @@ from .breadcrumbs import BreadCrumbs
5
5
 
6
6
  @solara.component
7
7
  def Gallery(route_external=None):
8
+ from ..pages.documentation.examples import pycafe_projects
9
+
8
10
  if route_external is not None:
9
11
  route_current = route_external
10
12
  else:
@@ -49,6 +51,8 @@ def Gallery(route_external=None):
49
51
  image_url = "https://dxhl76zpt6fap.cloudfront.net/public/api/" + child.path + ".gif"
50
52
  elif child.path in ["card", "dataframe", "pivot_table", "slider"]:
51
53
  image_url = "https://dxhl76zpt6fap.cloudfront.net/public/api/" + child.path + ".png"
54
+ elif child.path in pycafe_projects:
55
+ image_url = f"https://py.cafe/preview/solara/{child.path}"
52
56
  else:
53
57
  image_url = "https://dxhl76zpt6fap.cloudfront.net/public/logo.svg"
54
58
 
@@ -94,10 +98,29 @@ def WithCode(route_current):
94
98
  @solara.component
95
99
  def SubCategoryLayout(children=[]):
96
100
  route_current, all_routes = solara.use_route()
101
+ router = solara.use_router()
102
+ sibling_routes = router.path_routes[-2]
97
103
  if route_current is None:
98
104
  return solara.Error("Page not found")
99
105
  elif route_current.path == "/":
100
- return solara.Error("Not supposed to be rendered")
106
+ with solara.Column(
107
+ gap="10px", classes=["docs-card-container"], style={"flex-grow": 1, "max-width": "80%", "width": "550px", "padding-top": "64px"}, align="stretch"
108
+ ):
109
+ solara.HTML(tag="h2", unsafe_innerHTML=route_current.label, attributes={"id": route_current.path}, style="padding-left: 10%;")
110
+ for route in sibling_routes.children:
111
+ if route.path == "/":
112
+ continue
113
+ with solara.Link(route.path):
114
+ with solara.Row(
115
+ classes=["docs-card"],
116
+ style={
117
+ "background-color": "var(--docs-color-grey)",
118
+ "align-items": "center",
119
+ "height": "3rem",
120
+ },
121
+ ):
122
+ solara.HTML(tag="h3", unsafe_innerHTML=route.label, style={"color": "white", "display": "block", "flex-grow": "1", "padding": "0 24px"})
123
+ solara.v.Icon(children=["mdi-arrow-right"], color="var(--color-grey-light)", class_="docs-card-icon")
101
124
  elif route_current.module:
102
125
  WithCode(route_current)
103
126
  else:
@@ -1,4 +1,4 @@
1
- from typing import Dict, List, Union
1
+ from typing import Callable, Dict, List, Union, cast
2
2
 
3
3
  import yaml
4
4
  import markdown
@@ -12,6 +12,7 @@ from solara.components.markdown import formatter, _no_deep_copy_emojione
12
12
  # We want to separate metadata from the markdown files before rendering them, which solara.Markdown doesn't support
13
13
  @solara.component
14
14
  def MarkdownWithMetadata(content: str, unsafe_solara_execute=True):
15
+ cleanups = solara.use_ref(cast(List[Callable[[], None]], []))
15
16
  if "---" in content:
16
17
  pre_content, raw_metadata, post_content = content.split("---")
17
18
  metadata: Dict[str, Union[str, List[str]]] = yaml.safe_load(raw_metadata)
@@ -56,13 +57,17 @@ def MarkdownWithMetadata(content: str, unsafe_solara_execute=True):
56
57
  "name": "solara",
57
58
  "class": "",
58
59
  "validator": mkdocs_pycafe.validator,
59
- "format": mkdocs_pycafe.formatter(type="solara", next_formatter=formatter(unsafe_solara_execute), inside_last_div=False),
60
+ "format": mkdocs_pycafe.formatter(
61
+ type="solara", next_formatter=formatter(unsafe_solara_execute, cleanups.current), inside_last_div=False
62
+ ),
60
63
  },
61
64
  {
62
65
  "name": "python",
63
66
  "class": "highlight",
64
67
  "validator": mkdocs_pycafe.validator,
65
- "format": mkdocs_pycafe.formatter(type="solara", next_formatter=formatter(unsafe_solara_execute), inside_last_div=False),
68
+ "format": mkdocs_pycafe.formatter(
69
+ type="solara", next_formatter=formatter(unsafe_solara_execute, cleanups.current), inside_last_div=False
70
+ ),
66
71
  },
67
72
  ],
68
73
  },
@@ -71,6 +76,15 @@ def MarkdownWithMetadata(content: str, unsafe_solara_execute=True):
71
76
 
72
77
  md_parser = solara.use_memo(make_markdown_object, dependencies=[unsafe_solara_execute])
73
78
 
79
+ def cleanup_wrapper():
80
+ def cleanup():
81
+ for cleanup in cleanups.current:
82
+ cleanup()
83
+
84
+ return cleanup
85
+
86
+ solara.use_effect(cleanup_wrapper, [])
87
+
74
88
  with solara.v.Html(
75
89
  tag="div",
76
90
  style_="display: flex; flex-direction: row; justify-content: center; gap: 15px; max-width: 90%; margin: 0 auto;",
@@ -1,6 +1,23 @@
1
1
  # Solara Changelog
2
2
 
3
- ## Version 1.41.0
3
+ ## Version 1.43.0
4
+ * Feature: Time picker component. [#654](https://github.com/widgetti/solara/pull/654).
5
+ * Feature: Make the default container of sibling components configurable. By default the setting remains the same (using `solara.Column`), but will default to `reacton.Fragment` in Solara 2.0 (see [the roadmap](/roadmap)). Can be changed by setting the `SOLARA_DEFAULT_CONTAINER` environmental variable to the name of a component (e.g. `"Column"`). [#928](https://github.com/widgetti/solara/pull/928).
6
+ * Feature: Do not allow reactive to be used in boolean comparisons. This feature is turned off by default, and can be enabled by setting the `SOLARA_ALLOW_REACTIVE_BOOLEAN=1` environmental variable. This feature will be enabled by default starting in Solara 2.0, see [the roadmap](/roadmap). [#846](https://github.com/widgetti/solara/pull/846).
7
+ * Feature: Use index for row names of pandas dataframes. [#613](https://github.com/widgetti/solara/pull/613).
8
+ * Feature: Support setting `http_only` for Solara session cookie. [#876](https://github.com/widgetti/solara/pull/876).
9
+ * Feature: Allow disabling notebook extensions. [#842](https://github.com/widgetti/solara/pull/842).
10
+ * Feature: `custom_exceptions` is now defined in `FakeIPython`. [#839](https://github.com/widgetti/solara/pull/839).
11
+ * Bug Fix: `InputDate` would not accept values if format was changed. [#933](https://github.com/widgetti/solara/pull/933).
12
+ * Bug Fix: Close kernels when ASGI/Starlette server is shut down. [#930](https://github.com/widgetti/solara/pull/930).
13
+ * Bug Fix: Avoid and test for the existence of memory leaks. [#377](https://github.com/widgetti/solara/pull/377).
14
+ * Bug Fix: `send_text` sent bytes instead of string. [637a77f](https://github.com/widgetti/solara/commit/637a77f2539ee68555cf998313aee62cde802579).
15
+ * Bug Fix: Avoid solara run hanging because of PyPI version request. [#855](https://github.com/widgetti/solara/pull/855).
16
+ * Bug Fix: Catch exceptions raised by startlette on websocket send failure. [7e50ee7](https://github.com/widgetti/solara/commit/7e50ee7edb7a36644b02d9d80ae91e1ac292975e).
17
+ * Bug Fix: Numpy scalars were erroneously converted to a string instead of a number. [cffccca](https://github.com/widgetti/solara/commit/cffccca500c36e21357323168b73ddd716071885).
18
+ * Bug Fix: Failing websocket.send calls would suppress all errors. [51cbfa9](https://github.com/widgetti/solara/commit/51cbfa970d42e5ff2c2b25de268e951677013467).
19
+
20
+ ## Version 1.42.0
4
21
  * Feature: Mutation detection is now available under the `SOLARA_STORAGE_MUTATION_DETECTION` environmental variable. [#595](https://github.com/widgetti/solara/pull/595).
5
22
  * Feature: Autofocusing text inputs is now supported. [#788](https://github.com/widgetti/solara/pull/788).
6
23
  * Feature: Custom colours are now supported for the Solara loading spinner. [#858](https://github.com/widgetti/solara/pull/858)
@@ -9,6 +26,14 @@
9
26
  * Bug Fix: Solara apps running in qt mode (`--qt`) should now always work correctly. [#856](https://github.com/widgetti/solara/pull/856).
10
27
  * Bug Fix: Hot reloading of files outside working directory would crash app. [069a205](https://github.com/widgetti/solara/commit/069a205c88a8cbcb0b0ca23f4d56889c8ad6134a) and [#869](https://github.com/widgetti/solara/pull/869).
11
28
 
29
+ ## Version 1.41.0
30
+ * Feature: Support automatic resizing of Altair (Vega-Lite) figures. [#833](https://github.com/widgetti/solara/pull/833).
31
+ * Feature (Experimental): Support running Solara applications as standalone QT apps. [#835](https://github.com/widgetti/solara/pull/835).
32
+ * Feature: Add option to hide "This website runs on Solara"-banner. [#836](https://github.com/widgetti/solara/pull/836).
33
+ * Feature: Support navigating to hashes. [#814](https://github.com/widgetti/solara/pull/814).
34
+ * Bug Fix: Chunks and assets in nbextensions would fail to load. [9efe26c](https://github.com/widgetti/solara/commit/9efe26cbe00210163a6e8ef251ebfe50ca87fce2).
35
+ * Bug Fix: Vue widget decorator now always uses absolute paths. [#826](https://github.com/widgetti/solara/pull/826).
36
+
12
37
  ## Version 1.40.0
13
38
  * Feature: In Jupyter Notebook and Lab, Solara (server) now renders the [ipypopout](https://github.com/widgetti/ipypopout) window instead of Voila [#805](render ipypopout content in jupyter notebook and lab)
14
39
  * Feature: Support styling input field of [ChatInput component](https://solara.dev/documentation/components/lab/chat). [#800](https://github.com/widgetti/solara/pull/800).
@@ -122,4 +122,4 @@ The following [Container components](/documentation/advanced/understanding/conta
122
122
  * [GridDraggable](/documentation/components/layout/griddraggable)
123
123
  * [VBox](/documentation/components/layout/vbox) (kept for ipywidgets compatibility, please use Column)
124
124
  * [HBox](/documentation/components/layout/hbox) (kept for ipywidgets compatibility, please use Row)
125
- * [AppLayout](/documentation/components/layout/app_layout) Not often used directly, since Solara will already wrap your page in it. Sometimes re-used in a new `Layout` component.
125
+ * [AppLayout](/documentation/components/layout/app_layout) Not often used directly, since Solara will already wrap your page in it. Sometimes reused in a new `Layout` component.
@@ -51,6 +51,7 @@ browser pages. This can be used to store state that outlives a page refresh.
51
51
  We recommend storing the state in an external database, especially in the case of multiple workers/nodes. If you want to store state associated to a session in-memory, make sure to set up sticky sessions.
52
52
 
53
53
 
54
+ The `solara-session-id` cookie is accessible in the browser using JavaScript. If you deem this a security risk, you can disable the cookie by setting the `SOLARA_SESSION_HTTP_ONLY` environment variable to `True`.
54
55
 
55
56
 
56
57
  ## Readiness check
@@ -78,7 +79,16 @@ $ curl http://localhost:8765/resourcez\?verbose
78
79
 
79
80
  The JSON format may be subject to change.
80
81
 
82
+ ## Ignoring notebook extensions
81
83
 
84
+ Not all (classic) jupyter notebook extensions are compatible with Solara, and there is not way to distinguish between notebook extensions that are needed for widgets and those that are not.
85
+ To ignore notebook extensions, you can set the `SOLARA_SERVER_IGNORE_NBEXTENSIONS` environment variable. This is a comma separated list of notebook extensions to ignore. For example, to ignore the `dash/main` and `foo/bar` extensions, you can run:
86
+
87
+ ```bash
88
+ $ SOLARA_SERVER_IGNORE_NBEXTENSIONS="dash/main,foo/bar" solara run nogit/sol.py -a
89
+ ```
90
+
91
+ Note that these error are not fatal, and the Solara app will still run.
82
92
 
83
93
  ## Production mode
84
94
 
@@ -56,7 +56,7 @@ def Page():
56
56
  ```
57
57
  ## How to configure OAuth
58
58
 
59
- Solara supports the following OAuth providers: [Auth0](https://auth0.com/) and [Fief](https://fief.dev/).
59
+ Solara currently supports [Auth0](https://auth0.com/) as the sole OAuth provider. [Fief](https://fief.dev/) support is **deprecated** (currently untested), but is not planned to be removed. If you would like support for a different provider to be added, [contact us](/contact)
60
60
 
61
61
 
62
62
  ### Configuring Auth0
@@ -134,6 +134,8 @@ To create your own Auth0 application, follow these steps:
134
134
 
135
135
  ### Configuring Fief
136
136
 
137
+ ##### Note: Fief support is not maintained or tested. If you would like Fief to be supported, feel free to [contact us](/contact)
138
+
137
139
  You can also configure Solara to use our Fief test account. To do this, you need to set the following environment variables:
138
140
 
139
141
  ```bash
@@ -164,7 +166,7 @@ Solara provides two convenient components for creating a user interface for logi
164
166
  1. [Avatar](/documentation/components/enterprise/avatar): This component shows the user's avatar.
165
167
  2. [AvatarMenu](/documentation/components/enterprise/avatar_menu): This component shows a menu with the user's avatar and a logout button.
166
168
 
167
- ## Python version support
169
+ ## Python version support
168
170
 
169
171
  Please note that Python 3.6 is not supported for Solara OAuth.
170
172