Skip to content

Commit

Permalink
[minor] Replace HasToDict mixin with HasStateDisplay mixin (#435)
Browse files Browse the repository at this point in the history
* [minor] Replace `HasToDict` mixin with `HasStateDisplay` mixin

Functionality is similar -- the point is to get a nice JSON representation for child classes. But the public interface is different (hence minor flag), and in particular we don't use `to_dict` which might interfere with other pyiron activities. There's also some changes to exactly the display looks like, but since it's just a nice/useful thing for humans to look at, this is non-critical and can be improved with patches as needed.

* Remove unused import
  • Loading branch information
liamhuber authored Aug 22, 2024
1 parent 07734bc commit 89f71e2
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 80 deletions.
36 changes: 14 additions & 22 deletions pyiron_workflow/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@

from pyiron_snippets.singleton import Singleton

from pyiron_workflow.mixin.has_interface_mixins import HasChannel, HasLabel, UsesState
from pyiron_workflow.mixin.has_to_dict import HasToDict
from pyiron_workflow.mixin.has_interface_mixins import HasChannel, HasLabel
from pyiron_workflow.mixin.has_to_dict import HasStateDisplay
from pyiron_workflow.type_hinting import (
valid_value,
type_hint_is_as_or_more_specific_than,
Expand All @@ -29,7 +29,7 @@ class ChannelConnectionError(Exception):
pass


class Channel(UsesState, HasChannel, HasLabel, HasToDict, ABC):
class Channel(HasChannel, HasLabel, HasStateDisplay, ABC):
"""
Channels facilitate the flow of information (data or control signals) into and
out of :class:`HasIO` objects (namely nodes).
Expand Down Expand Up @@ -220,13 +220,6 @@ def copy_connections(self, other: Channel) -> None:
self.disconnect(*new_connections)
raise e

def to_dict(self) -> dict:
return {
"label": self.label,
"connected": self.connected,
"connections": [f"{c.owner.label}.{c.label}" for c in self.connections],
}

def __getstate__(self):
state = super().__getstate__()
state["connections"] = []
Expand All @@ -235,6 +228,12 @@ def __getstate__(self):
# bloat the data being sent cross-process if the owner is shipped off
return state

def display_state(self, state=None, ignore_private=True):
state = dict(self.__getstate__()) if state is None else state
state["owner"] = state["owner"].full_label # JSON can't handle recursion
state["connections"] = [c.full_label for c in self.connections]
return super().display_state(state=state, ignore_private=ignore_private)


class NotData(metaclass=Singleton):
"""
Expand Down Expand Up @@ -468,13 +467,6 @@ def _figure_out_who_is_who(self, other: DataChannel) -> (OutputData, InputData):
def __str__(self):
return str(self.value)

def to_dict(self) -> dict:
d = super().to_dict()
d["value"] = repr(self.value)
d["ready"] = self.ready
d["type_hint"] = str(self.type_hint)
return d

def activate_strict_hints(self) -> None:
self.strict_hints = True

Expand All @@ -488,6 +480,11 @@ def __getstate__(self):
# owning macro's responsibility
return state

def display_state(self, state=None, ignore_private=True):
state = dict(self.__getstate__()) if state is None else state
self._make_entry_public(state, "_value", "value")
return super().display_state(state=state, ignore_private=ignore_private)


class InputData(DataChannel):
@property
Expand Down Expand Up @@ -623,11 +620,6 @@ def __call__(self, other: typing.Optional[OutputSignal] = None) -> None:
def __str__(self):
return f"{self.label} runs {self.callback.__name__}"

def to_dict(self) -> dict:
d = super().to_dict()
d["callback"] = self.callback.__name__
return d

def _connect_output_signal(self, signal: OutputSignal):
self.connect(signal)

Expand Down
35 changes: 11 additions & 24 deletions pyiron_workflow/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@
HasChannel,
HasLabel,
HasRun,
UsesState,
)
from pyiron_workflow.mixin.has_to_dict import HasToDict
from pyiron_workflow.mixin.has_to_dict import HasStateDisplay


class IO(HasToDict, ABC):
class IO(HasStateDisplay, ABC):
"""
IO is a convenience layer for holding and accessing multiple input/output channels.
It allows key and dot-based access to the underlying channels.
Expand Down Expand Up @@ -156,14 +155,6 @@ def __dir__(self):
def __str__(self):
return f"{self.__class__.__name__} {self.labels}"

def to_dict(self):
return {
"label": self.__class__.__name__,
"connected": self.connected,
"fully_connected": self.fully_connected,
"channels": {l: c.to_dict() for l, c in self.channel_dict.items()},
}

def __getstate__(self):
# Compatibility with python <3.11
return dict(self.__dict__)
Expand All @@ -173,6 +164,13 @@ def __setstate__(self, state):
# __setstate__ the same way we need it in __init__
self.__dict__["channel_dict"] = state["channel_dict"]

def display_state(self, state=None, ignore_private=True):
state = dict(self.__getstate__()) if state is None else state
for k, v in state["channel_dict"].items():
state[k] = v
del state["channel_dict"]
return super().display_state(state=state, ignore_private=ignore_private)


class DataIO(IO, ABC):
def _assign_a_non_channel_value(self, channel: DataChannel, value) -> None:
Expand All @@ -189,11 +187,6 @@ def to_list(self):
def ready(self):
return all([c.ready for c in self])

def to_dict(self):
d = super().to_dict()
d["ready"] = self.ready
return d

def activate_strict_hints(self):
[c.activate_strict_hints() for c in self]

Expand Down Expand Up @@ -251,7 +244,7 @@ def _channel_class(self) -> type(OutputSignal):
return OutputSignal


class Signals:
class Signals(HasStateDisplay):
"""
A meta-container for input and output signal IO containers.
Expand Down Expand Up @@ -285,17 +278,11 @@ def connected(self):
def fully_connected(self):
return self.input.fully_connected and self.output.fully_connected

def to_dict(self):
return {
"input": self.input.to_dict(),
"output": self.output.to_dict(),
}

def __str__(self):
return f"{str(self.input)}\n{str(self.output)}"


class HasIO(UsesState, HasLabel, HasRun, ABC):
class HasIO(HasStateDisplay, HasLabel, HasRun, ABC):
"""
A mixin for classes that provide data and signal IO.
Expand Down
81 changes: 70 additions & 11 deletions pyiron_workflow/mixin/has_to_dict.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,76 @@
from abc import ABC, abstractmethod
from abc import ABC
from json import dumps

from pyiron_workflow.mixin.has_interface_mixins import UsesState

class HasToDict(ABC):
@abstractmethod
def to_dict(self):
pass

def _repr_json_(self):
return self.to_dict()
class HasStateDisplay(UsesState, ABC):
"""
A mixin that leverages :meth:`__getstate__` to automatically build a half-decent
JSON-compatible representation dictionary.
Child classes can over-ride :meth:`display_state` to add or remove elements from
the display dictionary, e.g. to (optionally) expose state elements that would
otherwise be private or to show properties that are computed and not stored in
state, or (mandatory -- JSON demands it) remove recursion from the state.
Provides a :meth:`_repr_json_` method leveraging this beautified state dictionary
to give a standard JSON representation in Jupyter notebooks.
"""

def display_state(
self, state: dict | None = None, ignore_private: bool = True
) -> dict:
"""
A dictionary of JSON-compatible objects based on the object state (plus
whatever modifications to the state the class designer has chosen to make).
Anything that fails to dump to JSON gets cast as a string and then dumped.
Args:
state (dict|None): The starting state. Default is None which uses
`__getstate__`, but available in case child classes want to first
sanitize the state values.
ignore_private (bool): Whether to ignore or include any state element
whose key starts with `'_'`. Default is True, only show public data.
def info(self):
print(dumps(self.to_dict(), indent=2))
Returns:
dict:
"""
display = dict(self.__getstate__()) if state is None else state
to_del = []
for k, v in display.items():
if ignore_private and k.startswith("_"):
to_del.append(k)

if isinstance(v, HasStateDisplay):
display[k] = v.display_state(ignore_private=ignore_private)
else:
try:
display[k] = dumps(v)
except TypeError:
display[k] = dumps(str(v))

for k in to_del:
del display[k]

return display

def _repr_json_(self):
return self.display_state()

def __str__(self):
return str(self.to_dict())
@staticmethod
def _make_entry_public(state: dict, private_key: str, public_key: str):
if private_key not in state.keys():
raise ValueError(
f"Can't make {private_key} public, it was not found among "
f"{list(state.keys())}"
)
if public_key in state.keys():
raise ValueError(
f"Can't make {private_key} public, {public_key} is already a key in"
f" the dict!"
)
state[public_key] = state[private_key]
del state[private_key]
return state
12 changes: 9 additions & 3 deletions pyiron_workflow/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

from pyiron_workflow.draw import Node as GraphvizNode
from pyiron_workflow.logging import logger
from pyiron_workflow.mixin.has_to_dict import HasToDict
from pyiron_workflow.mixin.injection import HasIOWithInjection
from pyiron_workflow.mixin.run import Runnable, ReadinessError
from pyiron_workflow.mixin.semantics import Semantic
Expand All @@ -38,10 +37,9 @@


class Node(
HasToDict,
HasIOWithInjection,
Semantic,
Runnable,
HasIOWithInjection,
ExploitsSingleOutput,
ABC,
):
Expand Down Expand Up @@ -937,3 +935,11 @@ def report_import_readiness(self, tabs=0, report_so_far=""):
report_so_far + f"{newline}{tabspace}{self.label}: "
f"{'ok' if self.import_ready else 'NOT IMPORTABLE'}"
)

def display_state(self, state=None, ignore_private=True):
state = dict(self.__getstate__()) if state is None else state
if self.parent is not None:
state["parent"] = self.parent.full_label
if len(state["_user_data"]) > 0:
self._make_entry_public(state, "_user_data", "user_data")
return super().display_state(state=state, ignore_private=ignore_private)
6 changes: 0 additions & 6 deletions pyiron_workflow/nodes/composite.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,6 @@ def deactivate_strict_hints(self):
for node in self:
node.deactivate_strict_hints()

def to_dict(self):
return {
"label": self.label,
"nodes": {n.label: n.to_dict() for n in self.children.values()},
}

def on_run(self):
# Reset provenance and run status trackers
self.provenance_by_execution = []
Expand Down
11 changes: 0 additions & 11 deletions pyiron_workflow/nodes/function.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,17 +337,6 @@ def _outputs_to_run_return(self):
output = output[0]
return output

def to_dict(self):
return {
"label": self.label,
"ready": self.ready,
"connected": self.connected,
"fully_connected": self.fully_connected,
"inputs": self.inputs.to_dict(),
"outputs": self.outputs.to_dict(),
"signals": self.signals.to_dict(),
}

@property
def color(self) -> str:
"""For drawing the graph"""
Expand Down
7 changes: 7 additions & 0 deletions pyiron_workflow/nodes/static_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,10 @@ def _guarantee_names_are_input_channels(self, presumed_input_keys: tuple[str]):
f"{self.full_label} cannot iterate on {non_input_kwargs} because "
f"they are not among input channels {self.inputs.labels}"
)

def display_state(self, state=None, ignore_private=True):
state = dict(self.__getstate__()) if state is None else state
self._make_entry_public(state, "_inputs", "inputs")
self._make_entry_public(state, "_outputs", "outputs")
self._make_entry_public(state, "_signals", "signals")
return super().display_state(state=state, ignore_private=ignore_private)
3 changes: 0 additions & 3 deletions pyiron_workflow/nodes/transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ class Transformer(StaticNode, ABC):
into a single output or vice-versa.
"""

def to_dict(self):
pass # Vestigial abstract method


class FromManyInputs(Transformer, ABC):
_output_name: ClassVar[str] # Mandatory attribute for non-abstract subclasses
Expand Down

0 comments on commit 89f71e2

Please sign in to comment.