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