Several tool related changes:

- `update_state` now accepts an `update_only` option so that you can update
   values for just a subset of the parameters (even just one). This is used
   by the workflow running interface since some parameters are set at build
   time and others at runtime.

 - `UnvalidatedValue` marker for dynamic selects. Since these cannot be
   validated at build time in workflows they need to be marked as needing
   later validation. This prevents users from sneaking arbitrary values
   onto the command line. NOTE: The runtime validation is not implemented,
   so these just fail at runtime for now.

 - Misc comment and documentation improvements
This commit is contained in:
James Taylor
2008-01-14 23:56:03 +00:00
parent 703c916b09
commit f51e89fd04
2 changed files with 126 additions and 40 deletions
+103 -37
View File
@@ -464,6 +464,10 @@ class Tool:
return param
def check_workflow_compatible( self ):
"""
Determine if a tool can be used in workflows. External tools and the
upload tool are currently not supported by workflows.
"""
# This is probably the best bet for detecting external web tools
# right now
if self.action != "/tool_runner/index":
@@ -479,7 +483,10 @@ class Tool:
def new_state( self, trans, all_pages=False ):
"""
Create a new `DefaultToolState` for this tool. It will be initialized
based on `self.inputs` with appropriate default values.
with default values for inputs.
Only inputs on the first page will be initialized unless `all_pages` is
True, in which case all inputs regardless of page are initialized.
"""
state = DefaultToolState()
state.inputs = {}
@@ -492,31 +499,39 @@ class Tool:
def fill_in_new_state( self, trans, inputs, state, context=None ):
"""
Fill in a state dictionary with default values taken from input.
Grouping elements are filled in recursively.
Fill in a tool state dictionary with default values for all parameters
in the dictionary `inputs`. Grouping elements are filled in recursively.
"""
context = ExpressionContext( state, context )
for input in inputs.itervalues():
if isinstance( input, Repeat ):
if isinstance( input, Repeat ):
# Repeat elements are always initialized to have 0 units.
state[ input.name ] = []
elif isinstance( input, Conditional ):
# State for a conditional is a plain dictionary.
s = state[ input.name ] = {}
# Get the default value for the 'test element' and use it
# to determine the current case
test_value = input.test_param.get_initial_value( trans, context )
current_case = input.get_current_case( test_value, trans )
# Recursively fill in state for selected case
self.fill_in_new_state( trans, input.cases[current_case].inputs, s, context )
# Store the current case in a special value
s['__current_case__'] = current_case
# Store the value of the test element
s[ input.test_param.name ] = test_value
else:
value = input.get_initial_value( trans, context )
state[ input.name ] = value
# `input` is just a plain parameter, get its default value
state[ input.name ] = input.get_initial_value( trans, context )
def get_param_html_map( self, trans, page=0, other_values={} ):
"""
Return a dictionary containing the HTML representation of each
parameter. This is used for rendering display elements. It is
currently not compatible with grouping constructs.
NOTE: This should be considered deprecated, it is only used for tools
with `display` elements. These should be eliminated.
"""
rval = dict()
for key, param in self.inputs_by_page[page].iteritems():
@@ -546,6 +561,13 @@ class Tool:
return None
def visit_inputs( self, value, callback ):
"""
Call the function `callback` on each parameter of this tool. Visits
grouping parameters recursively and constructs unique prefixes for
each nested set of parameters. The callback method is then called as:
`callback( level_prefix, parameter, parameter_value )`
"""
# HACK: Yet another hack around check_values
if not self.check_values:
return
@@ -569,21 +591,31 @@ class Tool:
state.decode( encoded_state, self, trans.app )
else:
state = self.new_state( trans )
# This feels a bit like a hack
# This feels a bit like a hack. It allows forcing full processing
# of inputs even when there is no state in the incoming dictionary
# by providing either 'runtool_btn' (the name of the submit button
# on the standard run form) or "URL" (a parameter provided by
# external data source tools).
if "runtool_btn" not in incoming and "URL" not in incoming:
return "tool_form.tmpl", dict( errors={}, tool_state=state, param_values={}, incoming={} )
# Fill in everything we can in the state and keep track of errors
# Now process new parameters for the current page
if self.check_values:
# Process incoming data
if not( self.check_values ):
# If `self.check_values` is false we don't do any checking or
# processing on inut parameters. This is used to pass raw values
# through to/from external sites. FIXME: This should be handled
# more cleanly, there is no reason why external sites need to
# post back to the same URL that the tool interface uses.
errors = {}
params = incoming
else:
# Update state for all inputs on the current page taking new
# values from `incoming`.
errors = self.update_state( trans, self.inputs_by_page[state.page], state.inputs, incoming )
# Any tool specific validation
# If the tool provides a `validate_input` hook, call it.
validate_input = self.get_hook( 'validate_input' )
if validate_input:
validate_input( trans, errors, state.inputs, self.inputs_by_page[state.page] )
params = state.inputs
else:
errors = {}
params = incoming
# Did the user actually click next / execute or is this just
# a refresh?
if 'runtool_btn' in incoming or 'URL' in incoming:
@@ -603,14 +635,20 @@ class Tool:
self.fill_in_new_state( trans, self.inputs_by_page[ state.page ], state.inputs )
return 'tool_form.tmpl', dict( errors=errors, tool_state=state )
else:
# Just a refresh, render the form with updated state and errors.
return 'tool_form.tmpl', dict( errors=errors, tool_state=state )
def update_state( self, trans, inputs, state, incoming, prefix = "", context=None ):
def update_state( self, trans, inputs, state, incoming,
prefix = "", context=None, update_only=False, old_errors={} ):
"""
Update the tool state in `state` using the user input in `incoming`.
This is designed to be called recursively: `inputs` contains the
set of inputs being processed, and `prefix` specifies a prefix to
add to the name of each input to extract it's value from `incoming`.
If `update_only` is True, values that are not in `incoming` will
not be modified. In this case `old_errors` can be provided, and any
errors for parameters which were *not* updated will be preserved.
"""
errors = dict()
# Push this level onto the context stack
@@ -621,6 +659,7 @@ class Tool:
if isinstance( input, Repeat ):
group_state = state[input.name]
group_errors = []
group_old_errors = old_errors.get( input.name, None )
any_group_errors = False
# Check any removals before updating state
for i in range( len( group_state ) ):
@@ -629,12 +668,18 @@ class Tool:
# Update state
for i in range( len( group_state ) ):
prefix = "%s_%d|" % ( key, i )
if group_old_errors:
rep_old_errors = group_old_errors[i]
else:
rep_old_errors = {}
rep_errors = self.update_state( trans,
input.inputs,
group_state[i],
incoming,
prefix,
context )
prefix=prefix,
context=context,
update_only=update_only,
old_errors=rep_old_errors )
if rep_errors:
any_group_errors = True
group_errors.append( rep_errors )
@@ -652,27 +697,41 @@ class Tool:
errors[input.name] = group_errors
elif isinstance( input, Conditional ):
group_state = state[input.name]
group_old_errors = old_errors.get( input.name, {} )
old_current_case = group_state['__current_case__']
prefix = "%s|" % ( key )
# Deal with the 'test' element and see if it's value changed
test_incoming = incoming.get( prefix + input.test_param.name, None )
value, test_param_error = \
self.check_param( trans, input.test_param, test_incoming, context )
current_case = input.get_current_case( value, trans )
test_param_key = prefix + input.test_param.name
if test_param_key not in incoming and update_only:
# Update only, keep previous value and state, but still
# recurse in case there are nested changes
value = group_state[ input.test_param.name ]
current_case = old_current_case
if input.test_param.name in old_errors:
errors[ input.test_param.name ] = old_errors[ input.test_param.name ]
else:
# Get value of test param and determine current case
test_incoming = incoming.get( prefix + input.test_param.name, None )
value, test_param_error = \
self.check_param( trans, input.test_param, test_incoming, context )
current_case = input.get_current_case( value, trans )
if current_case != old_current_case:
# Current case has changed, throw away old state
group_state = state[input.name] = {}
# TODO: we should try to preserve values if we can
self.fill_in_new_state( trans, input.cases[current_case].inputs, group_state, context )
group_errors = dict()
group_old_errors = dict()
else:
# Current case has not changed, update children
group_errors = self.update_state( trans,
input.cases[current_case].inputs,
group_state,
incoming,
prefix,
context )
prefix=prefix,
context=context,
update_only=update_only,
old_errors=group_old_errors )
if test_param_error:
group_errors[ input.test_param.name ] = test_param_error
if group_errors:
@@ -682,20 +741,27 @@ class Tool:
# Store the value of the test element
group_state[ input.test_param.name ] = value
else:
incoming_value = incoming.get( key, None )
if incoming_value == 'None' and isinstance( input, SelectToolParameter ) and input.is_dynamic:
# FIXME: This is a HACK, but is necessary because the values in incoming are not
# yet set by the user when the select list is dynamically generated.
legal_values = input.get_legal_values( trans, context )
if len( legal_values ) > 0 and incoming_value not in legal_values:
values = []
for v in legal_values: values.append( v )
values.sort()
incoming_value = values[0]
value, error = self.check_param( trans, input, incoming_value, context )
if error:
errors[ input.name ] = error
state[ input.name ] = value
if key not in incoming and update_only:
# The value is already in the state, but preserve the old
# error if it exists
if input.name in old_errors:
errors[ input.name ] = old_errors[ input.name ]
else:
incoming_value = incoming.get( key, None )
if incoming_value == 'None' and isinstance( input, SelectToolParameter ) and input.is_dynamic:
# FIXME: This is a HACK, but is necessary because the
# values in incoming are not yet set by the user when
# the select list is dynamically generated.
legal_values = input.get_legal_values( trans, context )
if len( legal_values ) > 0 and incoming_value not in legal_values:
values = []
for v in legal_values: values.append( v )
values.sort()
incoming_value = values[0]
value, error = self.check_param( trans, input, incoming_value, context )
if error:
errors[ input.name ] = error
state[ input.name ] = value
return errors
def check_param( self, trans, param, incoming_value, param_values ):
+23 -3
View File
@@ -438,6 +438,8 @@ class SelectToolParameter( ToolParameter ):
# Dynamic options are not yet supported in workflow, allow
# specifying the value as text for now.
if self.is_dynamic and trans.workflow_building_mode:
assert isinstance( value, UnvalidatedValue )
value = value.value
if self.multiple:
if value is None:
value = ""
@@ -457,7 +459,7 @@ class SelectToolParameter( ToolParameter ):
return field
def from_html( self, value, trans=None, other_values={} ):
if self.is_dynamic and trans.workflow_building_mode:
return value
return UnvalidatedValue( value )
legal_values = self.get_legal_values( trans, other_values )
if isinstance( value, list ):
if not(self.repeat):
@@ -484,12 +486,19 @@ class SelectToolParameter( ToolParameter ):
else:
return value
def value_to_basic( self, value, app ):
if isinstance( value, UnvalidatedValue ):
return { "__class__": "UnvalidatedValue", "value": value.value }
return value
def value_from_basic( self, value, app, ignore_errors=False ):
if isinstance( value, dict ):
assert value["__class__"] == "UnvalidatedValue"
return UnvalidatedValue( value["value"] )
return value
def get_initial_value( self, trans, context ):
# More working around dynamic options for workflow
if self.is_dynamic and trans.workflow_building_mode:
# Really the best we can do?
return None
return UnvalidatedValue( None )
options = list( self.get_options( trans, context ) )
value = [ optval for _, optval, selected in options if selected ]
if len( value ) == 0:
@@ -504,6 +513,11 @@ class SelectToolParameter( ToolParameter ):
value = value[0]
return value
def value_to_display_text( self, value, app ):
if isinstance( value, UnvalidatedValue ):
suffix = "\n(value not yet validated)"
value = value.value
else:
suffix = ""
if not isinstance( value, list ):
value = [ value ]
# FIXME: Currently only translating values back to labels if they
@@ -516,7 +530,7 @@ class SelectToolParameter( ToolParameter ):
for t, v, s in options:
if v in value:
rval.append( t )
return "\n".join( rval )
return "\n".join( rval ) + suffix
def get_dependencies( self ):
data_ref = param_ref = None
@@ -867,6 +881,12 @@ parameter_types = dict( text = TextToolParameter,
file = FileToolParameter,
data = DataToolParameter )
class UnvalidatedValue( object ):
"""
Wrapper to mark a value that has not been validated
"""
def __init__( self, value ):
self.value = value
def str_bool(in_str):
"""