Source code for galaxy.tools.parameters.validation

"""
Classes related to parameter validation.
"""

import abc
import logging
import os
from typing import (
    Any,
    cast,
    List,
    Optional,
    Union,
)

from galaxy import (
    model,
    util,
)
from galaxy.tool_util.parser.parameter_validators import (
    AnyValidatorModel,
    EmptyFieldParameterValidatorModel,
    ExpressionParameterValidatorModel,
    InRangeParameterValidatorModel,
    MetadataParameterValidatorModel,
    parse_xml_validators as parse_xml_validators_models,
    raise_error_if_valiation_fails,
    RegexParameterValidatorModel,
)

log = logging.getLogger(__name__)


[docs]class Validator(abc.ABC): """ A validator checks that a value meets some conditions OR raises ValueError """ requires_dataset_metadata = False
[docs] def __init__(self, message: str, negate: bool = False): self.message = message self.negate = util.asbool(negate) super().__init__()
[docs] @abc.abstractmethod def validate(self, value, trans=None, message=None, value_to_show=None): """ validate a value needs to be implemented in classes derived from validator. the implementation needs to call `super().validate()` giving result as a bool (which should be true if the validation is positive and false otherwise) and the value that is validated. the Validator.validate function will then negate the value depending on `self.negate` and return None if - value is True and negate is False - value is False and negate is True and raise a ValueError otherwise. return None if positive validation, otherwise a ValueError is raised """ raise_error_if_valiation_fails(value, self, message=message, value_to_show=value_to_show)
[docs]class RegexValidator(Validator): """ Validator that evaluates a regular expression """
[docs] def __init__(self, message: str, expression: str, negate: bool): super().__init__(message, negate) # Compile later. RE objects used to not be thread safe. Not sure about # the sre module. self.expression = expression
[docs] def validate(self, value, trans=None): RegexParameterValidatorModel.regex_validation(self.expression, value, self)
[docs]class ExpressionValidator(Validator): """ Validator that evaluates a python expression using the value """
[docs] def __init__(self, message: str, expression: str, negate: bool): super().__init__(message, negate) self.expression = expression # Save compiled expression, code objects are thread safe (right?) self.compiled_expression = ExpressionParameterValidatorModel.ensure_compiled(expression)
[docs] def validate(self, value, trans=None): ExpressionParameterValidatorModel.expression_validation( self.expression, value, self, compiled_expression=self.compiled_expression )
[docs]class InRangeValidator(ExpressionValidator): """ Validator that ensures a number is in a specified range """
[docs] def __init__( self, message: str, 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), while if set to False (the default), then range includes the end points (1.e., min <= value <= max). Combinations of exclude_min and exclude_max values are allowed. """ 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 = "<=" op2 = "<=" if self.exclude_min: op1 = "<" if self.exclude_max: op2 = "<" expression = f"float('{self.min}') {op1} float(value) {op2} float('{self.max}')" super().__init__(message, expression, negate)
[docs] @staticmethod def simple_range_validator(min: Optional[float], max: Optional[float]): return cast( InRangeParameterValidatorModel, _to_validator(None, InRangeParameterValidatorModel(min=min, max=max, implicit=True)), )
[docs]class LengthValidator(InRangeValidator): """ Validator that ensures the length of the provided string (value) is in a specific range """
[docs] def __init__(self, message: str, min: float, max: float, negate: bool): super().__init__(message, min=min, max=max, negate=negate)
[docs] def validate(self, value, trans=None): if value is None: raise ValueError("No value provided") super().validate(len(value) if value else 0, trans)
[docs]class DatasetOkValidator(Validator): """ Validator that checks if a dataset is in an 'ok' state """
[docs] def __init__(self, message: str, negate: bool = False): super().__init__(message, negate=negate)
[docs] def validate(self, value, trans=None): if value: super().validate(value.state == model.Dataset.states.OK)
[docs]class DatasetEmptyValidator(Validator): """ Validator that checks if a dataset has a positive file size. """
[docs] def __init__(self, message: str, negate: bool = False): super().__init__(message, negate=negate)
[docs] def validate(self, value, trans=None): if value: super().validate(value.get_size() != 0)
[docs]class DatasetExtraFilesPathEmptyValidator(Validator): """ Validator that checks if a dataset's extra_files_path exists and is not empty. """
[docs] def __init__(self, message: str, negate: bool = False): super().__init__(message, negate=negate)
[docs] def validate(self, value, trans=None): if value: super().validate(value.get_total_size() != value.get_size())
[docs]class MetadataValidator(Validator): """ Validator that checks for missing metadata """ requires_dataset_metadata = True
[docs] def __init__( self, message: str, check: Optional[List[str]] = None, skip: Optional[List[str]] = None, negate: bool = False, ): super().__init__(message, negate) self.check = check self.skip = skip
[docs] @staticmethod def default_metadata_validator() -> "MetadataValidator": return cast(MetadataValidator, _to_validator(None, MetadataParameterValidatorModel(implicit=True)))
[docs] def validate(self, value, trans=None): if value: # TODO why this validator checks for isinstance(value, model.DatasetInstance) missing = value.missing_meta(check=self.check, skip=self.skip) super().validate(isinstance(value, model.DatasetInstance) and not missing, value_to_show=missing)
[docs]class MetadataEqualValidator(Validator): """ Validator that checks for a metadata value for equality metadata values that are lists are converted as comma separated string everything else is converted to the string representation """ requires_dataset_metadata = True
[docs] def __init__(self, metadata_name=None, value=None, message=None, negate: bool = False): super().__init__(message, negate) self.metadata_name = metadata_name self.value = value
[docs] def validate(self, value, trans=None): if value: metadata_value = getattr(value.metadata, self.metadata_name) super().validate(metadata_value == self.value, value_to_show=metadata_value)
[docs]class UnspecifiedBuildValidator(Validator): """ Validator that checks for dbkey not equal to '?' """ requires_dataset_metadata = True
[docs] def __init__(self, message: str, negate: bool = False): super().__init__(message, negate=negate)
[docs] def validate(self, value, trans=None): # if value is None, we cannot validate if value: dbkey = value.metadata.dbkey # TODO can dbkey really be a list? if isinstance(dbkey, list): dbkey = dbkey[0] super().validate(dbkey != "?")
[docs]class NoOptionsValidator(Validator): """ Validator that checks for empty select list """
[docs] def __init__(self, message: str, negate: bool = False): super().__init__(message, negate=negate)
[docs] def validate(self, value, trans=None): super().validate(value is not None)
[docs]class EmptyTextfieldValidator(Validator): """ Validator that checks for empty text field """
[docs] def __init__(self, message: str, negate: bool = False): super().__init__(message, negate=negate)
[docs] def validate(self, value, trans=None): EmptyFieldParameterValidatorModel.empty_validate(value, self)
[docs]class MetadataInFileColumnValidator(Validator): """ Validator that checks if the value for a dataset's metadata item exists in a file. Deprecated: DataTables are now the preferred way. note: this is covered in a framework test (validation_dataset_metadata_in_file) """ requires_dataset_metadata = True
[docs] def __init__( self, filename: str, metadata_name: str, metadata_column: int, message: str, line_startswith: Optional[str] = None, split: str = "\t", negate: bool = False, ): super().__init__(message, negate) assert filename assert os.path.exists(filename), f"File {filename} specified by the 'filename' attribute not found" self.metadata_name = metadata_name self.valid_values = set() with open(filename) as fh: for line in fh: if line_startswith is None or line.startswith(line_startswith): fields = line.split(split) if metadata_column < len(fields): self.valid_values.add(fields[metadata_column].strip())
[docs] def validate(self, value, trans=None): if not value: return super().validate( value.metadata.spec[self.metadata_name].param.to_string(value.metadata.get(self.metadata_name)) in self.valid_values )
[docs]class ValueInDataTableColumnValidator(Validator): """ Validator that checks if a value is in a tool data table column. note: this is covered in a framework test (validation_value_in_datatable) """
[docs] def __init__( self, tool_data_table, metadata_column: Union[str, int], message: str, negate: bool = False, ): super().__init__(message, negate) self.valid_values: List[Any] = [] self._data_table_content_version = None self._tool_data_table = tool_data_table if isinstance(metadata_column, str): metadata_column = tool_data_table.columns[metadata_column] self._column = metadata_column self._load_values()
def _load_values(self): self._data_table_content_version, data_fields = self._tool_data_table.get_version_fields() self.valid_values = [] for fields in data_fields: if self._column < len(fields): self.valid_values.append(fields[self._column])
[docs] def validate(self, value, trans=None): if not value: return if not self._tool_data_table.is_current_version(self._data_table_content_version): log.debug( "ValueInDataTableColumnValidator: values are out of sync with data table (%s), updating validator.", self._tool_data_table.name, ) self._load_values() super().validate(value in self.valid_values)
[docs]class ValueNotInDataTableColumnValidator(ValueInDataTableColumnValidator): """ Validator that checks if a value is NOT in a tool data table column. Equivalent to ValueInDataTableColumnValidator with `negate="true"`. note: this is covered in a framework test (validation_value_in_datatable) """
[docs] def __init__( self, tool_data_table, metadata_column: Union[str, int], message="Value already present.", negate: bool = False ): super().__init__(tool_data_table, metadata_column, message, negate)
[docs] def validate(self, value, trans=None): try: super().validate(value) except ValueError: return else: raise ValueError(self.message)
[docs]class MetadataInDataTableColumnValidator(ValueInDataTableColumnValidator): """ Validator that checks if the value for a dataset's metadata item exists in a file. note: this is covered in a framework test (validation_metadata_in_datatable) """ requires_dataset_metadata = True
[docs] def __init__( self, tool_data_table, metadata_name: str, metadata_column: Union[str, int], message: str, negate: bool = False, ): super().__init__(tool_data_table, metadata_column, message, negate) self.metadata_name = metadata_name
[docs] def validate(self, value, trans=None): super().validate( value.metadata.spec[self.metadata_name].param.to_string(value.metadata.get(self.metadata_name)), trans )
[docs]class MetadataNotInDataTableColumnValidator(MetadataInDataTableColumnValidator): """ Validator that checks if the value for a dataset's metadata item doesn't exists in a file. Equivalent to MetadataInDataTableColumnValidator with `negate="true"`. note: this is covered in a framework test (validation_metadata_in_datatable) """ requires_dataset_metadata = True
[docs] def __init__( self, tool_data_table, metadata_name: str, metadata_column: Union[str, int], message: str, negate: bool = False, ): super().__init__(tool_data_table, metadata_name, metadata_column, message, negate)
[docs] def validate(self, value, trans=None): try: super().validate(value, trans) except ValueError: return else: raise ValueError(self.message)
[docs]class MetadataInRangeValidator(InRangeValidator): """ validator that ensures metadata is in a specified range note: this is covered in a framework test (validation_metadata_in_range) """ requires_dataset_metadata = True
[docs] def __init__( self, metadata_name: str, message: str, 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, min, max, exclude_min, exclude_max, negate)
[docs] def validate(self, value, trans=None): if value: if not isinstance(value, model.DatasetInstance): raise ValueError("A non-dataset value was provided.") try: value_to_check = float( value.metadata.spec[self.metadata_name].param.to_string(value.metadata.get(self.metadata_name)) ) except KeyError: raise ValueError(f"{self.metadata_name} Metadata missing") except ValueError: raise ValueError(f"{self.metadata_name} must be a float or an integer") super().validate(value_to_check, trans)
validator_types = dict( expression=ExpressionValidator, regex=RegexValidator, in_range=InRangeValidator, length=LengthValidator, metadata=MetadataValidator, dataset_metadata_equal=MetadataEqualValidator, unspecified_build=UnspecifiedBuildValidator, no_options=NoOptionsValidator, empty_field=EmptyTextfieldValidator, empty_dataset=DatasetEmptyValidator, empty_extra_files_path=DatasetExtraFilesPathEmptyValidator, dataset_metadata_in_data_table=MetadataInDataTableColumnValidator, dataset_metadata_not_in_data_table=MetadataNotInDataTableColumnValidator, dataset_metadata_in_range=MetadataInRangeValidator, value_in_data_table=ValueInDataTableColumnValidator, value_not_in_data_table=ValueNotInDataTableColumnValidator, dataset_ok_validator=DatasetOkValidator, ) deprecated_validator_types = dict(dataset_metadata_in_file=MetadataInFileColumnValidator) validator_types.update(deprecated_validator_types)
[docs]def parse_xml_validators(app, xml_el: util.Element) -> List[Validator]: return to_validators(app, parse_xml_validators_models(xml_el))
[docs]def to_validators(app, validator_models: List[AnyValidatorModel]) -> List[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") del as_dict["implicit"] if "table_name" in as_dict and app is not None: table_name = as_dict.pop("table_name") tool_data_table = app.tool_data_tables[table_name] as_dict["tool_data_table"] = tool_data_table if "filename" in as_dict and app is not None: filename = as_dict.pop("filename") as_dict["filename"] = f"{app.config.tool_data_path}/{filename}" return validator_types[validator_type](**as_dict)