Skip to content

Commit

Permalink
Hook up validators models to validator runtime.
Browse files Browse the repository at this point in the history
  • Loading branch information
jmchilton committed Oct 3, 2024
1 parent 214a761 commit f9216e6
Show file tree
Hide file tree
Showing 8 changed files with 328 additions and 297 deletions.
3 changes: 2 additions & 1 deletion lib/galaxy/tool_util/parser/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

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

if TYPE_CHECKING:
Expand Down Expand Up @@ -502,7 +503,7 @@ def parse_sanitizer_elem(self):
"""
return None

def parse_validator_elems(self):
def parse_validators(self) -> List[AnyValidatorModel]:
"""Return an XML description of sanitizers. This is a stop gap
until we can rework galaxy.tools.parameters.validation to not
explicitly depend on XML.
Expand Down
110 changes: 84 additions & 26 deletions lib/galaxy/tool_util/parser/parameter_validators.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import os.path
from typing import (
Any,
cast,
Expand All @@ -11,10 +12,11 @@
BaseModel,
ConfigDict,
Field,
PrivateAttr,
)
from typing_extensions import (
get_args,
Annotated,
get_args,
Literal,
)

Expand All @@ -23,6 +25,7 @@
Element,
)


class ValidationArgument:
doc: str
xml_body: bool
Expand All @@ -41,9 +44,7 @@ def __init__(

Negate = Annotated[
bool,
ValidationArgument(
"Negates the result of the validator."
),
ValidationArgument("Negates the result of the validator."),
]
NEGATE_DEFAULT = False
SPLIT_DEFAULT = "\t"
Expand Down Expand Up @@ -74,18 +75,30 @@ class StrictModel(BaseModel):
model_config = ConfigDict(extra="forbid")



class ParameterValidatorModel(StrictModel):
type: ValidatorType
message: Annotated[Optional[str], ValidationArgument(
"""The error message displayed on the tool form if validation fails. A placeholder string ``%s`` will be repaced by the ``value``"""
)] = None
message: Annotated[
Optional[str],
ValidationArgument(
"""The error message displayed on the tool form if validation fails. A placeholder string ``%s`` will be repaced by the ``value``"""
),
] = None
_static: bool = PrivateAttr(False)
_deprecated: bool = PrivateAttr(False)


class StaticValidatorModel(ParameterValidatorModel):
_static: bool = PrivateAttr(True)

def validate(self, v: Any) -> None:
...


class ExpressionParameterValidatorModel(ParameterValidatorModel):
"""Check if a one line python expression given expression evaluates to True.
The expression is given is the content of the validator tag."""

type: Literal["expression"]
negate: Negate = NEGATE_DEFAULT
expression: Annotated[str, ValidationArgument("Python expression to validate.", xml_body=True)]
Expand All @@ -97,26 +110,57 @@ class RegexParameterValidatorModel(ParameterValidatorModel):
``$`` at the end of the expression. The expression is given is the content
of the validator tag. Note that for ``selects`` each option is checked
separately."""

type: Literal["regex"]
negate: Negate = NEGATE_DEFAULT
regex: Annotated[str, ValidationArgument("Regular expression to validate against.", xml_body=True)]
expression: Annotated[str, ValidationArgument("Regular expression to validate against.", xml_body=True)]


class InRangeParameterValidatorModel(ParameterValidatorModel):
class InRangeParameterValidatorModel(StaticValidatorModel):
type: Literal["in_range"]
min: Optional[float] = None
max: Optional[float] = None
min: Optional[Union[float, int]] = None
max: Optional[Union[float, int]] = None
exclude_min: bool = False
exclude_max: bool = False
negate: Negate = NEGATE_DEFAULT


class LengthParameterValidatorModel(ParameterValidatorModel):
def validate(self, value: Any):
if isinstance(value, (int, float)):
validates = True
if self.min is not None and value == self.min and self.exclude_min:
validates = False
elif self.min is not None and value < self.min:
validates = False
elif self.max is not None and value == self.max and self.exclude_max:
validates = False
if self.max is not None and value > self.max:
validates = False
handle_negate(validates, self.negate, self.message)


class LengthParameterValidatorModel(StaticValidatorModel):
type: Literal["length"]
min: Optional[int] = None
max: Optional[int] = None
negate: Negate = NEGATE_DEFAULT

def validate(self, value: Any):
if isinstance(value, str):
length = len(value)
validates = True
if self.min is not None and length < self.min:
validates = False
if self.max is not None and length > self.max:
validates = False
handle_negate(validates, self.negate, self.message)


def handle_negate(result: bool, negate: bool, message: str):
if negate:
result = not result
if not result:
raise ValueError(message)


class MetadataParameterValidatorModel(ParameterValidatorModel):
type: Literal["metadata"]
Expand Down Expand Up @@ -176,8 +220,8 @@ class DatasetMetadataNotInDataTableParameterValidatorModel(ParameterValidatorMod
class DatasetMetadataInRangeParameterValidatorModel(ParameterValidatorModel):
type: Literal["dataset_metadata_in_range"]
metadata_name: str
min: Optional[float] = None
max: Optional[float] = None
min: Optional[Union[float, int]] = None
max: Optional[Union[float, int]] = None
exclude_min: bool = False
exclude_max: bool = False
negate: Negate = NEGATE_DEFAULT
Expand Down Expand Up @@ -210,6 +254,7 @@ class DatasetMetadataInFileParameterValidatorModel(ParameterValidatorModel):
line_startswith: Optional[str] = None
split: str = SPLIT_DEFAULT
negate: Negate = NEGATE_DEFAULT
_deprecated: bool = PrivateAttr(True)


AnyValidatorModel = Annotated[
Expand Down Expand Up @@ -245,6 +290,15 @@ def parse_xml_validators(input_elem: Element) -> List[AnyValidatorModel]:
return models


def static_validators(validator_models: List[AnyValidatorModel]) -> List[AnyValidatorModel]:
static_validators = []
for validator_model in validator_models:
print(validator_model._static)
if validator_model._static:
static_validators.append(validator_model)
return static_validators


def parse_xml_validator(validator_el: Element) -> AnyValidatorModel:
_type = validator_el.get("type")
if _type is None:
Expand All @@ -265,14 +319,14 @@ def parse_xml_validator(validator_el: Element) -> AnyValidatorModel:
type="regex",
message=_parse_message(validator_el),
negate=_parse_negate(validator_el),
regex=validator_el.text,
expression=validator_el.text,
)
elif validator_type == "in_range":
return InRangeParameterValidatorModel(
type="in_range",
message=_parse_message(validator_el),
min=_parse_float(validator_el, "min"),
max=_parse_float(validator_el, "max"),
min=_parse_number(validator_el, "min"),
max=_parse_number(validator_el, "max"),
exclude_min=_parse_bool(validator_el, "exclude_min", False),
exclude_max=_parse_bool(validator_el, "exclude_max", False),
negate=_parse_negate(validator_el),
Expand Down Expand Up @@ -354,8 +408,8 @@ def parse_xml_validator(validator_el: Element) -> AnyValidatorModel:
type="dataset_metadata_in_range",
message=_parse_message(validator_el),
metadata_name=validator_el.get("metadata_name"),
min=_parse_float(validator_el, "min"),
max=_parse_float(validator_el, "max"),
min=_parse_number(validator_el, "min"),
max=_parse_number(validator_el, "max"),
exclude_min=_parse_bool(validator_el, "exclude_min", False),
exclude_max=_parse_bool(validator_el, "exclude_max", False),
negate=_parse_negate(validator_el),
Expand Down Expand Up @@ -383,10 +437,12 @@ def parse_xml_validator(validator_el: Element) -> AnyValidatorModel:
negate=_parse_negate(validator_el),
)
elif validator_type == "dataset_metadata_in_file":
filename = validator_el.get("filename")
assert os.path.exists(filename), f"File {filename} specified by the 'filename' attribute not found"
return DatasetMetadataInFileParameterValidatorModel(
type="dataset_metadata_in_file",
message=_parse_message(validator_el),
filename=validator_el.get("filename"),
filename=filename,
metadata_name=validator_el.get("metadata_name"),
metadata_column=_parse_metadata_column(validator_el),
line_startswith=validator_el.get("line_startswith"),
Expand All @@ -402,17 +458,19 @@ def _parse_message(xml_el: Element) -> Optional[str]:
return message


def _parse_float(xml_el: Element, attribute: str) -> Optional[float]:
def _parse_int(xml_el: Element, attribute: str) -> Optional[int]:
raw_value = xml_el.get(attribute)
if raw_value:
return float(raw_value)
return int(raw_value)
else:
return None


def _parse_int(xml_el: Element, attribute: str) -> Optional[int]:
def _parse_number(xml_el: Element, attribute: str) -> Optional[Union[float, int]]:
raw_value = xml_el.get(attribute)
if raw_value:
if raw_value and "." in raw_value or "e" in raw_value:
return float(raw_value)
elif raw_value:
return int(raw_value)
else:
return None
Expand Down
8 changes: 6 additions & 2 deletions lib/galaxy/tool_util/parser/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@
ToolOutputCollection,
ToolOutputCollectionStructure,
)
from .parameter_validators import (
AnyValidatorModel,
parse_xml_validators,
)
from .stdio import (
aggressive_error_checks,
error_on_exit_code,
Expand Down Expand Up @@ -1339,8 +1343,8 @@ def parse_help(self):
def parse_sanitizer_elem(self):
return self.input_elem.find("sanitizer")

def parse_validator_elems(self):
return self.input_elem.findall("validator")
def parse_validators(self) -> List[AnyValidatorModel]:
return parse_xml_validators(self.input_elem)

def parse_dynamic_options(self) -> Optional[XmlDynamicOptions]:
"""Return a XmlDynamicOptions to describe dynamic options if options elem is available."""
Expand Down
53 changes: 53 additions & 0 deletions lib/galaxy/tool_util/unittest_utils/sample_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,56 @@
</macros>
"""
)

