diff --git a/lib/galaxy/tool_util/parameters/factory.py b/lib/galaxy/tool_util/parameters/factory.py
index ea149e4b7668..cd872e43a974 100644
--- a/lib/galaxy/tool_util/parameters/factory.py
+++ b/lib/galaxy/tool_util/parameters/factory.py
@@ -1,8 +1,10 @@
from typing import (
Any,
+ cast,
Dict,
List,
Optional,
+ Union,
)
from galaxy.tool_util.parser.cwl import CwlInputSource
@@ -27,7 +29,9 @@
CwlStringParameterModel,
CwlUnionParameterModel,
DataCollectionParameterModel,
+ DataColumnParameterModel,
DataParameterModel,
+ DrillDownParameterModel,
FloatParameterModel,
HiddenParameterModel,
IntegerParameterModel,
@@ -129,14 +133,9 @@ def _from_input_source_galaxy(input_source: InputSource) -> ToolParameterT:
elif param_type == "select":
# Function... example in devteam cummeRbund.
optional = input_source.parse_optional()
- dynamic_options = input_source.get("dynamic_options", None)
dynamic_options_config = input_source.parse_dynamic_options()
- if dynamic_options_config:
- dynamic_options_elem = dynamic_options.elem()
- else:
- dynamic_options_elem = None
+ is_static = dynamic_options_config is None
multiple = input_source.get_bool("multiple", False)
- is_static = dynamic_options is None and dynamic_options_elem is None
options: Optional[List[LabelValue]] = None
if is_static:
options = []
@@ -148,15 +147,40 @@ def _from_input_source_galaxy(input_source: InputSource) -> ToolParameterT:
options=options,
multiple=multiple,
)
+ elif param_type == "drill_down":
+ multiple = input_source.get_bool("multiple", False)
+ hierarchy = input_source.get("hierarchy", "exact")
+ dynamic_options = input_source.parse_drill_down_dynamic_options()
+ static_options = None
+ if dynamic_options is None:
+ static_options = input_source.parse_drill_down_static_options()
+ return DrillDownParameterModel(
+ name=input_source.parse_name(),
+ multiple=multiple,
+ hierarchy=hierarchy,
+ options=static_options,
+ )
+ elif param_type == "data_column":
+ return DataColumnParameterModel(
+ name=input_source.parse_name(),
+ )
else:
raise Exception(f"Unknown Galaxy parameter type {param_type}")
elif input_type == "conditional":
test_param_input_source = input_source.parse_test_input_source()
- test_parameter = _from_input_source_galaxy(test_param_input_source)
+ test_parameter = cast(
+ Union[BooleanParameterModel, SelectParameterModel], _from_input_source_galaxy(test_param_input_source)
+ )
whens = []
default_value = object()
if isinstance(test_parameter, BooleanParameterModel):
default_value = test_parameter.value
+ elif isinstance(test_parameter, SelectParameterModel):
+ select_parameter = cast(SelectParameterModel, test_parameter)
+ select_default_value = select_parameter.default_value
+ if select_default_value is not None:
+ default_value = select_default_value
+
# TODO: handle select parameter model...
for value, case_inputs_sources in input_source.parse_when_input_sources():
if isinstance(test_parameter, BooleanParameterModel):
diff --git a/lib/galaxy/tool_util/parameters/models.py b/lib/galaxy/tool_util/parameters/models.py
index 49635c4e7187..27efc38feee2 100644
--- a/lib/galaxy/tool_util/parameters/models.py
+++ b/lib/galaxy/tool_util/parameters/models.py
@@ -37,6 +37,7 @@
)
from galaxy.exceptions import RequestParameterInvalidException
+from galaxy.tool_util.parser.interface import DrillDownOptionsDict
from ._types import (
cast_as_type,
is_optional,
@@ -478,9 +479,112 @@ def pydantic_template(self, state_representation: StateRepresentationT) -> Dynam
def has_selected_static_option(self):
return self.options is not None and any(o.selected for o in self.options)
+ @property
+ def default_value(self) -> Optional[str]:
+ if self.options:
+ for option in self.options:
+ if option.selected:
+ return option.value
+ # single value pick up first value
+ if not self.optional:
+ return self.options[0].value
+
+ return None
+
+ @property
+ def request_requires_value(self) -> bool:
+ # API will allow an empty value and just grab the first static option
+ # see API Tests -> test_tools.py -> test_select_first_by_default
+ # so only require a value in the multiple case if optional is False
+ return self.multiple and not self.optional
+
+
+DrillDownHierarchyT = Literal["recurse", "exact"]
+
+
+def drill_down_possible_values(options: List[DrillDownOptionsDict], multiple: bool) -> List[str]:
+ possible_values = []
+
+ def add_value(option: str, is_leaf: bool):
+ if not multiple and not is_leaf:
+ return
+ possible_values.append(option)
+
+ def walk_selection(option: DrillDownOptionsDict):
+ child_options = option["options"]
+ is_leaf = not child_options
+ add_value(option["value"], is_leaf)
+ if not is_leaf:
+ for child_option in child_options:
+ walk_selection(child_option)
+
+ for option in options:
+ walk_selection(option)
+
+ return possible_values
+
+
+class DrillDownParameterModel(BaseGalaxyToolParameterModelDefinition):
+ parameter_type: Literal["gx_drill_down"] = "gx_drill_down"
+ options: Optional[List[DrillDownOptionsDict]] = None
+ multiple: bool
+ hierarchy: DrillDownHierarchyT
+
+ @property
+ def py_type(self) -> Type:
+ if self.options is not None:
+ literal_options: List[Type] = [
+ cast_as_type(Literal[o]) for o in drill_down_possible_values(self.options, self.multiple)
+ ]
+ py_type = union_type(literal_options)
+ else:
+ py_type = StrictStr
+
+ if self.multiple:
+ py_type = list_type(py_type)
+
+ return py_type
+
+ def pydantic_template(self, state_representation: StateRepresentationT) -> DynamicModelInformation:
+ return dynamic_model_information_from_py_type(self, self.py_type)
+
@property
def request_requires_value(self) -> bool:
- return not self.optional and not self.has_selected_static_option
+ options = self.options
+ if options:
+ # if any of these are selected, they seem to serve as defaults - check out test_tools -> test_drill_down_first_by_default
+ return not any_drill_down_options_selected(options)
+ else:
+ # I'm not sure how to handle dynamic options... they might or might not be required?
+ # do we need to default to assuming they're not required?
+ return False
+
+
+def any_drill_down_options_selected(options: List[DrillDownOptionsDict]) -> bool:
+ for option in options:
+ selected = option.get("selected")
+ if selected:
+ return True
+ child_options = option.get("options", [])
+ if any_drill_down_options_selected(child_options):
+ return True
+
+ return False
+
+
+class DataColumnParameterModel(BaseGalaxyToolParameterModelDefinition):
+ parameter_type: Literal["gx_data_column"] = "gx_data_column"
+
+ @property
+ def py_type(self) -> Type:
+ return StrictInt
+
+ def pydantic_template(self, state_representation: StateRepresentationT) -> DynamicModelInformation:
+ return dynamic_model_information_from_py_type(self, self.py_type)
+
+ @property
+ def request_requires_value(self) -> bool:
+ return False
DiscriminatorType = Union[bool, str]
@@ -586,18 +690,31 @@ def pydantic_template(self, state_representation: StateRepresentationT) -> Dynam
self.parameters, f"Repeat_{self.name}", state_representation
)
+ initialize_repeat: Any
+ if self.request_requires_value:
+ initialize_repeat = ...
+ else:
+ initialize_repeat = None
+
class RepeatType(RootModel):
- root: List[instance_class] = Field(..., min_length=self.min, max_length=self.max) # type: ignore[valid-type]
+ root: List[instance_class] = Field(initialize_repeat, min_length=self.min, max_length=self.max) # type: ignore[valid-type]
return DynamicModelInformation(
self.name,
- (RepeatType, ...),
+ (RepeatType, initialize_repeat),
{},
)
@property
def request_requires_value(self) -> bool:
- return True # TODO:
+ if self.min is None or self.min == 0:
+ return False
+ # so we know we need at least one value, but maybe none of the parameters in the list
+ # are required
+ for parameter in self.parameters:
+ if parameter.request_requires_value:
+ return True
+ return False
class SectionParameterModel(BaseGalaxyToolParameterModelDefinition):
@@ -799,8 +916,10 @@ def request_requires_value(self) -> bool:
SelectParameterModel,
DataParameterModel,
DataCollectionParameterModel,
+ DataColumnParameterModel,
DirectoryUriParameterModel,
RulesParameterModel,
+ DrillDownParameterModel,
ColorParameterModel,
ConditionalParameterModel,
RepeatParameterModel,
diff --git a/lib/galaxy/tool_util/parser/interface.py b/lib/galaxy/tool_util/parser/interface.py
index 0d23dfb1d94e..89b17698353e 100644
--- a/lib/galaxy/tool_util/parser/interface.py
+++ b/lib/galaxy/tool_util/parser/interface.py
@@ -424,6 +424,20 @@ def get_index_file_name(self) -> Optional[str]:
"""If dynamic options are loaded from an index file, return the name."""
+DrillDownDynamicFilters = Dict[str, Dict[str, dict]] # {input key: {metadata_key: metadata values}}
+
+
+class DrillDownDynamicOptions(metaclass=ABCMeta):
+
+ @abstractmethod
+ def from_code_block(self) -> Optional[str]:
+ """Get a code block to do an eval on."""
+
+ @abstractmethod
+ def from_filters(self) -> Optional[DrillDownDynamicFilters]:
+ """Get filters to apply to target datasets."""
+
+
class InputSource(metaclass=ABCMeta):
default_optional = False
@@ -491,12 +505,22 @@ def parse_dynamic_options(self) -> Optional[DynamicOptions]:
"""
return None
+ def parse_drill_down_dynamic_options(
+ self, tool_data_path: Optional[str] = None
+ ) -> Optional["DrillDownDynamicOptions"]:
+ return None
+
def parse_static_options(self) -> List[Tuple[str, str, bool]]:
"""Return list of static options if this is a select type without
defining a dynamic options.
"""
return []
+ def parse_drill_down_static_options(
+ self, tool_data_path: Optional[str] = None
+ ) -> Optional[List["DrillDownOptionsDict"]]:
+ return None
+
def parse_conversion_tuples(self):
"""Return list of (name, extension) to describe explicit conversions."""
return []
@@ -673,3 +697,10 @@ def from_dict(as_dict):
def to_dict(self):
return dict(name=self.name, attributes=self.attrib, element_tests=self.element_tests)
+
+
+class DrillDownOptionsDict(TypedDict):
+ name: Optional[str]
+ value: str
+ options: List["DrillDownOptionsDict"]
+ selected: bool
diff --git a/lib/galaxy/tool_util/parser/xml.py b/lib/galaxy/tool_util/parser/xml.py
index 04c28a8297c6..e5d37a737cff 100644
--- a/lib/galaxy/tool_util/parser/xml.py
+++ b/lib/galaxy/tool_util/parser/xml.py
@@ -30,12 +30,16 @@
Element,
ElementTree,
string_as_bool,
+ XML,
xml_text,
xml_to_string,
)
from .interface import (
AssertionList,
Citation,
+ DrillDownDynamicFilters,
+ DrillDownDynamicOptions,
+ DrillDownOptionsDict,
DynamicOptions,
InputSource,
PageSource,
@@ -1321,6 +1325,77 @@ def parse_static_options(self) -> List[Tuple[str, str, bool]]:
deduplicated_static_options[value] = (text, value, selected)
return list(deduplicated_static_options.values())
+ def parse_drill_down_dynamic_options(
+ self, tool_data_path: Optional[str] = None
+ ) -> Optional[DrillDownDynamicOptions]:
+ from_file = self.input_elem.get("from_file", None)
+ if from_file:
+ if not os.path.isabs(from_file):
+ assert tool_data_path, "This tool cannot be parsed outside of a Galaxy context"
+ from_file = os.path.join(tool_data_path, from_file)
+ elem = XML(f"{open(from_file).read()}")
+ else:
+ elem = self.input_elem
+
+ dynamic_options_raw = elem.get("dynamic_options", None)
+ dynamic_options: Optional[str] = str(dynamic_options_raw) if dynamic_options_raw else None
+ filters: Optional[DrillDownDynamicFilters] = None
+ if elem.find("filter"):
+ _filters: DrillDownDynamicFilters = {}
+ for filter in elem.findall("filter"):
+ # currently only filtering by metadata key matching input file is allowed
+ filter_type = filter.get("type")
+ if filter_type == "data_meta":
+ data_ref = filter.get("data_ref")
+ assert data_ref
+ if data_ref not in _filters:
+ _filters[data_ref] = {}
+ meta_key = filter.get("meta_key")
+ assert meta_key
+ if meta_key not in _filters[data_ref]:
+ _filters[data_ref][meta_key] = {}
+ meta_value = filter.get("value")
+ if meta_value not in _filters[data_ref][meta_key]:
+ _filters[data_ref][meta_key][meta_value] = []
+ assert meta_value
+ options_elem = filter.find("options")
+ assert options_elem
+ _recurse_drill_down_elems(
+ _filters[data_ref][meta_key][meta_value],
+ options_elem.findall("option"),
+ )
+ filters = _filters
+ if filters is None and dynamic_options is None:
+ return None
+ else:
+ return XmlDrillDownDynamicOptions(
+ code_block=dynamic_options,
+ filters=filters,
+ )
+
+ def parse_drill_down_static_options(
+ self, tool_data_path: Optional[str] = None
+ ) -> Optional[List[DrillDownOptionsDict]]:
+ from_file = self.input_elem.get("from_file", None)
+ if from_file:
+ if not os.path.isabs(from_file):
+ assert tool_data_path, "This tool cannot be parsed outside of a Galaxy context"
+ from_file = os.path.join(tool_data_path, from_file)
+ elem = XML(f"{open(from_file).read()}")
+ else:
+ elem = self.input_elem
+
+ dynamic_options_elem = elem.get("dynamic_options", None)
+ filter_elem = elem.get("filter", None)
+ if dynamic_options_elem is not None and filter_elem is not None:
+ return None
+
+ root_options: List[DrillDownOptionsDict] = []
+ options_elem = elem.find("options")
+ assert options_elem, "Non-dynamic drilldown parameters must supply an options element"
+ _recurse_drill_down_elems(root_options, options_elem.findall("option"))
+ return root_options
+
def parse_optional(self, default=None):
"""Return boolean indicating whether parameter is optional."""
elem = self.input_elem
@@ -1450,3 +1525,35 @@ def parse_citation_elem(citation_elem: Element) -> Optional[Citation]:
type=citation_type,
content=content,
)
+
+
+class XmlDrillDownDynamicOptions(DrillDownDynamicOptions):
+
+ def __init__(self, code_block: Optional[str], filters: Optional[DrillDownDynamicFilters]):
+ self._code_block = code_block
+ self._filters = filters
+
+ def from_code_block(self) -> Optional[str]:
+ """Get a code block to do an eval on."""
+ return self._code_block
+
+ def from_filters(self) -> Optional[DrillDownDynamicFilters]:
+ return self._filters
+
+
+def _recurse_drill_down_elems(options: List[DrillDownOptionsDict], option_elems: List[Element]):
+ for option_elem in option_elems:
+ selected = string_as_bool(option_elem.get("selected", False))
+ nested_options: List[DrillDownOptionsDict] = []
+ value = option_elem.get("value")
+ assert value
+ current_option: DrillDownOptionsDict = DrillDownOptionsDict(
+ {
+ "name": option_elem.get("name"),
+ "value": value,
+ "options": nested_options,
+ "selected": selected,
+ }
+ )
+ _recurse_drill_down_elems(nested_options, option_elem.findall("option"))
+ options.append(current_option)
diff --git a/lib/galaxy/tool_util/unittest_utils/parameters.py b/lib/galaxy/tool_util/unittest_utils/parameters.py
index 71738b5f4694..704a2ba15091 100644
--- a/lib/galaxy/tool_util/unittest_utils/parameters.py
+++ b/lib/galaxy/tool_util/unittest_utils/parameters.py
@@ -1,11 +1,15 @@
import os
from galaxy.tool_util.parameters import (
- from_input_source,
+ input_models_for_tool_source,
ToolParameterBundle,
+ ToolParameterBundleModel,
ToolParameterT,
)
-from galaxy.tool_util.parser.factory import get_tool_source
+from galaxy.tool_util.parser import (
+ get_tool_source,
+ ToolSource,
+)
from galaxy.util import galaxy_directory
@@ -19,22 +23,12 @@ def parameter_bundle(parameter: ToolParameterT) -> ParameterBundle:
return ParameterBundle(parameter)
-def parameter_bundle_for_file(filename: str) -> ParameterBundle:
- return parameter_bundle(tool_parameter(filename))
-
-
-def tool_parameter(filename: str) -> ToolParameterT:
- return from_input_source(parameter_source(filename))
-
-
-def parameter_source(filename: str):
+def parameter_bundle_for_file(filename: str) -> ToolParameterBundleModel:
tool_source = parameter_tool_source(filename)
- input_sources = tool_source.parse_input_pages().page_sources[0].parse_input_sources()
- assert len(input_sources) == 1
- return input_sources[0]
+ return input_models_for_tool_source(tool_source)
-def parameter_tool_source(basename: str):
+def parameter_tool_source(basename: str) -> ToolSource:
path_prefix = os.path.join(galaxy_directory(), "test/functional/tools/parameters", basename)
if os.path.exists(f"{path_prefix}.xml"):
path = f"{path_prefix}.xml"
diff --git a/lib/galaxy/tool_util/verify/__init__.py b/lib/galaxy/tool_util/verify/__init__.py
index 2bcb31106bfb..4096c4c9d865 100644
--- a/lib/galaxy/tool_util/verify/__init__.py
+++ b/lib/galaxy/tool_util/verify/__init__.py
@@ -48,6 +48,10 @@
from galaxy.tool_util.parser.yaml import to_test_assert_list
from galaxy.util import unicodify
from galaxy.util.compression_utils import get_fileobj
+from ._types import (
+ ExpandedToolInputsJsonified,
+ ToolTestDescriptionDict,
+)
from .asserts import verify_assertions
from .test_data import TestDataResolver
@@ -641,3 +645,14 @@ def verify_file_contents_against_dict(
keep_outputs_dir=test_data_target_dir,
verify_extra_files=None,
)
+
+
+__all__ = [
+ "DEFAULT_TEST_DATA_RESOLVER",
+ "ExpandedToolInputsJsonified",
+ "GetFilenameT",
+ "GetLocationT",
+ "ToolTestDescriptionDict",
+ "verify",
+ "verify_file_contents_against_dict",
+]
diff --git a/lib/galaxy/tool_util/verify/_types.py b/lib/galaxy/tool_util/verify/_types.py
index 26bb7161715d..e5aa85f1ddb7 100644
--- a/lib/galaxy/tool_util/verify/_types.py
+++ b/lib/galaxy/tool_util/verify/_types.py
@@ -4,9 +4,21 @@
Any,
Dict,
List,
+ Optional,
Tuple,
)
+from typing_extensions import (
+ NotRequired,
+ TypedDict,
+)
+
+from galaxy.tool_util.parser.interface import (
+ AssertionList,
+ TestSourceTestOutputColllection,
+ ToolSourceTestOutputs,
+)
+
# inputs that have been processed with parse.py and expanded out
ExpandedToolInputs = Dict[str, Any]
# ExpandedToolInputs where any model objects have been json-ified with to_dict()
@@ -16,3 +28,27 @@
RequiredFilesT = List[RequiredFileTuple]
RequiredDataTablesT = List[str]
RequiredLocFileT = List[str]
+
+
+class ToolTestDescriptionDict(TypedDict):
+ tool_id: str
+ tool_version: Optional[str]
+ name: str
+ test_index: int
+ inputs: ExpandedToolInputsJsonified
+ outputs: ToolSourceTestOutputs
+ output_collections: List[TestSourceTestOutputColllection]
+ stdout: Optional[AssertionList]
+ stderr: Optional[AssertionList]
+ expect_exit_code: Optional[int]
+ expect_failure: bool
+ expect_test_failure: bool
+ num_outputs: Optional[int]
+ command_line: Optional[AssertionList]
+ command_version: Optional[AssertionList]
+ required_files: List[Any]
+ required_data_tables: List[Any]
+ required_loc_files: List[str]
+ error: bool
+ exception: Optional[str]
+ maxseconds: NotRequired[Optional[int]]
diff --git a/lib/galaxy/tool_util/verify/asserts/json.py b/lib/galaxy/tool_util/verify/asserts/json.py
index 1ba151ba26c0..9b475f35d980 100644
--- a/lib/galaxy/tool_util/verify/asserts/json.py
+++ b/lib/galaxy/tool_util/verify/asserts/json.py
@@ -30,8 +30,8 @@ def assert_has_json_property_with_value(
value: str,
):
"""Assert JSON tree contains the specified property with specified JSON-ified value."""
- output_json = json.loads(output)
- expected_value = json.loads(value)
+ output_json = assert_json_and_load(output)
+ expected_value = assert_json_and_load(value)
def is_property(key, value):
return key == property and value == expected_value
@@ -45,9 +45,16 @@ def assert_has_json_property_with_text(
text: str,
):
"""Assert JSON tree contains the specified property with specified JSON-ified value."""
- output_json = json.loads(output)
+ output_json = assert_json_and_load(output)
def is_property(key, value):
return key == property and value == text
assert any_in_tree(is_property, output_json), f"Failed to find property [{property}] with text [{text}]"
+
+
+def assert_json_and_load(json_str: str):
+ try:
+ return json.loads(json_str)
+ except Exception:
+ raise AssertionError(f"Failed to parse JSON from {json_str[0:1024]}.")
diff --git a/lib/galaxy/tool_util/verify/interactor.py b/lib/galaxy/tool_util/verify/interactor.py
index e403d08fa1aa..e8971a0ec4f5 100644
--- a/lib/galaxy/tool_util/verify/interactor.py
+++ b/lib/galaxy/tool_util/verify/interactor.py
@@ -55,6 +55,7 @@
RequiredDataTablesT,
RequiredFilesT,
RequiredLocFileT,
+ ToolTestDescriptionDict,
)
from .asserts import verify_assertions
from .wait import wait_on
@@ -234,7 +235,7 @@ def get_tests_summary(self):
assert response.status_code == 200, f"Non 200 response from tool tests available API. [{response.content}]"
return response.json()
- def get_tool_tests(self, tool_id: str, tool_version: Optional[str] = None) -> List["ToolTestDescriptionDict"]:
+ def get_tool_tests(self, tool_id: str, tool_version: Optional[str] = None) -> List[ToolTestDescriptionDict]:
url = f"tools/{tool_id}/test_data"
params = {"tool_version": tool_version} if tool_version else None
response = self._get(url, data=params)
@@ -1314,7 +1315,7 @@ def verify_tool(
client_test_config: Optional[TestConfig] = None,
skip_with_reference_data: bool = False,
skip_on_dynamic_param_errors: bool = False,
- _tool_test_dicts: Optional[List["ToolTestDescriptionDict"]] = None, # extension point only for tests
+ _tool_test_dicts: Optional[List[ToolTestDescriptionDict]] = None, # extension point only for tests
):
if resource_parameters is None:
resource_parameters = {}
@@ -1633,30 +1634,6 @@ def __init__(self, output_exceptions, job_stdio):
self.output_exceptions = output_exceptions
-class ToolTestDescriptionDict(TypedDict):
- tool_id: str
- tool_version: Optional[str]
- name: str
- test_index: int
- inputs: ExpandedToolInputsJsonified
- outputs: ToolSourceTestOutputs
- output_collections: List[TestSourceTestOutputColllection]
- stdout: Optional[AssertionList]
- stderr: Optional[AssertionList]
- expect_exit_code: Optional[int]
- expect_failure: bool
- expect_test_failure: bool
- num_outputs: Optional[int]
- command_line: Optional[AssertionList]
- command_version: Optional[AssertionList]
- required_files: List[Any]
- required_data_tables: List[Any]
- required_loc_files: List[str]
- error: bool
- exception: Optional[str]
- maxseconds: NotRequired[Optional[int]]
-
-
DEFAULT_NUM_OUTPUTS: Optional[int] = None
DEFAULT_OUTPUT_COLLECTIONS: List[TestSourceTestOutputColllection] = []
DEFAULT_REQUIRED_FILES: RequiredFilesT = []
@@ -1673,7 +1650,7 @@ class ToolTestDescriptionDict(TypedDict):
DEFAULT_EXCEPTION: Optional[str] = None
-def adapt_tool_source_dict(processed_dict: ToolTestDict) -> "ToolTestDescriptionDict":
+def adapt_tool_source_dict(processed_dict: ToolTestDict) -> ToolTestDescriptionDict:
"""Convert the dictionaries parsed from tool sources (ToolTestDict) to a ToolTestDescriptionDict.
ToolTestDescription is used inside and outside of Galaxy, so convert the dictionaries to the format
diff --git a/lib/galaxy/tool_util/verify/script.py b/lib/galaxy/tool_util/verify/script.py
index fbb67fc11b37..fca086cf1325 100644
--- a/lib/galaxy/tool_util/verify/script.py
+++ b/lib/galaxy/tool_util/verify/script.py
@@ -23,10 +23,10 @@
import yaml
+from galaxy.tool_util.verify import ToolTestDescriptionDict
from galaxy.tool_util.verify.interactor import (
DictClientTestConfig,
GalaxyInteractorApi,
- ToolTestDescriptionDict,
verify_tool,
)
diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py
index 5e84b7782fc6..5eeba8aaecaa 100644
--- a/lib/galaxy/tools/parameters/basic.py
+++ b/lib/galaxy/tools/parameters/basic.py
@@ -53,7 +53,6 @@
string_as_bool,
string_as_bool_or_none,
unicodify,
- XML,
)
from galaxy.util.dictifiable import UsesDictVisibleKeys
from galaxy.util.expressions import ExpressionContext
@@ -168,6 +167,7 @@ class ToolParameter(UsesDictVisibleKeys):
of valid choices, validation logic, ...)
>>> from galaxy.util.bunch import Bunch
+ >>> from galaxy.util import XML
>>> trans = Bunch(app=None)
>>> p = ToolParameter(None, XML(''))
>>> assert p.name == 'parameter_name'
@@ -272,6 +272,7 @@ def to_text(self, value) -> str:
"""
Convert a value to a text representation suitable for displaying to
the user
+ >>> from galaxy.util import XML
>>> p = ToolParameter(None, XML(''))
>>> print(p.to_text(None))
Not available.
@@ -390,6 +391,7 @@ class TextToolParameter(SimpleTextToolParameter):
Parameter that can take on any text value.
>>> from galaxy.util.bunch import Bunch
+ >>> from galaxy.util import XML
>>> trans = Bunch(app=None)
>>> p = TextToolParameter(None, XML(''))
>>> print(p.name)
@@ -430,6 +432,7 @@ class IntegerToolParameter(TextToolParameter):
Parameter that takes an integer value.
>>> from galaxy.util.bunch import Bunch
+ >>> from galaxy.util import XML
>>> trans = Bunch(app=None, history=Bunch(), workflow_building_mode=True)
>>> p = IntegerToolParameter(None, XML(''))
>>> print(p.name)
@@ -502,6 +505,7 @@ class FloatToolParameter(TextToolParameter):
Parameter that takes a real number value.
>>> from galaxy.util.bunch import Bunch
+ >>> from galaxy.util import XML
>>> trans = Bunch(app=None, history=Bunch(), workflow_building_mode=True)
>>> p = FloatToolParameter(None, XML(''))
>>> print(p.name)
@@ -576,6 +580,7 @@ class BooleanToolParameter(ToolParameter):
Parameter that takes one of two values.
>>> from galaxy.util.bunch import Bunch
+ >>> from galaxy.util import XML
>>> trans = Bunch(app=None, history=Bunch())
>>> p = BooleanToolParameter(None, XML(''))
>>> print(p.name)
@@ -648,6 +653,7 @@ class FileToolParameter(ToolParameter):
Parameter that takes an uploaded file as a value.
>>> from galaxy.util.bunch import Bunch
+ >>> from galaxy.util import XML
>>> trans = Bunch(app=None, history=Bunch())
>>> p = FileToolParameter(None, XML(''))
>>> print(p.name)
@@ -721,6 +727,7 @@ class FTPFileToolParameter(ToolParameter):
Parameter that takes a file uploaded via FTP as a value.
>>> from galaxy.util.bunch import Bunch
+ >>> from galaxy.util import XML
>>> trans = Bunch(app=None, history=Bunch(), user=None)
>>> p = FTPFileToolParameter(None, XML(''))
>>> print(p.name)
@@ -794,6 +801,7 @@ class HiddenToolParameter(ToolParameter):
Parameter that takes one of two values.
>>> from galaxy.util.bunch import Bunch
+ >>> from galaxy.util import XML
>>> trans = Bunch(app=None, history=Bunch())
>>> p = HiddenToolParameter(None, XML(''))
>>> print(p.name)
@@ -818,6 +826,7 @@ class ColorToolParameter(ToolParameter):
Parameter that stores a color.
>>> from galaxy.util.bunch import Bunch
+ >>> from galaxy.util import XML
>>> trans = Bunch(app=None, history=Bunch())
>>> p = ColorToolParameter(None, XML(''))
>>> print(p.name)
@@ -860,6 +869,7 @@ class BaseURLToolParameter(HiddenToolParameter):
current server base url. Used in all redirects.
>>> from galaxy.util.bunch import Bunch
+ >>> from galaxy.util import XML
>>> trans = Bunch(app=None, history=Bunch())
>>> p = BaseURLToolParameter(None, XML(''))
>>> print(p.name)
@@ -901,6 +911,7 @@ class SelectToolParameter(ToolParameter):
Parameter that takes on one (or many) or a specific set of values.
>>> from galaxy.util.bunch import Bunch
+ >>> from galaxy.util import XML
>>> trans = Bunch(app=None, history=Bunch(), workflow_building_mode=False)
>>> p = SelectToolParameter(None, XML(
... '''
@@ -1187,6 +1198,7 @@ class GenomeBuildParameter(SelectToolParameter):
>>> # Create a mock transaction with 'hg17' as the current build
>>> from galaxy.util.bunch import Bunch
+ >>> from galaxy.util import XML
>>> trans = Bunch(app=None, history=Bunch(genome_build='hg17'), db_builds=read_dbnames(None))
>>> p = GenomeBuildParameter(None, XML(''))
>>> print(p.name)
@@ -1359,6 +1371,7 @@ class ColumnListParameter(SelectToolParameter):
>>> # Mock up a history (not connected to database)
>>> from galaxy.model import History, HistoryDatasetAssociation
>>> from galaxy.util.bunch import Bunch
+ >>> from galaxy.util import XML
>>> from galaxy.model.mapping import init
>>> sa_session = init("/tmp", "sqlite:///:memory:", create_tables=True).session
>>> hist = History()
@@ -1579,9 +1592,12 @@ class DrillDownSelectToolParameter(SelectToolParameter):
Parameter that takes on one (or many) of a specific set of values.
Creating a hierarchical select menu, which allows users to 'drill down' a tree-like set of options.
+ >>> from galaxy.util import XML
>>> from galaxy.util.bunch import Bunch
- >>> trans = Bunch(app=None, history=Bunch(genome_build='hg17'), db_builds=read_dbnames(None))
- >>> p = DrillDownSelectToolParameter(None, XML(
+ >>> app = Bunch(config=Bunch(tool_data_path=None))
+ >>> tool = Bunch(app=app)
+ >>> trans = Bunch(app=app, history=Bunch(genome_build='hg17'), db_builds=read_dbnames(None))
+ >>> p = DrillDownSelectToolParameter(tool, XML(
... '''
...
...
@@ -1619,54 +1635,24 @@ class DrillDownSelectToolParameter(SelectToolParameter):
"""
def __init__(self, tool, input_source, context=None):
- def recurse_option_elems(cur_options, option_elems):
- for option_elem in option_elems:
- selected = string_as_bool(option_elem.get("selected", False))
- cur_options.append(
- {
- "name": option_elem.get("name"),
- "value": option_elem.get("value"),
- "options": [],
- "selected": selected,
- }
- )
- recurse_option_elems(cur_options[-1]["options"], option_elem.findall("option"))
-
input_source = ensure_input_source(input_source)
ToolParameter.__init__(self, tool, input_source)
- # TODO: abstract XML out of here - so non-XML InputSources can
- # specify DrillDown parameters.
- elem = input_source.elem()
- self.multiple = string_as_bool(elem.get("multiple", False))
- self.display = elem.get("display", None)
- self.hierarchy = elem.get("hierarchy", "exact") # exact or recurse
- self.separator = elem.get("separator", ",")
- if from_file := elem.get("from_file", None):
- if not os.path.isabs(from_file):
- from_file = os.path.join(tool.app.config.tool_data_path, from_file)
- elem = XML(f"{open(from_file).read()}")
- self.dynamic_options = elem.get("dynamic_options", None)
- if self.dynamic_options:
- self.is_dynamic = True
- self.options = []
- self.filtered: Dict[str, Any] = {}
- if elem.find("filter"):
+ self.multiple = input_source.get_bool("multiple", False)
+ self.display = input_source.get("display", None)
+ self.hierarchy = input_source.get("hierarchy", "exact") # exact or recurse
+ self.separator = input_source.get("separator", ",")
+ tool_data_path = tool.app.config.tool_data_path
+ drill_down_dynamic_options = input_source.parse_drill_down_dynamic_options(tool_data_path)
+ if drill_down_dynamic_options is not None:
self.is_dynamic = True
- for filter in elem.findall("filter"):
- # currently only filtering by metadata key matching input file is allowed
- if filter.get("type") == "data_meta":
- if filter.get("data_ref") not in self.filtered:
- self.filtered[filter.get("data_ref")] = {}
- if filter.get("meta_key") not in self.filtered[filter.get("data_ref")]:
- self.filtered[filter.get("data_ref")][filter.get("meta_key")] = {}
- if filter.get("value") not in self.filtered[filter.get("data_ref")][filter.get("meta_key")]:
- self.filtered[filter.get("data_ref")][filter.get("meta_key")][filter.get("value")] = []
- recurse_option_elems(
- self.filtered[filter.get("data_ref")][filter.get("meta_key")][filter.get("value")],
- filter.find("options").findall("option"),
- )
- elif not self.dynamic_options:
- recurse_option_elems(self.options, elem.find("options").findall("option"))
+ self.dynamic_options = drill_down_dynamic_options.code_block
+ self.filtered = drill_down_dynamic_options.filters
+ self.options = []
+ else:
+ self.is_dynamic = False
+ self.dynamic_options = None
+ self.filtered = {}
+ self.options = input_source.parse_drill_down_static_options(tool_data_path)
def _get_options_from_code(self, trans=None, other_values=None):
assert self.dynamic_options, Exception("dynamic_options was not specifed")
diff --git a/lib/galaxy/webapps/galaxy/api/tools.py b/lib/galaxy/webapps/galaxy/api/tools.py
index 59ad93ef5f54..f5dacdc64541 100644
--- a/lib/galaxy/webapps/galaxy/api/tools.py
+++ b/lib/galaxy/webapps/galaxy/api/tools.py
@@ -31,7 +31,7 @@
FetchDataFormPayload,
FetchDataPayload,
)
-from galaxy.tool_util.verify.interactor import ToolTestDescriptionDict
+from galaxy.tool_util.verify import ToolTestDescriptionDict
from galaxy.tools.evaluation import global_tool_errors
from galaxy.util.zipstream import ZipstreamWrapper
from galaxy.web import (
diff --git a/lib/galaxy_test/api/test_tools.py b/lib/galaxy_test/api/test_tools.py
index 4b7ba5244232..53bc8209899c 100644
--- a/lib/galaxy_test/api/test_tools.py
+++ b/lib/galaxy_test/api/test_tools.py
@@ -879,6 +879,58 @@ def test_dataset_hidden_after_job_finish(self):
output_details = self.dataset_populator.get_history_dataset_details(history_id, dataset=output, wait=True)
assert not output_details["visible"]
+ @skip_without_tool("gx_select")
+ def test_select_first_by_default(self):
+ # we have a tool test for this but I wanted to verify it wasn't just the
+ # tool test framework filling in a default. Creating a raw request here
+ # verifies that currently select parameters don't require a selection.
+ with self.dataset_populator.test_history(require_new=False) as history_id:
+ inputs: Dict[str, Any] = {}
+ response = self._run("gx_select", history_id, inputs, assert_ok=True)
+ output = response["outputs"][0]
+ output1_content = self.dataset_populator.get_history_dataset_content(history_id, dataset=output)
+ assert output1_content.strip() == "--ex1"
+
+ inputs = {
+ "parameter": None,
+ }
+ response = self._run("gx_select", history_id, inputs, assert_ok=False)
+ self._assert_status_code_is(response, 400)
+ assert "an invalid option" in response.text
+
+ @skip_without_tool("gx_drill_down_exact")
+ @skip_without_tool("gx_drill_down_exact_multiple")
+ @skip_without_tool("gx_drill_down_recurse")
+ @skip_without_tool("gx_drill_down_recurse_multiple")
+ def test_drill_down_first_by_default(self):
+ # we have a tool test for this but I wanted to verify it wasn't just the
+ # tool test framework filling in a default. Creating a raw request here
+ # verifies that currently select parameters don't require a selection.
+ with self.dataset_populator.test_history(require_new=False) as history_id:
+ inputs: Dict[str, Any] = {}
+ response = self._run("gx_drill_down_exact", history_id, inputs, assert_ok=False)
+ self._assert_status_code_is(response, 400)
+ assert "an invalid option" in response.text
+
+ response = self._run("gx_drill_down_exact_multiple", history_id, inputs, assert_ok=False)
+ self._assert_status_code_is(response, 400)
+ assert "an invalid option" in response.text
+
+ response = self._run("gx_drill_down_recurse", history_id, inputs, assert_ok=False)
+ self._assert_status_code_is(response, 400)
+ assert "an invalid option" in response.text
+
+ response = self._run("gx_drill_down_recurse_multiple", history_id, inputs, assert_ok=False)
+ self._assert_status_code_is(response, 400)
+ assert "an invalid option" in response.text
+
+ # having an initially selected value - is useful for the UI but doesn't serve
+ # as a default and doesn't make the drill down optional in a someway.
+ response = self._run("gx_drill_down_exact_with_selection", history_id, inputs, assert_ok=True)
+ output = response["outputs"][0]
+ output1_content = self.dataset_populator.get_history_dataset_content(history_id, dataset=output)
+ assert output1_content.strip() == "parameter: aba"
+
@skip_without_tool("multi_select")
def test_multi_select_as_list(self):
with self.dataset_populator.test_history(require_new=False) as history_id:
@@ -905,6 +957,19 @@ def test_multi_select_optional(self):
assert output1_content.strip() == "--ex1"
assert output2_content.strip() == "None", output2_content
+ @skip_without_tool("gx_repeat_boolean_min")
+ def test_optional_repeats_with_mins_filled_id(self):
+ # we have a tool test for this but I wanted to verify it wasn't just the
+ # tool test framework filling in a default. Creating a raw request here
+ # verifies that currently select parameters don't require a selection.
+ with self.dataset_populator.test_history(require_new=False) as history_id:
+ inputs: Dict[str, Any] = {}
+ response = self._run("gx_repeat_boolean_min", history_id, inputs, assert_ok=True)
+ output = response["outputs"][0]
+ output1_content = self.dataset_populator.get_history_dataset_content(history_id, dataset=output)
+ assert "false" in output1_content
+ assert "length: 2" in output1_content
+
@skip_without_tool("library_data")
def test_library_data_param(self):
with self.dataset_populator.test_history(require_new=False) as history_id:
diff --git a/lib/tool_shed/webapp/frontend/src/schema/schema.ts b/lib/tool_shed/webapp/frontend/src/schema/schema.ts
index 6e53a87611b5..8122e6ae0647 100644
--- a/lib/tool_shed/webapp/frontend/src/schema/schema.ts
+++ b/lib/tool_shed/webapp/frontend/src/schema/schema.ts
@@ -453,8 +453,10 @@ export interface components {
| components["schemas"]["SelectParameterModel"]
| components["schemas"]["DataParameterModel"]
| components["schemas"]["DataCollectionParameterModel"]
+ | components["schemas"]["DataColumnParameterModel"]
| components["schemas"]["DirectoryUriParameterModel"]
| components["schemas"]["RulesParameterModel"]
+ | components["schemas"]["DrillDownParameterModel"]
| components["schemas"]["ColorParameterModel"]
| components["schemas"]["ConditionalParameterModel"]
| components["schemas"]["RepeatParameterModel"]
@@ -689,6 +691,39 @@ export interface components {
*/
parameter_type?: "gx_data_collection"
}
+ /** DataColumnParameterModel */
+ DataColumnParameterModel: {
+ /** Argument */
+ argument?: string | null
+ /** Help */
+ help?: string | null
+ /**
+ * Hidden
+ * @default false
+ */
+ hidden?: boolean
+ /**
+ * Is Dynamic
+ * @default false
+ */
+ is_dynamic?: boolean
+ /** Label */
+ label?: string | null
+ /** Name */
+ name: string
+ /**
+ * Optional
+ * @default false
+ */
+ optional?: boolean
+ /**
+ * Parameter Type
+ * @default gx_data_column
+ * @constant
+ * @enum {string}
+ */
+ parameter_type?: "gx_data_column"
+ }
/** DataParameterModel */
DataParameterModel: {
/** Argument */
@@ -815,6 +850,59 @@ export interface components {
/** Value */
value: string | null
}
+ /** DrillDownOptionsDict */
+ DrillDownOptionsDict: {
+ /** Name */
+ name: string | null
+ /** Options */
+ options: components["schemas"]["DrillDownOptionsDict"][]
+ /** Selected */
+ selected: boolean
+ /** Value */
+ value: string
+ }
+ /** DrillDownParameterModel */
+ DrillDownParameterModel: {
+ /** Argument */
+ argument?: string | null
+ /** Help */
+ help?: string | null
+ /**
+ * Hidden
+ * @default false
+ */
+ hidden?: boolean
+ /**
+ * Hierarchy
+ * @enum {string}
+ */
+ hierarchy: "recurse" | "exact"
+ /**
+ * Is Dynamic
+ * @default false
+ */
+ is_dynamic?: boolean
+ /** Label */
+ label?: string | null
+ /** Multiple */
+ multiple: boolean
+ /** Name */
+ name: string
+ /**
+ * Optional
+ * @default false
+ */
+ optional?: boolean
+ /** Options */
+ options?: components["schemas"]["DrillDownOptionsDict"][] | null
+ /**
+ * Parameter Type
+ * @default gx_drill_down
+ * @constant
+ * @enum {string}
+ */
+ parameter_type?: "gx_drill_down"
+ }
/** FailedRepositoryUpdateMessage */
FailedRepositoryUpdateMessage: {
/** Err Msg */
@@ -1062,8 +1150,10 @@ export interface components {
| components["schemas"]["SelectParameterModel"]
| components["schemas"]["DataParameterModel"]
| components["schemas"]["DataCollectionParameterModel"]
+ | components["schemas"]["DataColumnParameterModel"]
| components["schemas"]["DirectoryUriParameterModel"]
| components["schemas"]["RulesParameterModel"]
+ | components["schemas"]["DrillDownParameterModel"]
| components["schemas"]["ColorParameterModel"]
| components["schemas"]["ConditionalParameterModel"]
| components["schemas"]["RepeatParameterModel"]
@@ -1143,8 +1233,10 @@ export interface components {
| components["schemas"]["SelectParameterModel"]
| components["schemas"]["DataParameterModel"]
| components["schemas"]["DataCollectionParameterModel"]
+ | components["schemas"]["DataColumnParameterModel"]
| components["schemas"]["DirectoryUriParameterModel"]
| components["schemas"]["RulesParameterModel"]
+ | components["schemas"]["DrillDownParameterModel"]
| components["schemas"]["ColorParameterModel"]
| components["schemas"]["ConditionalParameterModel"]
| components["schemas"]["RepeatParameterModel"]
@@ -1489,8 +1581,10 @@ export interface components {
| components["schemas"]["SelectParameterModel"]
| components["schemas"]["DataParameterModel"]
| components["schemas"]["DataCollectionParameterModel"]
+ | components["schemas"]["DataColumnParameterModel"]
| components["schemas"]["DirectoryUriParameterModel"]
| components["schemas"]["RulesParameterModel"]
+ | components["schemas"]["DrillDownParameterModel"]
| components["schemas"]["ColorParameterModel"]
| components["schemas"]["ConditionalParameterModel"]
| components["schemas"]["RepeatParameterModel"]
diff --git a/test/functional/tools/multiple_versions_changes_v01.xml b/test/functional/tools/multiple_versions_changes_v01.xml
index da49bbcda794..a29a4388f198 100644
--- a/test/functional/tools/multiple_versions_changes_v01.xml
+++ b/test/functional/tools/multiple_versions_changes_v01.xml
@@ -15,7 +15,7 @@
-
+