Allow dynamic changes to st.pills and st.segmented_control options when key is provided#13684
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 switches ButtonGroup widgets (feedback, pills, segmented_control) to use string-based values over the wire and tightens dynamic-options behavior and test coverage, especially for pills and segmented controls with changing options. It also clarifies and tests ID stability semantics when options and other props change.
Changes:
- Extend the
ButtonGroupprotobuf with araw_valuesfield to carry string-based selections, and update Python/TS ButtonGroup implementations to consistently usestring_array_value/raw_valuesinstead of index-basedvalue. - Replace the old index-based serde stack with a unified
_ButtonGroupSerdethat works with formatted strings and integrates with sharedoptions_selector_utilsvalidation utilities for dynamic options, including enum coercion and value-reset logic. - Expand backend unit tests, frontend unit tests, and e2e tests for pills and segmented_control to cover string-based serialization, dynamic option changes (including removal/preservation scenarios), and refined stable-ID expectations based on
click_moderather than option lists.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| proto/streamlit/proto/ButtonGroup.proto | Adds a repeated string raw_values = 14 field and bumps the next field ID, enabling string-based value transport for ButtonGroup widgets. |
| lib/tests/streamlit/elements/button_group_test.py | Replaces legacy serde tests with coverage for _ButtonGroupSerde’s string-based behavior and expands ButtonGroup/pills/segmented_control tests to reflect new ID-stability and dynamic-options semantics. |
| lib/streamlit/elements/widgets/button_group.py | Introduces _ButtonGroupSerde, switches ButtonGroup widgets to string-array widget state and raw_values, wires in options-mapping and validation helpers, and refines key_as_main_identity so pills/segmented_control IDs depend on click_mode while feedback still depends on both options and click mode. |
| frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.tsx | Adds helpers to map between string values and option indices, updates widget state syncing to use setStringArrayValue, and reads current selection from rawValues instead of index-based value. |
| frontend/lib/src/components/widgets/ButtonGroup/ButtonGroup.test.tsx | Adjusts expectations from int-array to string-array widget values and adds assertions around rawValues-based updates and fragment ID propagation. |
| e2e_playwright/st_segmented_control_test.py | Refines the dynamic segmented control E2E test to explicitly validate reset vs. preservation behavior when options and defaults change, and documents the formatted-value-based preservation contract. |
| e2e_playwright/st_segmented_control.py | Updates the dynamic segmented control demo app to use overlapping option sets (with mango) and new defaults that exercise both reset and preservation paths under changing options. |
| e2e_playwright/st_pills_test.py | Mirrors the segmented control dynamic-options E2E coverage for st.pills, including reset when a selection disappears and preservation when a selection survives across different option lists. |
| e2e_playwright/st_pills.py | Adjusts the dynamic pills demo to parallel the segmented control scenario (shared mango option, shifted indices, new defaults) and documents the formatted-label preservation behavior. |
st.pills and st.segmented_control options when key is provided
|
@cursor review |
SummaryThis PR enables dynamic option changes for Key changes:
Code QualityThe code quality is high and follows Streamlit's established patterns well: Strengths:
Minor observations:
Test CoverageTest coverage is excellent and comprehensive: Python Unit Tests (
Frontend Unit Tests (
E2E Tests (
Backwards CompatibilityAnalysis:
Risk Assessment:
Recommendation: The // DEPRECATED: Use raw_values instead for string-based value handling.
// Kept for backwards compatibility with older frontends.
repeated uint32 value = 7 [deprecated=true];Security & RiskNo security concerns identified. The changes are well-contained to the button group widget implementation and follow established patterns in the codebase. Regression risks are minimal:
Recommendations
VerdictAPPROVED: This is a well-implemented feature that follows established Streamlit patterns, has comprehensive test coverage across all testing layers (unit, frontend, e2e), and poses minimal backwards compatibility risk. The code quality is high and the changes are well-documented through tests. The minor recommendations above are suggestions for improvement but do not block merging. This is an automated AI review using |
📉 Python coverage change detectedThe Python unit test coverage has decreased by 0.0407%
✅ Coverage change is within normal range. Coverage by files
|
📉 Frontend coverage change detectedThe frontend unit test (vitest) coverage has decreased by 0.0100%
✅ Coverage change is within normal range. |
Co-Authored-By: Claude <[email protected]>
Resolved conflicts: - button_group.py: Kept string-based serde for dynamic options, removed feedback method (now extracted to separate module) - button_group_test.py: Kept ButtonGroupSerde tests, removed feedback tests (moved to feedback_test.py) - ButtonGroup.proto: Kept raw_values field for string-based wire format
…vation - Store content strings in React state instead of indices - Derive selectedIndices from content strings via useMemo - This allows options to change dynamically without losing selection Co-Authored-By: Claude (claude-opus-4-5) <[email protected]>
The old index-based 'value' field is no longer used since we switched to string-based 'raw_values' for dynamic options support. Reserved the field number (7) and name to maintain proto compatibility. Co-Authored-By: Claude (claude-opus-4-5) <[email protected]>
Add validation to raise StreamlitAPIException when duplicate labels are detected, either from raw options or via format_func. This prevents ambiguous selection behavior where duplicate labels could map to the wrong option index. Co-Authored-By: Claude (claude-opus-4-5) <[email protected]>
Allow duplicate labels in st.pills and st.segmented_control with "last one wins" behavior, matching how radio, selectbox, and multiselect handle this case. Co-Authored-By: Claude (claude-opus-4-5) <[email protected]>
SummaryThis PR enables dynamic option changes for The implementation switches from index-based to string-based widget values, aligning with the approach already used by Key changes:
Code QualityThe code is well-structured and follows established patterns in the codebase: Strengths:
Minor observations:
Test CoverageThe test coverage is excellent and comprehensive: Python Unit Tests (
Frontend Unit Tests (
E2E Tests (
The tests follow best practices from Backwards CompatibilityProtocol changes are properly handled:
Impact on existing users:
Note: This is a wire format change, so mixed-version deployments (old frontend + new backend or vice versa) would have issues. This is expected and acceptable for Streamlit upgrades. Security & RiskNo security concerns identified:
Low regression risk:
RecommendationsNo blocking issues identified. A few optional suggestions:
VerdictAPPROVED: This is a well-implemented feature that aligns with existing patterns in the codebase, has comprehensive test coverage, and properly handles backwards compatibility. The change addresses user needs (issues #11277 and #12392) while maintaining code quality standards. This is an automated AI review using |
…ultiselect Simplify the pills/segmented_control serialization to use plain content strings instead of "content|index" format. This matches how radio, selectbox, and multiselect handle string-based state: - Backend mapping uses formatted string as key (last wins for duplicates) - Frontend stores and sends plain content strings - No index suffix stripping needed Co-Authored-By: Claude (claude-opus-4-5) <[email protected]>
- Inline key_as_main_identity set directly in function call - Remove "Always use string-based values" comment that states the obvious Co-Authored-By: Claude (claude-opus-4-5) <[email protected]>
Co-Authored-By: Claude (claude-opus-4-5) <[email protected]>
Co-Authored-By: Claude (claude-opus-4-5) <[email protected]>
- Remove if-else branches that did the same thing in both _serialize_single and _serialize_multi - Remove "mirrors radio/selectbox/multiselect" reference from docstring Co-Authored-By: Claude (claude-opus-4-5) <[email protected]>
The test "sets widget value on update" was incorrectly expecting the default value (icon_3) instead of the provided rawValues (icon_4).
- Extract getSelectionMode helper with switch statement - Simplify getContentElement return using shorthand notation
Refactor _ButtonGroupSerde into _SingleSelectButtonGroupSerde and _MultiSelectButtonGroupSerde for cleaner typing and reduced casts. This eliminates 5 type casts by having separate classes with precise type signatures for each selection mode.
- Refactor _internal_button_group to use single _button_group call - Remove redundant comment from ButtonGroup.tsx onClick handler
- Fix single-select deselection: distinguish between None (initial state, use default) and empty list (explicit deselection, return None) - Align frontend to backend's "last wins" behavior for duplicate labels in both ButtonGroup and Radio widgets - Add test for explicit deselection scenario
e2e_playwright/st_pills_test.py
Outdated
|
|
||
| This tests that: | ||
| 1. Options can be changed dynamically when a key is provided | ||
| 2. Format function can be changed dynamically |
There was a problem hiding this comment.
Nit: Im not seeing that this tests the format_func changing
There was a problem hiding this comment.
Removed this comment, it's a bit more complicated for format_func. Changing the formatting won't reset the component, but will cause the value to be reset to the default if the currently selected formatted value isn't part of the new formatted options. Which didn't made it useable in this test.
The test doesn't actually change format_func dynamically - both initial and updated states use the same capitalize function.
Describe your changes
Allow dynamically changing the options for
st.segmented_controlandst.pillswithout triggering an identity change/state reset. If the current selected options isn't in the list of available option, it will be reset to the default value.This also applies a needed change from index-based widget value to string-based (formatted options). The same change was already implemented for
st.selectboxandst.multiselect, andst.radio.GitHub Issue Link (if applicable)
Testing Plan
Contribution License Agreement
By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.