VALID_XML_VALIDATORS = [
"""<validator type="empty_dataset" />""",
"""<validator type="empty_dataset" message="foobar" />""",
"""<validator type="empty_dataset" message="foobar" negate="true" />""",
"""<validator type="expression">value == 7</validator>""",
"""<validator type="regex">mycoolexpression</validator>""",
"""<validator type="in_range" min="3.1" max="3.8" />""",
"""<validator type="in_range" min="3.1" />""",
"""<validator type="in_range" min="3.1" max="3.8" exclude_min="false" exclude_max="true" />""",
"""<validator type="length" min="3" />""",
"""<validator type="length" max="7" />""",
"""<validator type="length" min="2" max="7" negate="true" />""",
"""<validator type="empty_field" />""",
"""<validator type="empty_extra_files_path" />""",
"""<validator type="no_options" />""",
"""<validator type="unspecified_build" />""",
"""<validator type="dataset_ok_validator" />""",
"""<validator type="metadata" check="foo, bar" />""",
"""<validator type="metadata" skip="name, dbkey" />""",
"""<validator type="dataset_metadata_equal" metadata_name="foobar" value="moocow" />""",
"""<validator type="dataset_metadata_equal" metadata_name="foobar" value_json="null" />""",
"""<validator type="dataset_metadata_in_range" metadata_name="foobar" min="4.5" max="7.8" />""",
"""<validator type="dataset_metadata_in_range" metadata_name="foobar" min="4.5" max="7.8" include_min="true" />""",
"""<validator type="dataset_metadata_in_data_table" metadata_name="foobar" metadata_column="3" table_name="mycooltable" />""",
"""<validator type="dataset_metadata_not_in_data_table" metadata_name="foobar" metadata_column="3" table_name="mycooltable" />""",
"""<validator type="value_in_data_table" metadata_column="3" table_name="mycooltable" />""",
"""<validator type="value_in_data_table" table_name="mycooltable" />""",
"""<validator type="value_not_in_data_table" metadata_column="3" table_name="mycooltable" />""",
"""<validator type="value_not_in_data_table" table_name="mycooltable" />""",
]

