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

Add a new screen for logging #123

Merged
merged 11 commits into from
Nov 17, 2022
34 changes: 28 additions & 6 deletions ironflow/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,14 @@
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

if TYPE_CHECKING:
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):
"""
Expand All @@ -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.
Expand All @@ -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 = []
Expand Down Expand Up @@ -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.
Expand All @@ -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:
Expand Down
89 changes: 89 additions & 0 deletions ironflow/gui/log.py
Original file line number Diff line number Diff line change
@@ -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()
10 changes: 9 additions & 1 deletion ironflow/model/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down