streamlit 1.50.0__py3-none-any.whl → 1.51.0__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 (232) hide show
  1. streamlit/__init__.py +4 -1
  2. streamlit/commands/navigation.py +4 -6
  3. streamlit/commands/page_config.py +4 -6
  4. streamlit/components/v2/__init__.py +458 -0
  5. streamlit/components/v2/bidi_component/__init__.py +20 -0
  6. streamlit/components/v2/bidi_component/constants.py +29 -0
  7. streamlit/components/v2/bidi_component/main.py +386 -0
  8. streamlit/components/v2/bidi_component/serialization.py +265 -0
  9. streamlit/components/v2/bidi_component/state.py +92 -0
  10. streamlit/components/v2/component_definition_resolver.py +143 -0
  11. streamlit/components/v2/component_file_watcher.py +403 -0
  12. streamlit/components/v2/component_manager.py +431 -0
  13. streamlit/components/v2/component_manifest_handler.py +122 -0
  14. streamlit/components/v2/component_path_utils.py +245 -0
  15. streamlit/components/v2/component_registry.py +409 -0
  16. streamlit/components/v2/get_bidi_component_manager.py +51 -0
  17. streamlit/components/v2/manifest_scanner.py +615 -0
  18. streamlit/components/v2/presentation.py +198 -0
  19. streamlit/components/v2/types.py +324 -0
  20. streamlit/config.py +456 -53
  21. streamlit/config_option.py +4 -1
  22. streamlit/config_util.py +650 -1
  23. streamlit/dataframe_util.py +15 -8
  24. streamlit/delta_generator.py +6 -4
  25. streamlit/delta_generator_singletons.py +3 -1
  26. streamlit/deprecation_util.py +17 -6
  27. streamlit/elements/arrow.py +37 -9
  28. streamlit/elements/deck_gl_json_chart.py +97 -39
  29. streamlit/elements/dialog_decorator.py +2 -1
  30. streamlit/elements/exception.py +3 -1
  31. streamlit/elements/graphviz_chart.py +1 -3
  32. streamlit/elements/heading.py +3 -5
  33. streamlit/elements/image.py +2 -4
  34. streamlit/elements/layouts.py +31 -11
  35. streamlit/elements/lib/built_in_chart_utils.py +1 -3
  36. streamlit/elements/lib/color_util.py +8 -18
  37. streamlit/elements/lib/column_config_utils.py +4 -8
  38. streamlit/elements/lib/column_types.py +40 -12
  39. streamlit/elements/lib/dialog.py +2 -2
  40. streamlit/elements/lib/image_utils.py +3 -5
  41. streamlit/elements/lib/layout_utils.py +50 -13
  42. streamlit/elements/lib/mutable_status_container.py +2 -2
  43. streamlit/elements/lib/options_selector_utils.py +2 -2
  44. streamlit/elements/lib/utils.py +4 -4
  45. streamlit/elements/map.py +80 -37
  46. streamlit/elements/media.py +5 -7
  47. streamlit/elements/metric.py +3 -5
  48. streamlit/elements/pdf.py +2 -4
  49. streamlit/elements/plotly_chart.py +125 -17
  50. streamlit/elements/progress.py +2 -4
  51. streamlit/elements/space.py +113 -0
  52. streamlit/elements/vega_charts.py +339 -148
  53. streamlit/elements/widgets/audio_input.py +5 -5
  54. streamlit/elements/widgets/button.py +2 -4
  55. streamlit/elements/widgets/button_group.py +33 -7
  56. streamlit/elements/widgets/camera_input.py +2 -4
  57. streamlit/elements/widgets/chat.py +7 -1
  58. streamlit/elements/widgets/color_picker.py +1 -1
  59. streamlit/elements/widgets/data_editor.py +28 -24
  60. streamlit/elements/widgets/file_uploader.py +5 -10
  61. streamlit/elements/widgets/multiselect.py +4 -3
  62. streamlit/elements/widgets/number_input.py +2 -4
  63. streamlit/elements/widgets/radio.py +10 -3
  64. streamlit/elements/widgets/select_slider.py +8 -5
  65. streamlit/elements/widgets/selectbox.py +6 -3
  66. streamlit/elements/widgets/slider.py +38 -42
  67. streamlit/elements/widgets/time_widgets.py +6 -12
  68. streamlit/elements/write.py +27 -6
  69. streamlit/emojis.py +1 -1
  70. streamlit/errors.py +115 -0
  71. streamlit/hello/hello.py +8 -0
  72. streamlit/hello/utils.py +2 -1
  73. streamlit/material_icon_names.py +1 -1
  74. streamlit/navigation/page.py +4 -1
  75. streamlit/proto/ArrowData_pb2.py +27 -0
  76. streamlit/proto/ArrowData_pb2.pyi +46 -0
  77. streamlit/proto/BidiComponent_pb2.py +34 -0
  78. streamlit/proto/BidiComponent_pb2.pyi +153 -0
  79. streamlit/proto/Block_pb2.py +7 -7
  80. streamlit/proto/Block_pb2.pyi +4 -1
  81. streamlit/proto/DeckGlJsonChart_pb2.py +10 -4
  82. streamlit/proto/DeckGlJsonChart_pb2.pyi +9 -3
  83. streamlit/proto/Element_pb2.py +5 -3
  84. streamlit/proto/Element_pb2.pyi +14 -4
  85. streamlit/proto/HeightConfig_pb2.py +2 -2
  86. streamlit/proto/HeightConfig_pb2.pyi +6 -3
  87. streamlit/proto/NewSession_pb2.py +18 -18
  88. streamlit/proto/NewSession_pb2.pyi +25 -6
  89. streamlit/proto/PlotlyChart_pb2.py +8 -6
  90. streamlit/proto/PlotlyChart_pb2.pyi +3 -1
  91. streamlit/proto/Space_pb2.py +27 -0
  92. streamlit/proto/Space_pb2.pyi +42 -0
  93. streamlit/proto/WidgetStates_pb2.py +2 -2
  94. streamlit/proto/WidgetStates_pb2.pyi +13 -3
  95. streamlit/proto/WidthConfig_pb2.py +2 -2
  96. streamlit/proto/WidthConfig_pb2.pyi +6 -3
  97. streamlit/runtime/app_session.py +27 -1
  98. streamlit/runtime/caching/cache_data_api.py +4 -4
  99. streamlit/runtime/caching/cache_errors.py +4 -1
  100. streamlit/runtime/caching/cache_resource_api.py +3 -2
  101. streamlit/runtime/caching/cache_utils.py +2 -1
  102. streamlit/runtime/caching/cached_message_replay.py +3 -3
  103. streamlit/runtime/caching/hashing.py +3 -4
  104. streamlit/runtime/caching/legacy_cache_api.py +2 -1
  105. streamlit/runtime/connection_factory.py +1 -3
  106. streamlit/runtime/forward_msg_queue.py +4 -1
  107. streamlit/runtime/fragment.py +2 -1
  108. streamlit/runtime/memory_media_file_storage.py +1 -1
  109. streamlit/runtime/metrics_util.py +6 -2
  110. streamlit/runtime/runtime.py +14 -0
  111. streamlit/runtime/scriptrunner/exec_code.py +2 -1
  112. streamlit/runtime/scriptrunner/script_runner.py +2 -2
  113. streamlit/runtime/scriptrunner_utils/script_run_context.py +3 -6
  114. streamlit/runtime/secrets.py +2 -4
  115. streamlit/runtime/session_manager.py +3 -1
  116. streamlit/runtime/state/common.py +30 -5
  117. streamlit/runtime/state/presentation.py +85 -0
  118. streamlit/runtime/state/safe_session_state.py +2 -2
  119. streamlit/runtime/state/session_state.py +220 -16
  120. streamlit/runtime/state/widgets.py +19 -3
  121. streamlit/runtime/websocket_session_manager.py +3 -1
  122. streamlit/source_util.py +2 -2
  123. streamlit/static/index.html +2 -2
  124. streamlit/static/manifest.json +243 -226
  125. streamlit/static/static/css/{index.CIiu7Ygf.css → index.BpABIXK9.css} +1 -1
  126. streamlit/static/static/css/index.DgR7E2CV.css +1 -0
  127. streamlit/static/static/js/{ErrorOutline.esm.DUpR0_Ka.js → ErrorOutline.esm.YoJdlW1p.js} +1 -1
  128. streamlit/static/static/js/{FileDownload.esm.CN4j9-1w.js → FileDownload.esm.Ddx8VEYy.js} +1 -1
  129. streamlit/static/static/js/{FileHelper.CaIUKG91.js → FileHelper.90EtOmj9.js} +1 -1
  130. streamlit/static/static/js/{FormClearHelper.DTcdrasw.js → FormClearHelper.BB1Km6eP.js} +1 -1
  131. streamlit/static/static/js/InputInstructions.jhH15PqV.js +1 -0
  132. streamlit/static/static/js/{Particles.CElH0XX2.js → Particles.DUsputn1.js} +1 -1
  133. streamlit/static/static/js/{ProgressBar.DetlP5aY.js → ProgressBar.DLY8H6nE.js} +1 -1
  134. streamlit/static/static/js/{Toolbar.C77ar7rq.js → Toolbar.D8nHCkuz.js} +1 -1
  135. streamlit/static/static/js/{base-input.BQft14La.js → base-input.CJGiNqed.js} +3 -3
  136. streamlit/static/static/js/{checkbox.yZOfXCeX.js → checkbox.Cpdd482O.js} +1 -1
  137. streamlit/static/static/js/{createSuper.Dh9w1cs8.js → createSuper.CuQIogbW.js} +1 -1
  138. streamlit/static/static/js/{data-grid-overlay-editor.DcuHuCyW.js → data-grid-overlay-editor.2Ufgxc6y.js} +1 -1
  139. streamlit/static/static/js/{downloader.MeHtkq8r.js → downloader.CN0K7xlu.js} +1 -1
  140. streamlit/static/static/js/{es6.VpBPGCnM.js → es6.BJcsVXQ0.js} +2 -2
  141. streamlit/static/static/js/{iframeResizer.contentWindow.yMw_ARIL.js → iframeResizer.contentWindow.XzUvQqcZ.js} +1 -1
  142. streamlit/static/static/js/index.B1ZQh4P1.js +1 -0
  143. streamlit/static/static/js/index.BKstZk0M.js +27 -0
  144. streamlit/static/static/js/{index.Cnpi3o3E.js → index.BMcFsUee.js} +1 -1
  145. streamlit/static/static/js/{index.DKv_lNO7.js → index.BR-IdcTb.js} +1 -1
  146. streamlit/static/static/js/{index.FFOzOWzC.js → index.B_dWA3vd.js} +1 -1
  147. streamlit/static/static/js/{index.Bj9JgOEC.js → index.BgnZEMVh.js} +1 -1
  148. streamlit/static/static/js/{index.Bxz2yX3P.js → index.BohqXifI.js} +1 -1
  149. streamlit/static/static/js/{index.Dbe-Q3C-.js → index.Br5nxKNj.js} +1 -1
  150. streamlit/static/static/js/{index.BjCwMzj4.js → index.BrIKVbNc.js} +2 -2
  151. streamlit/static/static/js/index.BtWUPzle.js +1 -0
  152. streamlit/static/static/js/{index.CGYqqs6j.js → index.C0RLraek.js} +1 -1
  153. streamlit/static/static/js/{index.D2QEXQq_.js → index.CAIjskgG.js} +1 -1
  154. streamlit/static/static/js/{index.6xX1278W.js → index.CAj-7vWz.js} +131 -157
  155. streamlit/static/static/js/{index.DK7hD7_w.js → index.CMtEit2O.js} +1 -1
  156. streamlit/static/static/js/{index.DNLrMXgm.js → index.CkRlykEE.js} +1 -1
  157. streamlit/static/static/js/{index.ClELlchS.js → index.CmN3FXfI.js} +1 -1
  158. streamlit/static/static/js/{index.GRUzrudl.js → index.CwbFI1_-.js} +1 -1
  159. streamlit/static/static/js/{index.Ctn27_AE.js → index.CxIUUfab.js} +27 -27
  160. streamlit/static/static/js/index.D2KPNy7e.js +1 -0
  161. streamlit/static/static/js/{index.B0H9IXUJ.js → index.D3GPA5k4.js} +3 -3
  162. streamlit/static/static/js/{index.BycLveZ4.js → index.DGAh7DMq.js} +1 -1
  163. streamlit/static/static/js/index.DKb_NvmG.js +197 -0
  164. streamlit/static/static/js/{index.BPQo7BKk.js → index.DMqgUYKq.js} +1 -1
  165. streamlit/static/static/js/{index.CH1tqnSs.js → index.DOFlg3dS.js} +1 -1
  166. streamlit/static/static/js/{index.64ejlaaT.js → index.DPUXkcQL.js} +1 -1
  167. streamlit/static/static/js/{index.B-hiXRzw.js → index.DX1xY89g.js} +1 -1
  168. streamlit/static/static/js/index.DYATBCsq.js +2 -0
  169. streamlit/static/static/js/{index.DHh-U0dK.js → index.DaSmGJ76.js} +3 -3
  170. streamlit/static/static/js/{index.DuxqVQpd.js → index.Dd7bMeLP.js} +1 -1
  171. streamlit/static/static/js/{index.B4cAbHP6.js → index.DjmmgI5U.js} +1 -1
  172. streamlit/static/static/js/{index.DcPNYEUo.js → index.Dq56CyM2.js} +1 -1
  173. streamlit/static/static/js/{index.CiAQIz1H.js → index.DuiXaS5_.js} +1 -1
  174. streamlit/static/static/js/index.DvFidMLe.js +2 -0
  175. streamlit/static/static/js/{index.C9BdUqTi.js → index.DwkhC5Pc.js} +1 -1
  176. streamlit/static/static/js/{index.B4dUQfni.js → index.Q-3sFn1v.js} +1 -1
  177. streamlit/static/static/js/{index.CMItVsFA.js → index.QJ5QO9sJ.js} +1 -1
  178. streamlit/static/static/js/{index.CTBk8Vk2.js → index.VwTaeety.js} +1 -1
  179. streamlit/static/static/js/{index.Ck8rQ9OL.js → index.YOqQbeX8.js} +1 -1
  180. streamlit/static/static/js/{input.s6pjQ49A.js → input.D4MN_FzN.js} +1 -1
  181. streamlit/static/static/js/{memory.Cuvsdfrl.js → memory.DrZjtdGT.js} +1 -1
  182. streamlit/static/static/js/{number-overlay-editor.DdgVR5m3.js → number-overlay-editor.DRwAw1In.js} +1 -1
  183. streamlit/static/static/js/{possibleConstructorReturn.CqidKeei.js → possibleConstructorReturn.exeeJQEP.js} +1 -1
  184. streamlit/static/static/js/record.B-tDciZb.js +1 -0
  185. streamlit/static/static/js/{sandbox.CCQREcJx.js → sandbox.ClO3IuUr.js} +1 -1
  186. streamlit/static/static/js/{timepicker.mkJF97Bb.js → timepicker.DAhu-vcF.js} +1 -1
  187. streamlit/static/static/js/{toConsumableArray.De7I7KVR.js → toConsumableArray.DNbljYEC.js} +1 -1
  188. streamlit/static/static/js/{uniqueId.RI1LJdtz.js → uniqueId.oG4Gvj1v.js} +1 -1
  189. streamlit/static/static/js/{useBasicWidgetState.CedkNjUW.js → useBasicWidgetState.D6sOH6oI.js} +1 -1
  190. streamlit/static/static/js/{useTextInputAutoExpand.Ca7w8dVs.js → useTextInputAutoExpand.4u3_GcuN.js} +1 -1
  191. streamlit/static/static/js/{useUpdateUiValue.DeXelfRH.js → useUpdateUiValue.F2R3eTeR.js} +1 -1
  192. streamlit/static/static/js/wavesurfer.esm.vI8Eid4k.js +73 -0
  193. streamlit/static/static/js/{withFullScreenWrapper.C3561XxJ.js → withFullScreenWrapper.zothJIsI.js} +1 -1
  194. streamlit/static/static/media/MaterialSymbols-Rounded.C7IFxh57.woff2 +0 -0
  195. streamlit/string_util.py +1 -3
  196. streamlit/testing/v1/app_test.py +2 -2
  197. streamlit/testing/v1/element_tree.py +23 -9
  198. streamlit/testing/v1/util.py +2 -2
  199. streamlit/type_util.py +3 -4
  200. streamlit/url_util.py +1 -3
  201. streamlit/user_info.py +1 -2
  202. streamlit/util.py +3 -1
  203. streamlit/watcher/event_based_path_watcher.py +23 -12
  204. streamlit/watcher/local_sources_watcher.py +11 -1
  205. streamlit/watcher/path_watcher.py +9 -6
  206. streamlit/watcher/polling_path_watcher.py +4 -1
  207. streamlit/watcher/util.py +2 -2
  208. streamlit/web/cli.py +51 -22
  209. streamlit/web/server/bidi_component_request_handler.py +193 -0
  210. streamlit/web/server/component_file_utils.py +97 -0
  211. streamlit/web/server/component_request_handler.py +8 -21
  212. streamlit/web/server/oidc_mixin.py +3 -1
  213. streamlit/web/server/routes.py +2 -2
  214. streamlit/web/server/server.py +9 -0
  215. streamlit/web/server/server_util.py +3 -1
  216. streamlit/web/server/upload_file_request_handler.py +3 -1
  217. {streamlit-1.50.0.dist-info → streamlit-1.51.0.dist-info}/METADATA +4 -5
  218. {streamlit-1.50.0.dist-info → streamlit-1.51.0.dist-info}/RECORD +222 -194
  219. streamlit/static/static/css/index.CHEnSPGk.css +0 -1
  220. streamlit/static/static/js/Hooks.BRba_Own.js +0 -1
  221. streamlit/static/static/js/InputInstructions.xnSDuYeQ.js +0 -1
  222. streamlit/static/static/js/index.Baqa90pe.js +0 -2
  223. streamlit/static/static/js/index.Bm3VbPB5.js +0 -1
  224. streamlit/static/static/js/index.CFMf5_ez.js +0 -197
  225. streamlit/static/static/js/index.Cj7DSzVR.js +0 -73
  226. streamlit/static/static/js/index.DH71Ezyj.js +0 -1
  227. streamlit/static/static/js/index.DW0Grddz.js +0 -1
  228. streamlit/static/static/media/MaterialSymbols-Rounded.DeCZgS-4.woff2 +0 -0
  229. {streamlit-1.50.0.data → streamlit-1.51.0.data}/scripts/streamlit.cmd +0 -0
  230. {streamlit-1.50.0.dist-info → streamlit-1.51.0.dist-info}/WHEEL +0 -0
  231. {streamlit-1.50.0.dist-info → streamlit-1.51.0.dist-info}/entry_points.txt +0 -0
  232. {streamlit-1.50.0.dist-info → streamlit-1.51.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,92 @@
1
+ # Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Any, TypedDict
18
+
19
+ from streamlit.util import AttributeDictionary
20
+
21
+
22
+ class BidiComponentState(TypedDict, total=False):
23
+ """
24
+ The schema for the state of a bidirectional component.
25
+
26
+ The state is a flat dictionary-like object (key -> value) that supports
27
+ both key and attribute notation via :class:`AttributeDictionary`.
28
+ """
29
+
30
+ # Flat mapping of state key -> value
31
+ # (kept empty to reflect open set of keys)
32
+
33
+
34
+ class BidiComponentResult(AttributeDictionary):
35
+ """The schema for the custom component result object.
36
+
37
+ The custom component result object is a dictionary-like object that
38
+ supports both key and attribute notation. It contains all of the
39
+ component's state and trigger values.
40
+
41
+ Attributes
42
+ ----------
43
+ <state_keys> : Any
44
+ All state values from the component. State values are persistent across
45
+ app reruns until explicitly changed. You can have multiple state keys
46
+ as attributes.
47
+ <trigger_keys> : Any
48
+ All trigger values from the component. Trigger values are transient and
49
+ reset to ``None`` after one script run. You can have multiple trigger
50
+ keys as attributes.
51
+
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ state_vals: dict[str, Any] | None = None,
57
+ trigger_vals: dict[str, Any] | None = None,
58
+ ) -> None:
59
+ """Initialize a BidiComponentResult.
60
+
61
+ Parameters
62
+ ----------
63
+ state_vals : dict[str, Any] or None
64
+ A dictionary of state values from the component.
65
+ trigger_vals : dict[str, Any] or None
66
+ A dictionary of trigger values from the component.
67
+ """
68
+ if state_vals is None:
69
+ state_vals = {}
70
+ if trigger_vals is None:
71
+ trigger_vals = {}
72
+
73
+ super().__init__(
74
+ {
75
+ # The order here matters, because all stateful values will
76
+ # always be returned, but trigger values may be transient. This
77
+ # mirrors presentation behavior in
78
+ # `make_bidi_component_presenter`.
79
+ **trigger_vals,
80
+ **state_vals,
81
+ }
82
+ )
83
+
84
+
85
+ def unwrap_component_state(raw_state: Any) -> dict[str, Any]:
86
+ """Return flat mapping when given a dict; otherwise, empty dict.
87
+
88
+ The new canonical state is flat, so this is effectively an identity for
89
+ dict inputs and a guard for other types.
90
+ """
91
+
92
+ return dict(raw_state) if isinstance(raw_state, dict) else {}
@@ -0,0 +1,143 @@
1
+ # Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Shared resolver for building component definitions with path validation.
16
+
17
+ This module centralizes the logic for interpreting js/css inputs as inline
18
+ content vs path/glob strings, validating them against a component's asset
19
+ directory, and producing a BidiComponentDefinition with correct asset-relative
20
+ URLs used by the server.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from pathlib import Path
26
+ from typing import TYPE_CHECKING
27
+
28
+ from streamlit.components.v2.component_path_utils import ComponentPathUtils
29
+ from streamlit.components.v2.component_registry import BidiComponentDefinition
30
+ from streamlit.errors import StreamlitAPIException
31
+
32
+ if TYPE_CHECKING:
33
+ from streamlit.components.v2.component_manager import BidiComponentManager
34
+
35
+
36
+ def build_definition_with_validation(
37
+ *,
38
+ manager: BidiComponentManager,
39
+ component_key: str,
40
+ html: str | None,
41
+ css: str | None,
42
+ js: str | None,
43
+ ) -> BidiComponentDefinition:
44
+ """Construct a definition and validate ``js``/``css`` inputs against ``asset_dir``.
45
+
46
+ Parameters
47
+ ----------
48
+ manager : BidiComponentManager
49
+ Component manager used to resolve the component's ``asset_dir`` and
50
+ related metadata.
51
+ component_key : str
52
+ Fully-qualified name of the component to build a definition for.
53
+ html : str | None
54
+ Inline HTML content to include in the definition. If ``None``, the
55
+ component will not include HTML content.
56
+ css : str | None
57
+ Either inline CSS content or a path/glob to a CSS file inside the
58
+ component's ``asset_dir``. Inline strings are kept as-is; file-backed
59
+ inputs are validated and converted to an ``asset_dir``-relative URL.
60
+ js : str | None
61
+ Either inline JavaScript content or a path/glob to a JS file inside the
62
+ component's ``asset_dir``. Inline strings are kept as-is; file-backed
63
+ inputs are validated and converted to an ``asset_dir``-relative URL.
64
+
65
+ Returns
66
+ -------
67
+ BidiComponentDefinition
68
+ A component definition with inline content preserved and file-backed
69
+ entries resolved to absolute filesystem paths plus their
70
+ ``asset_dir``-relative URLs.
71
+
72
+ Raises
73
+ ------
74
+ StreamlitAPIException
75
+ If a path/glob is provided but the component has no declared
76
+ ``asset_dir``, if a glob resolves to zero or multiple files, or if any
77
+ resolved path escapes the declared ``asset_dir``.
78
+
79
+ Notes
80
+ -----
81
+ - Inline strings are treated as content (no manifest required).
82
+ - Path-like strings require the component to be declared in the package
83
+ manifest with an ``asset_dir``.
84
+ - Globs are supported only within ``asset_dir`` and must resolve to exactly
85
+ one file.
86
+ - Relative paths are resolved strictly against the component's ``asset_dir``
87
+ and must remain within it after resolution. Absolute paths are not
88
+ allowed.
89
+ - For file-backed entries, the URL sent to the frontend is the
90
+ ``asset_dir``-relative path, served under
91
+ ``/_stcore/bidi-components/<component>/<relative_path>``.
92
+ """
93
+
94
+ asset_root = manager.get_component_asset_root(component_key)
95
+
96
+ def _resolve_entry(
97
+ value: str | None, *, kind: str
98
+ ) -> tuple[str | None, str | None]:
99
+ # Inline content: None rel URL
100
+ if value is None:
101
+ return None, None
102
+ if ComponentPathUtils.looks_like_inline_content(value):
103
+ return value, None
104
+
105
+ # For path-like strings, asset_root must exist
106
+ if asset_root is None:
107
+ raise StreamlitAPIException(
108
+ f"Component '{component_key}' must be declared in pyproject.toml with asset_dir "
109
+ f"to use file-backed {kind}."
110
+ )
111
+
112
+ value_str = value
113
+
114
+ # If looks like a glob, resolve strictly inside asset_root
115
+ if ComponentPathUtils.has_glob_characters(value_str):
116
+ resolved = ComponentPathUtils.resolve_glob_pattern(value_str, asset_root)
117
+ ComponentPathUtils.ensure_within_root(resolved, asset_root, kind=kind)
118
+ # Use resolved absolute paths to avoid macOS /private prefix mismatch
119
+ rel_url = str(
120
+ resolved.resolve().relative_to(asset_root.resolve()).as_posix()
121
+ )
122
+ return str(resolved), rel_url
123
+
124
+ # Concrete path: must be asset-dir-relative (reject absolute & traversal)
125
+ ComponentPathUtils.validate_path_security(value_str)
126
+ candidate = asset_root / Path(value_str)
127
+ ComponentPathUtils.ensure_within_root(candidate, asset_root, kind=kind)
128
+ resolved_candidate = candidate.resolve()
129
+ rel_url = str(resolved_candidate.relative_to(asset_root.resolve()).as_posix())
130
+ return str(resolved_candidate), rel_url
131
+
132
+ css_value, css_rel = _resolve_entry(css, kind="css")
133
+ js_value, js_rel = _resolve_entry(js, kind="js")
134
+
135
+ # Build definition with possible asset_dir-relative paths
136
+ return BidiComponentDefinition(
137
+ name=component_key,
138
+ html=html,
139
+ css=css_value,
140
+ js=js_value,
141
+ css_asset_relative_path=css_rel,
142
+ js_asset_relative_path=js_rel,
143
+ )
@@ -0,0 +1,403 @@
1
+ # Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
16
+ """Component file watching utilities.
17
+
18
+ This module provides the `ComponentFileWatcher`, a utility that watches
19
+ component asset directories for changes and notifies a caller-provided callback
20
+ with the affected component names. It abstracts the underlying path-watcher
21
+ implementation and ensures exception-safe startup and cleanup.
22
+
23
+ Why this exists
24
+ ---------------
25
+ Streamlit supports advanced Custom Components that ship a package of static
26
+ assets (for example, a Vite/Webpack build output). While a user develops their
27
+ app, those frontend files may change. The component registry for Custom
28
+ Components v2 must stay synchronized with the on-disk assets so that the server
29
+ can resolve the up-to-date files.
30
+
31
+ This watcher exists to keep the registry in sync by listening for changes in
32
+ component asset roots and notifying a higher-level manager that can re-resolve
33
+ the affected component definitions.
34
+
35
+ Notes
36
+ -----
37
+ - Watching is directory-based with a recursive glob ("**/*").
38
+ - Common noisy directories (e.g., ``node_modules``) are ignored in callbacks.
39
+ - Startup is exception-safe and does not leak partially created watchers.
40
+
41
+ See Also
42
+ --------
43
+ - :class:`streamlit.watcher.local_sources_watcher.LocalSourcesWatcher` - watches
44
+ app source files per session to trigger reruns.
45
+ - :class:`streamlit.components.v2.component_registry.BidiComponentRegistry` -
46
+ the server-side store of Custom Component v2 definitions that reacts to
47
+ watcher notifications.
48
+ """
49
+
50
+ from __future__ import annotations
51
+
52
+ import threading
53
+ from typing import TYPE_CHECKING, Final, Protocol, cast
54
+
55
+ from streamlit.logger import get_logger
56
+
57
+ if TYPE_CHECKING:
58
+ from collections.abc import Callable
59
+ from pathlib import Path
60
+
61
+
62
+ _LOGGER: Final = get_logger(__name__)
63
+
64
+
65
+ class _HasClose(Protocol):
66
+ def close(self) -> None: ...
67
+
68
+
69
+ class ComponentFileWatcher:
70
+ """Handle file watching for component asset directories.
71
+
72
+ Parameters
73
+ ----------
74
+ component_update_callback : Callable[[list[str]], None]
75
+ Callback invoked when files change under any watched directory. It
76
+ receives a list of component names affected by the change.
77
+ """
78
+
79
+ def __init__(self, component_update_callback: Callable[[list[str]], None]) -> None:
80
+ """Initialize the file watcher.
81
+
82
+ Parameters
83
+ ----------
84
+ component_update_callback : Callable[[list[str]], None]
85
+ Callback function to call when components under watched roots change.
86
+ Signature: (affected_component_names)
87
+ """
88
+ self._component_update_callback = component_update_callback
89
+ self._lock = threading.Lock()
90
+
91
+ # File watching state
92
+ self._watched_directories: dict[
93
+ str, list[str]
94
+ ] = {} # directory -> component_names
95
+ self._path_watchers: list[_HasClose] = [] # Store actual watcher instances
96
+ self._watching_active = False
97
+
98
+ # Store asset roots to watch: component_name -> asset_root
99
+ self._asset_watch_roots: dict[str, Path] = {}
100
+
101
+ # Default noisy directories to ignore in callbacks
102
+ self._ignored_dirs: tuple[str, ...] = (
103
+ "__pycache__",
104
+ ".cache",
105
+ ".git",
106
+ ".hg",
107
+ ".mypy_cache",
108
+ ".pytest_cache",
109
+ ".ruff_cache",
110
+ ".svn",
111
+ ".swc",
112
+ ".yarn",
113
+ "coverage",
114
+ "node_modules",
115
+ "venv",
116
+ )
117
+
118
+ @property
119
+ def is_watching_active(self) -> bool:
120
+ """Check if file watching is currently active.
121
+
122
+ Returns
123
+ -------
124
+ bool
125
+ True if file watching is active, False otherwise
126
+ """
127
+ return self._watching_active
128
+
129
+ def start_file_watching(self, asset_watch_roots: dict[str, Path]) -> None:
130
+ """Start file watching for asset roots.
131
+
132
+ Parameters
133
+ ----------
134
+ asset_watch_roots : dict[str, Path]
135
+ Mapping of component names to asset root directories to watch.
136
+
137
+ Notes
138
+ -----
139
+ The method is idempotent: it stops any active watchers first, then
140
+ re-initializes watchers for the provided ``asset_watch_roots``.
141
+ """
142
+ # Always stop first to ensure a clean state, then start with the new roots.
143
+ # This sequencing avoids races between concurrent stop/start calls.
144
+ self.stop_file_watching()
145
+ self._start_file_watching(asset_watch_roots)
146
+
147
+ def stop_file_watching(self) -> None:
148
+ """Stop file watching and clean up watchers.
149
+
150
+ Notes
151
+ -----
152
+ This method is safe to call multiple times and will no-op if
153
+ watching is not active.
154
+ """
155
+ with self._lock:
156
+ if not self._watching_active:
157
+ return
158
+
159
+ # Close all path watchers
160
+ for watcher in self._path_watchers:
161
+ try:
162
+ watcher.close()
163
+ except Exception: # noqa: PERF203
164
+ _LOGGER.exception("Failed to close path watcher")
165
+
166
+ self._path_watchers.clear()
167
+ self._watched_directories.clear()
168
+ # Also clear asset root references to avoid stale state retention
169
+ self._asset_watch_roots.clear()
170
+ self._watching_active = False
171
+ _LOGGER.debug("Stopped file watching for component registry")
172
+
173
+ def _start_file_watching(self, asset_watch_roots: dict[str, Path]) -> None:
174
+ """Internal method to start file watching with the given roots.
175
+
176
+ This method is exception-safe: in case of failures while creating
177
+ watchers, any previously created watcher instances are closed and no
178
+ internal state is committed.
179
+ """
180
+ with self._lock:
181
+ if self._watching_active:
182
+ return
183
+
184
+ if not asset_watch_roots:
185
+ _LOGGER.debug("No asset roots to watch")
186
+ return
187
+
188
+ try:
189
+ path_watcher_class = self._get_default_path_watcher_class()
190
+ if path_watcher_class is None:
191
+ # NoOp watcher; skip activation
192
+ return
193
+
194
+ directories_to_watch = self._prepare_directories_to_watch(
195
+ asset_watch_roots
196
+ )
197
+
198
+ new_watchers, new_watched_dirs = self._build_watchers_for_directories(
199
+ path_watcher_class, directories_to_watch
200
+ )
201
+
202
+ # Commit new watchers and state only after successful creation
203
+ if new_watchers:
204
+ self._commit_watch_state(
205
+ new_watchers, new_watched_dirs, asset_watch_roots
206
+ )
207
+ else:
208
+ _LOGGER.debug("No directories were watched; staying inactive")
209
+ except Exception:
210
+ _LOGGER.exception("Failed to start file watching")
211
+
212
+ def _get_default_path_watcher_class(self) -> type | None:
213
+ """Return the default path watcher class.
214
+
215
+ Returns
216
+ -------
217
+ type | None
218
+ The concrete path watcher class to instantiate, or ``None`` if
219
+ the NoOp watcher is configured and file watching should be
220
+ skipped.
221
+ """
222
+ from streamlit.watcher.path_watcher import (
223
+ NoOpPathWatcher,
224
+ get_default_path_watcher_class,
225
+ )
226
+
227
+ path_watcher_class = get_default_path_watcher_class()
228
+ if path_watcher_class is NoOpPathWatcher:
229
+ _LOGGER.debug("NoOpPathWatcher in use; skipping component file watching")
230
+ return None
231
+ return path_watcher_class
232
+
233
+ def _prepare_directories_to_watch(
234
+ self, asset_watch_roots: dict[str, Path]
235
+ ) -> dict[str, list[str]]:
236
+ """Build a mapping of directory to component names.
237
+
238
+ Parameters
239
+ ----------
240
+ asset_watch_roots : dict[str, Path]
241
+ Mapping of component names to their asset root directories.
242
+
243
+ Returns
244
+ -------
245
+ dict[str, list[str]]
246
+ A map from absolute directory path to a deduplicated list of
247
+ component names contained in that directory.
248
+ """
249
+ directories_to_watch: dict[str, list[str]] = {}
250
+ for comp_name, root in asset_watch_roots.items():
251
+ directory = str(root.resolve())
252
+ if directory not in directories_to_watch:
253
+ directories_to_watch[directory] = []
254
+ if comp_name not in directories_to_watch[directory]:
255
+ directories_to_watch[directory].append(comp_name)
256
+ return directories_to_watch
257
+
258
+ def _build_watchers_for_directories(
259
+ self, path_watcher_class: type, directories_to_watch: dict[str, list[str]]
260
+ ) -> tuple[list[_HasClose], dict[str, list[str]]]:
261
+ """Create watchers for directories with rollback on failure.
262
+
263
+ Parameters
264
+ ----------
265
+ path_watcher_class : type
266
+ The path watcher class to instantiate for each directory.
267
+ directories_to_watch : dict[str, list[str]]
268
+ A map of directory to the associated component name list.
269
+
270
+ Returns
271
+ -------
272
+ tuple[list[_HasClose], dict[str, list[str]]]
273
+ The list of created watcher instances and the watched directory
274
+ mapping.
275
+
276
+ Raises
277
+ ------
278
+ Exception
279
+ Propagates any exception during watcher creation after closing
280
+ already-created watchers.
281
+ """
282
+ new_watchers: list[_HasClose] = []
283
+ new_watched_dirs: dict[str, list[str]] = {}
284
+
285
+ for directory, component_names in directories_to_watch.items():
286
+ try:
287
+ cb = self._make_directory_callback(tuple(component_names))
288
+ # Use a glob pattern that matches all files to let Streamlit's
289
+ # watcher handle MD5 calculation and change detection
290
+ watcher = path_watcher_class(
291
+ directory,
292
+ cb,
293
+ glob_pattern="**/*",
294
+ allow_nonexistent=False,
295
+ )
296
+ new_watchers.append(cast("_HasClose", watcher))
297
+ new_watched_dirs[directory] = component_names
298
+ _LOGGER.debug(
299
+ "Prepared watcher for directory %s (components: %s)",
300
+ directory,
301
+ component_names,
302
+ )
303
+ except Exception: # noqa: PERF203
304
+ # Roll back watchers created so far
305
+ self._rollback_watchers(new_watchers)
306
+ raise
307
+
308
+ return new_watchers, new_watched_dirs
309
+
310
+ def _commit_watch_state(
311
+ self,
312
+ new_watchers: list[_HasClose],
313
+ new_watched_dirs: dict[str, list[str]],
314
+ asset_watch_roots: dict[str, Path],
315
+ ) -> None:
316
+ """Commit created watchers and mark watching active.
317
+
318
+ Parameters
319
+ ----------
320
+ new_watchers : list[_HasClose]
321
+ Fully initialized watcher instances.
322
+ new_watched_dirs : dict[str, list[str]]
323
+ Mapping from directory to component names.
324
+ asset_watch_roots : dict[str, Path]
325
+ The asset roots used to initialize watchers; stored for reference.
326
+ """
327
+ self._path_watchers = new_watchers
328
+ self._watched_directories = new_watched_dirs
329
+ self._asset_watch_roots = dict(asset_watch_roots)
330
+ self._watching_active = True
331
+ _LOGGER.debug(
332
+ "Started file watching for %d directories", len(self._watched_directories)
333
+ )
334
+
335
+ def _rollback_watchers(self, watchers: list[_HasClose]) -> None:
336
+ """Close any created watchers when setup fails.
337
+
338
+ Parameters
339
+ ----------
340
+ watchers : list[_HasClose]
341
+ Watcher instances that were successfully created before a failure.
342
+ """
343
+ for w in watchers:
344
+ try:
345
+ w.close()
346
+ except Exception: # noqa: PERF203
347
+ _LOGGER.exception("Failed to close path watcher during rollback")
348
+
349
+ def _make_directory_callback(self, comps: tuple[str, ...]) -> Callable[[str], None]:
350
+ """Create a callback for a directory watcher that captures component names."""
351
+
352
+ def callback(changed_path: str) -> None:
353
+ if self._is_in_ignored_directory(changed_path):
354
+ _LOGGER.debug("Ignoring change in noisy directory: %s", changed_path)
355
+ return
356
+ _LOGGER.debug(
357
+ "Directory change detected: %s, checking components: %s",
358
+ changed_path,
359
+ comps,
360
+ )
361
+ self._handle_component_change(list(comps))
362
+
363
+ return callback
364
+
365
+ def _handle_component_change(self, affected_components: list[str]) -> None:
366
+ """Handle component changes for both directory and file events.
367
+
368
+ Parameters
369
+ ----------
370
+ affected_components : list[str]
371
+ List of component names affected by the change
372
+ """
373
+ if not self._watching_active:
374
+ return
375
+
376
+ # Notify manager to handle re-resolution based on recorded API inputs
377
+ try:
378
+ self._component_update_callback(affected_components)
379
+ except Exception:
380
+ # Never allow exceptions from user callbacks to break watcher loops
381
+ _LOGGER.exception("Component update callback raised")
382
+
383
+ def _is_in_ignored_directory(self, changed_path: str) -> bool:
384
+ """Return True if the changed path is inside an ignored directory.
385
+
386
+ Parameters
387
+ ----------
388
+ changed_path : str
389
+ The filesystem path that triggered the change event.
390
+
391
+ Returns
392
+ -------
393
+ bool
394
+ True if the path is located inside one of the ignored directories,
395
+ False otherwise.
396
+ """
397
+ try:
398
+ from pathlib import Path as _Path
399
+
400
+ parts = set(_Path(changed_path).resolve().parts)
401
+ return any(ignored in parts for ignored in self._ignored_dirs)
402
+ except Exception:
403
+ return False