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.
- streamlit/__init__.py +1 -31
- streamlit/auth_util.py +91 -2
- streamlit/cli_util.py +3 -2
- streamlit/commands/echo.py +2 -2
- streamlit/commands/execution_control.py +1 -1
- streamlit/commands/logo.py +76 -24
- streamlit/commands/navigation.py +1 -1
- streamlit/components/types/base_custom_component.py +0 -2
- streamlit/components/v1/custom_component.py +0 -2
- streamlit/components/v2/bidi_component/main.py +2 -2
- streamlit/components/v2/component_path_utils.py +17 -29
- streamlit/components/v2/manifest_scanner.py +8 -3
- streamlit/components/v2/presentation.py +1 -1
- streamlit/config.py +57 -13
- streamlit/config_util.py +5 -5
- streamlit/connections/snowflake_connection.py +6 -3
- streamlit/dataframe_util.py +10 -10
- streamlit/deprecation_util.py +19 -1
- streamlit/elements/arrow.py +18 -8
- streamlit/elements/deck_gl_json_chart.py +6 -2
- streamlit/elements/exception.py +4 -2
- streamlit/elements/form.py +1 -1
- streamlit/elements/layouts.py +1 -1
- streamlit/elements/lib/built_in_chart_utils.py +36 -13
- streamlit/elements/lib/color_util.py +21 -2
- streamlit/elements/lib/column_config_utils.py +9 -7
- streamlit/elements/lib/dialog.py +1 -1
- streamlit/elements/lib/image_utils.py +5 -5
- streamlit/elements/lib/layout_utils.py +1 -1
- streamlit/elements/lib/options_selector_utils.py +112 -18
- streamlit/elements/lib/policies.py +1 -1
- streamlit/elements/lib/streamlit_plotly_theme.py +9 -11
- streamlit/elements/lib/utils.py +1 -1
- streamlit/elements/map.py +6 -6
- streamlit/elements/plotly_chart.py +2 -2
- streamlit/elements/toast.py +1 -1
- streamlit/elements/vega_charts.py +30 -7
- streamlit/elements/widgets/button.py +3 -3
- streamlit/elements/widgets/button_group.py +3 -3
- streamlit/elements/widgets/chat.py +1 -1
- streamlit/elements/widgets/data_editor.py +6 -6
- streamlit/elements/widgets/multiselect.py +32 -8
- streamlit/elements/widgets/number_input.py +1 -1
- streamlit/elements/widgets/radio.py +91 -31
- streamlit/elements/widgets/select_slider.py +123 -37
- streamlit/elements/widgets/selectbox.py +38 -16
- streamlit/elements/widgets/slider.py +5 -5
- streamlit/elements/widgets/time_widgets.py +150 -18
- streamlit/elements/write.py +2 -3
- streamlit/env_util.py +1 -1
- streamlit/errors.py +2 -14
- streamlit/external/langchain/streamlit_callback_handler.py +1 -1
- streamlit/hello/dataframe_demo.py +1 -1
- streamlit/hello/plotting_demo.py +19 -12
- streamlit/path_security.py +98 -0
- streamlit/proto/Alert_pb2.py +2 -3
- streamlit/proto/AppPage_pb2.py +2 -3
- streamlit/proto/ArrowData_pb2.py +2 -3
- streamlit/proto/ArrowNamedDataSet_pb2.py +2 -3
- streamlit/proto/ArrowVegaLiteChart_pb2.py +2 -3
- streamlit/proto/Arrow_pb2.py +2 -3
- streamlit/proto/AudioInput_pb2.py +2 -3
- streamlit/proto/Audio_pb2.py +2 -3
- streamlit/proto/AuthRedirect_pb2.py +2 -3
- streamlit/proto/AutoRerun_pb2.py +2 -3
- streamlit/proto/BackMsg_pb2.py +2 -3
- streamlit/proto/Balloons_pb2.py +2 -3
- streamlit/proto/BidiComponent_pb2.py +2 -3
- streamlit/proto/Block_pb2.py +2 -3
- streamlit/proto/BokehChart_pb2.py +2 -3
- streamlit/proto/ButtonGroup_pb2.py +2 -3
- streamlit/proto/ButtonLikeIconPosition_pb2.py +2 -3
- streamlit/proto/Button_pb2.py +2 -3
- streamlit/proto/CameraInput_pb2.py +2 -3
- streamlit/proto/ChatInput_pb2.py +2 -3
- streamlit/proto/Checkbox_pb2.py +2 -3
- streamlit/proto/ClientState_pb2.py +2 -3
- streamlit/proto/Code_pb2.py +2 -3
- streamlit/proto/ColorPicker_pb2.py +2 -3
- streamlit/proto/Common_pb2.py +2 -3
- streamlit/proto/Components_pb2.py +2 -3
- streamlit/proto/DataFrame_pb2.py +2 -3
- streamlit/proto/DateInput_pb2.py +2 -3
- streamlit/proto/DateTimeInput_pb2.py +2 -3
- streamlit/proto/DeckGlJsonChart_pb2.py +2 -3
- streamlit/proto/Delta_pb2.py +2 -3
- streamlit/proto/DocString_pb2.py +2 -3
- streamlit/proto/DownloadButton_pb2.py +2 -3
- streamlit/proto/Element_pb2.py +2 -3
- streamlit/proto/Empty_pb2.py +2 -3
- streamlit/proto/Exception_pb2.py +2 -3
- streamlit/proto/Favicon_pb2.py +2 -3
- streamlit/proto/FileUploader_pb2.py +2 -3
- streamlit/proto/ForwardMsg_pb2.py +2 -3
- streamlit/proto/GapSize_pb2.py +2 -3
- streamlit/proto/GitInfo_pb2.py +2 -3
- streamlit/proto/GraphVizChart_pb2.py +2 -3
- streamlit/proto/Heading_pb2.py +2 -3
- streamlit/proto/HeightConfig_pb2.py +2 -3
- streamlit/proto/Html_pb2.py +2 -3
- streamlit/proto/IFrame_pb2.py +2 -3
- streamlit/proto/Image_pb2.py +2 -3
- streamlit/proto/Json_pb2.py +2 -3
- streamlit/proto/LabelVisibilityMessage_pb2.py +2 -3
- streamlit/proto/LinkButton_pb2.py +2 -3
- streamlit/proto/Logo_pb2.py +6 -5
- streamlit/proto/Logo_pb2.pyi +25 -1
- streamlit/proto/Markdown_pb2.py +2 -3
- streamlit/proto/Metric_pb2.py +2 -3
- streamlit/proto/MetricsEvent_pb2.py +2 -3
- streamlit/proto/MultiSelect_pb2.py +2 -3
- streamlit/proto/NamedDataSet_pb2.py +2 -3
- streamlit/proto/Navigation_pb2.py +2 -3
- streamlit/proto/NewSession_pb2.py +25 -24
- streamlit/proto/NewSession_pb2.pyi +28 -2
- streamlit/proto/NumberInput_pb2.py +2 -3
- streamlit/proto/PageConfig_pb2.py +2 -3
- streamlit/proto/PageInfo_pb2.py +2 -3
- streamlit/proto/PageLink_pb2.py +2 -3
- streamlit/proto/PageNotFound_pb2.py +2 -3
- streamlit/proto/PageProfile_pb2.py +2 -3
- streamlit/proto/PagesChanged_pb2.py +2 -3
- streamlit/proto/ParentMessage_pb2.py +2 -3
- streamlit/proto/PlotlyChart_pb2.py +2 -3
- streamlit/proto/Progress_pb2.py +2 -3
- streamlit/proto/Radio_pb2.py +5 -4
- streamlit/proto/Radio_pb2.pyi +20 -3
- streamlit/proto/RootContainer_pb2.py +2 -3
- streamlit/proto/Selectbox_pb2.py +2 -3
- streamlit/proto/SessionEvent_pb2.py +2 -3
- streamlit/proto/SessionStatus_pb2.py +2 -3
- streamlit/proto/Skeleton_pb2.py +2 -3
- streamlit/proto/Slider_pb2.py +7 -8
- streamlit/proto/Slider_pb2.pyi +9 -1
- streamlit/proto/Snow_pb2.py +2 -3
- streamlit/proto/Space_pb2.py +2 -3
- streamlit/proto/Spinner_pb2.py +2 -3
- streamlit/proto/TextAlignmentConfig_pb2.py +2 -3
- streamlit/proto/TextArea_pb2.py +2 -3
- streamlit/proto/TextInput_pb2.py +2 -3
- streamlit/proto/Text_pb2.py +2 -3
- streamlit/proto/TimeInput_pb2.py +2 -3
- streamlit/proto/Toast_pb2.py +2 -3
- streamlit/proto/Transient_pb2.py +2 -3
- streamlit/proto/VegaLiteChart_pb2.py +2 -3
- streamlit/proto/Video_pb2.py +2 -3
- streamlit/proto/WidgetStates_pb2.py +2 -3
- streamlit/proto/WidthConfig_pb2.py +2 -3
- streamlit/proto/openmetrics_data_model_pb2.py +2 -3
- streamlit/runtime/app_session.py +106 -60
- streamlit/runtime/caching/cache_data_api.py +3 -3
- streamlit/runtime/caching/cache_errors.py +0 -2
- streamlit/runtime/caching/cache_resource_api.py +1 -1
- streamlit/runtime/caching/cache_utils.py +2 -2
- streamlit/runtime/caching/hashing.py +1 -3
- streamlit/runtime/caching/storage/cache_storage_protocol.py +0 -3
- streamlit/runtime/connection_factory.py +1 -1
- streamlit/runtime/credentials.py +2 -2
- streamlit/runtime/metrics_util.py +3 -3
- streamlit/runtime/runtime.py +6 -6
- streamlit/runtime/scriptrunner/script_runner.py +17 -0
- streamlit/runtime/scriptrunner_utils/exceptions.py +0 -4
- streamlit/runtime/scriptrunner_utils/script_run_context.py +13 -31
- streamlit/runtime/secrets.py +3 -4
- streamlit/runtime/state/__init__.py +7 -1
- streamlit/runtime/state/common.py +13 -0
- streamlit/runtime/state/query_params.py +493 -24
- streamlit/runtime/state/session_state.py +179 -4
- streamlit/runtime/state/widgets.py +26 -1
- streamlit/runtime/stats.py +1 -10
- streamlit/static/index.html +1 -1
- streamlit/static/manifest.json +304 -304
- streamlit/static/static/js/{ErrorOutline.esm.Cxoit62D.js → ErrorOutline.esm.BWk6F-Tz.js} +1 -1
- streamlit/static/static/js/{FileDownload.esm.Cym2KVOR.js → FileDownload.esm.AllYUuOW.js} +1 -1
- streamlit/static/static/js/{FileHelper.C47VLeXF.js → FileHelper.BvVTNdmy.js} +1 -1
- streamlit/static/static/js/{FormClearHelper.CUrwwEeX.js → FormClearHelper.C__r5Llk.js} +1 -1
- streamlit/static/static/js/{InputInstructions.DyVOE42q.js → InputInstructions.DOtkdOMV.js} +1 -1
- streamlit/static/static/js/Particles.DCsqQZlE.js +1 -0
- streamlit/static/static/js/{ProgressBar.qKdiDYyx.js → ProgressBar.DLCRvt4m.js} +2 -2
- streamlit/static/static/js/{StreamlitSyntaxHighlighter.DUPp9dS3.js → StreamlitSyntaxHighlighter.CYFWoZHb.js} +1 -1
- streamlit/static/static/js/{TableChart.esm.C_g2CvCE.js → TableChart.esm.D6ydHcIm.js} +1 -1
- streamlit/static/static/js/Toolbar.BHDNzWBx.js +1 -0
- streamlit/static/static/js/{WidgetLabelHelpIconInline.Dy4yV6I2.js → WidgetLabelHelpIconInline.DEXBrVlc.js} +1 -1
- streamlit/static/static/js/{base-input.DQAb60v0.js → base-input.TSQjctlq.js} +4 -4
- streamlit/static/static/js/{checkbox.C0HE0ojW.js → checkbox.BKgfzJZV.js} +1 -1
- streamlit/static/static/js/{createDownloadLinkElement.DBMfH8_e.js → createDownloadLinkElement.CG7nr2a4.js} +1 -1
- streamlit/static/static/js/{data-grid-overlay-editor.CSZWem5Q.js → data-grid-overlay-editor.ChXO__lP.js} +1 -1
- streamlit/static/static/js/{downloader.Bp8c7mYD.js → downloader.DJ3R_zWA.js} +1 -1
- streamlit/static/static/js/embed.u3PPfLkw.js +193 -0
- streamlit/static/static/js/{es6.j7akTCaI.js → es6.C5Mfy8nd.js} +2 -2
- streamlit/static/static/js/{formatNumber.CfuUiEpF.js → formatNumber.CMRgW9EJ.js} +1 -1
- streamlit/static/static/js/{iconPosition.BVSTKfGd.js → iconPosition.B4EEXI3E.js} +1 -1
- streamlit/static/static/js/{iframeResizer.contentWindow.BZ3lugzo.js → iframeResizer.contentWindow.WSvOiTW0.js} +1 -1
- streamlit/static/static/js/index.-FOBV3nz.js +1 -0
- streamlit/static/static/js/{index.D0tXFTaW.js → index.-NF8OSF5.js} +1 -1
- streamlit/static/static/js/{index.Dk0CU4R6.js → index.4cBg8kn5.js} +1 -1
- streamlit/static/static/js/{index.DtZTtufl.js → index.B0pzzCsH.js} +1 -1
- streamlit/static/static/js/{index.DSaE74nc.js → index.BID6ND5j.js} +2 -2
- streamlit/static/static/js/index.BMp5bGjh.js +1 -0
- streamlit/static/static/js/{index.CAMxgVFm.js → index.BQcmlvas.js} +1 -1
- streamlit/static/static/js/{index.C0F0G-wg.js → index.BRcmclgI.js} +1 -1
- streamlit/static/static/js/index.BaUZR4IG.js +1 -0
- streamlit/static/static/js/{index.Cow0Hs9V.js → index.BbMJj4PN.js} +1 -1
- streamlit/static/static/js/{index.iboGgrkh.js → index.BdCTJtq3.js} +2 -2
- streamlit/static/static/js/index.BdETLMuI.js +1 -0
- streamlit/static/static/js/index.BnKMWhs1.js +1 -0
- streamlit/static/static/js/index.Br1kXwQW.js +2 -0
- streamlit/static/static/js/{index.B2fTHpId.js → index.Bt2olRE4.js} +1 -1
- streamlit/static/static/js/{index.DBIRzFM7.js → index.Bxwsv5T8.js} +1 -1
- streamlit/static/static/js/index.C4KskYz6.js +1 -0
- streamlit/static/static/js/{index.BgCYNmov.js → index.C6bmbXk0.js} +1 -1
- streamlit/static/static/js/{index.7S_sCSRx.js → index.CEfKfbta.js} +1 -1
- streamlit/static/static/js/index.CIuaA8q0.js +2 -0
- streamlit/static/static/js/{index.CWAvu1Qu.js → index.CV1sObFX.js} +1 -1
- streamlit/static/static/js/{index.C9QftD-S.js → index.CbR6dgaV.js} +1 -1
- streamlit/static/static/js/index.Cq6szKqJ.js +1 -0
- streamlit/static/static/js/index.CyouXqCz.js +1 -0
- streamlit/static/static/js/{index.BMFt07G_.js → index.D1NUgMFI.js} +1 -1
- streamlit/static/static/js/{index.Tq2okoAU.js → index.D7SWG4Om.js} +1 -1
- streamlit/static/static/js/{index.DgJeIFb5.js → index.DAYPEwLI.js} +1 -1
- streamlit/static/static/js/index.DKS75Vfg.js +11 -0
- streamlit/static/static/js/{index.FfR9SXQv.js → index.DOXrMIxB.js} +1 -1
- streamlit/static/static/js/{index.BiVJWMS-.js → index.DOzYX8yS.js} +3 -3
- streamlit/static/static/js/{index.nEa8y_He.js → index.DRFMYcC4.js} +4 -4
- streamlit/static/static/js/{index.dgs1TGpP.js → index.Divl5FCY.js} +1 -1
- streamlit/static/static/js/{index.95DldRtG.js → index.DjAJ_CUa.js} +1 -1
- streamlit/static/static/js/{index.Z0mB4zBp.js → index.Dncue2pm.js} +33 -33
- streamlit/static/static/js/{index.DFT9nVK6.js → index.Drusyo5m.js} +48 -48
- streamlit/static/static/js/{index.1PD6f3vh.js → index.DuUyDGnP.js} +1 -1
- streamlit/static/static/js/{index.DpU0Bc2F.js → index.DvgT2rB2.js} +223 -223
- streamlit/static/static/js/{index.Bukztsaz.js → index.DzutABu5.js} +2 -2
- streamlit/static/static/js/index.Dzw2iPzi.js +3 -0
- streamlit/static/static/js/{index.DYkkO_of.js → index.FsTmxLbT.js} +1 -1
- streamlit/static/static/js/{index.CTQ8QcOV.js → index.OIwPqGYN.js} +1 -1
- streamlit/static/static/js/{index.NtSfVVJe.js → index.RXLN7YFT.js} +2 -2
- streamlit/static/static/js/{index.BU3d_gp1.js → index.YYb2u0jk.js} +2 -2
- streamlit/static/static/js/{index.BXfSsjdq.js → index.h8ejt-W3.js} +1 -1
- streamlit/static/static/js/{index.gPUFpUqs.js → index.lFMCi9am.js} +1 -1
- streamlit/static/static/js/{index.BDA5l7b9.js → index.pOgf4cEj.js} +1 -1
- streamlit/static/static/js/index.s_E0s7LB.js +188 -0
- streamlit/static/static/js/{index.DysJZEAt.js → index.xLCbzoqj.js} +1 -1
- streamlit/static/static/js/{input.Pz8Lwzsi.js → input.BLG7kWaj.js} +2 -2
- streamlit/static/static/js/{main.BeiYkHRo.js → main.D_CmqChN.js} +1 -1
- streamlit/static/static/js/{memory.Dyx_JBbb.js → memory.T8u9KqIQ.js} +1 -1
- streamlit/static/static/js/{number-overlay-editor.NLIdF6b9.js → number-overlay-editor.BKBSXkAM.js} +2 -2
- streamlit/static/static/js/{pandasStylerUtils.DsNlDEqS.js → pandasStylerUtils.B4tLYMwS.js} +1 -1
- streamlit/static/static/js/{sandbox.bER7qtR1.js → sandbox.jRlkcPem.js} +1 -1
- streamlit/static/static/js/{styled-components.DcoFBb7G.js → styled-components.D2QhNwzd.js} +1 -1
- streamlit/static/static/js/{throttle.DOaQWO4U.js → throttle.Cyw_V0Dq.js} +1 -1
- streamlit/static/static/js/{timepicker.RjHB2IT4.js → timepicker.PzyuDDWl.js} +1 -1
- streamlit/static/static/js/{toConsumableArray.DFAIugL0.js → toConsumableArray.gE9fMkLj.js} +1 -1
- streamlit/static/static/js/uniqueId.B1GeHnT1.js +1 -0
- streamlit/static/static/js/{useBasicWidgetState.CTtyymrp.js → useBasicWidgetState.DFklfao0.js} +1 -1
- streamlit/static/static/js/{useIntlLocale.DG5haQGX.js → useIntlLocale.C3tUGWTU.js} +8 -8
- streamlit/static/static/js/{useTextInputAutoExpand.Cnfcep1Z.js → useTextInputAutoExpand.D9nU_y-e.js} +1 -1
- streamlit/static/static/js/useUpdateUiValue.ClTdrkJN.js +1 -0
- streamlit/static/static/js/{useWaveformController.DozaayUB.js → useWaveformController.lzTbjMW2.js} +1 -1
- streamlit/static/static/js/{withCalculatedWidth.SNNFFxhJ.js → withCalculatedWidth.Dxs9I5Oe.js} +1 -1
- streamlit/static/static/js/{withFullScreenWrapper.Dl2f8_gt.js → withFullScreenWrapper.DfpAcJxf.js} +1 -1
- streamlit/string_util.py +2 -2
- streamlit/testing/v1/app_test.py +1 -1
- streamlit/testing/v1/element_tree.py +33 -20
- streamlit/type_util.py +2 -2
- streamlit/url_util.py +2 -2
- streamlit/user_info.py +2 -41
- streamlit/util.py +1 -1
- streamlit/watcher/event_based_path_watcher.py +37 -7
- streamlit/watcher/path_watcher.py +61 -2
- streamlit/watcher/util.py +26 -10
- streamlit/web/bootstrap.py +16 -4
- streamlit/web/cli.py +1 -4
- streamlit/web/server/app_discovery.py +2 -1
- streamlit/web/server/app_static_file_handler.py +9 -0
- streamlit/web/server/bidi_component_request_handler.py +4 -4
- streamlit/web/server/component_file_utils.py +14 -6
- streamlit/web/server/component_request_handler.py +2 -2
- streamlit/web/server/oauth_authlib_routes.py +14 -42
- streamlit/web/server/server.py +1 -1
- streamlit/web/server/server_util.py +23 -1
- streamlit/web/server/starlette/starlette_app.py +7 -1
- streamlit/web/server/starlette/starlette_auth_routes.py +94 -16
- streamlit/web/server/starlette/starlette_path_security_middleware.py +97 -0
- streamlit/web/server/starlette/starlette_routes.py +16 -9
- streamlit/web/server/starlette/starlette_server.py +2 -2
- streamlit/web/server/starlette/starlette_static_routes.py +14 -4
- streamlit/web/server/stats_request_handler.py +1 -3
- {streamlit-1.53.0.dist-info → streamlit-1.54.0.dist-info}/METADATA +10 -25
- {streamlit-1.53.0.dist-info → streamlit-1.54.0.dist-info}/RECORD +291 -291
- {streamlit-1.53.0.dist-info → streamlit-1.54.0.dist-info}/WHEEL +1 -1
- streamlit/commands/experimental_query_params.py +0 -169
- streamlit/static/static/js/Particles.D5ZUTvE6.js +0 -1
- streamlit/static/static/js/Toolbar.BbO8bxwz.js +0 -1
- streamlit/static/static/js/embed.DQBlGL9Q.js +0 -195
- streamlit/static/static/js/index.5CsPRetw.js +0 -1
- streamlit/static/static/js/index.BGgra9Bb.js +0 -188
- streamlit/static/static/js/index.BGzJYcHz.js +0 -1
- streamlit/static/static/js/index.BNpEDrb2.js +0 -1
- streamlit/static/static/js/index.Bk5wGJXh.js +0 -1
- streamlit/static/static/js/index.By8GIgDH.js +0 -1
- streamlit/static/static/js/index.C8VoW8Ph.js +0 -1
- streamlit/static/static/js/index.CZzy-Gct.js +0 -1
- streamlit/static/static/js/index.CeFdbzfR.js +0 -11
- streamlit/static/static/js/index.CkmNfvPD.js +0 -1
- streamlit/static/static/js/index.CsmTnJl4.js +0 -3
- streamlit/static/static/js/index.DZGCJu4I.js +0 -2
- streamlit/static/static/js/index.svncz-Ad.js +0 -2
- streamlit/static/static/js/uniqueId.DEvFPH9n.js +0 -1
- streamlit/static/static/js/useUpdateUiValue.BWnXwmrp.js +0 -1
- streamlit-1.53.0.data/scripts/streamlit.cmd +0 -16
- {streamlit-1.53.0.dist-info → streamlit-1.54.0.dist-info}/entry_points.txt +0 -0
- {streamlit-1.53.0.dist-info → streamlit-1.54.0.dist-info}/top_level.txt +0 -0
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
+
import math
|
|
17
18
|
from collections.abc import Iterable, Iterator, Mapping, MutableMapping
|
|
18
19
|
from dataclasses import dataclass, field
|
|
19
|
-
from typing import TYPE_CHECKING, Final, cast
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Final, cast
|
|
20
21
|
from urllib import parse
|
|
21
22
|
|
|
22
23
|
from streamlit.errors import StreamlitAPIException, StreamlitQueryParamDictValueError
|
|
@@ -37,18 +38,115 @@ EMBED_QUERY_PARAMS_KEYS: Final[list[str]] = [
|
|
|
37
38
|
EMBED_OPTIONS_QUERY_PARAM,
|
|
38
39
|
]
|
|
39
40
|
|
|
41
|
+
# Protected parameters that cannot be bound to widgets
|
|
42
|
+
PROTECTED_QUERY_PARAMS: Final[frozenset[str]] = frozenset(
|
|
43
|
+
[EMBED_QUERY_PARAM, EMBED_OPTIONS_QUERY_PARAM]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class WidgetBinding:
|
|
49
|
+
"""Represents a binding between a widget and a query parameter."""
|
|
50
|
+
|
|
51
|
+
widget_id: str
|
|
52
|
+
param_key: str
|
|
53
|
+
value_type: str # e.g., "bool_value", "string_value", etc.
|
|
54
|
+
script_hash: str # For MPA: identifies main vs page script
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def parse_url_param(value: str | list[str], value_type: str) -> Any:
|
|
58
|
+
"""Convert URL param to Python value based on WidgetState value type.
|
|
59
|
+
|
|
60
|
+
Parameters
|
|
61
|
+
----------
|
|
62
|
+
value : str | list[str]
|
|
63
|
+
The URL parameter value(s).
|
|
64
|
+
value_type : str
|
|
65
|
+
The WidgetState value type (e.g., "bool_value", "string_value").
|
|
66
|
+
|
|
67
|
+
Returns
|
|
68
|
+
-------
|
|
69
|
+
Any
|
|
70
|
+
The parsed Python value appropriate for the widget type.
|
|
71
|
+
|
|
72
|
+
Raises
|
|
73
|
+
------
|
|
74
|
+
ValueError
|
|
75
|
+
If the value cannot be parsed for the given type.
|
|
76
|
+
"""
|
|
77
|
+
# For single-value types, get the last value if it's a list
|
|
78
|
+
val = value[-1] if isinstance(value, list) else value
|
|
79
|
+
|
|
80
|
+
match value_type:
|
|
81
|
+
case "bool_value":
|
|
82
|
+
lower_val = val.lower()
|
|
83
|
+
if lower_val == "true":
|
|
84
|
+
return True
|
|
85
|
+
if lower_val == "false":
|
|
86
|
+
return False
|
|
87
|
+
raise ValueError(f"Invalid boolean value: {val}")
|
|
88
|
+
case "int_value":
|
|
89
|
+
# Try to parse as int, but return string if it fails.
|
|
90
|
+
# This intentionally differs from double_value (which raises on failure)
|
|
91
|
+
# because int_value is used for selection widgets where URLs may contain
|
|
92
|
+
# human-readable option strings (e.g., ?fruit=apple instead of ?fruit=0).
|
|
93
|
+
# The deserializer will match the string against widget options.
|
|
94
|
+
try:
|
|
95
|
+
return int(val)
|
|
96
|
+
except ValueError:
|
|
97
|
+
return val
|
|
98
|
+
case "double_value":
|
|
99
|
+
return float(val)
|
|
100
|
+
case "string_value":
|
|
101
|
+
return val
|
|
102
|
+
case "string_array_value":
|
|
103
|
+
# Repeated params: ?foo=a&foo=b -> ["a", "b"]
|
|
104
|
+
return list(value) if isinstance(value, list) else [value]
|
|
105
|
+
case "double_array_value":
|
|
106
|
+
# Repeated params: ?foo=1.5&foo=2.5 -> [1.5, 2.5]
|
|
107
|
+
# Also handles string values for select_slider option matching
|
|
108
|
+
parts = list(value) if isinstance(value, list) else [value]
|
|
109
|
+
result_double: list[float | str] = []
|
|
110
|
+
for part in parts:
|
|
111
|
+
try:
|
|
112
|
+
result_double.append(float(part))
|
|
113
|
+
except ValueError: # noqa: PERF203
|
|
114
|
+
result_double.append(part) # Keep as string for select_slider
|
|
115
|
+
return result_double
|
|
116
|
+
case "int_array_value":
|
|
117
|
+
# Repeated params: ?foo=1&foo=2 -> [1, 2]
|
|
118
|
+
# Also handles string values for option matching (pills, etc.)
|
|
119
|
+
parts = list(value) if isinstance(value, list) else [value]
|
|
120
|
+
result_int: list[int | str] = []
|
|
121
|
+
for part in parts:
|
|
122
|
+
try:
|
|
123
|
+
result_int.append(int(part))
|
|
124
|
+
except ValueError: # noqa: PERF203
|
|
125
|
+
result_int.append(part) # Keep as string
|
|
126
|
+
return result_int
|
|
127
|
+
case _:
|
|
128
|
+
# Unknown type, return as-is
|
|
129
|
+
return val
|
|
130
|
+
|
|
40
131
|
|
|
41
132
|
@dataclass
|
|
42
133
|
class QueryParams(MutableMapping[str, str]):
|
|
43
134
|
"""A lightweight wrapper of a dict that sends forwardMsgs when state changes.
|
|
44
135
|
It stores str keys with str and List[str] values.
|
|
136
|
+
|
|
137
|
+
Also manages widget bindings to query parameters for the bind="query-params" feature.
|
|
45
138
|
"""
|
|
46
139
|
|
|
47
140
|
_query_params: dict[str, list[str] | str] = field(default_factory=dict)
|
|
48
141
|
|
|
49
|
-
|
|
50
|
-
|
|
142
|
+
# Widget binding registries
|
|
143
|
+
_bindings_by_param: dict[str, WidgetBinding] = field(default_factory=dict)
|
|
144
|
+
_bindings_by_widget: dict[str, WidgetBinding] = field(default_factory=dict)
|
|
51
145
|
|
|
146
|
+
# Store initial query params from URL at page load for seeding session state
|
|
147
|
+
_initial_query_params: dict[str, list[str]] = field(default_factory=dict)
|
|
148
|
+
|
|
149
|
+
def __iter__(self) -> Iterator[str]:
|
|
52
150
|
return iter(
|
|
53
151
|
key
|
|
54
152
|
for key in self._query_params
|
|
@@ -60,7 +158,6 @@ class QueryParams(MutableMapping[str, str]):
|
|
|
60
158
|
Returns the last item in a list or an empty string if empty.
|
|
61
159
|
If the key is not present, raise KeyError.
|
|
62
160
|
"""
|
|
63
|
-
self._ensure_single_query_api_used()
|
|
64
161
|
if key.lower() in EMBED_QUERY_PARAMS_KEYS:
|
|
65
162
|
raise KeyError(missing_key_error_message(key))
|
|
66
163
|
|
|
@@ -77,7 +174,12 @@ class QueryParams(MutableMapping[str, str]):
|
|
|
77
174
|
raise KeyError(missing_key_error_message(key))
|
|
78
175
|
|
|
79
176
|
def __setitem__(self, key: str, value: str | Iterable[str]) -> None:
|
|
80
|
-
|
|
177
|
+
# Prevent direct manipulation of bound query params
|
|
178
|
+
if self.is_bound(key):
|
|
179
|
+
raise StreamlitAPIException(
|
|
180
|
+
f"Cannot directly set query parameter '{key}' - "
|
|
181
|
+
f"it is bound to a widget. Modify the widget value instead."
|
|
182
|
+
)
|
|
81
183
|
self._set_item_internal(key, value)
|
|
82
184
|
self._send_query_param_msg()
|
|
83
185
|
|
|
@@ -85,9 +187,14 @@ class QueryParams(MutableMapping[str, str]):
|
|
|
85
187
|
_set_item_in_dict(self._query_params, key, value)
|
|
86
188
|
|
|
87
189
|
def __delitem__(self, key: str) -> None:
|
|
88
|
-
self._ensure_single_query_api_used()
|
|
89
190
|
if key.lower() in EMBED_QUERY_PARAMS_KEYS:
|
|
90
191
|
raise KeyError(missing_key_error_message(key))
|
|
192
|
+
# Prevent direct deletion of bound query params
|
|
193
|
+
if self.is_bound(key):
|
|
194
|
+
raise StreamlitAPIException(
|
|
195
|
+
f"Cannot directly delete query parameter '{key}' - "
|
|
196
|
+
f"it is bound to a widget. Modify the widget value instead."
|
|
197
|
+
)
|
|
91
198
|
try:
|
|
92
199
|
del self._query_params[key]
|
|
93
200
|
self._send_query_param_msg()
|
|
@@ -103,27 +210,45 @@ class QueryParams(MutableMapping[str, str]):
|
|
|
103
210
|
) -> None:
|
|
104
211
|
# This overrides the `update` provided by MutableMapping
|
|
105
212
|
# to ensure only one one ForwardMsg is sent.
|
|
106
|
-
|
|
213
|
+
|
|
214
|
+
# Consume dict-like objects into a list upfront to avoid iterating twice
|
|
215
|
+
# (once for keys, once for values). This prevents potential issues if
|
|
216
|
+
# `other` is modified during iteration.
|
|
217
|
+
other_as_list: list[tuple[str, str | Iterable[str]]]
|
|
107
218
|
if hasattr(other, "keys") and hasattr(other, "__getitem__"):
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
219
|
+
other_dict = cast("SupportsKeysAndGetItem[str, str | Iterable[str]]", other)
|
|
220
|
+
keys = list(other_dict.keys())
|
|
221
|
+
other_as_list = [(k, other_dict[k]) for k in keys]
|
|
111
222
|
else:
|
|
112
|
-
|
|
113
|
-
|
|
223
|
+
# other is an iterable of tuples - consume into list
|
|
224
|
+
other_as_list = list(other)
|
|
225
|
+
|
|
226
|
+
# Collect all keys to check for bound params before making any changes
|
|
227
|
+
keys_to_update = [key for key, _ in other_as_list]
|
|
228
|
+
keys_to_update.extend(kwds.keys())
|
|
229
|
+
|
|
230
|
+
# Check for bound params
|
|
231
|
+
for key in keys_to_update:
|
|
232
|
+
if self.is_bound(key):
|
|
233
|
+
raise StreamlitAPIException(
|
|
234
|
+
f"Cannot directly set query parameter '{key}' - "
|
|
235
|
+
f"it is bound to a widget. Modify the widget value instead."
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Now apply the updates
|
|
239
|
+
for key, value in other_as_list:
|
|
240
|
+
self._set_item_internal(key, value)
|
|
114
241
|
for key, value in kwds.items():
|
|
115
242
|
self._set_item_internal(key, value)
|
|
116
243
|
self._send_query_param_msg()
|
|
117
244
|
|
|
118
245
|
def get_all(self, key: str) -> list[str]:
|
|
119
|
-
self._ensure_single_query_api_used()
|
|
120
246
|
if key not in self._query_params or key.lower() in EMBED_QUERY_PARAMS_KEYS:
|
|
121
247
|
return []
|
|
122
248
|
value = self._query_params[key]
|
|
123
249
|
return value if isinstance(value, list) else [value]
|
|
124
250
|
|
|
125
251
|
def __len__(self) -> int:
|
|
126
|
-
self._ensure_single_query_api_used()
|
|
127
252
|
return len(
|
|
128
253
|
{
|
|
129
254
|
key
|
|
@@ -133,14 +258,12 @@ class QueryParams(MutableMapping[str, str]):
|
|
|
133
258
|
)
|
|
134
259
|
|
|
135
260
|
def __str__(self) -> str:
|
|
136
|
-
self._ensure_single_query_api_used()
|
|
137
261
|
return str(self._query_params)
|
|
138
262
|
|
|
139
263
|
def _send_query_param_msg(self) -> None:
|
|
140
264
|
ctx = get_script_run_ctx()
|
|
141
265
|
if ctx is None:
|
|
142
266
|
return
|
|
143
|
-
self._ensure_single_query_api_used()
|
|
144
267
|
|
|
145
268
|
msg = ForwardMsg()
|
|
146
269
|
msg.page_info_changed.query_string = parse.urlencode(
|
|
@@ -150,12 +273,18 @@ class QueryParams(MutableMapping[str, str]):
|
|
|
150
273
|
ctx.enqueue(msg)
|
|
151
274
|
|
|
152
275
|
def clear(self) -> None:
|
|
153
|
-
|
|
276
|
+
# Check if any bound params exist
|
|
277
|
+
bound_params = [key for key in self._query_params if self.is_bound(key)]
|
|
278
|
+
if bound_params:
|
|
279
|
+
raise StreamlitAPIException(
|
|
280
|
+
f"Cannot clear query parameters - the following are bound to widgets: "
|
|
281
|
+
f"{', '.join(repr(k) for k in bound_params)}. "
|
|
282
|
+
f"Modify the widget values instead, or remove the bind parameter."
|
|
283
|
+
)
|
|
154
284
|
self.clear_with_no_forward_msg(preserve_embed=True)
|
|
155
285
|
self._send_query_param_msg()
|
|
156
286
|
|
|
157
287
|
def to_dict(self) -> dict[str, str]:
|
|
158
|
-
self._ensure_single_query_api_used()
|
|
159
288
|
# return the last query param if multiple values are set
|
|
160
289
|
return {
|
|
161
290
|
key: self[key]
|
|
@@ -168,7 +297,6 @@ class QueryParams(MutableMapping[str, str]):
|
|
|
168
297
|
_dict: Iterable[tuple[str, str | Iterable[str]]]
|
|
169
298
|
| SupportsKeysAndGetItem[str, str | Iterable[str]],
|
|
170
299
|
) -> None:
|
|
171
|
-
self._ensure_single_query_api_used()
|
|
172
300
|
old_value = self._query_params.copy()
|
|
173
301
|
self.clear_with_no_forward_msg(preserve_embed=True)
|
|
174
302
|
try:
|
|
@@ -188,11 +316,352 @@ class QueryParams(MutableMapping[str, str]):
|
|
|
188
316
|
if key.lower() in EMBED_QUERY_PARAMS_KEYS and preserve_embed
|
|
189
317
|
}
|
|
190
318
|
|
|
191
|
-
def
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
319
|
+
def bind_widget(
|
|
320
|
+
self,
|
|
321
|
+
param_key: str,
|
|
322
|
+
widget_id: str,
|
|
323
|
+
value_type: str,
|
|
324
|
+
script_hash: str,
|
|
325
|
+
) -> None:
|
|
326
|
+
"""Register a widget binding to a query parameter.
|
|
327
|
+
|
|
328
|
+
If another widget was previously bound to this param_key, its binding
|
|
329
|
+
is replaced. The old widget's entry in _bindings_by_widget is cleaned up
|
|
330
|
+
to prevent orphaned references.
|
|
331
|
+
|
|
332
|
+
Parameters
|
|
333
|
+
----------
|
|
334
|
+
param_key : str
|
|
335
|
+
The query parameter key (same as the widget's user key).
|
|
336
|
+
widget_id : str
|
|
337
|
+
The unique widget ID.
|
|
338
|
+
value_type : str
|
|
339
|
+
The WidgetState value type (e.g., "bool_value", "string_value").
|
|
340
|
+
script_hash : str
|
|
341
|
+
The script hash for MPA support.
|
|
342
|
+
|
|
343
|
+
Raises
|
|
344
|
+
------
|
|
345
|
+
StreamlitAPIException
|
|
346
|
+
If the parameter is protected (embed, embed_options).
|
|
347
|
+
"""
|
|
348
|
+
if param_key.lower() in PROTECTED_QUERY_PARAMS:
|
|
349
|
+
raise StreamlitAPIException(
|
|
350
|
+
f"Cannot bind to reserved query parameter '{param_key}'. "
|
|
351
|
+
f"'{EMBED_QUERY_PARAM}' and '{EMBED_OPTIONS_QUERY_PARAM}' are "
|
|
352
|
+
f"used internally for Streamlit's embed functionality."
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# Clean up old binding if a different widget was bound to this param
|
|
356
|
+
old_binding = self._bindings_by_param.get(param_key)
|
|
357
|
+
if old_binding and old_binding.widget_id != widget_id:
|
|
358
|
+
self._bindings_by_widget.pop(old_binding.widget_id, None)
|
|
359
|
+
|
|
360
|
+
binding = WidgetBinding(
|
|
361
|
+
widget_id=widget_id,
|
|
362
|
+
param_key=param_key,
|
|
363
|
+
value_type=value_type,
|
|
364
|
+
script_hash=script_hash,
|
|
365
|
+
)
|
|
366
|
+
self._bindings_by_param[param_key] = binding
|
|
367
|
+
self._bindings_by_widget[widget_id] = binding
|
|
368
|
+
|
|
369
|
+
def unbind_widget(self, widget_id: str) -> None:
|
|
370
|
+
"""Remove a widget binding.
|
|
371
|
+
|
|
372
|
+
Parameters
|
|
373
|
+
----------
|
|
374
|
+
widget_id : str
|
|
375
|
+
The unique widget ID.
|
|
376
|
+
"""
|
|
377
|
+
binding = self._bindings_by_widget.pop(widget_id, None)
|
|
378
|
+
if binding:
|
|
379
|
+
self._bindings_by_param.pop(binding.param_key, None)
|
|
380
|
+
|
|
381
|
+
def is_bound(self, param_key: str) -> bool:
|
|
382
|
+
"""Check if a query parameter is bound to a widget.
|
|
383
|
+
|
|
384
|
+
Note: This check is case-sensitive, meaning "Foo" and "foo" are treated
|
|
385
|
+
as different parameters. This is intentional because Python keys are
|
|
386
|
+
case-sensitive and users explicitly choose their parameter names via
|
|
387
|
+
the widget's `key` argument. This differs from embed parameter checks
|
|
388
|
+
which are case-insensitive for URL compatibility.
|
|
389
|
+
|
|
390
|
+
Parameters
|
|
391
|
+
----------
|
|
392
|
+
param_key : str
|
|
393
|
+
The query parameter key (case-sensitive).
|
|
394
|
+
|
|
395
|
+
Returns
|
|
396
|
+
-------
|
|
397
|
+
bool
|
|
398
|
+
True if the parameter is bound to a widget.
|
|
399
|
+
"""
|
|
400
|
+
return param_key in self._bindings_by_param
|
|
401
|
+
|
|
402
|
+
def get_binding_for_param(self, param_key: str) -> WidgetBinding | None:
|
|
403
|
+
"""Get the binding for a query parameter.
|
|
404
|
+
|
|
405
|
+
Parameters
|
|
406
|
+
----------
|
|
407
|
+
param_key : str
|
|
408
|
+
The query parameter key.
|
|
409
|
+
|
|
410
|
+
Returns
|
|
411
|
+
-------
|
|
412
|
+
WidgetBinding | None
|
|
413
|
+
The binding if found, None otherwise.
|
|
414
|
+
"""
|
|
415
|
+
return self._bindings_by_param.get(param_key)
|
|
416
|
+
|
|
417
|
+
def get_binding_for_widget(self, widget_id: str) -> WidgetBinding | None:
|
|
418
|
+
"""Get the binding for a widget.
|
|
419
|
+
|
|
420
|
+
Parameters
|
|
421
|
+
----------
|
|
422
|
+
widget_id : str
|
|
423
|
+
The unique widget ID.
|
|
424
|
+
|
|
425
|
+
Returns
|
|
426
|
+
-------
|
|
427
|
+
WidgetBinding | None
|
|
428
|
+
The binding if found, None otherwise.
|
|
429
|
+
"""
|
|
430
|
+
return self._bindings_by_widget.get(widget_id)
|
|
431
|
+
|
|
432
|
+
def remove_param(self, param_key: str) -> bool:
|
|
433
|
+
"""Remove a query parameter without protection checks.
|
|
434
|
+
|
|
435
|
+
This is an internal method for use by SessionState when clearing
|
|
436
|
+
invalid URL-seeded values. It bypasses the bound param protection
|
|
437
|
+
since the binding system itself needs to clear these values.
|
|
438
|
+
|
|
439
|
+
Parameters
|
|
440
|
+
----------
|
|
441
|
+
param_key : str
|
|
442
|
+
The query parameter key to remove.
|
|
443
|
+
|
|
444
|
+
Returns
|
|
445
|
+
-------
|
|
446
|
+
bool
|
|
447
|
+
True if the param was removed, False if it didn't exist.
|
|
448
|
+
"""
|
|
449
|
+
if param_key in self._query_params:
|
|
450
|
+
del self._query_params[param_key]
|
|
451
|
+
self._send_query_param_msg()
|
|
452
|
+
return True
|
|
453
|
+
return False
|
|
454
|
+
|
|
455
|
+
def set_initial_query_params(self, query_string: str) -> None:
|
|
456
|
+
"""Store the initial query params from the URL for session state seeding.
|
|
457
|
+
|
|
458
|
+
Parameters
|
|
459
|
+
----------
|
|
460
|
+
query_string : str
|
|
461
|
+
The URL query string (without the leading '?').
|
|
462
|
+
"""
|
|
463
|
+
parsed = parse.parse_qs(query_string, keep_blank_values=True)
|
|
464
|
+
self._initial_query_params = parsed
|
|
465
|
+
|
|
466
|
+
def set_initial_query_params_from_current(self) -> None:
|
|
467
|
+
"""Set _initial_query_params from the current filtered _query_params.
|
|
468
|
+
|
|
469
|
+
This is called after MPA page transitions where populate_from_query_string()
|
|
470
|
+
has filtered out params bound to widgets on other pages. Using this ensures
|
|
471
|
+
widget seeding only uses params that are valid for the current page, preventing
|
|
472
|
+
stale values from previous pages from leaking through.
|
|
473
|
+
"""
|
|
474
|
+
# Convert _query_params to the list format used by _initial_query_params
|
|
475
|
+
# (parse_qs returns dict[str, list[str]])
|
|
476
|
+
self._initial_query_params = {
|
|
477
|
+
k: v if isinstance(v, list) else [v] for k, v in self._query_params.items()
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
def get_initial_value(self, param_key: str) -> str | list[str] | None:
|
|
481
|
+
"""Get the initial URL value for a query parameter.
|
|
482
|
+
|
|
483
|
+
This is used for seeding session state on initial page load.
|
|
484
|
+
|
|
485
|
+
Parameters
|
|
486
|
+
----------
|
|
487
|
+
param_key : str
|
|
488
|
+
The query parameter key.
|
|
489
|
+
|
|
490
|
+
Returns
|
|
491
|
+
-------
|
|
492
|
+
str | list[str] | None
|
|
493
|
+
The initial value(s) if present, None otherwise.
|
|
494
|
+
"""
|
|
495
|
+
values = self._initial_query_params.get(param_key)
|
|
496
|
+
if values is None:
|
|
497
|
+
return None
|
|
498
|
+
if len(values) == 1:
|
|
499
|
+
return values[0]
|
|
500
|
+
return values
|
|
501
|
+
|
|
502
|
+
def _set_corrected_value(self, param_key: str, value: Any, value_type: str) -> None:
|
|
503
|
+
"""Set a corrected value for a query parameter.
|
|
504
|
+
|
|
505
|
+
This is called when URL auto-correction is needed (e.g., after clamping
|
|
506
|
+
a value to min/max bounds). It updates both the internal query params
|
|
507
|
+
and sends a forward message to update the frontend URL.
|
|
508
|
+
|
|
509
|
+
Parameters
|
|
510
|
+
----------
|
|
511
|
+
param_key : str
|
|
512
|
+
The query parameter key.
|
|
513
|
+
value : Any
|
|
514
|
+
The corrected value to set.
|
|
515
|
+
value_type : str
|
|
516
|
+
The WidgetState value type (e.g., "double_value", "int_value").
|
|
517
|
+
"""
|
|
518
|
+
|
|
519
|
+
def format_number(v: Any) -> str:
|
|
520
|
+
"""Format a number, using integer format if value is a whole number.
|
|
521
|
+
|
|
522
|
+
Examples: 5.0 -> "5", 5.5 -> "5.5", 5 -> "5"
|
|
523
|
+
Handles special float values (NaN, Inf) by returning them as-is.
|
|
524
|
+
"""
|
|
525
|
+
# math.isfinite returns False for NaN, inf, -inf
|
|
526
|
+
# which would raise ValueError/OverflowError when converting to int
|
|
527
|
+
if isinstance(v, float) and math.isfinite(v) and v == int(v):
|
|
528
|
+
return str(int(v))
|
|
529
|
+
return str(v)
|
|
530
|
+
|
|
531
|
+
# Convert the value to a string representation for the URL
|
|
532
|
+
# All array types use repeated params: ?foo=a&foo=b
|
|
533
|
+
if value_type in {
|
|
534
|
+
"string_array_value",
|
|
535
|
+
"int_array_value",
|
|
536
|
+
"double_array_value",
|
|
537
|
+
}:
|
|
538
|
+
if isinstance(value, (list, tuple)):
|
|
539
|
+
# Store as list for repeated params
|
|
540
|
+
self._query_params[param_key] = [
|
|
541
|
+
format_number(v) if value_type == "double_array_value" else str(v)
|
|
542
|
+
for v in value
|
|
543
|
+
]
|
|
544
|
+
self._send_query_param_msg()
|
|
545
|
+
return
|
|
546
|
+
str_value = (
|
|
547
|
+
format_number(value)
|
|
548
|
+
if value_type == "double_array_value"
|
|
549
|
+
else str(value)
|
|
550
|
+
)
|
|
551
|
+
else:
|
|
552
|
+
str_value = str(value)
|
|
553
|
+
|
|
554
|
+
self._query_params[param_key] = str_value
|
|
555
|
+
self._send_query_param_msg()
|
|
556
|
+
|
|
557
|
+
def populate_from_query_string(
|
|
558
|
+
self,
|
|
559
|
+
query_string: str,
|
|
560
|
+
valid_script_hashes: set[str] | None = None,
|
|
561
|
+
) -> None:
|
|
562
|
+
"""Populate query params from a URL query string.
|
|
563
|
+
|
|
564
|
+
Clears current params and repopulates from the URL. When valid_script_hashes
|
|
565
|
+
is provided (for MPA page transitions), filters out params bound to other pages.
|
|
566
|
+
|
|
567
|
+
Parameters
|
|
568
|
+
----------
|
|
569
|
+
query_string : str
|
|
570
|
+
The raw query string from the URL (e.g., "foo=bar&baz=qux").
|
|
571
|
+
valid_script_hashes : set[str] | None
|
|
572
|
+
If provided, only keep params that are:
|
|
573
|
+
- Unbound (no widget binding)
|
|
574
|
+
- Bound to a widget with script_hash in this set
|
|
575
|
+
Params bound to other pages are filtered out.
|
|
576
|
+
If None, all params are kept (no filtering).
|
|
577
|
+
"""
|
|
578
|
+
parsed_query_params = parse.parse_qs(query_string, keep_blank_values=True)
|
|
579
|
+
|
|
580
|
+
self.clear_with_no_forward_msg()
|
|
581
|
+
stale_widget_ids: list[str] = []
|
|
582
|
+
|
|
583
|
+
for key, val in parsed_query_params.items():
|
|
584
|
+
binding = self._bindings_by_param.get(key)
|
|
585
|
+
should_keep = True
|
|
586
|
+
|
|
587
|
+
# If filtering is enabled, check if this param should be filtered out
|
|
588
|
+
if (
|
|
589
|
+
valid_script_hashes is not None
|
|
590
|
+
and binding is not None
|
|
591
|
+
and binding.script_hash not in valid_script_hashes
|
|
592
|
+
):
|
|
593
|
+
# Binding from a different page - filter it out
|
|
594
|
+
stale_widget_ids.append(binding.widget_id)
|
|
595
|
+
should_keep = False
|
|
596
|
+
|
|
597
|
+
if should_keep:
|
|
598
|
+
if len(val) == 0:
|
|
599
|
+
self.set_with_no_forward_msg(key, val="")
|
|
600
|
+
elif len(val) == 1:
|
|
601
|
+
self.set_with_no_forward_msg(key, val=val[-1])
|
|
602
|
+
else:
|
|
603
|
+
self.set_with_no_forward_msg(key, val)
|
|
604
|
+
|
|
605
|
+
# Clean up bindings for widgets from other pages
|
|
606
|
+
for widget_id in stale_widget_ids:
|
|
607
|
+
self.unbind_widget(widget_id)
|
|
608
|
+
|
|
609
|
+
# Update frontend URL if we filtered out any params
|
|
610
|
+
if stale_widget_ids:
|
|
611
|
+
self._send_query_param_msg()
|
|
612
|
+
|
|
613
|
+
def remove_stale_bindings(
|
|
614
|
+
self,
|
|
615
|
+
active_widget_ids: set[str],
|
|
616
|
+
fragment_ids_this_run: list[str] | None = None,
|
|
617
|
+
widget_metadata: dict[str, Any] | None = None,
|
|
618
|
+
) -> None:
|
|
619
|
+
"""Remove bindings and URL params for widgets that are no longer active.
|
|
620
|
+
|
|
621
|
+
This cleans up query params for conditional widgets that have been unmounted.
|
|
622
|
+
For fragment runs, widgets outside the running fragment(s) are preserved.
|
|
623
|
+
|
|
624
|
+
Note: Page-based cleanup for MPA navigation is handled separately via
|
|
625
|
+
populate_from_query_string() which is called before the script runs.
|
|
626
|
+
|
|
627
|
+
Parameters
|
|
628
|
+
----------
|
|
629
|
+
active_widget_ids : set[str]
|
|
630
|
+
Set of widget IDs that are currently active/rendered.
|
|
631
|
+
fragment_ids_this_run : list[str] | None
|
|
632
|
+
List of fragment IDs being run, or None for full script runs.
|
|
633
|
+
widget_metadata : dict[str, Any] | None
|
|
634
|
+
Widget metadata dict to check fragment IDs.
|
|
635
|
+
"""
|
|
636
|
+
stale_widget_ids = []
|
|
637
|
+
for widget_id in self._bindings_by_widget:
|
|
638
|
+
if widget_id in active_widget_ids:
|
|
639
|
+
# Widget is active in this run - keep it
|
|
640
|
+
continue
|
|
641
|
+
|
|
642
|
+
# For fragment runs, preserve widgets that aren't part of the running fragments
|
|
643
|
+
if fragment_ids_this_run and widget_metadata:
|
|
644
|
+
metadata = widget_metadata.get(widget_id)
|
|
645
|
+
if metadata and metadata.fragment_id not in fragment_ids_this_run:
|
|
646
|
+
# Widget belongs to a different fragment or main script - keep it
|
|
647
|
+
continue
|
|
648
|
+
|
|
649
|
+
stale_widget_ids.append(widget_id)
|
|
650
|
+
|
|
651
|
+
params_removed = False
|
|
652
|
+
for widget_id in stale_widget_ids:
|
|
653
|
+
binding = self._bindings_by_widget.get(widget_id)
|
|
654
|
+
if binding:
|
|
655
|
+
param_key = binding.param_key
|
|
656
|
+
# Remove the query param from the URL
|
|
657
|
+
if param_key in self._query_params:
|
|
658
|
+
del self._query_params[param_key]
|
|
659
|
+
params_removed = True
|
|
660
|
+
self.unbind_widget(widget_id)
|
|
661
|
+
|
|
662
|
+
# Send forward message to update frontend URL if we removed any params
|
|
663
|
+
if params_removed:
|
|
664
|
+
self._send_query_param_msg()
|
|
196
665
|
|
|
197
666
|
|
|
198
667
|
def missing_key_error_message(key: str) -> str:
|