diff --git a/lib/galaxy/tools/parameters/validation.py b/lib/galaxy/tools/parameters/validation.py index 588949d63eb..5641b86e9cf 100644 --- a/lib/galaxy/tools/parameters/validation.py +++ b/lib/galaxy/tools/parameters/validation.py @@ -5,25 +5,32 @@ Classes related to parameter validation. import os, re, logging from xml.etree.ElementTree import XML from galaxy import model +from galaxy import util log = logging.getLogger( __name__ ) + class LateValidationError( Exception ): + def __init__( self, message ): self.message = message + class Validator( object ): """ A validator checks that a value meets some conditions OR raises ValueError """ + @classmethod def from_element( cls, 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, history=None ): raise TypeError( "Abstract Method" ) + class RegexValidator( Validator ): """ Validator that evaluates a regular expression @@ -41,18 +48,22 @@ class RegexValidator( Validator ): ... ValueError: Not gonna happen """ + @classmethod def from_element( cls, param, elem ): return cls( elem.get( 'message' ), elem.text ) + def __init__( self, message, expression ): self.message = message # Compile later. RE objects used to not be thread safe. Not sure about # the sre module. self.expression = expression + def validate( self, value, history=None ): if re.match( self.expression, value ) is None: raise ValueError( self.message ) + class ExpressionValidator( Validator ): """ Validator that evaluates a python expression using the value @@ -70,14 +81,17 @@ class ExpressionValidator( Validator ): ... ValueError: Not gonna happen """ + @classmethod def from_element( cls, param, elem ): return cls( elem.get( 'message' ), elem.text, elem.get( 'substitute_value_in_message' ) ) + def __init__( self, message, expression, substitute_value_in_message ): self.message = message self.substitute_value_in_message = substitute_value_in_message # Save compiled expression, code objects are thread safe (right?) self.expression = compile( expression, '', 'eval' ) + def validate( self, value, history=None ): if not( eval( self.expression, dict( value=value ) ) ): message = self.message @@ -85,6 +99,7 @@ class ExpressionValidator( Validator ): message = message % value raise ValueError( message ) + class InRangeValidator( Validator ): """ Validator that ensures a number is in a specific range @@ -92,10 +107,13 @@ class InRangeValidator( Validator ): >>> from galaxy.tools.parameters import ToolParameter >>> p = ToolParameter.build( None, XML( ''' ... - ... + ... ... ... ''' ) ) >>> t = p.validate( 10 ) + Traceback (most recent call last): + ... + ValueError: Not gonna happen >>> t = p.validate( 15 ) >>> t = p.validate( 20 ) >>> t = p.validate( 21 ) @@ -103,20 +121,51 @@ class InRangeValidator( Validator ): ... ValueError: Not gonna happen """ + @classmethod def from_element( cls, param, elem ): - return cls( elem.get( 'message', None ), elem.get( 'min' ), elem.get( 'max' ) ) - def __init__( self, message, range_min, range_max ): + return cls( elem.get( 'message', None ), elem.get( 'min' ), + elem.get( 'max' ), elem.get( 'exclude_min', 'false' ), + elem.get( 'exclude_max', 'false' ) ) + + def __init__( self, message, range_min, range_max, exclude_min=False, exclude_max=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 = float( 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.exclude_max = util.asbool( exclude_max ) assert self.min <= 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( '.' ) - self.message = message or "Value must be between %s and %s" % ( self_min_str, self_max_str ) + op1 = '>=' + op2 = '<=' + if self.exclude_min: + op1 = '>' + if self.exclude_max: + op2 = '<' + self.message = message or "Value must be %s %s and %s %s" % ( op1, self_min_str, op2, self_max_str ) + def validate( self, value, history=None ): - if not( self.min <= float( value ) <= self.max ): - raise ValueError( self.message ) + 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 ) + class LengthValidator( Validator ): """ @@ -139,9 +188,11 @@ class LengthValidator( Validator ): ... ValueError: Must have length no more than 8 """ + @classmethod def from_element( cls, param, elem ): return cls( elem.get( 'message', None ), elem.get( 'min', None ), elem.get( 'max', None ) ) + def __init__( self, message, length_min, length_max ): self.message = message if length_min is not None: @@ -150,38 +201,47 @@ class LengthValidator( Validator ): length_max = int( length_max ) self.min = length_min self.max = length_max + def validate( self, value, history=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 ) ) + class DatasetOkValidator( Validator ): """ Validator that checks if a dataset is in an 'ok' state """ + def __init__( self, message=None ): self.message = message + @classmethod def from_element( cls, param, elem ): return cls( elem.get( 'message', None ) ) + def validate( self, value, history=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 ) + class MetadataValidator( Validator ): """ Validator that checks for missing metadata """ + def __init__( self, message = None, check = "", skip = "" ): self.message = message 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, history=None ): if value: if not isinstance( value, model.DatasetInstance ): @@ -191,18 +251,22 @@ class MetadataValidator( Validator ): self.message = "Metadata missing, click the pencil icon in the history item to edit / save the metadata attributes" raise ValueError( self.message ) + class UnspecifiedBuildValidator( Validator ): """ Validator that checks for dbkey not equal to '?' """ + 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 ) ) + def validate( self, value, history=None ): #if value is None, we cannot validate if value: @@ -212,36 +276,46 @@ class UnspecifiedBuildValidator( Validator ): if dbkey == '?': raise ValueError( self.message ) + class NoOptionsValidator( Validator ): """Validator that checks for empty select list""" + def __init__( self, message=None ): self.message = message + @classmethod def from_element( cls, param, elem ): return cls( elem.get( 'message', None ) ) + def validate( self, value, history=None ): if value is None: if self.message is None: self.message = "No options available for selection" raise ValueError( self.message ) + class EmptyTextfieldValidator( Validator ): """Validator that checks for empty text field""" + def __init__( self, message=None ): self.message = message + @classmethod def from_element( cls, param, elem ): return cls( elem.get( 'message', None ) ) + def validate( self, value, history=None ): if value == '': if self.message is None: self.message = "Field requires a value" raise ValueError( self.message ) + class MetadataInFileColumnValidator( Validator ): """ Validator that checks if the value for a dataset's metadata item exists in a file. """ + @classmethod def from_element( cls, param, elem ): filename = elem.get( "filename", None ) @@ -256,6 +330,7 @@ class MetadataInFileColumnValidator( Validator ): if line_startswith: line_startswith = line_startswith.strip() return cls( filename, metadata_name, metadata_column, message, line_startswith ) + def __init__( self, filename, metadata_name, metadata_column, message="Value for metadata not found.", line_startswith=None ): self.metadata_name = metadata_name self.message = message @@ -265,6 +340,7 @@ class MetadataInFileColumnValidator( Validator ): fields = line.split( '\t' ) if metadata_column < len( fields ): self.valid_values.append( fields[metadata_column].strip() ) + def validate( self, value, history = None ): if not value: return if hasattr( value, "metadata" ): @@ -272,10 +348,12 @@ class MetadataInFileColumnValidator( Validator ): return raise ValueError( self.message ) + class MetadataInDataTableColumnValidator( Validator ): """ Validator that checks if the value for a dataset's metadata item exists in a file. """ + @classmethod def from_element( cls, param, elem ): table_name = elem.get( "table_name", None ) @@ -323,7 +401,6 @@ class MetadataInDataTableColumnValidator( Validator ): return raise ValueError( self.message ) - validator_types = dict( expression=ExpressionValidator, regex=RegexValidator, in_range=InRangeValidator, @@ -340,4 +417,3 @@ def get_suite(): """Get unittest suite for this module""" import doctest, sys return doctest.DocTestSuite( sys.modules[__name__] ) -