Fix selectbox and multiselect clearing selections for custom objects#13648
Fix selectbox and multiselect clearing selections for custom objects#13648lukasmasuch merged 7 commits intodevelopfrom
Conversation
…#13646) When using st.multiselect() with custom class objects and a format_func, selections would be cleared after each script rerun in v1.53.0. This was a regression introduced by PR #13448. The issue occurred because register_widget() deepcopies widget values, and the validation logic used == comparison which falls back to identity checks for objects without __eq__. Deepcopied instances fail identity comparison, causing valid selections to be filtered out. The fix changes the validation to use format_func() for comparisons instead, comparing by formatted string representation. This is more robust and works correctly with custom objects regardless of __eq__ implementation. The solution also includes error handling for edge cases where format_func fails on incompatible value types. - Modified validate_and_sync_multiselect_value_with_options() to accept and use format_func parameter - Added comprehensive unit tests for custom objects with and without __eq__ - Added edge case test for format_func failures - Added E2E test to verify selections persist across script reruns Co-Authored-By: Claude <[email protected]>
✅ 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 fixes a regression where st.multiselect() would clear user selections after each script rerun when using custom class objects without __eq__ implementation. The root cause was that validation logic used == comparison, which falls back to identity comparison for objects without __eq__, causing deepcopied widget values to fail validation.
Changes:
- Modified validation logic to compare values using their formatted string representation via
format_funcinstead of direct equality comparison - Added comprehensive unit tests covering custom objects without
__eq__, partial matches, and edge cases - Added E2E test to verify selections persist across script reruns with custom objects
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
lib/streamlit/elements/lib/options_selector_utils.py |
Updated validate_and_sync_multiselect_value_with_options to compare values using format_func instead of ==, with exception handling for format failures |
lib/streamlit/elements/widgets/multiselect.py |
Passed format_func parameter to validation function |
lib/tests/streamlit/elements/lib/options_selector_utils_test.py |
Added 4 comprehensive unit tests covering custom objects scenarios and edge cases |
e2e_playwright/st_multiselect.py |
Added test app with custom class objects without __eq__ |
e2e_playwright/st_multiselect_test.py |
Added E2E test verifying selections persist across reruns |
📉 Frontend coverage change detectedThe frontend unit test (vitest) coverage has decreased by 0.0000%
✅ Coverage change is within normal range. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
| # can be formatted successfully. | ||
| continue | ||
|
|
||
| if formatted_value in formatted_options_set: |
There was a problem hiding this comment.
Validation and serialization use inconsistent comparison methods
Low Severity
The new validation uses format_func comparison while the existing serialize method uses options.index() (which relies on ==). Since register_widget always deepcopies values, custom objects without __eq__ can pass validation but fail serialization. When serialization fails, the fallback assumes the value is a string (per the comment at line 129-130), but it's actually a custom object. This inconsistency can cause incorrect serialization if widget_state.value_changed is True (e.g., when using st.session_state[key] = value).
Additional Locations (1)
SummaryThis PR fixes issue #13646 where Root Cause: The validation logic in Solution: Changed the validation to compare values using Code QualityThe code changes are well-structured, minimal, and follow existing patterns in the codebase.
|
Address PR review comment: The validation and serialization methods were using inconsistent comparison approaches. Validation used format_func while serialization used options.index() which relies on ==. This could cause incorrect serialization for custom objects without __eq__ when widget_state.value_changed is True. Changes: - Updated MultiSelectSerde to accept and store format_func parameter - Modified serialize() to use format_func for finding formatted options instead of options.index() which relies on == comparison - Added test for serializing deepcopied custom objects - Updated existing test to pass format_func to serde Co-Authored-By: Claude <[email protected]>
…bjects This extends the fix from #13646 to selectbox as well. The same issue existed where custom objects without __eq__ would fail validation and serialization after deepcopy because the old implementation used index_() which relies on == comparison. Changes: - Update SelectboxSerde to use format_func for serialization - Update validate_and_sync_value_with_options to use format_func - Pass format_func from selectbox widget to serde and validation - Add tests for selectbox with deepcopied custom objects Co-Authored-By: Claude <[email protected]>
For Enum values, use the original index_() approach which uses == comparison. This correctly handles enum class identity - enums from different classes (e.g., after script rerun) should NOT be considered equal, which is important for enum coercion to work correctly when coercion is disabled. For non-Enum values, use format_func comparison. This handles custom objects without __eq__ where widget values are deepcopied and the deepcopied instances would fail identity comparison with ==. Co-Authored-By: Claude <[email protected]>
kmcgrady
left a comment
There was a problem hiding this comment.
Code changes here are fine. Do we need to consider this for other options-based widgets? If not, can we just add to the PR description, why it's unnecessary?
st.radiost.pillsst.segmented_controlst.select_slider- Maybe some column config like SelectboxColumn?
st.radio -> already addressed in a recent PR |
…13648) ## Describe your changes Fixes issue #13646 where `st.multiselect()` and `st.selectbox` clears user selections after each script rerun when using custom class objects without `__eq__` implementation and a `format_func` parameter. This was a regression introduced in PR #13448. **Root Cause**: The validation logic compared multiselect values using `==`, which falls back to identity comparison for objects without `__eq__`. Since `register_widget()` deepcopies widget values, the new instances fail identity comparison with original options, causing valid selections to be filtered out. **Solution**: Changed validation to compare values using `format_func()` by their formatted string representation instead of using `==`. This is more robust and works correctly regardless of whether custom classes implement `__eq__`. ## Testing Plan - Unit Tests: Added 4 comprehensive test cases covering custom objects without `__eq__`, partial matches, objects with `__str__`, and edge cases where `format_func` fails on incompatible types - E2E Tests: Added `test_multiselect_custom_objects_without_eq` to verify selections persist across script reruns with custom class objects - Backward Compatibility: The `format_func` parameter has a default value of `str`, so existing code continues to work without changes --- **Contribution License Agreement** By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license. --------- Co-authored-by: Claude <[email protected]>
Describe your changes
Fixes issue #13646 where
st.multiselect()andst.selectboxclears user selections after each script rerun when using custom class objects without__eq__implementation and aformat_funcparameter. This was a regression introduced in PR #13448.Root Cause: The validation logic compared multiselect values using
==, which falls back to identity comparison for objects without__eq__. Sinceregister_widget()deepcopies widget values, the new instances fail identity comparison with original options, causing valid selections to be filtered out.Solution: Changed validation to compare values using
format_func()by their formatted string representation instead of using==. This is more robust and works correctly regardless of whether custom classes implement__eq__.Testing Plan
__eq__, partial matches, objects with__str__, and edge cases whereformat_funcfails on incompatible typestest_multiselect_custom_objects_without_eqto verify selections persist across script reruns with custom class objectsformat_funcparameter has a default value ofstr, so existing code continues to work without changesContribution License Agreement
By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.