INVALID_XML_VALIDATORS = [
"""<validator type="unknown" />""",
"""<validator type="empty_datasetx" />""",
"""<validator type="expression" />""",
"""<validator type="empty_dataset" message="foobar" negate="NOTABOOLVALUE" />""",
"""<validator type="regex" />""",
"""<validator type="in_range" min="3.1" max="3.8" exclude_min="false" exclude_max="foobar" />""",
"""<validator type="in_range" min="notanumber" max="3.8" />""",
"""<validator type="length" min="notanumber" />""",
"""<validator type="length" max="notanumber" />""",
"""<validator type="length" min="2" max="7" negate="notabool" />""",
"""<validator type="dataset_metadata_equal" />""",
"""<validator type="dataset_metadata_equal" metadaata_name="foobar" />""",
"""<validator type="dataset_metadata_equal" metadaata_name="foobar" value_json="undefined" />""",
"""<validator type="dataset_metadata_in_range" metadata_name="foobar" min="4.5" max="7.8" include_min="notabool" />"""
"""<validator type="dataset_metadata_in_range" min="4.5" max="7.8" />"""
"""<validator type="dataset_metadata_in_data_table" metadata_name="foobar" metadata_column="3" />""",
"""<validator type="dataset_metadata_in_data_table" metadata_column="3" table_name="mycooltable" />""",
"""<validator type="dataset_metadata_not_in_data_table" metadata_name="foobar" metadata_column="3" />""",
"""<validator type="dataset_metadata_not_in_data_table" metadata_column="3" table_name="mycooltable" />""",
]
9 changes: 5 additions & 4 deletions lib/galaxy/tools/parameters/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,7 @@ def __init__(self, tool, input_source, context=None):
self.sanitizer = ToolParameterSanitizer.from_element(sanitizer_elem)
else:
self.sanitizer = None
self.validators = []
for elem in input_source.parse_validator_elems():
self.validators.append(validation.Validator.from_element(self, elem))
self.validators = validation.to_validators(tool.app, input_source.parse_validators())

@property
def visible(self) -> bool:
Expand Down Expand Up @@ -2467,7 +2465,10 @@ def from_json(self, value, trans, other_values=None):
rval = value
elif isinstance(value, MutableMapping) and "src" in value and "id" in value:
if value["src"] == "hdca":
rval = cast(HistoryDatasetCollectionAssociation, src_id_to_item(sa_session=trans.sa_session, value=value, security=trans.security))
rval = cast(
HistoryDatasetCollectionAssociation,
src_id_to_item(sa_session=trans.sa_session, value=value, security=trans.security),
)
elif isinstance(value, list):
if len(value) > 0:
value = value[0]
Expand Down
Loading

0 comments on commit f9216e6

Please sign in to comment.