Skip to content

Commit

Permalink
🎉 Add logging & improve CLI (#231)
Browse files Browse the repository at this point in the history
🎉 Add logging & improve CLI (#231)
  • Loading branch information
MathisFederico authored Jan 30, 2022
2 parents d26f430 + 68cc3f6 commit 4255e28
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 16 deletions.
47 changes: 44 additions & 3 deletions pyflow/__main__.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,70 @@
# Pyflow an open-source tool for modular visual programing in python
# Copyright (C) 2021-2022 Bycelium <https://www.gnu.org/licenses/>
# pylint:disable=wrong-import-position
# pylint:disable=wrong-import-position, protected-access

""" Pyflow main module. """

import os
import sys

import argparse
import logging

import asyncio
from colorama import init, Fore, Style

init()
if os.name == "nt": # If on windows
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

from PyQt5.QtWidgets import QApplication

from pyflow.graphics.window import Window
from pyflow import __version__
from pyflow.logging import PyflowHandler

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))

if __name__ == "__main__":

parser = argparse.ArgumentParser()
parser.add_argument("-p", "--path", type=str, help="path to a file to open")
parser.add_argument(
"-v",
"--verbose",
type=str,
choices=logging._nameToLevel.keys(),
help="set logging level",
default="INFO",
)
args = parser.parse_args()

# Debug flag will lower logging level to DEBUG
log_level = logging._nameToLevel[args.verbose.upper()]
logging.basicConfig(
filename="pyflow.log",
level=logging.INFO,
)
pyflow_logger = logging.getLogger("pyflow")
pyflow_logger.setLevel(log_level)

stream_formater = logging.Formatter(
"%(asctime)s|%(levelname)s| %(pathname)s#%(lineno)d: > %(message)s",
datefmt="%H:%M:%S",
)
stream_handler = PyflowHandler()
stream_handler.setFormatter(stream_formater)
pyflow_logger.addHandler(stream_handler)

if log_level <= logging.DEBUG:
print(Fore.GREEN + "-" * 15 + " DEBUG MODE ON " + "-" * 15 + Style.RESET_ALL)

app = QApplication(sys.argv)
app.setStyle("Fusion")
wnd = Window()
if len(sys.argv) > 1:
wnd.createNewMdiChild(sys.argv[1])

if args.path:
wnd.createNewMdiChild(args.path)

wnd.setWindowTitle(f"Pyflow {__version__}")
wnd.show()
Expand Down
4 changes: 4 additions & 0 deletions pyflow/core/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@
from jupyter_client.manager import start_new_kernel

from pyflow.core.worker import Worker
from pyflow.logging import log_init_time, get_logger

LOGGER = get_logger(__name__)


class Kernel:

"""jupyter_client kernel used to execute code and return output."""

@log_init_time(LOGGER)
def __init__(self):
self.kernel_manager, self.client = start_new_kernel()
self.execution_queue = []
Expand Down
9 changes: 7 additions & 2 deletions pyflow/graphics/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@

from pyflow.scene import Scene
from pyflow.graphics.view import View
from pyflow.logging import log_init_time, get_logger

LOGGER = get_logger(__name__)


class Widget(QWidget):

"""Window for a graph visualisation."""
"""Widget for a graph visualisation."""

@log_init_time(LOGGER)
def __init__(self, parent=None):
super().__init__(parent)
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
Expand All @@ -35,14 +39,15 @@ def __init__(self, parent=None):
self.savepath = None

def updateTitle(self):
"""Update the window title."""
"""Update the widget title."""
if self.savepath is None:
title = "New Graph"
else:
title = os.path.basename(self.savepath)
if self.isModified():
title += "*"
self.setWindowTitle(title)
LOGGER.debug("Updated widget title to %s", title)

def isModified(self) -> bool:
"""Return True if the scene has been modified, False otherwise."""
Expand Down
24 changes: 14 additions & 10 deletions pyflow/graphics/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,17 @@
from pyflow.qss import loadStylesheets
from pyflow.qss import __file__ as QSS_INIT_PATH
from pyflow.scene.clipboard import BlocksClipboard
from pyflow.logging import log_init_time, get_logger

LOGGER = get_logger(__name__)
QSS_PATH = pathlib.Path(QSS_INIT_PATH).parent


class Window(QMainWindow):

"""Main window of the Pyflow Qt-based application."""

@log_init_time(LOGGER)
def __init__(self):
super().__init__()

Expand Down Expand Up @@ -309,7 +312,7 @@ def onFileOpen(self):
if os.path.isfile(filename):
subwnd = self.createNewMdiChild(filename)
subwnd.show()
self.statusbar.showMessage(f"Successfully loaded {filename}", 2000)
self.statusbar.showMessage(f"Loaded {filename}", 2000)

