/lib/galaxy/tools/parameters/dynamic_options.py

https://bitbucket.org/h_morita_dbcls/galaxy-central · Python · 516 lines · 469 code · 9 blank · 38 comment · 22 complexity · 6799e4578e511f9ccfcdad4c0cb8e020 MD5 · raw file

  1. """
  2. Support for generating the options for a SelectToolParameter dynamically (based
  3. on the values of other parameters or other aspects of the current state)
  4. """
  5. import operator, sys, os, logging
  6. import basic, validation
  7. from galaxy.util import string_as_bool
  8. log = logging.getLogger(__name__)
  9. class Filter( object ):
  10. """
  11. A filter takes the current options list and modifies it.
  12. """
  13. @classmethod
  14. def from_element( cls, d_option, elem ):
  15. """Loads the proper filter by the type attribute of elem"""
  16. type = elem.get( 'type', None )
  17. assert type is not None, "Required 'type' attribute missing from filter"
  18. return filter_types[type.strip()]( d_option, elem )
  19. def __init__( self, d_option, elem ):
  20. self.dynamic_option = d_option
  21. self.elem = elem
  22. def get_dependency_name( self ):
  23. """Returns the name of any depedencies, otherwise None"""
  24. return None
  25. def filter_options( self, options, trans, other_values ):
  26. """Returns a list of options after the filter is applied"""
  27. raise TypeError( "Abstract Method" )
  28. class StaticValueFilter( Filter ):
  29. """
  30. Filters a list of options on a column by a static value.
  31. Type: static_value
  32. Required Attributes:
  33. value: static value to compare to
  34. column: column in options to compare with
  35. Optional Attributes:
  36. keep: Keep columns matching value (True)
  37. Discard columns matching value (False)
  38. """
  39. def __init__( self, d_option, elem ):
  40. Filter.__init__( self, d_option, elem )
  41. self.value = elem.get( "value", None )
  42. assert self.value is not None, "Required 'value' attribute missing from filter"
  43. column = elem.get( "column", None )
  44. assert column is not None, "Required 'column' attribute missing from filter, when loading from file"
  45. self.column = d_option.column_spec_to_index( column )
  46. self.keep = string_as_bool( elem.get( "keep", 'True' ) )
  47. def filter_options( self, options, trans, other_values ):
  48. rval = []
  49. for fields in options:
  50. if ( self.keep and fields[self.column] == self.value ) or ( not self.keep and fields[self.column] != self.value ):
  51. rval.append( fields )
  52. return rval
  53. class DataMetaFilter( Filter ):
  54. """
  55. Filters a list of options on a column by a dataset metadata value.
  56. Type: data_meta
  57. When no 'from_' source has been specified in the <options> tag, this will populate the options list with (meta_value, meta_value, False).
  58. Otherwise, options which do not match the metadata value in the column are discarded.
  59. Required Attributes:
  60. ref: Name of input dataset
  61. key: Metadata key to use for comparison
  62. column: column in options to compare with (not required when not associated with input options)
  63. Optional Attributes:
  64. multiple: Option values are multiple, split column by separator (True)
  65. separator: When multiple split by this (,)
  66. """
  67. def __init__( self, d_option, elem ):
  68. Filter.__init__( self, d_option, elem )
  69. self.ref_name = elem.get( "ref", None )
  70. assert self.ref_name is not None, "Required 'ref' attribute missing from filter"
  71. d_option.has_dataset_dependencies = True
  72. self.key = elem.get( "key", None )
  73. assert self.key is not None, "Required 'key' attribute missing from filter"
  74. column = elem.get( "column", None )
  75. if column is None:
  76. assert self.dynamic_option.file_fields is None and self.dynamic_option.dataset_ref_name is None, "Required 'column' attribute missing from filter, when loading from file"
  77. else:
  78. self.column = d_option.column_spec_to_index( column )
  79. self.multiple = string_as_bool( elem.get( "multiple", "False" ) )
  80. self.separator = elem.get( "separator", "," )
  81. def get_dependency_name( self ):
  82. return self.ref_name
  83. def filter_options( self, options, trans, other_values ):
  84. def compare_meta_value( file_value, dataset_value ):
  85. if isinstance( dataset_value, list ):
  86. if self.multiple:
  87. file_value = file_value.split( self.separator )
  88. for value in dataset_value:
  89. if value not in file_value:
  90. return False
  91. return True
  92. return file_value in dataset_value
  93. if self.multiple:
  94. return dataset_value in file_value.split( self.separator )
  95. return file_value == dataset_value
  96. assert self.ref_name in other_values or ( trans is not None and trans.workflow_building_mode), "Required dependency '%s' not found in incoming values" % self.ref_name
  97. ref = other_values.get( self.ref_name, None )
  98. if not isinstance( ref, self.dynamic_option.tool_param.tool.app.model.HistoryDatasetAssociation ):
  99. return [] #not a valid dataset
  100. meta_value = ref.metadata.get( self.key, None )
  101. if meta_value is None: #assert meta_value is not None, "Required metadata value '%s' not found in referenced dataset" % self.key
  102. return [ ( disp_name, basic.UnvalidatedValue( optval ), selected ) for disp_name, optval, selected in options ]
  103. if self.column is not None:
  104. rval = []
  105. for fields in options:
  106. if compare_meta_value( fields[self.column], meta_value ):
  107. rval.append( fields )
  108. return rval
  109. else:
  110. if not isinstance( meta_value, list ):
  111. meta_value = [meta_value]
  112. for value in meta_value:
  113. options.append( ( value, value, False ) )
  114. return options
  115. class ParamValueFilter( Filter ):
  116. """
  117. Filters a list of options on a column by the value of another input.
  118. Type: param_value
  119. Required Attributes:
  120. ref: Name of input value
  121. column: column in options to compare with
  122. Optional Attributes:
  123. keep: Keep columns matching value (True)
  124. Discard columns matching value (False)
  125. ref_attribute: Period (.) separated attribute chain of input (ref) to use as value for filter
  126. """
  127. def __init__( self, d_option, elem ):
  128. Filter.__init__( self, d_option, elem )
  129. self.ref_name = elem.get( "ref", None )
  130. assert self.ref_name is not None, "Required 'ref' attribute missing from filter"
  131. column = elem.get( "column", None )
  132. assert column is not None, "Required 'column' attribute missing from filter"
  133. self.column = d_option.column_spec_to_index( column )
  134. self.keep = string_as_bool( elem.get( "keep", 'True' ) )
  135. self.ref_attribute = elem.get( "ref_attribute", None )
  136. if self.ref_attribute:
  137. self.ref_attribute = self.ref_attribute.split( '.' )
  138. else:
  139. self.ref_attribute = []
  140. def get_dependency_name( self ):
  141. return self.ref_name
  142. def filter_options( self, options, trans, other_values ):
  143. if trans is not None and trans.workflow_building_mode: return []
  144. assert self.ref_name in other_values, "Required dependency '%s' not found in incoming values" % self.ref_name
  145. ref = other_values.get( self.ref_name, None )
  146. for ref_attribute in self.ref_attribute:
  147. ref = getattr( ref, ref_attribute )
  148. ref = str( ref )
  149. rval = []
  150. for fields in options:
  151. if ( self.keep and fields[self.column] == ref ) or ( not self.keep and fields[self.column] != ref ):
  152. rval.append( fields )
  153. return rval
  154. class UniqueValueFilter( Filter ):
  155. """
  156. Filters a list of options to be unique by a column value.
  157. Type: unique_value
  158. Required Attributes:
  159. column: column in options to compare with
  160. """
  161. def __init__( self, d_option, elem ):
  162. Filter.__init__( self, d_option, elem )
  163. column = elem.get( "column", None )
  164. assert column is not None, "Required 'column' attribute missing from filter"
  165. self.column = d_option.column_spec_to_index( column )
  166. def get_dependency_name( self ):
  167. return self.dynamic_option.dataset_ref_name
  168. def filter_options( self, options, trans, other_values ):
  169. rval = []
  170. skip_list = []
  171. for fields in options:
  172. if fields[self.column] not in skip_list:
  173. rval.append( fields )
  174. skip_list.append( fields[self.column] )
  175. return rval
  176. class MultipleSplitterFilter( Filter ):
  177. """
  178. Turns a single line of options into multiple lines, by splitting a column and creating a line for each item.
  179. Type: multiple_splitter
  180. Required Attributes:
  181. column: column in options to compare with
  182. Optional Attributes:
  183. separator: Split column by this (,)
  184. """
  185. def __init__( self, d_option, elem ):
  186. Filter.__init__( self, d_option, elem )
  187. self.separator = elem.get( "separator", "," )
  188. columns = elem.get( "column", None )
  189. assert columns is not None, "Required 'columns' attribute missing from filter"
  190. self.columns = [ d_option.column_spec_to_index( column ) for column in columns.split( "," ) ]
  191. def filter_options( self, options, trans, other_values ):
  192. rval = []
  193. for fields in options:
  194. for column in self.columns:
  195. for field in fields[column].split( self.separator ):
  196. rval.append( fields[0:column] + [field] + fields[column+1:] )
  197. return rval
  198. class AttributeValueSplitterFilter( Filter ):
  199. """
  200. Filters a list of attribute-value pairs to be unique attribute names.
  201. Type: attribute_value_splitter
  202. Required Attributes:
  203. column: column in options to compare with
  204. Optional Attributes:
  205. pair_separator: Split column by this (,)
  206. name_val_separator: Split name-value pair by this ( whitespace )
  207. """
  208. def __init__( self, d_option, elem ):
  209. Filter.__init__( self, d_option, elem )
  210. self.pair_separator = elem.get( "pair_separator", "," )
  211. self.name_val_separator = elem.get( "name_val_separator", None )
  212. self.columns = elem.get( "column", None )
  213. assert self.columns is not None, "Required 'columns' attribute missing from filter"
  214. self.columns = [ int ( column ) for column in self.columns.split( "," ) ]
  215. def filter_options( self, options, trans, other_values ):
  216. attr_names = []
  217. rval = []
  218. for fields in options:
  219. for column in self.columns:
  220. for pair in fields[column].split( self.pair_separator ):
  221. ary = pair.split( self.name_val_separator )
  222. if len( ary ) == 2:
  223. name, value = ary
  224. if name not in attr_names:
  225. rval.append( fields[0:column] + [name] + fields[column:] )
  226. attr_names.append( name )
  227. return rval
  228. class AdditionalValueFilter( Filter ):
  229. """
  230. Adds a single static value to an options list.
  231. Type: add_value
  232. Required Attributes:
  233. value: value to appear in select list
  234. Optional Attributes:
  235. name: Display name to appear in select list (value)
  236. index: Index of option list to add value (APPEND)
  237. """
  238. def __init__( self, d_option, elem ):
  239. Filter.__init__( self, d_option, elem )
  240. self.value = elem.get( "value", None )
  241. assert self.value is not None, "Required 'value' attribute missing from filter"
  242. self.name = elem.get( "name", None )
  243. if self.name is None:
  244. self.name = self.value
  245. self.index = elem.get( "index", None )
  246. if self.index is not None:
  247. self.index = int( self.index )
  248. def filter_options( self, options, trans, other_values ):
  249. rval = list( options )
  250. add_value = []
  251. for i in range( self.dynamic_option.largest_index + 1 ):
  252. add_value.append( "" )
  253. add_value[self.dynamic_option.columns['value']] = self.value
  254. add_value[self.dynamic_option.columns['name']] = self.name
  255. if self.index is not None:
  256. rval.insert( self.index, add_value )
  257. else:
  258. rval.append( add_value )
  259. return rval
  260. class RemoveValueFilter( Filter ):
  261. """
  262. Removes a value from an options list.
  263. Type: remove_value
  264. Required Attributes:
  265. value: value to remove from select list
  266. or
  267. ref: param to refer to
  268. or
  269. meta_ref: dataset to refer to
  270. key: metadata key to compare to
  271. """
  272. def __init__( self, d_option, elem ):
  273. Filter.__init__( self, d_option, elem )
  274. self.value = elem.get( "value", None )
  275. self.ref_name = elem.get( "ref", None )
  276. self.meta_ref = elem.get( "meta_ref", None )
  277. self.metadata_key = elem.get( "key", None )
  278. assert self.value is not None or ( ( self.ref_name is not None or self.meta_ref is not None )and self.metadata_key is not None ), ValueError( "Required 'value' or 'ref' and 'key' attributes missing from filter" )
  279. self.multiple = string_as_bool( elem.get( "multiple", "False" ) )
  280. self.separator = elem.get( "separator", "," )
  281. def filter_options( self, options, trans, other_values ):
  282. if trans is not None and trans.workflow_building_mode: return options
  283. assert self.value is not None or ( self.ref_name is not None and self.ref_name in other_values ) or (self.meta_ref is not None and self.meta_ref in other_values ) or ( trans is not None and trans.workflow_building_mode), Exception( "Required dependency '%s' or '%s' not found in incoming values" % ( self.ref_name, self.meta_ref ) )
  284. def compare_value( option_value, filter_value ):
  285. if isinstance( filter_value, list ):
  286. if self.multiple:
  287. option_value = option_value.split( self.separator )
  288. for value in filter_value:
  289. if value not in filter_value:
  290. return False
  291. return True
  292. return option_value in filter_value
  293. if self.multiple:
  294. return filter_value in option_value.split( self.separator )
  295. return option_value == filter_value
  296. value = self.value
  297. if value is None:
  298. if self.ref_name is not None:
  299. value = other_values.get( self.ref_name )
  300. else:
  301. data_ref = other_values.get( self.meta_ref )
  302. if not isinstance( data_ref, self.dynamic_option.tool_param.tool.app.model.HistoryDatasetAssociation ):
  303. return options #cannot modify options
  304. value = data_ref.metadata.get( self.metadata_key, None )
  305. return [ ( disp_name, optval, selected ) for disp_name, optval, selected in options if not compare_value( optval, value ) ]
  306. class SortByColumnFilter( Filter ):
  307. """
  308. Sorts an options list by a column
  309. Type: sort_by
  310. Required Attributes:
  311. column: column to sort by
  312. """
  313. def __init__( self, d_option, elem ):
  314. Filter.__init__( self, d_option, elem )
  315. column = elem.get( "column", None )
  316. assert column is not None, "Required 'column' attribute missing from filter"
  317. self.column = d_option.column_spec_to_index( column )
  318. def filter_options( self, options, trans, other_values ):
  319. rval = []
  320. for i, fields in enumerate( options ):
  321. for j in range( 0, len( rval ) ):
  322. if fields[self.column] < rval[j][self.column]:
  323. rval.insert( j, fields )
  324. break
  325. else:
  326. rval.append( fields )
  327. return rval
  328. filter_types = dict( data_meta = DataMetaFilter,
  329. param_value = ParamValueFilter,
  330. static_value = StaticValueFilter,
  331. unique_value = UniqueValueFilter,
  332. multiple_splitter = MultipleSplitterFilter,
  333. attribute_value_splitter = AttributeValueSplitterFilter,
  334. add_value = AdditionalValueFilter,
  335. remove_value = RemoveValueFilter,
  336. sort_by = SortByColumnFilter )
  337. class DynamicOptions( object ):
  338. """Handles dynamically generated SelectToolParameter options"""
  339. def __init__( self, elem, tool_param ):
  340. def load_from_parameter( from_parameter, transform_lines = None ):
  341. obj = self.tool_param
  342. for field in from_parameter.split( '.' ):
  343. obj = getattr( obj, field )
  344. if transform_lines:
  345. obj = eval( transform_lines )
  346. return self.parse_file_fields( obj )
  347. self.tool_param = tool_param
  348. self.columns = {}
  349. self.filters = []
  350. self.file_fields = None
  351. self.largest_index = 0
  352. self.dataset_ref_name = None
  353. # True if the options generation depends on one or more other parameters
  354. # that are dataset inputs
  355. self.has_dataset_dependencies = False
  356. self.validators = []
  357. self.converter_safe = True
  358. # Parse the <options> tag
  359. self.separator = elem.get( 'separator', '\t' )
  360. self.line_startswith = elem.get( 'startswith', None )
  361. data_file = elem.get( 'from_file', None )
  362. dataset_file = elem.get( 'from_dataset', None )
  363. from_parameter = elem.get( 'from_parameter', None )
  364. tool_data_table_name = elem.get( 'from_data_table', None )
  365. # Options are defined from a data table loaded by the app
  366. self.tool_data_table = None
  367. if tool_data_table_name:
  368. app = tool_param.tool.app
  369. assert tool_data_table_name in app.tool_data_tables, \
  370. "Data table named '%s' is required by tool but not configured" % tool_data_table_name
  371. self.tool_data_table = app.tool_data_tables[ tool_data_table_name ]
  372. # Column definitions are optional, but if provided override those from the table
  373. if elem.find( "column" ) is not None:
  374. self.parse_column_definitions( elem )
  375. else:
  376. self.columns = self.tool_data_table.columns
  377. # Options are defined by parsing tabular text data from an data file
  378. # on disk, a dataset, or the value of another parameter
  379. elif data_file is not None or dataset_file is not None or from_parameter is not None:
  380. self.parse_column_definitions( elem )
  381. if data_file is not None:
  382. data_file = data_file.strip()
  383. if not os.path.isabs( data_file ):
  384. data_file = os.path.join( self.tool_param.tool.app.config.tool_data_path, data_file )
  385. self.file_fields = self.parse_file_fields( open( data_file ) )
  386. elif dataset_file is not None:
  387. self.dataset_ref_name = dataset_file
  388. self.has_dataset_dependencies = True
  389. self.converter_safe = False
  390. elif from_parameter is not None:
  391. transform_lines = elem.get( 'transform_lines', None )
  392. self.file_fields = list( load_from_parameter( from_parameter, transform_lines ) )
  393. # Load filters
  394. for filter_elem in elem.findall( 'filter' ):
  395. self.filters.append( Filter.from_element( self, filter_elem ) )
  396. # Load Validators
  397. for validator in elem.findall( 'validator' ):
  398. self.validators.append( validation.Validator.from_element( self.tool_param, validator ) )
  399. def parse_column_definitions( self, elem ):
  400. for column_elem in elem.findall( 'column' ):
  401. name = column_elem.get( 'name', None )
  402. assert name is not None, "Required 'name' attribute missing from column def"
  403. index = column_elem.get( 'index', None )
  404. assert index is not None, "Required 'index' attribute missing from column def"
  405. index = int( index )
  406. self.columns[name] = index
  407. if index > self.largest_index:
  408. self.largest_index = index
  409. assert 'value' in self.columns, "Required 'value' column missing from column def"
  410. if 'name' not in self.columns:
  411. self.columns['name'] = self.columns['value']
  412. def parse_file_fields( self, reader ):
  413. rval = []
  414. for line in reader:
  415. if line.startswith( '#' ) or ( self.line_startswith and not line.startswith( self.line_startswith ) ):
  416. continue
  417. line = line.rstrip( "\n\r" )
  418. if line:
  419. fields = line.split( self.separator )
  420. if self.largest_index < len( fields ):
  421. rval.append( fields )
  422. return rval
  423. def get_dependency_names( self ):
  424. """
  425. Return the names of parameters these options depend on -- both data
  426. and other param types.
  427. """
  428. rval = []
  429. if self.dataset_ref_name:
  430. rval.append( self.dataset_ref_name )
  431. for filter in self.filters:
  432. depend = filter.get_dependency_name()
  433. if depend:
  434. rval.append( depend )
  435. return rval
  436. def get_fields( self, trans, other_values ):
  437. if self.dataset_ref_name:
  438. dataset = other_values.get( self.dataset_ref_name, None )
  439. assert dataset is not None, "Required dataset '%s' missing from input" % self.dataset_ref_name
  440. if not dataset: return [] #no valid dataset in history
  441. options = self.parse_file_fields( open( dataset.file_name ) )
  442. elif self.tool_data_table:
  443. options = self.tool_data_table.get_fields()
  444. else:
  445. options = list( self.file_fields )
  446. for filter in self.filters:
  447. options = filter.filter_options( options, trans, other_values )
  448. return options
  449. def get_options( self, trans, other_values ):
  450. rval = []
  451. if self.file_fields is not None or self.tool_data_table is not None or self.dataset_ref_name is not None:
  452. options = self.get_fields( trans, other_values )
  453. for fields in options:
  454. rval.append( ( fields[self.columns['name']], fields[self.columns['value']], False ) )
  455. else:
  456. for filter in self.filters:
  457. rval = filter.filter_options( rval, trans, other_values )
  458. return rval
  459. def column_spec_to_index( self, column_spec ):
  460. """
  461. Convert a column specification (as read from the config file), to an
  462. index. A column specification can just be a number, a column name, or
  463. a column alias.
  464. """
  465. # Name?
  466. if column_spec in self.columns:
  467. return self.columns[column_spec]
  468. # Int?
  469. return int( column_spec )