solara-ui 1.41.0__py2.py3-none-any.whl → 1.43.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 (83) hide show
  1. solara/__init__.py +1 -1
  2. solara/__main__.py +17 -6
  3. solara/_stores.py +189 -0
  4. solara/components/__init__.py +18 -1
  5. solara/components/component_vue.py +23 -0
  6. solara/components/datatable.py +4 -4
  7. solara/components/echarts.py +5 -2
  8. solara/components/echarts.vue +22 -5
  9. solara/components/file_drop.py +20 -0
  10. solara/components/input.py +21 -1
  11. solara/components/markdown.py +62 -17
  12. solara/components/misc.py +2 -2
  13. solara/components/spinner-solara.vue +2 -2
  14. solara/components/spinner.py +17 -2
  15. solara/hooks/use_reactive.py +8 -1
  16. solara/lab/components/__init__.py +1 -0
  17. solara/lab/components/chat.py +3 -3
  18. solara/lab/components/input_time.py +133 -0
  19. solara/lab/hooks/dataframe.py +1 -0
  20. solara/lab/utils/dataframe.py +11 -1
  21. solara/reactive.py +9 -3
  22. solara/server/app.py +63 -30
  23. solara/server/flask.py +12 -2
  24. solara/server/jupyter/server_extension.py +1 -0
  25. solara/server/kernel.py +52 -4
  26. solara/server/kernel_context.py +66 -7
  27. solara/server/patch.py +25 -29
  28. solara/server/qt.py +1 -1
  29. solara/server/server.py +15 -5
  30. solara/server/settings.py +11 -0
  31. solara/server/shell.py +19 -1
  32. solara/server/starlette.py +39 -11
  33. solara/server/static/solara_bootstrap.py +1 -1
  34. solara/settings.py +17 -0
  35. solara/tasks.py +18 -8
  36. solara/template/portal/pyproject.toml +1 -1
  37. solara/test/pytest_plugin.py +4 -0
  38. solara/toestand.py +170 -16
  39. solara/util.py +40 -0
  40. solara/website/components/docs.py +4 -0
  41. solara/website/components/markdown.py +60 -2
  42. solara/website/pages/changelog/changelog.md +17 -0
  43. solara/website/pages/documentation/advanced/content/20-understanding/50-solara-server.md +10 -0
  44. solara/website/pages/documentation/api/cross_filter/cross_filter_dataframe.py +4 -5
  45. solara/website/pages/documentation/api/cross_filter/cross_filter_report.py +3 -5
  46. solara/website/pages/documentation/api/cross_filter/cross_filter_select.py +3 -5
  47. solara/website/pages/documentation/api/cross_filter/cross_filter_slider.py +3 -5
  48. solara/website/pages/documentation/api/hooks/use_cross_filter.py +3 -5
  49. solara/website/pages/documentation/api/hooks/use_exception.py +9 -11
  50. solara/website/pages/documentation/api/hooks/use_previous.py +6 -9
  51. solara/website/pages/documentation/api/hooks/use_state_or_update.py +23 -26
  52. solara/website/pages/documentation/api/hooks/use_thread.py +11 -13
  53. solara/website/pages/documentation/api/routing/route.py +10 -12
  54. solara/website/pages/documentation/api/routing/use_route.py +26 -30
  55. solara/website/pages/documentation/api/utilities/on_kernel_start.py +17 -0
  56. solara/website/pages/documentation/components/advanced/link.py +6 -8
  57. solara/website/pages/documentation/components/advanced/meta.py +6 -9
  58. solara/website/pages/documentation/components/advanced/style.py +7 -9
  59. solara/website/pages/documentation/components/input/file_browser.py +12 -14
  60. solara/website/pages/documentation/components/input/input.py +22 -0
  61. solara/website/pages/documentation/components/lab/input_time.py +15 -0
  62. solara/website/pages/documentation/components/layout/columns_responsive.py +37 -39
  63. solara/website/pages/documentation/components/layout/gridfixed.py +4 -6
  64. solara/website/pages/documentation/components/output/html.py +1 -3
  65. solara/website/pages/documentation/components/page/head.py +4 -7
  66. solara/website/pages/documentation/components/viz/echarts.py +3 -1
  67. solara/website/pages/documentation/examples/__init__.py +9 -0
  68. solara/website/pages/documentation/examples/ai/chatbot.py +60 -44
  69. solara/website/pages/documentation/examples/general/live_update.py +1 -0
  70. solara/website/pages/documentation/examples/general/pokemon_search.py +3 -3
  71. solara/website/pages/documentation/examples/visualization/linked_views.py +0 -3
  72. solara/website/pages/documentation/faq/content/99-faq.md +9 -0
  73. solara/website/pages/documentation/getting_started/content/00-quickstart.md +2 -2
  74. solara/website/pages/documentation/getting_started/content/01-introduction.md +1 -1
  75. solara/website/pages/documentation/getting_started/content/04-tutorials/_jupyter_dashboard_1.ipynb +23 -19
  76. solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md +2 -2
  77. solara/website/pages/roadmap/roadmap.md +6 -0
  78. {solara_ui-1.41.0.dist-info → solara_ui-1.43.0.dist-info}/METADATA +9 -6
  79. {solara_ui-1.41.0.dist-info → solara_ui-1.43.0.dist-info}/RECORD +83 -80
  80. {solara_ui-1.41.0.dist-info → solara_ui-1.43.0.dist-info}/WHEEL +1 -1
  81. {solara_ui-1.41.0.data → solara_ui-1.43.0.data}/data/etc/jupyter/jupyter_notebook_config.d/solara.json +0 -0
  82. {solara_ui-1.41.0.data → solara_ui-1.43.0.data}/data/etc/jupyter/jupyter_server_config.d/solara.json +0 -0
  83. {solara_ui-1.41.0.dist-info → solara_ui-1.43.0.dist-info}/licenses/LICENSE +0 -0
