diff --git a/aiida/backends/tests/__init__.py b/aiida/backends/tests/__init__.py index f089da8f82..de23638533 100644 --- a/aiida/backends/tests/__init__.py +++ b/aiida/backends/tests/__init__.py @@ -56,7 +56,7 @@ 'work.futures': ['aiida.backends.tests.work.test_futures'], 'work.persistence': ['aiida.backends.tests.work.persistence'], 'work.process': ['aiida.backends.tests.work.process'], - 'work.processSpec': ['aiida.backends.tests.work.processSpec'], + 'work.process_spec': ['aiida.backends.tests.work.test_process_spec'], 'work.rmq': ['aiida.backends.tests.work.test_rmq'], 'work.run': ['aiida.backends.tests.work.run'], 'work.runners': ['aiida.backends.tests.work.test_runners'], diff --git a/aiida/backends/tests/work/job_processes.py b/aiida/backends/tests/work/job_processes.py index d33833a9d0..7167f19355 100644 --- a/aiida/backends/tests/work/job_processes.py +++ b/aiida/backends/tests/work/job_processes.py @@ -10,16 +10,49 @@ from aiida import work from aiida.backends.testbase import AiidaTestCase +from aiida.common.utils import classproperty +from aiida.orm.data.base import Int from aiida.orm.calculation.job.simpleplugins.templatereplacer import TemplatereplacerCalculation from aiida.work.class_loader import ClassLoader from aiida.work.job_processes import JobProcess from . import utils -Job = TemplatereplacerCalculation.process() +class AdditionalParameterCalculation(TemplatereplacerCalculation): + """ + Subclass of TemplatereplacerCalculation that also defines a use method + with an additional parameter + """ + @classproperty + def _use_methods(cls): + retdict = TemplatereplacerCalculation._use_methods + retdict.update({ + 'pseudo': { + 'valid_types': Int, + 'additional_parameter': "kind", + 'linkname': cls._get_linkname_pseudo, + 'docstring': (''), + }, + }) + return retdict + + @classmethod + def _get_linkname_pseudo(cls, kind): + """ + Create the linkname based on the additional parameter + """ + if isinstance(kind, (tuple, list)): + suffix_string = '_'.join(kind) + elif isinstance(kind, basestring): + suffix_string = kind + else: + raise TypeError('invalid additional parameter type') + + return '{}_{}'.format('pseudo', suffix_string) class TestJobProcess(AiidaTestCase): + def setUp(self): super(TestJobProcess, self).setUp() self.assertEquals(len(work.ProcessStack.stack()), 0) @@ -54,7 +87,8 @@ def test_job_process_set_label_and_description(self): '_label': label, '_description': description } - job = Job(inputs) + process = TemplatereplacerCalculation.process() + job = process(inputs) self.assertEquals(job.calc.label, label) self.assertEquals(job.calc.description, description) @@ -76,4 +110,48 @@ def test_job_process_set_none(self): '_description': None } - Job(inputs) + process = TemplatereplacerCalculation.process() + job = process(inputs) + +class TestAdditionalParameterJobProcess(AiidaTestCase): + + def setUp(self): + super(TestAdditionalParameterJobProcess, self).setUp() + self.assertEquals(len(work.ProcessStack.stack()), 0) + self.runner = utils.create_test_runner() + + def tearDown(self): + super(TestAdditionalParameterJobProcess, self).tearDown() + self.assertEquals(len(work.ProcessStack.stack()), 0) + self.runner.close() + self.runner = None + work.set_runner(None) + + def test_class_loader(self): + cl = ClassLoader() + AdditionalParameterProcess = JobProcess.build(AdditionalParameterCalculation) + + def test_job_process_with_additional_parameter(self): + """ + Verify that the additional parameter use_method 'pseudo' is supported + """ + label = 'test_label' + description = 'test_description' + inputs = { + '_options': { + 'computer': self.computer, + 'resources': { + 'num_machines': 1, + 'num_mpiprocs_per_machine': 1 + }, + 'max_wallclock_seconds': 10, + }, + 'pseudo': { + 'a': Int(1), + 'b': Int(2), + }, + '_label': label, + '_description': description + } + process = AdditionalParameterCalculation.process() + job = process(inputs) \ No newline at end of file diff --git a/aiida/backends/tests/work/process.py b/aiida/backends/tests/work/process.py index 2196e276ec..fcb71c1dca 100644 --- a/aiida/backends/tests/work/process.py +++ b/aiida/backends/tests/work/process.py @@ -7,19 +7,56 @@ # For further information on the license, see the LICENSE.txt file # # For further information please visit http://www.aiida.net # ########################################################################### - - import threading -import aiida.work.utils as util +from plum.utils import AttributesFrozendict +from aiida import work from aiida.backends.testbase import AiidaTestCase from aiida.common.lang import override from aiida.orm import load_node from aiida.orm.data.base import Int from aiida.orm.data.frozendict import FrozenDict -from aiida.work.test_utils import DummyProcess, BadOutput -from aiida import work -from aiida.work.launch import run, run_get_pid +from aiida.work import utils +from aiida.work import test_utils + +class TestProcessNamespace(AiidaTestCase): + + def setUp(self): + super(TestProcessNamespace, self).setUp() + self.assertEquals(len(utils.ProcessStack.stack()), 0) + + def tearDown(self): + super(TestProcessNamespace, self).tearDown() + self.assertEquals(len(utils.ProcessStack.stack()), 0) + + def test_namespaced_process(self): + """ + Test that inputs in nested namespaces are properly validated and the link labels + are properly formatted by connecting the namespaces with underscores + """ + class NameSpacedProcess(work.Process): + + @classmethod + def define(cls, spec): + super(NameSpacedProcess, cls).define(spec) + spec.input('some.name.space.a', valid_type=Int) + + proc = NameSpacedProcess(inputs={'some': {'name': {'space': {'a': Int(5)}}}}) + + # Test that the namespaced inputs are AttributesFrozenDicts + self.assertIsInstance(proc.inputs, AttributesFrozendict) + self.assertIsInstance(proc.inputs.some, AttributesFrozendict) + self.assertIsInstance(proc.inputs.some.name, AttributesFrozendict) + self.assertIsInstance(proc.inputs.some.name.space, AttributesFrozendict) + + # Test that the input node is in the inputs of the process + input_node = proc.inputs.some.name.space.a + self.assertTrue(isinstance(input_node, Int)) + self.assertEquals(input_node.value, 5) + + # Check that the link of the WorkCalculation node has the correct link name + self.assertTrue('some_name_space_a' in proc.calc.get_inputs_dict()) + self.assertEquals(proc.calc.get_inputs_dict()['some_name_space_a'], 5) class ProcessStackTest(work.Process): @@ -44,26 +81,26 @@ class TestProcess(AiidaTestCase): def setUp(self): super(TestProcess, self).setUp() - self.assertEquals(len(util.ProcessStack.stack()), 0) + self.assertEquals(len(utils.ProcessStack.stack()), 0) def tearDown(self): super(TestProcess, self).tearDown() - self.assertEquals(len(util.ProcessStack.stack()), 0) + self.assertEquals(len(utils.ProcessStack.stack()), 0) def test_process_stack(self): - run(ProcessStackTest) + work.launch.run(ProcessStackTest) def test_inputs(self): with self.assertRaises(TypeError): - run(BadOutput) + work.launch.run(test_utils.BadOutput) def test_input_link_creation(self): dummy_inputs = ["1", "2", "3", "4"] inputs = {l: Int(l) for l in dummy_inputs} inputs['_store_provenance'] = True - p = DummyProcess(inputs) + p = test_utils.DummyProcess(inputs) for label, value in p._calc.get_inputs_dict().iteritems(): self.assertTrue(label in inputs) @@ -75,30 +112,30 @@ def test_input_link_creation(self): def test_none_input(self): # Check that if we pass no input the process runs fine - run(DummyProcess) + work.launch.run(test_utils.DummyProcess) def test_seal(self): - pid = run_get_pid(DummyProcess).pid + pid = work.launch.run_get_pid(test_utils.DummyProcess).pid self.assertTrue(load_node(pk=pid).is_sealed) def test_description(self): - dp = DummyProcess(inputs={'_description': "Rockin' process"}) + dp = test_utils.DummyProcess(inputs={'_description': "Rockin' process"}) self.assertEquals(dp.calc.description, "Rockin' process") with self.assertRaises(ValueError): - DummyProcess(inputs={'_description': 5}) + test_utils.DummyProcess(inputs={'_description': 5}) def test_label(self): - dp = DummyProcess(inputs={'_label': 'My label'}) + dp = test_utils.DummyProcess(inputs={'_label': 'My label'}) self.assertEquals(dp.calc.label, 'My label') with self.assertRaises(ValueError): - DummyProcess(inputs={'_label': 5}) + test_utils.DummyProcess(inputs={'_label': 5}) def test_work_calc_finish(self): - p = DummyProcess() + p = test_utils.DummyProcess() self.assertFalse(p.calc.has_finished_ok()) - run(p) + work.launch.run(p) self.assertTrue(p.calc.has_finished_ok()) def test_calculation_input(self): @@ -106,24 +143,23 @@ def test_calculation_input(self): def simple_wf(): return {'a': Int(6), 'b': Int(7)} - outputs, pid = run_get_pid(simple_wf) + outputs, pid = work.launch.run_get_pid(simple_wf) calc = load_node(pid) - dp = DummyProcess(inputs={'calc': calc}) - run(dp) + dp = test_utils.DummyProcess(inputs={'calc': calc}) + work.launch.run(dp) input_calc = dp.calc.get_inputs_dict()['calc'] self.assertTrue(isinstance(input_calc, FrozenDict)) self.assertEqual(input_calc['a'], outputs['a']) def test_save_instance_state(self): - proc = DummyProcess() + proc = test_utils.DummyProcess() # Save the instance state bundle = work.Bundle(proc) proc2 = bundle.unbundle() - class TestFunctionProcess(AiidaTestCase): def test_fixed_inputs(self): def wf(a, b, c): @@ -131,7 +167,7 @@ def wf(a, b, c): inputs = {'a': Int(4), 'b': Int(5), 'c': Int(6)} function_process_class = work.FunctionProcess.build(wf) - self.assertEqual(run(function_process_class, **inputs), inputs) + self.assertEqual(work.launch.run(function_process_class, **inputs), inputs) def test_kwargs(self): def wf_with_kwargs(**kwargs): @@ -147,13 +183,13 @@ def wf_fixed_args(a): inputs = {'a': a} function_process_class = work.FunctionProcess.build(wf_with_kwargs) - outs = run(function_process_class, **inputs) + outs = work.launch.run(function_process_class, **inputs) self.assertEqual(outs, inputs) function_process_class = work.FunctionProcess.build(wf_without_kwargs) with self.assertRaises(ValueError): - run(function_process_class, **inputs) + work.launch.run(function_process_class, **inputs) function_process_class = work.FunctionProcess.build(wf_fixed_args) - outs = run(function_process_class, **inputs) + outs = work.launch.run(function_process_class, **inputs) self.assertEqual(outs, inputs) diff --git a/aiida/backends/tests/work/processSpec.py b/aiida/backends/tests/work/test_process_spec.py similarity index 80% rename from aiida/backends/tests/work/processSpec.py rename to aiida/backends/tests/work/test_process_spec.py index 2ce0c43665..3ce5442217 100644 --- a/aiida/backends/tests/work/processSpec.py +++ b/aiida/backends/tests/work/test_process_spec.py @@ -44,11 +44,10 @@ def test_dynamic_input(self): n = Node() d = Data() - port = self.spec.get_dynamic_input() - self.assertFalse(port.validate("foo")[0]) - self.assertFalse(port.validate(5)[0]) - self.assertFalse(port.validate(n)[0]) - self.assertTrue(port.validate(d)[0]) + self.assertFalse(self.spec.validate_inputs({'key': 'foo'})[0]) + self.assertFalse(self.spec.validate_inputs({'key': 5})[0]) + self.assertFalse(self.spec.validate_inputs({'key': n})[0]) + self.assertTrue(self.spec.validate_inputs({'key': d})[0]) def test_dynamic_output(self): from aiida.orm import Node @@ -56,11 +55,10 @@ def test_dynamic_output(self): n = Node() d = Data() - port = self.spec.get_dynamic_output() - self.assertFalse(port.validate("foo")[0]) - self.assertFalse(port.validate(5)[0]) - self.assertFalse(port.validate(n)[0]) - self.assertTrue(port.validate(d)[0]) + self.assertFalse(self.spec.validate_inputs({'key': 'foo'})[0]) + self.assertFalse(self.spec.validate_inputs({'key': 5})[0]) + self.assertFalse(self.spec.validate_inputs({'key': n})[0]) + self.assertTrue(self.spec.validate_inputs({'key': d})[0]) def _test_template(self, template): template.a = 2 diff --git a/aiida/backends/tests/work/work_chain.py b/aiida/backends/tests/work/work_chain.py index 19ed2601c7..5714078f58 100644 --- a/aiida/backends/tests/work/work_chain.py +++ b/aiida/backends/tests/work/work_chain.py @@ -34,7 +34,7 @@ def define(cls, spec): super(Wf, cls).define(spec) spec.input("value", default=Str('A')) spec.input("n", default=Int(3)) - spec.dynamic_output() + spec.outputs.dynamic = True spec.outline( cls.s1, if_(cls.isA)( @@ -331,7 +331,7 @@ class MainWorkChain(WorkChain): def define(cls, spec): super(MainWorkChain, cls).define(spec) spec.outline(cls.run, cls.check) - spec.dynamic_output() + spec.outputs.dynamic = True def run(self): return ToContext(subwc=self.submit(SubWorkChain)) @@ -356,7 +356,7 @@ class MainWorkChain(WorkChain): def define(cls, spec): super(MainWorkChain, cls).define(spec) spec.outline(cls.run, cls.check) - spec.dynamic_output() + spec.outputs.dynamic = True def run(self): return ToContext(subwc=self.submit(SubWorkChain)) @@ -408,7 +408,7 @@ class TestWorkChain(WorkChain): def define(cls, spec): super(TestWorkChain, cls).define(spec) spec.outline(cls.run, cls.check) - spec.dynamic_output() + spec.outputs.dynamic = True def run(self): from aiida.orm.backend import construct @@ -703,3 +703,88 @@ def test_simple_kill_through_node(self): self.assertEquals(process.calc.has_finished_ok(), False) self.assertEquals(process.calc.has_failed(), False) self.assertEquals(process.calc.has_aborted(), True) + + +class TestImmutableInputWorkchain(AiidaTestCase): + """ + Test that inputs cannot be modified + """ + def setUp(self): + super(TestImmutableInputWorkchain, self).setUp() + self.assertEquals(len(ProcessStack.stack()), 0) + + def tearDown(self): + super(TestImmutableInputWorkchain, self).tearDown() + self.assertEquals(len(ProcessStack.stack()), 0) + + def test_immutable_input(self): + """ + Check that from within the WorkChain self.inputs returns an AttributesFrozendict which should be immutable + """ + test_class = self + + class Wf(WorkChain): + @classmethod + def define(cls, spec): + super(Wf, cls).define(spec) + spec.input('a', valid_type=Int) + spec.input('b', valid_type=Int) + spec.outline( + cls.step_one, + cls.step_two, + ) + + def step_one(self): + # Attempt to manipulate the inputs dictionary which since it is a AttributesFrozendict should raise + with test_class.assertRaises(TypeError): + self.inputs['a'] = Int(3) + with test_class.assertRaises(AttributeError): + self.inputs.pop('b') + with test_class.assertRaises(TypeError): + self.inputs['c'] = Int(4) + + def step_two(self): + # Verify that original inputs are still there with same value and no inputs were added + test_class.assertIn('a', self.inputs) + test_class.assertIn('b', self.inputs) + test_class.assertNotIn('c', self.inputs) + test_class.assertEquals(self.inputs['a'].value, 1) + + work.launch.run(Wf, a=Int(1), b=Int(2)) + + + def test_immutable_input_groups(self): + """ + Check that namespaced inputs also return AttributeFrozendicts and are hence immutable + """ + test_class = self + + class Wf(WorkChain): + @classmethod + def define(cls, spec): + super(Wf, cls).define(spec) + spec.input_namespace('subspace', dynamic=True) + spec.outline( + cls.step_one, + cls.step_two, + ) + + def step_one(self): + # Attempt to manipulate the namespaced inputs dictionary which should raise + with test_class.assertRaises(TypeError): + self.inputs.subspace['one'] = Int(3) + with test_class.assertRaises(AttributeError): + self.inputs.subspace.pop('two') + with test_class.assertRaises(TypeError): + self.inputs.subspace['four'] = Int(4) + + def step_two(self): + # Verify that original inputs are still there with same value and no inputs were added + test_class.assertIn('one', self.inputs.subspace) + test_class.assertIn('two', self.inputs.subspace) + test_class.assertNotIn('four', self.inputs.subspace) + test_class.assertEquals(self.inputs.subspace['one'].value, 1) + + x = Int(1) + y = Int(2) + work.launch.run(Wf, subspace={'one': Int(1), 'two': Int(2)}) \ No newline at end of file diff --git a/aiida/work/job_processes.py b/aiida/work/job_processes.py index ce8af783cc..c50e48ea7f 100644 --- a/aiida/work/job_processes.py +++ b/aiida/work/job_processes.py @@ -111,16 +111,19 @@ def define(cls_, spec): spec.input(cls.OPTIONS_INPUT_LABEL, validator=processes.DictSchema(options)) # Inputs from use methods - for k, v in calc_class._use_methods.iteritems(): - if v.get('additional_parameter'): - spec.input_group(k, help=v.get('docstring', None), - valid_type=v['valid_types'], required=False) + for key, use_method in calc_class._use_methods.iteritems(): + + valid_type = use_method['valid_types'] + docstring = use_method.get('docstring', None) + additional_parameter = use_method.get('additional_parameter') + + if additional_parameter: + spec.input_namespace(key, help=docstring, valid_type=valid_type, required=False, dynamic=True) else: - spec.input(k, help=v.get('docstring', None), - valid_type=v['valid_types'], required=False) + spec.input(key, help=docstring, valid_type=valid_type, required=False) # Outputs - spec.dynamic_output(valid_type=Data) + spec.outputs.valid_type = Data class_name = "{}_{}".format(cls.__name__, utils.class_name(calc_class)) @@ -167,9 +170,8 @@ def _setup_db_record(self): continue # Call the 'use' methods to set up the data-calc links - if isinstance(self.spec().get_input(name), port.InputGroupPort): - additional = \ - self._CALC_CLASS._use_methods[name]['additional_parameter'] + if isinstance(self.spec().inputs[name], port.PortNamespace): + additional = self._CALC_CLASS._use_methods[name]['additional_parameter'] for k, v in input.iteritems(): try: diff --git a/aiida/work/processes.py b/aiida/work/processes.py index 824e600e3c..9cfe0f0a16 100644 --- a/aiida/work/processes.py +++ b/aiida/work/processes.py @@ -104,18 +104,18 @@ class InputPort(_WithNonDb, plum.port.InputPort): pass -class DynamicInputPort(_WithNonDb, plum.port.DynamicInputPort): - pass - - -class InputGroupPort(_WithNonDb, plum.port.InputGroupPort): +class PortNamespace(_WithNonDb, plum.port.PortNamespace): pass class ProcessSpec(plum.process.ProcessSpec): + INPUT_PORT_TYPE = InputPort - DYNAMIC_INPUT_PORT_TYPE = DynamicInputPort - INPUT_GROUP_PORT_TYPE = InputGroupPort + PORT_NAMESPACE_TYPE = PortNamespace + + def __init__(self): + super(ProcessSpec, self).__init__() + def get_inputs_template(self): """ @@ -163,16 +163,11 @@ class SaveKeys(Enum): @classmethod def define(cls, spec): super(Process, cls).define(spec) - - spec.input("_store_provenance", valid_type=bool, default=True, - non_db=True) - spec.input("_description", valid_type=basestring, required=False, - non_db=True) - spec.input("_label", valid_type=basestring, required=False, - non_db=True) - - spec.dynamic_input(valid_type=(aiida.orm.Data, aiida.orm.Calculation)) - spec.dynamic_output(valid_type=aiida.orm.Data) + spec.input("_store_provenance", valid_type=bool, default=True, non_db=True) + spec.input("_description", valid_type=basestring, required=False, non_db=True) + spec.input("_label", valid_type=basestring, required=False, non_db=True) + spec.inputs.valid_type = (aiida.orm.Data, aiida.orm.Calculation) + spec.outputs.valid_type = (aiida.orm.Data) @classmethod def get_inputs_template(cls): @@ -191,8 +186,7 @@ def get_or_create_db_record(cls): _spec_type = ProcessSpec - def __init__(self, inputs=None, logger=None, runner=None, - parent_pid=None, enable_persistence=True): + def __init__(self, inputs=None, logger=None, runner=None, parent_pid=None, enable_persistence=True): self._runner = runner if runner is not None else runners.get_runner() super(Process, self).__init__( @@ -426,41 +420,19 @@ def _setup_db_record(self): parent_calc = self.get_parent_calc() - # First get a dictionary of all the inputs to link, this is needed to - # deal with things like input groups - to_link = {} - for name, input in self.inputs.iteritems(): - try: - port = self.spec().get_input(name) - except ValueError: - # It's not in the spec, so we better support dynamic inputs - assert self.spec().has_dynamic_input() - to_link[name] = input - else: - # Ignore any inputs that should not be saved - if port.non_db: - continue - - if isinstance(port, plum.port.InputGroupPort): - to_link.update( - {"{}_{}".format(name, k): v for k, v in - input.iteritems()}) - else: - to_link[name] = input - - for name, input in to_link.iteritems(): + for name, input_value in self._flat_inputs().iteritems(): - if isinstance(input, Calculation): - input = utils.get_or_create_output_group(input) + if isinstance(input_value, Calculation): + input_value = utils.get_or_create_output_group(input_value) - if not input.is_stored: + if not input_value.is_stored: # If the input isn't stored then assume our parent created it if parent_calc: - input.add_link_from(parent_calc, "CREATE", link_type=LinkType.CREATE) + input_value.add_link_from(parent_calc, "CREATE", link_type=LinkType.CREATE) if self.inputs._store_provenance: - input.store() + input_value.store() - self.calc.add_link_from(input, name) + self.calc.add_link_from(input_value, name) if parent_calc: self.calc.add_link_from(parent_calc, "CALL", @@ -477,6 +449,46 @@ def _add_description_and_label(self): if label is not None: self._calc.label = label + def _flat_inputs(self): + """ + Return a flattened version of the parsed inputs dictionary. The eventual + keys will be a concatenation of the nested keys + + :return: flat dictionary of parsed inputs + """ + return dict(self._flatten_inputs(self.spec().inputs, self.inputs)) + + def _flatten_inputs(self, port, port_value, parent_name='', separator='_'): + """ + Function that will recursively flatten the inputs dictionary, omitting inputs for ports that + are marked as being non database storable + + :param port: port against which to map the port value, can be InputPort or PortNamespace + :param port_value: value for the current port, can be a Mapping + :param parent_name: the parent key with which to prefix the keys + :param separator: character to use for the concatenation of keys + """ + items = [] + + if isinstance(port_value, collections.Mapping): + + for name, value in port_value.iteritems(): + + prefixed_key = parent_name + separator + name if parent_name else name + + try: + nested_port = port[name] + except KeyError: + # Port does not exist in the port namespace, add it regardless of type of value + items.append((prefixed_key, value)) + else: + sub_items = self._flatten_inputs(nested_port, value, prefixed_key, separator) + items.extend(sub_items) + else: + if not port.non_db: + items.append((parent_name, port_value)) + + return items class FunctionProcess(Process): _func_args = None @@ -524,12 +536,12 @@ def _define(cls, spec): # If the function support kwargs then allow dynamic inputs, # otherwise disallow if keywords is not None: - spec.dynamic_input() + spec.inputs.dynamic = True else: - spec.no_dynamic_input() + spec.inputs.dynamic = False # We don't know what a function will return so keep it dynamic - spec.dynamic_output(valid_type=Data) + spec.outputs.valid_type = Data return type(func.__name__, (FunctionProcess,), {'_func': staticmethod(func), @@ -578,10 +590,10 @@ def _run(self): kwargs = {} for name, value in self.inputs.items(): try: - if self.spec().get_input(name).non_db: + if self.spec().inputs[name].non_db: # Don't consider non-database inputs continue - except ValueError: + except KeyError: pass # No port found # Check if it is a positional arg, if not then keyword diff --git a/aiida/work/runners.py b/aiida/work/runners.py index 99e1ecc9fe..fb686399f0 100644 --- a/aiida/work/runners.py +++ b/aiida/work/runners.py @@ -166,7 +166,7 @@ def run_until_complete(self, future): def close(self): if self._rmq_connector is not None: - self._rmq_connector.close() + self._rmq_connector.disconnect() def run(self, process, *args, **inputs): """ diff --git a/aiida/work/test_utils.py b/aiida/work/test_utils.py index fa3990b9de..0519166141 100644 --- a/aiida/work/test_utils.py +++ b/aiida/work/test_utils.py @@ -21,8 +21,8 @@ class DummyProcess(Process): @classmethod def define(cls, spec): super(DummyProcess, cls).define(spec) - spec.dynamic_input() - spec.dynamic_output() + spec.inputs.dynamic = True + spec.outputs.dynamic = True def _run(self): pass @@ -48,7 +48,7 @@ class BadOutput(Process): @classmethod def define(cls, spec): super(BadOutput, cls).define(spec) - spec.dynamic_output() + spec.outputs.dynamic = True def _run(self): self.out("bad_output", 5) diff --git a/aiida/work/workchain.py b/aiida/work/workchain.py index a706717f90..41421c168f 100644 --- a/aiida/work/workchain.py +++ b/aiida/work/workchain.py @@ -10,6 +10,7 @@ import abc import functools import inspect +import re import plum from aiida.common.extendeddicts import AttributeDict @@ -31,13 +32,12 @@ def __init__(self): self._outline = None def get_description(self): - desc = [super(_WorkChainSpec, self).get_description()] + description = super(_WorkChainSpec, self).get_description() + if self._outline: - desc.append("Outline") - desc.append("=======") - desc.append(self._outline.get_description()) + description['outline'] = self._outline.get_description() - return "\n".join(desc) + return description def outline(self, *commands): """ @@ -66,8 +66,8 @@ def define(cls, spec): super(WorkChain, cls).define(spec) # For now workchains can accept any input and emit any output # If this changes in the future the spec should be updated here. - spec.dynamic_input() - spec.dynamic_output() + spec.inputs.dynamic = True + spec.outputs.dynamic = True def __init__(self, inputs=None, logger=None, runner=None): super(WorkChain, self).__init__(inputs=inputs, logger=logger, runner=runner) @@ -433,19 +433,18 @@ def create_stepper(self, workflow): return _BlockStepper(workflow, self._commands) @override - def get_description(self, indent_level=0, indent_increment=4): - indent = ' ' * (indent_level * indent_increment) - desc = [] + def get_description(self): + description = {} + for c in self._commands: if isinstance(c, _Instruction): - desc.append(c.get_description()) + description[type(c).__name__] = c.get_description() else: - desc.append('{}* {}'.format(indent, c.__name__)) if c.__doc__: - doc = c.__doc__ - desc.append('{}{}'.format(indent,doc)) + doc = re.sub(r'\n\s*', ' ', c.__doc__) + description[c.__name__] = doc.strip() - return '\n'.join(desc) + return description class _Conditional(object): @@ -582,11 +581,21 @@ def conditionals(self): @override def get_description(self): - description = ['if {}:\n{}'.format(self._ifs[0].condition.__name__, self._ifs[0].body.get_description(indent_level=1))] + description = {} + + description['if'] = { + 'condition': self._ifs[0].condition.__name__, + 'body': self._ifs[0].body.get_description() + } + for conditional in self._ifs[1:]: - description.append('elif {}:\n{}'.format( - conditional.condition.__name__, conditional.body.get_description(indent_level=1))) - return '\n'.join(description) + description.setdefault('elif', []) + description['elif'].append({ + 'condition': conditional.condition.__name__, + 'body': conditional.body.get_description() + }) + + return description class _WhileStepper(Stepper): @@ -662,7 +671,10 @@ def create_stepper(self, workflow): @override def get_description(self): - return "while {}:\n{}".format(self.condition.__name__, self.body.get_description(indent_level=1)) + return { + 'condition': self.condition.__name__, + 'body': self.body.get_description(), + } class _PropagateReturn(BaseException): @@ -702,7 +714,7 @@ def get_description(self): :return: The description :rtype: str """ - return "Return from the outline immediately" + return 'Return from the outline immediately' def if_(condition): diff --git a/requirements.txt b/requirements.txt index eda88a99bb..cb94761053 100644 --- a/requirements.txt +++ b/requirements.txt @@ -145,4 +145,4 @@ Werkzeug==0.14.1 wrapt==1.10.11 yapf==0.19.0 kiwipy>=0.2.0.dev1 --e git+https://github.com/muhrin/plumpy.git@de20256e681323012d270788f8c89e985a605f1c#egg=plumpy +-e git+https://github.com/muhrin/plumpy.git@6b5707a3a8e83a9b404cb6744ef477c1c8c0c5e4#egg=plumpy