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
solara/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Build webapps using IPywidgets"""
2
2
 
3
- __version__ = "1.41.0"
3
+ __version__ = "1.43.0"
4
4
  github_url = "https://github.com/widgetti/solara"
5
5
  git_branch = "master"
6
6
 
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
- print(f"New version of Solara available: {latest_version}. You have {solara.__version__}. Please upgrade using:") # noqa: T201
84
- print(f'\t$ pip install "solara=={latest_version}"') # noqa: T201
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():
@@ -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
@@ -341,7 +345,13 @@ def run(
341
345
  reload_excludes = restart_excludes if restart_excludes else []
342
346
  del restart_excludes
343
347
  reload_excludes = [str(solara_root / "website"), str(solara_root / "template")]
344
- reload_excludes.append(app)
348
+ app_path = Path(app)
349
+ if app_path.exists():
350
+ # if app is not a child of the current working directory
351
+ # uvicorn crashes
352
+ if not str(app_path.resolve()).startswith(str(Path.cwd().resolve())):
353
+ reload_excludes.append(str(app_path.resolve()))
354
+ del app_path
345
355
  del solara_root
346
356
  reload = True
347
357
  # avoid sending many restarts
@@ -384,7 +394,8 @@ def run(
384
394
  if open and not qt:
385
395
  threading.Thread(target=open_browser, daemon=True).start()
386
396
 
387
- rich.print(f"Solara server is starting at {url}")
397
+ with print_mutex:
398
+ rich.print(f"Solara server is starting at {url}")
388
399
 
389
400
  if log_level is not None:
390
401
  LOGGING_CONFIG["loggers"]["solara"]["level"] = log_level.upper()
solara/_stores.py ADDED
@@ -0,0 +1,189 @@
1
+ import copy
2
+ import dataclasses
3
+ import inspect
4
+ from typing import Callable, ContextManager, Generic, Optional, Union, cast
5
+ import warnings
6
+ from .toestand import ValueBase, KernelStore, S, _find_outside_solara_frame
7
+ import solara.util
8
+
9
+
10
+ class _PublicValueNotSet:
11
+ pass
12
+
13
+
14
+ class _SetValueNotSet:
15
+ pass
16
+
17
+
18
+ @dataclasses.dataclass
19
+ class StoreValue(Generic[S]):
20
+ private: S # the internal private value, should never be mutated
21
+ public: Union[S, _PublicValueNotSet] # this is the value that is exposed in .get(), it is a deep copy of private
22
+ get_traceback: Optional[inspect.Traceback]
23
+ set_value: Union[S, _SetValueNotSet] # the value that was set using .set(..), we deepcopy this to set private
24
+ set_traceback: Optional[inspect.Traceback]
25
+
26
+
27
+ class MutateDetectorStore(ValueBase[S]):
28
+ def __init__(self, store: KernelStore[StoreValue[S]], equals=solara.util.equals_extra):
29
+ self._storage = store
30
+ self._enabled = True
31
+ super().__init__(equals=equals)
32
+
33
+ @property
34
+ def lock(self):
35
+ return self._storage.lock
36
+
37
+ def get(self) -> S:
38
+ self.check_mutations()
39
+ self._ensure_public_exists()
40
+ value = self._storage.get()
41
+ # value.public is of type Optional[S], so it's tempting to check for None here,
42
+ # but S could include None as a valid value, so best we can do is cast
43
+ public_value = cast(S, value.public)
44
+ return public_value
45
+
46
+ def peek(self) -> S:
47
+ """Return the value without automatically subscribing to listeners."""
48
+ self.check_mutations()
49
+ store_value = self._storage.peek()
50
+ self._ensure_public_exists()
51
+ public_value = cast(S, store_value.public)
52
+ return public_value
53
+
54
+ def set(self, value: S):
55
+ self.check_mutations()
56
+ self._ensure_public_exists()
57
+ private = copy.deepcopy(value)
58
+ self._check_equals(private, value)
59
+ frame = _find_outside_solara_frame()
60
+ if frame is not None:
61
+ frame_info = inspect.getframeinfo(frame)
62
+ else:
63
+ frame_info = None
64
+ store_value = StoreValue(private=private, public=_PublicValueNotSet(), get_traceback=None, set_value=value, set_traceback=frame_info)
65
+ self._storage.set(store_value)
66
+
67
+ def check_mutations(self):
68
+ self._storage._check_mutation()
69
+ if not self._enabled:
70
+ return
71
+ store_value = self._storage.peek()
72
+ if not isinstance(store_value.public, _PublicValueNotSet) and not self.equals(store_value.public, store_value.private):
73
+ tb = store_value.get_traceback
74
+ # TODO: make the error message as elaborate as below
75
+ msg = (
76
+ f"Reactive variable was read when it had the value of {store_value.private!r}, but was later mutated to {store_value.public!r}.\n"
77
+ "Mutation should not be done on the value of a reactive variable, as in production mode we would be unable to track changes.\n"
78
+ )
79
+ if tb:
80
+ if tb.code_context:
81
+ code = tb.code_context[0]
82
+ else:
83
+ code = "<No code context available>"
84
+ msg += f"The last value was read in the following code:\n" f"{tb.filename}:{tb.lineno}\n" f"{code}"
85
+ raise ValueError(msg)
86
+ elif not isinstance(store_value.set_value, _SetValueNotSet) and not self.equals(store_value.set_value, store_value.private):
87
+ tb = store_value.set_traceback
88
+ 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}.
89
+
90
+ Mutation should not be done on the value of a reactive variable, as in production mode we would be unable to track changes.
91
+
92
+ Bad:
93
+ mylist = reactive([]]
94
+ some_values = [1, 2, 3]
95
+ mylist.value = some_values # you give solara a reference to your list
96
+ some_values.append(4) # but later mutate it (solara cannot detect this change, so a render will not be triggered)
97
+ # if later on a re-render happens for a different reason, you will read of the mutated list.
98
+
99
+ Good (if you want the reactive variable to be updated):
100
+ mylist = reactive([]]
101
+ some_values = [1, 2, 3]
102
+ mylist.value = some_values
103
+ mylist.value = some_values + [4]
104
+
105
+ Good (if you want to keep mutating your own list):
106
+ mylist = reactive([]]
107
+ some_values = [1, 2, 3]
108
+ mylist.value = some_values.copy() # this gives solara a copy of the list
109
+ some_values.append(4) # you are free to mutate your own list, solara will not see this
110
+
111
+ """
112
+ if tb:
113
+ if tb.code_context:
114
+ code = tb.code_context[0]
115
+ else:
116
+ code = "<No code context available>"
117
+ msg += "The last time the value was set was at:\n" f"{tb.filename}:{tb.lineno}\n" f"{code}"
118
+ raise ValueError(msg)
119
+
120
+ def _ensure_public_exists(self):
121
+ store_value = self._storage.peek()
122
+ if isinstance(store_value.public, _PublicValueNotSet):
123
+ with self.lock:
124
+ if isinstance(store_value.public, _PublicValueNotSet):
125
+ frame = _find_outside_solara_frame()
126
+ if frame is not None:
127
+ frame_info = inspect.getframeinfo(frame)
128
+ else:
129
+ frame_info = None
130
+ store_value.public = copy.deepcopy(store_value.private)
131
+ self._check_equals(store_value.public, store_value.private)
132
+ store_value.get_traceback = frame_info
133
+
134
+ def _check_equals(self, a: S, b: S):
135
+ if not self._enabled:
136
+ return
137
+ if not self.equals(a, b):
138
+ frame = _find_outside_solara_frame()
139
+ if frame is not None:
140
+ frame_info = inspect.getframeinfo(frame)
141
+ else:
142
+ frame_info = None
143
+
144
+ warn = """The equals function for this reactive value returned False when comparing a deepcopy to itself.
145
+
146
+ This reactive variable will not be able to detect mutations correctly, and is therefore disabled.
147
+
148
+ To avoid this warning, and to ensure that mutation detection works correctly, please provide a better equals function to the reactive variable.
149
+ 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.
150
+
151
+ Example:
152
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
153
+ reactive_df = solara.reactive(df, equals=solara.util.equals_pickle)
154
+ """
155
+ tb = frame_info
156
+ if tb:
157
+ if tb.code_context:
158
+ code = tb.code_context[0]
159
+ else:
160
+ code = "<No code context available>"
161
+ warn += "This warning was triggered from:\n" f"{tb.filename}:{tb.lineno}\n" f"{code}"
162
+ warnings.warn(warn)
163
+ self._enabled = False
164
+
165
+ def subscribe(self, listener: Callable[[S], None], scope: Optional[ContextManager] = None):
166
+ def listener_wrapper(new: StoreValue[S], previous: StoreValue[S]):
167
+ self._ensure_public_exists()
168
+ assert not isinstance(new.public, _PublicValueNotSet)
169
+ assert not isinstance(previous.public, _PublicValueNotSet)
170
+ previous_value = previous.set_value if not isinstance(previous.set_value, _SetValueNotSet) else previous.private
171
+ new_value = new.set_value
172
+ assert not isinstance(new_value, _SetValueNotSet)
173
+ if not self.equals(new_value, previous_value):
174
+ listener(new_value)
175
+
176
+ return self._storage.subscribe_change(listener_wrapper, scope=scope)
177
+
178
+ def subscribe_change(self, listener: Callable[[S, S], None], scope: Optional[ContextManager] = None):
179
+ def listener_wrapper(new: StoreValue[S], previous: StoreValue[S]):
180
+ self._ensure_public_exists()
181
+ assert not isinstance(new.public, _PublicValueNotSet)
182
+ assert not isinstance(previous.public, _PublicValueNotSet)
183
+ previous_value = previous.set_value if not isinstance(previous.set_value, _SetValueNotSet) else previous.private
184
+ new_value = new.set_value
185
+ assert not isinstance(new_value, _SetValueNotSet)
186
+ if not self.equals(new_value, previous_value):
187
+ listener(new_value, previous_value)
188
+
189
+ return self._storage.subscribe_change(listener_wrapper, scope=scope)
@@ -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
- reacton.core._default_container = Column # noqa: F405
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
@@ -82,6 +82,29 @@ def component_vue(vue_path: str, vuetify=True) -> Callable[[Callable[P, None]],
82
82
 
83
83
  See the [Vue component example](/documentation/examples/general/vue_component) for an example of how to use this decorator.
84
84
 
85
+
86
+ ## Examples
87
+
88
+ A component that takes a `foo` argument and an `on_foo` callback that gets called when `foo` changes (from the frontend).
89
+
90
+ ```python
91
+ import solara
92
+
93
+ @solara.component_vue("my_foo_component.vue")
94
+ def MyFooComponent(foo: int, on_foo: Callable[[int], None]):
95
+ pass
96
+ ```
97
+
98
+ The following component only takes in a month argument and an event_date_clicked callback that gets called from
99
+ the vue template using `this.date_clicked({'extra-data': 42, 'day': this.day})`.
100
+ ```python
101
+ import solara
102
+
103
+ @solara.component_vue("my_date_component.vue")
104
+ def MyDateComponent(month: int, event_date_clicked: Callable):
105
+ pass
106
+ ```
107
+
85
108
  ## Arguments
86
109
 
87
110
  * `vue_path`: The path to the Vue template file.
@@ -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)
@@ -10,7 +10,7 @@ class EchartsWidget(ipyvuetify.VuetifyTemplate):
10
10
  template_file = (__file__, "echarts.vue")
