Bind widgets to query params - st.multiselect#13951
Conversation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
✅ PR preview is ready!
|
There was a problem hiding this comment.
Pull request overview
This PR adds query parameter binding support to st.multiselect, enabling two-way synchronization between multiselect widget values and URL query parameters. The implementation follows established patterns from other bound widgets (checkbox, radio, selectbox, text_input) and introduces robust URL sanitization for array-valued widgets.
Changes:
- Added
bind="query-params"parameter tost.multiselectthat uses repeated URL params (e.g.,?tags=Red&tags=Blue) - Implemented URL sanitization pipeline for array-valued widgets: filters invalid options, deduplicates values, and truncates to
max_selectionslimit - When
accept_new_options=True, any URL string is accepted without validation - Multiselect is always clearable (empty URL param
?key=clears to[])
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| proto/streamlit/proto/MultiSelect.proto | Added optional query_param_key field (15) to enable query param binding |
| lib/streamlit/runtime/state/common.py | Added max_array_length field to WidgetMetadata for array truncation support |
| lib/streamlit/runtime/state/session_state.py | Implemented _sanitize_url_array helper and integrated sanitization into _seed_widget_from_url |
| lib/streamlit/runtime/state/widgets.py | Added max_array_length parameter to register_widget function |
| lib/streamlit/elements/widgets/multiselect.py | Added bind parameter to multiselect, set query_param_key in proto when bound, pass metadata for URL validation |
| frontend/lib/src/components/widgets/Multiselect/Multiselect.tsx | Created queryParamBinding config with urlFormat: "repeated" and passed to useBasicWidgetState |
| lib/tests/streamlit/typing/multiselect_types.py | Added type tests for bind parameter with various configurations |
| lib/tests/streamlit/runtime/state/session_state_test.py | Added 13 unit tests covering array sanitization: filtering, deduplication, truncation, and their composition |
| lib/tests/streamlit/elements/multiselect_test.py | Added 6 unit tests for bind parameter validation and proto setup |
| frontend/lib/src/components/widgets/Multiselect/Multiselect.test.tsx | Added 3 frontend unit tests for query param registration/unregistration |
| e2e_playwright/st_multiselect_test.py | Added 13 E2E tests covering URL seeding, updates, validation, filtering, truncation, and edge cases |
| e2e_playwright/st_multiselect.py | Added 5 test widgets with different query param configurations |
SummaryThis PR adds Key changes span the full stack:
Code QualityThe code is well-structured and follows existing patterns established by other query-param-bound widgets (selectbox, radio, checkbox, etc.). Strengths:
Minor observations (non-blocking):
Test CoverageTest coverage is excellent and thorough across all layers: Python unit tests (
Session state unit tests (
Frontend unit tests (
E2E tests (
Typing tests (
Backwards CompatibilityNo breaking changes. The
Security & Risk
AccessibilityThe frontend changes are minimal — only adding the Recommendations
VerdictAPPROVED: This is a well-implemented feature addition that follows established patterns, includes thorough test coverage across all layers, and handles edge cases (invalid values, duplicates, max_selections, accept_new_options) robustly. The code is clean, well-documented, and backwards compatible. This is an automated AI review by |
5199f31 to
279ce76
Compare
SummaryAdds Code QualityImplementation follows existing query-parameter binding patterns and keeps sanitization logic centralized. No blocking correctness issues found. Test CoverageAdded Python unit tests ( Backwards Compatibility
Security & RiskNo new security-sensitive surfaces beyond optional URL synchronization. Sanitization and deduplication reduce risk of invalid URL state. AccessibilityNo changes to interaction semantics or labeling patterns; accessibility behavior appears unchanged. Recommendations
# Handle query param binding
url_value_seeded = False
if metadata.bind == "query-params" and user_key is not None:
url_value_seeded = self._handle_query_param_binding(
metadata, user_key, widget_id
)
if (
widget_id not in self
and (user_key is None or user_key not in self)
and not url_value_seeded
):
# This is the first time the widget is registered, so we save its
# value in widget state (unless we already seeded from URL).
deserializer = metadata.deserializer
initial_widget_value = deepcopy(deserializer(None))
self._new_widget_state.set_from_value(widget_id, initial_widget_value)
# Get the current value of the widget for use as its return value.
# We return a copy, so that reference types can't be accidentally
# mutated by user code.
widget_value = cast("T", self[widget_id])
widget_value = deepcopy(widget_value)
# widget_value_changed indicates to the caller that the widget's
# current value is different from what is in the frontend.
widget_value_changed = user_key is not None and self.is_new_state_value(
user_key
)
return RegisterWidgetResult(widget_value, widget_value_changed)
def _handle_query_param_binding(
self, metadata: WidgetMetadata[T], user_key: str, widget_id: str
) -> bool:
"""Handle query param binding for a widget.
Registers the binding, then attempts to seed the widget's value from URL
based on priority rules: def remove_stale_bindings(
self,
active_widget_ids: set[str],
fragment_ids_this_run: list[str] | None = None,
widget_metadata: dict[str, Any] | None = None,
) -> None:
"""Remove bindings and URL params for widgets that are no longer active.
This cleans up query params for conditional widgets that have been unmounted.
For fragment runs, widgets outside the running fragment(s) are preserved.
Note: Page-based cleanup for MPA navigation is handled separately via
populate_from_query_string() which is called before the script runs.
"""
stale_widget_ids = []
for widget_id in self._bindings_by_widget:
if widget_id in active_widget_ids:
# Widget is active in this run - keep it
continue
# For fragment runs, preserve widgets that aren't part of the running fragments
if fragment_ids_this_run and widget_metadata:
metadata = widget_metadata.get(widget_id)
if metadata and metadata.fragment_id not in fragment_ids_this_run:
# Widget belongs to a different fragment or main script - keep it
continue
stale_widget_ids.append(widget_id)
params_removed = False
for widget_id in stale_widget_ids:
binding = self._bindings_by_widget.get(widget_id)
if binding:
param_key = binding.param_key
# Remove the query param from the URL
if param_key in self._query_params:
del self._query_params[param_key]
params_removed = True
self.unbind_widget(widget_id)
# Send forward message to update frontend URL if we removed any params
if params_removed:
self._send_query_param_msg()VerdictAPPROVED: Solid implementation with thorough tests; only a minor unbinding edge case to consider. This is an automated AI review by |
lukasmasuch
left a comment
There was a problem hiding this comment.
LGTM 👍
Consider unbinding on the backend when a keyed widget stops using bind="query-params" so URL params are not left permanently marked as bound for active widgets.
Is this valid? If so, might be a good to add to potential follow-ups.
Adds the bind parameter to `st.multiselect` to enable two-way sync between widget values and URL query parameters. **Key changes:** - Added `bind="query-params"` support to `st.multiselect` using repeated params (`?tags=Red&tags=Blue`) - Implement robust URL sanitization for array-valued widgets: invalid option filtering, duplicate deduplication, and max_selections truncation — all with automatic URL correction - When `accept_new_options=True`, any URL string accepted (no filtering) - Multiselect is always clearable (with `?foo=`)
Describe your changes
Adds the bind parameter to
st.multiselectto enable two-way sync between widget values and URL query parameters.Key changes:
bind="query-params"support tost.multiselectusing repeated params (?tags=Red&tags=Blue)accept_new_options=True, any URL string accepted (no filtering)?foo=)Testing Plan