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
@@ -23,6 +23,7 @@ from enum import Enum
23
23
  from typing import TYPE_CHECKING, Final, NamedTuple
24
24
 
25
25
  from streamlit.components.lib.local_component_registry import LocalComponentRegistry
26
+ from streamlit.components.v2.component_manager import BidiComponentManager
26
27
  from streamlit.logger import get_logger
27
28
  from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
28
29
  from streamlit.runtime.app_session import AppSession
@@ -101,6 +102,11 @@ class RuntimeConfig:
101
102
  default_factory=LocalComponentRegistry
102
103
  )
103
104
 
105
+ # The BidiComponentManager instance to use for v2 components.
106
+ bidi_component_registry: BidiComponentManager = field(
107
+ default_factory=BidiComponentManager
108
+ )
109
+
104
110
  # The SessionManager class to be used.
105
111
  session_manager_class: type[SessionManager] = WebsocketSessionManager
106
112
 
@@ -201,11 +207,15 @@ class Runtime:
201
207
 
202
208
  # Initialize managers
203
209
  self._component_registry = config.component_registry
210
+ self._bidi_component_registry = config.bidi_component_registry
204
211
  self._uploaded_file_mgr = config.uploaded_file_manager
205
212
  self._media_file_mgr = MediaFileManager(storage=config.media_file_storage)
206
213
  self._cache_storage_manager = config.cache_storage_manager
207
214
  self._script_cache = ScriptCache()
208
215
 
