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

https://bitbucket.org/cistrome/cistrome-harvard/ · Python · 343 lines · 327 code · 5 blank · 11 comment · 0 complexity · 8f3a474528408342733ba236278869ae MD5 · raw file

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