Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to Tool Test Parsing #18560

Merged
merged 8 commits into from
Jul 19, 2024
8 changes: 6 additions & 2 deletions lib/galaxy/app_unittest_utils/tools_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,13 @@ def setup_app(self):
self.app.config.tool_secret = "testsecret"
self.app.config.track_jobs_in_database = False

def __setup_tool(self):
@property
def tool_source(self):
tool_source = get_tool_source(self.tool_file)
self.tool = create_tool_from_source(self.app, tool_source, config_file=self.tool_file)
return tool_source

def __setup_tool(self):
self.tool = create_tool_from_source(self.app, self.tool_source, config_file=self.tool_file)
if getattr(self, "tool_action", None):
self.tool.tool_action = self.tool_action
return self.tool
Expand Down
6 changes: 5 additions & 1 deletion lib/galaxy/tool_util/parameters/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,11 @@ def _from_input_source_galaxy(input_source: InputSource) -> ToolParameterT:
# Function... example in devteam cummeRbund.
optional = input_source.parse_optional()
dynamic_options = input_source.get("dynamic_options", None)
dynamic_options_elem = input_source.parse_dynamic_options_elem()
dynamic_options_config = input_source.parse_dynamic_options()
if dynamic_options_config:
dynamic_options_elem = dynamic_options.elem()
else:
dynamic_options_elem = 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
Expand Down
2 changes: 1 addition & 1 deletion lib/galaxy/tool_util/parser/cwl.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def parse_description(self):
def parse_interactivetool(self):
return []

def parse_input_pages(self):
def parse_input_pages(self) -> PagesSource:
page_source = CwlPageSource(self.tool_proxy)
return PagesSource([page_source])

Expand Down
38 changes: 32 additions & 6 deletions lib/galaxy/tool_util/parser/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from pydantic import BaseModel
from typing_extensions import TypedDict

from galaxy.util import Element
from galaxy.util.path import safe_walk
from .util import _parse_name

Expand All @@ -42,11 +43,15 @@ class AssertionDict(TypedDict):
AssertionList = Optional[List[AssertionDict]]
XmlInt = Union[str, int]

ToolSourceTestInputs = Any
ToolSourceTestOutputs = Any
TestSourceTestOutputColllection = Any


