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