216
+ # Discover and register components for CCv2 from installed packages
217
+ self._bidi_component_registry.discover_and_register_components()
218
+
209
219
  self._session_mgr = config.session_manager_class(
210
220
  session_storage=config.session_storage,
211
221
  uploaded_file_manager=self._uploaded_file_mgr,
@@ -227,6 +237,10 @@ class Runtime:
227
237
  def component_registry(self) -> BaseComponentRegistry:
228
238
  return self._component_registry
229
239
 
240
+ @property
241
+ def bidi_component_registry(self) -> BidiComponentManager:
242
+ return self._bidi_component_registry
243
+
230
244
  @property
231
245
  def uploaded_file_mgr(self) -> UploadedFileManager:
232
246
  return self._uploaded_file_mgr
@@ -15,7 +15,7 @@
15
15
  from __future__ import annotations
16
16
 
17
17
  import sys
18
- from typing import TYPE_CHECKING, Any, Callable, Literal
18
+ from typing import TYPE_CHECKING, Any, Literal
19
19
 
20
20
  from streamlit import util
21
21
  from streamlit.delta_generator_singletons import (
@@ -30,6 +30,7 @@ from streamlit.runtime.scriptrunner_utils.exceptions import (
30
30
  )
31
31
 
32
32
  if TYPE_CHECKING:
33
+ from collections.abc import Callable
33
34
  from types import TracebackType
34
35
 
35
36
  from streamlit.runtime.scriptrunner_utils.script_requests import RerunData
@@ -21,7 +21,7 @@ import types
21
21
  from contextlib import contextmanager
22
22
  from enum import Enum
23
23
  from timeit import default_timer as timer
24
- from typing import TYPE_CHECKING, Any, Callable, Final, Literal, cast
24
+ from typing import TYPE_CHECKING, Any, Final, Literal, cast
25
25
 
26
26
  from blinker import Signal
27
27
 
@@ -62,7 +62,7 @@ from streamlit.runtime.state import (
62
62
  from streamlit.source_util import page_sort_key
63
63
 
64
64
  if TYPE_CHECKING:
65
- from collections.abc import Generator
65
+ from collections.abc import Callable, Generator
66
66
 
67
67
  from streamlit.runtime.fragment import FragmentStorage
68
68
  from streamlit.runtime.scriptrunner.script_cache import ScriptCache
@@ -22,14 +22,11 @@ from collections import Counter
22
22
  from dataclasses import dataclass, field
23
23
  from typing import (
24
24
  TYPE_CHECKING,
25
- Callable,
26
25
  Final,
27
- Union,
26
+ TypeAlias,
28
27
  )
29
28
  from urllib import parse
30
29
 
31
- from typing_extensions import TypeAlias
32
-
33
30
  from streamlit.errors import (
34
31
  NoSessionContext,
35
32
  StreamlitAPIException,
@@ -41,7 +38,7 @@ from streamlit.runtime.forward_msg_cache import (
41
38
  )
42
39
 
43
40
  if TYPE_CHECKING:
44
- from collections.abc import Generator
41
+ from collections.abc import Callable, Generator
45
42
  from pathlib import Path
46
43
 
47
44
  from streamlit.cursor import RunningCursor
@@ -55,7 +52,7 @@ if TYPE_CHECKING:
55
52
  from streamlit.runtime.uploaded_file_manager import UploadedFileManager
56
53
  _LOGGER: Final = get_logger(__name__)
57
54
 
58
- UserInfo: TypeAlias = dict[str, Union[str, bool, None]]
55
+ UserInfo: TypeAlias = dict[str, str | bool | None]
59
56
 
60
57
 
61
58
  # If true, it indicates that we are in a cached function that disallows the usage of
@@ -16,11 +16,10 @@ from __future__ import annotations
16
16
 
17
17
  import os
18
18
  import threading
19
- from collections.abc import ItemsView, Iterator, KeysView, Mapping, ValuesView
19
+ from collections.abc import Callable, ItemsView, Iterator, KeysView, Mapping, ValuesView
20
20
  from copy import deepcopy
21
21
  from typing import (
22
22
  Any,
23
- Callable,
24
23
  Final,
25
24
  NoReturn,
26
25
  )
@@ -461,8 +460,7 @@ class Secrets(Mapping[str, Any]):
461
460
  return value
462
461
  return AttrDict(value)
463
462
  # We add FileNotFoundError since __getattr__ is expected to only raise
464
- # AttributeError. Without handling FileNotFoundError, unittests.mocks
465
- # fails during mock creation on Python3.9
463
+ # AttributeError and mocking utilities expect that contract.
466
464
  except (KeyError, FileNotFoundError):
467
465
  raise AttributeError(_missing_attr_error_message(key))
468
466
 
@@ -16,9 +16,11 @@ from __future__ import annotations
16
16
 
17
17
  from abc import abstractmethod
18
18
  from dataclasses import dataclass
19
- from typing import TYPE_CHECKING, Callable, Protocol, cast
19
+ from typing import TYPE_CHECKING, Protocol, cast
20
20
 
21
21
  if TYPE_CHECKING:
22
+ from collections.abc import Callable
23
+
22
24
  from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
23
25
  from streamlit.runtime.app_session import AppSession
24
26
  from streamlit.runtime.script_data import ScriptData
@@ -16,26 +16,29 @@
16
16
 
17
17
  from __future__ import annotations
18
18
 
19
+ from collections.abc import Callable
19
20
  from dataclasses import dataclass, field
20
21
  from typing import (
22
+ TYPE_CHECKING,
21
23
  Any,
22
- Callable,
23
24
  Final,
24
25
  Generic,
25
26
  Literal,
27
+ TypeAlias,
28
+ TypeGuard,
26
29
  TypeVar,
27
- Union,
28
30
  cast,
29
31
  get_args,
30
32
  )
31
33
 
32
- from typing_extensions import TypeAlias, TypeGuard
33
-
34
34
  from streamlit import util
35
35
  from streamlit.errors import (
36
36
  StreamlitAPIException,
37
37
  )
38
38
 
39
+ if TYPE_CHECKING:
40
+ from streamlit.runtime.state.session_state import SessionState
41
+
39
42
  GENERATED_ELEMENT_ID_PREFIX: Final = "$$ID"
40
43
  TESTING_KEY = "$$STREAMLIT_INTERNAL_KEY_TESTING"
41
44
 
@@ -44,7 +47,7 @@ T = TypeVar("T")
44
47
  T_co = TypeVar("T_co", covariant=True)
45
48
 
46
49
 
47
- WidgetArgs: TypeAlias = Union[tuple[Any, ...], list[Any]]
50
+ WidgetArgs: TypeAlias = tuple[Any, ...] | list[Any]
48
51
  WidgetKwargs: TypeAlias = dict[str, Any]
49
52
  WidgetCallback: TypeAlias = Callable[..., None]
50
53
 
@@ -93,6 +96,7 @@ ValueFieldName: TypeAlias = Literal[
93
96
  "file_uploader_state_value",
94
97
  "int_value",
95
98
  "json_value",
99
+ "json_trigger_value",
96
100
  "string_value",
97
101
  "trigger_value",
98
102
  "string_trigger_value",
@@ -104,6 +108,13 @@ def is_array_value_field_name(obj: object) -> TypeGuard[ArrayValueFieldName]:
104
108
  return obj in _ARRAY_VALUE_FIELD_NAMES
105
109
 
106
110
 
111
+ # Optional hook that allows a widget to customize how its value should be
112
+ # presented in `st.session_state` without altering the underlying stored value
113
+ # or callback semantics. The presenter receives the widget's base value and the
114
+ # SessionState instance in case it needs to access additional widget state.
115
+ WidgetValuePresenter: TypeAlias = Callable[[Any, "SessionState"], Any]
116
+
117
+
107
118
  @dataclass(frozen=True)
108
119
  class WidgetMetadata(Generic[T]):
109
120
  """Metadata associated with a single widget. Immutable."""
@@ -117,11 +128,25 @@ class WidgetMetadata(Generic[T]):
117
128
  # Widget callbacks are called at the start of a script run, before the
118
129
  # body of the script is executed.
119
130
  callback: WidgetCallback | None = None
131
+
132
+ # An optional dictionary of event names to user-code callbacks. These are
133
+ # invoked when the corresponding widget event occurs. Callbacks are called
134
+ # at the start of a script run, before the body of the script is executed.
135
+ # Right now, multiple callbacks are only supported for widgets with a
136
+ # `value_type` of `json_value` or `json_trigger_value`. The keys in this
137
+ # dictionary should correspond to keys in the widget's JSON state.
138
+ callbacks: dict[str, WidgetCallback] | None = None
120
139
  callback_args: WidgetArgs | None = None
121
140
  callback_kwargs: WidgetKwargs | None = None
122
141
 
123
142
  fragment_id: str | None = None
124
143
 
144
+ # Optional presenter hook used for customizing the user-visible value in
145
+ # st.session_state. This is intended for advanced widgets (e.g. Custom
146
+ # Components v2) that need to synthesize a presentation-only value from
147
+ # multiple internal widget states.
148
+ presenter: WidgetValuePresenter | None = None
149
+
125
150
  def __repr__(self) -> str:
126
151
  return util.repr_(self)
127
152
 
@@ -0,0 +1,85 @@
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 TYPE_CHECKING, Any, Final
18
+
19
+ from streamlit.logger import get_logger
20
+
21
+ _LOGGER: Final = get_logger(__name__)
22
+
23
+ if TYPE_CHECKING:
24
+ from streamlit.runtime.state.session_state import SessionState
25
+
26
+
27
+ def apply_presenter(
28
+ session_state: SessionState, widget_id: str, base_value: Any
29
+ ) -> Any:
30
+ """Return the user-visible value for a widget if it has a ``presenter``.
31
+
32
+ If the widget's metadata defines a ``presenter`` callable, it is used to
33
+ transform the stored value into its presentation form. Any exception raised
34
+ while resolving metadata or invoking the presenter is swallowed and
35
+ ``base_value`` is returned, so presentation never interferes with core
36
+ behavior.
37
+
38
+ Notes
39
+ -----
40
+ Presentation is applied exclusively for user-facing access paths such as:
41
+ - `st.session_state[... ]` via `SessionState.__getitem__`
42
+ - `SessionState.filtered_state`
43
+
44
+ Internal serialization paths (for example `WStates.as_widget_states()` and
45
+ `SessionState.get_widget_states()`) must operate on base (unpresented)
46
+ values to ensure stable and lossless serialization. Do not use
47
+ `apply_presenter` in serialization code paths.
48
+
49
+ Parameters
50
+ ----------
51
+ session_state : SessionState
52
+ The current session state object that holds widget state and metadata.
53
+ widget_id : str
54
+ The identifier of the widget whose value is being presented.
55
+ base_value : Any
56
+ The raw value stored for the widget.
57
+
58
+ Returns
59
+ -------
60
+ Any
61
+ The value that should be shown to the user.
62
+ """
63
+
64
+ try:
65
+ meta = session_state._get_widget_metadata(widget_id)
66
+ presenter = getattr(meta, "presenter", None) if meta is not None else None
67
+ if presenter is None:
68
+ return base_value
69
+
70
+ # Ensure the presenter is callable to avoid silently failing on a
71
+ # TypeError when attempting to invoke a non-callable value.
72
+ if not callable(presenter):
73
+ _LOGGER.warning(
74
+ "Widget '%s' has a non-callable presenter (%r); returning base value.",
75
+ widget_id,
76
+ presenter,
77
+ )
78
+ return base_value
79
+ try:
80
+ return presenter(base_value, session_state)
81
+ except Exception:
82
+ return base_value
83
+ except Exception:
84
+ # If metadata is unavailable or any other error occurs, degrade gracefully.
85
+ return base_value
@@ -16,10 +16,10 @@ from __future__ import annotations
16
16
 
17
17
  import threading
18
18
  from contextlib import contextmanager
19
- from typing import TYPE_CHECKING, Any, Callable
19
+ from typing import TYPE_CHECKING, Any
20
20
 
21
21
  if TYPE_CHECKING:
22
- from collections.abc import Iterator
22
+ from collections.abc import Callable, Iterator
23
23
 
24
24
  from streamlit.proto.WidgetStates_pb2 import WidgetState as WidgetStateProto
25
25
  from streamlit.proto.WidgetStates_pb2 import WidgetStates as WidgetStatesProto
@@ -23,14 +23,12 @@ from typing import (
23
23
  TYPE_CHECKING,
24
24
  Any,
25
25
  Final,
26
- Union,
26
+ TypeAlias,
27
27
  cast,
28
28
  )
29
29
 
30
- from typing_extensions import TypeAlias
31
-
32
- import streamlit as st
33
30
  from streamlit import config, util
31
+ from streamlit.delta_generator_singletons import get_dg_singleton_instance
34
32
  from streamlit.errors import StreamlitAPIException, UnserializableSessionStateError
35
33
  from streamlit.proto.WidgetStates_pb2 import WidgetState as WidgetStateProto
36
34
  from streamlit.proto.WidgetStates_pb2 import WidgetStates as WidgetStatesProto
@@ -39,11 +37,14 @@ from streamlit.runtime.state.common import (
39
37
  RegisterWidgetResult,
40
38
  T,
41
39
  ValueFieldName,
40
+ WidgetArgs,
41
+ WidgetCallback,
42
42
  WidgetMetadata,
43
43
  is_array_value_field_name,
44
44
  is_element_id,
45
45
  is_keyed_element_id,
46
46
  )
47
+ from streamlit.runtime.state.presentation import apply_presenter
47
48
  from streamlit.runtime.state.query_params import QueryParams
48
49
  from streamlit.runtime.stats import CacheStat, CacheStatsProvider, group_stats
49
50
 
@@ -71,7 +72,7 @@ class Value:
71
72
  value: Any
72
73
 
73
74
 
74
- WState: TypeAlias = Union[Value, Serialized]
75
+ WState: TypeAlias = Value | Serialized
75
76
 
76
77
 
77
78
  @dataclass
@@ -228,7 +229,7 @@ class WStates(MutableMapping[str, Any]):
228
229
  if is_array_value_field_name(field):
229
230
  arr = getattr(widget, field)
230
231
  arr.data.extend(serialized)
231
- elif field == "json_value":
232
+ elif field in {"json_value", "json_trigger_value"}:
232
233
  setattr(widget, field, json.dumps(serialized))
233
234
  elif field == "file_uploader_state_value":
234
235
  widget.file_uploader_state_value.CopyFrom(serialized)
@@ -401,7 +402,7 @@ class SessionState:
401
402
 
402
403
  @property
403
404
  def filtered_state(self) -> dict[str, Any]:
404
- """The combined session and widget state, excluding keyless widgets."""
405
+ """The combined session and widget state, excluding keyless widgets and internal widgets."""
405
406
 
406
407
  wid_key_map = self._key_id_mapper.id_key_mapping
407
408
 
@@ -414,9 +415,10 @@ class SessionState:
414
415
  for k in self._keys():
415
416
  if not is_element_id(k) and not _is_internal_key(k):
416
417
  state[k] = self[k]
417
- elif is_keyed_element_id(k):
418
+ elif is_keyed_element_id(k) and not _is_internal_key(k):
418
419
  try:
419
420
  key = wid_key_map[k]
421
+ # Value returned by __getitem__ is already presented.
420
422
  state[key] = self[k]
421
423
  except KeyError:
422
424
  # Widget id no longer maps to a key, it is a not yet
@@ -466,7 +468,12 @@ class SessionState:
466
468
  # the "key" is a raw widget id, so get its associated user key for lookup
467
469
  key = wid_key_map[widget_id]
468
470
  try:
469
- return self._getitem(widget_id, key)
471
+ base_value = self._getitem(widget_id, key)
472
+ return (
473
+ apply_presenter(self, widget_id, base_value)
474
+ if widget_id is not None
475
+ else base_value
476
+ )
470
477
  except KeyError:
471
478
  raise KeyError(_missing_key_error_message(key))
472
479
 
@@ -576,19 +583,210 @@ class SessionState:
576
583
  self._call_callbacks()
577
584
 
578
585
  def _call_callbacks(self) -> None:
579
- """Call any callback associated with each widget whose value
580
- changed between the previous and current script runs.
581
- """
586
+ """Call callbacks for widgets whose value changed or whose trigger fired."""
582
587
  from streamlit.runtime.scriptrunner import RerunException
583
588
 
584
- changed_widget_ids = [
585
- wid for wid in self._new_widget_state if self._widget_changed(wid)
589
+ # Path 1: single callback.
590
+ changed_widget_ids_for_single_callback = [
591
+ wid
592
+ for wid in self._new_widget_state
593
+ if self._widget_changed(wid)
594
+ and (metadata := self._new_widget_state.widget_metadata.get(wid))
595
+ is not None
596
+ and metadata.callback is not None
586
597
  ]
587
- for wid in changed_widget_ids:
598
+
599
+ for wid in changed_widget_ids_for_single_callback:
588
600
  try:
589
601
  self._new_widget_state.call_callback(wid)
590
602
  except RerunException: # noqa: PERF203
591
- st.warning("Calling st.rerun() within a callback is a no-op.")
603
+ get_dg_singleton_instance().main_dg.warning(
604
+ "Calling st.rerun() within a callback is a no-op."
605
+ )
606
+
607
+ # Path 2: multiple callbacks.
608
+ widget_ids_to_process = list(self._new_widget_state.states.keys())
609
+
610
+ for wid in widget_ids_to_process:
611
+ metadata = self._new_widget_state.widget_metadata.get(wid)
612
+ if not metadata or metadata.callbacks is None:
613
+ continue
614
+
615
+ args = metadata.callback_args or ()
616
+ kwargs = metadata.callback_kwargs or {}
617
+
618
+ # 1) Trigger dispatch: bool + JSON trigger aggregator
619
+ self._dispatch_trigger_callbacks(wid, metadata, args, kwargs)
620
+
621
+ # 2) JSON value change dispatch
622
+ if metadata.value_type == "json_value":
623
+ self._dispatch_json_change_callbacks(wid, metadata, args, kwargs)
624
+
625
+ def _execute_widget_callback(
626
+ self,
627
+ callback_fn: WidgetCallback,
628
+ cb_metadata: WidgetMetadata[Any],
629
+ cb_args: WidgetArgs,
630
+ cb_kwargs: dict[str, Any],
631
+ ) -> None:
632
+ """Execute a widget callback with fragment-aware context.
633
+
634
+ If the widget belongs to a fragment, temporarily marks the current
635
+ script context as being inside a fragment callback to adapt rerun
636
+ semantics. Attempts to call ``st.rerun()`` inside a widget callback are
637
+ converted to a user-visible warning and treated as a no-op.
638
+
639
+ Parameters
640
+ ----------
641
+ callback_fn : WidgetCallback
642
+ The user-provided callback to execute.
643
+ cb_metadata : WidgetMetadata[Any]
644
+ Metadata of the widget associated with the callback.
645
+ cb_args : WidgetArgs
646
+ Positional arguments passed to the callback.
647
+ cb_kwargs : dict[str, Any]
648
+ Keyword arguments passed to the callback.
649
+ """
650
+ from streamlit.runtime.scriptrunner import RerunException
651
+
652
+ ctx = get_script_run_ctx()
653
+ if ctx and cb_metadata.fragment_id is not None:
654
+ ctx.in_fragment_callback = True
655
+ try:
656
+ callback_fn(*cb_args, **cb_kwargs)
657
+ except RerunException:
658
+ get_dg_singleton_instance().main_dg.warning(
659
+ "Calling st.rerun() within a callback is a no-op."
660
+ )
661
+ finally:
662
+ ctx.in_fragment_callback = False
663
+ else:
664
+ try:
665
+ callback_fn(*cb_args, **cb_kwargs)
666
+ except RerunException:
667
+ get_dg_singleton_instance().main_dg.warning(
668
+ "Calling st.rerun() within a callback is a no-op."
669
+ )
670
+
671
+ def _dispatch_trigger_callbacks(
672
+ self,
673
+ wid: str,
674
+ metadata: WidgetMetadata[Any],
675
+ args: WidgetArgs,
676
+ kwargs: dict[str, Any],
677
+ ) -> None:
678
+ """Dispatch trigger-style callbacks for a widget.
679
+
680
+ Handles the JSON trigger aggregator. The JSON payload may be a single
681
+ event dict or a list of event dicts; each event must contain an
682
+ ``"event"`` field that maps to the corresponding callback name in
683
+ ``metadata.callbacks``.
684
+
685
+ Examples
686
+ --------
687
+ A component with a "submit" callback:
688
+
689
+ >>> metadata.callbacks = {"submit": on_submit}
690
+
691
+ The frontend can send a single event payload:
692
+
693
+ >>> {"event": "submit", "value": "payload"}
694
+
695
+ Or a list of event payloads to be processed in order:
696
+
697
+ >>> [{"event": "edit", ...}, {"event": "submit", ...}]
698
+
699
+ Parameters
700
+ ----------
701
+ wid : str
702
+ The widget ID.
703
+ metadata : WidgetMetadata[Any]
704
+ Metadata for the widget, including registered callbacks.
705
+ args : WidgetArgs
706
+ Positional arguments forwarded to the callback.
707
+ kwargs : dict[str, Any]
708
+ Keyword arguments forwarded to the callback.
709
+ """
710
+ widget_proto_state = self._new_widget_state.get_serialized(wid)
711
+ if not widget_proto_state:
712
+ return
713
+
714
+ # JSON trigger aggregator: value is deserialized by metadata.deserializer
715
+ if widget_proto_state.json_trigger_value:
716
+ try:
717
+ deserialized = self._new_widget_state[wid]
718
+ except KeyError:
719
+ deserialized = None
720
+
721
+ payloads: list[object]
722
+ if isinstance(deserialized, list):
723
+ payloads = deserialized
724
+ else:
725
+ payloads = [deserialized]
726
+
727
+ for payload in payloads:
728
+ if isinstance(payload, dict):
729
+ event_name = payload.get("event")
730
+ if isinstance(event_name, str) and metadata.callbacks:
731
+ cb = metadata.callbacks.get(event_name)
732
+ if cb is not None:
733
+ self._execute_widget_callback(cb, metadata, args, kwargs)
734
+
735
+ def _dispatch_json_change_callbacks(
736
+ self,
737
+ wid: str,
738
+ metadata: WidgetMetadata[Any],
739
+ args: WidgetArgs,
740
+ kwargs: dict[str, Any],
741
+ ) -> None:
742
+ """Dispatch change callbacks for JSON-valued widgets.
743
+
744
+ Computes a shallow diff between the new and old JSON maps and invokes
745
+ callbacks for keys that changed or were added/removed.
746
+
747
+ Parameters
748
+ ----------
749
+ wid : str
750
+ The widget ID.
751
+ metadata : WidgetMetadata[Any]
752
+ Metadata for the widget, including registered callbacks.
753
+ args : WidgetArgs
754
+ Positional arguments forwarded to the callback.
755
+ kwargs : dict[str, Any]
756
+ Keyword arguments forwarded to the callback.
757
+ """
758
+ if not metadata.callbacks:
759
+ return
760
+
761
+ try:
762
+ new_val = self._new_widget_state.get(wid)
763
+ except KeyError:
764
+ new_val = None
765
+ old_val = self._old_state.get(wid)
766
+
767
+ def unwrap(obj: object) -> dict[str, object]:
768
+ if not isinstance(obj, dict):
769
+ return {}
770
+
771
+ obj = cast("dict[str, Any]", obj)
772
+ if set(obj.keys()) == {"value"}:
773
+ value = obj.get("value")
774
+ if isinstance(value, dict):
775
+ return dict(value) # shallow copy
776
+
777
+ return dict(obj)
778
+
779
+ new_map = unwrap(new_val)
780
+ old_map = unwrap(old_val)
781
+
782
+ if new_map or old_map:
783
+ all_keys = new_map.keys() | old_map.keys()
784
+ changed_keys = {k for k in all_keys if old_map.get(k) != new_map.get(k)}
785
+
786
+ for key in changed_keys:
787
+ cb = metadata.callbacks.get(key)
788
+ if cb is not None:
789
+ self._execute_widget_callback(cb, metadata, args, kwargs)
592
790
 
593
791
  def _widget_changed(self, widget_id: str) -> bool:
594
792
  """True if the given widget's value changed between the previous
@@ -623,6 +821,7 @@ class SessionState:
623
821
  elif metadata.value_type in {
624
822
  "string_trigger_value",
625
823
  "chat_input_value",
824
+ "json_trigger_value",
626
825
  }:
627
826
  self._new_widget_state[state_id] = Value(None)
628
827
 
@@ -634,6 +833,7 @@ class SessionState:
634
833
  elif metadata.value_type in {
635
834
  "string_trigger_value",
636
835
  "chat_input_value",
836
+ "json_trigger_value",
637
837
  }:
638
838
  self._old_state[state_id] = None
639
839
 
@@ -663,6 +863,10 @@ class SessionState:
663
863
  )
664
864
  }
665
865
 
866
+ def _get_widget_metadata(self, widget_id: str) -> WidgetMetadata[Any] | None:
867
+ """Return the metadata for a widget id from the current widget state."""
868
+ return self._new_widget_state.widget_metadata.get(widget_id)
869
+
666
870
  def _set_widget_metadata(self, widget_metadata: WidgetMetadata[Any]) -> None:
667
871
  """Set a widget's metadata."""
668
872
  widget_id = widget_metadata.id