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 logging & improve CLI #231

Merged
merged 8 commits into from
Jan 30, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
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