Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 28 additions & 21 deletions src/workflow/StreamlitUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,31 @@
)


def _matches_topp_parameter(pattern: str, key: bytes) -> bool:
"""
Match an include/exclude pattern against a TOPP parameter key.

Key format: b"ToolName:1:section:subsection:param_name"

A pattern matches the end of the parameter path, bounded by ':' or the
start of the path ("tol" matches "FD:tol"). A pattern ending in ':' names
a whole section from the top of the path instead ("fd:" matches every
parameter under "fd:", but not "FD:" or "algorithm:fd:").
"""
pattern = pattern.lstrip(":") # Strip legacy leading colon
key_str = key.decode()

# Extract param path after "ToolName:1:"
parts = key_str.split(":")
param_path = ":".join(parts[2:]) if len(parts) > 2 else key_str

if pattern.endswith(":"):
return param_path.startswith(pattern)

# Check if pattern matches as a suffix, bounded by ':' or start
return param_path == pattern or param_path.endswith(":" + pattern)


def _mounted_data_root() -> Union[Path, None]:
"""Return the validated mount root from the ``local_data_dir`` setting.

Expand Down Expand Up @@ -866,7 +891,7 @@ def input_TOPP(
Args:
topp_tool_name (str): The name of the TOPP tool for which to generate inputs.
num_cols (int, optional): Number of columns to use for the layout. Defaults to 3.
exclude_parameters (List[str], optional): List of parameter names to exclude from the widget. Defaults to an empty list.
exclude_parameters (List[str], optional): List of parameter names to exclude from the widget. A name ending in ":" excludes that whole top-level section (e.g. "fd:"). Defaults to an empty list.
include_parameters (List[str], optional): List of parameter names to include in the widget. Defaults to an empty list.
flag_parameters (List[str], optional): List of parameter names that should
be treated as no-value CLI flags during command construction.
Expand Down Expand Up @@ -1054,24 +1079,6 @@ def _input_TOPP_impl(
param = poms.Param()
poms.ParamXMLFile().load(str(ini_file_path), param)

def _matches_parameter(pattern: str, key: bytes) -> bool:
"""
Match pattern against TOPP parameter key using suffix matching.

Key format: b"ToolName:1:section:subsection:param_name"

Returns True if pattern matches the end of the param path,
bounded by ':' or start of path.
"""
pattern = pattern.lstrip(":") # Strip legacy leading colon
key_str = key.decode()

# Extract param path after "ToolName:1:"
parts = key_str.split(":")
param_path = ":".join(parts[2:]) if len(parts) > 2 else key_str

# Check if pattern matches as a suffix, bounded by ':' or start
return param_path == pattern or param_path.endswith(":" + pattern)

# Always apply base exclusions (input/output files, standard excludes)
excluded_keys = [
Expand All @@ -1090,15 +1097,15 @@ def _matches_parameter(pattern: str, key: bytes) -> bool:
if not (
b"input file" in param.getTags(key)
or b"output file" in param.getTags(key)
or any([_matches_parameter(k, key) for k in excluded_keys])
or any([_matches_topp_parameter(k, key) for k in excluded_keys])
)
]

# Track which keys are "included" (shown by default) vs "non-included" (advanced only)
if include_parameters:
included_keys = {
key for key in valid_keys
if any([_matches_parameter(k, key) for k in include_parameters])
if any([_matches_topp_parameter(k, key) for k in include_parameters])
}
else:
included_keys = set(valid_keys) # All are included when no filter specified
Expand Down
66 changes: 66 additions & 0 deletions tests/test_topp_parameter_matching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""
``_matches_topp_parameter()`` — how ``input_TOPP(exclude_parameters=...,
include_parameters=...)`` patterns select TOPP parameters.

A plain name matches the end of the parameter path, bounded by ':'. A name
ending in ':' selects a whole top-level section, so a tool can hide a
subsection (e.g. FLASHTnT's ``fd:`` parameters, which only apply to raw input)
without listing every parameter in it.
"""
import os
import sys
from unittest.mock import MagicMock

import pytest

PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.append(PROJECT_ROOT)

# Same sys.modules mocking as tests/test_streamlit_ui_param_read.py.
mock_streamlit = MagicMock()
mock_streamlit.session_state = {}

_originals = {
name: sys.modules.get(name)
for name in ("streamlit", "pyopenms", "src.common.common")
}
sys.modules["streamlit"] = mock_streamlit
if _originals["pyopenms"] is None:
sys.modules["pyopenms"] = MagicMock()
if _originals["src.common.common"] is None:
sys.modules["src.common.common"] = MagicMock()

from src.workflow.StreamlitUI import _matches_topp_parameter

for name, module in _originals.items():
if module is not None:
sys.modules[name] = module
else:
sys.modules.pop(name, None)

for _key in list(sys.modules.keys()):
if _key.startswith("src.workflow"):
sys.modules.pop(_key, None)


@pytest.mark.parametrize(
"pattern, key, expected",
[
# suffix matching, bounded by ':'
("tol", b"FLASHTnT:1:fd:FD:tol", True),
("FD:tol", b"FLASHTnT:1:fd:FD:tol", True),
("ol", b"FLASHTnT:1:fd:FD:tol", False),
("prsm_fdr", b"FLASHTnT:1:prsm_fdr", True),
# legacy leading colon is ignored
(":prsm_fdr", b"FLASHTnT:1:prsm_fdr", True),
# a trailing ':' names a top-level section
("fd:", b"FLASHTnT:1:fd:FD:tol", True),
("fd:FD:", b"FLASHTnT:1:fd:FD:tol", True),
("fd:", b"FLASHTnT:1:fdr", False),
("fd:", b"FLASHTnT:1:prsm_fdr", False),
("FD:", b"FLASHTnT:1:fd:FD:tol", False),
("fd:", b"FLASHTnT:1:tag:fd:x", False),
],
)
def test_matches_topp_parameter(pattern, key, expected):
assert _matches_topp_parameter(pattern, key) is expected
Loading