PageRenderTime 41ms CodeModel.GetById 16ms app.highlight 21ms RepoModel.GetById 1ms app.codeStats 0ms

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

https://bitbucket.org/h_morita_dbcls/galaxy-central
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 )