3 new commits in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/aa8d4381785e/ Changeset: aa8d4381785e User: jmchilton Date: 2014-09-12 15:32:22 Summary: More workflow module documentation improvements. I nearly understand workflow modules... Affected #: 2 files diff -r b8520d9c8269c4c7deca6418d54f2e722886073e -r aa8d4381785ee94746dbdcfd35c9b0798a10be30 lib/galaxy/workflow/modules.py --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -79,10 +79,16 @@ def get_state( self ): """ Return a serializable representation of the persistable state of the step - for tools it DefaultToolState.encode returns a string and - for inputs a json description is dumped out. + for simpler module types a json description is dumped out. """ return None + def update_state( self, incoming ): + """ Update the current state of the module against the user supplied + parameters in the dict-like object `incoming`. + """ + pass + def get_errors( self ): """ It seems like this is effectively just used as boolean - some places in the tool shed self.errors is set to boolean, other places 'unavailable', @@ -97,9 +103,6 @@ def get_data_outputs( self ): return [] - def update_state( self ): - pass - def get_config_form( self ): """ Render form that is embedded in workflow editor for modifying the step state of a node. @@ -120,33 +123,38 @@ ## ---- Run time --------------------------------------------------------- def get_runtime_inputs( self ): - """ Used internally to modules and when displaying inputs in display - and run workflow templates. The ToolModule doesn't implement this and - these templates contain specialized logic for dealing with the tool and - state directly in these cases. + """ Used internally by modules and when displaying inputs in workflow + editor and run workflow templates. + + Note: The ToolModule doesn't implement this and these templates contain + specialized logic for dealing with the tool and state directly in the + case of ToolModules. """ raise TypeError( "Abstract method" ) def encode_runtime_state( self, trans, state ): - """ Encode the runtime state (loaded from the stored step and - populated via the WorkflowModuleInjector below) for use in a hidden - parameter on the webpage. + """ Encode the default runtime state at return as a simple `str` for + use in a hidden parameter on the workflow run submission form. - This will combined with runtime parameters supplied by user running - the workflow to create the final state to pass along to execute during - workflow invocation. + This default runtime state will be combined with user supplied + parameters in `compute_runtime_state` below at workflow invocation time to + actually describe how each step will be executed. """ raise TypeError( "Abstract method" ) - def compute_state( self, trans, step_updates=None ): - """ Recover the transient "state" attribute to populate corresponding - step with (currently this is always a DefaultToolState instance, - though I am not sure this is strictly nessecary). + def compute_runtime_state( self, trans, step_updates=None ): + """ Determine the runtime state (potentially different from self.state + which describes configuration state). This (again unlike self.state) is + currently always a `DefaultToolState` object. If `step_updates` is `None`, this is likely for rendering the run form for instance and no runtime properties are available and state must be - solely determined by step. If `step_updates` are available they describe - the runtime properties supplied by the workflow runner. + solely determined by the default runtime state described by the step. + + If `step_updates` are available they describe the runtime properties + supplied by the workflow runner (potentially including a `tool_state` + parameter which is the serialized default encoding state created with + encode_runtime_state above). """ raise TypeError( "Abstract method" ) @@ -232,7 +240,7 @@ errors[ name ] = error return errors - def compute_state( self, trans, step_updates=None ): + def compute_runtime_state( self, trans, step_updates=None ): if step_updates: # Fix this for multiple inputs state = self.decode_runtime_state( trans, step_updates.pop( "tool_state" ) ) @@ -559,7 +567,7 @@ def check_and_update_state( self ): return self.tool.check_and_update_param_values( self.state.inputs, self.trans, allow_workflow_parameters=True ) - def compute_state( self, trans, step_updates=None ): + def compute_runtime_state( self, trans, step_updates=None ): # Warning: This method destructively modifies existing step state. step_errors = None state = self.state @@ -788,7 +796,7 @@ # are not persisted so we need to do it every time) module.add_dummy_datasets( connections=step.input_connections ) - state, step_errors = module.compute_state( trans, step_args ) + state, step_errors = module.compute_runtime_state( trans, step_args ) step.state = state return step_errors diff -r b8520d9c8269c4c7deca6418d54f2e722886073e -r aa8d4381785ee94746dbdcfd35c9b0798a10be30 test/unit/workflows/test_modules.py --- a/test/unit/workflows/test_modules.py +++ b/test/unit/workflows/test_modules.py @@ -7,7 +7,6 @@ from galaxy import model from galaxy.workflow import modules -from galaxy.tools import parameters from .workflow_support import MockTrans @@ -49,17 +48,17 @@ __assert_has_runtime_input( module, label="Cool Input" ) -def test_data_input_compute_state_default(): +def test_data_input_compute_runtime_state_default(): module = __from_step( type="data_input", ) - state, errors = module.compute_state( module.trans ) + state, errors = module.compute_runtime_state( module.trans ) assert not errors assert 'input' in state.inputs assert state.inputs[ 'input' ] is None -def test_data_input_compute_state_args(): +def test_data_input_compute_runtime_state_args(): module = __from_step( type="data_input", ) @@ -68,7 +67,7 @@ hda = model.HistoryDatasetAssociation() with mock.patch('galaxy.workflow.modules.check_param') as check_method: check_method.return_value = ( hda, None ) - state, errors = module.compute_state( module.trans, { 'input': 4, 'tool_state': tool_state } ) + state, errors = module.compute_runtime_state( module.trans, { 'input': 4, 'tool_state': tool_state } ) assert not errors assert 'input' in state.inputs https://bitbucket.org/galaxy/galaxy-central/commits/945ebcd32f33/ Changeset: 945ebcd32f33 User: jmchilton Date: 2014-09-12 15:32:22 Summary: Rework workflow modules to reduce duplication and add more consistency.. Adding a recover_state method that to both implementations (though meant to be part of the module interface) that recovers the configuration state (module.state) from a dictionary. This turns out to reduce a lot of duplication between data and data collection input modules. Affected #: 1 file diff -r aa8d4381785ee94746dbdcfd35c9b0798a10be30 -r 945ebcd32f336a5b3b97c4c66aa1274a49d8e07b lib/galaxy/workflow/modules.py --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -171,24 +171,41 @@ @classmethod def new( Class, trans, tool_id=None ): module = Class( trans ) - module.state = dict( name=Class.default_name ) + module.state = Class.default_state() return module @classmethod def from_dict( Class, trans, d, secure=True ): module = Class( trans ) state = loads( d["tool_state"] ) - module.state = dict( name=state.get( "name", Class.default_name ) ) + module.recover_state( state ) return module @classmethod def from_workflow_step( Class, trans, step ): module = Class( trans ) - module.state = dict( name="Input Dataset" ) - if step.tool_inputs and "name" in step.tool_inputs: - module.state['name'] = step.tool_inputs[ 'name' ] + module.recover_state( step.tool_inputs ) return module + @classmethod + def default_state( Class ): + """ This method should return a dictionary describing each + configuration property and its default value. + """ + raise TypeError( "Abstract method" ) + + def recover_state( self, state, **kwds ): + """ Recover state `dict` from simple dictionary describing configuration + state (potentially from persisted step state). + + Sub-classes should supply `default_state` method and `state_fields` + attribute which are used to build up the state `dict`. + """ + self.state = self.default_state() + for key in self.state_fields: + if state and key in state: + self.state[ key ] = state[ key ] + def save_to_step( self, step ): step.type = self.type step.tool_id = None @@ -277,6 +294,11 @@ type = "data_input" name = "Input dataset" default_name = "Input Dataset" + state_fields = [ "name" ] + + @classmethod + def default_state( Class ): + return dict( name=Class.default_name ) class InputDataCollectionModule( InputModule ): @@ -285,34 +307,11 @@ type = "data_collection_input" name = "Input dataset collection" collection_type = default_collection_type + state_fields = [ "name", "collection_type" ] @classmethod - def new( Class, trans, tool_id=None ): - module = Class( trans ) - module.state = dict( name=Class.default_name, collection_type=Class.default_collection_type ) - return module - - @classmethod - def from_dict( Class, trans, d, secure=True ): - module = Class( trans ) - state = loads( d["tool_state"] ) - module.state = dict( - name=state.get( "name", Class.default_name ), - collection_type=state.get( "collection_type", Class.default_collection_type ) - ) - return module - - @classmethod - def from_workflow_step( Class, trans, step ): - module = Class( trans ) - module.state = dict( - name=Class.default_name, - collection_type=Class.default_collection_type - ) - for key in [ "name", "collection_type" ]: - if step.tool_inputs and key in step.tool_inputs: - module.state[ key ] = step.tool_inputs[ key ] - return module + def default_state( Class ): + return dict( name=Class.default_name, collection_type=Class.default_collection_type ) def get_runtime_inputs( self, filter_set=['data'] ): label = self.state.get( "name", self.default_name ) @@ -408,10 +407,9 @@ # tool being previously unavailable. return module_factory.from_dict(trans, loads(step.config), secure=False) module = Class( trans, tool_id ) - module.state = galaxy.tools.DefaultToolState() if step.tool_version and (step.tool_version != module.tool.version): module.version_changes.append("%s: using version '%s' instead of version '%s' indicated in this workflow." % (tool_id, module.tool.version, step.tool_version)) - module.state.inputs = module.tool.params_from_strings( step.tool_inputs, trans.app, ignore_errors=True ) + module.recover_state( step.tool_inputs ) module.errors = step.tool_errors module.workflow_outputs = step.workflow_outputs pjadict = {} @@ -421,6 +419,17 @@ return module return None + def recover_state( self, state, **kwds ): + """ Recover module configuration state property (a `DefaultToolState` + object) using the tool's `params_from_strings` method. + """ + app = self.trans.app + self.state = galaxy.tools.DefaultToolState() + params_from_kwds = dict( + ignore_errors=kwds.get( "ignore_errors", True ) + ) + self.state.inputs = self.tool.params_from_strings( state, app, **params_from_kwds ) + @classmethod def __get_tool_version( cls, trans, tool_id ): # Return a ToolVersion if one exists for tool_id. https://bitbucket.org/galaxy/galaxy-central/commits/2346e098b27e/ Changeset: 2346e098b27e User: jmchilton Date: 2014-09-12 15:32:22 Summary: Better OOP design for separation between input and input collection modules. (Lot easier now that I understand what all of the module methods are doing and have an example of a 4th module downstream.) Now with even more unit tests. Affected #: 2 files diff -r 945ebcd32f336a5b3b97c4c66aa1274a49d8e07b -r 2346e098b27e0141fdd3e9821895413e27bc2c2f lib/galaxy/workflow/modules.py --- a/lib/galaxy/workflow/modules.py +++ b/lib/galaxy/workflow/modules.py @@ -214,12 +214,8 @@ def get_data_inputs( self ): return [] - def get_data_outputs( self ): - return [ dict( name='output', extensions=['input'] ) ] - def get_config_form( self ): - form = formbuilder.FormBuilder( title=self.name ) \ - .add_text( "name", "Name", value=self.state['name'] ) + form = self._abstract_config_form( ) return self.trans.fill_template( "workflow/editor_generic_form.mako", module=self, form=form ) @@ -227,11 +223,7 @@ return dumps( self.state ) def update_state( self, incoming ): - self.state['name'] = incoming.get( 'name', 'Input Dataset' ) - - def get_runtime_inputs( self, filter_set=['data'] ): - label = self.state.get( "name", "Input Dataset" ) - return dict( input=DataToolParameter( None, Element( "param", name="input", label=label, multiple=True, type="data", format=', '.join(filter_set) ), self.trans ) ) + self.recover_state( incoming ) def get_runtime_state( self ): state = galaxy.tools.DefaultToolState() @@ -300,6 +292,18 @@ def default_state( Class ): return dict( name=Class.default_name ) + def _abstract_config_form( self ): + form = formbuilder.FormBuilder( title=self.name ) \ + .add_text( "name", "Name", value=self.state['name'] ) + return form + + def get_data_outputs( self ): + return [ dict( name='output', extensions=['input'] ) ] + + def get_runtime_inputs( self, filter_set=['data'] ): + label = self.state.get( "name", "Input Dataset" ) + return dict( input=DataToolParameter( None, Element( "param", name="input", label=label, multiple=True, type="data", format=', '.join(filter_set) ), self.trans ) ) + class InputDataCollectionModule( InputModule ): default_name = "Input Dataset Collection" @@ -319,7 +323,7 @@ input_element = Element( "param", name="input", label=label, type="data_collection", collection_type=collection_type ) return dict( input=DataCollectionToolParameter( None, input_element, self.trans ) ) - def get_config_form( self ): + def _abstract_config_form( self ): type_hints = odict.odict() type_hints[ "list" ] = "List of Datasets" type_hints[ "paired" ] = "Dataset Pair" @@ -338,12 +342,7 @@ "name", "Name", value=self.state['name'] ) form.inputs.append( type_input ) - return self.trans.fill_template( "workflow/editor_generic_form.mako", - module=self, form=form ) - - def update_state( self, incoming ): - self.state[ 'name' ] = incoming.get( 'name', self.default_name ) - self.state[ 'collection_type' ] = incoming.get( 'collection_type', self.collection_type ) + return form def get_data_outputs( self ): return [ dict( name='output', extensions=['input_collection'], collection_type=self.state[ 'collection_type' ] ) ] diff -r 945ebcd32f336a5b3b97c4c66aa1274a49d8e07b -r 2346e098b27e0141fdd3e9821895413e27bc2c2f test/unit/workflows/test_modules.py --- a/test/unit/workflows/test_modules.py +++ b/test/unit/workflows/test_modules.py @@ -74,6 +74,49 @@ assert state.inputs[ 'input' ] is hda +def test_data_input_connections(): + module = __from_step( + type="data_input", + ) + assert len( module.get_data_inputs() ) == 0 + + outputs = module.get_data_outputs() + assert len( outputs ) == 1 + output = outputs[ 0 ] + assert output[ 'name' ] == 'output' + assert output[ 'extensions' ] == [ 'input' ] + + +def test_data_input_update(): + module = __from_step( + type="data_input", + tool_inputs={ + "name": "Cool Input", + }, + ) + module.update_state( dict( name="Awesome New Name" ) ) + assert module.state[ 'name' ] == "Awesome New Name" + + +def test_data_input_get_form(): + module = __from_step( + type="data_input", + tool_inputs={ + "name": "Cool Input", + }, + ) + + def test_form(template, **kwds ): + assert template == "workflow/editor_generic_form.mako" + assert "form" in kwds + assert len( kwds[ "form" ].inputs ) == 1 + return "TEMPLATE" + + fill_mock = mock.Mock( side_effect=test_form ) + module.trans.fill_template = fill_mock + assert module.get_config_form() == "TEMPLATE" + + def test_data_collection_input_default_state(): trans = MockTrans() module = modules.module_factory.new( trans, "data_collection_input" ) @@ -99,6 +142,55 @@ __assert_has_runtime_input( module, label="Cool Input Collection", collection_type="list:paired" ) +def test_data_collection_input_connections(): + module = __from_step( + type="data_collection_input", + tool_inputs={ + 'collection_type': 'list:paired' + } + ) + assert len( module.get_data_inputs() ) == 0 + + outputs = module.get_data_outputs() + assert len( outputs ) == 1 + output = outputs[ 0 ] + assert output[ 'name' ] == 'output' + assert output[ 'extensions' ] == [ 'input_collection' ] + assert output[ 'collection_type' ] == 'list:paired' + + +def test_data_collection_input_update(): + module = __from_step( + type="data_collection_input", + tool_inputs={ + 'name': 'Cool Collection', + 'collection_type': 'list:paired', + } + ) + module.update_state( dict( name="New Collection", collection_type="list" ) ) + assert module.state[ 'name' ] == "New Collection" + + +def test_data_collection_input_config_form(): + module = __from_step( + type="data_collection_input", + tool_inputs={ + 'name': 'Cool Collection', + 'collection_type': 'list:paired', + } + ) + + def test_form(template, **kwds ): + assert template == "workflow/editor_generic_form.mako" + assert "form" in kwds + assert len( kwds[ "form" ].inputs ) == 2 + return "TEMPLATE" + + fill_mock = mock.Mock( side_effect=test_form ) + module.trans.fill_template = fill_mock + assert module.get_config_form() == "TEMPLATE" + + def test_cannot_create_tool_modules_for_missing_tools(): trans = MockTrans() exception = False Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.