Warning

This document is for an in-development version of Galaxy. You can alternatively view this page in the latest release if it exists or view the top of the latest release's documentation.

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)