diff --git a/lib/galaxy/tool_util/linters/inputs.py b/lib/galaxy/tool_util/linters/inputs.py
index f8a843fb11a..437efe5e76f 100644
--- a/lib/galaxy/tool_util/linters/inputs.py
+++ b/lib/galaxy/tool_util/linters/inputs.py
@@ -19,12 +19,12 @@ FILTER_TYPES = [
ATTRIB_VALIDATOR_COMPATIBILITY = {
"check": ["metadata"],
- "expression": ["regex"],
+ "expression": ["regex", "substitute_value_in_message"],
"table_name": ["dataset_metadata_in_data_table", "dataset_metadata_not_in_data_table", "value_in_data_table", "value_not_in_data_table"],
"filename": ["dataset_metadata_in_file"],
"metadata_name": ["dataset_metadata_in_data_table", "dataset_metadata_not_in_data_table", "dataset_metadata_in_file"],
"metadata_column": ["dataset_metadata_in_data_table", "dataset_metadata_not_in_data_table", "value_in_data_table", "value_not_in_data_table", "dataset_metadata_in_file options"],
- "line_startswith": ["dataset_metadata_in_file", "dataset_metadata_in_data_table", "dataset_metadata_not_in_data_table", "value_in_data_table", "value_not_in_data_table"],
+ "line_startswith": ["dataset_metadata_in_file"],
"min": ["in_range", "length"],
"max": ["in_range", "length"],
"exclude_min": ["in_range"],
diff --git a/lib/galaxy/tool_util/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd
index 2ecbbf6eec5..1e4ddd10e17 100644
--- a/lib/galaxy/tool_util/xsd/galaxy.xsd
+++ b/lib/galaxy/tool_util/xsd/galaxy.xsd
@@ -3726,10 +3726,14 @@ parameters a ``metadata`` validator is added automatically.
- ``dataset_ok_validator``: Check if the data set is in state OK.
- ``dataset_metadata_in_range``: Check if a numeric metadata value is within
a given range.
-- ``dataset_metadata_in_file``: Check if a metadata value is contained in a
-specific column of another data set.
-- ``dataset_metadata_in_data_table`` (``dataset_metadata_not_in_data_table``):
-Check if a metadata value is contained in a column of a data table.
+- ``dataset_metadata_in_data_table``: Check if a metadata value is contained in a column of a data table.
+- ``dataset_metadata_not_in_data_table``: Equivalent to ``dataset_metadata_in_data_table`` with ``negate="true"``.
+
+Deprecated data validators:
+
+- ``dataset_metadata_in_file``: Use data tables with ``dataset_metadata_in_data_table``.
+Check if a metadata value is contained in a specific column of a file in the ``tool_data_path``
+(which is set in Galaxy's config).
### Validators for textual inputs (``text``, ``select``, ...)
@@ -3749,8 +3753,9 @@ For ``text`` inputs the following validators are useful:
- ``length``: Check if the length of the value is within a range.
- ``empty_field``: Check if the string is not empty
-- ``value_in_data_table`` (``value_not_in_data_table``): Check if the value is
+- ``value_in_data_table``: Check if the value is
contained in a column of a given data table.
+- ``value_not_in_data_table``: Equivalent to ``value_in_data_table`` with ``negate="true"``.
### Validators for numeric inputs (``integer``, ``float``)
@@ -3808,21 +3813,29 @@ use in filenames may not contain ``..``.
-
+``dataset_ok_validator``, ``dataset_metadata_in_range``.
+Deprecated validator: ``dataset_metadata_in_file``.
+The list of supported
+validators is in the ``validator_types`` dictionary in
+[/lib/galaxy/tools/parameters/validation.py](https://github.com/galaxyproject/galaxy/blob/dev/lib/galaxy/tools/parameters/validation.py).
+]]>
-The error message displayed on the tool form if validation fails.
+The error message displayed on the tool form if validation fails. A placeholder string ``%s`` will be repaced by the ``value``
+
+
+
+
+
+Negates the result of the validator.
@@ -3844,7 +3857,7 @@ more information.
- Tool data filename to check against
+ Deprecated: use ``dataset_metadata_in_data_table``. Tool data filename to check against
if ``type`` is ``dataset_metadata_in_file``. File should be present Galaxy's
``tool-data`` directory.
@@ -3862,13 +3875,6 @@ in ``dataset_metadata_in_data_table``, ``dataset_metadata_not_in_data_table``, `
This can be an integer index to the column or a column name.
-
-
- Used to indicate lines in the file
-being used for validation start with a this attribute value.
-For use with validators of type ``dataset_metadata_in_file``, ``dataset_metadata_in_data_table``, ``dataset_metadata_not_in_data_table``, ``value_in_data_table``, ``value_not_in_data_tabl``
-
-
When the ``type`` attribute value is
@@ -3908,6 +3914,18 @@ fields to skip if type is ``metadata``. If not specified, all non-optional
metadata fields will be checked unless ``check`` attribute is specified.
+
+
+ Deprecated. Used to indicate lines in the file
+being used for validation start with a this attribute value.
+For use with validator ``dataset_metadata_in_file``
+
+
+
+
+ Deprecated. This is now always done.
+
+
diff --git a/lib/galaxy/tools/parameters/dynamic_options.py b/lib/galaxy/tools/parameters/dynamic_options.py
index a0614a47f5a..759457f4a6a 100644
--- a/lib/galaxy/tools/parameters/dynamic_options.py
+++ b/lib/galaxy/tools/parameters/dynamic_options.py
@@ -586,6 +586,9 @@ class DynamicOptions:
@property
def tool_data_table(self):
if self.tool_data_table_name:
+ # this is needed for the validator unit tests and should not happen in real life
+ if self.tool_param.tool is None:
+ return None
tool_data_table = self.tool_param.tool.app.tool_data_tables.get(self.tool_data_table_name, None)
if tool_data_table:
# Column definitions are optional, but if provided override those from the table
diff --git a/lib/galaxy/tools/parameters/test/1.tabular b/lib/galaxy/tools/parameters/test/1.tabular
new file mode 120000
index 00000000000..929b6a0e404
--- /dev/null
+++ b/lib/galaxy/tools/parameters/test/1.tabular
@@ -0,0 +1 @@
+../../../../../test-data/1.tabular
\ No newline at end of file
diff --git a/lib/galaxy/tools/parameters/test/empty.txt b/lib/galaxy/tools/parameters/test/empty.txt
new file mode 120000
index 00000000000..c37ecd1e239
--- /dev/null
+++ b/lib/galaxy/tools/parameters/test/empty.txt
@@ -0,0 +1 @@
+../../../../../test-data/empty.txt
\ No newline at end of file
diff --git a/lib/galaxy/tools/parameters/validation.py b/lib/galaxy/tools/parameters/validation.py
index 9b4d86e226d..3b5e322e0d9 100644
--- a/lib/galaxy/tools/parameters/validation.py
+++ b/lib/galaxy/tools/parameters/validation.py
@@ -1,7 +1,9 @@
"""
Classes related to parameter validation.
"""
+import abc
import logging
+import os.path
import re
@@ -13,7 +15,14 @@ from galaxy import (
log = logging.getLogger(__name__)
-class Validator:
+def get_test_fname(fname):
+ """Returns test data filename"""
+ path, name = os.path.split(__file__)
+ full_path = os.path.join(path, 'test', fname)
+ return full_path
+
+
+class Validator(abc.ABC):
"""
A validator checks that a value meets some conditions OR raises ValueError
"""
@@ -35,17 +44,43 @@ class 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', None)
- assert type is not None, "Required 'type' attribute missing from validator"
- return validator_types[type].from_element(param, elem)
+ _type = elem.get('type', None)
+ assert _type is not None, "Required 'type' attribute missing from validator"
+ return validator_types[_type].from_element(param, elem)
- def validate(self, value, trans=None):
+ def __init__(self, message, negate=False):
+ self.message = message
+ self.negate = util.asbool(negate)
+ super().__init__()
+
+ @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 TypeError("Abstract Method")
+ assert isinstance(value, bool), 'value must be boolean'
+ if message is None:
+ message = self.message
+ if value_to_show and "%s" in message:
+ message = message % value_to_show
+ if (not self.negate and value) or (self.negate and not value):
+ return
+ else:
+ raise ValueError(message)
class RegexValidator(Validator):
@@ -56,7 +91,7 @@ class RegexValidator(Validator):
>>> from galaxy.tools.parameters.basic import ToolParameter
>>> p = ToolParameter.build(None, XML('''
...
- ... [Ff]oo
+ ... [Ff]oo
...
... '''))
>>> t = p.validate("Foo")
@@ -64,20 +99,42 @@ class RegexValidator(Validator):
>>> t = p.validate("Fop")
Traceback (most recent call last):
...
- ValueError: Not gonna happen
+ ValueError: Value 'Fop' does not match regular expression '[Ff]oo'
>>> t = p.validate(["Foo", "foo"])
>>> t = p.validate(["Foo", "Fop"])
Traceback (most recent call last):
...
- ValueError: Not gonna happen
+ ValueError: Value 'Fop' does not match regular expression '[Ff]oo'
+ >>>
+ >>> p = ToolParameter.build(None, XML('''
+ ...
+ ... [Ff]oo
+ ...
+ ... '''))
+ >>> t = p.validate("Foo")
+ Traceback (most recent call last):
+ ...
+ ValueError: Value 'Foo' does match regular expression '[Ff]oo'
+ >>> t = p.validate("foo")
+ Traceback (most recent call last):
+ ...
+ ValueError: Value 'foo' does match regular expression '[Ff]oo'
+ >>> t = p.validate("Fop")
+ >>> t = p.validate(["Fop", "foo"])
+ Traceback (most recent call last):
+ ...
+ ValueError: Value 'foo' does match regular expression '[Ff]oo'
+ >>> t = p.validate(["Fop", "Fop"])
"""
@classmethod
def from_element(cls, param, elem):
- return cls(elem.get('message'), elem.text)
+ return cls(elem.get('message', None), elem.text, elem.get('negate', 'false'))
- def __init__(self, message, expression):
- self.message = message
+ def __init__(self, message, expression, negate):
+ if message is None:
+ message = f"Value '%s' does {'not ' if negate == 'false' else ''}match regular expression '{expression}'"
+ super().__init__(message, negate)
# Compile later. RE objects used to not be thread safe. Not sure about
# the sre module.
self.expression = expression
@@ -86,8 +143,8 @@ class RegexValidator(Validator):
if not isinstance(value, list):
value = [value]
for val in value:
- if re.match(self.expression, val or '') is None:
- raise ValueError(self.message)
+ match = re.match(self.expression, val or '')
+ super().validate(match is not None, value_to_show=val)
class ExpressionValidator(Validator):
@@ -107,32 +164,46 @@ class ExpressionValidator(Validator):
Traceback (most recent call last):
...
ValueError: Not gonna happen
+ >>>
+ >>> p = ToolParameter.build(None, XML('''
+ ...
+ ... value.lower() == "foo"
+ ...
+ ... '''))
+ >>> t = p.validate("Foo")
+ Traceback (most recent call last):
+ ...
+ ValueError: Not gonna happen
+ >>> t = p.validate("foo")
+ Traceback (most recent call last):
+ ...
+ ValueError: Not gonna happen
+ >>> t = p.validate("Fop")
"""
@classmethod
def from_element(cls, param, elem):
- return cls(elem.get('message'), elem.text, elem.get('substitute_value_in_message'))
+ return cls(elem.get('message', None), elem.text, elem.get('negate', 'false'))
- def __init__(self, message, expression, substitute_value_in_message):
- self.message = message
- self.substitute_value_in_message = substitute_value_in_message
+ 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}'"
+ super().__init__(message, negate)
# Save compiled expression, code objects are thread safe (right?)
+ log.error(f"ExpressionValidator expression {expression}")
self.expression = compile(expression, '', 'eval')
def validate(self, value, trans=None):
- message = self.message
- if self.substitute_value_in_message:
- message = message % value
+ log.error(f"ExpressionValidator.validate value {value} expression {self.expression}")
try:
evalresult = eval(self.expression, dict(value=value))
except Exception:
- log.debug(f"Validator {self.expression} could not be evaluated on {str(value)}", exc_info=True)
- raise ValueError(message)
- if not(evalresult):
- raise ValueError(message)
+ log.debug(f"Validator '{self.expression}' could not be evaluated on '{str(value)}'", exc_info=True)
+ super().validate(False, value, f"Validator '{self.expression}' could not be evaluated on '%s'")
+ super().validate(evalresult, value_to_show=value)
-class InRangeValidator(Validator):
+class InRangeValidator(ExpressionValidator):
"""
Validator that ensures a number is in a specified range
@@ -140,28 +211,45 @@ class InRangeValidator(Validator):
>>> from galaxy.tools.parameters.basic import ToolParameter
>>> p = ToolParameter.build(None, XML('''
...
- ...
+ ...
...
... '''))
>>> t = p.validate(10)
Traceback (most recent call last):
...
- ValueError: Not gonna happen
+ ValueError: Doh!! 10 not in range
>>> t = p.validate(15)
>>> t = p.validate(20)
>>> t = p.validate(21)
Traceback (most recent call last):
...
- ValueError: Not gonna happen
+ ValueError: Doh!! 21 not in range
+ >>>
+ >>> p = ToolParameter.build(None, XML('''
+ ...
+ ...
+ ...
+ ... '''))
+ >>> t = p.validate(10)
+ >>> t = p.validate(15)
+ Traceback (most recent call last):
+ ...
+ ValueError: Value ('15') must not fulfill float('10') < value <= float('20')
+ >>> t = p.validate(20)
+ Traceback (most recent call last):
+ ...
+ ValueError: Value ('20') must not fulfill float('10') < value <= float('20')
+ >>> t = p.validate(21)
"""
@classmethod
def from_element(cls, param, elem):
return cls(elem.get('message', None), elem.get('min'),
elem.get('max'), elem.get('exclude_min', 'false'),
- elem.get('exclude_max', 'false'))
+ elem.get('exclude_max', 'false'),
+ elem.get('negate', 'false'))
- def __init__(self, message, range_min, range_max, exclude_min=False, exclude_max=False):
+ def __init__(self, message, range_min, range_max, exclude_min=False, exclude_max=False, negate=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),
@@ -169,38 +257,25 @@ class InRangeValidator(Validator):
(1.e., min <= value <= max). Combinations of exclude_min and exclude_max
values are allowed.
"""
- self.min = float(range_min if range_min is not None else '-inf')
+ self.min = range_min if range_min is not None else '-inf'
self.exclude_min = util.asbool(exclude_min)
- self.max = float(range_max if range_max is not None else 'inf')
+ self.max = range_max if range_max is not None else 'inf'
self.exclude_max = util.asbool(exclude_max)
- assert self.min <= self.max, 'min must be less than or equal to 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.
- self_min_str = str(self.min).rstrip('0').rstrip('.')
- self_max_str = str(self.max).rstrip('0').rstrip('.')
- op1 = '>='
+ op1 = '<='
op2 = '<='
if self.exclude_min:
- op1 = '>'
+ op1 = '<'
if self.exclude_max:
op2 = '<'
- self.message = message or f"Value must be {op1} {self_min_str} and {op2} {self_max_str}"
-
- def validate(self, value, trans=None):
- if self.exclude_min:
- if not self.min < float(value):
- raise ValueError(self.message)
- else:
- if not self.min <= float(value):
- raise ValueError(self.message)
- if self.exclude_max:
- if not float(value) < self.max:
- raise ValueError(self.message)
- else:
- if not float(value) <= self.max:
- raise ValueError(self.message)
+ expression = f"float('{self.min}') {op1} value {op2} float('{self.max}')"
+ if message is None:
+ message = f"Value ('%s') must {'not ' if negate == 'true' else ''}fulfill {expression}"
+ super().__init__(message, expression, negate)
-class LengthValidator(Validator):
+class LengthValidator(InRangeValidator):
"""
Validator that ensures the length of the provided string (value) is in a specific range
@@ -216,176 +291,442 @@ class LengthValidator(Validator):
>>> t = p.validate("f")
Traceback (most recent call last):
...
- ValueError: Must have length of at least 2
+ ValueError: Must have length of at least 2 and at most 8
>>> t = p.validate("foobarbaz")
Traceback (most recent call last):
...
- ValueError: Must have length no more than 8
+ ValueError: Must have length of at least 2 and at most 8
+ >>>
+ >>> p = ToolParameter.build(None, XML('''
+ ...
+ ...
+ ...
+ ... '''))
+ >>> t = p.validate("foo")
+ Traceback (most recent call last):
+ ...
+ ValueError: Must not have length of at least 2 and at most 8
+ >>> t = p.validate("bar")
+ Traceback (most recent call last):
+ ...
+ ValueError: Must not have length of at least 2 and at most 8
+ >>> t = p.validate("f")
+ >>> t = p.validate("foobarbaz")
"""
@classmethod
def from_element(cls, param, elem):
- return cls(elem.get('message', None), elem.get('min', None), elem.get('max', None))
+ return cls(elem.get('message', None), elem.get('min', None), elem.get('max', None), elem.get('negate', 'false'))
- def __init__(self, message, length_min, length_max):
- self.message = message
- if length_min is not None:
- length_min = int(length_min)
- if length_max is not None:
- length_max = int(length_max)
- self.min = length_min
- self.max = length_max
+ def __init__(self, message, length_min, length_max, negate):
+ 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)
def validate(self, value, trans=None):
- if self.min is not None and len(value) < self.min:
- raise ValueError(self.message or ("Must have length of at least %d" % self.min))
- if self.max is not None and len(value) > self.max:
- raise ValueError(self.message or ("Must have length no more than %d" % self.max))
+ super().validate(len(value), trans)
class DatasetOkValidator(Validator):
"""
Validator that checks if a dataset is in an 'ok' state
- """
- def __init__(self, message=None):
- self.message = message
+ >>> from galaxy.datatypes.registry import example_datatype_registry_for_sample
+ >>> from galaxy.model import History, HistoryDatasetAssociation, set_datatypes_registry
+ >>> from galaxy.model.mapping import init
+ >>> from galaxy.util import XML
+ >>> from galaxy.tools.parameters.basic import ToolParameter
+ >>>
+ >>> sa_session = init("/tmp", "sqlite:///:memory:", create_tables=True).session
+ >>> hist = History()
+ >>> sa_session.add(hist)
+ >>> sa_session.flush()
+ >>> set_datatypes_registry(example_datatype_registry_for_sample())
+ >>> ok_hda = hist.add_dataset(HistoryDatasetAssociation(id=1, extension='interval', create_dataset=True, sa_session=sa_session))
+ >>> ok_hda.set_dataset_state(model.Dataset.states.OK)
+ >>> notok_hda = hist.add_dataset(HistoryDatasetAssociation(id=2, extension='interval', create_dataset=True, sa_session=sa_session))
+ >>> notok_hda.set_dataset_state(model.Dataset.states.EMPTY)
+ >>>
+ >>> p = ToolParameter.build(None, XML('''
+ ...
+ ...
+ ...
+ ... '''))
+ >>> t = p.validate(ok_hda)
+ >>> t = p.validate(notok_hda)
+ Traceback (most recent call last):
+ ...
+ ValueError: The selected dataset is still being generated, select another dataset or wait until it is completed
+ >>>
+ >>> p = ToolParameter.build(None, XML('''
+ ...
+ ...
+ ...
+ ... '''))
+ >>> t = p.validate(ok_hda)
+ Traceback (most recent call last):
+ ...
+ ValueError: The selected dataset must not be in state OK
+ >>> t = p.validate(notok_hda)
+ """
@classmethod
def from_element(cls, param, elem):
- return cls(elem.get('message', None))
+ negate = elem.get('negate', 'false')
+ message = elem.get('message', None)
+ if message is None:
+ if negate == 'false':
+ 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)
def validate(self, value, trans=None):
- if value and value.state != model.Dataset.states.OK:
- if self.message is None:
- self.message = "The selected dataset is still being generated, select another dataset or wait until it is completed"
- raise ValueError(self.message)
+ if value:
+ super().validate(value.state == model.Dataset.states.OK)
class DatasetEmptyValidator(Validator):
- """Validator that checks if a dataset has a positive file size."""
+ """
+ Validator that checks if a dataset has a positive file size.
- def __init__(self, message=None):
- self.message = message
+ >>> from galaxy.datatypes.registry import example_datatype_registry_for_sample
+ >>> from galaxy.model import Dataset, History, HistoryDatasetAssociation, set_datatypes_registry
+ >>> from galaxy.model.mapping import init
+ >>> from galaxy.util import XML
+ >>> from galaxy.tools.parameters.basic import ToolParameter
+ >>>
+ >>> sa_session = init("/tmp", "sqlite:///:memory:", create_tables=True).session
+ >>> hist = History()
+ >>> sa_session.add(hist)
+ >>> sa_session.flush()
+ >>> set_datatypes_registry(example_datatype_registry_for_sample())
+ >>> empty_dataset = Dataset(external_filename=get_test_fname("empty.txt"))
+ >>> empty_hda = hist.add_dataset(HistoryDatasetAssociation(id=1, extension='interval', dataset=empty_dataset, sa_session=sa_session))
+ >>> full_dataset = Dataset(external_filename=get_test_fname("1.tabular"))
+ >>> full_hda = hist.add_dataset(HistoryDatasetAssociation(id=2, extension='interval', dataset=full_dataset, sa_session=sa_session))
+ >>>
+ >>> p = ToolParameter.build(None, XML('''
+ ...
+ ...
+ ...
+ ... '''))
+ >>> t = p.validate(full_hda)
+ >>> t = p.validate(empty_hda)
+ Traceback (most recent call last):
+ ...
+ ValueError: The selected dataset is empty, this tool expects non-empty files.
+ >>>
+ >>> p = ToolParameter.build(None, XML('''
+ ...
+ ...
+ ...
+ ... '''))
+ >>> t = p.validate(full_hda)
+ Traceback (most recent call last):
+ ...
+ ValueError: The selected dataset is non-empty, this tool expects empty files.
+ >>> t = p.validate(empty_hda)
+ """
@classmethod
def from_element(cls, param, elem):
- return cls(elem.get('message', None))
+ message = elem.get('message', None)
+ 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 validate(self, value, trans=None):
if value:
- if value.get_size() == 0:
- if self.message is None:
- self.message = "The selected dataset is empty, this tool expects non-empty files."
- raise ValueError(self.message)
+ super().validate(value.get_size() != 0)
class DatasetExtraFilesPathEmptyValidator(Validator):
- """Validator that checks if a dataset's extra_files_path exists and is not empty."""
+ """
+ Validator that checks if a dataset's extra_files_path exists and is not empty.
- def __init__(self, message=None):
- self.message = message
+ >>> from galaxy.datatypes.registry import example_datatype_registry_for_sample
+ >>> from galaxy.model import History, HistoryDatasetAssociation, set_datatypes_registry
+ >>> from galaxy.model.mapping import init
+ >>> from galaxy.util import XML
+ >>> from galaxy.tools.parameters.basic import ToolParameter
+ >>>
+ >>> sa_session = init("/tmp", "sqlite:///:memory:", create_tables=True).session
+ >>> hist = History()
+ >>> sa_session.add(hist)
+ >>> sa_session.flush()
+ >>> set_datatypes_registry(example_datatype_registry_for_sample())
+ >>> has_extra_hda = hist.add_dataset(HistoryDatasetAssociation(id=1, extension='interval', create_dataset=True, sa_session=sa_session))
+ >>> has_extra_hda.dataset.file_size = 10
+ >>> has_extra_hda.dataset.total_size = 15
+ >>> has_no_extra_hda = hist.add_dataset(HistoryDatasetAssociation(id=2, extension='interval', create_dataset=True, sa_session=sa_session))
+ >>> has_no_extra_hda.dataset.file_size = 10
+ >>> has_no_extra_hda.dataset.total_size = 10
+ >>>
+ >>> p = ToolParameter.build(None, XML('''
+ ...
+ ...
+ ...
+ ... '''))
+ >>> t = p.validate(has_extra_hda)
+ >>> t = p.validate(has_no_extra_hda)
+ Traceback (most recent call last):
+ ...
+ ValueError: The selected dataset's extra_files_path directory is empty or does not exist, this tool expects non-empty extra_files_path directories associated with the selected input.
+ >>>
+ >>> p = ToolParameter.build(None, XML('''
+ ...
+ ...
+ ...
+ ... '''))
+ >>> t = p.validate(has_extra_hda)
+ Traceback (most recent call last):
+ ...
+ ValueError: The selected dataset's extra_files_path directory is non-empty or does exist, this tool expects empty extra_files_path directories associated with the selected input.
+ >>> t = p.validate(has_no_extra_hda)
+ """
@classmethod
def from_element(cls, param, elem):
- return cls(elem.get('message', None))
+ message = elem.get('message', None)
+ 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 validate(self, value, trans=None):
if value:
- if value.get_total_size() == value.get_size():
- if self.message is None:
- self.message = "The selected dataset's extra_files_path directory is empty or does not exist, this tool expects non-empty extra_files_path directories associated with the selected input."
- raise ValueError(self.message)
+ super().validate(value.get_total_size() != value.get_size())
class MetadataValidator(Validator):
"""
Validator that checks for missing metadata
+
+ >>> from galaxy.datatypes.registry import example_datatype_registry_for_sample
+ >>> from galaxy.model import History, HistoryDatasetAssociation, set_datatypes_registry
+ >>> from galaxy.model.mapping import init
+ >>> from galaxy.util import XML
+ >>> from galaxy.tools.parameters.basic import ToolParameter
+ >>>
+ >>> sa_session = init("/tmp", "sqlite:///:memory:", create_tables=True).session
+ >>> hist = History()
+ >>> sa_session.add(hist)
+ >>> sa_session.flush()
+ >>> set_datatypes_registry(example_datatype_registry_for_sample())
+ >>> hda = hist.add_dataset(HistoryDatasetAssociation(id=1, extension='interval', create_dataset=True, sa_session=sa_session))
+ >>> hda.set_dataset_state(model.Dataset.states.OK)
+ >>> # TODO I did not find a way to remove a metadata from the hda, therefore I used two parameters, ideas?
+ >>> p = ToolParameter.build(None, XML('''
+ ...
+ ...
+ ...
+ ... '''))
+ >>> p2 = ToolParameter.build(None, XML('''
+ ...
+ ...
+ ...
+ ... '''))
+ >>> t = p.validate(hda)
+ >>> t = p2.validate(hda)
+ Traceback (most recent call last):
+ ...
+ ValueError: Metadata missing, click the pencil icon in the history item to edit / save the metadata attributes
+ >>> p = ToolParameter.build(None, XML('''
+ ...
+ ...
+ ...
+ ... '''))
+ >>> p2 = ToolParameter.build(None, XML('''
+ ...
+ ...
+ ...
+ ... '''))
+ >>> t = p.validate(hda)
+ Traceback (most recent call last):
+ ...
+ ValueError: Metadata missing, click the pencil icon in the history item to edit / save the metadata attributes
+ >>> t = p2.validate(hda)
"""
requires_dataset_metadata = True
- def __init__(self, message=None, check="", skip=""):
- self.message = message
+ @classmethod
+ def from_element(cls, param, elem):
+ message = elem.get('message', None)
+ if not message:
+ # TODO message not useful for negate="true" .. but maybe OK since the validator itself is not useful then
+ message = "Metadata missing, click the pencil icon in the history item to edit / save the metadata attributes"
+ 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'):
+ super().__init__(message, negate)
self.check = check.split(",")
self.skip = skip.split(",")
- @classmethod
- def from_element(cls, param, elem):
- return cls(message=elem.get('message', None), check=elem.get('check', ""), skip=elem.get('skip', ""))
-
def validate(self, value, trans=None):
if value:
- if not isinstance(value, model.DatasetInstance):
- raise ValueError('A non-dataset value was provided.')
- if value.missing_meta(check=self.check, skip=self.skip):
- if self.message is None:
- self.message = "Metadata missing, click the pencil icon in the history item to edit / save the metadata attributes"
- raise ValueError(self.message)
+ # TODO why this validator checks for isinstance(value, model.DatasetInstance)
+ super().validate(isinstance(value, model.DatasetInstance) and not value.missing_meta(check=self.check, skip=self.skip))
class UnspecifiedBuildValidator(Validator):
"""
Validator that checks for dbkey not equal to '?'
+
+ >>> from galaxy.datatypes.registry import example_datatype_registry_for_sample
+ >>> from galaxy.model import History, HistoryDatasetAssociation, set_datatypes_registry
+ >>> from galaxy.model.mapping import init
+ >>> from galaxy.util import XML
+ >>> from galaxy.tools.parameters.basic import ToolParameter
+ >>>
+ >>> sa_session = init("/tmp", "sqlite:///:memory:", create_tables=True).session
+ >>> hist = History()
+ >>> sa_session.add(hist)
+ >>> sa_session.flush()
+ >>> set_datatypes_registry(example_datatype_registry_for_sample())
+ >>> has_dbkey_hda = hist.add_dataset(HistoryDatasetAssociation(id=1, extension='interval', create_dataset=True, sa_session=sa_session))
+ >>> has_dbkey_hda.set_dataset_state(model.Dataset.states.OK)
+ >>> has_dbkey_hda.metadata.dbkey = 'hg19'
+ >>> has_no_dbkey_hda = hist.add_dataset(HistoryDatasetAssociation(id=2, extension='interval', create_dataset=True, sa_session=sa_session))
+ >>> has_no_dbkey_hda.set_dataset_state(model.Dataset.states.OK)
+ >>>
+ >>> p = ToolParameter.build(None, XML('''
+ ...
+ ...
+ ...
+ ... '''))
+ >>> t = p.validate(has_dbkey_hda)
+ >>> t = p.validate(has_no_dbkey_hda)
+ Traceback (most recent call last):
+ ...
+ ValueError: Unspecified genome build, click the pencil icon in the history item to set the genome build
+ >>>
+ >>> p = ToolParameter.build(None, XML('''
+ ...
+ ...
+ ...
+ ... '''))
+ >>> t = p.validate(has_dbkey_hda)
+ Traceback (most recent call last):
+ ...
+ ValueError: Specified genome build, click the pencil icon in the history item to remove the genome build
+ >>> t = p.validate(has_no_dbkey_hda)
"""
requires_dataset_metadata = True
- def __init__(self, message=None):
- if message is None:
- self.message = "Unspecified genome build, click the pencil icon in the history item to set the genome build"
- else:
- self.message = message
-
@classmethod
def from_element(cls, param, elem):
- return cls(elem.get('message', None))
+ message = elem.get('message', None)
+ 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 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]
- if dbkey == '?':
- raise ValueError(self.message)
+ super().validate(dbkey != '?')
class NoOptionsValidator(Validator):
- """Validator that checks for empty select list"""
+ """
+ Validator that checks for empty select list
- def __init__(self, message=None):
- self.message = message
+ >>> from galaxy.util import XML
+ >>> from galaxy.tools.parameters.basic import ToolParameter
+ >>> p = ToolParameter.build(None, XML('''
+ ...
+ ...
+ ...
+ ... '''))
+ >>> t = p.validate('foo')
+ >>> t = p.validate(None)
+ Traceback (most recent call last):
+ ...
+ ValueError: No options available for selection
+ >>>
+ >>> p = ToolParameter.build(None, XML('''
+ ...
+ ...
+ ...
+ ...
+ ... '''))
+ >>> t = p.validate('foo')
+ Traceback (most recent call last):
+ ...
+ ValueError: Options available for selection
+ >>> t = p.validate(None)
+ """
@classmethod
def from_element(cls, param, elem):
- return cls(elem.get('message', None))
+ message = elem.get('message', None)
+ 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 validate(self, value, trans=None):
- if value is None:
- if self.message is None:
- self.message = "No options available for selection"
- raise ValueError(self.message)
+ super().validate(value is not None)
class EmptyTextfieldValidator(Validator):
- """Validator that checks for empty text field"""
+ """
+ Validator that checks for empty text field
- def __init__(self, message=None):
- self.message = message
+ >>> from galaxy.util import XML
+ >>> from galaxy.tools.parameters.basic import ToolParameter
+ >>> p = ToolParameter.build(None, XML('''
+ ...
+ ...
+ ...
+ ... '''))
+ >>> t = p.validate("")
+ Traceback (most recent call last):
+ ...
+ ValueError: Field requires a value
+ >>> p = ToolParameter.build(None, XML('''
+ ...
+ ...
+ ...
+ ... '''))
+ >>> t = p.validate("foo")
+ Traceback (most recent call last):
+ ...
+ ValueError: Field must not set a value
+ >>> t = p.validate("")
+ """
@classmethod
def from_element(cls, param, elem):
- return cls(elem.get('message', None))
+ message = elem.get('message', None)
+ negate = elem.get('negate', 'false')
+ if not message:
+ if negate == 'false':
+ message = elem.get('message', "Field requires a value")
+ else:
+ message = elem.get('message', "Field must not set a value")
+ return cls(message, negate)
def validate(self, value, trans=None):
- if value == '':
- if self.message is None:
- self.message = "Field requires a value"
- raise ValueError(self.message)
+ super().validate(value != '')
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
@@ -403,30 +744,30 @@ class MetadataInFileColumnValidator(Validator):
line_startswith = elem.get("line_startswith", None)
if line_startswith:
line_startswith = line_startswith.strip()
- return cls(filename, metadata_name, metadata_column, message, line_startswith, split)
+ 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"):
+ def __init__(self, filename, metadata_name, metadata_column, message="Value for metadata not found.", line_startswith=None, split="\t", negate="false"):
+ super().__init__(message, negate)
self.metadata_name = metadata_name
- self.message = message
- self.valid_values = []
+ self.valid_values = set()
for line in open(filename):
if line_startswith is None or line.startswith(line_startswith):
fields = line.split(split)
if metadata_column < len(fields):
- self.valid_values.append(fields[metadata_column].strip())
+ self.valid_values.add(fields[metadata_column].strip())
def validate(self, value, trans=None):
if not value:
return
- if hasattr(value, "metadata"):
- if value.metadata.spec[self.metadata_name].param.to_string(value.metadata.get(self.metadata_name)) in self.valid_values:
- return
- raise ValueError(self.message)
+ super().validate(value.metadata.spec[self.metadata_name].param.to_string(value.metadata.get(self.metadata_name)) in self.valid_values)
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)
"""
@classmethod
@@ -440,13 +781,11 @@ class ValueInDataTableColumnValidator(Validator):
except ValueError:
pass
message = elem.get("message", f"Value was not found in {table_name}.")
- line_startswith = elem.get("line_startswith", None)
- if line_startswith:
- line_startswith = line_startswith.strip()
- return cls(tool_data_table, column, message, line_startswith)
+ negate = elem.get('negate', 'false')
+ return cls(tool_data_table, column, message, negate)
- def __init__(self, tool_data_table, column, message="Value not found.", line_startswith=None):
- self.message = message
+ def __init__(self, tool_data_table, column, message="Value not found.", negate='false'):
+ super().__init__(message, negate)
self.valid_values = []
self._data_table_content_version = None
self._tool_data_table = tool_data_table
@@ -460,7 +799,7 @@ class ValueInDataTableColumnValidator(Validator):
self.valid_values = []
for fields in data_fields:
if self._column < len(fields):
- self.valid_values.append(fields[self._metadata_column])
+ self.valid_values.append(fields[self._column])
def validate(self, value, trans=None):
if not value:
@@ -468,31 +807,34 @@ class ValueInDataTableColumnValidator(Validator):
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()
- if value in self.valid_values:
- return
- raise ValueError(self.message)
+ super().validate(value in self.valid_values)
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)
"""
- def __init__(self, tool_data_table, metadata_column, message="Value already present.", line_startswith=None):
- super().__init__(tool_data_table, metadata_column, message, line_startswith)
+ def __init__(self, tool_data_table, metadata_column, message="Value already present.", negate='false'):
+ super().__init__(tool_data_table, metadata_column, message, negate)
def validate(self, value, trans=None):
try:
- super(ValueInDataTableColumnValidator, self).validate(value, trans)
+ super().validate(value)
except ValueError:
return
else:
raise ValueError(self.message)
-class MetadataInDataTableColumnValidator(Validator):
+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
@@ -504,59 +846,39 @@ class MetadataInDataTableColumnValidator(Validator):
metadata_name = elem.get("metadata_name", None)
if metadata_name:
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}.")
- line_startswith = elem.get("line_startswith", None)
- if line_startswith:
- line_startswith = line_startswith.strip()
- return cls(tool_data_table, metadata_name, metadata_column, message, line_startswith)
+ 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.", line_startswith=None):
+ def __init__(self, tool_data_table, metadata_name, metadata_column, message="Value for metadata not found.", negate="false"):
+ super().__init__(tool_data_table, metadata_column, message, negate)
self.metadata_name = metadata_name
- self.message = message
- self.valid_values = []
- 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._metadata_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._metadata_column < len(fields):
- self.valid_values.append(fields[self._metadata_column])
def validate(self, value, trans=None):
- if not value:
- return
- if hasattr(value, "metadata"):
- if not self._tool_data_table.is_current_version(self._data_table_content_version):
- log.debug('MetadataInDataTableColumnValidator values are out of sync with data table (%s), updating validator.', self._tool_data_table.name)
- self._load_values()
- if value.metadata.spec[self.metadata_name].param.to_string(value.metadata.get(self.metadata_name)) in self.valid_values:
- return
- raise ValueError(self.message)
+ super().validate(value.metadata.spec[self.metadata_name].param.to_string(value.metadata.get(self.metadata_name)), trans)
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
- def __init__(self, tool_data_table, metadata_name, metadata_column, message="Value for metadata not found.", line_startswith=None):
- super(MetadataInDataTableColumnValidator, self).__init__(tool_data_table, metadata_name, metadata_column, message, line_startswith)
+ def __init__(self, tool_data_table, metadata_name, metadata_column, message="Value for metadata not found.", negate="false"):
+ super().__init__(tool_data_table, metadata_name, metadata_column, message, negate)
def validate(self, value, trans=None):
try:
- super(MetadataInDataTableColumnValidator, self).validate(value, trans)
+ super().validate(value, trans)
except ValueError:
return
else:
@@ -565,7 +887,9 @@ class MetadataNotInDataTableColumnValidator(MetadataInDataTableColumnValidator):
class MetadataInRangeValidator(InRangeValidator):
"""
- Validator that ensures metadata is in a specified range
+ 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
@@ -574,14 +898,16 @@ class MetadataInRangeValidator(InRangeValidator):
metadata_name = elem.get('metadata_name', None)
assert metadata_name, "dataset_metadata_in_range validator requires metadata_name attribute."
metadata_name = metadata_name.strip()
- return cls(metadata_name,
- elem.get('message', None), elem.get('min'),
- elem.get('max'), elem.get('exclude_min', 'false'),
- elem.get('exclude_max', 'false'))
+ ret = cls(metadata_name, elem.get('message', None),
+ 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=False, exclude_max=False):
+ def __init__(self, metadata_name, message, range_min, range_max, exclude_min, exclude_max, negate):
self.metadata_name = metadata_name
- super().__init__(message, range_min, range_max, exclude_min, exclude_max)
+ super().__init__(message, range_min, range_max, exclude_min, exclude_max, negate)
def validate(self, value, trans=None):
if value:
@@ -593,6 +919,7 @@ class MetadataInRangeValidator(InRangeValidator):
raise ValueError(f'{self.metadata_name} Metadata missing')
except ValueError:
raise ValueError(f'{self.metadata_name} must be a float or an integer')
+ log.error(f"MetadataInRangeValidato.validate value_to_check {value_to_check}")
super().validate(value_to_check, trans)
@@ -607,7 +934,6 @@ validator_types = dict(
empty_field=EmptyTextfieldValidator,
empty_dataset=DatasetEmptyValidator,
empty_extra_files_path=DatasetExtraFilesPathEmptyValidator,
- dataset_metadata_in_file=MetadataInFileColumnValidator,
dataset_metadata_in_data_table=MetadataInDataTableColumnValidator,
dataset_metadata_not_in_data_table=MetadataNotInDataTableColumnValidator,
dataset_metadata_in_range=MetadataInRangeValidator,
@@ -616,6 +942,11 @@ validator_types = dict(
dataset_ok_validator=DatasetOkValidator,
)
+deprecated_validator_types = dict(
+ dataset_metadata_in_file=MetadataInFileColumnValidator
+)
+validator_types.update(deprecated_validator_types)
+
def get_suite():
"""Get unittest suite for this module"""
diff --git a/packages/test.sh b/packages/test.sh
index 0c9ddfe5f2f..30bcb3d0033 100755
--- a/packages/test.sh
+++ b/packages/test.sh
@@ -1,6 +1,6 @@
#!/bin/bash
-set -e
+set -ex
# Don't display the pip progress bar when running under CI
[ "$CI" = 'true' ] && export PIP_PROGRESS_BAR=off
diff --git a/test/functional/tools/samples_tool_conf.xml b/test/functional/tools/samples_tool_conf.xml
index 1110bfe4af0..69847fa6c1a 100644
--- a/test/functional/tools/samples_tool_conf.xml
+++ b/test/functional/tools/samples_tool_conf.xml
@@ -136,6 +136,9 @@
+
+
+
diff --git a/test/functional/tools/validation_dataset_metadata_in_file.xml b/test/functional/tools/validation_dataset_metadata_in_file.xml
new file mode 100644
index 00000000000..1192046f640
--- /dev/null
+++ b/test/functional/tools/validation_dataset_metadata_in_file.xml
@@ -0,0 +1,31 @@
+
+ out1
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/functional/tools/validation_metadata_in_datatable.xml b/test/functional/tools/validation_metadata_in_datatable.xml
new file mode 100644
index 00000000000..7e3e6933c13
--- /dev/null
+++ b/test/functional/tools/validation_metadata_in_datatable.xml
@@ -0,0 +1,37 @@
+
+ out1
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/functional/tools/validation_value_in_datatable.xml b/test/functional/tools/validation_value_in_datatable.xml
new file mode 100644
index 00000000000..86ef401743a
--- /dev/null
+++ b/test/functional/tools/validation_value_in_datatable.xml
@@ -0,0 +1,35 @@
+
+ out1
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+