diff --git a/lib/galaxy/tool_util/parameters/factory.py b/lib/galaxy/tool_util/parameters/factory.py
index b73dde4ab52e..b98bef854fd8 100644
--- a/lib/galaxy/tool_util/parameters/factory.py
+++ b/lib/galaxy/tool_util/parameters/factory.py
@@ -30,6 +30,7 @@
CwlUnionParameterModel,
DataCollectionParameterModel,
DataParameterModel,
+ DrillDownParameterModel,
FloatParameterModel,
HiddenParameterModel,
IntegerParameterModel,
@@ -145,6 +146,19 @@ 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,
+ )
else:
raise Exception(f"Unknown Galaxy parameter type {param_type}")
elif input_type == "conditional":
diff --git a/lib/galaxy/tool_util/parameters/models.py b/lib/galaxy/tool_util/parameters/models.py
index 157037c5a5e0..f3abe7f4c199 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,
@@ -498,6 +499,61 @@ def request_requires_value(self) -> bool:
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:
+ # TODO: retest this logic in the API for drill downs
+ return self.multiple and not self.optional
+
+
DiscriminatorType = Union[bool, str]
@@ -829,6 +885,7 @@ def request_requires_value(self) -> bool:
DataCollectionParameterModel,
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..7b6f7e4a3ed8 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,20 @@ 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 +695,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..7d9b22a2effa 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,71 @@ 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
+ _recurse_drill_down_elems(
+ _filters[data_ref][meta_key][meta_value],
+ filter.find("options").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] = []
+ _recurse_drill_down_elems(root_options, elem.find("options").findall("option"))
+ return root_options
+
def parse_optional(self, default=None):
"""Return boolean indicating whether parameter is optional."""
elem = self.input_elem
@@ -1450,3 +1519,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/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/test/functional/tools/parameters/gx_drill_down_exact.xml b/test/functional/tools/parameters/gx_drill_down_exact.xml
new file mode 100644
index 000000000000..42603cc7c4c9
--- /dev/null
+++ b/test/functional/tools/parameters/gx_drill_down_exact.xml
@@ -0,0 +1,35 @@
+
+
+ macros.xml
+
+ '$output'
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/functional/tools/parameters/gx_drill_down_exact_multiple.xml b/test/functional/tools/parameters/gx_drill_down_exact_multiple.xml
new file mode 100644
index 000000000000..303a81089d4b
--- /dev/null
+++ b/test/functional/tools/parameters/gx_drill_down_exact_multiple.xml
@@ -0,0 +1,35 @@
+
+
+ macros.xml
+
+ '$output'
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/functional/tools/parameters/gx_drill_down_recurse.xml b/test/functional/tools/parameters/gx_drill_down_recurse.xml
new file mode 100644
index 000000000000..561bc04a955f
--- /dev/null
+++ b/test/functional/tools/parameters/gx_drill_down_recurse.xml
@@ -0,0 +1,32 @@
+
+
+ macros.xml
+
+ '$output'
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/functional/tools/parameters/gx_drill_down_recurse_multiple.xml b/test/functional/tools/parameters/gx_drill_down_recurse_multiple.xml
new file mode 100644
index 000000000000..23c13303db11
--- /dev/null
+++ b/test/functional/tools/parameters/gx_drill_down_recurse_multiple.xml
@@ -0,0 +1,35 @@
+
+
+ macros.xml
+
+ '$output'
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/functional/tools/parameters/macros.xml b/test/functional/tools/parameters/macros.xml
index e47d243f75c4..7b00d8004b34 100644
--- a/test/functional/tools/parameters/macros.xml
+++ b/test/functional/tools/parameters/macros.xml
@@ -1,4 +1,9 @@
+
+
+
+
+
> '$output';
@@ -30,4 +35,22 @@ cat '$inputs' >> '$inputs_json';
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/unit/tool_util/parameter_specification.yml b/test/unit/tool_util/parameter_specification.yml
index 7ada0a8aab89..39b663888642 100644
--- a/test/unit/tool_util/parameter_specification.yml
+++ b/test/unit/tool_util/parameter_specification.yml
@@ -620,6 +620,17 @@ gx_section_data:
request_internal_invalid:
- parameter: { data_parameter: { src: hda, id: abcdabcd } }
+gx_drill_down_exact:
+ request_valid:
+ - parameter: aa
+ - parameter: bbb
+ - parameter: ba
+ request_invalid:
+ # not multiple so cannot choose a non-leaf
+ - parameter: a
+ - parameter: c
+ - parameter: {}
+
cwl_int:
request_valid:
- parameter: 5