11
11
 
12
12
  attributes = traitlets.Dict(default_value=None, allow_none=True).tag(sync=True)
13
-
13
+ responsive = traitlets.Bool(False).tag(sync=True)
14
14
  maps = traitlets.Any({}).tag(sync=True)
15
15
  option = traitlets.Any({}).tag(sync=True)
16
16
  on_click = traitlets.Callable(None, allow_none=True)
@@ -49,7 +49,8 @@ def FigureEcharts(
49
49
  on_mouseover: Callable[[Any], Any] = None,
50
50
  on_mouseout: Callable[[Any], Any] = None,
51
51
  maps: dict = {},
52
- attributes={"style": "height: 400px"},
52
+ attributes={"style": "height: 400px;"},
53
+ responsive: bool = False,
53
54
  ):
54
55
  """Create a Echarts figure.
55
56
 
@@ -68,6 +69,7 @@ def FigureEcharts(
68
69
  * on_mouseout: Callable, a function that will be called when the user moves the mouse out of a certain component.
69
70
  * maps: dict, a dictionary of maps to be used in the figure.
70
71
  * attributes: dict, a dictionary of attributes to be passed to the container (like style, class).
72
+ * responsive: bool, whether the chart should resize when the container changes size.
71
73
 
72
74
 
73
75
  """
