streamlit-nightly 1.53.2.dev20260125__py3-none-any.whl → 1.53.2.dev20260128__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/commands/logo.py +81 -25
- streamlit/config.py +11 -0
- streamlit/deprecation_util.py +19 -1
- streamlit/elements/arrow.py +2 -1
- streamlit/elements/lib/built_in_chart_utils.py +2 -2
- streamlit/elements/lib/options_selector_utils.py +72 -22
- streamlit/elements/widgets/select_slider.py +123 -37
- streamlit/hello/plotting_demo.py +19 -12
- streamlit/proto/Logo_pb2.py +5 -3
- streamlit/proto/Logo_pb2.pyi +25 -1
- streamlit/proto/NewSession_pb2.py +24 -22
- streamlit/proto/NewSession_pb2.pyi +23 -1
- streamlit/proto/Slider_pb2.py +6 -6
- streamlit/proto/Slider_pb2.pyi +9 -1
- streamlit/runtime/app_session.py +19 -0
- streamlit/runtime/scriptrunner/script_runner.py +17 -0
- streamlit/runtime/scriptrunner_utils/script_run_context.py +13 -10
- streamlit/runtime/state/__init__.py +7 -1
- streamlit/runtime/state/common.py +13 -0
- streamlit/runtime/state/query_params.py +494 -6
- streamlit/runtime/state/session_state.py +178 -3
- streamlit/runtime/state/widgets.py +26 -1
- streamlit/static/index.html +1 -1
- streamlit/static/manifest.json +299 -299
- streamlit/static/static/js/{ErrorOutline.esm.CIFYUdwC.js → ErrorOutline.esm.D71F8ziR.js} +1 -1
- streamlit/static/static/js/{FileDownload.esm.DWVTnTHm.js → FileDownload.esm.yTkppsJy.js} +1 -1
- streamlit/static/static/js/{FileHelper.BPYQIPd1.js → FileHelper.hUOqtbwa.js} +1 -1
- streamlit/static/static/js/{FormClearHelper.CypmvhYZ.js → FormClearHelper.DN8D_YXO.js} +1 -1
- streamlit/static/static/js/{InputInstructions.Bi62hDTQ.js → InputInstructions.DbssY6d4.js} +1 -1
- streamlit/static/static/js/{Particles.yebG0VuV.js → Particles.BznyVdfo.js} +1 -1
- streamlit/static/static/js/{ProgressBar.Dy9CI6w4.js → ProgressBar.C5uBOtcx.js} +1 -1
- streamlit/static/static/js/{StreamlitSyntaxHighlighter.Btk92CPv.js → StreamlitSyntaxHighlighter.Nf1895x-.js} +1 -1
- streamlit/static/static/js/{TableChart.esm.DBeVaFNt.js → TableChart.esm.DHKzVs3a.js} +1 -1
- streamlit/static/static/js/{Toolbar.DC2Tp-qb.js → Toolbar.CQsWYXer.js} +1 -1
- streamlit/static/static/js/{WidgetLabelHelpIconInline.3DnEd9BK.js → WidgetLabelHelpIconInline.6xCU76OE.js} +1 -1
- streamlit/static/static/js/{base-input.7Sj6pVk0.js → base-input.Cs-E6S71.js} +1 -1
- streamlit/static/static/js/{checkbox.CcUx3XuQ.js → checkbox.OTGupu18.js} +1 -1
- streamlit/static/static/js/{createDownloadLinkElement.DZuwkCqy.js → createDownloadLinkElement.DnBEQQbK.js} +1 -1
- streamlit/static/static/js/{data-grid-overlay-editor.Dw-AewlN.js → data-grid-overlay-editor.COiiMi5r.js} +1 -1
- streamlit/static/static/js/{downloader.Bsx5M2Du.js → downloader.K0GUNeuj.js} +1 -1
- streamlit/static/static/js/embed.o8HvK3mH.js +193 -0
- streamlit/static/static/js/{es6.BpAqZaR_.js → es6.BHy5pqTP.js} +2 -2
- streamlit/static/static/js/{formatNumber.DjehVPVS.js → formatNumber.BK7h0k2z.js} +1 -1
- streamlit/static/static/js/{iconPosition.D02OPE-d.js → iconPosition.2YynQUxu.js} +1 -1
- streamlit/static/static/js/{iframeResizer.contentWindow.xtstqPd7.js → iframeResizer.contentWindow.D5h3hQuU.js} +1 -1
- streamlit/static/static/js/{index.5H98WqjT.js → index.5zqfJ-in.js} +1 -1
- streamlit/static/static/js/{index.B5tD5YeV.js → index.6c-qDsD7.js} +1 -1
- streamlit/static/static/js/{index.CA0RmxJF.js → index.8MlRyIxN.js} +1 -1
- streamlit/static/static/js/{index.DSSapl3Q.js → index.BIqcOZ_u.js} +1 -1
- streamlit/static/static/js/{index.DJjSqPAx.js → index.BPdmXoYW.js} +1 -1
- streamlit/static/static/js/{index.iXzAofuY.js → index.BZ-GJVxB.js} +2 -2
- streamlit/static/static/js/{index.CKUBdVQ9.js → index.BfMPq234.js} +1 -1
- streamlit/static/static/js/{index.B8-HOwf1.js → index.Bfo1cXfC.js} +1 -1
- streamlit/static/static/js/{index.-faJDV20.js → index.Bgf49D1Z.js} +1 -1
- streamlit/static/static/js/{index.CgARjn28.js → index.Bqmx23jK.js} +1 -1
- streamlit/static/static/js/{index.D6Z9hKJY.js → index.BtRWcqZV.js} +1 -1
- streamlit/static/static/js/{index.ZIA43eTF.js → index.BtuskCwg.js} +1 -1
- streamlit/static/static/js/{index.BV6XgCij.js → index.BzTVI_BY.js} +1 -1
- streamlit/static/static/js/{index.DDu_qTm0.js → index.C2EoeVjP.js} +1 -1
- streamlit/static/static/js/{index.8FPw0_gD.js → index.C65jHNhe.js} +1 -1
- streamlit/static/static/js/{index.D6J2UPzF.js → index.C6wyTXhz.js} +1 -1
- streamlit/static/static/js/{index.CGbvkEtg.js → index.C7wst9Tm.js} +1 -1
- streamlit/static/static/js/{index.BIcJe97b.js → index.COh5V_89.js} +1 -1
- streamlit/static/static/js/index.CSPY26T2.js +1 -0
- streamlit/static/static/js/{index.CEwnDCn9.js → index.CUkhn-vu.js} +1 -1
- streamlit/static/static/js/{index.DO2T-QzF.js → index.CX0KdFyR.js} +1 -1
- streamlit/static/static/js/{index.BDlI2pRp.js → index.CYhhEdja.js} +1 -1
- streamlit/static/static/js/{index.DgLRJfs3.js → index.CZf7Go1Z.js} +1 -1
- streamlit/static/static/js/{index.DZv5AoR1.js → index.Cb03y5I8.js} +1 -1
- streamlit/static/static/js/{index.BVhVdVeE.js → index.CdsyTabv.js} +1 -1
- streamlit/static/static/js/{index.JL0uGAeJ.js → index.CgVv04GM.js} +1 -1
- streamlit/static/static/js/index.CjRU8O1O.js +2 -0
- streamlit/static/static/js/{index.BqfJJr3c.js → index.CwtpGPHA.js} +1 -1
- streamlit/static/static/js/{index.iF5zYERg.js → index.CxWzt6oi.js} +1 -1
- streamlit/static/static/js/{index.m3dn5Bai.js → index.DBPWUJsj.js} +5 -5
- streamlit/static/static/js/{index.D9RL5sRp.js → index.DJfMW0Gy.js} +1 -1
- streamlit/static/static/js/{index.BOkpEbJS.js → index.DLUSo6de.js} +1 -1
- streamlit/static/static/js/{index.S-mjkUeF.js → index.DL_yE83J.js} +1 -1
- streamlit/static/static/js/{index.BK9S5qug.js → index.DVRCyxMp.js} +1 -1
- streamlit/static/static/js/{index.D_TIyPF4.js → index.Dc5-tFdw.js} +1 -1
- streamlit/static/static/js/index.DcngUOyD.js +2 -0
- streamlit/static/static/js/{index.B9gbSNsw.js → index.Dh3PJIlq.js} +1 -1
- streamlit/static/static/js/{index.BvZbnSMC.js → index.DlgcEr0f.js} +1 -1
- streamlit/static/static/js/{index.Bo1ztye0.js → index.DxGXuhh6.js} +1 -1
- streamlit/static/static/js/{index.x1B588Xu.js → index.DxfYCrPp.js} +1 -1
- streamlit/static/static/js/{index.CyDHwK5P.js → index.HmRK3HyC.js} +1 -1
- streamlit/static/static/js/{index.DdxofXV8.js → index.TjMWsKSH.js} +3 -3
- streamlit/static/static/js/{index.Bri1T2TS.js → index.VwDKazgt.js} +1 -1
- streamlit/static/static/js/{index.BB_iwaVr.js → index.aCorc3Yt.js} +34 -34
- streamlit/static/static/js/{index.CdRwiHPm.js → index.cfuZ69LI.js} +1 -1
- streamlit/static/static/js/{index.C9v49R-a.js → index.hlAfdSqC.js} +1 -1
- streamlit/static/static/js/{index.BGTMh3Uu.js → index.iUV9rb8C.js} +1 -1
- streamlit/static/static/js/{index.XFMDBL5n.js → index.q0ceUXt6.js} +1 -1
- streamlit/static/static/js/{input.VYKyGuhi.js → input.CXGIJ7D6.js} +1 -1
- streamlit/static/static/js/{main.u5Bb3MY7.js → main.CCVkbuxC.js} +1 -1
- streamlit/static/static/js/{memory.BOMt4yAV.js → memory.CNbnYs2A.js} +1 -1
- streamlit/static/static/js/{number-overlay-editor.CihlAHgl.js → number-overlay-editor.CvI6wkld.js} +1 -1
- streamlit/static/static/js/{pandasStylerUtils.BuqSgXpk.js → pandasStylerUtils.CFSReOTm.js} +1 -1
- streamlit/static/static/js/{sandbox.COGR4pqz.js → sandbox.Bld0L3us.js} +1 -1
- streamlit/static/static/js/{styled-components.BEf3c4IJ.js → styled-components.BoUHK6TA.js} +1 -1
- streamlit/static/static/js/{throttle.Bl-XsA9N.js → throttle.ByDFm7WV.js} +1 -1
- streamlit/static/static/js/{timepicker.B-HgBYlK.js → timepicker.CN6CUZEL.js} +1 -1
- streamlit/static/static/js/{toConsumableArray.BrQebwtE.js → toConsumableArray.DwMycSpg.js} +1 -1
- streamlit/static/static/js/uniqueId.DcCWa2cf.js +1 -0
- streamlit/static/static/js/{useBasicWidgetState.8WwISl9r.js → useBasicWidgetState.Bg0ZMUt5.js} +1 -1
- streamlit/static/static/js/{useIntlLocale.D37LWdCR.js → useIntlLocale.DgBUDcPA.js} +1 -1
- streamlit/static/static/js/{useTextInputAutoExpand.Bb_KqJvq.js → useTextInputAutoExpand.DDBezxks.js} +1 -1
- streamlit/static/static/js/{useUpdateUiValue.D1BLS5t7.js → useUpdateUiValue.Df1h6fXC.js} +1 -1
- streamlit/static/static/js/{useWaveformController.Ce0-qTws.js → useWaveformController.DbWw5MEk.js} +1 -1
- streamlit/static/static/js/{withCalculatedWidth.BX2K3UVv.js → withCalculatedWidth.YaK0HIIP.js} +1 -1
- streamlit/static/static/js/{withFullScreenWrapper.CqfGs8T2.js → withFullScreenWrapper.CcWCKoY8.js} +1 -1
- streamlit/testing/v1/element_tree.py +23 -8
- streamlit/web/bootstrap.py +5 -2
- streamlit/web/server/server_util.py +22 -0
- {streamlit_nightly-1.53.2.dev20260125.dist-info → streamlit_nightly-1.53.2.dev20260128.dist-info}/METADATA +1 -1
- {streamlit_nightly-1.53.2.dev20260125.dist-info → streamlit_nightly-1.53.2.dev20260128.dist-info}/RECORD +120 -120
- streamlit/static/static/js/embed.C7by6AoE.js +0 -195
- streamlit/static/static/js/index.Bhy8EBYI.js +0 -2
- streamlit/static/static/js/index.C5ehUqNt.js +0 -2
- streamlit/static/static/js/index.m4WkwGMu.js +0 -1
- streamlit/static/static/js/uniqueId.8R4hbkYl.js +0 -1
- {streamlit_nightly-1.53.2.dev20260125.data → streamlit_nightly-1.53.2.dev20260128.data}/scripts/streamlit.cmd +0 -0
- {streamlit_nightly-1.53.2.dev20260125.dist-info → streamlit_nightly-1.53.2.dev20260128.dist-info}/WHEEL +0 -0
- {streamlit_nightly-1.53.2.dev20260125.dist-info → streamlit_nightly-1.53.2.dev20260128.dist-info}/entry_points.txt +0 -0
- {streamlit_nightly-1.53.2.dev20260125.dist-info → streamlit_nightly-1.53.2.dev20260128.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,15 +38,114 @@ 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
|
|
|
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)
|
|
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
|
+
|
|
49
149
|
def __iter__(self) -> Iterator[str]:
|
|
50
150
|
return iter(
|
|
51
151
|
key
|
|
@@ -74,6 +174,12 @@ class QueryParams(MutableMapping[str, str]):
|
|
|
74
174
|
raise KeyError(missing_key_error_message(key))
|
|
75
175
|
|
|
76
176
|
def __setitem__(self, key: str, value: str | Iterable[str]) -> None:
|
|
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
|
+
)
|
|
77
183
|
self._set_item_internal(key, value)
|
|
78
184
|
self._send_query_param_msg()
|
|
79
185
|
|
|
@@ -83,6 +189,12 @@ class QueryParams(MutableMapping[str, str]):
|
|
|
83
189
|
def __delitem__(self, key: str) -> None:
|
|
84
190
|
if key.lower() in EMBED_QUERY_PARAMS_KEYS:
|
|
85
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
|
+
)
|
|
86
198
|
try:
|
|
87
199
|
del self._query_params[key]
|
|
88
200
|
self._send_query_param_msg()
|
|
@@ -98,13 +210,34 @@ class QueryParams(MutableMapping[str, str]):
|
|
|
98
210
|
) -> None:
|
|
99
211
|
# This overrides the `update` provided by MutableMapping
|
|
100
212
|
# to ensure only one one ForwardMsg is sent.
|
|
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]]]
|
|
101
218
|
if hasattr(other, "keys") and hasattr(other, "__getitem__"):
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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]
|
|
105
222
|
else:
|
|
106
|
-
|
|
107
|
-
|
|
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)
|
|
108
241
|
for key, value in kwds.items():
|
|
109
242
|
self._set_item_internal(key, value)
|
|
110
243
|
self._send_query_param_msg()
|
|
@@ -140,6 +273,14 @@ class QueryParams(MutableMapping[str, str]):
|
|
|
140
273
|
ctx.enqueue(msg)
|
|
141
274
|
|
|
142
275
|
def clear(self) -> None:
|
|
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
|
+
)
|
|
143
284
|
self.clear_with_no_forward_msg(preserve_embed=True)
|
|
144
285
|
self._send_query_param_msg()
|
|
145
286
|
|
|
@@ -175,6 +316,353 @@ class QueryParams(MutableMapping[str, str]):
|
|
|
175
316
|
if key.lower() in EMBED_QUERY_PARAMS_KEYS and preserve_embed
|
|
176
317
|
}
|
|
177
318
|
|
|
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()
|
|
665
|
+
|
|
178
666
|
|
|
179
667
|
def missing_key_error_message(key: str) -> str:
|
|
180
668
|
return f'st.query_params has no key "{key}".'
|