diff --git a/lib/galaxy/tool_util/linters/tests.py b/lib/galaxy/tool_util/linters/tests.py index f6f483279003..a14438c5bd14 100644 --- a/lib/galaxy/tool_util/linters/tests.py +++ b/lib/galaxy/tool_util/linters/tests.py @@ -11,6 +11,7 @@ from galaxy.tool_util.lint import Linter from galaxy.tool_util.parameters import validate_test_cases_for_tool_source from galaxy.tool_util.verify.assertion_models import assertion_list +from galaxy.tool_util.verify.asserts import parse_xml_assertions from galaxy.util import asbool from ._util import is_datasource @@ -150,11 +151,9 @@ def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"): # TODO: validate command, command_version, element tests. What about children? for output in test["outputs"]: asserts_raw = output.get("attributes", {}).get("assert_list") or [] - to_yaml_assertions = [] - for raw_assert in asserts_raw: - to_yaml_assertions.append({"that": raw_assert["tag"], **raw_assert.get("attributes", {})}) + as_python_dicts = parse_xml_assertions(asserts_raw) try: - assertion_list.model_validate(to_yaml_assertions) + assertion_list.model_validate(as_python_dicts) except Exception as e: error_str = _cleanup_pydantic_error(e) lint_ctx.warn( diff --git a/lib/galaxy/tool_util/parser/xml.py b/lib/galaxy/tool_util/parser/xml.py index aba5a6874248..5a5e1e616da1 100644 --- a/lib/galaxy/tool_util/parser/xml.py +++ b/lib/galaxy/tool_util/parser/xml.py @@ -746,10 +746,10 @@ def _test_elem_to_dict(test_elem, i, profile=None) -> ToolSourceTest: output_collections=__parse_output_collection_elems(test_elem, profile=profile), inputs=__parse_input_elems(test_elem, i), expect_num_outputs=test_elem.get("expect_num_outputs"), - command=__parse_assert_list_from_elem(test_elem.find("assert_command")), - command_version=__parse_assert_list_from_elem(test_elem.find("assert_command_version")), - stdout=__parse_assert_list_from_elem(test_elem.find("assert_stdout")), - stderr=__parse_assert_list_from_elem(test_elem.find("assert_stderr")), + command=parse_assert_list_from_elem(test_elem.find("assert_command")), + command_version=parse_assert_list_from_elem(test_elem.find("assert_command_version")), + stdout=parse_assert_list_from_elem(test_elem.find("assert_stdout")), + stderr=parse_assert_list_from_elem(test_elem.find("assert_stderr")), expect_exit_code=test_elem.get("expect_exit_code"), expect_failure=string_as_bool(test_elem.get("expect_failure", False)), expect_test_failure=string_as_bool(test_elem.get("expect_test_failure", False)), @@ -783,7 +783,7 @@ def __parse_output_elem(output_elem): def __parse_command_elem(test_elem): assert_elem = test_elem.find("command") - return __parse_assert_list_from_elem(assert_elem) + return parse_assert_list_from_elem(assert_elem) def __parse_output_collection_elems(test_elem, profile=None): @@ -918,10 +918,10 @@ def __parse_test_attributes( def __parse_assert_list(output_elem) -> AssertionList: assert_elem = output_elem.find("assert_contents") - return __parse_assert_list_from_elem(assert_elem) + return parse_assert_list_from_elem(assert_elem) -def __parse_assert_list_from_elem(assert_elem) -> AssertionList: +def parse_assert_list_from_elem(assert_elem) -> AssertionList: assert_list = None def convert_elem(elem): diff --git a/lib/galaxy/tool_util/verify/assertion_models.py b/lib/galaxy/tool_util/verify/assertion_models.py index 4e184334e368..4bb02626bafd 100644 --- a/lib/galaxy/tool_util/verify/assertion_models.py +++ b/lib/galaxy/tool_util/verify/assertion_models.py @@ -60,6 +60,15 @@ def check_non_negative_if_set(v: typing.Any): return v +def check_non_negative_if_set_permissive(v: typing.Any): + if v is not None: + try: + assert float(v) >= 0.0 + except TypeError: + raise AssertionError(f"Invalid type found {v}") + return v + + def check_non_negative_if_int(v: typing.Any): if v is not None and isinstance(v, int): assert typing.cast(int, v) >= 0 @@ -2185,3 +2194,1431 @@ class assertion_dict(AssertionModel): assertions = typing.Union[assertion_list, assertion_dict] + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_line_model_python_dict(AssertionModel): + r"""Asserts the specified output contains the line specified by the + argument line. The exact number of occurrences can be optionally + specified by the argument n""" + + that: Literal["has_line"] = "has_line" + + line: str = Field( + ..., + description=has_line_line_description, + ) + + n: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_line_n_description, + ) + + delta: Annotated[ + typing.Union[int, str], BeforeValidator(check_bytes), BeforeValidator(check_non_negative_if_int) + ] = Field( + 0, + description=has_line_delta_description, + ) + + min: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_line_min_description, + ) + + max: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_line_max_description, + ) + + negate: typing.Union[bool, str] = Field( + False, + description=has_line_negate_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_line_matching_model_python_dict(AssertionModel): + r"""Asserts the specified output contains a line matching the + regular expression specified by the argument expression. If n is given + the assertion checks for exactly n occurences.""" + + that: Literal["has_line_matching"] = "has_line_matching" + + expression: str = Field( + ..., + description=has_line_matching_expression_description, + ) + + n: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_line_matching_n_description, + ) + + delta: Annotated[ + typing.Union[int, str], BeforeValidator(check_bytes), BeforeValidator(check_non_negative_if_int) + ] = Field( + 0, + description=has_line_matching_delta_description, + ) + + min: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_line_matching_min_description, + ) + + max: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_line_matching_max_description, + ) + + negate: typing.Union[bool, str] = Field( + False, + description=has_line_matching_negate_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_n_lines_model_python_dict(AssertionModel): + r"""Asserts the specified output contains ``n`` lines allowing + for a difference in the number of lines (delta) + or relative differebce in the number of lines""" + + that: Literal["has_n_lines"] = "has_n_lines" + + n: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_n_lines_n_description, + ) + + delta: Annotated[ + typing.Union[int, str], BeforeValidator(check_bytes), BeforeValidator(check_non_negative_if_int) + ] = Field( + 0, + description=has_n_lines_delta_description, + ) + + min: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_n_lines_min_description, + ) + + max: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_n_lines_max_description, + ) + + negate: typing.Union[bool, str] = Field( + False, + description=has_n_lines_negate_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_text_model_python_dict(AssertionModel): + r"""Asserts specified output contains the substring specified by + the argument text. The exact number of occurrences can be + optionally specified by the argument n""" + + that: Literal["has_text"] = "has_text" + + text: str = Field( + ..., + description=has_text_text_description, + ) + + n: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_text_n_description, + ) + + delta: Annotated[ + typing.Union[int, str], BeforeValidator(check_bytes), BeforeValidator(check_non_negative_if_int) + ] = Field( + 0, + description=has_text_delta_description, + ) + + min: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_text_min_description, + ) + + max: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_text_max_description, + ) + + negate: typing.Union[bool, str] = Field( + False, + description=has_text_negate_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_text_matching_model_python_dict(AssertionModel): + r"""Asserts the specified output contains text matching the + regular expression specified by the argument expression. + If n is given the assertion checks for exacly n (nonoverlapping) + occurences.""" + + that: Literal["has_text_matching"] = "has_text_matching" + + expression: str = Field( + ..., + description=has_text_matching_expression_description, + ) + + n: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_text_matching_n_description, + ) + + delta: Annotated[ + typing.Union[int, str], BeforeValidator(check_bytes), BeforeValidator(check_non_negative_if_int) + ] = Field( + 0, + description=has_text_matching_delta_description, + ) + + min: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_text_matching_min_description, + ) + + max: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_text_matching_max_description, + ) + + negate: typing.Union[bool, str] = Field( + False, + description=has_text_matching_negate_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class not_has_text_model_python_dict(AssertionModel): + r"""Asserts specified output does not contain the substring + specified by the argument text""" + + that: Literal["not_has_text"] = "not_has_text" + + text: str = Field( + ..., + description=not_has_text_text_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_n_columns_model_python_dict(AssertionModel): + r"""Asserts tabular output contains the specified + number (``n``) of columns. + + For instance, ````. The assertion tests only the first line. + Number of columns can optionally also be specified with ``delta``. Alternatively the + range of expected occurences can be specified by ``min`` and/or ``max``. + + Optionally a column separator (``sep``, default is `` ``) `and comment character(s) + can be specified (``comment``, default is empty string). The first non-comment + line is used for determining the number of columns.""" + + that: Literal["has_n_columns"] = "has_n_columns" + + n: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_n_columns_n_description, + ) + + delta: Annotated[ + typing.Union[int, str], BeforeValidator(check_bytes), BeforeValidator(check_non_negative_if_int) + ] = Field( + 0, + description=has_n_columns_delta_description, + ) + + min: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_n_columns_min_description, + ) + + max: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_n_columns_max_description, + ) + + sep: str = Field( + " ", + description=has_n_columns_sep_description, + ) + + comment: str = Field( + "", + description=has_n_columns_comment_description, + ) + + negate: typing.Union[bool, str] = Field( + False, + description=has_n_columns_negate_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class attribute_is_model_python_dict(AssertionModel): + r"""Asserts the XML ``attribute`` for the element (or tag) with the specified + XPath-like ``path`` is the specified ``text``. + + For example: + + ```xml + + ``` + + The assertion implicitly also asserts that an element matching ``path`` exists. + With ``negate`` the result of the assertion (on the equality) can be inverted (the + implicit assertion on the existence of the path is not affected).""" + + that: Literal["attribute_is"] = "attribute_is" + + path: str = Field( + ..., + description=attribute_is_path_description, + ) + + attribute: str = Field( + ..., + description=attribute_is_attribute_description, + ) + + text: str = Field( + ..., + description=attribute_is_text_description, + ) + + negate: typing.Union[bool, str] = Field( + False, + description=attribute_is_negate_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class attribute_matches_model_python_dict(AssertionModel): + r"""Asserts the XML ``attribute`` for the element (or tag) with the specified + XPath-like ``path`` matches the regular expression specified by ``expression``. + + For example: + + ```xml + + ``` + + The assertion implicitly also asserts that an element matching ``path`` exists. + With ``negate`` the result of the assertion (on the matching) can be inverted (the + implicit assertion on the existence of the path is not affected).""" + + that: Literal["attribute_matches"] = "attribute_matches" + + path: str = Field( + ..., + description=attribute_matches_path_description, + ) + + attribute: str = Field( + ..., + description=attribute_matches_attribute_description, + ) + + expression: Annotated[str, BeforeValidator(check_regex)] = Field( + ..., + description=attribute_matches_expression_description, + ) + + negate: typing.Union[bool, str] = Field( + False, + description=attribute_matches_negate_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class element_text_model_python_dict(AssertionModel): + r"""This tag allows the developer to recurisively specify additional assertions as + child elements about just the text contained in the element specified by the + XPath-like ``path``, e.g. + + ```xml + + + + ``` + + The assertion implicitly also asserts that an element matching ``path`` exists. + With ``negate`` the result of the implicit assertions can be inverted. + The sub-assertions, which have their own ``negate`` attribute, are not affected + by ``negate``.""" + + that: Literal["element_text"] = "element_text" + + path: str = Field( + ..., + description=element_text_path_description, + ) + + negate: typing.Union[bool, str] = Field( + False, + description=element_text_negate_description, + ) + + children: typing.Optional["assertion_list"] = None + asserts: typing.Optional["assertion_list"] = None + + @model_validator(mode="before") + @classmethod + def validate_children(self, data: typing.Any): + if isinstance(data, dict) and "children" not in data and "asserts" not in data: + raise ValueError("At least one of 'children' or 'asserts' must be specified for this assertion type.") + return data + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class element_text_is_model_python_dict(AssertionModel): + r"""Asserts the text of the XML element with the specified XPath-like ``path`` is + the specified ``text``. + + For example: + + ```xml + + ``` + + The assertion implicitly also asserts that an element matching ``path`` exists. + With ``negate`` the result of the assertion (on the equality) can be inverted (the + implicit assertion on the existence of the path is not affected).""" + + that: Literal["element_text_is"] = "element_text_is" + + path: str = Field( + ..., + description=element_text_is_path_description, + ) + + text: str = Field( + ..., + description=element_text_is_text_description, + ) + + negate: typing.Union[bool, str] = Field( + False, + description=element_text_is_negate_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class element_text_matches_model_python_dict(AssertionModel): + r"""Asserts the text of the XML element with the specified XPath-like ``path`` + matches the regular expression defined by ``expression``. + + For example: + + ```xml + + ``` + + The assertion implicitly also asserts that an element matching ``path`` exists. + With ``negate`` the result of the assertion (on the matching) can be inverted (the + implicit assertion on the existence of the path is not affected).""" + + that: Literal["element_text_matches"] = "element_text_matches" + + path: str = Field( + ..., + description=element_text_matches_path_description, + ) + + expression: Annotated[str, BeforeValidator(check_regex)] = Field( + ..., + description=element_text_matches_expression_description, + ) + + negate: typing.Union[bool, str] = Field( + False, + description=element_text_matches_negate_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_element_with_path_model_python_dict(AssertionModel): + r"""Asserts the XML output contains at least one element (or tag) with the specified + XPath-like ``path``, e.g. + + ```xml + + ``` + + With ``negate`` the result of the assertion can be inverted.""" + + that: Literal["has_element_with_path"] = "has_element_with_path" + + path: str = Field( + ..., + description=has_element_with_path_path_description, + ) + + negate: typing.Union[bool, str] = Field( + False, + description=has_element_with_path_negate_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_n_elements_with_path_model_python_dict(AssertionModel): + r"""Asserts the XML output contains the specified number (``n``, optionally with ``delta``) of elements (or + tags) with the specified XPath-like ``path``. + + For example: + + ```xml + + ``` + + Alternatively to ``n`` and ``delta`` also the ``min`` and ``max`` attributes + can be used to specify the range of the expected number of occurences. + With ``negate`` the result of the assertion can be inverted.""" + + that: Literal["has_n_elements_with_path"] = "has_n_elements_with_path" + + path: str = Field( + ..., + description=has_n_elements_with_path_path_description, + ) + + n: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_n_elements_with_path_n_description, + ) + + delta: Annotated[ + typing.Union[int, str], BeforeValidator(check_bytes), BeforeValidator(check_non_negative_if_int) + ] = Field( + 0, + description=has_n_elements_with_path_delta_description, + ) + + min: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_n_elements_with_path_min_description, + ) + + max: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_n_elements_with_path_max_description, + ) + + negate: typing.Union[bool, str] = Field( + False, + description=has_n_elements_with_path_negate_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class is_valid_xml_model_python_dict(AssertionModel): + r"""Asserts the output is a valid XML file (e.g. ````).""" + + that: Literal["is_valid_xml"] = "is_valid_xml" + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class xml_element_model_python_dict(AssertionModel): + r"""Assert if the XML file contains element(s) or tag(s) with the specified + [XPath-like ``path``](https://lxml.de/xpathxslt.html). If ``n`` and ``delta`` + or ``min`` and ``max`` are given also the number of occurences is checked. + + ```xml + + + + + + ``` + + With ``negate="true"`` the outcome of the assertions wrt the precence and number + of ``path`` can be negated. If there are any sub assertions then check them against + + - the content of the attribute ``attribute`` + - the element's text if no attribute is given + + ```xml + + + + + + ``` + + Sub-assertions are not subject to the ``negate`` attribute of ``xml_element``. + If ``all`` is ``true`` then the sub assertions are checked for all occurences. + + Note that all other XML assertions can be expressed by this assertion (Galaxy + also implements the other assertions by calling this one).""" + + that: Literal["xml_element"] = "xml_element" + + path: str = Field( + ..., + description=xml_element_path_description, + ) + + attribute: typing.Optional[typing.Union[str]] = Field( + None, + description=xml_element_attribute_description, + ) + + all: typing.Union[bool, str] = Field( + False, + description=xml_element_all_description, + ) + + n: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=xml_element_n_description, + ) + + delta: Annotated[ + typing.Union[int, str], BeforeValidator(check_bytes), BeforeValidator(check_non_negative_if_int) + ] = Field( + 0, + description=xml_element_delta_description, + ) + + min: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=xml_element_min_description, + ) + + max: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=xml_element_max_description, + ) + + negate: typing.Union[bool, str] = Field( + False, + description=xml_element_negate_description, + ) + + children: typing.Optional["assertion_list"] = None + asserts: typing.Optional["assertion_list"] = None + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_json_property_with_text_model_python_dict(AssertionModel): + r"""Asserts the JSON document contains a property or key with the specified text (i.e. string) value. + + ```xml + + ```""" + + that: Literal["has_json_property_with_text"] = "has_json_property_with_text" + + property: str = Field( + ..., + description=has_json_property_with_text_property_description, + ) + + text: str = Field( + ..., + description=has_json_property_with_text_text_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_json_property_with_value_model_python_dict(AssertionModel): + r"""Asserts the JSON document contains a property or key with the specified JSON value. + + ```xml + + ```""" + + that: Literal["has_json_property_with_value"] = "has_json_property_with_value" + + property: str = Field( + ..., + description=has_json_property_with_value_property_description, + ) + + value: str = Field( + ..., + description=has_json_property_with_value_value_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_h5_attribute_model_python_dict(AssertionModel): + r"""Asserts HDF5 output contains the specified ``value`` for an attribute (``key``), e.g. + + ```xml + + ```""" + + that: Literal["has_h5_attribute"] = "has_h5_attribute" + + key: str = Field( + ..., + description=has_h5_attribute_key_description, + ) + + value: str = Field( + ..., + description=has_h5_attribute_value_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_h5_keys_model_python_dict(AssertionModel): + r"""Asserts the specified HDF5 output has the given keys.""" + + that: Literal["has_h5_keys"] = "has_h5_keys" + + keys: str = Field( + ..., + description=has_h5_keys_keys_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_archive_member_model_python_dict(AssertionModel): + r"""This tag allows to check if ``path`` is contained in a compressed file. + + The path is a regular expression that is matched against the full paths of the objects in + the compressed file (remember that "matching" means it is checked if a prefix of + the full path of an archive member is described by the regular expression). + Valid archive formats include ``.zip``, ``.tar``, and ``.tar.gz``. Note that + depending on the archive creation method: + + - full paths of the members may be prefixed with ``./`` + - directories may be treated as empty files + + ```xml + + ``` + + With ``n`` and ``delta`` (or ``min`` and ``max``) assertions on the number of + archive members matching ``path`` can be expressed. The following could be used, + e.g., to assert an archive containing n±1 elements out of which at least + 4 need to have a ``txt`` extension. + + ```xml + + + ``` + + In addition the tag can contain additional assertions as child elements about + the first member in the archive matching the regular expression ``path``. For + instance + + ```xml + + + + ``` + + If the ``all`` attribute is set to ``true`` then all archive members are subject + to the assertions. Note that, archive members matching the ``path`` are sorted + alphabetically. + + The ``negate`` attribute of the ``has_archive_member`` assertion only affects + the asserts on the presence and number of matching archive members, but not any + sub-assertions (which can offer the ``negate`` attribute on their own). The + check if the file is an archive at all, which is also done by the function, is + not affected.""" + + that: Literal["has_archive_member"] = "has_archive_member" + + path: str = Field( + ..., + description=has_archive_member_path_description, + ) + + all: typing.Union[bool, str] = Field( + False, + description=has_archive_member_all_description, + ) + + n: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_archive_member_n_description, + ) + + delta: Annotated[ + typing.Union[int, str], BeforeValidator(check_bytes), BeforeValidator(check_non_negative_if_int) + ] = Field( + 0, + description=has_archive_member_delta_description, + ) + + min: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_archive_member_min_description, + ) + + max: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_archive_member_max_description, + ) + + negate: typing.Union[bool, str] = Field( + False, + description=has_archive_member_negate_description, + ) + + children: typing.Optional["assertion_list"] = None + asserts: typing.Optional["assertion_list"] = None + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_size_model_python_dict(AssertionModel): + r"""Asserts the specified output has a size of the specified value + + Attributes size and value or synonyms though value is considered deprecated. + The size optionally allows for absolute (``delta``) difference.""" + + that: Literal["has_size"] = "has_size" + + value: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_size_value_description, + ) + + size: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_size_size_description, + ) + + delta: Annotated[ + typing.Union[int, str], BeforeValidator(check_bytes), BeforeValidator(check_non_negative_if_int) + ] = Field( + 0, + description=has_size_delta_description, + ) + + min: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_size_min_description, + ) + + max: Annotated[ + typing.Optional[typing.Union[str, int]], + BeforeValidator(check_bytes), + BeforeValidator(check_non_negative_if_int), + ] = Field( + None, + description=has_size_max_description, + ) + + negate: typing.Union[bool, str] = Field( + False, + description=has_size_negate_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_image_center_of_mass_model_python_dict(AssertionModel): + r"""Asserts the specified output is an image and has the specified center of mass. + + Asserts the output is an image and has a specific center of mass, + or has an Euclidean distance of ``eps`` or less to that point (e.g., + ````).""" + + that: Literal["has_image_center_of_mass"] = "has_image_center_of_mass" + + center_of_mass: Annotated[str, BeforeValidator(check_center_of_mass)] = Field( + ..., + description=has_image_center_of_mass_center_of_mass_description, + ) + + channel: typing.Optional[typing.Union[str, int]] = Field( + None, + description=has_image_center_of_mass_channel_description, + ) + + slice: typing.Optional[typing.Union[str, int]] = Field( + None, + description=has_image_center_of_mass_slice_description, + ) + + frame: typing.Optional[typing.Union[str, int]] = Field( + None, + description=has_image_center_of_mass_frame_description, + ) + + eps: Annotated[typing.Union[float, str], BeforeValidator(check_non_negative_if_set_permissive)] = Field( + 0.01, + description=has_image_center_of_mass_eps_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_image_channels_model_python_dict(AssertionModel): + r"""Asserts the output is an image and has a specific number of channels. + + The number of channels is plus/minus ``delta`` (e.g., ````). + + Alternatively the range of the expected number of channels can be specified by ``min`` and/or ``max``.""" + + that: Literal["has_image_channels"] = "has_image_channels" + + channels: Annotated[ + typing.Optional[typing.Union[str, int]], BeforeValidator(check_non_negative_if_set_permissive) + ] = Field( + None, + description=has_image_channels_channels_description, + ) + + delta: Annotated[typing.Union[int, str], BeforeValidator(check_non_negative_if_set_permissive)] = Field( + 0, + description=has_image_channels_delta_description, + ) + + min: Annotated[typing.Optional[typing.Union[str, int]], BeforeValidator(check_non_negative_if_set_permissive)] = ( + Field( + None, + description=has_image_channels_min_description, + ) + ) + + max: Annotated[typing.Optional[typing.Union[str, int]], BeforeValidator(check_non_negative_if_set_permissive)] = ( + Field( + None, + description=has_image_channels_max_description, + ) + ) + + negate: typing.Union[bool, str] = Field( + False, + description=has_image_channels_negate_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_image_depth_model_python_dict(AssertionModel): + r"""Asserts the output is an image and has a specific depth (number of slices). + + The depth is plus/minus ``delta`` (e.g., ````). + Alternatively the range of the expected depth can be specified by ``min`` and/or ``max``.""" + + that: Literal["has_image_depth"] = "has_image_depth" + + depth: Annotated[typing.Optional[typing.Union[str, int]], BeforeValidator(check_non_negative_if_set_permissive)] = ( + Field( + None, + description=has_image_depth_depth_description, + ) + ) + + delta: Annotated[typing.Union[int, str], BeforeValidator(check_non_negative_if_set_permissive)] = Field( + 0, + description=has_image_depth_delta_description, + ) + + min: Annotated[typing.Optional[typing.Union[str, int]], BeforeValidator(check_non_negative_if_set_permissive)] = ( + Field( + None, + description=has_image_depth_min_description, + ) + ) + + max: Annotated[typing.Optional[typing.Union[str, int]], BeforeValidator(check_non_negative_if_set_permissive)] = ( + Field( + None, + description=has_image_depth_max_description, + ) + ) + + negate: typing.Union[bool, str] = Field( + False, + description=has_image_depth_negate_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_image_frames_model_python_dict(AssertionModel): + r"""Asserts the output is an image and has a specific number of frames (number of time steps). + + The number of frames is plus/minus ``delta`` (e.g., ````). + Alternatively the range of the expected number of frames can be specified by ``min`` and/or ``max``.""" + + that: Literal["has_image_frames"] = "has_image_frames" + + frames: Annotated[ + typing.Optional[typing.Union[str, int]], BeforeValidator(check_non_negative_if_set_permissive) + ] = Field( + None, + description=has_image_frames_frames_description, + ) + + delta: Annotated[typing.Union[int, str], BeforeValidator(check_non_negative_if_set_permissive)] = Field( + 0, + description=has_image_frames_delta_description, + ) + + min: Annotated[typing.Optional[typing.Union[str, int]], BeforeValidator(check_non_negative_if_set_permissive)] = ( + Field( + None, + description=has_image_frames_min_description, + ) + ) + + max: Annotated[typing.Optional[typing.Union[str, int]], BeforeValidator(check_non_negative_if_set_permissive)] = ( + Field( + None, + description=has_image_frames_max_description, + ) + ) + + negate: typing.Union[bool, str] = Field( + False, + description=has_image_frames_negate_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_image_height_model_python_dict(AssertionModel): + r"""Asserts the output is an image and has a specific height (in pixels). + + The height is plus/minus ``delta`` (e.g., ````). + Alternatively the range of the expected height can be specified by ``min`` and/or ``max``.""" + + that: Literal["has_image_height"] = "has_image_height" + + height: Annotated[ + typing.Optional[typing.Union[str, int]], BeforeValidator(check_non_negative_if_set_permissive) + ] = Field( + None, + description=has_image_height_height_description, + ) + + delta: Annotated[typing.Union[int, str], BeforeValidator(check_non_negative_if_set_permissive)] = Field( + 0, + description=has_image_height_delta_description, + ) + + min: Annotated[typing.Optional[typing.Union[str, int]], BeforeValidator(check_non_negative_if_set_permissive)] = ( + Field( + None, + description=has_image_height_min_description, + ) + ) + + max: Annotated[typing.Optional[typing.Union[str, int]], BeforeValidator(check_non_negative_if_set_permissive)] = ( + Field( + None, + description=has_image_height_max_description, + ) + ) + + negate: typing.Union[bool, str] = Field( + False, + description=has_image_height_negate_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_image_mean_intensity_model_python_dict(AssertionModel): + r"""Asserts the output is an image and has a specific mean intensity value. + + The mean intensity value is plus/minus ``eps`` (e.g., ````). + Alternatively the range of the expected mean intensity value can be specified by ``min`` and/or ``max``.""" + + that: Literal["has_image_mean_intensity"] = "has_image_mean_intensity" + + channel: typing.Optional[typing.Union[str, int]] = Field( + None, + description=has_image_mean_intensity_channel_description, + ) + + slice: typing.Optional[typing.Union[str, int]] = Field( + None, + description=has_image_mean_intensity_slice_description, + ) + + frame: typing.Optional[typing.Union[str, int]] = Field( + None, + description=has_image_mean_intensity_frame_description, + ) + + mean_intensity: typing.Optional[typing.Union[float, str]] = Field( + None, + description=has_image_mean_intensity_mean_intensity_description, + ) + + eps: Annotated[typing.Union[float, str], BeforeValidator(check_non_negative_if_set_permissive)] = Field( + 0.01, + description=has_image_mean_intensity_eps_description, + ) + + min: typing.Optional[typing.Union[float, str]] = Field( + None, + description=has_image_mean_intensity_min_description, + ) + + max: typing.Optional[typing.Union[float, str]] = Field( + None, + description=has_image_mean_intensity_max_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_image_mean_object_size_model_python_dict(AssertionModel): + r"""Asserts the output is an image with labeled objects which have the specified mean size (number of pixels), + + The mean size is plus/minus ``eps`` (e.g., ````). + + The labels must be unique.""" + + that: Literal["has_image_mean_object_size"] = "has_image_mean_object_size" + + channel: typing.Optional[typing.Union[str, int]] = Field( + None, + description=has_image_mean_object_size_channel_description, + ) + + slice: typing.Optional[typing.Union[str, int]] = Field( + None, + description=has_image_mean_object_size_slice_description, + ) + + frame: typing.Optional[typing.Union[str, int]] = Field( + None, + description=has_image_mean_object_size_frame_description, + ) + + labels: typing.Optional[typing.Union[str, typing.List[int]]] = Field( + None, + description=has_image_mean_object_size_labels_description, + ) + + exclude_labels: typing.Optional[typing.Union[str, typing.List[int]]] = Field( + None, + description=has_image_mean_object_size_exclude_labels_description, + ) + + mean_object_size: Annotated[ + typing.Optional[typing.Union[float, str]], BeforeValidator(check_non_negative_if_set_permissive) + ] = Field( + None, + description=has_image_mean_object_size_mean_object_size_description, + ) + + eps: Annotated[typing.Union[float, str], BeforeValidator(check_non_negative_if_set_permissive)] = Field( + 0.01, + description=has_image_mean_object_size_eps_description, + ) + + min: Annotated[typing.Optional[typing.Union[float, str]], BeforeValidator(check_non_negative_if_set_permissive)] = ( + Field( + None, + description=has_image_mean_object_size_min_description, + ) + ) + + max: Annotated[typing.Optional[typing.Union[float, str]], BeforeValidator(check_non_negative_if_set_permissive)] = ( + Field( + None, + description=has_image_mean_object_size_max_description, + ) + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_image_n_labels_model_python_dict(AssertionModel): + r"""Asserts the output is an image and has the specified labels. + + Labels can be a number of labels or unique values (e.g., + ````). + + The primary usage of this assertion is to verify the number of objects in images with uniquely labeled objects.""" + + that: Literal["has_image_n_labels"] = "has_image_n_labels" + + channel: typing.Optional[typing.Union[str, int]] = Field( + None, + description=has_image_n_labels_channel_description, + ) + + slice: typing.Optional[typing.Union[str, int]] = Field( + None, + description=has_image_n_labels_slice_description, + ) + + frame: typing.Optional[typing.Union[str, int]] = Field( + None, + description=has_image_n_labels_frame_description, + ) + + labels: typing.Optional[typing.Union[str, typing.List[int]]] = Field( + None, + description=has_image_n_labels_labels_description, + ) + + exclude_labels: typing.Optional[typing.Union[str, typing.List[int]]] = Field( + None, + description=has_image_n_labels_exclude_labels_description, + ) + + n: Annotated[typing.Optional[typing.Union[str, int]], BeforeValidator(check_non_negative_if_set_permissive)] = ( + Field( + None, + description=has_image_n_labels_n_description, + ) + ) + + delta: Annotated[typing.Union[int, str], BeforeValidator(check_non_negative_if_set_permissive)] = Field( + 0, + description=has_image_n_labels_delta_description, + ) + + min: Annotated[typing.Optional[typing.Union[str, int]], BeforeValidator(check_non_negative_if_set_permissive)] = ( + Field( + None, + description=has_image_n_labels_min_description, + ) + ) + + max: Annotated[typing.Optional[typing.Union[str, int]], BeforeValidator(check_non_negative_if_set_permissive)] = ( + Field( + None, + description=has_image_n_labels_max_description, + ) + ) + + negate: typing.Union[bool, str] = Field( + False, + description=has_image_n_labels_negate_description, + ) + + +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class has_image_width_model_python_dict(AssertionModel): + r"""Asserts the output is an image and has a specific width (in pixels). + + The width is plus/minus ``delta`` (e.g., ````). + Alternatively the range of the expected width can be specified by ``min`` and/or ``max``.""" + + that: Literal["has_image_width"] = "has_image_width" + + width: Annotated[typing.Optional[typing.Union[str, int]], BeforeValidator(check_non_negative_if_set_permissive)] = ( + Field( + None, + description=has_image_width_width_description, + ) + ) + + delta: Annotated[typing.Union[int, str], BeforeValidator(check_non_negative_if_set_permissive)] = Field( + 0, + description=has_image_width_delta_description, + ) + + min: Annotated[typing.Optional[typing.Union[str, int]], BeforeValidator(check_non_negative_if_set_permissive)] = ( + Field( + None, + description=has_image_width_min_description, + ) + ) + + max: Annotated[typing.Optional[typing.Union[str, int]], BeforeValidator(check_non_negative_if_set_permissive)] = ( + Field( + None, + description=has_image_width_max_description, + ) + ) + + negate: typing.Union[bool, str] = Field( + False, + description=has_image_width_negate_description, + ) + + +any_assertion_model_python_dict = typing.Union[ + has_line_model_python_dict, + has_line_matching_model_python_dict, + has_n_lines_model_python_dict, + has_text_model_python_dict, + has_text_matching_model_python_dict, + not_has_text_model_python_dict, + has_n_columns_model_python_dict, + attribute_is_model_python_dict, + attribute_matches_model_python_dict, + element_text_model_python_dict, + element_text_is_model_python_dict, + element_text_matches_model_python_dict, + has_element_with_path_model_python_dict, + has_n_elements_with_path_model_python_dict, + is_valid_xml_model_python_dict, + xml_element_model_python_dict, + has_json_property_with_text_model_python_dict, + has_json_property_with_value_model_python_dict, + has_h5_attribute_model_python_dict, + has_h5_keys_model_python_dict, + has_archive_member_model_python_dict, + has_size_model_python_dict, + has_image_center_of_mass_model_python_dict, + has_image_channels_model_python_dict, + has_image_depth_model_python_dict, + has_image_frames_model_python_dict, + has_image_height_model_python_dict, + has_image_mean_intensity_model_python_dict, + has_image_mean_object_size_model_python_dict, + has_image_n_labels_model_python_dict, + has_image_width_model_python_dict, +] + +assertion_list_python = RootModel[typing.List[any_assertion_model_python_dict]] diff --git a/lib/galaxy/tool_util/verify/asserts/__init__.py b/lib/galaxy/tool_util/verify/asserts/__init__.py index 532692c6198a..1f812400a396 100644 --- a/lib/galaxy/tool_util/verify/asserts/__init__.py +++ b/lib/galaxy/tool_util/verify/asserts/__init__.py @@ -6,8 +6,10 @@ ) from tempfile import NamedTemporaryFile from typing import ( + Any, Callable, Dict, + List, Tuple, ) @@ -41,6 +43,18 @@ assertion_functions: Dict[str, Callable] = {k: v[1] for (k, v) in assertion_module_and_functions.items()} +def parse_xml_assertions(assertion_els: list) -> List[Dict[str, Any]]: + python_dict_assertions: List[Dict[str, Any]] = [] + for raw_assert in assertion_els: + as_dict = {"that": raw_assert["tag"], **raw_assert.get("attributes", {})} + children = raw_assert.get("children") + if children: + as_dict["children"] = parse_xml_assertions(children) + python_dict_assertions.append(as_dict) + + return python_dict_assertions + + def verify_assertions(data: bytes, assertion_description_list: list, decompress: bool = False): """This function takes a list of assertions and a string to check these assertions against.""" diff --git a/lib/galaxy/tool_util/verify/codegen.py b/lib/galaxy/tool_util/verify/codegen.py index 18132fb078e8..98bef3978bdc 100644 --- a/lib/galaxy/tool_util/verify/codegen.py +++ b/lib/galaxy/tool_util/verify/codegen.py @@ -101,6 +101,15 @@ def check_non_negative_if_set(v: typing.Any): return v +def check_non_negative_if_set_permissive(v: typing.Any): + if v is not None: + try: + assert float(v) >= 0.0 + except TypeError: + raise AssertionError(f"Invalid type found {v}") + return v + + def check_non_negative_if_int(v: typing.Any): if v is not None and isinstance(v, int): assert typing.cast(int, v) >= 0 @@ -168,6 +177,44 @@ class assertion_dict(AssertionModel): assertions = typing.Union[assertion_list, assertion_dict] + + +{% for assertion in assertions %} +# a version of these validators for parsing more directly from XML where more of the types +# can be strings - so the typing should match the Python types of the assertion functions. +class {{assertion.name}}_model_python_dict(AssertionModel): + r\"\"\"{{ assertion.docstring }}\"\"\" + that: Literal["{{assertion.name}}"] = "{{assertion.name}}" +{% for parameter in assertion.parameters %} +{% if not parameter.is_deprecated %} + {{ parameter.name }}: {{ parameter.python_type_str }} = Field( + {{ parameter.field_default_str }}, + description={{ assertion.name }}_{{ parameter.name }}_description, + ) +{% endif %} +{% endfor %} +{% if assertion.children in ["required", "allowed"] %} + children: typing.Optional["assertion_list"] = None + asserts: typing.Optional["assertion_list"] = None + +{% if assertion.children == "required" %} + @model_validator(mode='before') + @classmethod + def validate_children(self, data: typing.Any): + if isinstance(data, dict) and 'children' not in data and 'asserts' not in data: + raise ValueError("At least one of 'children' or 'asserts' must be specified for this assertion type.") + return data +{% endif %} +{% endif %} +{% endfor %} + +any_assertion_model_python_dict = typing.Union[ +{% for assertion in assertions %} + {{assertion.name}}_model_python_dict, +{% endfor %} +] + +assertion_list_python = RootModel[typing.List[any_assertion_model_python_dict]] """ @@ -321,6 +368,24 @@ def type_str(self) -> str: return raw_type_str + @property + def python_type_str(self) -> str: + raw_type_str = as_type_str(self.type, ignore_json_type=True) + validators = self.validators[:] + if self.xml_type_str == "Bytes": + validators.append("check_bytes") + validators.append("check_non_negative_if_int") + if len(validators) > 0: + validator_strs = [] + for v in validators: + if v == "check_non_negative_if_set": + v = "check_non_negative_if_set_permissive" + validator_strs.append(f"BeforeValidator({v})") + validation_str = ",".join(validator_strs) + return f"Annotated[{raw_type_str}, {validation_str}]" + + return raw_type_str + @property def xml_type_str(self) -> str: return as_xml_type(self.type) @@ -395,11 +460,11 @@ def as_xml_type(target_type) -> str: return "xs:string" -def as_type_str(target_type): +def as_type_str(target_type, ignore_json_type: bool = False): if get_origin(target_type) is Annotated: args = get_args(target_type) if len(args) > 1: - if args[1].json_type: + if args[1].json_type and not ignore_json_type: return args[1].json_type return as_type_str(args[0]) diff --git a/lib/galaxy/tool_util/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd index cd3fa9df84cb..536af8c2c65e 100644 --- a/lib/galaxy/tool_util/xsd/galaxy.xsd +++ b/lib/galaxy/tool_util/xsd/galaxy.xsd @@ -8075,7 +8075,6 @@ favour of a ``has_size`` assertion. - @@ -8086,7 +8085,6 @@ favour of a ``has_size`` assertion. - """, """""", """""", + """""", + """""", ] invalid_assertions = [ @@ -227,6 +235,15 @@ def test_valid_xsd(tmp_path): assert ret == 0, f"{assertion_xml} failed to validate" +def test_valid_xsd_to_ptyhon(): + for assertion_xml in valid_xml_assertions: + el = etree.fromstring(f"{assertion_xml}") + assertions_raw = parse_assert_list_from_elem(el) + assert assertions_raw + as_dicts = parse_xml_assertions(assertions_raw) + assertion_list_python.model_validate(as_dicts) + + @skip_unless_executable("xmllint") def test_invalid_xsd(tmp_path): for assertion_xml in invalid_xml_assertions: diff --git a/test/unit/tool_util/verify/test_asserts.py b/test/unit/tool_util/verify/test_asserts.py index 2f2e9648f64b..acf9b59d4bd4 100644 --- a/test/unit/tool_util/verify/test_asserts.py +++ b/test/unit/tool_util/verify/test_asserts.py @@ -9,7 +9,7 @@ except ImportError: h5py = None -from galaxy.tool_util.parser.xml import __parse_assert_list_from_elem +from galaxy.tool_util.parser.xml import parse_assert_list_from_elem from galaxy.tool_util.verify import asserts from galaxy.util import parse_xml_string @@ -1323,7 +1323,7 @@ def test_has_h5_attribute_failure(): def run_assertions(assertion_xml: str, data, decompress=False) -> Tuple: assertion = parse_xml_string(assertion_xml) - assertion_description = __parse_assert_list_from_elem(assertion) + assertion_description = parse_assert_list_from_elem(assertion) assert assertion_description try: asserts.verify_assertions(data, assertion_description, decompress=decompress)