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.
- solara/__init__.py +1 -1
- solara/__main__.py +12 -7
- solara/_stores.py +128 -16
- solara/cache.py +6 -4
- solara/checks.py +1 -1
- solara/components/__init__.py +18 -1
- solara/components/datatable.py +4 -4
- solara/components/input.py +5 -1
- solara/components/markdown.py +46 -10
- solara/components/misc.py +2 -2
- solara/components/select.py +1 -1
- solara/components/style.py +1 -1
- solara/hooks/use_reactive.py +16 -1
- solara/lab/components/__init__.py +1 -0
- solara/lab/components/chat.py +15 -9
- solara/lab/components/input_time.py +133 -0
- solara/lab/hooks/dataframe.py +1 -0
- solara/lab/utils/dataframe.py +11 -1
- solara/server/app.py +66 -30
- solara/server/flask.py +12 -2
- solara/server/jupyter/server_extension.py +1 -0
- solara/server/kernel.py +50 -3
- solara/server/kernel_context.py +68 -9
- solara/server/patch.py +28 -30
- solara/server/server.py +16 -6
- solara/server/settings.py +11 -0
- solara/server/shell.py +19 -1
- solara/server/starlette.py +72 -14
- solara/server/static/solara_bootstrap.py +1 -1
- solara/settings.py +3 -0
- solara/tasks.py +30 -9
- solara/test/pytest_plugin.py +4 -2
- solara/toestand.py +119 -28
- solara/util.py +18 -0
- solara/website/components/docs.py +24 -1
- solara/website/components/markdown.py +17 -3
- solara/website/pages/changelog/changelog.md +26 -1
- solara/website/pages/documentation/advanced/content/10-howto/20-layout.md +1 -1
- solara/website/pages/documentation/advanced/content/20-understanding/50-solara-server.md +10 -0
- solara/website/pages/documentation/advanced/content/30-enterprise/10-oauth.md +4 -2
- solara/website/pages/documentation/api/routing/route.py +10 -12
- solara/website/pages/documentation/api/routing/use_route.py +26 -30
- solara/website/pages/documentation/components/advanced/link.py +6 -8
- solara/website/pages/documentation/components/advanced/meta.py +6 -9
- solara/website/pages/documentation/components/advanced/style.py +7 -9
- solara/website/pages/documentation/components/input/file_browser.py +12 -14
- solara/website/pages/documentation/components/lab/input_time.py +15 -0
- solara/website/pages/documentation/components/lab/theming.py +6 -4
- solara/website/pages/documentation/components/layout/columns_responsive.py +37 -39
- solara/website/pages/documentation/components/layout/gridfixed.py +4 -6
- solara/website/pages/documentation/components/output/html.py +1 -3
- solara/website/pages/documentation/components/output/sql_code.py +23 -25
- solara/website/pages/documentation/components/page/head.py +4 -7
- solara/website/pages/documentation/components/page/title.py +12 -14
- solara/website/pages/documentation/components/status/error.py +17 -18
- solara/website/pages/documentation/components/status/info.py +17 -18
- solara/website/pages/documentation/examples/__init__.py +10 -0
- solara/website/pages/documentation/examples/ai/chatbot.py +62 -44
- solara/website/pages/documentation/examples/general/live_update.py +22 -28
- solara/website/pages/documentation/examples/general/pokemon_search.py +1 -1
- solara/website/pages/documentation/faq/content/99-faq.md +9 -0
- solara/website/pages/documentation/getting_started/content/00-quickstart.md +1 -1
- solara/website/pages/documentation/getting_started/content/04-tutorials/_jupyter_dashboard_1.ipynb +23 -19
- solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md +2 -2
- solara/website/pages/roadmap/roadmap.md +3 -0
- {solara_ui-1.42.0.dist-info → solara_ui-1.44.0.dist-info}/METADATA +2 -2
- {solara_ui-1.42.0.dist-info → solara_ui-1.44.0.dist-info}/RECORD +71 -69
- {solara_ui-1.42.0.data → solara_ui-1.44.0.data}/data/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
- {solara_ui-1.42.0.data → solara_ui-1.44.0.data}/data/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
- {solara_ui-1.42.0.dist-info → solara_ui-1.44.0.dist-info}/WHEEL +0 -0
- {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.
|
|
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[
|
|
717
|
+
) -> Callable[[Callable[[], R]], "Task[[], R]"]: ...
|
|
697
718
|
|
|
698
719
|
|
|
699
720
|
@overload
|
|
700
721
|
def use_task(
|
|
701
|
-
f: Callable[
|
|
722
|
+
f: Callable[[], R],
|
|
702
723
|
*,
|
|
703
|
-
dependencies: None = ...,
|
|
724
|
+
dependencies: Literal[None] = ...,
|
|
704
725
|
raise_error=...,
|
|
705
726
|
prefer_threaded=...,
|
|
706
|
-
) -> Task[
|
|
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[
|
|
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[
|
|
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[
|
|
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=[])
|
solara/test/pytest_plugin.py
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
147
|
-
for listener,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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", "
|
|
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
|
|
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
|
|
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=
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
169
|
+
## Python version support
|
|
168
170
|
|
|
169
171
|
Please note that Python 3.6 is not supported for Solara OAuth.
|
|
170
172
|
|