From 017e181063f419d61d5055b8fa2d19bb71453bf8 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Fri, 12 Jun 2015 15:41:10 -0400 Subject: [PATCH 1/6] Add a GreaterThanZeroValidator for numeric tool parameters. --- lib/galaxy/tools/parameters/validation.py | 35 +++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/tools/parameters/validation.py b/lib/galaxy/tools/parameters/validation.py index 588949d63eb..132d439410d 100644 --- a/lib/galaxy/tools/parameters/validation.py +++ b/lib/galaxy/tools/parameters/validation.py @@ -324,6 +324,37 @@ class MetadataInDataTableColumnValidator( Validator ): raise ValueError( self.message ) +class GreaterThanZeroValidator( Validator ): + """ + Validator that ensures a number is greater than zero. + + >>> from galaxy.tools.parameters import ToolParameter + >>> p = ToolParameter.build( None, XML( ''' + ... + ... + ... + ... ''' ) ) + >>> t = p.validate( 1 ) + >>> t = p.validate( 0.5 ) + >>> t = p.validate( 0.0 ) + Traceback (most recent call last): + ... + ValueError: Value must be greater than zero + """ + + 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 <= 0: + if self.message is None: + self.message = "Value must be greater than zero" + raise ValueError( self.message ) + validator_types = dict( expression=ExpressionValidator, regex=RegexValidator, in_range=InRangeValidator, @@ -334,10 +365,10 @@ validator_types = dict( expression=ExpressionValidator, empty_field=EmptyTextfieldValidator, dataset_metadata_in_file=MetadataInFileColumnValidator, dataset_metadata_in_data_table=MetadataInDataTableColumnValidator, - dataset_ok_validator=DatasetOkValidator ) + dataset_ok_validator=DatasetOkValidator, + greater_than_zero=GreaterThanZeroValidator ) def get_suite(): """Get unittest suite for this module""" import doctest, sys return doctest.DocTestSuite( sys.modules[__name__] ) - From 2a7f1f741e466223cf3d1e7c414ede5ee7ac4c5a Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Mon, 15 Jun 2015 12:16:37 -0400 Subject: [PATCH 2/6] Enhance the InRangeValidator to enalbe the use of new strict_min and strict_max attributes which, if set to true, will exclude the range end points. For example, if strict_min is true, min < value, whereas if strict_min is false (the default), then min <= value. Combinations of strict_min and strict_max are allowed. --- lib/galaxy/tools/parameters/validation.py | 128 +++++++++++++++------- 1 file changed, 88 insertions(+), 40 deletions(-) diff --git a/lib/galaxy/tools/parameters/validation.py b/lib/galaxy/tools/parameters/validation.py index 132d439410d..0bab4837be8 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,54 @@ 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( 'strict_min', 'false' ), + elem.get( 'strict_max', 'false' ) ) + + def __init__( self, message, range_min, range_max, strict_min=False, strict_max=False ): + """ + When the optional strict_min and strict_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 strict_min and strict_max + values are allowed. + """ self.min = float( range_min if range_min is not None else '-inf' ) + self.strict_min = util.asbool( strict_min ) self.max = float( range_max if range_max is not None else 'inf' ) + self.strict_max = util.asbool( strict_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.strict_min and self.strict_max: + op1 = '>' + op2 = '<' + if self.strict_min and not self.strict_max: + op1 = '>' + elif not self.strict_min and self.strict_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.strict_min and self.strict_max: + if not( self.min < float( value ) < self.max ): + raise ValueError( self.message ) + elif self.strict_min and not self.strict_max: + if not( self.min < float( value ) <= self.max ): + raise ValueError( self.message ) + elif not self.strict_min and self.strict_max: + if not( self.min <= float( value ) < self.max ): + raise ValueError( self.message ) + elif not self.strict_min and not self.strict_max: + if not( self.min <= float( value ) <= self.max ): + raise ValueError( self.message ) + class LengthValidator( Validator ): """ @@ -139,9 +191,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 +204,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 +254,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 +279,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 +333,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 +343,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 +351,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,38 +404,6 @@ class MetadataInDataTableColumnValidator( Validator ): return raise ValueError( self.message ) - -class GreaterThanZeroValidator( Validator ): - """ - Validator that ensures a number is greater than zero. - - >>> from galaxy.tools.parameters import ToolParameter - >>> p = ToolParameter.build( None, XML( ''' - ... - ... - ... - ... ''' ) ) - >>> t = p.validate( 1 ) - >>> t = p.validate( 0.5 ) - >>> t = p.validate( 0.0 ) - Traceback (most recent call last): - ... - ValueError: Value must be greater than zero - """ - - 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 <= 0: - if self.message is None: - self.message = "Value must be greater than zero" - raise ValueError( self.message ) - validator_types = dict( expression=ExpressionValidator, regex=RegexValidator, in_range=InRangeValidator, @@ -365,8 +414,7 @@ validator_types = dict( expression=ExpressionValidator, empty_field=EmptyTextfieldValidator, dataset_metadata_in_file=MetadataInFileColumnValidator, dataset_metadata_in_data_table=MetadataInDataTableColumnValidator, - dataset_ok_validator=DatasetOkValidator, - greater_than_zero=GreaterThanZeroValidator ) + dataset_ok_validator=DatasetOkValidator ) def get_suite(): """Get unittest suite for this module""" From 2a3ad8148b95dc964620e8c6f0caff43c2c1d561 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Mon, 15 Jun 2015 13:21:24 -0400 Subject: [PATCH 3/6] Simplify the checks for setting strict_min and strict_max operators. --- lib/galaxy/tools/parameters/validation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/galaxy/tools/parameters/validation.py b/lib/galaxy/tools/parameters/validation.py index 0bab4837be8..13647e3db22 100644 --- a/lib/galaxy/tools/parameters/validation.py +++ b/lib/galaxy/tools/parameters/validation.py @@ -149,9 +149,9 @@ class InRangeValidator( Validator ): if self.strict_min and self.strict_max: op1 = '>' op2 = '<' - if self.strict_min and not self.strict_max: + if self.strict_min: op1 = '>' - elif not self.strict_min and self.strict_max: + elif self.strict_max: op2 = '<' self.message = message or "Value must be %s %s and %s %s" % ( op1, self_min_str, op2, self_max_str ) From aa2a7c9fb3e5a4830720b6f13f1fe705ff5c93e0 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Mon, 15 Jun 2015 14:46:36 -0400 Subject: [PATCH 4/6] Added @nsoranzo's simplified validation logic. --- lib/galaxy/tools/parameters/validation.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/galaxy/tools/parameters/validation.py b/lib/galaxy/tools/parameters/validation.py index 13647e3db22..4f685e42a60 100644 --- a/lib/galaxy/tools/parameters/validation.py +++ b/lib/galaxy/tools/parameters/validation.py @@ -156,17 +156,17 @@ class InRangeValidator( Validator ): 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 self.strict_min and self.strict_max: - if not( self.min < float( value ) < self.max ): + if self.strict_min: + if not self.min < float( value ): raise ValueError( self.message ) - elif self.strict_min and not self.strict_max: - if not( self.min < float( value ) <= self.max ): + else: + if not self.min <= float( value ): raise ValueError( self.message ) - elif not self.strict_min and self.strict_max: - if not( self.min <= float( value ) < self.max ): + if self.strict_max: + if not float( value ) < self.max: raise ValueError( self.message ) - elif not self.strict_min and not self.strict_max: - if not( self.min <= float( value ) <= self.max ): + else: + if not float( value ) <= self.max: raise ValueError( self.message ) From db5c81acd78728dee37173aaf034024af2e5f302 Mon Sep 17 00:00:00 2001 From: Nicola Soranzo Date: Mon, 15 Jun 2015 20:04:06 +0100 Subject: [PATCH 5/6] Simplify strict_min strict_max logic. --- lib/galaxy/tools/parameters/validation.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/galaxy/tools/parameters/validation.py b/lib/galaxy/tools/parameters/validation.py index 4f685e42a60..360b8e1970d 100644 --- a/lib/galaxy/tools/parameters/validation.py +++ b/lib/galaxy/tools/parameters/validation.py @@ -146,12 +146,9 @@ class InRangeValidator( Validator ): self_max_str = str( self.max ).rstrip( '0' ).rstrip( '.' ) op1 = '>=' op2 = '<=' - if self.strict_min and self.strict_max: - op1 = '>' - op2 = '<' if self.strict_min: op1 = '>' - elif self.strict_max: + if self.strict_max: op2 = '<' self.message = message or "Value must be %s %s and %s %s" % ( op1, self_min_str, op2, self_max_str ) From 373c98098c884f85ef24a795334d4e49c10d4f58 Mon Sep 17 00:00:00 2001 From: Greg Von Kuster Date: Tue, 16 Jun 2015 13:55:06 -0400 Subject: [PATCH 6/6] Change strict_mXX to exclude_mXX for InRangeValidator enhancement. --- lib/galaxy/tools/parameters/validation.py | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/galaxy/tools/parameters/validation.py b/lib/galaxy/tools/parameters/validation.py index 360b8e1970d..5641b86e9cf 100644 --- a/lib/galaxy/tools/parameters/validation.py +++ b/lib/galaxy/tools/parameters/validation.py @@ -107,7 +107,7 @@ class InRangeValidator( Validator ): >>> from galaxy.tools.parameters import ToolParameter >>> p = ToolParameter.build( None, XML( ''' ... - ... + ... ... ... ''' ) ) >>> t = p.validate( 10 ) @@ -125,41 +125,41 @@ class InRangeValidator( Validator ): @classmethod def from_element( cls, param, elem ): return cls( elem.get( 'message', None ), elem.get( 'min' ), - elem.get( 'max' ), elem.get( 'strict_min', 'false' ), - elem.get( 'strict_max', 'false' ) ) + elem.get( 'max' ), elem.get( 'exclude_min', 'false' ), + elem.get( 'exclude_max', 'false' ) ) - def __init__( self, message, range_min, range_max, strict_min=False, strict_max=False ): + def __init__( self, message, range_min, range_max, exclude_min=False, exclude_max=False ): """ - When the optional strict_min and strict_max attributes are set + 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 strict_min and strict_max + (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.strict_min = util.asbool( strict_min ) + self.exclude_min = util.asbool( exclude_min ) self.max = float( range_max if range_max is not None else 'inf' ) - self.strict_max = util.asbool( strict_max ) + 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( '.' ) op1 = '>=' op2 = '<=' - if self.strict_min: + if self.exclude_min: op1 = '>' - if self.strict_max: + 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 self.strict_min: + 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.strict_max: + if self.exclude_max: if not float( value ) < self.max: raise ValueError( self.message ) else: