streamlit 1.53.0__py3-none-any.whl → 1.54.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 (311) hide show
  1. streamlit/__init__.py +1 -31
  2. streamlit/auth_util.py +91 -2
  3. streamlit/cli_util.py +3 -2
  4. streamlit/commands/echo.py +2 -2
  5. streamlit/commands/execution_control.py +1 -1
  6. streamlit/commands/logo.py +76 -24
  7. streamlit/commands/navigation.py +1 -1
  8. streamlit/components/types/base_custom_component.py +0 -2
  9. streamlit/components/v1/custom_component.py +0 -2
  10. streamlit/components/v2/bidi_component/main.py +2 -2
  11. streamlit/components/v2/component_path_utils.py +17 -29
  12. streamlit/components/v2/manifest_scanner.py +8 -3
  13. streamlit/components/v2/presentation.py +1 -1
  14. streamlit/config.py +57 -13
  15. streamlit/config_util.py +5 -5
  16. streamlit/connections/snowflake_connection.py +6 -3
  17. streamlit/dataframe_util.py +10 -10
  18. streamlit/deprecation_util.py +19 -1
  19. streamlit/elements/arrow.py +18 -8
  20. streamlit/elements/deck_gl_json_chart.py +6 -2
  21. streamlit/elements/exception.py +4 -2
  22. streamlit/elements/form.py +1 -1
  23. streamlit/elements/layouts.py +1 -1
  24. streamlit/elements/lib/built_in_chart_utils.py +36 -13
  25. streamlit/elements/lib/color_util.py +21 -2
  26. streamlit/elements/lib/column_config_utils.py +9 -7
  27. streamlit/elements/lib/dialog.py +1 -1
  28. streamlit/elements/lib/image_utils.py +5 -5
  29. streamlit/elements/lib/layout_utils.py +1 -1
  30. streamlit/elements/lib/options_selector_utils.py +112 -18
  31. streamlit/elements/lib/policies.py +1 -1
  32. streamlit/elements/lib/streamlit_plotly_theme.py +9 -11
  33. streamlit/elements/lib/utils.py +1 -1
  34. streamlit/elements/map.py +6 -6
  35. streamlit/elements/plotly_chart.py +2 -2
  36. streamlit/elements/toast.py +1 -1
  37. streamlit/elements/vega_charts.py +30 -7
  38. streamlit/elements/widgets/button.py +3 -3
  39. streamlit/elements/widgets/button_group.py +3 -3
  40. streamlit/elements/widgets/chat.py +1 -1
  41. streamlit/elements/widgets/data_editor.py +6 -6
  42. streamlit/elements/widgets/multiselect.py +32 -8
  43. streamlit/elements/widgets/number_input.py +1 -1
  44. streamlit/elements/widgets/radio.py +91 -31
  45. streamlit/elements/widgets/select_slider.py +123 -37
  46. streamlit/elements/widgets/selectbox.py +38 -16
  47. streamlit/elements/widgets/slider.py +5 -5
  48. streamlit/elements/widgets/time_widgets.py +150 -18
  49. streamlit/elements/write.py +2 -3
  50. streamlit/env_util.py +1 -1
  51. streamlit/errors.py +2 -14
  52. streamlit/external/langchain/streamlit_callback_handler.py +1 -1
  53. streamlit/hello/dataframe_demo.py +1 -1
  54. streamlit/hello/plotting_demo.py +19 -12
  55. streamlit/path_security.py +98 -0
  56. streamlit/proto/Alert_pb2.py +2 -3
  57. streamlit/proto/AppPage_pb2.py +2 -3
  58. streamlit/proto/ArrowData_pb2.py +2 -3
  59. streamlit/proto/ArrowNamedDataSet_pb2.py +2 -3
  60. streamlit/proto/ArrowVegaLiteChart_pb2.py +2 -3
  61. streamlit/proto/Arrow_pb2.py +2 -3
  62. streamlit/proto/AudioInput_pb2.py +2 -3
  63. streamlit/proto/Audio_pb2.py +2 -3
  64. streamlit/proto/AuthRedirect_pb2.py +2 -3
  65. streamlit/proto/AutoRerun_pb2.py +2 -3
  66. streamlit/proto/BackMsg_pb2.py +2 -3
  67. streamlit/proto/Balloons_pb2.py +2 -3
  68. streamlit/proto/BidiComponent_pb2.py +2 -3
  69. streamlit/proto/Block_pb2.py +2 -3
  70. streamlit/proto/BokehChart_pb2.py +2 -3
  71. streamlit/proto/ButtonGroup_pb2.py +2 -3
  72. streamlit/proto/ButtonLikeIconPosition_pb2.py +2 -3
  73. streamlit/proto/Button_pb2.py +2 -3
  74. streamlit/proto/CameraInput_pb2.py +2 -3
  75. streamlit/proto/ChatInput_pb2.py +2 -3
  76. streamlit/proto/Checkbox_pb2.py +2 -3
  77. streamlit/proto/ClientState_pb2.py +2 -3
  78. streamlit/proto/Code_pb2.py +2 -3
  79. streamlit/proto/ColorPicker_pb2.py +2 -3
  80. streamlit/proto/Common_pb2.py +2 -3
  81. streamlit/proto/Components_pb2.py +2 -3
  82. streamlit/proto/DataFrame_pb2.py +2 -3
  83. streamlit/proto/DateInput_pb2.py +2 -3
  84. streamlit/proto/DateTimeInput_pb2.py +2 -3
  85. streamlit/proto/DeckGlJsonChart_pb2.py +2 -3
  86. streamlit/proto/Delta_pb2.py +2 -3
  87. streamlit/proto/DocString_pb2.py +2 -3
  88. streamlit/proto/DownloadButton_pb2.py +2 -3
  89. streamlit/proto/Element_pb2.py +2 -3
  90. streamlit/proto/Empty_pb2.py +2 -3
  91. streamlit/proto/Exception_pb2.py +2 -3
  92. streamlit/proto/Favicon_pb2.py +2 -3
  93. streamlit/proto/FileUploader_pb2.py +2 -3
  94. streamlit/proto/ForwardMsg_pb2.py +2 -3
  95. streamlit/proto/GapSize_pb2.py +2 -3
  96. streamlit/proto/GitInfo_pb2.py +2 -3
  97. streamlit/proto/GraphVizChart_pb2.py +2 -3
  98. streamlit/proto/Heading_pb2.py +2 -3
  99. streamlit/proto/HeightConfig_pb2.py +2 -3
  100. streamlit/proto/Html_pb2.py +2 -3
  101. streamlit/proto/IFrame_pb2.py +2 -3
  102. streamlit/proto/Image_pb2.py +2 -3
  103. streamlit/proto/Json_pb2.py +2 -3
  104. streamlit/proto/LabelVisibilityMessage_pb2.py +2 -3
  105. streamlit/proto/LinkButton_pb2.py +2 -3
  106. streamlit/proto/Logo_pb2.py +6 -5
  107. streamlit/proto/Logo_pb2.pyi +25 -1
  108. streamlit/proto/Markdown_pb2.py +2 -3
  109. streamlit/proto/Metric_pb2.py +2 -3
  110. streamlit/proto/MetricsEvent_pb2.py +2 -3
  111. streamlit/proto/MultiSelect_pb2.py +2 -3
  112. streamlit/proto/NamedDataSet_pb2.py +2 -3
  113. streamlit/proto/Navigation_pb2.py +2 -3
  114. streamlit/proto/NewSession_pb2.py +25 -24
  115. streamlit/proto/NewSession_pb2.pyi +28 -2
  116. streamlit/proto/NumberInput_pb2.py +2 -3
  117. streamlit/proto/PageConfig_pb2.py +2 -3
  118. streamlit/proto/PageInfo_pb2.py +2 -3
  119. streamlit/proto/PageLink_pb2.py +2 -3
  120. streamlit/proto/PageNotFound_pb2.py +2 -3
  121. streamlit/proto/PageProfile_pb2.py +2 -3
  122. streamlit/proto/PagesChanged_pb2.py +2 -3
  123. streamlit/proto/ParentMessage_pb2.py +2 -3
  124. streamlit/proto/PlotlyChart_pb2.py +2 -3
  125. streamlit/proto/Progress_pb2.py +2 -3
  126. streamlit/proto/Radio_pb2.py +5 -4
  127. streamlit/proto/Radio_pb2.pyi +20 -3
  128. streamlit/proto/RootContainer_pb2.py +2 -3
  129. streamlit/proto/Selectbox_pb2.py +2 -3
  130. streamlit/proto/SessionEvent_pb2.py +2 -3
  131. streamlit/proto/SessionStatus_pb2.py +2 -3
  132. streamlit/proto/Skeleton_pb2.py +2 -3
  133. streamlit/proto/Slider_pb2.py +7 -8
  134. streamlit/proto/Slider_pb2.pyi +9 -1
  135. streamlit/proto/Snow_pb2.py +2 -3
  136. streamlit/proto/Space_pb2.py +2 -3
  137. streamlit/proto/Spinner_pb2.py +2 -3
  138. streamlit/proto/TextAlignmentConfig_pb2.py +2 -3
  139. streamlit/proto/TextArea_pb2.py +2 -3
  140. streamlit/proto/TextInput_pb2.py +2 -3
  141. streamlit/proto/Text_pb2.py +2 -3
  142. streamlit/proto/TimeInput_pb2.py +2 -3
  143. streamlit/proto/Toast_pb2.py +2 -3
  144. streamlit/proto/Transient_pb2.py +2 -3
  145. streamlit/proto/VegaLiteChart_pb2.py +2 -3
  146. streamlit/proto/Video_pb2.py +2 -3
  147. streamlit/proto/WidgetStates_pb2.py +2 -3
  148. streamlit/proto/WidthConfig_pb2.py +2 -3
  149. streamlit/proto/openmetrics_data_model_pb2.py +2 -3
  150. streamlit/runtime/app_session.py +106 -60
  151. streamlit/runtime/caching/cache_data_api.py +3 -3
  152. streamlit/runtime/caching/cache_errors.py +0 -2
  153. streamlit/runtime/caching/cache_resource_api.py +1 -1
  154. streamlit/runtime/caching/cache_utils.py +2 -2
  155. streamlit/runtime/caching/hashing.py +1 -3
  156. streamlit/runtime/caching/storage/cache_storage_protocol.py +0 -3
  157. streamlit/runtime/connection_factory.py +1 -1
  158. streamlit/runtime/credentials.py +2 -2
  159. streamlit/runtime/metrics_util.py +3 -3
  160. streamlit/runtime/runtime.py +6 -6
  161. streamlit/runtime/scriptrunner/script_runner.py +17 -0
  162. streamlit/runtime/scriptrunner_utils/exceptions.py +0 -4
  163. streamlit/runtime/scriptrunner_utils/script_run_context.py +13 -31
  164. streamlit/runtime/secrets.py +3 -4
  165. streamlit/runtime/state/__init__.py +7 -1
  166. streamlit/runtime/state/common.py +13 -0
  167. streamlit/runtime/state/query_params.py +493 -24
  168. streamlit/runtime/state/session_state.py +179 -4
  169. streamlit/runtime/state/widgets.py +26 -1
  170. streamlit/runtime/stats.py +1 -10
  171. streamlit/static/index.html +1 -1
  172. streamlit/static/manifest.json +304 -304
  173. streamlit/static/static/js/{ErrorOutline.esm.Cxoit62D.js → ErrorOutline.esm.BWk6F-Tz.js} +1 -1
  174. streamlit/static/static/js/{FileDownload.esm.Cym2KVOR.js → FileDownload.esm.AllYUuOW.js} +1 -1
  175. streamlit/static/static/js/{FileHelper.C47VLeXF.js → FileHelper.BvVTNdmy.js} +1 -1
  176. streamlit/static/static/js/{FormClearHelper.CUrwwEeX.js → FormClearHelper.C__r5Llk.js} +1 -1
  177. streamlit/static/static/js/{InputInstructions.DyVOE42q.js → InputInstructions.DOtkdOMV.js} +1 -1
  178. streamlit/static/static/js/Particles.DCsqQZlE.js +1 -0
  179. streamlit/static/static/js/{ProgressBar.qKdiDYyx.js → ProgressBar.DLCRvt4m.js} +2 -2
  180. streamlit/static/static/js/{StreamlitSyntaxHighlighter.DUPp9dS3.js → StreamlitSyntaxHighlighter.CYFWoZHb.js} +1 -1
  181. streamlit/static/static/js/{TableChart.esm.C_g2CvCE.js → TableChart.esm.D6ydHcIm.js} +1 -1
  182. streamlit/static/static/js/Toolbar.BHDNzWBx.js +1 -0
  183. streamlit/static/static/js/{WidgetLabelHelpIconInline.Dy4yV6I2.js → WidgetLabelHelpIconInline.DEXBrVlc.js} +1 -1
  184. streamlit/static/static/js/{base-input.DQAb60v0.js → base-input.TSQjctlq.js} +4 -4
  185. streamlit/static/static/js/{checkbox.C0HE0ojW.js → checkbox.BKgfzJZV.js} +1 -1
  186. streamlit/static/static/js/{createDownloadLinkElement.DBMfH8_e.js → createDownloadLinkElement.CG7nr2a4.js} +1 -1
  187. streamlit/static/static/js/{data-grid-overlay-editor.CSZWem5Q.js → data-grid-overlay-editor.ChXO__lP.js} +1 -1
  188. streamlit/static/static/js/{downloader.Bp8c7mYD.js → downloader.DJ3R_zWA.js} +1 -1
  189. streamlit/static/static/js/embed.u3PPfLkw.js +193 -0
  190. streamlit/static/static/js/{es6.j7akTCaI.js → es6.C5Mfy8nd.js} +2 -2
  191. streamlit/static/static/js/{formatNumber.CfuUiEpF.js → formatNumber.CMRgW9EJ.js} +1 -1
  192. streamlit/static/static/js/{iconPosition.BVSTKfGd.js → iconPosition.B4EEXI3E.js} +1 -1
  193. streamlit/static/static/js/{iframeResizer.contentWindow.BZ3lugzo.js → iframeResizer.contentWindow.WSvOiTW0.js} +1 -1
  194. streamlit/static/static/js/index.-FOBV3nz.js +1 -0
  195. streamlit/static/static/js/{index.D0tXFTaW.js → index.-NF8OSF5.js} +1 -1
  196. streamlit/static/static/js/{index.Dk0CU4R6.js → index.4cBg8kn5.js} +1 -1
  197. streamlit/static/static/js/{index.DtZTtufl.js → index.B0pzzCsH.js} +1 -1
  198. streamlit/static/static/js/{index.DSaE74nc.js → index.BID6ND5j.js} +2 -2
  199. streamlit/static/static/js/index.BMp5bGjh.js +1 -0
  200. streamlit/static/static/js/{index.CAMxgVFm.js → index.BQcmlvas.js} +1 -1
  201. streamlit/static/static/js/{index.C0F0G-wg.js → index.BRcmclgI.js} +1 -1
  202. streamlit/static/static/js/index.BaUZR4IG.js +1 -0
  203. streamlit/static/static/js/{index.Cow0Hs9V.js → index.BbMJj4PN.js} +1 -1
  204. streamlit/static/static/js/{index.iboGgrkh.js → index.BdCTJtq3.js} +2 -2
  205. streamlit/static/static/js/index.BdETLMuI.js +1 -0
  206. streamlit/static/static/js/index.BnKMWhs1.js +1 -0
  207. streamlit/static/static/js/index.Br1kXwQW.js +2 -0
  208. streamlit/static/static/js/{index.B2fTHpId.js → index.Bt2olRE4.js} +1 -1
  209. streamlit/static/static/js/{index.DBIRzFM7.js → index.Bxwsv5T8.js} +1 -1
  210. streamlit/static/static/js/index.C4KskYz6.js +1 -0
  211. streamlit/static/static/js/{index.BgCYNmov.js → index.C6bmbXk0.js} +1 -1
  212. streamlit/static/static/js/{index.7S_sCSRx.js → index.CEfKfbta.js} +1 -1
  213. streamlit/static/static/js/index.CIuaA8q0.js +2 -0
  214. streamlit/static/static/js/{index.CWAvu1Qu.js → index.CV1sObFX.js} +1 -1
  215. streamlit/static/static/js/{index.C9QftD-S.js → index.CbR6dgaV.js} +1 -1
  216. streamlit/static/static/js/index.Cq6szKqJ.js +1 -0
  217. streamlit/static/static/js/index.CyouXqCz.js +1 -0
  218. streamlit/static/static/js/{index.BMFt07G_.js → index.D1NUgMFI.js} +1 -1
  219. streamlit/static/static/js/{index.Tq2okoAU.js → index.D7SWG4Om.js} +1 -1
  220. streamlit/static/static/js/{index.DgJeIFb5.js → index.DAYPEwLI.js} +1 -1
  221. streamlit/static/static/js/index.DKS75Vfg.js +11 -0
  222. streamlit/static/static/js/{index.FfR9SXQv.js → index.DOXrMIxB.js} +1 -1
  223. streamlit/static/static/js/{index.BiVJWMS-.js → index.DOzYX8yS.js} +3 -3
  224. streamlit/static/static/js/{index.nEa8y_He.js → index.DRFMYcC4.js} +4 -4
  225. streamlit/static/static/js/{index.dgs1TGpP.js → index.Divl5FCY.js} +1 -1
  226. streamlit/static/static/js/{index.95DldRtG.js → index.DjAJ_CUa.js} +1 -1
  227. streamlit/static/static/js/{index.Z0mB4zBp.js → index.Dncue2pm.js} +33 -33
  228. streamlit/static/static/js/{index.DFT9nVK6.js → index.Drusyo5m.js} +48 -48
  229. streamlit/static/static/js/{index.1PD6f3vh.js → index.DuUyDGnP.js} +1 -1
  230. streamlit/static/static/js/{index.DpU0Bc2F.js → index.DvgT2rB2.js} +223 -223
  231. streamlit/static/static/js/{index.Bukztsaz.js → index.DzutABu5.js} +2 -2
  232. streamlit/static/static/js/index.Dzw2iPzi.js +3 -0
  233. streamlit/static/static/js/{index.DYkkO_of.js → index.FsTmxLbT.js} +1 -1
  234. streamlit/static/static/js/{index.CTQ8QcOV.js → index.OIwPqGYN.js} +1 -1
  235. streamlit/static/static/js/{index.NtSfVVJe.js → index.RXLN7YFT.js} +2 -2
  236. streamlit/static/static/js/{index.BU3d_gp1.js → index.YYb2u0jk.js} +2 -2
  237. streamlit/static/static/js/{index.BXfSsjdq.js → index.h8ejt-W3.js} +1 -1
  238. streamlit/static/static/js/{index.gPUFpUqs.js → index.lFMCi9am.js} +1 -1
  239. streamlit/static/static/js/{index.BDA5l7b9.js → index.pOgf4cEj.js} +1 -1
  240. streamlit/static/static/js/index.s_E0s7LB.js +188 -0
  241. streamlit/static/static/js/{index.DysJZEAt.js → index.xLCbzoqj.js} +1 -1
  242. streamlit/static/static/js/{input.Pz8Lwzsi.js → input.BLG7kWaj.js} +2 -2
  243. streamlit/static/static/js/{main.BeiYkHRo.js → main.D_CmqChN.js} +1 -1
  244. streamlit/static/static/js/{memory.Dyx_JBbb.js → memory.T8u9KqIQ.js} +1 -1
  245. streamlit/static/static/js/{number-overlay-editor.NLIdF6b9.js → number-overlay-editor.BKBSXkAM.js} +2 -2
  246. streamlit/static/static/js/{pandasStylerUtils.DsNlDEqS.js → pandasStylerUtils.B4tLYMwS.js} +1 -1
  247. streamlit/static/static/js/{sandbox.bER7qtR1.js → sandbox.jRlkcPem.js} +1 -1
  248. streamlit/static/static/js/{styled-components.DcoFBb7G.js → styled-components.D2QhNwzd.js} +1 -1
  249. streamlit/static/static/js/{throttle.DOaQWO4U.js → throttle.Cyw_V0Dq.js} +1 -1
  250. streamlit/static/static/js/{timepicker.RjHB2IT4.js → timepicker.PzyuDDWl.js} +1 -1
  251. streamlit/static/static/js/{toConsumableArray.DFAIugL0.js → toConsumableArray.gE9fMkLj.js} +1 -1
  252. streamlit/static/static/js/uniqueId.B1GeHnT1.js +1 -0
  253. streamlit/static/static/js/{useBasicWidgetState.CTtyymrp.js → useBasicWidgetState.DFklfao0.js} +1 -1
  254. streamlit/static/static/js/{useIntlLocale.DG5haQGX.js → useIntlLocale.C3tUGWTU.js} +8 -8
  255. streamlit/static/static/js/{useTextInputAutoExpand.Cnfcep1Z.js → useTextInputAutoExpand.D9nU_y-e.js} +1 -1
  256. streamlit/static/static/js/useUpdateUiValue.ClTdrkJN.js +1 -0
  257. streamlit/static/static/js/{useWaveformController.DozaayUB.js → useWaveformController.lzTbjMW2.js} +1 -1
  258. streamlit/static/static/js/{withCalculatedWidth.SNNFFxhJ.js → withCalculatedWidth.Dxs9I5Oe.js} +1 -1
  259. streamlit/static/static/js/{withFullScreenWrapper.Dl2f8_gt.js → withFullScreenWrapper.DfpAcJxf.js} +1 -1
  260. streamlit/string_util.py +2 -2
  261. streamlit/testing/v1/app_test.py +1 -1
  262. streamlit/testing/v1/element_tree.py +33 -20
  263. streamlit/type_util.py +2 -2
  264. streamlit/url_util.py +2 -2
  265. streamlit/user_info.py +2 -41
  266. streamlit/util.py +1 -1
  267. streamlit/watcher/event_based_path_watcher.py +37 -7
  268. streamlit/watcher/path_watcher.py +61 -2
  269. streamlit/watcher/util.py +26 -10
  270. streamlit/web/bootstrap.py +16 -4
  271. streamlit/web/cli.py +1 -4
  272. streamlit/web/server/app_discovery.py +2 -1
  273. streamlit/web/server/app_static_file_handler.py +9 -0
  274. streamlit/web/server/bidi_component_request_handler.py +4 -4
  275. streamlit/web/server/component_file_utils.py +14 -6
  276. streamlit/web/server/component_request_handler.py +2 -2
  277. streamlit/web/server/oauth_authlib_routes.py +14 -42
  278. streamlit/web/server/server.py +1 -1
  279. streamlit/web/server/server_util.py +23 -1
  280. streamlit/web/server/starlette/starlette_app.py +7 -1
  281. streamlit/web/server/starlette/starlette_auth_routes.py +94 -16
  282. streamlit/web/server/starlette/starlette_path_security_middleware.py +97 -0
  283. streamlit/web/server/starlette/starlette_routes.py +16 -9
  284. streamlit/web/server/starlette/starlette_server.py +2 -2
  285. streamlit/web/server/starlette/starlette_static_routes.py +14 -4
  286. streamlit/web/server/stats_request_handler.py +1 -3
  287. {streamlit-1.53.0.dist-info → streamlit-1.54.0.dist-info}/METADATA +10 -25
  288. {streamlit-1.53.0.dist-info → streamlit-1.54.0.dist-info}/RECORD +291 -291
  289. {streamlit-1.53.0.dist-info → streamlit-1.54.0.dist-info}/WHEEL +1 -1
  290. streamlit/commands/experimental_query_params.py +0 -169
  291. streamlit/static/static/js/Particles.D5ZUTvE6.js +0 -1
  292. streamlit/static/static/js/Toolbar.BbO8bxwz.js +0 -1
  293. streamlit/static/static/js/embed.DQBlGL9Q.js +0 -195
  294. streamlit/static/static/js/index.5CsPRetw.js +0 -1
  295. streamlit/static/static/js/index.BGgra9Bb.js +0 -188
  296. streamlit/static/static/js/index.BGzJYcHz.js +0 -1
  297. streamlit/static/static/js/index.BNpEDrb2.js +0 -1
  298. streamlit/static/static/js/index.Bk5wGJXh.js +0 -1
  299. streamlit/static/static/js/index.By8GIgDH.js +0 -1
  300. streamlit/static/static/js/index.C8VoW8Ph.js +0 -1
  301. streamlit/static/static/js/index.CZzy-Gct.js +0 -1
  302. streamlit/static/static/js/index.CeFdbzfR.js +0 -11
  303. streamlit/static/static/js/index.CkmNfvPD.js +0 -1
  304. streamlit/static/static/js/index.CsmTnJl4.js +0 -3
  305. streamlit/static/static/js/index.DZGCJu4I.js +0 -2
  306. streamlit/static/static/js/index.svncz-Ad.js +0 -2
  307. streamlit/static/static/js/uniqueId.DEvFPH9n.js +0 -1
  308. streamlit/static/static/js/useUpdateUiValue.BWnXwmrp.js +0 -1
  309. streamlit-1.53.0.data/scripts/streamlit.cmd +0 -16
  310. {streamlit-1.53.0.dist-info → streamlit-1.54.0.dist-info}/entry_points.txt +0 -0
  311. {streamlit-1.53.0.dist-info → streamlit-1.54.0.dist-info}/top_level.txt +0 -0
