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)