PageRenderTime 40ms CodeModel.GetById 17ms app.highlight 18ms RepoModel.GetById 1ms app.codeStats 1ms

/lib/galaxy/tools/parameters/validation.py

https://bitbucket.org/cistrome/cistrome-harvard/
Python | 343 lines | 324 code | 5 blank | 14 comment | 13 complexity | 8f3a474528408342733ba236278869ae MD5 | raw file
  1"""
  2Classes related to parameter validation.
  3"""
  4
  5import os, re, logging
  6from elementtree.ElementTree import XML
  7from galaxy import model
  8
  9log = logging.getLogger( __name__ )
 10
 11class LateValidationError( Exception ):
 12    def __init__( self, message ):
 13        self.message = message
 14
 15class Validator( object ):
 16    """
 17    A validator checks that a value meets some conditions OR raises ValueError
 18    """
 19    @classmethod
 20    def from_element( cls, param, elem ):
 21        type = elem.get( 'type', None )
 22        assert type is not None, "Required 'type' attribute missing from validator"
 23        return validator_types[type].from_element( param, elem )
 24    def validate( self, value, history=None ):
 25        raise TypeError( "Abstract Method" )
 26
 27class RegexValidator( Validator ):
 28    """
 29    Validator that evaluates a regular expression
 30
 31    >>> from galaxy.tools.parameters import ToolParameter
 32    >>> p = ToolParameter.build( None, XML( '''
 33    ... <param name="blah" type="text" size="10" value="10">
 34    ...     <validator type="regex" message="Not gonna happen">[Ff]oo</validator>
 35    ... </param>
 36    ... ''' ) )
 37    >>> t = p.validate( "Foo" )
 38    >>> t = p.validate( "foo" )
 39    >>> t = p.validate( "Fop" )
 40    Traceback (most recent call last):
 41        ...
 42    ValueError: Not gonna happen
 43    """
 44    @classmethod
 45    def from_element( cls, param, elem ):
 46        return cls( elem.get( 'message' ), elem.text )
 47    def __init__( self, message, expression ):
 48        self.message = message
 49        # Compile later. RE objects used to not be thread safe. Not sure about
 50        # the sre module.
 51        self.expression = expression
 52    def validate( self, value, history=None ):
 53        if re.match( self.expression, value ) is None:
 54            raise ValueError( self.message )
 55
 56class ExpressionValidator( Validator ):
 57    """
 58    Validator that evaluates a python expression using the value
 59
 60    >>> from galaxy.tools.parameters import ToolParameter
 61    >>> p = ToolParameter.build( None, XML( '''
 62    ... <param name="blah" type="text" size="10" value="10">
 63    ...     <validator type="expression" message="Not gonna happen">value.lower() == "foo"</validator>
 64    ... </param>
 65    ... ''' ) )
 66    >>> t = p.validate( "Foo" )
 67    >>> t = p.validate( "foo" )
 68    >>> t = p.validate( "Fop" )
 69    Traceback (most recent call last):
 70        ...
 71    ValueError: Not gonna happen
 72    """
 73    @classmethod
 74    def from_element( cls, param, elem ):
 75        return cls( elem.get( 'message' ), elem.text, elem.get( 'substitute_value_in_message' ) )
 76    def __init__( self, message, expression, substitute_value_in_message ):
 77        self.message = message
 78        self.substitute_value_in_message = substitute_value_in_message
 79        # Save compiled expression, code objects are thread safe (right?)
 80        self.expression = compile( expression, '<string>', 'eval' )
 81    def validate( self, value, history=None ):
 82        if not( eval( self.expression, dict( value=value ) ) ):
 83            message = self.message
 84            if self.substitute_value_in_message:
 85                message = message % value
 86            raise ValueError( message )
 87
 88class InRangeValidator( Validator ):
 89    """
 90    Validator that ensures a number is in a specific range
 91
 92    >>> from galaxy.tools.parameters import ToolParameter
 93    >>> p = ToolParameter.build( None, XML( '''
 94    ... <param name="blah" type="integer" size="10" value="10">
 95    ...     <validator type="in_range" message="Not gonna happen" min="10" max="20"/>
 96    ... </param>
 97    ... ''' ) )
 98    >>> t = p.validate( 10 )
 99    >>> t = p.validate( 15 )
100    >>> t = p.validate( 20 )
101    >>> t = p.validate( 21 )
102    Traceback (most recent call last):
103        ...
104    ValueError: Not gonna happen
105    """
106    @classmethod
107    def from_element( cls, param, elem ):
108        return cls( elem.get( 'message', None ), elem.get( 'min' ), elem.get( 'max' ) )
109    def __init__( self, message, range_min, range_max ):
110        self.min = float( range_min if range_min is not None else '-inf' )
111        self.max = float( range_max if range_max is not None else 'inf' )
112        assert self.min <= self.max, 'min must be less than or equal to max'
113        # Remove unneeded 0s and decimal from floats to make message pretty.
114        self_min_str = str( self.min ).rstrip( '0' ).rstrip( '.' )
115        self_max_str = str( self.max ).rstrip( '0' ).rstrip( '.' )
116        self.message = message or "Value must be between %s and %s" % ( self_min_str, self_max_str )
117    def validate( self, value, history=None ):
118        if not( self.min <= float( value ) <= self.max ):
119            raise ValueError( self.message )
120
121class LengthValidator( Validator ):
122    """
123    Validator that ensures the length of the provided string (value) is in a specific range
124
125    >>> from galaxy.tools.parameters import ToolParameter
126    >>> p = ToolParameter.build( None, XML( '''
127    ... <param name="blah" type="text" size="10" value="foobar">
128    ...     <validator type="length" min="2" max="8"/>
129    ... </param>
130    ... ''' ) )
131    >>> t = p.validate( "foo" )
132    >>> t = p.validate( "bar" )
133    >>> t = p.validate( "f" )
134    Traceback (most recent call last):
135        ...
136    ValueError: Must have length of at least 2
137    >>> t = p.validate( "foobarbaz" )
138    Traceback (most recent call last):
139        ...
140    ValueError: Must have length no more than 8
141    """
142    @classmethod
143    def from_element( cls, param, elem ):
144        return cls( elem.get( 'message', None ), elem.get( 'min', None ), elem.get( 'max', None ) )
145    def __init__( self, message, length_min, length_max ):
146        self.message = message
147        if length_min is not None:
148            length_min = int( length_min )
149        if length_max is not None:
150            length_max = int( length_max )
151        self.min = length_min
152        self.max = length_max
153    def validate( self, value, history=None ):
154        if self.min is not None and len( value ) < self.min:
155            raise ValueError( self.message or ( "Must have length of at least %d" % self.min ) )
156        if self.max is not None and len( value ) > self.max:
157            raise ValueError( self.message or ( "Must have length no more than %d" % self.max ) )
158
159class DatasetOkValidator( Validator ):
160    """
161    Validator that checks if a dataset is in an 'ok' state
162    """
163    def __init__( self, message=None ):
164        self.message = message
165    @classmethod
166    def from_element( cls, param, elem ):
167        return cls( elem.get( 'message', None ) )
168    def validate( self, value, history=None ):
169        if value and value.state != model.Dataset.states.OK:
170            if self.message is None:
171                self.message = "The selected dataset is still being generated, select another dataset or wait until it is completed"
172            raise ValueError( self.message )
173
174class MetadataValidator( Validator ):
175    """
176    Validator that checks for missing metadata
177    """
178    def __init__( self, message = None, check = "", skip = "" ):
179        self.message = message
180        self.check = check.split( "," )
181        self.skip = skip.split( "," )
182    @classmethod
183    def from_element( cls, param, elem ):
184        return cls( message=elem.get( 'message', None ), check=elem.get( 'check', "" ), skip=elem.get( 'skip', "" ) )
185    def validate( self, value, history=None ):
186        if value:
187            if not isinstance( value, model.DatasetInstance ):
188                raise ValueError( 'A non-dataset value was provided.' )
189            if value.missing_meta( check = self.check, skip = self.skip ):
190                if self.message is None:
191                    self.message = "Metadata missing, click the pencil icon in the history item to edit / save the metadata attributes"
192                raise ValueError( self.message )
193
194class UnspecifiedBuildValidator( Validator ):
195    """
196    Validator that checks for dbkey not equal to '?'
197    """
198    def __init__( self, message=None ):
199        if message is None:
200            self.message = "Unspecified genome build, click the pencil icon in the history item to set the genome build"
201        else:
202            self.message = message
203    @classmethod
204    def from_element( cls, param, elem ):
205        return cls( elem.get( 'message', None ) )
206    def validate( self, value, history=None ):
207        #if value is None, we cannot validate
208        if value:
209            dbkey = value.metadata.dbkey
210            if isinstance( dbkey, list ):
211                dbkey = dbkey[0]
212            if dbkey == '?':
213                raise ValueError( self.message )
214
215class NoOptionsValidator( Validator ):
216    """Validator that checks for empty select list"""
217    def __init__( self, message=None ):
218        self.message = message
219    @classmethod
220    def from_element( cls, param, elem ):
221        return cls( elem.get( 'message', None ) )
222    def validate( self, value, history=None ):
223        if value is None:
224            if self.message is None:
225                self.message = "No options available for selection"
226            raise ValueError( self.message )
227
228class EmptyTextfieldValidator( Validator ):
229    """Validator that checks for empty text field"""
230    def __init__( self, message=None ):
231        self.message = message
232    @classmethod
233    def from_element( cls, param, elem ):
234        return cls( elem.get( 'message', None ) )
235    def validate( self, value, history=None ):
236        if value == '':
237            if self.message is None:
238                self.message = "Field requires a value"
239            raise ValueError( self.message )
240
241class MetadataInFileColumnValidator( Validator ):
242    """
243    Validator that checks if the value for a dataset's metadata item exists in a file.
244    """
245    @classmethod
246    def from_element( cls, param, elem ):
247        filename = elem.get( "filename", None )
248        if filename:
249            filename = "%s/%s" % ( param.tool.app.config.tool_data_path, filename.strip() )
250        metadata_name = elem.get( "metadata_name", None )
251        if metadata_name:
252            metadata_name = metadata_name.strip()
253        metadata_column = int( elem.get( "metadata_column", 0 ) )
254        message = elem.get( "message", "Value for metadata %s was not found in %s." % ( metadata_name, filename ) )
255        line_startswith = elem.get( "line_startswith", None  )
256        if line_startswith:
257            line_startswith = line_startswith.strip()
258        return cls( filename, metadata_name, metadata_column, message, line_startswith )
259    def __init__( self, filename, metadata_name, metadata_column, message="Value for metadata not found.", line_startswith=None ):
260        self.metadata_name = metadata_name
261        self.message = message
262        self.valid_values = []
263        for line in open( filename ):
264            if line_startswith is None or line.startswith( line_startswith ):
265                fields = line.split( '\t' )
266                if metadata_column < len( fields ):
267                    self.valid_values.append( fields[metadata_column].strip() )
268    def validate( self, value, history = None ):
269        if not value: return
270        if hasattr( value, "metadata" ):
271            if value.metadata.spec[self.metadata_name].param.to_string( value.metadata.get( self.metadata_name ) ) in self.valid_values:
272                return
273        raise ValueError( self.message )
274
275class MetadataInDataTableColumnValidator( Validator ):
276    """
277    Validator that checks if the value for a dataset's metadata item exists in a file.
278    """
279    @classmethod
280    def from_element( cls, param, elem ):
281        table_name = elem.get( "table_name", None )
282        assert table_name, 'You must specify a table_name.'
283        tool_data_table = param.tool.app.tool_data_tables[ table_name ]
284        metadata_name = elem.get( "metadata_name", None )
285        if metadata_name:
286            metadata_name = metadata_name.strip()
287        metadata_column = elem.get( "metadata_column", 0 )
288        try:
289            metadata_column = int( metadata_column )
290        except:
291            pass
292        message = elem.get( "message", "Value for metadata %s was not found in %s." % ( metadata_name, table_name ) )
293        line_startswith = elem.get( "line_startswith", None  )
294        if line_startswith:
295            line_startswith = line_startswith.strip()
296        return cls( tool_data_table, metadata_name, metadata_column, message, line_startswith )
297
298    def __init__( self, tool_data_table, metadata_name, metadata_column, message="Value for metadata not found.", line_startswith=None ):
299        self.metadata_name = metadata_name
300        self.message = message
301        self.valid_values = []
302        self._data_table_content_version = None
303        self._tool_data_table = tool_data_table
304        if isinstance( metadata_column, basestring ):
305            metadata_column = tool_data_table.columns[ metadata_column ]
306        self._metadata_column = metadata_column
307        self._load_values()
308
309    def _load_values( self ):
310        self._data_table_content_version, data_fields = self._tool_data_table.get_version_fields()
311        self.valid_values = []
312        for fields in data_fields:
313            if self._metadata_column < len( fields ):
314                self.valid_values.append( fields[ self._metadata_column ] )
315
316    def validate( self, value, history = None ):
317        if not value: return
318        if hasattr( value, "metadata" ):
319            if not self._tool_data_table.is_current_version( self._data_table_content_version ):
320                log.debug( 'MetadataInDataTableColumnValidator values are out of sync with data table (%s), updating validator.', self._tool_data_table.name )
321                self._load_values()
322            if value.metadata.spec[self.metadata_name].param.to_string( value.metadata.get( self.metadata_name ) ) in self.valid_values:
323                return
324        raise ValueError( self.message )
325
326
327validator_types = dict( expression=ExpressionValidator,
328                        regex=RegexValidator,
329                        in_range=InRangeValidator,
330                        length=LengthValidator,
331                        metadata=MetadataValidator,
332                        unspecified_build=UnspecifiedBuildValidator,
333                        no_options=NoOptionsValidator,
334                        empty_field=EmptyTextfieldValidator,
335                        dataset_metadata_in_file=MetadataInFileColumnValidator,
336                        dataset_metadata_in_data_table=MetadataInDataTableColumnValidator,
337                        dataset_ok_validator=DatasetOkValidator )
338
339def get_suite():
340    """Get unittest suite for this module"""
341    import doctest, sys
342    return doctest.DocTestSuite( sys.modules[__name__] )
343