@@ -290,12 +290,14 @@ def validate_and_sync_value_with_options(
290
290
  opt: Sequence[T],
291
291
  default_index: int | None,
292
292
  key: str | int | None,
293
+ format_func: Callable[[Any], str] = str,
293
294
  ) -> tuple[T | None, bool]:
294
295
  """Validate current value against options, resetting session state if invalid.
295
296
 
296
297
  This function has a side-effect: if the value is not found in the options
297
298
  and a key is provided, it will update session state with the new value.
298
299
 
300
+
299
301
  Parameters
300
302
  ----------
301
303
  current_value
@@ -306,6 +308,11 @@ def validate_and_sync_value_with_options(
306
308
  The default index to reset to if value is invalid.
307
309
  key
308
310
  The widget key for session state updates.
311
+ format_func
312
+ Function to format options for comparison. Used to compare values by their
313
+ string representation instead of using == directly. This is necessary because
314
+ widget values are deepcopied, and for custom classes without __eq__, the
315
+ deepcopied instances would fail identity comparison.
309
316
 
310
317
  Returns
311
318
  -------
@@ -315,30 +322,37 @@ def validate_and_sync_value_with_options(
315
322
  if current_value is None:
316
323
  return current_value, False
317
324
 
318
- # Check if current value is still in the new options
325
+ # Use format_func comparison for all values. This correctly handles:
326
+ # - Custom objects without __eq__ (deepcopied instances)
327
+ # - Enum values (already from current class due to serde deserialization)
328
+ formatted_options_set = {format_func(o) for o in opt}
319
329
  try:
320
- index_(opt, current_value)
321
- return current_value, False
322
- except ValueError:
323
- # Value not in options - reset to default
324
- if default_index is not None and len(opt) > 0:
325
- new_value: T | None = opt[default_index]
326
- else:
327
- new_value = None
330
+ formatted_value = format_func(current_value)
331
+ if formatted_value in formatted_options_set:
332
+ return current_value, False
333
+ except Exception: # noqa: S110
334
+ pass # format_func failed - value is invalid, fall through to reset
335
+
336
+ # Value not in options - reset to default
337
+ if default_index is not None and len(opt) > 0:
338
+ new_value: T | None = opt[default_index]
339
+ else:
340
+ new_value = None
328
341
 
329
- if key is not None:
330
- # Update session_state so subsequent accesses in this run
331
- # return the corrected value. Use reset_state_value to avoid
332
- # the "cannot be modified after widget instantiated" error.
333
- get_session_state().reset_state_value(str(key), new_value)
342
+ if key is not None:
343
+ # Update session_state so subsequent accesses in this run
344
+ # return the corrected value. Use reset_state_value to avoid
345
+ # the "cannot be modified after widget instantiated" error.
346
+ get_session_state().reset_state_value(str(key), new_value)
334
347
 
335
- return new_value, True
348
+ return new_value, True
336
349
 
337
350
 
338
351
  def validate_and_sync_multiselect_value_with_options(
339
352
  current_values: list[T] | list[T | str],
340
353
  opt: Sequence[T],
341
354
  key: str | int | None,
355
+ format_func: Callable[[Any], str] = str,
342
356
  ) -> tuple[list[T] | list[T | str], bool]:
343
357
  """Validate multiselect values against options, syncing session state if needed.
