solara-ui 1.41.0__py2.py3-none-any.whl → 1.42.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- solara/__init__.py +1 -1
- solara/__main__.py +7 -1
- solara/_stores.py +185 -0
- solara/components/component_vue.py +23 -0
- solara/components/echarts.py +5 -2
- solara/components/echarts.vue +22 -5
- solara/components/file_drop.py +20 -0
- solara/components/input.py +16 -0
- solara/components/markdown.py +22 -13
- solara/components/spinner-solara.vue +2 -2
- solara/components/spinner.py +17 -2
- solara/hooks/use_reactive.py +8 -1
- solara/reactive.py +9 -3
- solara/server/kernel.py +2 -1
- solara/server/qt.py +1 -1
- solara/server/starlette.py +2 -2
- solara/server/static/solara_bootstrap.py +1 -1
- solara/settings.py +14 -0
- solara/template/portal/pyproject.toml +1 -1
- solara/test/pytest_plugin.py +3 -0
- solara/toestand.py +139 -16
- solara/util.py +22 -0
- solara/website/components/markdown.py +45 -1
- solara/website/pages/changelog/changelog.md +9 -0
- solara/website/pages/documentation/api/cross_filter/cross_filter_dataframe.py +4 -5
- solara/website/pages/documentation/api/cross_filter/cross_filter_report.py +3 -5
- solara/website/pages/documentation/api/cross_filter/cross_filter_select.py +3 -5
- solara/website/pages/documentation/api/cross_filter/cross_filter_slider.py +3 -5
- solara/website/pages/documentation/api/hooks/use_cross_filter.py +3 -5
- solara/website/pages/documentation/api/hooks/use_exception.py +9 -11
- solara/website/pages/documentation/api/hooks/use_previous.py +6 -9
- solara/website/pages/documentation/api/hooks/use_state_or_update.py +23 -26
- solara/website/pages/documentation/api/hooks/use_thread.py +11 -13
- solara/website/pages/documentation/api/utilities/on_kernel_start.py +17 -0
- solara/website/pages/documentation/components/input/input.py +22 -0
- solara/website/pages/documentation/components/viz/echarts.py +3 -1
- solara/website/pages/documentation/examples/general/pokemon_search.py +3 -3
- solara/website/pages/documentation/examples/visualization/linked_views.py +0 -3
- solara/website/pages/documentation/getting_started/content/00-quickstart.md +1 -1
- solara/website/pages/documentation/getting_started/content/01-introduction.md +1 -1
- solara/website/pages/roadmap/roadmap.md +3 -0
- {solara_ui-1.41.0.dist-info → solara_ui-1.42.0.dist-info}/METADATA +8 -5
- {solara_ui-1.41.0.dist-info → solara_ui-1.42.0.dist-info}/RECORD +47 -46
- {solara_ui-1.41.0.dist-info → solara_ui-1.42.0.dist-info}/WHEEL +1 -1
- {solara_ui-1.41.0.data → solara_ui-1.42.0.data}/data/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
- {solara_ui-1.41.0.data → solara_ui-1.42.0.data}/data/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
- {solara_ui-1.41.0.dist-info → solara_ui-1.42.0.dist-info}/licenses/LICENSE +0 -0
solara/reactive.py
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
from typing import TypeVar
|
|
1
|
+
from typing import Any, Callable, TypeVar
|
|
2
2
|
|
|
3
3
|
from solara.toestand import Reactive
|
|
4
|
+
import solara.util
|
|
4
5
|
|
|
5
6
|
__all__ = ["reactive", "Reactive"]
|
|
6
7
|
|
|
7
8
|
T = TypeVar("T")
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
def reactive(value: T) -> Reactive[T]:
|
|
11
|
+
def reactive(value: T, equals: Callable[[Any, Any], bool] = solara.util.equals_extra) -> Reactive[T]:
|
|
11
12
|
"""Creates a new Reactive object with the given initial value.
|
|
12
13
|
|
|
13
14
|
Reactive objects are mostly used to manage global or application-wide state in
|
|
@@ -35,6 +36,11 @@ def reactive(value: T) -> Reactive[T]:
|
|
|
35
36
|
|
|
36
37
|
Args:
|
|
37
38
|
value (T): The initial value of the reactive variable.
|
|
39
|
+
equals: A function that returns True if two values are considered equal, and False otherwise.
|
|
40
|
+
The default function is `solara.util.equals`, which performs a deep comparison of the two values
|
|
41
|
+
and is more forgiving than the default `==` operator.
|
|
42
|
+
You can provide a custom function if you need to define a different notion of equality.
|
|
43
|
+
|
|
38
44
|
|
|
39
45
|
Returns:
|
|
40
46
|
Reactive[T]: A new Reactive object with the specified initial value.
|
|
@@ -90,4 +96,4 @@ def reactive(value: T) -> Reactive[T]:
|
|
|
90
96
|
Whenever the counter value changes, `CounterDisplay` automatically updates to display the new value.
|
|
91
97
|
|
|
92
98
|
"""
|
|
93
|
-
return Reactive(value)
|
|
99
|
+
return Reactive(value, equals=equals)
|
solara/server/kernel.py
CHANGED
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
import logging
|
|
3
3
|
import pdb
|
|
4
4
|
import queue
|
|
5
|
+
import re
|
|
5
6
|
import struct
|
|
6
7
|
import warnings
|
|
7
8
|
from binascii import b2a_base64
|
|
@@ -73,7 +74,7 @@ def json_dumps(data):
|
|
|
73
74
|
)
|
|
74
75
|
|
|
75
76
|
|
|
76
|
-
ipykernel_version = tuple(map(int,
|
|
77
|
+
ipykernel_version = tuple(map(int, re.split(r"\D+", ipykernel.__version__)[:3]))
|
|
77
78
|
if ipykernel_version >= (6, 18, 0):
|
|
78
79
|
import comm.base_comm
|
|
79
80
|
|
solara/server/qt.py
CHANGED
solara/server/starlette.py
CHANGED
|
@@ -86,8 +86,8 @@ prefix = ""
|
|
|
86
86
|
# Since starlette seems to accept really large values for http, lets do the same for websockets
|
|
87
87
|
# An arbitrarily large value we settled on for now is 32kb
|
|
88
88
|
# If we don't do this, users with many cookies will fail to get a websocket connection.
|
|
89
|
-
|
|
90
|
-
if
|
|
89
|
+
ws_major_version = int(websockets.__version__.split(".")[0])
|
|
90
|
+
if ws_major_version >= 13:
|
|
91
91
|
websockets.legacy.http.MAX_LINE_LENGTH = int(os.environ.get("WEBSOCKETS_MAX_LINE_LENGTH", str(1024 * 32))) # type: ignore
|
|
92
92
|
else:
|
|
93
93
|
websockets.legacy.http.MAX_LINE = 1024 * 32 # type: ignore
|
|
@@ -119,7 +119,7 @@ async def main():
|
|
|
119
119
|
]
|
|
120
120
|
for dep in requirements:
|
|
121
121
|
await micropip.install(dep, keep_going=True)
|
|
122
|
-
await micropip.install("/wheels/solara-1.
|
|
122
|
+
await micropip.install("/wheels/solara-1.42.0-py2.py3-none-any.whl", keep_going=True)
|
|
123
123
|
import solara
|
|
124
124
|
|
|
125
125
|
el = solara.Warning("lala")
|
solara/settings.py
CHANGED
|
@@ -61,9 +61,23 @@ class MainSettings(BaseSettings):
|
|
|
61
61
|
env_file = ".env"
|
|
62
62
|
|
|
63
63
|
|
|
64
|
+
class Storage(BaseSettings):
|
|
65
|
+
mutation_detection: Optional[bool] = None # True/False, or None to auto determine
|
|
66
|
+
factory: str = "solara.toestand.default_storage"
|
|
67
|
+
|
|
68
|
+
def get_factory(self):
|
|
69
|
+
return solara.util.import_item(self.factory)
|
|
70
|
+
|
|
71
|
+
class Config:
|
|
72
|
+
env_prefix = "solara_storage_"
|
|
73
|
+
case_sensitive = False
|
|
74
|
+
env_file = ".env"
|
|
75
|
+
|
|
76
|
+
|
|
64
77
|
assets: Assets = Assets()
|
|
65
78
|
cache: Cache = Cache()
|
|
66
79
|
main = MainSettings()
|
|
80
|
+
storage = Storage()
|
|
67
81
|
|
|
68
82
|
if main.check_hooks not in ["off", "warn", "raise"]:
|
|
69
83
|
raise ValueError(f"Invalid value for check_hooks: {main.check_hooks}, expected one of ['off', 'warn', 'raise']")
|
solara/test/pytest_plugin.py
CHANGED
|
@@ -515,6 +515,8 @@ def create_runner_solara(solara_server, solara_app, page_session: "playwright.sy
|
|
|
515
515
|
|
|
516
516
|
def run(f: Callable, locals={}):
|
|
517
517
|
nonlocal count
|
|
518
|
+
from IPython.display import clear_output
|
|
519
|
+
|
|
518
520
|
path = Path(f.__code__.co_filename)
|
|
519
521
|
cwd = str(path.parent)
|
|
520
522
|
current_dir = os.getcwd()
|
|
@@ -523,6 +525,7 @@ def create_runner_solara(solara_server, solara_app, page_session: "playwright.sy
|
|
|
523
525
|
|
|
524
526
|
sys.path.append(cwd)
|
|
525
527
|
try:
|
|
528
|
+
clear_output()
|
|
526
529
|
f()
|
|
527
530
|
finally:
|
|
528
531
|
os.chdir(current_dir)
|
solara/toestand.py
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import dataclasses
|
|
3
|
+
import inspect
|
|
3
4
|
import logging
|
|
5
|
+
import os
|
|
4
6
|
import sys
|
|
5
7
|
import threading
|
|
8
|
+
from types import FrameType
|
|
6
9
|
import warnings
|
|
10
|
+
import copy
|
|
7
11
|
from abc import ABC, abstractmethod
|
|
8
12
|
from collections import defaultdict
|
|
9
13
|
from operator import getitem
|
|
@@ -24,9 +28,10 @@ from typing import (
|
|
|
24
28
|
|
|
25
29
|
import react_ipywidgets as react
|
|
26
30
|
import reacton.core
|
|
27
|
-
from
|
|
31
|
+
from solara.util import equals_extra
|
|
28
32
|
|
|
29
33
|
import solara
|
|
34
|
+
import solara.settings
|
|
30
35
|
from solara import _using_solara_server
|
|
31
36
|
|
|
32
37
|
T = TypeVar("T")
|
|
@@ -61,7 +66,7 @@ def use_sync_external_store(subscribe: Callable[[Callable[[], None]], Callable[[
|
|
|
61
66
|
|
|
62
67
|
def on_store_change(_ignore_new_state=None):
|
|
63
68
|
new_state = get_snapshot()
|
|
64
|
-
if not
|
|
69
|
+
if not equals_extra(new_state, prev_state.current):
|
|
65
70
|
prev_state.current = new_state
|
|
66
71
|
force_update()
|
|
67
72
|
|
|
@@ -87,8 +92,9 @@ def merge_state(d1: S, **kwargs) -> S:
|
|
|
87
92
|
|
|
88
93
|
|
|
89
94
|
class ValueBase(Generic[T]):
|
|
90
|
-
def __init__(self, merge: Callable = merge_state):
|
|
95
|
+
def __init__(self, merge: Callable = merge_state, equals=equals_extra):
|
|
91
96
|
self.merge = merge
|
|
97
|
+
self.equals = equals
|
|
92
98
|
self.listeners: Dict[str, Set[Tuple[Callable[[T], None], Optional[ContextManager]]]] = defaultdict(set)
|
|
93
99
|
self.listeners2: Dict[str, Set[Tuple[Callable[[T, T], None], Optional[ContextManager]]]] = defaultdict(set)
|
|
94
100
|
|
|
@@ -199,8 +205,8 @@ class KernelStore(ValueBase[S], ABC):
|
|
|
199
205
|
_type_counter: Dict[Any, int] = defaultdict(int)
|
|
200
206
|
scope_lock = threading.RLock()
|
|
201
207
|
|
|
202
|
-
def __init__(self, key=None):
|
|
203
|
-
super().__init__()
|
|
208
|
+
def __init__(self, key=None, equals: Callable[[Any, Any], bool] = equals_extra):
|
|
209
|
+
super().__init__(equals=equals)
|
|
204
210
|
self.storage_key = key
|
|
205
211
|
self._global_dict = {}
|
|
206
212
|
# since a set can trigger events, which can trigger new updates, we need a recursive lock
|
|
@@ -250,7 +256,7 @@ class KernelStore(ValueBase[S], ABC):
|
|
|
250
256
|
def set(self, value: S):
|
|
251
257
|
scope_dict, scope_id = self._get_dict()
|
|
252
258
|
old = self.get()
|
|
253
|
-
if equals(old, value):
|
|
259
|
+
if self.equals(old, value):
|
|
254
260
|
return
|
|
255
261
|
scope_dict[self.storage_key] = value
|
|
256
262
|
|
|
@@ -268,23 +274,114 @@ class KernelStore(ValueBase[S], ABC):
|
|
|
268
274
|
def initial_value(self) -> S:
|
|
269
275
|
pass
|
|
270
276
|
|
|
277
|
+
def _check_mutation(self):
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _is_internal_module(file_name: str):
|
|
282
|
+
file_name_parts = file_name.split(os.sep)
|
|
283
|
+
if len(file_name_parts) < 2:
|
|
284
|
+
return False
|
|
285
|
+
return (
|
|
286
|
+
file_name_parts[-2:] == ["solara", "toestand.py"]
|
|
287
|
+
or file_name_parts[-2:] == ["solara", "reactive.py"]
|
|
288
|
+
or file_name_parts[-2:] == ["solara", "use_reactive.py"]
|
|
289
|
+
or file_name_parts[-2:] == ["reacton", "core.py"]
|
|
290
|
+
# If we use SomeClass[K](...) we go via the typing module, so we need to skip that as well
|
|
291
|
+
or (file_name_parts[-2].startswith("python") and file_name_parts[-1] == "typing.py")
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _find_outside_solara_frame() -> Optional[FrameType]:
|
|
296
|
+
# the module where the call stack origined from
|
|
297
|
+
current_frame: Optional[FrameType] = None
|
|
298
|
+
module_frame = None
|
|
299
|
+
|
|
300
|
+
# _getframe is not guaranteed to exist in all Python implementations,
|
|
301
|
+
# but is much faster than the inspect module
|
|
302
|
+
if hasattr(sys, "_getframe"):
|
|
303
|
+
current_frame = sys._getframe(1)
|
|
304
|
+
else:
|
|
305
|
+
current_frame = inspect.currentframe()
|
|
306
|
+
|
|
307
|
+
while current_frame is not None:
|
|
308
|
+
file_name = current_frame.f_code.co_filename
|
|
309
|
+
# Skip most common cases, i.e. toestand.py, reactive.py, use_reactive.py, Reacton's core.py, and the typing module
|
|
310
|
+
if not _is_internal_module(file_name):
|
|
311
|
+
module_frame = current_frame
|
|
312
|
+
break
|
|
313
|
+
current_frame = current_frame.f_back
|
|
314
|
+
|
|
315
|
+
return module_frame
|
|
316
|
+
|
|
271
317
|
|
|
272
318
|
class KernelStoreValue(KernelStore[S]):
|
|
273
319
|
default_value: S
|
|
320
|
+
_traceback: Optional[inspect.Traceback]
|
|
321
|
+
_default_value_copy: Optional[S]
|
|
274
322
|
|
|
275
|
-
def __init__(self, default_value: S, key=None):
|
|
323
|
+
def __init__(self, default_value: S, key=None, equals: Callable[[Any, Any], bool] = equals_extra, unwrap=lambda x: x):
|
|
276
324
|
self.default_value = default_value
|
|
325
|
+
self._unwrap = unwrap
|
|
326
|
+
self.equals = equals
|
|
327
|
+
self._mutation_detection = solara.settings.storage.mutation_detection
|
|
328
|
+
if self._mutation_detection:
|
|
329
|
+
frame = _find_outside_solara_frame()
|
|
330
|
+
if frame is not None:
|
|
331
|
+
self._traceback = inspect.getframeinfo(frame)
|
|
332
|
+
else:
|
|
333
|
+
self._traceback = None
|
|
334
|
+
self._default_value_copy = copy.deepcopy(default_value)
|
|
335
|
+
if not self.equals(self._unwrap(self.default_value), self._unwrap(self._default_value_copy)):
|
|
336
|
+
msg = """The equals function for this reactive value returned False when comparing a deepcopy to itself.
|
|
337
|
+
|
|
338
|
+
This reactive variable will not be able to detect mutations correctly, and is therefore disabled.
|
|
339
|
+
|
|
340
|
+
To avoid this warning, and to ensure that mutation detection works correctly, please provide a better equals function to the reactive variable.
|
|
341
|
+
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.
|
|
342
|
+
|
|
343
|
+
Example:
|
|
344
|
+
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
|
|
345
|
+
reactive_df = solara.reactive(df, equals=solara.util.equals_pickle)
|
|
346
|
+
"""
|
|
347
|
+
tb = self._traceback
|
|
348
|
+
if tb:
|
|
349
|
+
if tb.code_context:
|
|
350
|
+
code = tb.code_context[0]
|
|
351
|
+
else:
|
|
352
|
+
code = "<No code context available>"
|
|
353
|
+
msg += "This warning was triggered from:\n" f"{tb.filename}:{tb.lineno}\n" f"{code}"
|
|
354
|
+
warnings.warn(msg)
|
|
355
|
+
self._mutation_detection = False
|
|
277
356
|
cls = type(default_value)
|
|
278
357
|
if key is None:
|
|
279
358
|
with KernelStoreValue.scope_lock:
|
|
280
359
|
index = self._type_counter[cls]
|
|
281
360
|
self._type_counter[cls] += 1
|
|
282
361
|
key = cls.__module__ + ":" + cls.__name__ + ":" + str(index)
|
|
283
|
-
super().__init__(key=key)
|
|
362
|
+
super().__init__(key=key, equals=equals)
|
|
284
363
|
|
|
285
364
|
def initial_value(self) -> S:
|
|
365
|
+
self._check_mutation()
|
|
286
366
|
return self.default_value
|
|
287
367
|
|
|
368
|
+
def _check_mutation(self):
|
|
369
|
+
if not self._mutation_detection:
|
|
370
|
+
return
|
|
371
|
+
initial = self._unwrap(self._default_value_copy)
|
|
372
|
+
current = self._unwrap(self.default_value)
|
|
373
|
+
if not self.equals(initial, current):
|
|
374
|
+
tb = self._traceback
|
|
375
|
+
if tb:
|
|
376
|
+
if tb.code_context:
|
|
377
|
+
code = tb.code_context[0].strip()
|
|
378
|
+
else:
|
|
379
|
+
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}"
|
|
381
|
+
else:
|
|
382
|
+
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
|
+
raise ValueError(msg)
|
|
384
|
+
|
|
288
385
|
|
|
289
386
|
def _create_key_callable(f: Callable[[], S]):
|
|
290
387
|
try:
|
|
@@ -302,22 +399,48 @@ def _create_key_callable(f: Callable[[], S]):
|
|
|
302
399
|
|
|
303
400
|
|
|
304
401
|
class KernelStoreFactory(KernelStore[S]):
|
|
305
|
-
def __init__(self, factory: Callable[[], S], key=None):
|
|
402
|
+
def __init__(self, factory: Callable[[], S], key=None, equals: Callable[[Any, Any], bool] = equals_extra):
|
|
306
403
|
self.factory = factory
|
|
307
404
|
key = key or _create_key_callable(factory)
|
|
308
|
-
super().__init__(key=key)
|
|
405
|
+
super().__init__(key=key, equals=equals)
|
|
309
406
|
|
|
310
407
|
def initial_value(self) -> S:
|
|
311
408
|
return self.factory()
|
|
312
409
|
|
|
313
410
|
|
|
411
|
+
def mutation_detection_storage(default_value: S, key=None, equals=None) -> ValueBase[S]:
|
|
412
|
+
from solara.util import equals_pickle as default_equals
|
|
413
|
+
from ._stores import MutateDetectorStore, StoreValue, _PublicValueNotSet
|
|
414
|
+
|
|
415
|
+
kernel_store = KernelStoreValue[StoreValue[S]](
|
|
416
|
+
StoreValue[S](private=default_value, public=_PublicValueNotSet(), get_traceback=None, set_value=None, set_traceback=None),
|
|
417
|
+
key=key,
|
|
418
|
+
unwrap=lambda x: x.private,
|
|
419
|
+
)
|
|
420
|
+
return MutateDetectorStore[S](kernel_store, equals=equals or default_equals)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def default_storage(default_value: S, key=None, equals=None) -> ValueBase[S]:
|
|
424
|
+
# in solara v2 we will also do this when mutation_detection is None
|
|
425
|
+
# and we do not run on production mode
|
|
426
|
+
if solara.settings.storage.mutation_detection is True:
|
|
427
|
+
return mutation_detection_storage(default_value, key=key, equals=equals)
|
|
428
|
+
else:
|
|
429
|
+
return KernelStoreValue[S](default_value, key=key, equals=equals or equals_extra)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _call_storage_factory(default_value: S, key=None, equals=None) -> ValueBase[S]:
|
|
433
|
+
factory = solara.settings.storage.get_factory()
|
|
434
|
+
return factory(default_value, key=key, equals=equals)
|
|
435
|
+
|
|
436
|
+
|
|
314
437
|
class Reactive(ValueBase[S]):
|
|
315
438
|
_storage: ValueBase[S]
|
|
316
439
|
|
|
317
|
-
def __init__(self, default_value: Union[S, ValueBase[S]], key=None):
|
|
440
|
+
def __init__(self, default_value: Union[S, ValueBase[S]], key=None, equals=None):
|
|
318
441
|
super().__init__()
|
|
319
442
|
if not isinstance(default_value, ValueBase):
|
|
320
|
-
self._storage =
|
|
443
|
+
self._storage = _call_storage_factory(default_value, key=key, equals=equals)
|
|
321
444
|
else:
|
|
322
445
|
self._storage = default_value
|
|
323
446
|
self.__post__init__()
|
|
@@ -505,11 +628,11 @@ def computed(
|
|
|
505
628
|
|
|
506
629
|
|
|
507
630
|
class ReactiveField(Reactive[T]):
|
|
508
|
-
def __init__(self, field: "FieldBase"):
|
|
631
|
+
def __init__(self, field: "FieldBase", equals: Callable[[Any, Any], bool] = equals_extra):
|
|
509
632
|
# super().__init__() # type: ignore
|
|
510
633
|
# We skip the Reactive constructor, because we do not need it, but we do
|
|
511
634
|
# want to be an instanceof for use in use_reactive
|
|
512
|
-
ValueBase.__init__(self)
|
|
635
|
+
ValueBase.__init__(self, equals=equals)
|
|
513
636
|
self._field = field
|
|
514
637
|
field = field
|
|
515
638
|
while not isinstance(field, ValueBase):
|
|
@@ -536,7 +659,7 @@ class ReactiveField(Reactive[T]):
|
|
|
536
659
|
except KeyError:
|
|
537
660
|
return # same
|
|
538
661
|
old_value = self._field.get(old)
|
|
539
|
-
if not equals(new_value, old_value):
|
|
662
|
+
if not self.equals(new_value, old_value):
|
|
540
663
|
listener(new_value)
|
|
541
664
|
|
|
542
665
|
return self._root.subscribe_change(on_change, scope=scope)
|
|
@@ -550,7 +673,7 @@ class ReactiveField(Reactive[T]):
|
|
|
550
673
|
except KeyError:
|
|
551
674
|
return # see subscribe
|
|
552
675
|
old_value = self._field.get(old)
|
|
553
|
-
if not equals(new_value, old_value):
|
|
676
|
+
if not self.equals(new_value, old_value):
|
|
554
677
|
listener(new_value, old_value)
|
|
555
678
|
|
|
556
679
|
return self._root.subscribe_change(on_change, scope=scope)
|
solara/util.py
CHANGED
|
@@ -31,6 +31,28 @@ except RuntimeError:
|
|
|
31
31
|
has_threads = False
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
from reacton.utils import equals as equals_extra
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def equals_pickle(a, b):
|
|
38
|
+
"""Compare two values for equality.
|
|
39
|
+
|
|
40
|
+
Avoid false negative, e.g. when comparing dataframes, we want to compare
|
|
41
|
+
the data, not the object identity.
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
if equals_extra(a, b):
|
|
45
|
+
return True
|
|
46
|
+
import pickle
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
if pickle.dumps(a) == pickle.dumps(b):
|
|
50
|
+
return True
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
34
56
|
def github_url(file):
|
|
35
57
|
rel_path = os.path.relpath(file, Path(solara.__file__).parent.parent)
|
|
36
58
|
github_url = solara.github_url + f"/blob/{solara.git_branch}/" + rel_path
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
from typing import Dict, List, Union
|
|
2
2
|
|
|
3
3
|
import yaml
|
|
4
|
+
import markdown
|
|
5
|
+
import mkdocs_pycafe
|
|
6
|
+
import pymdownx.superfences
|
|
4
7
|
|
|
5
8
|
import solara
|
|
9
|
+
from solara.components.markdown import formatter, _no_deep_copy_emojione
|
|
6
10
|
|
|
7
11
|
|
|
8
12
|
# We want to separate metadata from the markdown files before rendering them, which solara.Markdown doesn't support
|
|
@@ -27,12 +31,52 @@ def MarkdownWithMetadata(content: str, unsafe_solara_execute=True):
|
|
|
27
31
|
solara.Meta(property=key, content=value)
|
|
28
32
|
else:
|
|
29
33
|
solara.Meta(name=key, content=value)
|
|
34
|
+
|
|
35
|
+
def make_markdown_object():
|
|
36
|
+
return markdown.Markdown( # type: ignore
|
|
37
|
+
extensions=[
|
|
38
|
+
"pymdownx.highlight",
|
|
39
|
+
"pymdownx.superfences",
|
|
40
|
+
"pymdownx.emoji",
|
|
41
|
+
"toc", # so we get anchors for h1 h2 etc
|
|
42
|
+
"tables",
|
|
43
|
+
],
|
|
44
|
+
extension_configs={
|
|
45
|
+
"pymdownx.emoji": {
|
|
46
|
+
"emoji_index": _no_deep_copy_emojione,
|
|
47
|
+
},
|
|
48
|
+
"pymdownx.superfences": {
|
|
49
|
+
"custom_fences": [
|
|
50
|
+
{
|
|
51
|
+
"name": "mermaid",
|
|
52
|
+
"class": "mermaid",
|
|
53
|
+
"format": pymdownx.superfences.fence_div_format,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "solara",
|
|
57
|
+
"class": "",
|
|
58
|
+
"validator": mkdocs_pycafe.validator,
|
|
59
|
+
"format": mkdocs_pycafe.formatter(type="solara", next_formatter=formatter(unsafe_solara_execute), inside_last_div=False),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "python",
|
|
63
|
+
"class": "highlight",
|
|
64
|
+
"validator": mkdocs_pycafe.validator,
|
|
65
|
+
"format": mkdocs_pycafe.formatter(type="solara", next_formatter=formatter(unsafe_solara_execute), inside_last_div=False),
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
md_parser = solara.use_memo(make_markdown_object, dependencies=[unsafe_solara_execute])
|
|
73
|
+
|
|
30
74
|
with solara.v.Html(
|
|
31
75
|
tag="div",
|
|
32
76
|
style_="display: flex; flex-direction: row; justify-content: center; gap: 15px; max-width: 90%; margin: 0 auto;",
|
|
33
77
|
attributes={"id": "markdown-to-navigate"},
|
|
34
78
|
):
|
|
35
|
-
solara.Markdown(content, unsafe_solara_execute=unsafe_solara_execute, style="flex-grow: 1; max-width: min(100%, 1024px);")
|
|
79
|
+
solara.Markdown(content, unsafe_solara_execute=unsafe_solara_execute, style="flex-grow: 1; max-width: min(100%, 1024px);", md_parser=md_parser)
|
|
36
80
|
MarkdownNavigation(id="markdown-to-navigate").key("markdown-nav" + str(hash(content)))
|
|
37
81
|
|
|
38
82
|
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Solara Changelog
|
|
2
2
|
|
|
3
|
+
## Version 1.41.0
|
|
4
|
+
* Feature: Mutation detection is now available under the `SOLARA_STORAGE_MUTATION_DETECTION` environmental variable. [#595](https://github.com/widgetti/solara/pull/595).
|
|
5
|
+
* Feature: Autofocusing text inputs is now supported. [#788](https://github.com/widgetti/solara/pull/788).
|
|
6
|
+
* Feature: Custom colours are now supported for the Solara loading spinner. [#858](https://github.com/widgetti/solara/pull/858)
|
|
7
|
+
* Bug Fix: Echarts responsive size is now properly supported. [#273](https://github.com/widgetti/solara/pull/273).
|
|
8
|
+
* Bug Fix: Some version checks would prevent Solara from starting. [#904](https://github.com/widgetti/solara/pull/904).
|
|
9
|
+
* Bug Fix: Solara apps running in qt mode (`--qt`) should now always work correctly. [#856](https://github.com/widgetti/solara/pull/856).
|
|
10
|
+
* 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
|
+
|
|
3
12
|
## Version 1.40.0
|
|
4
13
|
* 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)
|
|
5
14
|
* Feature: Support styling input field of [ChatInput component](https://solara.dev/documentation/components/lab/chat). [#800](https://github.com/widgetti/solara/pull/800).
|
|
@@ -13,11 +13,10 @@ df = plotly.data.gapminder()
|
|
|
13
13
|
@solara.component
|
|
14
14
|
def Page():
|
|
15
15
|
solara.provide_cross_filter()
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return main
|
|
16
|
+
|
|
17
|
+
solara.CrossFilterReport(df, classes=["py-2"])
|
|
18
|
+
solara.CrossFilterSelect(df, "country")
|
|
19
|
+
solara.CrossFilterDataFrame(df)
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
__doc__ += apidoc(solara.CrossFilterDataFrame.f) # type: ignore
|
|
@@ -12,11 +12,9 @@ df = plotly.data.gapminder()
|
|
|
12
12
|
@solara.component
|
|
13
13
|
def Page():
|
|
14
14
|
solara.provide_cross_filter()
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
solara.CrossFilterSlider(df, "gdpPercap", mode=">")
|
|
19
|
-
return main
|
|
15
|
+
solara.CrossFilterReport(df, classes=["py-2"])
|
|
16
|
+
solara.CrossFilterSelect(df, "country")
|
|
17
|
+
solara.CrossFilterSlider(df, "gdpPercap", mode=">")
|
|
20
18
|
|
|
21
19
|
|
|
22
20
|
__doc__ += apidoc(solara.CrossFilterReport.f) # type: ignore
|
|
@@ -12,11 +12,9 @@ df = plotly.data.tips()
|
|
|
12
12
|
@solara.component
|
|
13
13
|
def Page():
|
|
14
14
|
solara.provide_cross_filter()
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
solara.CrossFilterSelect(df, "time")
|
|
19
|
-
return main
|
|
15
|
+
solara.CrossFilterReport(df, classes=["py-2"])
|
|
16
|
+
solara.CrossFilterSelect(df, "sex")
|
|
17
|
+
solara.CrossFilterSelect(df, "time")
|
|
20
18
|
|
|
21
19
|
|
|
22
20
|
__doc__ += apidoc(solara.CrossFilterSelect.f) # type: ignore
|
|
@@ -12,11 +12,9 @@ df = plotly.data.gapminder()
|
|
|
12
12
|
@solara.component
|
|
13
13
|
def Page():
|
|
14
14
|
solara.provide_cross_filter()
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
solara.CrossFilterSlider(df, "gdpPercap", mode="<")
|
|
19
|
-
return main
|
|
15
|
+
solara.CrossFilterReport(df, classes=["py-2"])
|
|
16
|
+
solara.CrossFilterSlider(df, "pop", mode=">")
|
|
17
|
+
solara.CrossFilterSlider(df, "gdpPercap", mode="<")
|
|
20
18
|
|
|
21
19
|
|
|
22
20
|
__doc__ += apidoc(solara.CrossFilterSlider.f) # type: ignore
|
|
@@ -15,11 +15,9 @@ df = plotly.data.gapminder()
|
|
|
15
15
|
@solara.component
|
|
16
16
|
def Page():
|
|
17
17
|
solara.provide_cross_filter()
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
solara.CrossFilterSlider(df, "gdpPercap", mode=">")
|
|
22
|
-
return main
|
|
18
|
+
solara.CrossFilterReport(df, classes=["py-2"])
|
|
19
|
+
solara.CrossFilterSelect(df, "continent")
|
|
20
|
+
solara.CrossFilterSlider(df, "gdpPercap", mode=">")
|
|
23
21
|
|
|
24
22
|
|
|
25
23
|
__doc__ += apidoc(solara.use_cross_filter) # type: ignore
|
|
@@ -18,16 +18,14 @@ def Page():
|
|
|
18
18
|
value_previous = solara.use_previous(value)
|
|
19
19
|
exception, clear_exception = solara.use_exception()
|
|
20
20
|
# print(exception)
|
|
21
|
-
|
|
22
|
-
if exception:
|
|
21
|
+
if exception:
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
def reset():
|
|
24
|
+
set_value(value_previous)
|
|
25
|
+
clear_exception()
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return main
|
|
27
|
+
solara.Text("Exception: " + str(exception))
|
|
28
|
+
solara.Button(label="Go to previous state", on_click=reset)
|
|
29
|
+
else:
|
|
30
|
+
solara.IntSlider(value=value, min=0, max=10, on_value=set_value, label="Pick a number, except 3")
|
|
31
|
+
UnstableComponent(value)
|
|
@@ -20,14 +20,11 @@ title = "use_previous"
|
|
|
20
20
|
def Page():
|
|
21
21
|
value, set_value = solara.use_state(4)
|
|
22
22
|
value_previous = solara.use_previous(value)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
**Current**: `{value}`
|
|
23
|
+
solara.IntSlider("value", value=value, on_value=set_value)
|
|
24
|
+
solara.Markdown(
|
|
25
|
+
f"""
|
|
26
|
+
**Current**: `{value}`
|
|
28
27
|
|
|
29
|
-
|
|
28
|
+
**Previous**: `{value_previous}`
|
|
30
29
|
"""
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return main
|
|
30
|
+
)
|