@@ -4,7 +4,8 @@ import logging
4
4
  import textwrap
5
5
  import traceback
6
6
  import warnings
7
- from typing import Any, Dict, List, Union, cast
7
+ from typing import Any, Callable, Dict, List, Optional, Union, cast
8
+ import typing
8
9
 
9
10
  import ipyvuetify as v
10
11
 
@@ -16,6 +17,7 @@ try:
16
17
  has_pymdownx = True
17
18
  except ModuleNotFoundError:
18
19
  has_pymdownx = False
20
+ import reacton.core
19
21
 
20
22
  import solara
21
23
  import solara.components.applayout
@@ -30,6 +32,9 @@ else:
30
32
  from pygments.formatters import HtmlFormatter
31
33
  from pygments.lexers import get_lexer_by_name
32
34
 
35
+ if typing.TYPE_CHECKING:
36
+ import markdown
37
+
33
38
  logger = logging.getLogger(__name__)
34
39
 
35
40
  html_no_execute_enabled = "<div><i>Solara execution is not enabled</i></div>"
@@ -50,7 +55,7 @@ def ExceptionGuard(children=[]):
50
55
  solara.Column(children=children)
51
56
 
52
57
 
53
- def _run_solara(code):
58
+ def _run_solara(code, cleanups):
54
59
  ast = compile(code, "markdown", "exec")
55
60
  local_scope: Dict[Any, Any] = {}
56
61
  exec(ast, local_scope)
@@ -63,6 +68,13 @@ def _run_solara(code):
63
68
  else:
64
69
  raise NameError("No Page or app defined")
65
70
  box = v.Html(tag="div")
71
+
72
+ rc: reacton.core.RenderContext
73
+
74
+ def cleanup():
75
+ rc.close()
76
+
77
+ cleanups.append(cleanup)
66
78
  box, rc = solara.render(cast(solara.Element, app), container=box) # type: ignore