344
358
 
@@ -356,6 +370,11 @@ def validate_and_sync_multiselect_value_with_options(
356
370
  The sequence of valid options.
357
371
  key
358
372
  The widget key for session state updates.
373
+ format_func
374
+ Function to format options for comparison. Used to compare values by their
375
+ string representation instead of using == directly. This is necessary because
376
+ widget values are deepcopied, and for custom classes without __eq__, the
377
+ deepcopied instances would fail identity comparison.
359
378
 
360
379
  Returns
361
380
  -------
@@ -365,13 +384,26 @@ def validate_and_sync_multiselect_value_with_options(
365
384
  if not current_values:
366
385
  return current_values, False
367
386
 
387
+ # Create a set of formatted options for O(1) lookup.
388
+ # We use format_func to compare values by their string representation
389
+ # instead of using == directly. This is necessary because widget values
390
+ # are deepcopied, and for custom classes without __eq__, the deepcopied
391
+ # instances would fail identity comparison.
392
+ formatted_options_set = {format_func(o) for o in opt}
393
+
368
394
  valid_values: list[T | str] = []
369
395
  for value in current_values:
370
396
  try:
371
- index_(opt, value)
397
+ formatted_value = format_func(value)
398
+ except Exception: # noqa: S112
399
+ # format_func failed on this value (e.g., a string value from a previous
400
+ # session when format_func expects an object with specific attributes).
401
+ # In this case, the value is definitely not valid since the current options
402
+ # can be formatted successfully.
403
+ continue
404
+
405
+ if formatted_value in formatted_options_set:
372
406
  valid_values.append(value)
373
- except ValueError: # noqa: PERF203
374
- pass
375
407
 
376
408
  if len(valid_values) == len(current_values):
377
409
  return current_values, False
@@ -380,3 +412,65 @@ def validate_and_sync_multiselect_value_with_options(
380
412
  get_session_state().reset_state_value(str(key), valid_values)
381
413
 
382
414
  return valid_values, True
415
+
416
+
417
+ def validate_and_sync_range_value_with_options(
418
+ current_value: tuple[T, T],
419
+ opt: Sequence[T],
420
+ default_indices: list[int],
421
+ key: str | int | None,
422
+ format_func: Callable[[Any], str] = str,
423
+ ) -> tuple[tuple[T, T], bool]:
424
+ """Validate a range value (tuple of two values) against options.
425
+
426
+ If either value in the range is not found in options, the entire range is
427
+ reset to the default. This function has a side-effect: if the values are
428
+ invalid and a key is provided, it will update session state with the new value.
429
+
430
+ Parameters
431
+ ----------
432
+ current_value
433
+ The current range value (tuple of two values) to validate.
434
+ opt
435
+ The sequence of valid options.
436
+ default_indices
437
+ The default indices to reset to if value is invalid. Should contain
438
+ at least one index; if only one index is provided, the second default
439
+ will be the last option.
440
+ key
441
+ The widget key for session state updates.
442
+ format_func
443
+ Function to format options for comparison. Used to compare values by their
444
+ string representation instead of using == directly.
445
+
446
+ Returns
447
+ -------
448
+ tuple[tuple[T, T], bool]
449
+ A tuple of (validated_value, value_was_reset).
450
+ """
451
+ if len(opt) == 0:
452
+ return current_value, False
453
+
454
+ formatted_options_set = {format_func(o) for o in opt}
455
+
456
+ def is_valid(val: Any) -> bool:
457
+ """Check if a value exists in options via format_func comparison."""
458
+ try:
459
+ return format_func(val) in formatted_options_set
460
+ except Exception:
461
+ return False
462
+
463
+ def get_default_range() -> tuple[T, T]:
464
+ """Get the default range value."""
465
+ end_idx = default_indices[1] if len(default_indices) > 1 else len(opt) - 1
466
+ return (opt[default_indices[0]], opt[end_idx])
467
+
468
+ # Validate both values in the range.
469
+ if is_valid(current_value[0]) and is_valid(current_value[1]):
470
+ return current_value, False
471
+
472
+ # Either value is invalid - reset entire range.
473
+ new_value = get_default_range()
474
+ if key is not None:
475
+ get_session_state().reset_state_value(str(key), new_value)
476
+ return new_value, True
@@ -188,7 +188,7 @@ def maybe_raise_label_warnings(label: str | None, label_visibility: str | None)
188
188
  "if needed.",
189
189
  stack_info=True,
190
190
  )
191
- if label_visibility not in ("visible", "hidden", "collapsed"):
191
+ if label_visibility not in {"visible", "hidden", "collapsed"}:
192
192
  raise errors.StreamlitAPIException(
193
193
  f"Unsupported label_visibility option '{label_visibility}'. "
194
194
  f"Valid values are 'visible', 'hidden' or 'collapsed'."
@@ -58,7 +58,6 @@ DIVERGING_6: Final = "#000027"
58
58
  DIVERGING_7: Final = "#000028"
59
59
  DIVERGING_8: Final = "#000029"
60
60
  DIVERGING_9: Final = "#000030"
61
- DIVERGING_10: Final = "#000031"
62
61
 
63
62
  INCREASING: Final = "#000032"
64
63
  DECREASING: Final = "#000033"
@@ -189,16 +188,15 @@ def configure_streamlit_plotly_theme() -> None:
189
188
  sequentialminus=streamlit_colorscale,
190
189
  diverging=[
191
190
  [0.0, DIVERGING_0],
192
- [0.1, DIVERGING_1],
193
- [0.2, DIVERGING_2],
194
- [0.3, DIVERGING_3],
195
- [0.4, DIVERGING_4],
196
- [0.5, DIVERGING_5],
197
- [0.6, DIVERGING_6],
198
- [0.7, DIVERGING_7],
199
- [0.8, DIVERGING_8],
200
- [0.9, DIVERGING_9],
201
- [1.0, DIVERGING_10],
191
+ [0.1111111111111111, DIVERGING_1],
192
+ [0.2222222222222222, DIVERGING_2],
193
+ [0.3333333333333333, DIVERGING_3],
194
+ [0.4444444444444444, DIVERGING_4],
195
+ [0.5555555555555556, DIVERGING_5],
196
+ [0.6666666666666666, DIVERGING_6],
197
+ [0.7777777777777778, DIVERGING_7],
198
+ [0.8888888888888888, DIVERGING_8],
199
+ [1.0, DIVERGING_9],
202
200
  ],
203
201
  ),
204
202
  coloraxis=go.layout.Coloraxis(colorscale=streamlit_colorscale),
@@ -59,10 +59,10 @@ SAFE_VALUES: TypeAlias = Union[
59
59
  time,
60
60
  datetime,
61
61
  timedelta,
62
- None,
63
62
  "ellipsis",
64
63
  Message,
65
64
  PROTO_SCALAR_VALUE,
65
+ None,
66
66
  ]
67
67
 
68
68
 
streamlit/elements/map.py CHANGED
@@ -93,8 +93,8 @@ class MapMixin:
93
93
  *,
94
94
  latitude: str | None = None,
95
95
  longitude: str | None = None,
96
- color: None | str | Color = None,
97
- size: None | str | float = None,
96
+ color: str | Color | None = None,
97
+ size: str | float | None = None,
98
98
  zoom: int | None = None,
99
99
  width: WidthWithoutContent = "stretch",
100
100
  height: HeightWithoutContent = 500,
@@ -296,8 +296,8 @@ def to_deckgl_json(
296
296
  data: Data,
297
297
  lat: str | None,
298
298
  lon: str | None,
299
- size: None | str | float,
300
- color: None | str | Collection[float],
299
+ size: str | float | None,
300
+ color: str | Collection[float] | None,
301
301
  zoom: int | None,
302
302
  ) -> str:
303
303
  if data is None:
@@ -432,7 +432,7 @@ def _convert_color_arg_or_column(
432
432
  data: DataFrame,
433
433
  color_arg: str,
434
434
  color_col_name: str | None,
435
- ) -> None | str | IntColorTuple:
435
+ ) -> str | IntColorTuple | None:
436
436
  """Converts color to a format accepted by PyDeck.
437
437
 
438
438
  For example:
@@ -443,7 +443,7 @@ def _convert_color_arg_or_column(
443
443
  NOTE: This function mutates the data argument.
444
444
  """
445
445
 
446
- color_arg_out: None | str | IntColorTuple = None
446
+ color_arg_out: str | IntColorTuple | None = None
447
447
 
448
448
  if color_col_name is not None:
449
449
  # Convert color column to the right format.
@@ -677,14 +677,14 @@ class PlotlyMixin:
677
677
  "options."
678
678
  )
