Skip to content

Commit

Permalink
Add tab-navigation methods to (some) nodes
Browse files Browse the repository at this point in the history
This fixes #2230. In particular, now:

- CalculationNode have a `.inputs.<LABEL>` property that returns
  a manager to navigate incoming INPUT_CALC nodes
- CalculationNode have a `.outputs.<LABEL>` property that returns
  a manager to navigate outgoing CREATE nodes
- WorkflowNode have a `.inputs.<LABEL>` property that returns
  a manager to navigate incoming INPUT_WORK nodes
- WorkflowNode have a `.outputs.<LABEL>` property that returns
  a manager to navigate outgoing RETURN nodes
- Node *does not have anymore* `.inp` and `.out` as these were
  referring to incoming and outgoing and were ambiguous

Moreover, `Data.created_by` and `ProcessNode.called_by` methods were
already implemented and have been renamed to `Data.creator` and
`ProcessNode.caller`.

Also tests added for all these methods, including the already existing
ones.
  • Loading branch information
giovannipizzi committed Mar 5, 2019
1 parent 174f5b1 commit a93c5f6
Show file tree
Hide file tree
Showing 21 changed files with 373 additions and 244 deletions.
2 changes: 1 addition & 1 deletion .ci/polish/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions .ci/polish/lib/template/workchain.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .ci/test_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion .ci/workchains.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
6 changes: 3 additions & 3 deletions aiida/backends/tests/engine/test_calcfunctions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
16 changes: 8 additions & 8 deletions aiida/backends/tests/engine/test_work_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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):

Expand Down Expand Up @@ -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):

Expand Down Expand Up @@ -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)

Expand Down
6 changes: 3 additions & 3 deletions aiida/backends/tests/engine/test_workfunctions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
76 changes: 76 additions & 0 deletions aiida/backends/tests/orm/node/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions aiida/backends/tests/test_parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions aiida/cmdline/commands/cmd_calcjob.py
Original file line number Diff line number Diff line change
Expand Up @@ -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?")

Expand Down Expand Up @@ -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?")

Expand Down
4 changes: 2 additions & 2 deletions aiida/orm/nodes/data/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 0 additions & 23 deletions aiida/orm/nodes/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
27 changes: 27 additions & 0 deletions aiida/orm/nodes/process/calculation/calculation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',)
Expand All @@ -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)
8 changes: 4 additions & 4 deletions aiida/orm/nodes/process/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
27 changes: 27 additions & 0 deletions aiida/orm/nodes/process/workflow/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',)
Expand All @@ -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)
Loading

0 comments on commit a93c5f6

Please sign in to comment.