@@ -80,4 +82,5 @@ def FigureEcharts(
80
82
  on_mouseout=on_mouseout,
81
83
  on_mouseout_enabled=on_mouseout is not None,
82
84
  attributes=attributes,
85
+ responsive=responsive,
83
86
  )
@@ -1,7 +1,5 @@
1
1
  <template>
2
- <div>
3
- <div ref="echarts" class="solara-echarts" v-bind="attributes"></div>
4
- </div>
2
+ <div ref="echarts" class="solara-echarts" v-bind="attributes"></div>
5
3
  </template>
6
4
  <script>
7
5
  module.exports = {
@@ -14,6 +12,22 @@ module.exports = {
14
12
  this.echarts = echarts;
15
13
  this.create();
16
14
  })();
15
+ if(this.responsive){
16
+ this.resizeObserver = new ResizeObserver(entries => {
17
+ for (let entry of entries) {
18
+ if (entry.target === this.$refs.echarts) {
19
+ this.handleContainerResize();
20
+ }
21
+ }
22
+ });
23
+ this.resizeObserver.observe(this.$refs.echarts);
24
+ };
25
+ },
26
+ beforeDestroy() {
27
+ if (this.resizeObserver) {
28
+ this.resizeObserver.unobserve(this.$refs.echarts);
29
+ this.resizeObserver.disconnect();
30
+ }
17
31
  },