679
679
 
680
- if theme not in ["streamlit", None]:
680
+ if theme not in {"streamlit", None}:
681
681
  raise StreamlitAPIException(
682
682
  f'You set theme="{theme}" while Streamlit charts only support '
683
683
  "theme=”streamlit” or theme=None to fallback to the default "
684
684
  "library theme."
685
685
  )
686
686
 
687
- if on_select not in ["ignore", "rerun"] and not callable(on_select):
687
+ if on_select not in {"ignore", "rerun"} and not callable(on_select):
688
688
  raise StreamlitAPIException(
689
689
  f"You have passed {on_select} to `on_select`. But only 'ignore', "
690
690
  "'rerun', or a callable is supported."
@@ -151,7 +151,7 @@ class ToastMixin:
151
151
  toast_proto.body = clean_text(validate_text(body))
152
152
  toast_proto.icon = validate_icon_or_emoji(icon)
153
153
 
154
- if duration in ["short", "long", "infinite"] or (
154
+ if duration in {"short", "long", "infinite"} or (
155
155
  isinstance(duration, int) and duration > 0
156
156
  ):
157
157
  if duration == "short":
@@ -725,6 +725,10 @@ class VegaChartsMixin:
725
725
  - An RGB or RGBA tuple with the red, green, blue, and alpha
726
726
  components specified as ints from 0 to 255 or floats from 0.0 to
727
727
  1.0.
728
+ - A built-in color name: "red", "orange", "yellow", "green",
729
+ "blue", "violet", "gray"/"grey", or "primary". These map to
730
+ theme colors that you can customize using ``theme.<color>Color``
731
+ configuration options.
728
732
 
729
733
  For a line chart with multiple lines, where the dataframe is in
730
734
  long format (that is, y is None or just one column), this can be:
@@ -753,7 +757,8 @@ class VegaChartsMixin:
753
757
  - A list of string colors or color tuples to be used for each of
754
758
  the lines in the chart. This list should have the same length
755
759
  as the number of y values (e.g. ``color=["#fd0", "#f0f", "#04f"]``
756
- for three lines).
760
+ for three lines). You can also use built-in color names in the
761
+ list (e.g. ``color=["red", "blue", "green"]``).
757
762
 
758
763
  You can set the default colors in the ``theme.chartCategoryColors``
759
764
  configuration option.
@@ -959,6 +964,10 @@ class VegaChartsMixin:
959
964
  - An RGB or RGBA tuple with the red, green, blue, and alpha
960
965
  components specified as ints from 0 to 255 or floats from 0.0 to
961
966
  1.0.
967
+ - A built-in color name: "red", "orange", "yellow", "green",
968
+ "blue", "violet", "gray"/"grey", or "primary". These map to
969
+ theme colors that you can customize using ``theme.<color>Color``
970
+ configuration options.
962
971
 
963
972
  For an area chart with multiple series, where the dataframe is in
964
973
  long format (that is, y is None or just one column), this can be:
@@ -987,7 +996,8 @@ class VegaChartsMixin:
987
996
  - A list of string colors or color tuples to be used for each of
988
997
  the series in the chart. This list should have the same length
989
998
  as the number of y values (e.g. ``color=["#fd0", "#f0f", "#04f"]``
990
- for three lines).
999
+ for three lines). You can also use built-in color names in the
1000
+ list (e.g. ``color=["red", "blue", "green"]``).
991
1001
 
