diff --git a/.github/workflows/build-windows-executable-app.yaml b/.github/workflows/build-windows-executable-app.yaml index 99b41773..03748875 100644 --- a/.github/workflows/build-windows-executable-app.yaml +++ b/.github/workflows/build-windows-executable-app.yaml @@ -55,7 +55,7 @@ jobs: shell: bash run: | choco install ccache ninja -y --no-progress - choco install cmake --version=3.31.1 -y --no-progress --force + choco install cmake --version=3.31.12 -y --no-progress --force ## GH CLI "SHOULD BE" installed. Sometimes I had to manually install nonetheless. Super weird. # https://github.com/actions/runner-images/blob/main/images/win/scripts/Installers/Install-GitHub-CLI.ps1 echo "C:/Program Files (x86)/GitHub CLI" >> $GITHUB_PATH diff --git a/src/workflow/StreamlitUI.py b/src/workflow/StreamlitUI.py index f9adf5ec..69cf3264 100644 --- a/src/workflow/StreamlitUI.py +++ b/src/workflow/StreamlitUI.py @@ -559,7 +559,9 @@ def _select_input_file_impl(self, key, name, multiple, display_file_path, reacti if not path.exists(): st.warning(f"No **{name}** files!") return - options = [str(f) for f in path.iterdir() if "external_files.txt" not in str(f)] + options = sorted( + str(f) for f in path.iterdir() if "external_files.txt" not in str(f) + ) # Check if local files are available external_files = Path( @@ -675,11 +677,21 @@ def format_files(input: Any) -> List[str]: key = f"{self.parameter_manager.param_prefix}{key}" + # Streamlit ignores a widget's initial-value argument (value=/default=/index=) + # once that key already exists in session state -- but on Streamlit < 1.50 the + # argument is still hashed into the widget's element id. Since this method feeds + # the persisted parameter straight back in as that argument, the id changed on + # every interaction, the following interaction arrived under the now-stale id and + # was silently dropped: selecting six mzML files kept only three. Seed the widget + # on first render only; from then on session state owns the value. + def seed(**kwargs: Any) -> dict: + return {} if key in st.session_state else kwargs + if widget_type == "text": - st.text_input(name, value=value, key=key, help=help, on_change=on_change) + st.text_input(name, key=key, help=help, on_change=on_change, **seed(value=value)) elif widget_type == "textarea": - st.text_area(name, value=value, key=key, help=help, on_change=on_change) + st.text_area(name, key=key, help=help, on_change=on_change, **seed(value=value)) elif widget_type == "number": number_type = float if isinstance(value, float) else int @@ -693,27 +705,27 @@ def format_files(input: Any) -> List[str]: name, min_value=min_value, max_value=max_value, - value=value, step=step_size, format=None, key=key, help=help, on_change=on_change, + **seed(value=value), ) elif widget_type == "checkbox": - st.checkbox(name, value=value, key=key, help=help, on_change=on_change) + st.checkbox(name, key=key, help=help, on_change=on_change, **seed(value=value)) elif widget_type == "selectbox": if options is not None: st.selectbox( name, options=options, - index=options.index(value) if value in options else 0, key=key, format_func=format_files, help=help, on_change=on_change, + **seed(index=options.index(value) if value in options else 0), ) else: st.warning(f"Select widget '{name}' requires options parameter") @@ -723,11 +735,11 @@ def format_files(input: Any) -> List[str]: st.multiselect( name, options=options, - default=value, key=key, format_func=format_files, help=help, on_change=on_change, + **seed(default=value), ) else: st.warning(f"Select widget '{name}' requires options parameter") @@ -744,12 +756,12 @@ def format_files(input: Any) -> List[str]: name, min_value=min_value, max_value=max_value, - value=value, step=step_size, key=key, format=None, help=help, on_change=on_change, + **seed(value=value), ) else: st.warning( @@ -757,12 +769,12 @@ def format_files(input: Any) -> List[str]: ) elif widget_type == "password": - st.text_input(name, value=value, type="password", key=key, help=help, on_change=on_change) + st.text_input(name, type="password", key=key, help=help, on_change=on_change, **seed(value=value)) elif widget_type == "auto": # Auto-determine widget type based on value if isinstance(value, bool): - st.checkbox(name, value=value, key=key, help=help, on_change=on_change) + st.checkbox(name, key=key, help=help, on_change=on_change, **seed(value=value)) elif isinstance(value, (int, float)): self._input_widget_impl( key, diff --git a/tests/test_tool_instance_name.py b/tests/test_tool_instance_name.py index cd060cac..d1eef17a 100644 --- a/tests/test_tool_instance_name.py +++ b/tests/test_tool_instance_name.py @@ -24,6 +24,23 @@ _original_streamlit = sys.modules.get('streamlit') sys.modules['streamlit'] = mock_streamlit + +def _drop_cached_workflow_modules() -> None: + """Forget any cached src.workflow modules. + + A module binds `st` once, at import time. If an earlier test file has already + imported src.workflow.ParameterManager against the real streamlit, the import + below is just a cache hit and the mock never takes effect - which is why these + tests passed when run alone but failed in a full-suite run. + """ + for _key in list(sys.modules.keys()): + if _key.startswith('src.workflow'): + sys.modules.pop(_key, None) + + +# Drop first, so the import below really binds the mock. +_drop_cached_workflow_modules() + from src.workflow.ParameterManager import ParameterManager if _original_streamlit is not None: @@ -31,10 +48,9 @@ else: sys.modules.pop('streamlit', None) -# Remove cached src.workflow modules -for _key in list(sys.modules.keys()): - if _key.startswith('src.workflow'): - sys.modules.pop(_key, None) +# Drop again, so later test files re-import against the real streamlit instead of +# the mock-bound modules this file just created. +_drop_cached_workflow_modules() @pytest.fixture