Skip to content

fix(workflow): stop losing widget input on Streamlit < 1.50 - #31

Merged
t0mdavid-m merged 4 commits into
mainfrom
fix/widget-state-loss
Aug 31, 2026
Merged

fix(workflow): stop losing widget input on Streamlit < 1.50#31
t0mdavid-m merged 4 commits into
mainfrom
fix/widget-state-loss

Conversation

@t0mdavid-m

Copy link
Copy Markdown
Member

src/workflow/StreamlitUI.py is template code. The identical patch is
proposed upstream in OpenMS/streamlit-template#402 — the two diffs are
byte-identical, so a later template sync will not conflict.

Problem

Selecting multiple mzML files in the workflow configure page silently loses
some of them: select six files, only three stick — every second click is
discarded, and the file visibly pops back out of the box.

Root cause

StreamlitUI._input_widget_impl feeds the persisted parameter back into every
widget as its initial-value argument:

value = self.params[key]
st.multiselect(name, options=options, default=value, key=key, ...)

On Streamlit < 1.50 that argument is hashed into the widget's element id
(compute_and_register_element_id(..., default=default_values, ...)). So each
interaction changed default → changed the element id → the browser's next
interaction arrived filed under the previous id, the lookup fell through to
the prior run's value, and that interaction was silently dropped.

Streamlit 1.50.0 changed exactly this, with the comment "Treat the provided
key as the main identity. Only include changes to the options, accept_new_options,
and max_selections in the identity computation as those can invalidate the current
selection."
default was removed from the id. So ≥ 1.50 was never affected.

This is not multiselect-specific. On 1.43.0 every widget type hashes its
current value into the id (text_widgets/checkbox/slider/number_input
hash value=, selectbox hashes index=), so the fix is applied uniformly.

Fix

Seed a widget only on its first render. Streamlit already ignores the
initial-value argument once a keyed widget has an entry in session state, so
this is behaviour-preserving — it just stops the id from churning:

def seed(**kwargs: Any) -> dict:
    return {} if key in st.session_state else kwargs

st.multiselect(name, options=options, key=key, ..., **seed(default=value))

This is consistent with how the codebase already works: apply_preset()
deletes the affected session keys ("so widgets re-initialize fresh") and
clear_parameter_session_state() exists for the same purpose. Reloading from
params.json still works because those paths clear session state first.

Also sorts the select_input_file options — path.iterdir() is unordered, so
both the displayed order and (on < 1.50) the element id varied between restarts.

Verification

Driven against the real StreamlitUI._input_widget_impl and the real
ParameterManager, on streamlit 1.49.1 (the pinned version):

before (main):  6 clicked -> 3 STICK: ['A.mzML', 'C.mzML', 'E.mzML']
after  (patch): 6 clicked -> 6 STICK: all six

Regression-tested across text, number (int and float), checkbox,
selectbox, slider and multiselect — first-render seeding, reload from
params.json, and persisted value type all unchanged, on both 1.49.1 and
1.53.1.

Test suite shows no change: failures/errors are byte-identical before and
after the patch (they are pre-existing, from missing example data).

before after
streamlit-template 4 failed, 164 passed, 4 skipped 4 failed, 164 passed, 4 skipped
quantms-web 6 failed, 80 passed, 2 skipped, 4 errors 6 failed, 80 passed, 2 skipped, 4 errors

Note on the pin

requirements.txt pins streamlit==1.49.1, which is on the affected side of
the 1.50 boundary — so this bug is live on main today. This fix is
version-independent and does not require bumping the pin, but bumping to
>=1.50 would be worthwhile separately as defence in depth.

🤖 Generated with Claude Code

https://claude.ai/code/session_016Qn3mLqr7zBr7rgCokx6ku

`_input_widget_impl` fed the persisted parameter back into every widget as
its initial-value argument (`value=` / `default=` / `index=`). On Streamlit
< 1.50 that argument is hashed into the widget's element id, so the id
changed on every interaction; the following interaction then arrived under
the now-stale id and was silently discarded.

The visible effect was in the mzML file selector: selecting six files kept
only three, because every second click was dropped.