992
1002
  You can set the default colors in the ``theme.chartCategoryColors``
993
1003
  configuration option.
@@ -1248,6 +1258,10 @@ class VegaChartsMixin:
1248
1258
  - An RGB or RGBA tuple with the red, green, blue, and alpha
1249
1259
  components specified as ints from 0 to 255 or floats from 0.0 to
1250
1260
  1.0.
1261
+ - A built-in color name: "red", "orange", "yellow", "green",
1262
+ "blue", "violet", "gray"/"grey", or "primary". These map to
1263
+ theme colors that you can customize using ``theme.<color>Color``
1264
+ configuration options.
1251
1265
 
1252
1266
  For a bar chart with multiple series, where the dataframe is in
1253
1267
  long format (that is, y is None or just one column), this can be:
@@ -1276,7 +1290,8 @@ class VegaChartsMixin:
1276
1290
  - A list of string colors or color tuples to be used for each of
1277
1291
  the series in the chart. This list should have the same length
1278
1292
  as the number of y values (e.g. ``color=["#fd0", "#f0f", "#04f"]``
1279
- for three lines).
1293
+ for three lines). You can also use built-in color names in the
1294
+ list (e.g. ``color=["red", "blue", "green"]``).
1280
1295
 
1281
1296
  You can set the default colors in the ``theme.chartCategoryColors``
