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.
Files changed (47) hide show
  1. solara/__init__.py +1 -1
  2. solara/__main__.py +7 -1
  3. solara/_stores.py +185 -0
  4. solara/components/component_vue.py +23 -0
  5. solara/components/echarts.py +5 -2
  6. solara/components/echarts.vue +22 -5
  7. solara/components/file_drop.py +20 -0
  8. solara/components/input.py +16 -0
  9. solara/components/markdown.py +22 -13
  10. solara/components/spinner-solara.vue +2 -2
  11. solara/components/spinner.py +17 -2
  12. solara/hooks/use_reactive.py +8 -1
  13. solara/reactive.py +9 -3
  14. solara/server/kernel.py +2 -1
  15. solara/server/qt.py +1 -1
  16. solara/server/starlette.py +2 -2
  17. solara/server/static/solara_bootstrap.py +1 -1
  18. solara/settings.py +14 -0
  19. solara/template/portal/pyproject.toml +1 -1
  20. solara/test/pytest_plugin.py +3 -0
  21. solara/toestand.py +139 -16
  22. solara/util.py +22 -0
  23. solara/website/components/markdown.py +45 -1
  24. solara/website/pages/changelog/changelog.md +9 -0
  25. solara/website/pages/documentation/api/cross_filter/cross_filter_dataframe.py +4 -5
  26. solara/website/pages/documentation/api/cross_filter/cross_filter_report.py +3 -5
  27. solara/website/pages/documentation/api/cross_filter/cross_filter_select.py +3 -5
  28. solara/website/pages/documentation/api/cross_filter/cross_filter_slider.py +3 -5
  29. solara/website/pages/documentation/api/hooks/use_cross_filter.py +3 -5
  30. solara/website/pages/documentation/api/hooks/use_exception.py +9 -11
  31. solara/website/pages/documentation/api/hooks/use_previous.py +6 -9
  32. solara/website/pages/documentation/api/hooks/use_state_or_update.py +23 -26
  33. solara/website/pages/documentation/api/hooks/use_thread.py +11 -13
  34. solara/website/pages/documentation/api/utilities/on_kernel_start.py +17 -0
  35. solara/website/pages/documentation/components/input/input.py +22 -0
  36. solara/website/pages/documentation/components/viz/echarts.py +3 -1
  37. solara/website/pages/documentation/examples/general/pokemon_search.py +3 -3
  38. solara/website/pages/documentation/examples/visualization/linked_views.py +0 -3
  39. solara/website/pages/documentation/getting_started/content/00-quickstart.md +1 -1
  40. solara/website/pages/documentation/getting_started/content/01-introduction.md +1 -1
  41. solara/website/pages/roadmap/roadmap.md +3 -0
  42. {solara_ui-1.41.0.dist-info → solara_ui-1.42.0.dist-info}/METADATA +8 -5
  43. {solara_ui-1.41.0.dist-info → solara_ui-1.42.0.dist-info}/RECORD +47 -46
  44. {solara_ui-1.41.0.dist-info → solara_ui-1.42.0.dist-info}/WHEEL +1 -1
  45. {solara_ui-1.41.0.data → solara_ui-1.42.0.data}/data/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
  46. {solara_ui-1.41.0.data → solara_ui-1.42.0.data}/data/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
  47. {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, ipykernel.__version__.split(".")))
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
@@ -81,7 +81,7 @@ class QWebEngineViewWithPopup(QWebEngineView):
81
81
 
82
82
 
83
83
  def run_qt(url):
84
- app = QApplication([])
84
+ app = QApplication(["Solara App"])
85
85
  web = QWebEngineViewWithPopup()
86
86
  web.setUrl(QtCore.QUrl(url))
87
87
  web.resize(1024, 1024)
@@ -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
- ws_version = tuple(map(int, websockets.__version__.split(".")))
90
- if ws_version[0] >= 13:
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.41.0-py2.py3-none-any.whl", keep_going=True)
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']")
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ["hatchling >=0.25"]
2
+ requires = ["hatchling==1.26.3"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
@@ -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 reacton.utils import equals
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 equals(new_state, prev_state.current):
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 = KernelStoreValue(default_value, key=key)
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
- with solara.VBox() as main:
17
- solara.CrossFilterReport(df, classes=["py-2"])
18
- solara.CrossFilterSelect(df, "country")
19
- solara.CrossFilterDataFrame(df)
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
- with solara.VBox() as main:
16
- solara.CrossFilterReport(df, classes=["py-2"])
17
- solara.CrossFilterSelect(df, "country")
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
- with solara.VBox() as main:
16
- solara.CrossFilterReport(df, classes=["py-2"])
17
- solara.CrossFilterSelect(df, "sex")
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
- with solara.VBox() as main:
16
- solara.CrossFilterReport(df, classes=["py-2"])
17
- solara.CrossFilterSlider(df, "pop", mode=">")
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
- with solara.VBox() as main:
19
- solara.CrossFilterReport(df, classes=["py-2"])
20
- solara.CrossFilterSelect(df, "continent")
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
- with solara.VBox() as main:
22
- if exception:
21
+ if exception:
23
22
 
24
- def reset():
25
- set_value(value_previous)
26
- clear_exception()
23
+ def reset():
24
+ set_value(value_previous)
25
+ clear_exception()
27
26
 
28
- solara.Text("Exception: " + str(exception))
29
- solara.Button(label="Go to previous state", on_click=reset)
30
- else:
31
- solara.IntSlider(value=value, min=0, max=10, on_value=set_value, label="Pick a number, except 3")
32
- UnstableComponent(value)
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
- with solara.VBox() as main:
24
- solara.IntSlider("value", value=value, on_value=set_value)
25
- solara.Markdown(
26
- f"""
27
- **Current**: `{value}`
23
+ solara.IntSlider("value", value=value, on_value=set_value)
24
+ solara.Markdown(
25
+ f"""
26
+ **Current**: `{value}`
28
27
 
29
- **Previous**: `{value_previous}`
28
+ **Previous**: `{value_previous}`
30
29
  """
31
- )
32
-
33
- return main
30
+ )