def onFileSave(self) -> bool:
"""Save file.
Expand Down Expand Up @@ -366,19 +369,18 @@ def onFileSaveAsJupyter(self) -> bool:
if filename == "":
return False
current_window.saveAsJupyter(filename)
self.statusbar.showMessage(
f"Successfully saved ipygraph as jupter notebook at {filename}",
2000,
)
success_msg = f"Saved as jupter notebook at {filename}"
self.statusbar.showMessage(success_msg, 2000)
LOGGER.info(success_msg)
return True
return False

def saveWindow(self, window: Widget):
"""Save the given window."""
window.save()
self.statusbar.showMessage(
f"Successfully saved ipygraph at {window.savepath}", 2000
)
success_msg = f"Saved ipygraph at {window.savepath}"
self.statusbar.showMessage(success_msg, 2000)
LOGGER.info(success_msg)

@staticmethod
def is_not_editing(current_window: Widget):
Expand Down Expand Up @@ -471,20 +473,22 @@ def activeMdiChild(self) -> Widget:

def readSettings(self):
"""Read the settings from the config file."""
settings = QSettings("AutopIA", "Pyflow")
settings = QSettings("Bycelium", "Pyflow")
pos = settings.value("pos", QPoint(200, 200))
size = settings.value("size", QSize(400, 400))
self.move(pos)
self.resize(size)
if settings.value("isMaximized", False) == "true":
self.showMaximized()
LOGGER.info("Loaded settings under Bycelium/Pyflow")

def writeSettings(self):
"""Write the settings to the config file."""
settings = QSettings("AutopIA", "Pyflow")
settings = QSettings("Bycelium", "Pyflow")
settings.setValue("pos", self.pos())
settings.setValue("size", self.size())
settings.setValue("isMaximized", self.isMaximized())
LOGGER.info("Saved settings under Bycelium/Pyflow")

def setActiveSubWindow(self, window):
"""Set the active subwindow to the given window."""
Expand Down
90 changes: 90 additions & 0 deletions pyflow/logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Pyflow an open-source tool for modular visual programing in python
# Copyright (C) 2021-2022 Bycelium <https://www.gnu.org/licenses/>

"""Utilitaries for logging in Pyflow."""

from time import time
import logging
from functools import wraps
from colorama import Fore, Style


def log_init_time(logger: logging.Logger, level=logging.DEBUG):
"""Decorator for logging a class init time."""

def inner(func):
"""Inner decorator for logging a class init time."""

@wraps(func)
def wrapper_func(self: type, *args, **kwargs):
"""Wrapper for logging a class init time."""
init_time = time()
func(self, *args, **kwargs)
class_name = str(self).split(" ", maxsplit=1)[0].split(".")[-1]
logger.log(
level, "Built %s in %.3fs", class_name, time() - init_time, stacklevel=2
)

return wrapper_func

return inner


def get_logger(name: str) -> logging.Logger:
"""Get the logger for the current module given it's name.
Args:
name (str): Name of the module usualy obtained using '__main___'
Returns:
logging.Logger: Logger for the given module name.
"""
return logging.getLogger(name)


class PyflowHandler(logging.StreamHandler):

"""Custom logging handler for Pyflow."""

COLOR_BY_LEVEL = {
"DEBUG": Fore.GREEN,
"INFO": Fore.BLUE,
"WARNING": Fore.LIGHTRED_EX,
"WARN": Fore.YELLOW,
"ERROR": Fore.RED,
"FATAL": Fore.RED,
"CRITICAL": Fore.RED,
}

def emit(self, record: logging.LogRecord):
record.pathname = "pyflow" + record.pathname.split("pyflow")[-1]

level_color = self.COLOR_BY_LEVEL.get(record.levelname)
record.levelname = fill_size(record.levelname, 8)
if level_color:
record.levelname = level_color + record.levelname + Style.RESET_ALL
return super().emit(record)


def fill_size(text: str, size: int, filler: str = " "):
"""Make a text fill a given size using a given filler.
Args:
text (str): Text to fit in given size.
size (int): Given size to fit text in.
filler (str, optional): Character to fill with if place there is. Defaults to " ".
Raises:
ValueError: The given filler is not a single character.
Returns:
str: A string containing the text and filler of the given size.
"""
if len(filler) > 1:
raise ValueError(
f"Given filler was more than one character ({len(filler)}>1): {filler}"
)
if len(text) < size:
missing_size = size - len(text)
return filler + text + filler * (missing_size - 1)
return text[:size]
4 changes: 4 additions & 0 deletions pyflow/scene/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@
from pyflow.scene.from_ipynb_conversion import ipynb_to_ipyg
from pyflow.scene.to_ipynb_conversion import ipyg_to_ipynb
from pyflow import blocks
from pyflow.logging import log_init_time, get_logger

LOGGER = get_logger(__name__)


class Scene(QGraphicsScene, Serializable):

"""Scene for the Window."""

@log_init_time(LOGGER)
def __init__(
self,
parent=None,
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ ipykernel>=6.5.0
ansi2html>=1.6.0
markdown>=3.3.6
pyqtwebengine>=5.15.5
networkx >= 2.6.2
networkx >= 2.6.2
colorama >= 0.4.4

0 comments on commit 4255e28

Please sign in to comment.