1282
1297
  configuration option.
@@ -1575,6 +1590,10 @@ class VegaChartsMixin:
1575
1590
  - An RGB or RGBA tuple with the red, green, blue, and alpha
1576
1591
  components specified as ints from 0 to 255 or floats from 0.0 to
1577
1592
  1.0.
1593
+ - A built-in color name: "red", "orange", "yellow", "green",
1594
+ "blue", "violet", "gray"/"grey", or "primary". These map to
1595
+ theme colors that you can customize using ``theme.<color>Color``
1596
+ configuration options.
1578
1597
  - The name of a column in the dataset where the color of that
1579
1598
  datapoint will come from.
1580
1599
 
@@ -1602,7 +1621,8 @@ class VegaChartsMixin:
1602
1621
  - A list of string colors or color tuples to be used for each of
1603
1622
  the series in the chart. This list should have the same length
1604
1623
  as the number of y values (e.g. ``color=["#fd0", "#f0f", "#04f"]``
1605
- for three series).
1624
+ for three series). You can also use built-in color names in the
1625
+ list (e.g. ``color=["red", "blue", "green"]``).
1606
1626
 
1607
1627
  size : str, float, int, or None
1608
1628
  The size of the circles representing each point.
@@ -2282,14 +2302,14 @@ class VegaChartsMixin:
2282
2302
 
2283
2303
  See the `vega_lite_chart` method docstring for more information.
2284
2304
  """
2285
- if theme not in ["streamlit", None]:
2305
+ if theme not in {"streamlit", None}:
2286
2306
  raise StreamlitAPIException(
2287
2307
  f'You set theme="{theme}" while Streamlit charts only support '
2288
2308
  "theme=”streamlit” or theme=None to fallback to the default "
2289
2309
  "library theme."
2290
2310
  )
2291
2311
 
2292
- if on_select not in ["ignore", "rerun"] and not callable(on_select):
2312
+ if on_select not in {"ignore", "rerun"} and not callable(on_select):
2293
2313
  raise StreamlitAPIException(
2294
2314
  f"You have passed {on_select} to `on_select`. But only 'ignore', "
2295
2315
  "'rerun', or a callable is supported."
@@ -2404,7 +2424,10 @@ class VegaChartsMixin:
2404
2424
  vega_lite_proto.id = compute_and_register_element_id(
2405
2425
  "arrow_vega_lite_chart",
2406
2426
  user_key=key,
2407
- key_as_main_identity=False,
2427
+ # There are some edge cases where selections can become orphaned when the data changes.
2428
+ # The frontend can handle this without errors, but it might be a nice enhancement
2429
+ # to automatically reset the backend & frontend selection state in this case.
2430
+ key_as_main_identity={"selection_mode"},
2408
2431
  dg=self.dg,
2409
2432
  vega_lite_spec=vega_lite_proto.spec,
2410
2433
  # The data is either in vega_lite_proto.data.data
@@ -356,7 +356,7 @@ class ButtonMixin:
356
356
  width = "stretch" if use_container_width else "content"
357
357
 
358
358
  # Checks whether the entered button type is one of the allowed options
359
- if type not in ["primary", "secondary", "tertiary"]:
359
+ if type not in {"primary", "secondary", "tertiary"}:
360
360
  raise StreamlitAPIException(
361
361
  'The type argument to st.button must be "primary", "secondary", or "tertiary". '
362
362
  f'\nThe argument passed was "{type}".'
@@ -736,7 +736,7 @@ class ButtonMixin:
736
736
  if use_container_width is not None:
737
737
  width = "stretch" if use_container_width else "content"
738
738
 
739
- if type not in ["primary", "secondary", "tertiary"]:
739
+ if type not in {"primary", "secondary", "tertiary"}:
740
740
  raise StreamlitAPIException(
741
741
  'The type argument to st.download_button must be "primary", "secondary", or "tertiary". \n'
742
742
  f'The argument passed was "{type}".'
@@ -915,7 +915,7 @@ class ButtonMixin:
915
915
 
916
916
  """
917
917
  # Checks whether the entered button type is one of the allowed options - either "primary" or "secondary"
