diff --git a/ironflow/gui/gui.py b/ironflow/gui/gui.py index 2ea1dd43..67bd3fdb 100644 --- a/ironflow/gui/gui.py +++ b/ironflow/gui/gui.py @@ -20,6 +20,7 @@ FlowBox, ) from ironflow.gui.canvas_widgets import FlowCanvas +from ironflow.gui.log import LogScreen from ironflow.model.model import HasSession from ironflow.utils import display_string @@ -27,8 +28,6 @@ from ironflow.model.node import Node from ironflow.gui.canvas_widgets.nodes import NodeWidget -debug_view = widgets.Output(layout={"border": "1px solid black"}) - class GUI(HasSession): """ @@ -44,6 +43,8 @@ def __init__( session_title: str, extra_nodes_packages: Optional[list] = None, script_title: Optional[str] = None, + enable_ryven_log: bool = True, + log_to_display: bool = True, ): """ Create a new gui instance. @@ -57,9 +58,21 @@ def __init__( `*_Node` will be registered. (Default is None, don't register any extra nodes.) script_title (str|None): Title for an initial script. (Default is None, which generates "script_0" if a new script is needed on initialization, i.e. when existing session data cannot be read.) + enable_ryven_log (bool): Activate Ryven's logging system to catch Ryven actions and node errors. (Default + is True.) + log_to_display (bool): Re-route stdout (and node error's captured by the Ryven logger, if activated) to a + separate output widget. (Default is True.) """ + self.log_screen = LogScreen( + gui=self, enable_ryven_log=enable_ryven_log, log_to_display=log_to_display + ) + # Log screen needs to be instantiated before the rest of the init so we know whether to look at the ryven log + # as we boot + super().__init__( - session_title=session_title, extra_nodes_packages=extra_nodes_packages + session_title=session_title, + extra_nodes_packages=extra_nodes_packages, + enable_ryven_log=enable_ryven_log, ) self.flow_canvases = [] @@ -181,7 +194,12 @@ def redraw_active_flow_canvas(self): def print(self, msg: str): self.text_out.print(msg) - @debug_view.capture(clear_output=True) + def log_to_display(self): + self.log_screen.log_to_display() + + def log_to_stdout(self): + self.log_screen.log_to_stdout() + def draw(self) -> widgets.VBox: """ Build the gui. @@ -207,17 +225,21 @@ def draw(self) -> widgets.VBox: self.toolbar.buttons.zoom_out.on_click(self._click_zoom_out) self.flow_box.script_tabs.observe(self._change_script_tabs) - return widgets.VBox( + flow_screen = widgets.VBox( [ self.toolbar.box, self.input.box, self.flow_box.box, self.text_out.box, widgets.HBox([self.node_controller.box, self.node_presenter.box]), - debug_view, ] ) + window = widgets.Tab([flow_screen, self.log_screen.box]) + window.set_title(0, "Workflow") + window.set_title(1, "Log") + return window + # Type hinting for unused `change` argument in callbacks taken from ipywidgets docs: # https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Events.html#Traitlet-events def _change_alg_mode_dropdown(self, change: dict) -> None: diff --git a/ironflow/gui/log.py b/ironflow/gui/log.py new file mode 100644 index 00000000..b2e54721 --- /dev/null +++ b/ironflow/gui/log.py @@ -0,0 +1,89 @@ +# coding: utf-8 +# Copyright (c) Max-Planck-Institut für Eisenforschung GmbH - Computational Materials Design (CM) Department +# Distributed under the terms of "New BSD License", see the LICENSE file. +""" +Control the underlying Ryven logging system, and route logs to a widget. +""" + +from __future__ import annotations + +import sys +from io import TextIOBase + +import ipywidgets as widgets +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ironflow.gui.gui import GUI + + +class StdOutPut(TextIOBase): + """Helper class that can be assigned to stdout and/or stderr, passing string to a widget""" + + def __init__(self): + self.output = widgets.Output() + + def write(self, s): + self.output.append_stdout(s) + + +class LogScreen: + """ + A class that can redirect stdout and stderr to a widget, and gives controls for both this and toggling the + Ryven logger. + """ + + def __init__(self, gui: GUI, enable_ryven_log: bool, log_to_display: bool): + self._gui = gui + self._stdoutput = StdOutPut() + self._standard_stdout = sys.stdout + self._standard_stderr = sys.stderr + + if log_to_display: + self.log_to_display() + + self.ryven_log_button = widgets.Checkbox( + value=enable_ryven_log, description="Use Ryven's InfoMsgs system" + ) + self.display_log_button = widgets.Checkbox( + value=log_to_display, description="Route stdout to ironflow" + ) + + self.ryven_log_button.observe(self._toggle_ryven_log) + self.display_log_button.observe(self._toggle_display_log) + + @property + def box(self): + return widgets.VBox( + [ + widgets.HBox([self.display_log_button, self.ryven_log_button]), + self.output, + ], + layout=widgets.Layout(height="470px"), + ) + + @property + def output(self): + return self._stdoutput.output + + def log_to_display(self): + sys.stdout = self._stdoutput + sys.stderr = self._stdoutput + + def log_to_stdout(self): + sys.stdout = self._standard_stdout + sys.stderr = self._standard_stderr + + def _toggle_ryven_log(self, change: dict): + if change["name"] == "value": + if change["new"]: + self._gui.session.info_messenger().enable() + else: + self._gui.session.info_messenger().disable() + + def _toggle_display_log(self, change: dict): + if change["name"] == "value": + if change["new"]: + self.log_to_display() + else: + self.log_to_stdout() diff --git a/ironflow/model/model.py b/ironflow/model/model.py index 7aa94a65..ee12419c 100644 --- a/ironflow/model/model.py +++ b/ironflow/model/model.py @@ -24,11 +24,19 @@ class HasSession(ABC): """Mixin for an object which has a Ryven session as the underlying model""" - def __init__(self, session_title: str, extra_nodes_packages: Optional[list] = None): + def __init__( + self, + session_title: str, + extra_nodes_packages: Optional[list] = None, + enable_ryven_log: bool = True, + ): self._session = Session() self.session_title = session_title self._active_script_index = 0 + if enable_ryven_log: + self.session.info_messenger().enable() + self.nodes_dictionary = {} from ironflow.nodes import built_in from ironflow.nodes.pyiron import atomistics_nodes