67
79
  widget_id = box._model_id
68
80
  return (
@@ -231,9 +243,8 @@ module.exports = {
231
243
  return template
232
244
 
233
245
 
234
- def _highlight(src, language, unsafe_solara_execute, extra, *args, **kwargs):
246
+ def _highlight(src, language, class_name=None, options=None, md=None, unsafe_solara_execute=False, cleanups=None, **kwargs):
235
247
  """Highlight a block of code"""
236
-
237
248
  if not has_pygments:
238
249
  warnings.warn("Pygments is not installed, code highlighting will not work, use pip install pygments to install it.")
239
250
  src_safe = html.escape(src)
@@ -250,7 +261,7 @@ def _highlight(src, language, unsafe_solara_execute, extra, *args, **kwargs):
250
261
 
251
262
  if run_src_with_solara:
252
263
  if unsafe_solara_execute:
253
- html_widget = _run_solara(src)
264
+ html_widget = _run_solara(src, cleanups)
254
265
  return src_html + html_widget
255
266
  else:
256
267
  return src_html + html_no_execute_enabled
@@ -258,6 +269,19 @@ def _highlight(src, language, unsafe_solara_execute, extra, *args, **kwargs):
258
269
  return src_html
259
270
 
260
271
 
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
283
+
284
+
261
285
  @solara.component
262
286
  def MarkdownIt(md_text: str, highlight: List[int] = [], unsafe_solara_execute: bool = False):
263
287
  md_text = textwrap.dedent(md_text)
@@ -267,8 +291,10 @@ def MarkdownIt(md_text: str, highlight: List[int] = [], unsafe_solara_execute: b
267
291
  from mdit_py_plugins.footnote import footnote_plugin # noqa: F401
268
292
  from mdit_py_plugins.front_matter import front_matter_plugin # noqa: F401
269
293
 
294
+ cleanups = solara.use_ref(cast(List[Callable[[], None]], []))
295
+
270
296
  def highlight_code(code, name, attrs):
271
- return _highlight(code, name, unsafe_solara_execute, attrs)
297
+ return _highlight(cleanups.current, code, name, unsafe_solara_execute, attrs)
272
298
 
273
299
  md = MarkdownItMod(
274
300
  "js-default",
@@ -281,6 +307,15 @@ def MarkdownIt(md_text: str, highlight: List[int] = [], unsafe_solara_execute: b
281
307
  md = md.use(container.container_plugin, name="note")
282
308
  html = md.render(md_text)
283
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)
284
319
  return v.VuetifyTemplate.element(template=_markdown_template(html)).key(hash)
285
320
 
286
321
 
@@ -293,7 +328,7 @@ def _no_deep_copy_emojione(options, md):
293
328
 
294
329
 
295
330
  @solara.component
296
- def Markdown(md_text: str, unsafe_solara_execute=False, style: Union[str, Dict, None] = None):
331
+ def Markdown(md_text: str, unsafe_solara_execute=False, style: Union[str, Dict, None] = None, md_parser: Optional["markdown.Markdown"] = None):
297
332
  """Renders markdown text
298
333
 
299
334
  Renders markdown using https://python-markdown.github.io/
@@ -333,21 +368,19 @@ def Markdown(md_text: str, unsafe_solara_execute=False, style: Union[str, Dict,
333
368
  * `unsafe_solara_execute`: If True, code marked with language "solara" will be executed. This is potentially unsafe
334
369
  if the markdown text can come from user input and should only be used for trusted markdown.
335
370
  * `style`: A string or dict of css styles to apply to the rendered markdown.
371
+ * `md_parser`: A markdown object to use for rendering. If not provided, a markdown object will be created.
336
372
 
337
373
  """
338
374
  import markdown
339
375
 
340
376
  md_text = textwrap.dedent(md_text)
341
377
  style = solara.util._flatten_style(style)
378
+ cleanups = solara.use_ref(cast(List[Callable[[], None]], []))
342
379
 
343
380
  def make_markdown_object():
344
- def highlight(src, language, *args, **kwargs):
345
- try:
346
- return _highlight(src, language, unsafe_solara_execute, *args, **kwargs)
347
- except Exception as e:
348
- logger.exception("Error highlighting code: %s", src)
349
- return repr(e)
350
-
381
+ if md_parser is not None:
382
+ # we won't use the use_memo
383
+ return None
351
384
  if has_pymdownx:
352
385
  return markdown.Markdown( # type: ignore
353
386
  extensions=[
@@ -371,7 +404,7 @@ def Markdown(md_text: str, unsafe_solara_execute=False, style: Union[str, Dict,
371
404
  {
372
405
  "name": "solara",
373
406
  "class": "",
374
- "format": highlight,
407
+ "format": formatter(unsafe_solara_execute, cleanups=cleanups.current),
375
408
  },
376
409
  ],
377
410
  },
@@ -388,8 +421,20 @@ def Markdown(md_text: str, unsafe_solara_execute=False, style: Union[str, Dict,
388
421
  ],
389
422
  )
390
423
 
391
- md = solara.use_memo(make_markdown_object, dependencies=[unsafe_solara_execute])
392
- html = md.convert(md_text)
424
+ md_self = solara.use_memo(make_markdown_object, dependencies=[unsafe_solara_execute])
425
+ if md_parser is None:
426
+ assert md_self is not None
427
+ md_parser = md_self
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, [])
393
438
  # if we update the template value, the whole vue tree will rerender (ipvue/ipyvuetify issue)
394
439
  # however, using the hash we simply generate a new widget each time
395
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 `Row` instead."""
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 `Column` instead."""
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;"
@@ -5,10 +5,10 @@
5
5
  xmlns:svgjs="http://svgjs.com/svgjs" :width="size" :height="size" viewBox="0 0 65 65" fill="none">
6
6
  <path id="sun-spinner1" style="transform-origin: center;"
7
7
  d="M57.11 30.64L61.47 17.87L48.7 13.51L42.76 1.39999L30.65 7.34999L17.87 2.97999L13.51 15.75L1.40002 21.7L7.35002 33.81L2.99002 46.58L15.76 50.94L21.71 63.06L33.82 57.11L46.59 61.47L50.95 48.7L63.06 42.75L57.11 30.64ZM54.26 34.39L34.39 54.26C33.19 55.46 31.25 55.46 30.05 54.26L10.2 34.4C9.00002 33.2 9.00002 31.26 10.2 30.07L30.06 10.2C31.26 8.99999 33.2 8.99999 34.4 10.2L54.27 30.07C55.47 31.27 55.47 33.21 54.27 34.4L54.26 34.39Z"
8
- fill="#FFCF64"></path>
8
+ :fill="color_back"></path>
9
9
  <path id="sun-spinner2" style="transform-origin: center;"
10
10
  d="M53.62 19.42L51.65 6.07L38.3 8.04L27.46 0L19.42 10.84L6.07 12.82L8.04 26.17L0 37L10.84 45.04L12.81 58.39L26.16 56.42L37 64.46L45.04 53.62L58.39 51.64L56.42 38.29L64.46 27.45L53.62 19.4V19.42ZM52.8 24.06L44.24 50.82C43.72 52.43 42 53.32 40.39 52.81L13.63 44.25C12.02 43.74 11.13 42.01 11.64 40.4L20.21 13.64C20.72 12.03 22.45 11.14 24.06 11.65L50.82 20.21C52.43 20.72 53.32 22.45 52.81 24.06H52.8Z"
11
- fill="#FF8C3E"></path>
11
+ :fill="color_front"></path>
12
12
  </svg>
13
13
  </div>
14
14
  </template>
@@ -8,12 +8,15 @@ class SpinnerSolaraWidget(ipyvue.VueTemplate):
8
8
  template_file = (__file__, "spinner-solara.vue")
9
9
 
10
10
  size = traitlets.Unicode("64px").tag(sync=True)
11
+ color_back = traitlets.Unicode("#FFCF64").tag(sync=True)
12
+ color_front = traitlets.Unicode("#FF8C3E").tag(sync=True)
11
13
 
12
14
 
13
15
  @solara.component
14
- def SpinnerSolara(size="64px"):
16
+ def SpinnerSolara(size="64px", color_back="#FFCF64", color_front="#FF8C3E"):
15
17
  """Spinner component with the Solara logo to indicate the app is busy.
16
18
 
19
+ ## Examples
17
20
  ### Basic example
18
21
 
19
22
  ```solara
@@ -24,7 +27,19 @@ def SpinnerSolara(size="64px"):
24
27
  solara.SpinnerSolara(size="100px")
25
28
  ```
26
29
 
30
+ ## Changing the colors
31
+ ```solara
32
+ import solara
33
+
34
+ @solara.component
35
+ def Page():
36
+ solara.SpinnerSolara(size="100px", color_back="Grey", color_front="Lime")
37
+ ```
38
+
39
+
27
40
  ## Arguments
28
41
  * `size`: Size of the spinner.
42
+ * `color_back`: Color of the spinner in the background.
43
+ * `color_front`: Color of the spinner in the foreground.
29
44
  """
30
- return SpinnerSolaraWidget.element(size=size)
45
+ return SpinnerSolaraWidget.element(size=size, color_back=color_back, color_front=color_front)
@@ -1,4 +1,4 @@
1
- from typing import Callable, Optional, TypeVar, Union
1
+ from typing import Any, Callable, Optional, TypeVar, Union
2
2
 
3
3
  import solara
4
4
 
@@ -8,6 +8,7 @@ T = TypeVar("T")
8
8
  def use_reactive(
9
9
  value: Union[T, solara.Reactive[T]],
10
10
  on_change: Optional[Callable[[T], None]] = None,
11
+ equals: Callable[[Any, Any], bool] = solara.util.equals_extra,
11
12
  ) -> solara.Reactive[T]:
12
13
  """Creates a reactive variable with the a local component scope.
13
14
 
@@ -44,6 +45,12 @@ def use_reactive(
44
45
  * on_change (Optional[Callable[[T], None]]): An optional callback function
45
46
  that will be called when the reactive variable's value changes.
46
47
 
48
+ * equals: A function that returns True if two values are considered equal, and False otherwise.
49
+ The default function is `solara.util.equals`, which performs a deep comparison of the two values
50
+ and is more forgiving than the default `==` operator.
51
+ You can provide a custom function if you need to define a different notion of equality.
52
+
53
+
47
54
  Returns:
48
55
  solara.Reactive[T]: A reactive variable with the specified initial value
49
56
  or the provided reactive variable.
@@ -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
@@ -43,7 +43,7 @@ def ChatBox(
43
43
 
44
44
  @solara.component
45
45
  def ChatInput(
46
- send_callback: Optional[Callable] = None,
46
+ send_callback: Optional[Callable[[str], None]] = None,
47
47
  disabled: bool = False,
48
48
  style: Optional[Union[str, Dict[str, str]]] = None,
49
49
  input_text_style: Optional[Union[str, Dict[str, str]]] = None,
@@ -55,7 +55,7 @@ def ChatInput(
55
55
 
56
56
  # Arguments
57
57
 
58
- * `send_callback`: A callback function for when the user presses enter or clicks the send button.
58
+ * `send_callback`: A callback function for when the user presses enter or clicks the send button taking the message as an argument.
59
59
  * `disabled`: Whether the input should be disabled. Useful for disabling sending further messages while a chatbot is replying,
60
60
  among other things.
61
61
  * `style`: CSS styles to apply to the `solara.Row` containing the input field and submit button. Either a string or a dictionary.
@@ -98,7 +98,7 @@ def ChatInput(
98
98
 
99
99
  @solara.component
100
100
  def ChatMessage(
101
- children: Union[List[solara.Element], str],
101
+ children: Union[List[solara.Element], str] = [],
102
102
  user: bool = False,
103
103
  avatar: Union[solara.Element, str, Literal[False], None] = None,
104
104
  name: Optional[str] = None,
@@ -0,0 +1,133 @@
1
+ import datetime as dt
2
+ from typing import Callable, Dict, List, Optional, Union
3
+
4
+ import solara
5
+ import solara.lab
6
+ from solara.lab.components.input_date import use_close_menu
7
+ from solara.components.input import _use_input_type
8
+
9
+
10
+ @solara.component
11
+ def InputTime(
12
+ value: Union[solara.Reactive[Optional[dt.time]], Optional[dt.time]],
13
+ on_value: Optional[Callable[[Optional[dt.time]], None]] = None,
14
+ label: str = "Pick a time",
15
+ children: List[solara.Element] = [],
16
+ open_value: Union[solara.Reactive[bool], bool] = False,
17
+ on_open_value: Optional[Callable[[bool], None]] = None,
18
+ optional: bool = False,
19
+ twelve_hour_clock: bool = False,
20
+ use_seconds: bool = False,
21
+ allowed_minutes: Optional[List[int]] = None,
22
+ style: Optional[Union[str, Dict[str, str]]] = None,
23
+ classes: Optional[List[str]] = None,
24
+ ):
25
+ """
26
+ Show a textfield, which when clicked, opens a timepicker. The input time should be of type `datetime.time`.
27
+
28
+ ## Basic Example
29
+
30
+ ```solara {pycafe-link}
31
+ import solara
32
+ import solara.lab
33
+ import datetime as dt
34
+
35
+
36
+ @solara.component
37
+ def Page():
38
+ time = solara.use_reactive(dt.time(12, 0))
39
+
40
+ solara.lab.InputTime(time, allowed_minutes=[0, 15, 30, 45])
41
+ solara.Text(str(time.value))
42
+ ```
43
+
44
+ ## Arguments
45
+
46
+ * value: Reactive variable of type `datetime.time`, or `None`. This time is selected the first time the component is rendered.
47
+ * on_value: a callback function for when value changes. The callback function receives the new value as an argument.
48
+ * label: Text used to label the text field that triggers the timepicker.
49
+ * children: List of Elements to be rendered under the timepicker. If empty, a close button is rendered.
50
+ * open_value: Controls and communicates the state of the timepicker. If True, the timepicker is open. If False, the timepicker is closed.
51
+ Intended to be used in conjunction with a custom set of controls to close the timepicker.
52
+ * on_open_value: a callback function for when open_value changes. Also receives the new value as an argument.
53
+ * optional: Determines whether to show an error when value is `None`. If `True`, no error is shown.
54
+ * twelve_hour_clock: If `True`, the timepicker will display in 12-hour format. If `False`, the timepicker will display in 24-hour format.
55
+ * use_seconds: If `True`, the timepicker will allow input of seconds.
56
+ * allowed_minutes: List of allowed minutes for the timepicker. Restricts the input to specific minute intervals.
57
+ * style: CSS style to apply to the text field. Either a string or a dictionary of CSS properties (i.e. `{"property": "value"}`).
58
+ * classes: List of CSS classes to apply to the text field.
59
+ """
60
+ time_format_internal = f"%H:%M{':%S' if use_seconds else ''}"
61
+ time_format_display = f"%H:%M{':%S' if use_seconds else ''}"
62
+ if twelve_hour_clock:
63
+ time_format_display = f"%I:%M{':%S' if use_seconds else ''} %p"
64
+ value_reactive = solara.use_reactive(value, on_value) # type: ignore
65
+ del value, on_value
66
+ timepicker_is_open = solara.use_reactive(open_value, on_open_value) # type: ignore
67
+ del open_value, on_open_value
68
+
69
+ def set_time_typed_cast(value: Optional[str]):
70
+ if value:
71
+ try:
72
+ time_value = dt.datetime.strptime(value, time_format_display).time()
73
+ return time_value
74
+ except ValueError:
75
+ raise ValueError(f"Time {value} does not match format {time_format_display.replace('%', '')}")
76
+ elif optional:
77
+ return None
78
+ else:
79
+ raise ValueError("Time cannot be empty")
80
+
81
+ def time_to_str(time: Optional[dt.time]) -> str:
82
+ if time is not None:
83
+ return time.strftime(time_format_display)
84
+ return ""
85
+
86
+ def set_time_cast(new_value: Optional[str]):
87
+ if new_value:
88
+ time_value = dt.datetime.strptime(new_value, time_format_internal).time()
89
+ value_reactive.value = time_value
90
+
91
+ def standard_strfy(time: Optional[dt.time]):
92
+ if time is None:
93
+ return None
94
+ else:
95
+ return time.strftime(time_format_internal)
96
+
97
+ time_standard_str = standard_strfy(value_reactive.value)
98
+
99
+ style_flat = solara.util._flatten_style(style)
100
+
101
+ internal_value, error_message, set_value_cast = _use_input_type(value_reactive, set_time_typed_cast, time_to_str)
102
+
103
+ if error_message:
104
+ label += f" ({error_message})"
105
+ input = solara.v.TextField(
106
+ label=label,
107
+ v_model=internal_value,
108
+ on_v_model=set_value_cast,
109
+ append_icon="mdi-clock",
110
+ error=bool(error_message),
111
+ style_="min-width: 290px;" + style_flat,
112
+ class_=", ".join(classes) if classes else "",
113
+ )
114
+
115
+ use_close_menu(input, timepicker_is_open)
116
+
117
+ with solara.lab.Menu(
118
+ activator=input,
119
+ close_on_content_click=False,
120
+ open_value=timepicker_is_open,
121
+ use_activator_width=False,
122
+ ):
123
+ with solara.v.TimePicker(
124
+ ampm_in_title=twelve_hour_clock,
125
+ v_model=time_standard_str,
126
+ on_v_model=set_time_cast,
127
+ format="24hr" if not twelve_hour_clock else "ampm",
128
+ allowed_minutes=allowed_minutes,
129
+ use_seconds=use_seconds,
130
+ style_="width: 100%;",
131
+ ):
132
+ if len(children) > 0:
133
+ solara.display(*children)
@@ -1 +1,2 @@
1
1
  from ..utils.dataframe import df_columns as use_df_column_names # noqa: F401
2
+ from ..utils.dataframe import df_row_names as df_row_names
@@ -1,4 +1,4 @@
1
- from typing import List
1
+ from typing import List, Union
2
2
 
3
3
 
4
4
  def get_pandas_major():
@@ -28,6 +28,16 @@ def df_columns(df) -> List[str]:
28
28
  raise TypeError(f"{type(df)} not supported")
29
29
 
30
30
 
31
+ def df_row_names(df) -> List[Union[int, str]]:
32
+ """Return a list of row names from a dataframe."""
33
+ if df_type(df) == "vaex" or df_type(df) == "polars":
34
+ return list(range(df_len(df)))
35
+ elif df_type(df) == "pandas":
36
+ return df.index.tolist()
37
+ else:
38
+ raise TypeError(f"{type(df)} not supported")
39
+
40
+
31
41
  def df_slice(df, start: int, stop: int):
32
42
  """Return a subset of rows from a dataframe."""
33
43
  if df_type(df) == "pandas":
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)