diff --git a/lib/galaxy/tool_util/parser/interface.py b/lib/galaxy/tool_util/parser/interface.py index a52e39e0200b..aeebd6142dbf 100644 --- a/lib/galaxy/tool_util/parser/interface.py +++ b/lib/galaxy/tool_util/parser/interface.py @@ -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: @@ -509,7 +510,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. diff --git a/lib/galaxy/tool_util/parser/parameter_validators.py b/lib/galaxy/tool_util/parser/parameter_validators.py index 5aa140542c16..b256aafce22f 100644 --- a/lib/galaxy/tool_util/parser/parameter_validators.py +++ b/lib/galaxy/tool_util/parser/parameter_validators.py @@ -1,4 +1,5 @@ import json +import os.path from typing import ( Any, cast, @@ -13,8 +14,8 @@ Field, ) from typing_extensions import ( - get_args, Annotated, + get_args, Literal, ) @@ -23,6 +24,7 @@ Element, ) + class ValidationArgument: doc: str xml_body: bool @@ -41,9 +43,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" @@ -74,18 +74,21 @@ 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 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)] @@ -97,9 +100,10 @@ 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): @@ -109,7 +113,7 @@ class InRangeParameterValidatorModel(ParameterValidatorModel): exclude_min: bool = False exclude_max: bool = False negate: Negate = NEGATE_DEFAULT - + class LengthParameterValidatorModel(ParameterValidatorModel): type: Literal["length"] @@ -265,7 +269,7 @@ 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( @@ -383,10 +387,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"), diff --git a/lib/galaxy/tool_util/parser/xml.py b/lib/galaxy/tool_util/parser/xml.py index c5f39568447b..b93eaa730d23 100644 --- a/lib/galaxy/tool_util/parser/xml.py +++ b/lib/galaxy/tool_util/parser/xml.py @@ -72,6 +72,10 @@ ToolOutputCollection, ToolOutputCollectionStructure, ) +from .parameter_validators import ( + AnyValidatorModel, + parse_xml_validators, +) from .stdio import ( aggressive_error_checks, error_on_exit_code, @@ -1340,8 +1344,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.""" diff --git a/lib/galaxy/tool_util/unittest_utils/sample_data.py b/lib/galaxy/tool_util/unittest_utils/sample_data.py index d4b6ddb6f027..e9b19283401a 100644 --- a/lib/galaxy/tool_util/unittest_utils/sample_data.py +++ b/lib/galaxy/tool_util/unittest_utils/sample_data.py @@ -17,3 +17,56 @@ """ ) + +VALID_XML_VALIDATORS = [ + """""", + """""", + """""", + """value == 7""", + """mycoolexpression""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", +] + +INVALID_XML_VALIDATORS = [ + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""", + """""" + """""" + """""", + """""", + """""", + """""", +] diff --git a/lib/galaxy/tools/parameters/basic.py b/lib/galaxy/tools/parameters/basic.py index e79a6c2a1028..27aaa8d97fae 100644 --- a/lib/galaxy/tools/parameters/basic.py +++ b/lib/galaxy/tools/parameters/basic.py @@ -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: @@ -2486,7 +2484,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] diff --git a/lib/galaxy/tools/parameters/validation.py b/lib/galaxy/tools/parameters/validation.py index 6334fd95f8b8..3728bb7ecc9b 100644 --- a/lib/galaxy/tools/parameters/validation.py +++ b/lib/galaxy/tools/parameters/validation.py @@ -3,9 +3,11 @@ """ import abc -import json import logging -import os.path +from typing import ( + List, + Optional, +) import regex @@ -13,6 +15,10 @@ model, util, ) +from galaxy.tool_util.parser.parameter_validators import ( + AnyValidatorModel, + parse_xml_validators as parse_xml_validators_models, +) log = logging.getLogger(__name__) @@ -24,27 +30,7 @@ class Validator(abc.ABC): requires_dataset_metadata = False - @classmethod - def from_element(cls, param, elem): - """ - Initialize the appropriate Validator class - - example call `validation.Validator.from_element(ToolParameter_object, Validator_object)` - - needs to be implemented in the subclasses and should return the - corresponding Validator object by a call to `cls( ... )` which calls the - `__init__` method of the corresponding validator - - param cls the Validator class - param param the element to be evaluated (which contains the validator) - param elem the validator element - return an object of a Validator subclass that corresponds to the type attribute of the validator element - """ - _type = elem.get("type") - assert _type is not None, "Required 'type' attribute missing from validator" - return validator_types[_type].from_element(param, elem) - - def __init__(self, message, negate=False): + def __init__(self, message: Optional[str], negate: bool = False): self.message = message self.negate = util.asbool(negate) super().__init__() @@ -84,13 +70,9 @@ class RegexValidator(Validator): Validator that evaluates a regular expression """ - @classmethod - def from_element(cls, param, elem): - return cls(elem.get("message"), elem.text, elem.get("negate", "false")) - - def __init__(self, message, expression, negate): + def __init__(self, message: Optional[str], expression: str, negate: bool): if message is None: - message = f"Value '%s' does {'not ' if negate == 'false' else ''}match regular expression '{expression.replace('%', '%%')}'" + message = f"Value '%s' does {'not ' if not negate else ''}match regular expression '{expression.replace('%', '%%')}'" super().__init__(message, negate) # Compile later. RE objects used to not be thread safe. Not sure about # the sre module. @@ -109,13 +91,9 @@ class ExpressionValidator(Validator): Validator that evaluates a python expression using the value """ - @classmethod - def from_element(cls, param, elem): - return cls(elem.get("message"), elem.text, elem.get("negate", "false")) - def __init__(self, message, expression, negate): if message is None: - message = f"Value '%s' does not evaluate to {'True' if negate == 'false' else 'False'} for '{expression}'" + message = f"Value '%s' does not evaluate to {'True' if not negate else 'False'} for '{expression}'" super().__init__(message, negate) self.expression = expression # Save compiled expression, code objects are thread safe (right?) @@ -134,18 +112,15 @@ class InRangeValidator(ExpressionValidator): Validator that ensures a number is in a specified range """ - @classmethod - def from_element(cls, param, elem): - return cls( - elem.get("message"), - elem.get("min"), - elem.get("max"), - elem.get("exclude_min", "false"), - elem.get("exclude_max", "false"), - elem.get("negate", "false"), - ) - - def __init__(self, message, range_min, range_max, exclude_min=False, exclude_max=False, negate=False): + def __init__( + self, + message: Optional[str] = None, + min: Optional[float] = None, + max: Optional[float] = None, + exclude_min: bool = False, + exclude_max: bool = False, + negate: bool = False, + ): """ When the optional exclude_min and exclude_max attributes are set to true, the range excludes the end points (i.e., min < value < max), @@ -153,10 +128,10 @@ def __init__(self, message, range_min, range_max, exclude_min=False, exclude_max (1.e., min <= value <= max). Combinations of exclude_min and exclude_max values are allowed. """ - self.min = range_min if range_min is not None else "-inf" - self.exclude_min = util.asbool(exclude_min) - self.max = range_max if range_max is not None else "inf" - self.exclude_max = util.asbool(exclude_max) + self.min = str(min) if min is not None else "-inf" + self.exclude_min = exclude_min + self.max = str(max) if max is not None else "inf" + self.exclude_max = exclude_max assert float(self.min) <= float(self.max), "min must be less than or equal to max" # Remove unneeded 0s and decimal from floats to make message pretty. op1 = "<=" @@ -167,7 +142,7 @@ def __init__(self, message, range_min, range_max, exclude_min=False, exclude_max op2 = "<" expression = f"float('{self.min}') {op1} float(value) {op2} float('{self.max}')" if message is None: - message = f"Value ('%s') must {'not ' if negate == 'true' else ''}fulfill {expression}" + message = f"Value ('%s') must {'not ' if negate else ''}fulfill {expression}" super().__init__(message, expression, negate) @@ -176,14 +151,10 @@ class LengthValidator(InRangeValidator): Validator that ensures the length of the provided string (value) is in a specific range """ - @classmethod - def from_element(cls, param, elem): - return cls(elem.get("message"), elem.get("min"), elem.get("max"), elem.get("negate", "false")) - - def __init__(self, message, length_min, length_max, negate): + def __init__(self, message: Optional[str], min: float, max: float, negate: bool): if message is None: - message = f"Must {'not ' if negate == 'true' else ''}have length of at least {length_min} and at most {length_max}" - super().__init__(message, range_min=length_min, range_max=length_max, negate=negate) + message = f"Must {'not ' if negate else ''}have length of at least {min} and at most {max}" + super().__init__(message, min=min, max=max, negate=negate) def validate(self, value, trans=None): if value is None: @@ -196,16 +167,13 @@ class DatasetOkValidator(Validator): Validator that checks if a dataset is in an 'ok' state """ - @classmethod - def from_element(cls, param, elem): - negate = elem.get("negate", "false") - message = elem.get("message") + def __init__(self, message: Optional[str], negate: bool = False): if message is None: - if negate == "false": + if not negate: message = "The selected dataset is still being generated, select another dataset or wait until it is completed" else: message = "The selected dataset must not be in state OK" - return cls(message, negate) + return super().__init__(message, negate=negate) def validate(self, value, trans=None): if value: @@ -217,13 +185,10 @@ class DatasetEmptyValidator(Validator): Validator that checks if a dataset has a positive file size. """ - @classmethod - def from_element(cls, param, elem): - message = elem.get("message") - negate = elem.get("negate", "false") - if not message: - message = f"The selected dataset is {'non-' if negate == 'true' else ''}empty, this tool expects {'non-' if negate == 'false' else ''}empty files." - return cls(message, negate) + def __init__(self, message: Optional[str], negate: bool = False): + if message is None: + message = f"The selected dataset is {'non-' if negate else ''}empty, this tool expects {'non-' if not negate else ''}empty files." + return super().__init__(message, negate=negate) def validate(self, value, trans=None): if value: @@ -235,13 +200,10 @@ class DatasetExtraFilesPathEmptyValidator(Validator): Validator that checks if a dataset's extra_files_path exists and is not empty. """ - @classmethod - def from_element(cls, param, elem): - message = elem.get("message") - negate = elem.get("negate", "false") - if not message: - message = f"The selected dataset's extra_files_path directory is {'non-' if negate == 'true' else ''}empty or does {'not ' if negate == 'false' else ''}exist, this tool expects {'non-' if negate == 'false' else ''}empty extra_files_path directories associated with the selected input." - return cls(message, negate) + def __init__(self, message: Optional[str], negate: bool = False): + if message is None: + message = f"The selected dataset's extra_files_path directory is {'non-' if negate else ''}empty or does {'not ' if not negate else ''}exist, this tool expects {'non-' if not negate else ''}empty extra_files_path directories associated with the selected input." + return super().__init__(message, negate=negate) def validate(self, value, trans=None): if value: @@ -255,25 +217,26 @@ class MetadataValidator(Validator): requires_dataset_metadata = True - @classmethod - def from_element(cls, param, elem): - message = elem.get("message") - return cls( - message=message, check=elem.get("check", ""), skip=elem.get("skip", ""), negate=elem.get("negate", "false") - ) - - def __init__(self, message=None, check="", skip="", negate="false"): + def __init__( + self, + message: Optional[str] = None, + check: Optional[List[str]] = None, + skip: Optional[List[str]] = None, + negate: bool = False, + ): + print(check) + print(skip) if not message: - if not util.asbool(negate): + if not negate: message = "Metadata '%s' missing, click the pencil icon in the history item to edit / save the metadata attributes" else: - if check != "": - message = f"At least one of the checked metadata '{check}' is set, click the pencil icon in the history item to edit / save the metadata attributes" - elif skip != "": - message = f"At least one of the non skipped metadata '{skip}' is set, click the pencil icon in the history item to edit / save the metadata attributes" + if check: + message = f"""At least one of the checked metadata '{",".join(check)}' is set, click the pencil icon in the history item to edit / save the metadata attributes""" + elif skip: + message = f"""At least one of the non skipped metadata '{",".join(skip)}' is set, click the pencil icon in the history item to edit / save the metadata attributes""" super().__init__(message, negate) - self.check = check.split(",") if check else None - self.skip = skip.split(",") if skip else None + self.check = check + self.skip = skip def validate(self, value, trans=None): if value: @@ -302,16 +265,6 @@ def __init__(self, metadata_name=None, value=None, message=None, negate="false") self.metadata_name = metadata_name self.value = value - @classmethod - def from_element(cls, param, elem): - value = elem.get("value", None) or json.loads(elem.get("value_json", "null")) - return cls( - metadata_name=elem.get("metadata_name", None), - value=value, - message=elem.get("message", None), - negate=elem.get("negate", "false"), - ) - def validate(self, value, trans=None): if value: metadata_value = getattr(value.metadata, self.metadata_name) @@ -325,13 +278,10 @@ class UnspecifiedBuildValidator(Validator): requires_dataset_metadata = True - @classmethod - def from_element(cls, param, elem): - message = elem.get("message") - negate = elem.get("negate", "false") - if not message: - message = f"{'Unspecified' if negate == 'false' else 'Specified'} genome build, click the pencil icon in the history item to {'set' if negate == 'false' else 'remove'} the genome build" - return cls(message, negate) + def __init__(self, message: Optional[str], negate: bool = False): + if message is None: + message = f"{'Unspecified' if not negate else 'Specified'} genome build, click the pencil icon in the history item to {'set' if not negate else 'remove'} the genome build" + return super().__init__(message, negate=negate) def validate(self, value, trans=None): # if value is None, we cannot validate @@ -348,13 +298,10 @@ class NoOptionsValidator(Validator): Validator that checks for empty select list """ - @classmethod - def from_element(cls, param, elem): - message = elem.get("message") - negate = elem.get("negate", "false") - if not message: - message = f"{'No options' if negate == 'false' else 'Options'} available for selection" - return cls(message, negate) + def __init__(self, message: Optional[str], negate: bool = False): + if message is None: + message = f"{'No options' if not negate else 'Options'} available for selection" + return super().__init__(message, negate=negate) def validate(self, value, trans=None): super().validate(value is not None) @@ -365,16 +312,13 @@ class EmptyTextfieldValidator(Validator): Validator that checks for empty text field """ - @classmethod - def from_element(cls, param, elem): - message = elem.get("message") - negate = elem.get("negate", "false") - if not message: - if negate == "false": - message = elem.get("message", "Field requires a value") + def __init__(self, message: Optional[str], negate: bool = False): + if message is None: + if not negate: + message = "Field requires a value" else: - message = elem.get("message", "Field must not set a value") - return cls(message, negate) + message = "Field must not set a value" + return super().__init__(message, negate=negate) def validate(self, value, trans=None): super().validate(value != "") @@ -391,34 +335,18 @@ class MetadataInFileColumnValidator(Validator): requires_dataset_metadata = True - @classmethod - def from_element(cls, param, elem): - filename = elem.get("filename") - assert filename, f"Required 'filename' attribute missing from {elem.get('type')} validator." - filename = f"{param.tool.app.config.tool_data_path}/{filename.strip()}" - assert os.path.exists(filename), f"File {filename} specified by the 'filename' attribute not found" - metadata_name = elem.get("metadata_name") - assert metadata_name, f"Required 'metadata_name' attribute missing from {elem.get('type')} validator." - metadata_name = metadata_name.strip() - metadata_column = int(elem.get("metadata_column", 0)) - split = elem.get("split", "\t") - message = elem.get("message", f"Value for metadata {metadata_name} was not found in {filename}.") - line_startswith = elem.get("line_startswith") - if line_startswith: - line_startswith = line_startswith.strip() - negate = elem.get("negate", "false") - return cls(filename, metadata_name, metadata_column, message, line_startswith, split, negate) - def __init__( self, - filename, - metadata_name, - metadata_column, - message="Value for metadata not found.", - line_startswith=None, - split="\t", - negate="false", + filename: str, + metadata_name: str, + metadata_column: int, + message: Optional[str] = None, + line_startswith: Optional[str] = None, + split: str = "\t", + negate: bool = False, ): + if message is None: + message = "Value for metadata not found." super().__init__(message, negate) self.metadata_name = metadata_name self.valid_values = set() @@ -445,28 +373,16 @@ class ValueInDataTableColumnValidator(Validator): note: this is covered in a framework test (validation_value_in_datatable) """ - @classmethod - def from_element(cls, param, elem): - table_name = elem.get("table_name") - assert table_name, f"Required 'table_name' attribute missing from {elem.get('type')} validator." - tool_data_table = param.tool.app.tool_data_tables[table_name] - column = elem.get("metadata_column", 0) - try: - column = int(column) - except ValueError: - pass - message = elem.get("message", f"Value was not found in {table_name}.") - negate = elem.get("negate", "false") - return cls(tool_data_table, column, message, negate) - - def __init__(self, tool_data_table, column, message="Value not found.", negate="false"): + def __init__( + self, tool_data_table, metadata_column: str, message: Optional[str] = "Value not found.", negate: bool = False + ): super().__init__(message, negate) self.valid_values = [] self._data_table_content_version = None self._tool_data_table = tool_data_table - if isinstance(column, str): - column = tool_data_table.columns[column] - self._column = column + if isinstance(metadata_column, str): + metadata_column = tool_data_table.columns[metadata_column] + self._column = metadata_column self._load_values() def _load_values(self): @@ -517,26 +433,13 @@ class MetadataInDataTableColumnValidator(ValueInDataTableColumnValidator): requires_dataset_metadata = True - @classmethod - def from_element(cls, param, elem): - table_name = elem.get("table_name") - assert table_name, f"Required 'table_name' attribute missing from {elem.get('type')} validator." - tool_data_table = param.tool.app.tool_data_tables[table_name] - metadata_name = elem.get("metadata_name") - assert metadata_name, f"Required 'metadata_name' attribute missing from {elem.get('type')} validator." - metadata_name = metadata_name.strip() - # TODO rename to column? - metadata_column = elem.get("metadata_column", 0) - try: - metadata_column = int(metadata_column) - except ValueError: - pass - message = elem.get("message", f"Value for metadata {metadata_name} was not found in {table_name}.") - negate = elem.get("negate", "false") - return cls(tool_data_table, metadata_name, metadata_column, message, negate) - def __init__( - self, tool_data_table, metadata_name, metadata_column, message="Value for metadata not found.", negate="false" + self, + tool_data_table, + metadata_name: str, + metadata_column: int, + message: Optional[str] = "Value for metadata not found.", + negate: bool = False, ): super().__init__(tool_data_table, metadata_column, message, negate) self.metadata_name = metadata_name @@ -558,7 +461,12 @@ class MetadataNotInDataTableColumnValidator(MetadataInDataTableColumnValidator): requires_dataset_metadata = True def __init__( - self, tool_data_table, metadata_name, metadata_column, message="Value for metadata not found.", negate="false" + self, + tool_data_table, + metadata_name: str, + metadata_column: int, + message: Optional[str] = "Value for metadata not found.", + negate: bool = False, ): super().__init__(tool_data_table, metadata_name, metadata_column, message, negate) @@ -580,26 +488,18 @@ class MetadataInRangeValidator(InRangeValidator): requires_dataset_metadata = True - @classmethod - def from_element(cls, param, elem): - metadata_name = elem.get("metadata_name") - assert metadata_name, f"Required 'metadata_name' attribute missing from {elem.get('type')} validator." - metadata_name = metadata_name.strip() - ret = cls( - metadata_name, - elem.get("message"), - elem.get("min"), - elem.get("max"), - elem.get("exclude_min", "false"), - elem.get("exclude_max", "false"), - elem.get("negate", "false"), - ) - ret.message = "Metadata: " + ret.message - return ret - - def __init__(self, metadata_name, message, range_min, range_max, exclude_min, exclude_max, negate): + def __init__( + self, + metadata_name: str, + message: Optional[str] = None, + min: Optional[float] = None, + max: Optional[float] = None, + exclude_min: bool = False, + exclude_max: bool = False, + negate: bool = False, + ): self.metadata_name = metadata_name - super().__init__(message, range_min, range_max, exclude_min, exclude_max, negate) + super().__init__(message, min, max, exclude_min, exclude_max, negate) def validate(self, value, trans=None): if value: @@ -638,3 +538,24 @@ def validate(self, value, trans=None): deprecated_validator_types = dict(dataset_metadata_in_file=MetadataInFileColumnValidator) validator_types.update(deprecated_validator_types) + + +def parse_xml_validators(app, xml_el: util.Element) -> List[Validator]: + return to_validators(app, parse_xml_validators_models(xml_el)) + + +def to_validators(app, validator_models: List[AnyValidatorModel]) -> Validator: + validators = [] + for validator_model in validator_models: + validators.append(_to_validator(app, validator_model)) + return validators + + +def _to_validator(app, validator_model: AnyValidatorModel) -> Validator: + as_dict = validator_model.model_dump() + validator_type = as_dict.pop("type") + if "table_name" in as_dict: + table_name = as_dict.pop("table_name") + tool_data_table = app.tool_data_tables[table_name] + as_dict["tool_data_table"] = tool_data_table + return validator_types[validator_type](**as_dict) diff --git a/test/unit/app/tools/test_validation_parsing.py b/test/unit/app/tools/test_validation_parsing.py new file mode 100644 index 000000000000..40848c2cb95e --- /dev/null +++ b/test/unit/app/tools/test_validation_parsing.py @@ -0,0 +1,39 @@ +from galaxy.tool_util.unittest_utils.sample_data import ( + INVALID_XML_VALIDATORS, + VALID_XML_VALIDATORS, +) +from galaxy.tools.parameters.validation import parse_xml_validators +from galaxy.util import XML + + +class MockApp: + + @property + def tool_data_tables(self): + return {"mycooltable": MockTable()} + + +class MockTable: + + def get_version_fields(self): + return (1, []) + + +def test_xml_validation_valid(): + for xml_validator in VALID_XML_VALIDATORS: + _validate_xml_str(xml_validator) + + +def test_xml_validation_invalid(): + for xml_validator in INVALID_XML_VALIDATORS: + exc: Optional[Exception] = None + try: + _validate_xml_str(xml_validator) + except ValueError as e: + exc = e + assert exc is not None, f"{xml_validator} - validated when it wasn't expected to" + + +def _validate_xml_str(xml_str: str): + xml_el = XML(f"{xml_str}") + parse_xml_validators(MockApp(), xml_el) diff --git a/test/unit/tool_util/test_parameter_validator_models.py b/test/unit/tool_util/test_parameter_validator_models.py index 3802a4235d11..17af98f2ce6c 100644 --- a/test/unit/tool_util/test_parameter_validator_models.py +++ b/test/unit/tool_util/test_parameter_validator_models.py @@ -1,68 +1,20 @@ +from typing import Optional + from galaxy.tool_util.parser.parameter_validators import parse_xml_validators +from galaxy.tool_util.unittest_utils.sample_data import ( + INVALID_XML_VALIDATORS, + VALID_XML_VALIDATORS, +) from galaxy.util import XML -valid_xml_validators = [ - """""", - """""", - """""", - """value == 7""", - """mycoolexpression""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", -] - -invalid_xml_validators = [ - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""", - """""" - """""" - """""", - """""", - """""", - """""", -] - - def test_xml_validation_valid(): - for xml_validator in valid_xml_validators: + for xml_validator in VALID_XML_VALIDATORS: _validate_xml_str(xml_validator) def test_xml_validation_invalid(): - for xml_validator in invalid_xml_validators: + for xml_validator in INVALID_XML_VALIDATORS: exc: Optional[Exception] = None try: _validate_xml_str(xml_validator)