streamlit 1.49.1__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 (259) hide show
  1. streamlit/__init__.py +4 -1
  2. streamlit/column_config.py +2 -0
  3. streamlit/commands/navigation.py +7 -7
  4. streamlit/commands/page_config.py +4 -6
  5. streamlit/components/v1/custom_component.py +17 -42
  6. streamlit/components/v2/__init__.py +458 -0
  7. streamlit/components/v2/bidi_component/__init__.py +20 -0
  8. streamlit/components/v2/bidi_component/constants.py +29 -0
  9. streamlit/components/v2/bidi_component/main.py +386 -0
  10. streamlit/components/v2/bidi_component/serialization.py +265 -0
  11. streamlit/components/v2/bidi_component/state.py +92 -0
  12. streamlit/components/v2/component_definition_resolver.py +143 -0
  13. streamlit/components/v2/component_file_watcher.py +403 -0
  14. streamlit/components/v2/component_manager.py +431 -0
  15. streamlit/components/v2/component_manifest_handler.py +122 -0
  16. streamlit/components/v2/component_path_utils.py +245 -0
  17. streamlit/components/v2/component_registry.py +409 -0
  18. streamlit/components/v2/get_bidi_component_manager.py +51 -0
  19. streamlit/components/v2/manifest_scanner.py +615 -0
  20. streamlit/components/v2/presentation.py +198 -0
  21. streamlit/components/v2/types.py +324 -0
  22. streamlit/config.py +741 -32
  23. streamlit/config_option.py +4 -1
  24. streamlit/config_util.py +650 -1
  25. streamlit/connections/base_connection.py +4 -2
  26. streamlit/dataframe_util.py +18 -10
  27. streamlit/delta_generator.py +8 -7
  28. streamlit/delta_generator_singletons.py +3 -1
  29. streamlit/deprecation_util.py +17 -6
  30. streamlit/elements/arrow.py +90 -42
  31. streamlit/elements/deck_gl_json_chart.py +98 -39
  32. streamlit/elements/dialog_decorator.py +2 -1
  33. streamlit/elements/exception.py +3 -1
  34. streamlit/elements/form.py +6 -6
  35. streamlit/elements/graphviz_chart.py +24 -9
  36. streamlit/elements/heading.py +3 -5
  37. streamlit/elements/iframe.py +0 -2
  38. streamlit/elements/image.py +12 -13
  39. streamlit/elements/layouts.py +89 -22
  40. streamlit/elements/lib/built_in_chart_utils.py +95 -31
  41. streamlit/elements/lib/color_util.py +8 -18
  42. streamlit/elements/lib/column_config_utils.py +9 -8
  43. streamlit/elements/lib/column_types.py +595 -148
  44. streamlit/elements/lib/dialog.py +3 -2
  45. streamlit/elements/lib/image_utils.py +3 -5
  46. streamlit/elements/lib/layout_utils.py +50 -13
  47. streamlit/elements/lib/mutable_status_container.py +2 -2
  48. streamlit/elements/lib/options_selector_utils.py +2 -2
  49. streamlit/elements/lib/pandas_styler_utils.py +30 -14
  50. streamlit/elements/lib/utils.py +21 -9
  51. streamlit/elements/map.py +81 -40
  52. streamlit/elements/media.py +7 -7
  53. streamlit/elements/metric.py +11 -35
  54. streamlit/elements/pdf.py +2 -4
  55. streamlit/elements/plotly_chart.py +142 -26
  56. streamlit/elements/progress.py +2 -4
  57. streamlit/elements/pyplot.py +6 -6
  58. streamlit/elements/space.py +113 -0
  59. streamlit/elements/vega_charts.py +400 -143
  60. streamlit/elements/widgets/audio_input.py +52 -4
  61. streamlit/elements/widgets/button.py +29 -29
  62. streamlit/elements/widgets/button_group.py +33 -6
  63. streamlit/elements/widgets/camera_input.py +3 -4
  64. streamlit/elements/widgets/chat.py +7 -0
  65. streamlit/elements/widgets/checkbox.py +1 -0
  66. streamlit/elements/widgets/color_picker.py +1 -0
  67. streamlit/elements/widgets/data_editor.py +34 -29
  68. streamlit/elements/widgets/file_uploader.py +6 -10
  69. streamlit/elements/widgets/multiselect.py +14 -3
  70. streamlit/elements/widgets/number_input.py +5 -4
  71. streamlit/elements/widgets/radio.py +10 -2
  72. streamlit/elements/widgets/select_slider.py +8 -4
  73. streamlit/elements/widgets/selectbox.py +9 -2
  74. streamlit/elements/widgets/slider.py +38 -41
  75. streamlit/elements/widgets/text_widgets.py +6 -0
  76. streamlit/elements/widgets/time_widgets.py +15 -12
  77. streamlit/elements/write.py +28 -23
  78. streamlit/emojis.py +1 -1
  79. streamlit/errors.py +115 -0
  80. streamlit/git_util.py +65 -43
  81. streamlit/hello/hello.py +8 -0
  82. streamlit/hello/utils.py +2 -1
  83. streamlit/material_icon_names.py +1 -1
  84. streamlit/navigation/page.py +4 -1
  85. streamlit/proto/ArrowData_pb2.py +27 -0
  86. streamlit/proto/ArrowData_pb2.pyi +46 -0
  87. streamlit/proto/Arrow_pb2.py +10 -8
  88. streamlit/proto/Arrow_pb2.pyi +31 -2
  89. streamlit/proto/AudioInput_pb2.py +2 -2
  90. streamlit/proto/AudioInput_pb2.pyi +6 -2
  91. streamlit/proto/BidiComponent_pb2.py +34 -0
  92. streamlit/proto/BidiComponent_pb2.pyi +153 -0
  93. streamlit/proto/Block_pb2.py +11 -11
  94. streamlit/proto/Block_pb2.pyi +9 -1
  95. streamlit/proto/DeckGlJsonChart_pb2.py +10 -4
  96. streamlit/proto/DeckGlJsonChart_pb2.pyi +9 -3
  97. streamlit/proto/Element_pb2.py +5 -3
  98. streamlit/proto/Element_pb2.pyi +14 -4
  99. streamlit/proto/HeightConfig_pb2.py +2 -2
  100. streamlit/proto/HeightConfig_pb2.pyi +6 -3
  101. streamlit/proto/NewSession_pb2.py +18 -16
  102. streamlit/proto/NewSession_pb2.pyi +158 -6
  103. streamlit/proto/PlotlyChart_pb2.py +8 -6
  104. streamlit/proto/PlotlyChart_pb2.pyi +3 -1
  105. streamlit/proto/Space_pb2.py +27 -0
  106. streamlit/proto/Space_pb2.pyi +42 -0
  107. streamlit/proto/WidgetStates_pb2.py +2 -2
  108. streamlit/proto/WidgetStates_pb2.pyi +13 -3
  109. streamlit/proto/WidthConfig_pb2.py +2 -2
  110. streamlit/proto/WidthConfig_pb2.pyi +6 -3
  111. streamlit/runtime/app_session.py +45 -6
  112. streamlit/runtime/caching/cache_data_api.py +4 -4
  113. streamlit/runtime/caching/cache_errors.py +4 -1
  114. streamlit/runtime/caching/cache_resource_api.py +3 -2
  115. streamlit/runtime/caching/cache_utils.py +2 -1
  116. streamlit/runtime/caching/cached_message_replay.py +3 -3
  117. streamlit/runtime/caching/hashing.py +3 -4
  118. streamlit/runtime/caching/legacy_cache_api.py +2 -1
  119. streamlit/runtime/connection_factory.py +1 -3
  120. streamlit/runtime/forward_msg_queue.py +4 -1
  121. streamlit/runtime/fragment.py +2 -1
  122. streamlit/runtime/memory_media_file_storage.py +1 -1
  123. streamlit/runtime/metrics_util.py +6 -2
  124. streamlit/runtime/runtime.py +14 -0
  125. streamlit/runtime/scriptrunner/exec_code.py +2 -1
  126. streamlit/runtime/scriptrunner/script_runner.py +2 -2
  127. streamlit/runtime/scriptrunner_utils/script_run_context.py +3 -6
  128. streamlit/runtime/secrets.py +2 -4
  129. streamlit/runtime/session_manager.py +3 -1
  130. streamlit/runtime/state/common.py +30 -5
  131. streamlit/runtime/state/presentation.py +85 -0
  132. streamlit/runtime/state/safe_session_state.py +2 -2
  133. streamlit/runtime/state/session_state.py +220 -16
  134. streamlit/runtime/state/widgets.py +19 -3
  135. streamlit/runtime/theme_util.py +148 -0
  136. streamlit/runtime/websocket_session_manager.py +3 -1
  137. streamlit/source_util.py +2 -2
  138. streamlit/static/index.html +2 -2
  139. streamlit/static/manifest.json +244 -227
  140. streamlit/static/static/css/{index.C8X8rNzw.css → index.BpABIXK9.css} +1 -1
  141. streamlit/static/static/css/index.DgR7E2CV.css +1 -0
  142. streamlit/static/static/js/{ErrorOutline.esm.DcGrhbBP.js → ErrorOutline.esm.YoJdlW1p.js} +1 -1
  143. streamlit/static/static/js/{FileDownload.esm.DgBvV6Pq.js → FileDownload.esm.Ddx8VEYy.js} +1 -1
  144. streamlit/static/static/js/{FileHelper.M6AAaeuA.js → FileHelper.90EtOmj9.js} +1 -1
  145. streamlit/static/static/js/{FormClearHelper.DHh1GFzm.js → FormClearHelper.BB1Km6eP.js} +1 -1
  146. streamlit/static/static/js/InputInstructions.jhH15PqV.js +1 -0
  147. streamlit/static/static/js/{Particles.DDVT-6Qc.js → Particles.DUsputn1.js} +1 -1
  148. streamlit/static/static/js/{ProgressBar.BEY0cXXV.js → ProgressBar.DLY8H6nE.js} +2 -2
  149. streamlit/static/static/js/Toolbar.D8nHCkuz.js +1 -0
  150. streamlit/static/static/js/{base-input.CK3UVGp1.js → base-input.CJGiNqed.js} +3 -3
  151. streamlit/static/static/js/{checkbox.D8W881TL.js → checkbox.Cpdd482O.js} +1 -1
  152. streamlit/static/static/js/{createSuper.B6W-Dh9S.js → createSuper.CuQIogbW.js} +1 -1
  153. streamlit/static/static/js/data-grid-overlay-editor.2Ufgxc6y.js +1 -0
  154. streamlit/static/static/js/{downloader.DiKpuU_S.js → downloader.CN0K7xlu.js} +1 -1
  155. streamlit/static/static/js/{es6.B8zRNPZ-.js → es6.BJcsVXQ0.js} +2 -2
  156. streamlit/static/static/js/{iframeResizer.contentWindow.DIewJmmh.js → iframeResizer.contentWindow.XzUvQqcZ.js} +1 -1
  157. streamlit/static/static/js/index.B1ZQh4P1.js +1 -0
  158. streamlit/static/static/js/index.BKstZk0M.js +27 -0
  159. streamlit/static/static/js/{index.Bte_9Lyq.js → index.BMcFsUee.js} +1 -1
  160. streamlit/static/static/js/{index.qhs54UAB.js → index.BR-IdcTb.js} +1 -1
  161. streamlit/static/static/js/{index.CejBxbg1.js → index.B_dWA3vd.js} +1 -1
  162. streamlit/static/static/js/{index.D5naqx-J.js → index.BgnZEMVh.js} +1 -1
  163. streamlit/static/static/js/{index.C7fRKRs4.js → index.BohqXifI.js} +1 -1
  164. streamlit/static/static/js/{index.cnnXF7xQ.js → index.Br5nxKNj.js} +1 -1
  165. streamlit/static/static/js/index.BrIKVbNc.js +3 -0
  166. streamlit/static/static/js/index.BtWUPzle.js +1 -0
  167. streamlit/static/static/js/index.C0RLraek.js +1 -0
  168. streamlit/static/static/js/{index.CP5TD2z1.js → index.CAIjskgG.js} +1 -1
  169. streamlit/static/static/js/{index.CD8HuT3N.js → index.CAj-7vWz.js} +135 -162
  170. streamlit/static/static/js/{index.DtYN2x4k.js → index.CMtEit2O.js} +1 -1
  171. streamlit/static/static/js/index.CkRlykEE.js +12 -0
  172. streamlit/static/static/js/{index.Ts_0SdB9.js → index.CmN3FXfI.js} +2 -2
  173. streamlit/static/static/js/{index.BnEpvLEz.js → index.CwbFI1_-.js} +1 -1
  174. streamlit/static/static/js/{index.CcJf6BCU.js → index.CxIUUfab.js} +27 -27
  175. streamlit/static/static/js/index.D2KPNy7e.js +1 -0
  176. streamlit/static/static/js/{index.Ch7MBCx0.js → index.D3GPA5k4.js} +47 -47
  177. streamlit/static/static/js/{index.ho6NIXGl.js → index.DGAh7DMq.js} +1 -1
  178. streamlit/static/static/js/index.DKb_NvmG.js +197 -0
  179. streamlit/static/static/js/{index.CvYYtxD_.js → index.DMqgUYKq.js} +1 -1
  180. streamlit/static/static/js/{index.zecpGxtj.js → index.DOFlg3dS.js} +1 -1
  181. streamlit/static/static/js/{index.B9mjBcgE.js → index.DPUXkcQL.js} +1 -1
  182. streamlit/static/static/js/index.DX1xY89g.js +1 -0
  183. streamlit/static/static/js/index.DYATBCsq.js +2 -0
  184. streamlit/static/static/js/{index.D2-atlaQ.js → index.DaSmGJ76.js} +3 -3
  185. streamlit/static/static/js/index.Dd7bMeLP.js +1 -0
  186. streamlit/static/static/js/{index.4eF4NxG2.js → index.DjmmgI5U.js} +1 -1
  187. streamlit/static/static/js/index.Dq56CyM2.js +1 -0
  188. streamlit/static/static/js/index.DuiXaS5_.js +7 -0
  189. streamlit/static/static/js/index.DvFidMLe.js +2 -0
  190. streamlit/static/static/js/{index.452cqrrL.js → index.DwkhC5Pc.js} +1 -1
  191. streamlit/static/static/js/{index.Dk4C7X3i.js → index.Q-3sFn1v.js} +1 -1
  192. streamlit/static/static/js/{index.CjXWwH-y.js → index.QJ5QO9sJ.js} +1 -1
  193. streamlit/static/static/js/{index.B6U8LQo3.js → index.VwTaeety.js} +1 -1
  194. streamlit/static/static/js/index.YOqQbeX8.js +1 -0
  195. streamlit/static/static/js/{input.nzVJphXi.js → input.D4MN_FzN.js} +1 -1
  196. streamlit/static/static/js/{memory.CjCgTQz3.js → memory.DrZjtdGT.js} +1 -1
  197. streamlit/static/static/js/{number-overlay-editor.DaRFzZEO.js → number-overlay-editor.DRwAw1In.js} +1 -1
  198. streamlit/static/static/js/{possibleConstructorReturn.DgiPnZ9N.js → possibleConstructorReturn.exeeJQEP.js} +1 -1
  199. streamlit/static/static/js/record.B-tDciZb.js +1 -0
  200. streamlit/static/static/js/{sandbox.mithfq7Z.js → sandbox.ClO3IuUr.js} +1 -1
  201. streamlit/static/static/js/{timepicker.Dbl5KFh6.js → timepicker.DAhu-vcF.js} +4 -4
  202. streamlit/static/static/js/{toConsumableArray.D-Dx88BQ.js → toConsumableArray.DNbljYEC.js} +1 -1
  203. streamlit/static/static/js/{uniqueId.Bh26R_3S.js → uniqueId.oG4Gvj1v.js} +1 -1
  204. streamlit/static/static/js/{useBasicWidgetState.DeK-QJpD.js → useBasicWidgetState.D6sOH6oI.js} +1 -1
  205. streamlit/static/static/js/{useTextInputAutoExpand.4iAdLWD-.js → useTextInputAutoExpand.4u3_GcuN.js} +2 -2
  206. streamlit/static/static/js/{useUpdateUiValue.CmT7_nJN.js → useUpdateUiValue.F2R3eTeR.js} +1 -1
  207. streamlit/static/static/js/wavesurfer.esm.vI8Eid4k.js +73 -0
  208. streamlit/static/static/js/withFullScreenWrapper.zothJIsI.js +1 -0
  209. streamlit/static/static/media/MaterialSymbols-Rounded.C7IFxh57.woff2 +0 -0
  210. streamlit/string_util.py +56 -1
  211. streamlit/testing/v1/app_test.py +2 -2
  212. streamlit/testing/v1/element_tree.py +23 -9
  213. streamlit/testing/v1/util.py +2 -2
  214. streamlit/type_util.py +3 -4
  215. streamlit/url_util.py +1 -3
  216. streamlit/user_info.py +1 -2
  217. streamlit/util.py +3 -1
  218. streamlit/watcher/event_based_path_watcher.py +23 -12
  219. streamlit/watcher/local_sources_watcher.py +11 -1
  220. streamlit/watcher/path_watcher.py +9 -6
  221. streamlit/watcher/polling_path_watcher.py +4 -1
  222. streamlit/watcher/util.py +2 -2
  223. streamlit/web/bootstrap.py +0 -31
  224. streamlit/web/cli.py +51 -22
  225. streamlit/web/server/bidi_component_request_handler.py +193 -0
  226. streamlit/web/server/component_file_utils.py +97 -0
  227. streamlit/web/server/component_request_handler.py +8 -21
  228. streamlit/web/server/oidc_mixin.py +3 -1
  229. streamlit/web/server/routes.py +18 -5
  230. streamlit/web/server/server.py +10 -0
  231. streamlit/web/server/server_util.py +3 -1
  232. streamlit/web/server/upload_file_request_handler.py +3 -1
  233. {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/METADATA +4 -5
  234. {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/RECORD +238 -209
  235. streamlit/static/static/css/index.COe1010n.css +0 -1
  236. streamlit/static/static/js/Hooks.DGu1od_L.js +0 -1
  237. streamlit/static/static/js/InputInstructions.z6sVgyYt.js +0 -1
  238. streamlit/static/static/js/Toolbar.DSnK1fUh.js +0 -1
  239. streamlit/static/static/js/data-grid-overlay-editor.DRTHOydk.js +0 -1
  240. streamlit/static/static/js/index.BXYmrqnf.js +0 -1
  241. streamlit/static/static/js/index.B_8AnktO.js +0 -1
  242. streamlit/static/static/js/index.Bl7zGQSh.js +0 -7
  243. streamlit/static/static/js/index.BnJIOYn9.js +0 -73
  244. streamlit/static/static/js/index.C1HcTl5K.js +0 -1
  245. streamlit/static/static/js/index.C7lSmSOP.js +0 -1
  246. streamlit/static/static/js/index.C_tmcx4B.js +0 -1
  247. streamlit/static/static/js/index.D3K5nOu9.js +0 -197
  248. streamlit/static/static/js/index.DkKT3LUI.js +0 -1
  249. streamlit/static/static/js/index.MTPPBDHk.js +0 -2
  250. streamlit/static/static/js/index.pqW9AMJD.js +0 -3
  251. streamlit/static/static/js/index.urHgTgMQ.js +0 -12
  252. streamlit/static/static/js/index.wzkv_11M.js +0 -1
  253. streamlit/static/static/js/index.yF5AncHY.js +0 -1
  254. streamlit/static/static/js/withFullScreenWrapper.DLp1ENGm.js +0 -1
  255. streamlit/static/static/media/MaterialSymbols-Rounded.CBxVaFdk.woff2 +0 -0
  256. {streamlit-1.49.1.data → streamlit-1.51.0.data}/scripts/streamlit.cmd +0 -0
  257. {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/WHEEL +0 -0
  258. {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/entry_points.txt +0 -0
  259. {streamlit-1.49.1.dist-info → streamlit-1.51.0.dist-info}/top_level.txt +0 -0
@@ -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
@@ -16,6 +16,7 @@ from __future__ import annotations
16
16
 
17
17
  from typing import TYPE_CHECKING
18
18
 
19
+ from streamlit.errors import StreamlitAPIException
19
20
  from streamlit.runtime.state.common import (
20
21
  RegisterWidgetResult,
21
22
  T,
@@ -26,6 +27,7 @@ from streamlit.runtime.state.common import (
26
27
  WidgetKwargs,
27
28
  WidgetMetadata,
28
29
  WidgetSerializer,
30
+ WidgetValuePresenter,
29
31
  user_key_from_element_id,
30
32
  )
31
33
 
@@ -39,10 +41,12 @@ def register_widget(
39
41
  deserializer: WidgetDeserializer[T],
40
42
  serializer: WidgetSerializer[T],
41
43
  ctx: ScriptRunContext | None,
44
+ callbacks: dict[str, WidgetCallback] | None = None,
42
45
  on_change_handler: WidgetCallback | None = None,
43
46
  args: WidgetArgs | None = None,
44
47
  kwargs: WidgetKwargs | None = None,
45
48
  value_type: ValueFieldName,
49
+ presenter: WidgetValuePresenter | None = None,
46
50
  ) -> RegisterWidgetResult[T]:
47
51
  """Register a widget with Streamlit, and return its current value.
48
52
  NOTE: This function should be called after the proto has been filled.
@@ -58,13 +62,15 @@ def register_widget(
58
62
  Called to convert a widget's value to its protobuf representation.
59
63
  ctx : ScriptRunContext or None
60
64
  Used to ensure uniqueness of widget IDs, and to look up widget values.
65
+ callbacks : dict[str, WidgetCallback] or None
66
+ A dictionary of callbacks for multi-callback support.
61
67
  on_change_handler : WidgetCallback or None
62
68
  An optional callback invoked when the widget's value changes.
63
69
  args : WidgetArgs or None
64
- args to pass to on_change_handler when invoked
70
+ Positional arguments to pass to the `on_change_handler` or `callbacks`.
65
71
  kwargs : WidgetKwargs or None
66
- kwargs to pass to on_change_handler when invoked
67
- value_type: ValueType
72
+ Keyword arguments to pass to the `on_change_handler` or `callbacks`.
73
+ value_type: ValueFieldName
68
74
  The value_type the widget is going to use.
69
75
  We use this information to start with a best-effort guess for the value_type
70
76
  of each widget. Once we actually receive a proto for a widget from the
@@ -72,6 +78,9 @@ def register_widget(
72
78
  not able to always rely on the proto as the type may be needed earlier.
73
79
  Thankfully, in these cases (when value_type == "trigger_value"), the static
74
80
  table here being slightly inaccurate should never pose a problem.
81
+ presenter : WidgetValuePresenter or None
82
+ An optional hook that allows a widget to customize how its value should be
83
+ presented.
75
84
 
76
85
 
77
86
  Returns
@@ -98,6 +107,11 @@ def register_widget(
98
107
  For both paths a widget return value is provided, allowing the widgets
99
108
  to be used in a non-streamlit setting.
100
109
  """
110
+ if on_change_handler is not None and callbacks is not None:
111
+ raise StreamlitAPIException(
112
+ "Cannot provide both `on_change` and `callbacks` to a widget."
113
+ )
114
+
101
115
  # Create the widget's updated metadata, and register it with session_state.
102
116
  metadata = WidgetMetadata(
103
117
  element_id,
@@ -105,9 +119,11 @@ def register_widget(
105
119
  serializer,
106
120
  value_type=value_type,
107
121
  callback=on_change_handler,
122
+ callbacks=callbacks,
108
123
  callback_args=args,
109
124
  callback_kwargs=kwargs,
110
125
  fragment_id=ctx.current_fragment_id if ctx else None,
126
+ presenter=presenter,
111
127
  )
112
128
  return register_widget_from_metadata(metadata, ctx)
113
129