18
32
  watch: {
19
33
  option() {
@@ -25,9 +39,7 @@ module.exports = {
25
39
  methods: {
26
40
  create() {
27
41
  this.chart = this.echarts.init(this.$refs.echarts);
28
- console.log(this.maps);
29
42
  Object.keys(this.maps).forEach((mapName) => {
30
- console.log(mapName);
31
43
  this.echarts.registerMap(mapName, this.maps[mapName]);
32
44
  });
33
45
 
@@ -66,6 +78,11 @@ module.exports = {
66
78
  if (this.on_mouseout_enabled) this.on_mouseout(eventData);
67
79
  });
68
80
  },
81
+ handleContainerResize() {
82
+ if (this.chart) {
83
+ this.chart.resize();
84
+ }
85
+ },
69
86
  import(deps) {
70
87
  return this.loadRequire().then(() => {
71
88
  if (window.jupyterVue) {
@@ -111,6 +111,26 @@ def FileDrop(
111
111
  * `lazy`: Whether to load the file contents into memory or not. If `False`,
112
112
  the file contents will be loaded into memory via the `.data` attribute of file object(s).
113
113
 
114
+ ## Load into Pandas
115
+ To load the data into a Pandas DF, set `lazy=False` and use `file['file_obj']` (be careful of memory)<br>
116
+ You can run this directly in your Jupyter notebook
117
+
118
+ ```python
119
+ import io
120
+ import pandas as pd
121
+ import solara
122
+
123
+ @solara.component
124
+ def Page():
125
+ def load_file_df(file):
126
+ df = pd.read_csv(file["file_obj"])
127
+ print("Loaded dataframe:")
128
+ print(df)
129
+
130
+ solara.FileDrop(label="Drop file to see dataframe!", on_file=load_file_df)
131
+
132
+ ```
133
+
114
134
  """
115
135
 
116
136
  return _FileDrop(label=label, on_total_progress=on_total_progress, on_file=on_file, lazy=lazy, multiple=False)
@@ -49,6 +49,7 @@ def InputText(
49
49
  message: Optional[str] = None,
50
50
  classes: List[str] = [],
51
51
  style: Optional[Union[str, Dict[str, str]]] = None,
52
+ autofocus: bool = False,
52
53
  ):
53
54
  """Free form text input.
54
55
 
@@ -105,6 +106,7 @@ def InputText(
105
106
  * `message`: Message to show below the input. If `error` is a string, this will be ignored.
106
107
  * `classes`: List of CSS classes to apply to the input.
107
108
  * `style`: CSS style to apply to the input.
109
+ * `autofocus`: Determines if a component is to be autofocused or not (Default is False). Autofocus will occur during page load and only one component per page can have autofocus active.
108
110
  """
109
111
  reactive_value = solara.use_reactive(value, on_value)
110
112
  del value, on_value
@@ -133,6 +135,7 @@ def InputText(
133
135
  messages=messages,
134
136
  class_=classes_flat,
135
137
  style_=style_flat,
138
+ autofocus=autofocus,
136
139
  )
137
140
  use_change(text_field, set_value_cast, enabled=not continuous_update, update_events=update_events)
138
141
  return text_field
@@ -150,6 +153,7 @@ def InputFloat(
150
153
  clearable: bool = ...,
151
154
  classes: List[str] = ...,
152
155
  style: Optional[Union[str, Dict[str, str]]] = ...,
156
+ autofocus: bool = False,
153
157
  ) -> reacton.core.ValueElement[vw.TextField, Any]: ...
154
158
 
155
159
 
@@ -165,6 +169,7 @@ def InputFloat(
165
169
  clearable: bool = ...,
166
170
  classes: List[str] = ...,
167
171
  style: Optional[Union[str, Dict[str, str]]] = ...,
172
+ autofocus: bool = False,
168
173
  ) -> reacton.core.ValueElement[vw.TextField, Any]: ...
169
174
 
170
175
 
@@ -179,6 +184,7 @@ def InputFloat(
179
184
  clearable: bool = False,
180
185
  classes: List[str] = [],
181
186
  style: Optional[Union[str, Dict[str, str]]] = None,
187
+ autofocus: bool = False,
182
188
  ):
183
189
  """Numeric input (floats).
184
190
 
@@ -211,6 +217,7 @@ def InputFloat(
211
217
  * `clearable`: Whether the input can be cleared.
212
218
  * `classes`: List of CSS classes to apply to the input.
213
219
  * `style`: CSS style to apply to the input.
220
+ * `autofocus`: Determines if a component is to be autofocused or not (Default is False). Autofocus will occur either during page load, or when the component becomes visible (for example, dialog being opened). Only one component per page should have autofocus on each such event.
214
221
 
215
222
  """
216
223
 
@@ -237,6 +244,7 @@ def InputFloat(
237
244
  clearable=clearable,
238
245
  classes=classes,
239
246
  style=style,
247
+ autofocus=autofocus,
240
248
  )
241
249
 
242
250
 
@@ -252,6 +260,7 @@ def InputInt(
252
260
  clearable: bool = ...,
253
261
  classes: List[str] = ...,
254
262
  style: Optional[Union[str, Dict[str, str]]] = ...,
263
+ autofocus: bool = False,
255
264
  ) -> reacton.core.ValueElement[vw.TextField, Any]: ...
256
265
 
257
266
 
@@ -267,6 +276,7 @@ def InputInt(
267
276
  clearable: bool = ...,
268
277
  classes: List[str] = ...,
269
278
  style: Optional[Union[str, Dict[str, str]]] = ...,
279
+ autofocus: bool = False,
270
280
  ) -> reacton.core.ValueElement[vw.TextField, Any]: ...
271
281
 
272
282
 
@@ -281,6 +291,7 @@ def InputInt(
281
291
  clearable: bool = False,
282
292
  classes: List[str] = [],
283
293
  style: Optional[Union[str, Dict[str, str]]] = None,
294
+ autofocus: bool = False,
284
295
  ):
285
296
  """Numeric input (integers).
286
297
 
@@ -312,6 +323,7 @@ def InputInt(
312
323
  * `clearable`: Whether the input can be cleared.
313
324
  * `classes`: List of CSS classes to apply to the input.
314
325
  * `style`: CSS style to apply to the input.
326
+ * `autofocus`: Determines if a component is to be autofocused or not (Default is False). Autofocus will occur either during page load, or when the component becomes visible (for example, dialog being opened). Only one component per page should have autofocus on each such event.
315
327
  """
316
328
 
317
329
  def str_to_int(value: Optional[str]):
@@ -336,6 +348,7 @@ def InputInt(
336
348
  clearable=clearable,
337
349
  classes=classes,
338
350
  style=style,
351
+ autofocus=autofocus,
339
352
  )
340
353
 
341
354
 
@@ -360,6 +373,10 @@ def _use_input_type(
360
373
  error_message = str(e.args[0])
361
374
 
362
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
+
363
380
  def on_external_value_change(new_value: Optional[T]):
364
381
  new_string_value = stringify(new_value)
365
382
  try:
@@ -373,7 +390,7 @@ def _use_input_type(
373
390
 
374
391
  return reactive_value.subscribe(on_external_value_change)
375
392
 
376
- solara.use_effect(sync_back_input_value, [reactive_value])
393
+ solara.use_effect(sync_back_input_value, [reactive_value, parse, stringify])
377
394
 
378
395
  return string_value, error_message, set_string_value
379
396
 
@@ -389,6 +406,7 @@ def _InputNumeric(
389
406
  clearable: bool = False,
390
407
  classes: List[str] = [],
391
408
  style: Optional[Union[str, Dict[str, str]]] = None,
409
+ autofocus: bool = False,
392
410
  ):
393
411
  """Numeric input.
394
412
 
@@ -401,6 +419,7 @@ def _InputNumeric(
401
419
  * `continuous_update`: Whether to call the `on_value` callback on every change or only when the input loses focus or the enter key is pressed.
402
420
  * `classes`: List of CSS classes to apply to the input.
403
421
  * `style`: CSS style to apply to the input.
422
+ * `autofocus`: Determines if a component is to be autofocused or not (Default is False). Autofocus will occur either during page load, or when the component becomes visible (for example, dialog being opened). Only one component per page should have autofocus on each such event.
404
423
  """
405
424
  style_flat = solara.util._flatten_style(style)
406
425
  classes_flat = solara.util._combine_classes(classes)
@@ -431,6 +450,7 @@ def _InputNumeric(
431
450
  error=bool(error),
432
451
  class_=classes_flat,
433
452
  style_=style_flat,
453
+ autofocus=autofocus,
434
454
  )
435
455
  use_change(text_field, set_value_cast, enabled=not continuous_update)
436
456
  return text_field