diff --git a/.ci/polish/cli.py b/.ci/polish/cli.py index 2fed94d1a2..62d7458048 100755 --- a/.ci/polish/cli.py +++ b/.ci/polish/cli.py @@ -154,7 +154,7 @@ def launch(expression, code, use_calculations, use_calcfunctions, sleep, timeout sys.exit(1) try: - result = workchain.out.result + result = workchain.outputs.result except AttributeError: click.secho('Failed: ', fg='red', bold=True, nl=False) click.secho('the workchain<{}> did not return a result output node'.format(workchain.pk), bold=True) diff --git a/.ci/polish/lib/template/workchain.tpl b/.ci/polish/lib/template/workchain.tpl index f32e839efd..20fb6924a7 100644 --- a/.ci/polish/lib/template/workchain.tpl +++ b/.ci/polish/lib/template/workchain.tpl @@ -81,7 +81,7 @@ ${outline} return self.ERROR_NO_JOB_CALCULATION try: - self.ctx.result = calculation.out.sum % self.inputs.modulo + self.ctx.result = calculation.outputs.sum % self.inputs.modulo except AttributeError as exception: self.report('no output node found') return self.ERROR_NO_OUTPUT_NODE @@ -118,7 +118,7 @@ ${outline} def post_raise_power(self): sub_result = prod(self.ctx.iterators_sign) for workchain in self.ctx.workchains: - sub_result *= workchain.out.result.value + sub_result *= workchain.outputs.result.value sub_result %= self.inputs.modulo.value self.ctx.result += sub_result diff --git a/.ci/test_daemon.py b/.ci/test_daemon.py index 8ec49ef8cd..73229c18cd 100644 --- a/.ci/test_daemon.py +++ b/.ci/test_daemon.py @@ -86,7 +86,7 @@ def validate_calculations(expected_results): valid = False try: - actual_dict = calc.out.output_parameters.get_dict() + actual_dict = calc.outputs.output_parameters.get_dict() except exceptions.NotExistent: print('Could not retrieve `output_parameters` node for Calculation<{}>'.format(pk)) print_report(pk) @@ -112,7 +112,7 @@ def validate_workchains(expected_results): this_valid = True try: calc = load_node(pk) - actual_value = calc.out.output + actual_value = calc.outputs.output except (exceptions.NotExistent, AttributeError) as exception: print("* UNABLE TO RETRIEVE VALUE for workchain pk={}: I expected {}, I got {}: {}" .format(pk, expected_value, type(exception), exception)) diff --git a/.ci/workchains.py b/.ci/workchains.py index 43694e0be8..90226f6f4c 100644 --- a/.ci/workchains.py +++ b/.ci/workchains.py @@ -47,7 +47,7 @@ def finalize(self): if self.should_submit(): self.report('Getting sub-workchain output.') sub_workchain = self.ctx.workchain[0] - self.out('output', sub_workchain.out.output + 1) + self.out('output', sub_workchain.outputs.output + 1) else: self.report('Bottom-level workchain reached.') self.out('output', Int(0)) diff --git a/aiida/backends/tests/engine/test_calcfunctions.py b/aiida/backends/tests/engine/test_calcfunctions.py index 37f27375fe..22e93cdc0c 100644 --- a/aiida/backends/tests/engine/test_calcfunctions.py +++ b/aiida/backends/tests/engine/test_calcfunctions.py @@ -67,9 +67,9 @@ def test_calcfunction_default_linkname(self): """Verify that a calcfunction that returns a single Data node gets a default link label.""" _, node = self.test_calcfunction.run_get_node(self.default_int) - self.assertEqual(node.out.result, self.default_int.value + 1) - self.assertEqual(getattr(node.out, Process.SINGLE_OUTPUT_LINKNAME), self.default_int.value + 1) - self.assertEqual(node.out[Process.SINGLE_OUTPUT_LINKNAME], self.default_int.value + 1) + self.assertEqual(node.outputs.result, self.default_int.value + 1) + self.assertEqual(getattr(node.outputs, Process.SINGLE_OUTPUT_LINKNAME), self.default_int.value + 1) + self.assertEqual(node.outputs[Process.SINGLE_OUTPUT_LINKNAME], self.default_int.value + 1) def test_calcfunction_caching(self): """Verify that a calcfunction can be cached.""" diff --git a/aiida/backends/tests/engine/test_work_chain.py b/aiida/backends/tests/engine/test_work_chain.py index 6bbbf53735..640c527d7e 100644 --- a/aiida/backends/tests/engine/test_work_chain.py +++ b/aiida/backends/tests/engine/test_work_chain.py @@ -409,15 +409,15 @@ def s1(self): return ToContext(r1=self.submit(ReturnA), r2=self.submit(ReturnB)) def s2(self): - test_case.assertEquals(self.ctx.r1.out.res, A) - test_case.assertEquals(self.ctx.r2.out.res, B) + test_case.assertEquals(self.ctx.r1.outputs.res, A) + test_case.assertEquals(self.ctx.r2.outputs.res, B) # Try overwriting r1 return ToContext(r1=self.submit(ReturnB)) def s3(self): - test_case.assertEquals(self.ctx.r1.out.res, B) - test_case.assertEquals(self.ctx.r2.out.res, B) + test_case.assertEquals(self.ctx.r1.outputs.res, B) + test_case.assertEquals(self.ctx.r2.outputs.res, B) run_and_check_success(Wf) @@ -519,7 +519,7 @@ def do_run(self): def check(self): pass - assert self.ctx.subwc.out.value == Int(5) + assert self.ctx.subwc.outputs.value == Int(5) class SubWorkChain(WorkChain): @@ -547,7 +547,7 @@ def do_run(self): return ToContext(subwc=self.submit(SubWorkChain)) def check(self): - assert self.ctx.subwc.out.value == Int(5) + assert self.ctx.subwc.outputs.value == Int(5) class SubWorkChain(WorkChain): @@ -649,8 +649,8 @@ def begin(self): return ToContext(result_b=self.submit(SimpleWc)) def result(self): - test_case.assertEquals(self.ctx.result_a.out._return, val) - test_case.assertEquals(self.ctx.result_b.out._return, val) + test_case.assertEquals(self.ctx.result_a.outputs._return, val) + test_case.assertEquals(self.ctx.result_b.outputs._return, val) run_and_check_success(Workchain) diff --git a/aiida/backends/tests/engine/test_workfunctions.py b/aiida/backends/tests/engine/test_workfunctions.py index f80ed73448..006860b09c 100644 --- a/aiida/backends/tests/engine/test_workfunctions.py +++ b/aiida/backends/tests/engine/test_workfunctions.py @@ -66,9 +66,9 @@ def test_workfunction_default_linkname(self): """Verify that a workfunction that returns a single Data node gets a default link label.""" _, node = self.test_workfunction.run_get_node(self.default_int) - self.assertEqual(node.out.result, self.default_int) - self.assertEqual(getattr(node.out, Process.SINGLE_OUTPUT_LINKNAME), self.default_int) - self.assertEqual(node.out[Process.SINGLE_OUTPUT_LINKNAME], self.default_int) + self.assertEqual(node.outputs.result, self.default_int) + self.assertEqual(getattr(node.outputs, Process.SINGLE_OUTPUT_LINKNAME), self.default_int) + self.assertEqual(node.outputs[Process.SINGLE_OUTPUT_LINKNAME], self.default_int) def test_workfunction_caching(self): """Verify that a workfunction cannot be cached.""" diff --git a/aiida/backends/tests/orm/node/test_node.py b/aiida/backends/tests/orm/node/test_node.py index 693ce99408..dcff7d9fd7 100644 --- a/aiida/backends/tests/orm/node/test_node.py +++ b/aiida/backends/tests/orm/node/test_node.py @@ -303,3 +303,79 @@ def test_get_node_by_label(self): with self.assertRaises(exceptions.NotExistent): data.get_outgoing(link_type=LinkType.INPUT_CALC).get_node_by_label('some_weird_label') + + def test_tab_completable_properties(self): + """Test properties to go from one node to a neighboring one""" + input1 = Data().store() + input2 = Data().store() + + top_workflow = WorkflowNode().store() + workflow = WorkflowNode().store() + calc1 = CalculationNode().store() + calc2 = CalculationNode().store() + + output1 = Data().store() + output2 = Data().store() + + # top_workflow has two inputs, proxies them to workflow, that in turn + # calls two calcs (passing 1 data to each), + # and return the two data nodes returned one by each called calculation + top_workflow.add_incoming(input1, link_type=LinkType.INPUT_WORK, link_label='a') + top_workflow.add_incoming(input2, link_type=LinkType.INPUT_WORK, link_label='b') + + workflow.add_incoming(input1, link_type=LinkType.INPUT_WORK, link_label='a') + workflow.add_incoming(input2, link_type=LinkType.INPUT_WORK, link_label='b') + workflow.add_incoming(top_workflow, link_type=LinkType.CALL_WORK, link_label='CALL') + + calc1.add_incoming(input1, link_type=LinkType.INPUT_CALC, link_label='input_value') + calc1.add_incoming(workflow, link_type=LinkType.CALL_CALC, link_label='CALL') + output1.add_incoming(calc1, link_type=LinkType.CREATE, link_label='result') + + calc2.add_incoming(input2, link_type=LinkType.INPUT_CALC, link_label='input_value') + calc2.add_incoming(workflow, link_type=LinkType.CALL_CALC, link_label='CALL') + output2.add_incoming(calc2, link_type=LinkType.CREATE, link_label='result') + + output1.add_incoming(workflow, link_type=LinkType.RETURN, link_label='result_a') + output2.add_incoming(workflow, link_type=LinkType.RETURN, link_label='result_b') + output1.add_incoming(top_workflow, link_type=LinkType.RETURN, link_label='result_a') + output2.add_incoming(top_workflow, link_type=LinkType.RETURN, link_label='result_b') + + ## Now we test the methods + # creator + self.assertEqual(output1.creator.pk, calc1.pk) + self.assertEqual(output2.creator.pk, calc2.pk) + + # caller (for calculations) + self.assertEqual(calc1.caller.pk, workflow.pk) + self.assertEqual(calc2.caller.pk, workflow.pk) + + # caller (for workflows) + self.assertEqual(workflow.caller.pk, top_workflow.pk) + + # .inputs for calculations + self.assertEqual(calc1.inputs.input_value.pk, input1.pk) + self.assertEqual(calc2.inputs.input_value.pk, input2.pk) + with self.assertRaises(exceptions.NotExistent): + _ = calc1.inputs.some_label + + # .inputs for workflows + self.assertEqual(top_workflow.inputs.a.pk, input1.pk) + self.assertEqual(top_workflow.inputs.b.pk, input2.pk) + self.assertEqual(workflow.inputs.a.pk, input1.pk) + self.assertEqual(workflow.inputs.b.pk, input2.pk) + with self.assertRaises(exceptions.NotExistent): + _ = workflow.inputs.some_label + + # .outputs for calculations + self.assertEqual(calc1.outputs.result.pk, output1.pk) + self.assertEqual(calc2.outputs.result.pk, output2.pk) + with self.assertRaises(exceptions.NotExistent): + _ = calc1.outputs.some_label + + # .outputs for workflows + self.assertEqual(top_workflow.outputs.result_a.pk, output1.pk) + self.assertEqual(top_workflow.outputs.result_b.pk, output2.pk) + self.assertEqual(workflow.outputs.result_a.pk, output1.pk) + self.assertEqual(workflow.outputs.result_b.pk, output2.pk) + with self.assertRaises(exceptions.NotExistent): + _ = workflow.outputs.some_label # noqa diff --git a/aiida/backends/tests/test_parsers.py b/aiida/backends/tests/test_parsers.py index e0a02d0049..b8ef91fc0f 100644 --- a/aiida/backends/tests/test_parsers.py +++ b/aiida/backends/tests/test_parsers.py @@ -103,7 +103,7 @@ def output_test(pk, testname, skip_uuids_from_inputs=[]): folder = Folder(outfolder) to_export = [c.dbnode] + inputs try: - to_export.append(c.out.retrieved.dbnode) + to_export.append(c.outputs.retrieved.dbnode) except AttributeError: raise ValueError("No output retrieved node; without it, we cannot test the parser!") export_tree(to_export, folder=folder, also_parents=False, also_calc_outputs=False) @@ -188,7 +188,7 @@ def read_test(self, outfolder): calc = c break - retrieved = calc.out.retrieved + retrieved = calc.outputs.retrieved try: with io.open(os.path.join(outfolder, '_aiida_checks.json', encoding='utf8')) as fhandle: diff --git a/aiida/cmdline/commands/cmd_calcjob.py b/aiida/cmdline/commands/cmd_calcjob.py index b2cf861255..a50b73270b 100644 --- a/aiida/cmdline/commands/cmd_calcjob.py +++ b/aiida/cmdline/commands/cmd_calcjob.py @@ -132,7 +132,7 @@ def calcjob_outputcat(calcjob, path): entry_point.name)) try: - retrieved = calcjob.out.retrieved + retrieved = calcjob.outputs.retrieved except AttributeError: echo.echo_critical("No 'retrieved' node found. Have the calcjob files already been retrieved?") @@ -178,7 +178,7 @@ def calcjob_outputls(calcjob, path, color): from aiida.cmdline.utils.repository import list_repository_contents try: - retrieved = calcjob.out.retrieved + retrieved = calcjob.outputs.retrieved except AttributeError: echo.echo_critical("No 'retrieved' node found. Have the calcjob files already been retrieved?") diff --git a/aiida/orm/nodes/data/data.py b/aiida/orm/nodes/data/data.py index 6b27cdf403..19bd85056c 100644 --- a/aiida/orm/nodes/data/data.py +++ b/aiida/orm/nodes/data/data.py @@ -128,14 +128,14 @@ def set_source(self, source): self.source = source @property - def created_by(self): + def creator(self): """Return the creator of this node or None if it does not exist. :return: the creating node or None """ inputs = self.get_incoming(link_type=LinkType.CREATE) if inputs: - return inputs.first() + return inputs.first().node return None diff --git a/aiida/orm/nodes/node.py b/aiida/orm/nodes/node.py index 2fa5778e04..ad45a5bf9e 100644 --- a/aiida/orm/nodes/node.py +++ b/aiida/orm/nodes/node.py @@ -27,7 +27,6 @@ from aiida.orm.utils.links import LinkManager, LinkTriple from aiida.orm.utils.repository import Repository from aiida.orm.utils.node import AbstractNodeMeta, clean_value -from aiida.orm.utils.managers import NodeInputManager, NodeOutputManager from ..comments import Comment from ..computers import Computer @@ -973,28 +972,6 @@ def has_cached_links(self): """ return bool(self._incoming_cache) - @property - def out(self): - """Return an instance of `NodeOutputManager` - - The `NodeOutputManager` allows you to easily explore the nodes that have outgoing links from this node. - The outgoing nodes are reachable by their link labels which are attributes of the manager. - - :return: `NodeOutputManager` - """ - return NodeOutputManager(self) - - @property - def inp(self): - """Return an instance of `NodeInputManager` - - The `NodeInputManager` allows you to easily explore the nodes that have incoming links to this node. - The incoming nodes are reachable by their link labels which are attributes of the manager. - - :return: `NodeInputManager` - """ - return NodeInputManager(self) - def store_all(self, with_transaction=True, use_cache=None): """Store the node, together with all input links. diff --git a/aiida/orm/nodes/process/calculation/calculation.py b/aiida/orm/nodes/process/calculation/calculation.py index ff9e7cd2eb..1a708a3034 100644 --- a/aiida/orm/nodes/process/calculation/calculation.py +++ b/aiida/orm/nodes/process/calculation/calculation.py @@ -2,6 +2,9 @@ """Module with `Node` sub class for calculation processes.""" from __future__ import absolute_import +from aiida.common.links import LinkType +from aiida.orm.utils.managers import NodeLinksManager + from ..process import ProcessNode __all__ = ('CalculationNode',) @@ -16,3 +19,27 @@ class CalculationNode(ProcessNode): # Calculation nodes are storable _storable = True _unstorable_message = 'storing for this node has been disabled' + + @property + def inputs(self): + """Return an instance of `NodeLinksManager` to manage incoming INPUT_CALC links + + The returned Manager allows you to easily explore the nodes connected to this node + via an incoming INPUT_CALC link. + The incoming nodes are reachable by their link labels which are attributes of the manager. + + :return: `NodeLinksManager` + """ + return NodeLinksManager(node=self, link_type=LinkType.INPUT_CALC, incoming=True) + + @property + def outputs(self): + """Return an instance of `NodeLinksManager` to manage outgoing CREATE links + + The returned Manager allows you to easily explore the nodes connected to this node + via an outgoing CREATE link. + The outgoing nodes are reachable by their link labels which are attributes of the manager. + + :return: `NodeLinksManager` + """ + return NodeLinksManager(node=self, link_type=LinkType.CREATE, incoming=False) diff --git a/aiida/orm/nodes/process/process.py b/aiida/orm/nodes/process/process.py index fcbb1fb4c0..0a8ba512cf 100644 --- a/aiida/orm/nodes/process/process.py +++ b/aiida/orm/nodes/process/process.py @@ -406,15 +406,15 @@ def called_descendants(self): return descendants @property - def called_by(self): + def caller(self): """ Return the process node that called this process node, or None if it does not have a caller :returns: process node that called this process node instance or None """ - called_by = self.get_incoming(link_type=(LinkType.CALL_CALC, LinkType.CALL_WORK)) - if called_by: - return called_by.first() + caller = self.get_incoming(link_type=(LinkType.CALL_CALC, LinkType.CALL_WORK)) + if caller: + return caller.first().node return None diff --git a/aiida/orm/nodes/process/workflow/workflow.py b/aiida/orm/nodes/process/workflow/workflow.py index cd66cf4c81..8d2746f5e3 100644 --- a/aiida/orm/nodes/process/workflow/workflow.py +++ b/aiida/orm/nodes/process/workflow/workflow.py @@ -2,6 +2,9 @@ """Module with `Node` sub class for workflow processes.""" from __future__ import absolute_import +from aiida.common.links import LinkType +from aiida.orm.utils.managers import NodeLinksManager + from ..process import ProcessNode __all__ = ('WorkflowNode',) @@ -15,3 +18,27 @@ class WorkflowNode(ProcessNode): # Workflow nodes are storable _storable = True _unstorable_message = 'storing for this node has been disabled' + + @property + def inputs(self): + """Return an instance of `NodeLinksManager` to manage incoming INPUT_WORK links + + The returned Manager allows you to easily explore the nodes connected to this node + via an incoming INPUT_WORK link. + The incoming nodes are reachable by their link labels which are attributes of the manager. + + :return: `NodeLinksManager` + """ + return NodeLinksManager(node=self, link_type=LinkType.INPUT_WORK, incoming=True) + + @property + def outputs(self): + """Return an instance of `NodeLinksManager` to manage outgoing RETURN links + + The returned Manager allows you to easily explore the nodes connected to this node + via an outgoing RETURN link. + The outgoing nodes are reachable by their link labels which are attributes of the manager. + + :return: `NodeLinksManager` + """ + return NodeLinksManager(node=self, link_type=LinkType.RETURN, incoming=False) diff --git a/aiida/orm/utils/managers.py b/aiida/orm/utils/managers.py index 11ae637dff..90e39b0851 100644 --- a/aiida/orm/utils/managers.py +++ b/aiida/orm/utils/managers.py @@ -10,83 +10,70 @@ """ Contain utility classes for "managers", i.e., classes that act allow to access members of other classes via TAB-completable attributes -(e.g. the class underlying `node.inp` to allow to do `node.inp.label`). +(e.g. the class underlying `calculation.inputs` to allow to do `calculation.inputs.