diff --git a/pyiron_workflow/channels.py b/pyiron_workflow/channels.py index e26016652..871ec57f1 100644 --- a/pyiron_workflow/channels.py +++ b/pyiron_workflow/channels.py @@ -13,7 +13,7 @@ import inspect from warnings import warn -from pyiron_workflow.has_interface_mixins import HasChannel, UsesState +from pyiron_workflow.has_interface_mixins import HasChannel, HasLabel, UsesState from pyiron_workflow.has_to_dict import HasToDict from pyiron_workflow.snippets.singleton import Singleton from pyiron_workflow.type_hinting import ( @@ -29,13 +29,13 @@ class ChannelConnectionError(Exception): pass -class Channel(UsesState, HasChannel, HasToDict, ABC): +class Channel(UsesState, HasChannel, HasLabel, HasToDict, ABC): """ Channels facilitate the flow of information (data or control signals) into and out of nodes. They must have an identifier (`label: str`) and belong to a parent node - (`node: pyiron_workflow.node.Node`). + (`owner: pyiron_workflow.node.Node`). Non-abstract channel classes should come in input/output pairs and specify the a necessary ancestor for instances they can connect to @@ -62,27 +62,30 @@ class Channel(UsesState, HasChannel, HasToDict, ABC): Attributes: label (str): The name of the channel. - node (pyiron_workflow.node.Node): The node to which the channel - belongs. + owner (pyiron_workflow.node.Node): The channel's owner. connections (list[Channel]): Other channels to which this channel is connected. """ def __init__( self, label: str, - node: Node, + owner: Node, ): """ Make a new channel. Args: label (str): A name for the channel. - node (pyiron_workflow.node.Node): The node to which the channel belongs. + owner (pyiron_workflow.node.Node): The channel's owner. """ - self.label: str = label - self.node: Node = node + self._label = label + self.owner: Node = owner self.connections: list[Channel] = [] + @property + def label(self) -> str: + return self._label + @abstractmethod def __str__(self): pass @@ -97,8 +100,8 @@ def connection_partner_type(self) -> type[Channel]: @property def scoped_label(self) -> str: - """A label combining the channel's usual label and its node's label""" - return f"{self.node.label}__{self.label}" + """A label combining the channel's usual label and its owner's label""" + return f"{self.owner.label}__{self.label}" def _valid_connection(self, other: Channel) -> bool: """ @@ -213,17 +216,17 @@ def to_dict(self) -> dict: return { "label": self.label, "connected": self.connected, - "connections": [f"{c.node.label}.{c.label}" for c in self.connections], + "connections": [f"{c.owner.label}.{c.label}" for c in self.connections], } def __getstate__(self): state = super().__getstate__() # To avoid cyclic storage and avoid storing complex objects, purge some # properties from the state - state["node"] = None - # It is the responsibility of the owning node to restore the node property + state["owner"] = None + # It is the responsibility of the owner to restore the owner property state["connections"] = [] - # It is the responsibility of the owning node's parent to store and restore + # It is the responsibility of the owner's parent to store and restore # connections (if any) return state @@ -274,7 +277,7 @@ class DataChannel(Channel, ABC): Channels with such partners pass any data updates they receive directly to this partner (via the :attr:`value` setter). (This is helpful for passing data between scopes, where we want input at one scope - to be passed to the input of nodes at a deeper scope, i.e. macro input passing to + to be passed to the input of owners at a deeper scope, i.e. macro input passing to child node input, or vice versa for output.) All these type hint tests can be disabled on the input/receiving channel @@ -325,30 +328,31 @@ class DataChannel(Channel, ABC): yet included in our test suite and behaviour is not guaranteed. Attributes: - value: The actual data value held by the node. + value: The actual data value held by the channel. label (str): The label for the channel. - node (pyiron_workflow.node.Node): The node to which this channel belongs. + owner (pyiron_workflow.node.Node): The channel's owner. default (typing.Any|None): The default value to initialize to. (Default is the singleton `NOT_DATA`.) type_hint (typing.Any|None): A type hint for values. (Default is None.) strict_hints (bool): Whether to check new values, connections, and partners - when this node is a value receiver. This can potentially be expensive, so + when this channel is a value receiver. This can potentially be expensive, so consider deactivating strict hints everywhere for production runs. (Default is True, raise exceptions when type hints get violated.) - value_receiver (pyiron_workflow.node.Node|None): Another node of the same class - whose value will always get updated when this node's value gets updated. + value_receiver (pyiron_workflow.channel.DataChannel|None): Another channel of + the same class whose value will always get updated when this channel's + value gets updated. """ def __init__( self, label: str, - node: Node, + owner: Node, default: typing.Optional[typing.Any] = NOT_DATA, type_hint: typing.Optional[typing.Any] = None, strict_hints: bool = True, value_receiver: typing.Optional[InputData] = None, ): - super().__init__(label=label, node=node) + super().__init__(label=label, owner=owner) self._value = NOT_DATA self._value_receiver = None self.type_hint = type_hint @@ -386,7 +390,7 @@ def value_receiver(self) -> InputData | OutputData | None: Another data channel of the same type to whom new values are always pushed (without type checking of any sort, not even when forming the couple!) - Useful for macros, so that the IO of owned nodes and IO at the macro level can + Useful for macros, so that the IO of children and IO at the macro level can be kept synchronized. """ return self._value_receiver @@ -520,7 +524,7 @@ def fetch(self) -> None: 0th connection; build graphs accordingly. Raises: - RuntimeError: If the parent node is :attr:`running`. + RuntimeError: If the owner is :attr:`running`. """ for out in self.connections: if out.value is not NOT_DATA: @@ -533,9 +537,9 @@ def value(self): @value.setter def value(self, new_value): - if self.node.running: + if self.owner.running: raise RuntimeError( - f"Parent node {self.node.label} of {self.label} is running, so value " + f"Owner {self.owner.label} of {self.label} is running, so value " f"cannot be updated." ) self._type_check_new_value(new_value) @@ -582,12 +586,12 @@ def _node_injection(self, injection_class, *args, inject_self=True): label = self._get_injection_label(injection_class, *args) try: # First check if the node already exists - return self.node.parent.children[label] + return self.owner.parent.children[label] except (AttributeError, KeyError): # Fall back on creating a new node in case parent is None or node nexists node_args = (self, *args) if inject_self else args return injection_class( - *node_args, parent=self.node.parent, label=label, run_after_init=True + *node_args, parent=self.owner.parent, label=label, run_after_init=True ) # We don't wrap __all__ the operators, because you might really want the string or @@ -767,7 +771,7 @@ class SignalChannel(Channel, ABC): """ Signal channels give the option control execution flow by triggering callback functions when the channel is called. - Callbacks must be methods on the parent node that require no positional arguments. + Callbacks must be methods on the owner that require no positional arguments. Inputs optionally accept an output signal on call, which output signals always send when they call their input connections. @@ -795,7 +799,7 @@ def connection_partner_type(self): def __init__( self, label: str, - node: Node, + owner: Node, callback: callable, ): """ @@ -803,25 +807,24 @@ def __init__( Args: label (str): A name for the channel. - node (pyiron_workflow.node.Node): The node to which the - channel belongs. + owner (pyiron_workflow.node.Node): The channel's owner. callback (callable): An argument-free callback to invoke when calling this - object. + object. Must be a method on the owner. """ - super().__init__(label=label, node=node) - if self._is_node_method(callback) and self._takes_zero_arguments(callback): + super().__init__(label=label, owner=owner) + if self._is_method_on_owner(callback) and self._takes_zero_arguments(callback): self._callback: str = callback.__name__ else: raise BadCallbackError( - f"The channel {self.label} on {self.node.label} got an unexpected " + f"The channel {self.label} on {self.owner.label} got an unexpected " f"callback: {callback}. " - f"Lives on node: {self._is_node_method(callback)}; " + f"Lives on owner: {self._is_method_on_owner(callback)}; " f"take no args: {self._takes_zero_arguments(callback)} " ) - def _is_node_method(self, callback): + def _is_method_on_owner(self, callback): try: - return callback == getattr(self.node, callback.__name__) + return callback == getattr(self.owner, callback.__name__) except AttributeError: return False @@ -840,7 +843,7 @@ def _no_positional_args(func): @property def callback(self) -> callable: - return getattr(self.node, self._callback) + return getattr(self.owner, self._callback) def __call__(self, other: typing.Optional[OutputSignal] = None) -> None: self.callback() @@ -866,10 +869,10 @@ class AccumulatingInputSignal(InputSignal): def __init__( self, label: str, - node: Node, + owner: Node, callback: callable, ): - super().__init__(label=label, node=node, callback=callback) + super().__init__(label=label, owner=owner, callback=callback) self.received_signals: set[str] = set() def __call__(self, other: OutputSignal) -> None: @@ -915,7 +918,7 @@ def __call__(self) -> None: def __str__(self): return ( f"{self.label} activates " - f"{[f'{c.node.label}.{c.label}' for c in self.connections]}" + f"{[f'{c.owner.label}.{c.label}' for c in self.connections]}" ) def __rshift__(self, other: InputSignal | Node): diff --git a/pyiron_workflow/composite.py b/pyiron_workflow/composite.py index 953c7df6a..cf736002b 100644 --- a/pyiron_workflow/composite.py +++ b/pyiron_workflow/composite.py @@ -537,7 +537,7 @@ def _get_connections_as_strings( the name is protected. """ return [ - ((inp.node.label, inp.label), (out.node.label, out.label)) + ((inp.owner.label, inp.label), (out.owner.label, out.label)) for child in self for inp in panel_getter(child) for out in inp.connections diff --git a/pyiron_workflow/function.py b/pyiron_workflow/function.py index 6d7145a8d..9319b5cd5 100644 --- a/pyiron_workflow/function.py +++ b/pyiron_workflow/function.py @@ -458,7 +458,7 @@ def _build_input_channels(self): channels.append( InputData( label=label, - node=self, + owner=self, default=default, type_hint=type_hint, ) @@ -497,7 +497,7 @@ def _build_output_channels(self, *return_labels: str): channels.append( OutputData( label=label, - node=self, + owner=self, type_hint=hint, ) ) diff --git a/pyiron_workflow/macro.py b/pyiron_workflow/macro.py index 562b87090..7ab20430d 100644 --- a/pyiron_workflow/macro.py +++ b/pyiron_workflow/macro.py @@ -458,7 +458,7 @@ def _get_linking_channel( """ composite_channel = child_reference_channel.__class__( label=composite_io_key, - node=self, + owner=self, default=child_reference_channel.default, type_hint=child_reference_channel.type_hint, ) @@ -565,7 +565,7 @@ def _input_value_links(self): the name is protected. """ return [ - (c.label, (c.value_receiver.node.label, c.value_receiver.label)) + (c.label, (c.value_receiver.owner.label, c.value_receiver.label)) for c in self.inputs ] @@ -579,7 +579,7 @@ def _output_value_links(self): the name is protected. """ return [ - ((c.node.label, c.label), c.value_receiver.label) + ((c.owner.label, c.label), c.value_receiver.label) for child in self for c in child.outputs if c.value_receiver is not None diff --git a/pyiron_workflow/node.py b/pyiron_workflow/node.py index 37466954d..9b28e922f 100644 --- a/pyiron_workflow/node.py +++ b/pyiron_workflow/node.py @@ -923,7 +923,7 @@ def __setstate__(self, state): # Channels don't store their own node in their state, so repopulate it for io_panel in self._owned_io_panels: for channel in io_panel: - channel.node = self + channel.owner = self @property def _owned_io_panels(self) -> list[IO]: diff --git a/pyiron_workflow/topology.py b/pyiron_workflow/topology.py index 394dabad2..65c136d9d 100644 --- a/pyiron_workflow/topology.py +++ b/pyiron_workflow/topology.py @@ -56,23 +56,23 @@ def nodes_to_data_digraph(nodes: dict[str, Node]) -> dict[str, set[str]]: locally_scoped_dependencies = [] for upstream in channel.connections: try: - upstream_node = nodes[upstream.node.label] + upstream_node = nodes[upstream.owner.label] except KeyError as e: raise KeyError( f"The {channel.label} channel of {node.label} has a connection " - f"to {upstream.label} channel of {upstream.node.label}, but " - f"{upstream.node.label} was not found among nodes. All nodes " + f"to {upstream.label} channel of {upstream.owner.label}, but " + f"{upstream.owner.label} was not found among nodes. All nodes " f"in the data flow dependency tree must be included." ) - if upstream_node is not upstream.node: + if upstream_node is not upstream.owner: raise ValueError( f"The {channel.label} channel of {node.label} has a connection " - f"to {upstream.label} channel of {upstream.node.label}, but " + f"to {upstream.label} channel of {upstream.owner.label}, but " f"that channel's node is not the same as the nodes passed " f"here. All nodes in the data flow dependency tree must be " f"included." ) - locally_scoped_dependencies.append(upstream.node.label) + locally_scoped_dependencies.append(upstream.owner.label) node_dependencies.extend(locally_scoped_dependencies) node_dependencies = set(node_dependencies) if node.label in node_dependencies: @@ -183,7 +183,7 @@ def _set_run_connections_according_to_dag(nodes: dict[str, Node]) -> list[Node]: for node in nodes.values(): upstream_connections = [con for inp in node.inputs for con in inp.connections] - upstream_nodes = set([c.node for c in upstream_connections]) + upstream_nodes = set([c.owner for c in upstream_connections]) upstream_rans = [n.signals.output.ran for n in upstream_nodes] node.signals.input.accumulate_and_run.connect(*upstream_rans) # Note: We can be super fast-and-loose here because the `nodes_to_data_digraph` call @@ -227,7 +227,7 @@ def get_nodes_in_data_tree(node: Node) -> set[Node]: nodes = set([node]) for channel in node.inputs: for connection in channel.connections: - nodes = nodes.union(get_nodes_in_data_tree(connection.node)) + nodes = nodes.union(get_nodes_in_data_tree(connection.owner)) return nodes except RecursionError: raise CircularDataFlowError( diff --git a/pyiron_workflow/workflow.py b/pyiron_workflow/workflow.py index 1ea0947a6..c0bc148bf 100644 --- a/pyiron_workflow/workflow.py +++ b/pyiron_workflow/workflow.py @@ -282,7 +282,7 @@ def _data_connections(self) -> list[tuple[tuple[str, str], tuple[str, str]]]: for inp_label, inp in node.inputs.items(): for conn in inp.connections: data_connections.append( - ((node.label, inp_label), (conn.node.label, conn.label)) + ((node.label, inp_label), (conn.owner.label, conn.label)) ) return data_connections @@ -305,7 +305,7 @@ def _signal_connections(self) -> list[tuple[tuple[str, str], tuple[str, str]]]: for inp_label, inp in node.signals.input.items(): for conn in inp.connections: signal_connections.append( - ((node.label, inp_label), (conn.node.label, conn.label)) + ((node.label, inp_label), (conn.owner.label, conn.label)) ) return signal_connections diff --git a/tests/unit/test_channels.py b/tests/unit/test_channels.py index ca3fb9506..4ace5049b 100644 --- a/tests/unit/test_channels.py +++ b/tests/unit/test_channels.py @@ -111,26 +111,26 @@ class TestDataChannels(unittest.TestCase): def setUp(self) -> None: self.ni1 = InputData( - label="numeric", node=DummyNode(), default=1, type_hint=int|float + label="numeric", owner=DummyNode(), default=1, type_hint=int | float ) self.ni2 = InputData( - label="numeric", node=DummyNode(), default=1, type_hint=int|float + label="numeric", owner=DummyNode(), default=1, type_hint=int | float ) self.no = OutputData( - label="numeric", node=DummyNode(), default=0, type_hint=int|float + label="numeric", owner=DummyNode(), default=0, type_hint=int | float ) self.no_empty = OutputData( - label="not_data", node=DummyNode(), type_hint=int|float + label="not_data", owner=DummyNode(), type_hint=int | float ) - self.si = InputData(label="list", node=DummyNode(), type_hint=list) + self.si = InputData(label="list", owner=DummyNode(), type_hint=list) self.so1 = OutputData( - label="list", node=DummyNode(), default=["foo"], type_hint=list + label="list", owner=DummyNode(), default=["foo"], type_hint=list ) def test_mutable_defaults(self): so2 = OutputData( - label="list", node=DummyNode(), default=["foo"], type_hint=list + label="list", owner=DummyNode(), default=["foo"], type_hint=list ) self.so1.default.append("bar") self.assertEqual( @@ -278,7 +278,7 @@ def test_value_receiver(self): self.ni1.value_receiver = self.si # Should work fine if the receiver is not # strictly checking hints - unhinted = InputData(label="unhinted", node=DummyNode()) + unhinted = InputData(label="unhinted", owner=DummyNode()) self.ni1.value_receiver = unhinted unhinted.value_receiver = self.ni2 # Should work fine if either lacks a hint @@ -287,13 +287,13 @@ def test_value_assignment(self): self.ni1.value = 2 # Should be fine when value matches hint self.ni1.value = NOT_DATA # Should be able to clear the data - self.ni1.node.running = True + self.ni1.owner.running = True with self.assertRaises( RuntimeError, msg="Input data should be locked while its node runs" ): self.ni1.value = 3 - self.ni1.node.running = False + self.ni1.owner.running = False with self.assertRaises( TypeError, @@ -310,7 +310,7 @@ def test_value_assignment(self): def test_ready(self): with self.subTest("Test defaults and not-data"): - without_default = InputData(label="without_default", node=DummyNode()) + without_default = InputData(label="without_default", owner=DummyNode()) self.assertIs( without_default.value, NOT_DATA, @@ -342,8 +342,8 @@ def test_if_not_data(self): class TestSignalChannels(unittest.TestCase): def setUp(self) -> None: node = DummyNode() - self.inp = InputSignal(label="inp", node=node, callback=node.update) - self.out = OutputSignal(label="out", node=DummyNode()) + self.inp = InputSignal(label="inp", owner=node, callback=node.update) + self.out = OutputSignal(label="out", owner=DummyNode()) def test_connections(self): with self.subTest("Good connection"): @@ -362,7 +362,7 @@ def test_connections(self): self.assertEqual(len(self.out.connections), 0) with self.subTest("No connections to non-SignalChannels"): - bad = InputData(label="numeric", node=DummyNode(), default=1, type_hint=int) + bad = InputData(label="numeric", owner=DummyNode(), default=1, type_hint=int) with self.assertRaises(TypeError): self.inp.connect(bad) @@ -374,13 +374,13 @@ def test_connections(self): def test_calls(self): self.out.connect(self.inp) self.out() - self.assertListEqual(self.inp.node.foo, [0, 1]) + self.assertListEqual(self.inp.owner.foo, [0, 1]) self.inp() - self.assertListEqual(self.inp.node.foo, [0, 1, 2]) + self.assertListEqual(self.inp.owner.foo, [0, 1, 2]) def test_aggregating_call(self): node = DummyNode() - agg = AccumulatingInputSignal(label="agg", node=node, callback=node.update) + agg = AccumulatingInputSignal(label="agg", owner=node, callback=node.update) with self.assertRaises( TypeError, @@ -389,7 +389,7 @@ def test_aggregating_call(self): ): agg() - out2 = OutputSignal(label="out2", node=DummyNode()) + out2 = OutputSignal(label="out2", owner=DummyNode()) agg.connect(self.out, out2) self.assertEqual( @@ -495,7 +495,7 @@ def doesnt_belong_to_node(): node.classmethod_without_args ]: with self.subTest(callback.__name__): - InputSignal(label="inp", node=node, callback=callback) + InputSignal(label="inp", owner=node, callback=callback) with self.subTest("Invalid callbacks"): for callback in [ @@ -506,7 +506,7 @@ def doesnt_belong_to_node(): ]: with self.subTest(callback.__name__): with self.assertRaises(BadCallbackError): - InputSignal(label="inp", node=node, callback=callback) + InputSignal(label="inp", owner=node, callback=callback) if __name__ == '__main__': unittest.main() diff --git a/tests/unit/test_io.py b/tests/unit/test_io.py index 8eb85e617..757eb76cf 100644 --- a/tests/unit/test_io.py +++ b/tests/unit/test_io.py @@ -21,14 +21,14 @@ class TestDataIO(unittest.TestCase): def setUp(self) -> None: node = DummyNode() self.inputs = [ - InputData(label="x", node=node, default=0., type_hint=float), - InputData(label="y", node=node, default=1., type_hint=float) + InputData(label="x", owner=node, default=0., type_hint=float), + InputData(label="y", owner=node, default=1., type_hint=float) ] outputs = [ - OutputData(label="a", node=node, type_hint=float), + OutputData(label="a", owner=node, type_hint=float), ] - self.post_facto_output = OutputData(label="b", node=node, type_hint=float) + self.post_facto_output = OutputData(label="b", owner=node, type_hint=float) self.input = Inputs(*self.inputs) self.output = Outputs(*outputs) diff --git a/tests/unit/test_macro.py b/tests/unit/test_macro.py index 3bcb2cbaf..33b8a7df8 100644 --- a/tests/unit/test_macro.py +++ b/tests/unit/test_macro.py @@ -422,7 +422,7 @@ def fail_at_zero(x): ) self.assertIs( n_not_used, - n2.signals.input.run.connections[0].node, + n2.signals.input.run.connections[0].owner, msg="Original connections should get restored on upstream failure" )