From 80f44b7febea4dc6a575ff02c15d6329f5b3102b Mon Sep 17 00:00:00 2001 From: Yannis Chatzikonstantinou Date: Tue, 30 Apr 2024 02:30:14 +0300 Subject: [PATCH] improve logging --- studio/Python/setup.py | 1 + studio/Python/tinymovr/gui/__init__.py | 4 +- studio/Python/tinymovr/gui/helpers.py | 66 ++++++++++++++++++++++++-- studio/Python/tinymovr/gui/window.py | 13 ++--- studio/Python/tinymovr/gui/worker.py | 2 +- 5 files changed, 74 insertions(+), 12 deletions(-) diff --git a/studio/Python/setup.py b/studio/Python/setup.py index a97e4c85..a97887bd 100644 --- a/studio/Python/setup.py +++ b/studio/Python/setup.py @@ -60,6 +60,7 @@ "docopt", "flatten-dict", "pint", + "pretty_errors" ], extras_require={"gui": ["pyside6", "pyqtgraph>=0.13.3"]}, entry_points={ diff --git a/studio/Python/tinymovr/gui/__init__.py b/studio/Python/tinymovr/gui/__init__.py index df9eba5c..ea6f8ae6 100644 --- a/studio/Python/tinymovr/gui/__init__.py +++ b/studio/Python/tinymovr/gui/__init__.py @@ -8,11 +8,13 @@ display_file_save_dialog, magnitude_of, StreamRedirector, + QTextBrowserLogger, TimedGetter, check_selected_items, get_dynamic_attrs, is_dark_mode, - strtobool + strtobool, + configure_pretty_errors ) from tinymovr.gui.widgets import ( NodeTreeWidgetItem, diff --git a/studio/Python/tinymovr/gui/helpers.py b/studio/Python/tinymovr/gui/helpers.py index c3ba8dd4..99e94d31 100644 --- a/studio/Python/tinymovr/gui/helpers.py +++ b/studio/Python/tinymovr/gui/helpers.py @@ -16,16 +16,29 @@ """ import time +import logging +from datetime import datetime import os import enum import pint from PySide6 import QtGui -from PySide6.QtGui import QGuiApplication, QPalette +from PySide6.QtGui import QGuiApplication, QPalette, QTextCursor from PySide6.QtWidgets import QMessageBox, QFileDialog from avlos.definitions import RemoteAttribute, RemoteEnum, RemoteBitmask +import pretty_errors import tinymovr +class ConsoleColor: + NORMAL = "" + ERROR = "" + WARNING = "" + INFO = "" + DEBUG = "" + TIMESTAMP = "" + END = "" + + app_stylesheet = """ /* --------------------------------------- QPushButton -----------------------------------*/ @@ -467,21 +480,66 @@ def magnitude_of(val): class StreamRedirector(object): - """A class to redirect writes from a stream to a QPlainTextEdit.""" + """ + A class to redirect writes from a stream to a QPlainTextEdit, including pretty_errors handling and timestamps. + """ def __init__(self, widget): self.widget = widget self.buffer = '' def write(self, message): + # Timestamp the message + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + message_with_timestamp = f"[{timestamp}] {message}" + + # Redirect to the QPlainTextEdit widget self.widget.moveCursor(QtGui.QTextCursor.End) - self.widget.insertPlainText(message) - self.widget.moveCursor(QtGui.QTextCursor.End) + self.widget.insertPlainText(message_with_timestamp) self.widget.ensureCursorVisible() def flush(self): pass +class QTextBrowserLogger(logging.Handler): + """A logging handler that directs logging output to a QTextBrowser widget.""" + def __init__(self, widget): + super().__init__() + self.widget = widget + self.widget.setReadOnly(True) + self.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) + + def emit(self, record): + msg = self.format(record) + self.widget.append(self.colorize_message(record.levelno, msg)) + + def colorize_message(self, level, message): + color = { + logging.DEBUG: "lightgreen", + logging.INFO: "lightblue", + logging.WARNING: "orange", + logging.ERROR: "red", + logging.CRITICAL: "purple" + }.get(level, "black") + timestamp = datetime.now().strftime('%H:%M:%S') + return f'[{timestamp}] {message}' + + +def configure_pretty_errors(): + pretty_errors.configure( + separator_character='*', + filename_display=pretty_errors.FILENAME_EXTENDED, + line_number_first=True, + display_link=True, + lines_before=5, + lines_after=2, + line_color=pretty_errors.RED + '> ' + pretty_errors.default_config.line_color, + code_color=' ' + pretty_errors.default_config.code_color, + truncate_code=True, # Truncate code lines to not overflow in the GUI + display_locals=True + ) + + class TimedGetter: """ An interface class that maintains timing diff --git a/studio/Python/tinymovr/gui/window.py b/studio/Python/tinymovr/gui/window.py index 61a4385f..b8e42cb6 100644 --- a/studio/Python/tinymovr/gui/window.py +++ b/studio/Python/tinymovr/gui/window.py @@ -35,7 +35,7 @@ QMessageBox, QTreeWidgetItem, QSplitter, - QPlainTextEdit + QTextBrowser ) from PySide6.QtGui import QAction import pyqtgraph as pg @@ -54,11 +54,13 @@ PlaceholderQTreeWidget, BoolTreeWidgetItem, StreamRedirector, + QTextBrowserLogger, format_value, display_file_open_dialog, display_file_save_dialog, magnitude_of, check_selected_items, + configure_pretty_errors ) @@ -74,6 +76,7 @@ def __init__(self, app, arguments, logger): self.start_time = time.time() self.logger = logger if logger is not None else logging.getLogger("tinymovr") + configure_pretty_errors() self.attr_widgets_by_id = {} self.graphs_by_id = {} @@ -142,10 +145,10 @@ def __init__(self, app, arguments, logger): self.splitter.addWidget(self.left_frame) self.splitter.addWidget(self.right_frame) - self.console = QPlainTextEdit() + self.console = QTextBrowser() self.console.setReadOnly(True) - sys.stdout = StreamRedirector(self.console) - sys.stderr = StreamRedirector(self.console) + self.log_handler = QTextBrowserLogger(self.console) + self.logger.addHandler(self.log_handler) self.main_splitter = QSplitter(QtCore.Qt.Vertical) self.main_splitter.setHandleWidth(0) @@ -219,8 +222,6 @@ def about_to_quit(self): self.visibility_update_timer.stop() self.worker_update_timer.stop() self.worker.stop() - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ @QtCore.Slot() def handle_worker_error(self, e): diff --git a/studio/Python/tinymovr/gui/worker.py b/studio/Python/tinymovr/gui/worker.py index 77ae57db..c1cbea88 100644 --- a/studio/Python/tinymovr/gui/worker.py +++ b/studio/Python/tinymovr/gui/worker.py @@ -139,7 +139,7 @@ def _update(self): def _device_appeared(self, device, node_id): self.mutx.lock() display_name = "{}{}".format(device.name, node_id) - self.logger.warn("Found {} with device uid {}".format(display_name, node_id)) + self.logger.info("Found {} (uid {})".format(display_name, device.uid)) self.devices_by_name[display_name] = device self.names_by_id[node_id] = display_name device.name = display_name