918
- if type not in ["primary", "secondary", "tertiary"]:
918
+ if type not in {"primary", "secondary", "tertiary"}:
919
919
  raise StreamlitAPIException(
920
920
  'The type argument to st.link_button must be "primary", "secondary", or "tertiary". '
921
921
  f'\nThe argument passed was "{type}".'
@@ -255,7 +255,7 @@ def _build_proto(
255
255
 
256
256
  def _maybe_raise_selection_mode_warning(selection_mode: SelectionMode) -> None:
257
257
  """Check if the selection_mode value is valid or raise exception otherwise."""
258
- if selection_mode not in ["single", "multi"]:
258
+ if selection_mode not in {"single", "multi"}:
259
259
  raise StreamlitAPIException(
260
260
  "The selection_mode argument must be one of ['single', 'multi']. "
261
261
  f"The argument passed was '{selection_mode}'."
@@ -410,7 +410,7 @@ class ButtonGroupMixin:
410
410
 
411
411
  """
412
412
 
413
- if options not in ["thumbs", "faces", "stars"]:
413
+ if options not in {"thumbs", "faces", "stars"}:
414
414
  raise StreamlitAPIException(
415
415
  "The options argument to st.feedback must be one of "
416
416
  "['thumbs', 'faces', 'stars']. "
@@ -1062,7 +1062,7 @@ class ButtonGroupMixin:
1062
1062
  "`selection_mode='single'`."
1063
1063
  )
1064
1064
 
1065
- if style not in ["borderless", "pills", "segmented_control"]:
1065
+ if style not in {"borderless", "pills", "segmented_control"}:
1066
1066
  raise StreamlitAPIException(
1067
1067
  "The style argument must be one of ['borderless', 'pills', 'segmented_control']. "
1068
1068
  f"The argument passed was '{style}'."
@@ -223,7 +223,7 @@ def _process_avatar_input(
223
223
  AvatarType.ICON,
224
224
  (
225
225
  "assistant"
226
- if avatar in [PresetNames.AI, PresetNames.ASSISTANT]
226
+ if avatar in {PresetNames.AI, PresetNames.ASSISTANT}
227
227
  else "user"
228
228
  ),
229
229
  )
@@ -238,11 +238,11 @@ def _parse_value(
238
238
  if column_data_kind == ColumnDataKind.TIMEDELTA:
239
239
  return pd.Timedelta(value)
240
240
 
241
- if column_data_kind in [
241
+ if column_data_kind in {
242
242
  ColumnDataKind.DATETIME,
243
243
  ColumnDataKind.DATE,
244
244
  ColumnDataKind.TIME,
245
- ]:
245
+ }:
246
246
  datetime_value = pd.Timestamp(value)
247
247
 
248
248
  if pd.isna(datetime_value):
@@ -481,7 +481,7 @@ def _is_supported_index(df_index: pd.Index[Any]) -> bool:
481
481
 
482
482
  return (
483
483
  type(df_index)
484
- in [
484
+ in {
485
485
  pd.RangeIndex,
486
486
  pd.Index,
487
487
  pd.DatetimeIndex,
@@ -490,7 +490,7 @@ def _is_supported_index(df_index: pd.Index[Any]) -> bool:
490
490
  # pd.IntervalIndex,
491
491
  # Period type isn't editable currently:
492
492
  # pd.PeriodIndex,
493
- ]
493
+ }
494
494
  # We need to check these index types without importing, since they are
495
495
  # deprecated and planned to be removed soon.
496
496
  or is_type(df_index, "pandas.core.indexes.numeric.Int64Index")
@@ -1029,7 +1029,7 @@ class DataEditorMixin:
1029
1029
  update_column_config(
1030
1030
  column_config_mapping, INDEX_IDENTIFIER, {"required": True}
1031
1031
  )
1032
- if num_rows in ("dynamic", "add") and hide_index is True:
1032
+ if num_rows in {"dynamic", "add"} and hide_index is True:
1033
1033
  _LOGGER.warning(
1034
1034
  "Setting `hide_index=True` in data editor with a non-range index will not have any effect "
1035
1035
  "when `num_rows` is '%s'. It is required for the user to fill in index values for "
@@ -1038,7 +1038,7 @@ class DataEditorMixin:
1038
1038
  num_rows,
1039
1039
  )
1040
1040
 
1041
- if hide_index is None and has_range_index and num_rows in ("dynamic", "add"):
1041
+ if hide_index is None and has_range_index and num_rows in {"dynamic", "add"}:
1042
1042
  # Temporary workaround:
1043
1043
  # We hide range indices if num_rows allows adding rows.
1044
1044
  # since the current way of handling this index during editing is a
@@ -82,6 +82,7 @@ class MultiSelectSerde(Generic[T]):
82
82
  formatted_options: list[str]
83
83
  formatted_option_to_option_index: dict[str, int]
84
84
  default_options_indices: list[int]
85
+ format_func: Callable[[Any], str]
85
86
 
86
87
  def __init__(
87
88
  self,
@@ -90,6 +91,7 @@ class MultiSelectSerde(Generic[T]):
90
91
  formatted_options: list[str],
91
92
  formatted_option_to_option_index: dict[str, int],
92
93
  default_options_indices: list[int] | None = None,
94
+ format_func: Callable[[Any], str] = str,
93
95
  ) -> None:
94
96
  """Initialize the MultiSelectSerde.
95
97
 
@@ -111,24 +113,45 @@ class MultiSelectSerde(Generic[T]):
111
113
  default_option_index : int or None, optional
112
114
  The index of the default option to use when no selection is made.
113
115
  If None, no default option is selected.
116
+ format_func : Callable[[Any], str], optional
117
+ Function to format options for comparison. Used to compare values by their
118
+ string representation instead of using == directly. This is necessary because
119
+ widget values are deepcopied, and for custom classes without __eq__, the
120
+ deepcopied instances would fail identity comparison.
114
121
  """
115
122
 
116
123
  self.options = options
117
124
  self.formatted_options = formatted_options
118
125
  self.formatted_option_to_option_index = formatted_option_to_option_index
119
126
  self.default_options_indices = default_options_indices or []
127
+ self.format_func = format_func
120
128
 
121
129
  def serialize(self, value: list[T | str] | list[T]) -> list[str]:
122
130
  converted_value = convert_anything_to_list(value)
123
131
  values: list[str] = []
124
132
  for v in converted_value:
133
+ # Use format_func to find the formatted option instead of using
134
+ # self.options.index(v) which relies on == comparison. This is necessary
135
+ # because widget values are deepcopied, and for custom classes without
136
+ # __eq__, the deepcopied instances would fail identity comparison.
125
137
  try:
126
- option_index = self.options.index(v)
127
- values.append(self.formatted_options[option_index])
128
- except ValueError: # noqa: PERF203
129
- # at this point we know that v is a string, otherwise
130
- # it would have been found in the options
131
- values.append(cast("str", v))
138
+ formatted_value = self.format_func(v)
139
+ except Exception:
140
+ # format_func failed (e.g., v is a string but format_func expects
141
+ # an object with specific attributes). Use str(v) to ensure we append
142
+ # a proper string, not the original object. This handles both cases:
143
+ # - v is already a string -> str(v) returns it unchanged
144
+ # - v is a custom object -> str(v) gives its string representation
145
+ values.append(str(v))
146
+ continue
147
+
148
+ if formatted_value in self.formatted_option_to_option_index:
149
+ values.append(formatted_value)
150
+ else:
151
+ # Value not found in options - it's likely a user-entered string
152
+ # (when accept_new_options=True) or an invalid value. Use the
153
+ # formatted string (not the original object) for type consistency.
154
+ values.append(formatted_value)
132
155
  return values
133
156
 
134
157
  def deserialize(self, ui_value: list[str] | None) -> list[T | str] | list[T]:
@@ -247,7 +270,7 @@ class MultiSelectMixin:
247
270
  placeholder: str | None = None,
248
271
  disabled: bool = False,
249
272
  label_visibility: LabelVisibility = "visible",
250
- accept_new_options: Literal[False, True] | bool = False,
273
+ accept_new_options: bool = False,
251
274
  width: WidthWithoutContent = "stretch",
252
275
  ) -> list[T] | list[T | str]:
253
276
  r"""Display a multiselect widget.
@@ -530,6 +553,7 @@ class MultiSelectMixin:
530
553
  formatted_options=formatted_options,
531
554
  formatted_option_to_option_index=formatted_option_to_option_index,
532
555
  default_options_indices=default_values,
556
+ format_func=format_func,
533
557
  )
534
558
 
535
559
  widget_state = register_widget(
@@ -560,7 +584,7 @@ class MultiSelectMixin:
560
584
  # previously selected values are no longer available.
561
585
  current_values, value_needs_reset = (
562
586
  validate_and_sync_multiselect_value_with_options(
563
- widget_state.value, indexable_options, key
587
+ widget_state.value, indexable_options, key, format_func
564
588
  )
565
589
  )
566
590
 
@@ -512,7 +512,7 @@ class NumberInputMixin:
512
512
  number_format = ("%d" if int_value else "%0.2f") if format is None else format
513
513
 
514
514
  # Warn user if they format an int type as a float or vice versa.
515
- if number_format in ["%d", "%u", "%i"] and float_value:
515
+ if number_format in {"%d", "%u", "%i"} and float_value:
516
516
  import streamlit as st
517
517
 
518
518
  st.warning(