class ToolSourceTest(TypedDict):
inputs: Any
outputs: Any
output_collections: List[Any]
inputs: ToolSourceTestInputs
outputs: ToolSourceTestOutputs
output_collections: List[TestSourceTestOutputColllection]
stdout: AssertionList
stderr: AssertionList
expect_exit_code: Optional[XmlInt]
Expand Down Expand Up @@ -245,7 +250,7 @@ def parse_requirements_and_containers(
"""Return triple of ToolRequirement, ContainerDescription and ResourceRequirement lists."""

@abstractmethod
def parse_input_pages(self):
def parse_input_pages(self) -> "PagesSource":
"""Return a PagesSource representing inputs by page for tool."""

def parse_provided_metadata_style(self):
Expand Down Expand Up @@ -359,6 +364,23 @@ def inputs_defined(self):
return True


class DynamicOptions(metaclass=ABCMeta):

def elem(self) -> Element:
# For things in transition that still depend on XML - provide a way
# to grab it and just throw an error if feature is attempted to be
# used with other tool sources.
raise NotImplementedError(NOT_IMPLEMENTED_MESSAGE)

@abstractmethod
def get_data_table_name(self) -> Optional[str]:
"""If dynamic options are loaded from a data table, return the name."""

@abstractmethod
def get_index_file_name(self) -> Optional[str]:
"""If dynamic options are loaded from an index file, return the name."""


class InputSource(metaclass=ABCMeta):
default_optional = False

Expand Down Expand Up @@ -418,8 +440,12 @@ def parse_optional(self, default=None):
default = self.default_optional
return self.get_bool("optional", default)

def parse_dynamic_options_elem(self):
"""Return an XML element describing dynamic options."""
def parse_dynamic_options(self) -> Optional[DynamicOptions]:
"""Return an optional element describing dynamic options.

These options are still very XML based but as they are adapted to the infrastructure, the return
type here will evolve.
"""
return None

def parse_static_options(self) -> List[Tuple[str, str, bool]]:
Expand Down
64 changes: 64 additions & 0 deletions lib/galaxy/tool_util/parser/util.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
from collections import OrderedDict
from typing import (
Optional,
Tuple,
TYPE_CHECKING,
Union,
)

from packaging.version import Version

if TYPE_CHECKING:
from .interface import (
InputSource,
ToolSource,
)

DEFAULT_DELTA = 10000
DEFAULT_DELTA_FRAC = None
Expand All @@ -23,3 +37,53 @@ def _parse_name(name, argument):
raise ValueError("parameter must specify a 'name' or 'argument'.")
name = argument.lstrip("-").replace("-", "_")
return name


def parse_profile_version(tool_source: "ToolSource") -> float:
return float(tool_source.parse_profile())


def parse_tool_version_with_defaults(
id: Optional[str], tool_source: "ToolSource", profile: Optional[Version] = None
) -> str:
if profile is None:
profile = Version(tool_source.parse_profile())

version = tool_source.parse_version()
if not version:
if profile < Version("16.04"):
# For backward compatibility, some tools may not have versions yet.
version = "1.0.0"
else:
raise Exception(f"Missing tool 'version' for tool with id '{id}' at '{tool_source}'")
return version


def boolean_is_checked(input_source: "InputSource"):
nullable = input_source.get_bool("optional", False)
return input_source.get_bool("checked", None if nullable else False)


def boolean_true_and_false_values(input_source, profile: Optional[Union[float, str]] = None) -> Tuple[str, str]:
truevalue = input_source.get("truevalue", "true")
falsevalue = input_source.get("falsevalue", "false")
if profile and Version(str(profile)) >= Version("23.1"):
if truevalue == falsevalue:
raise ParameterParseException("Cannot set true and false to the same value")
if truevalue.lower() == "false":
raise ParameterParseException(
f"Cannot set truevalue to [{truevalue}], Galaxy state may encounter issues distinguishing booleans and strings in this case."
)
if falsevalue.lower() == "true":
raise ParameterParseException(
f"Cannot set falsevalue to [{falsevalue}], Galaxy state may encounter issues distinguishing booleans and strings in this case."
)
return (truevalue, falsevalue)


class ParameterParseException(Exception):
message: str

def __init__(self, message):
super().__init__(message)
self.message = message
27 changes: 21 additions & 6 deletions lib/galaxy/tool_util/parser/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from .interface import (
AssertionList,
Citation,
DynamicOptions,
InputSource,
PageSource,
PagesSource,
Expand Down Expand Up @@ -391,7 +392,7 @@ def parse_include_exclude_list(tag_name):
def parse_requirements_and_containers(self):
return requirements.parse_requirements_from_xml(self.root, parse_resources=True)

def parse_input_pages(self):
def parse_input_pages(self) -> "XmlPagesSource":
return XmlPagesSource(self.root)

def parse_provided_metadata_style(self):
Expand Down Expand Up @@ -1217,6 +1218,22 @@ def parse_input_sources(self):
return list(map(XmlInputSource, self.parent_elem))


class XmlDynamicOptions(DynamicOptions):

def __init__(self, options_elem: Element):
self._options_elem = options_elem

def elem(self) -> Element:
return self._options_elem

def get_data_table_name(self) -> Optional[str]:
"""If dynamic options are loaded from a data table, return the name."""
return self._options_elem.get("from_data_table")

def get_index_file_name(self) -> Optional[str]:
return self._options_elem.get("from_file")


class XmlInputSource(InputSource):
def __init__(self, input_elem):
self.input_elem = input_elem
Expand Down Expand Up @@ -1246,12 +1263,10 @@ def parse_sanitizer_elem(self):
def parse_validator_elems(self):
return self.input_elem.findall("validator")

def parse_dynamic_options_elem(self):
"""Return a galaxy.tools.parameters.dynamic_options.DynamicOptions
if appropriate.
"""
def parse_dynamic_options(self) -> Optional[XmlDynamicOptions]:
"""Return a XmlDynamicOptions to describe dynamic options if options elem is available."""
options_elem = self.input_elem.find("options")
return options_elem
return XmlDynamicOptions(options_elem) if options_elem is not None else None

def parse_static_options(self) -> List[Tuple[str, str, bool]]:
"""
Expand Down
2 changes: 1 addition & 1 deletion lib/galaxy/tool_util/parser/yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def parse_requirements_and_containers(self):
resource_requirements=[r for r in mixed_requirements if r.get("type") == "resource"],
)

def parse_input_pages(self):
def parse_input_pages(self) -> PagesSource:
# All YAML tools have only one page (feature is deprecated)
page_source = YamlPageSource(self.root_dict.get("inputs", {}))
return PagesSource([page_source])
Expand Down
7 changes: 7 additions & 0 deletions lib/galaxy/tool_util/unittest_utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from typing import (
Callable,
Dict,
Expand All @@ -6,6 +7,8 @@
)
from unittest.mock import Mock

from galaxy.util import galaxy_directory


def mock_trans(has_user=True, is_admin=False):
"""A mock ``trans`` object for exposing user info to toolbox filter unit tests."""
Expand All @@ -26,3 +29,7 @@ def get_content(filename: Optional[str]) -> bytes:
return content

return get_content


def functional_test_tool_path(test_path: str) -> str:
return os.path.join(galaxy_directory(), "test/functional/tools", test_path)
14 changes: 14 additions & 0 deletions lib/galaxy/tool_util/verify/_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Types used by interactor and test case processor."""

from typing import (
Any,
Dict,
List,
Tuple,
)

ExtraFileInfoDictT = Dict[str, Any]
RequiredFileTuple = Tuple[str, ExtraFileInfoDictT]
RequiredFilesT = List[RequiredFileTuple]
RequiredDataTablesT = List[str]
RequiredLocFileT = List[str]
Loading
Loading