Verified against the real `StreamlitUI._input_widget_impl` and
`ParameterManager` on streamlit 1.49.1, the pinned version:

    before:  6 clicked -> 3 stick  ['A.mzML', 'C.mzML', 'E.mzML']
    after:   6 clicked -> 6 stick

Streamlit ignores the initial-value argument once a keyed widget already
has an entry in session state, so seeding on first render only is
behaviour-preserving: `apply_preset()` and `clear_parameter_session_state()`
already delete the session keys when they want params.json to take effect
again. Regression-tested for text, number (int and float), checkbox,
selectbox, slider and multiselect - first-render seeding, reload from
params.json and value type are all unchanged, on both 1.49.1 and 1.53.1.

Also sorts the `select_input_file` options: `path.iterdir()` is unordered,
so both the displayed order and (on < 1.50) the element id varied between
restarts.

Streamlit >= 1.50 stopped hashing `default` into the element id and was
already unaffected; this fix is version-independent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Qn3mLqr7zBr7rgCokx6ku
@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e767d6f9-365c-42f3-bf05-3fec3c718614


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

t0mdavid-m and others added 3 commits August 31, 2026 19:34
`build-openms` started failing on every branch with:

    cmake - cmake not installed. The package was not found with the source(s) listed.
    Version was specified as '3.31.1'.

Chocolatey delisted that exact patch: its package listing feed now returns
3.31.6, 3.31.10, 3.31.11 and 3.31.12 for the 3.31 series, but not 3.31.1.
Nothing in this repo changed - the last green run was 2026-08-29.

Moves to 3.31.12, the newest patch in the same minor series, deliberately
staying off cmake 4.x: 4.0 dropped compatibility with
`cmake_minimum_required(VERSION < 3.5)`, which the OpenMS build still relies on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Qn3mLqr7zBr7rgCokx6ku
…ly bind

The module swapped `sys.modules['streamlit']` for a MagicMock, imported
ParameterManager, restored the real streamlit, and only then dropped the cached
`src.workflow` modules. That ordering is wrong: a module binds `st` once, at
import time, so if any earlier-collected test had already imported
`src.workflow.ParameterManager` against the real streamlit, the import here was
just a cache hit and the mock never took effect.

The result was a test that passed alone and failed in a full run - the six
`TestSaveParametersWithInstanceName` cases asserted against
`mock_streamlit.session_state` while ParameterManager was reading the real one.

Drops the cached modules *before* the import so the mock binds, and again
afterwards so later test files re-import against the real streamlit.

    pytest tests/test_results_helpers.py tests/test_tool_instance_name.py
      before: 6 failed, 6 passed        after: 12 passed
    pytest tests/test_workflow_manager_stop.py tests/test_tool_instance_name.py
      before: 6 failed, 5 passed        after: 11 passed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Qn3mLqr7zBr7rgCokx6ku
The template sync in #30 restored the template's own `test_gui.py` and two
tests aimed at example pages quantms-web deleted long ago, so the
`continuous-integration` job has been red on main since 2026-08-29:

    34 failed, 89 passed, 4 errors

28 of those failures and all 4 errors were FileNotFoundError for pages that
do not exist here - content/documentation.py, content/simple_workflow.py,
content/run_subprocess.py, content/topp_workflow_*.py, content/file_upload.py,
content/raw_data_viewer.py, content/run_example_workflow.py and
content/download_section.py.

Rewrites test_gui.py to smoke-test the pages app.py actually registers, the
same approach f43b0ad took the last time a sync reintroduced these files, and
deletes tests/test_run_subprocess.py and tests/test_simple_workflow.py.

Only workflow_fileupload, workflow_configure and workflow_run load standalone;
the other 14 call st.page_link and need the navigation context that exists only
when app.py runs (loading them directly raises KeyError: 'url_pathname'), so
they stay covered indirectly by test_app_loads.

Adds test_registered_pages_exist, which parses app.py and asserts every
registered page exists on disk - a direct guard against the next sync
reintroducing this class of breakage.

    pytest test_gui.py tests/   before: 34 failed, 89 passed, 4 errors
                                after:  100 passed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Qn3mLqr7zBr7rgCokx6ku
@t0mdavid-m
t0mdavid-m merged commit 87c0449 into main Aug 31, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant