Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generic HasIO classes to specify data output panel types #551

Merged
merged 8 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions pyiron_workflow/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,17 @@ def fetch(self):
c.fetch()


class Outputs(OutputsIO, DataIO):
OutputDataType = TypeVar("OutputDataType", bound=OutputData)


class GenericOutputs(OutputsIO, DataIO, Generic[OutputDataType], ABC):
@property
@abstractmethod
def _channel_class(self) -> type[OutputDataType]:
pass


class Outputs(GenericOutputs[OutputData]):
@property
def _channel_class(self) -> type[OutputData]:
return OutputData
Expand Down Expand Up @@ -294,7 +304,10 @@ def __str__(self):
return f"{str(self.input)}\n{str(self.output)}"


class HasIO(HasStateDisplay, HasLabel, HasRun, ABC):
OutputsType = TypeVar("OutputsType", bound=GenericOutputs)


class HasIO(HasStateDisplay, HasLabel, HasRun, Generic[OutputsType], ABC):
"""
A mixin for classes that provide data and signal IO.

Expand Down Expand Up @@ -329,7 +342,7 @@ def data_input_locked(self) -> bool:

@property
@abstractmethod
def outputs(self) -> Outputs:
def outputs(self) -> OutputsType:
pass

@property
Expand Down
16 changes: 12 additions & 4 deletions pyiron_workflow/mixin/has_interface_mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Generic, TypeVar

if TYPE_CHECKING:
from pyiron_workflow.channels import Channel
Expand Down Expand Up @@ -69,9 +69,7 @@ def full_label(self) -> str:

class HasChannel(ABC):
"""
A mix-in class for use with the :class:`Channel` class.
A :class:`Channel` is able to (attempt to) connect to any child instance of :class:`HasConnection`
by looking at its :attr:`connection` attribute.
A mix-in class for use with the :class:`Channel` class and its children.

This is useful for letting channels attempt to connect to non-channel objects
directly by pointing them to some channel that object holds.
Expand All @@ -83,6 +81,16 @@ def channel(self) -> Channel:
pass


ChannelType = TypeVar("ChannelType", bound="Channel")


class HasGenericChannel(HasChannel, Generic[ChannelType], ABC):
@property
@abstractmethod
def channel(self) -> ChannelType:
pass


class HasRun(ABC):
"""
A mixin to guarantee that the :meth:`run` method exists.
Expand Down
14 changes: 3 additions & 11 deletions pyiron_workflow/mixin/injection.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@

from __future__ import annotations

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any

from pyiron_workflow.channels import NOT_DATA, OutputData
from pyiron_workflow.io import HasIO, Outputs
from pyiron_workflow.io import GenericOutputs
from pyiron_workflow.mixin.has_interface_mixins import HasChannel

if TYPE_CHECKING:
Expand Down Expand Up @@ -275,14 +274,7 @@ def __round__(self):
return self._node_injection(Round)


class OutputsWithInjection(Outputs):
class OutputsWithInjection(GenericOutputs[OutputDataWithInjection]):
@property
def _channel_class(self) -> type(OutputDataWithInjection):
def _channel_class(self) -> type[OutputDataWithInjection]:
return OutputDataWithInjection


class HasIOWithInjection(HasIO, ABC):
@property
@abstractmethod
def outputs(self) -> OutputsWithInjection:
pass
7 changes: 5 additions & 2 deletions pyiron_workflow/mixin/single_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

from abc import ABC, abstractmethod

from pyiron_workflow.mixin.has_interface_mixins import HasChannel, HasLabel
from pyiron_workflow.io import HasIO
from pyiron_workflow.mixin.has_interface_mixins import HasGenericChannel
from pyiron_workflow.mixin.injection import (
OutputDataWithInjection,
OutputsWithInjection,
Expand All @@ -18,7 +19,9 @@ class AmbiguousOutputError(ValueError):
"""Raised when searching for exactly one output, but multiple are found."""


class ExploitsSingleOutput(HasLabel, HasChannel, ABC):
class ExploitsSingleOutput(
HasIO[OutputsWithInjection], HasGenericChannel[OutputDataWithInjection], ABC
):
@property
@abstractmethod
def outputs(self) -> OutputsWithInjection:
Expand Down
7 changes: 3 additions & 4 deletions pyiron_workflow/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

from pyiron_workflow.draw import Node as GraphvizNode
from pyiron_workflow.logging import logger
from pyiron_workflow.mixin.injection import HasIOWithInjection
from pyiron_workflow.mixin.run import ReadinessError, Runnable
from pyiron_workflow.mixin.semantics import Semantic
from pyiron_workflow.mixin.single_output import ExploitsSingleOutput
Expand All @@ -40,7 +39,6 @@


class Node(
HasIOWithInjection,
Semantic["Composite"],
Runnable,
ExploitsSingleOutput,
Expand Down Expand Up @@ -179,8 +177,9 @@ class Node(
inputs (pyiron_workflow.io.Inputs): **Abstract.** Children must define
a property returning an :class:`Inputs` object.
label (str): A name for the node.
outputs (pyiron_workflow.io.Outputs): **Abstract.** Children must define
a property returning an :class:`Outputs` object.
outputs (pyiron_workflow.mixin.injection.OutputsWithInjection): **Abstract.**
Children must define a property returning an :class:`OutputsWithInjection`
object.
parent (pyiron_workflow.composite.Composite | None): The parent object
owning this, if any.
ready (bool): Whether the inputs are all ready and the node is neither
Expand Down
5 changes: 3 additions & 2 deletions pyiron_workflow/nodes/macro.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@

from pyiron_snippets.factory import classfactory

from pyiron_workflow.io import Inputs, Outputs
from pyiron_workflow.io import Inputs
from pyiron_workflow.mixin.has_interface_mixins import HasChannel
from pyiron_workflow.mixin.injection import OutputsWithInjection
from pyiron_workflow.mixin.preview import ScrapesIO
from pyiron_workflow.nodes.composite import Composite
from pyiron_workflow.nodes.multiple_distpatch import dispatch_output_labels
Expand Down Expand Up @@ -342,7 +343,7 @@ def inputs(self) -> Inputs:
return self._inputs

@property
def outputs(self) -> Outputs:
def outputs(self) -> OutputsWithInjection:
return self._outputs

def _parse_remotely_executed_self(self, other_self):
Expand Down
11 changes: 6 additions & 5 deletions pyiron_workflow/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

from bidict import bidict

from pyiron_workflow.io import Inputs, Outputs
from pyiron_workflow.io import Inputs
from pyiron_workflow.mixin.injection import OutputsWithInjection
from pyiron_workflow.nodes.composite import Composite

if TYPE_CHECKING:
Expand Down Expand Up @@ -294,7 +295,7 @@ def _build_inputs(self):
return self._build_io("inputs", self.inputs_map)

@property
def outputs(self) -> Outputs:
def outputs(self) -> OutputsWithInjection:
return self._build_outputs()

def _build_outputs(self):
Expand All @@ -304,7 +305,7 @@ def _build_io(
self,
i_or_o: Literal["inputs", "outputs"],
key_map: dict[str, str | None] | None,
) -> Inputs | Outputs:
) -> Inputs | OutputsWithInjection:
"""
Build an IO panel for exposing child node IO to the outside world at the level
of the composite node's IO.
Expand All @@ -320,10 +321,10 @@ def _build_io(
(which normally would be exposed) by providing a string-None map.

Returns:
(Inputs|Outputs): The populated panel.
(Inputs|OutputsWithInjection): The populated panel.
"""
key_map = {} if key_map is None else key_map
io = Inputs() if i_or_o == "inputs" else Outputs()
io = Inputs() if i_or_o == "inputs" else OutputsWithInjection()
for node in self.children.values():
panel = getattr(node, i_or_o)
for channel in panel:
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
)


class Dummy(HasIO):
class Dummy(HasIO[Outputs]):
def __init__(self, label: str | None = "has_io"):
super().__init__()
self._label = label
Expand Down
Loading