From 5f4d00820328b35c45f7e15186fe84a15fb70a3d Mon Sep 17 00:00:00 2001 From: Rebecca Breu Date: Fri, 14 May 2021 13:37:44 +0200 Subject: [PATCH] Log to file; Add 'Show Debug Log' action --- beeref/__main__.py | 16 +++++++--- beeref/actions/actions.py | 5 ++++ beeref/actions/menu_structure.py | 1 + beeref/assets.py | 4 +-- beeref/config.py | 51 +++++++++++++++++++++++++++++++- beeref/fileio/__init__.py | 3 +- beeref/fileio/sql.py | 3 +- beeref/gui.py | 38 +++++++++++++++++++++++- beeref/items.py | 3 +- beeref/scene.py | 3 +- beeref/selection.py | 3 +- beeref/utils.py | 11 +++++++ beeref/view.py | 17 ++++++----- tests/conftest.py | 12 ++++++++ tests/test_utils.py | 21 +++++++++++++ tests/test_view.py | 5 ++++ 16 files changed, 170 insertions(+), 26 deletions(-) create mode 100644 tests/conftest.py diff --git a/beeref/__main__.py b/beeref/__main__.py index 91aadd0..d94d0ab 100755 --- a/beeref/__main__.py +++ b/beeref/__main__.py @@ -26,7 +26,7 @@ from beeref.config import CommandlineArgs from beeref import constants from beeref.view import BeeGraphicsView -logger = logging.getLogger(constants.APPNAME) +logger = logging.getLogger(__name__) class BeeRefMainWindow(QtWidgets.QWidget): @@ -66,15 +66,23 @@ def handle_sigint(signum, frame): QtWidgets.QApplication.quit() +def handle_uncaught_exception(exc_type, value, traceback): + logger.critical('Unhandled exception', + exc_info=(exc_type, value, traceback)) + + +sys.excepthook = handle_uncaught_exception + + def main(): - commandline_args = CommandlineArgs(with_check=True) - logging.basicConfig(level=getattr(logging, commandline_args.loglevel)) + logger.info(f'Starting {constants.APPNAME} version {constants.VERSION}') + CommandlineArgs(with_check=True) # Force checking app = QtWidgets.QApplication(sys.argv) bee = BeeRefMainWindow(app) # NOQA:F841 signal.signal(signal.SIGINT, handle_sigint) # Repeatedly run python-noop to give the interpreter time to - # handel signals + # handle signals safe_timer(50, lambda: None) app.exec() diff --git a/beeref/actions/actions.py b/beeref/actions/actions.py index fa82b24..ccf8614 100644 --- a/beeref/actions/actions.py +++ b/beeref/actions/actions.py @@ -203,6 +203,11 @@ actions = [ 'shortcuts': ['F1', 'Ctrl+H'], 'callback': 'on_action_help', }, + { + 'id': 'debuglog', + 'text': 'Show &Debug Log', + 'callback': 'on_action_debuglog', + }, { 'id': 'show_scrollbars', 'text': 'Show &Scrollbars', diff --git a/beeref/actions/menu_structure.py b/beeref/actions/menu_structure.py index e82bc4d..8177dfc 100644 --- a/beeref/actions/menu_structure.py +++ b/beeref/actions/menu_structure.py @@ -89,6 +89,7 @@ menu_structure = [ 'menu': '&Help', 'items': [ 'help', + 'debuglog', ], }, ] diff --git a/beeref/assets.py b/beeref/assets.py index e3821a1..5bb7658 100644 --- a/beeref/assets.py +++ b/beeref/assets.py @@ -20,10 +20,8 @@ import os.path from PyQt6 import QtGui -from beeref import constants - -logger = logging.getLogger(constants.APPNAME) +logger = logging.getLogger(__name__) class BeeAssets: diff --git a/beeref/config.py b/beeref/config.py index bf80051..885ac70 100644 --- a/beeref/config.py +++ b/beeref/config.py @@ -17,6 +17,7 @@ import argparse import logging +import logging.config import os.path from PyQt6 import QtCore @@ -24,7 +25,7 @@ from PyQt6 import QtCore from beeref import constants -logger = logging.getLogger(constants.APPNAME) +logger = logging.getLogger(__name__) parser = argparse.ArgumentParser( @@ -135,3 +136,51 @@ class BeeSettings(QtCore.QSettings): if existing_only: values = [f for f in values if os.path.exists(f)] return values + + +logfile_name = os.path.join( + os.path.dirname(BeeSettings().fileName()), f'{constants.APPNAME}.log') + +logging_conf = { + 'version': 1, + 'formatters': { + 'verbose': { + 'format': ('{asctime} {name} {process:d} {thread:d} {message}'), + 'style': '{', + }, + 'simple': { + 'format': '{levelname} {name}: {message}', + 'style': '{', + }, + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'simple', + 'level': CommandlineArgs().loglevel, + }, + 'file': { + 'class': 'beeref.utils.BeeRotatingFileHandler', + 'formatter': 'verbose', + 'filename': logfile_name, + 'maxBytes': 1024 * 1000 * 50, + 'backupCount': 1, + 'level': 'DEBUG', + 'delay': True, + } + }, + 'loggers': { + 'beeref': { + 'handlers': ['console', 'file'], + 'level': 'DEBUG', + 'propagate': False, + }, + }, + 'root': { + 'handlers': ['console', 'file'], + 'level': 'DEBUG', + }, +} + + +logging.config.dictConfig(logging_conf) diff --git a/beeref/fileio/__init__.py b/beeref/fileio/__init__.py index b8bf4f1..866d73a 100644 --- a/beeref/fileio/__init__.py +++ b/beeref/fileio/__init__.py @@ -19,7 +19,6 @@ import logging from PyQt6 import QtCore, QtGui from beeref import commands -from beeref import constants from beeref.fileio.errors import BeeFileIOError from beeref.fileio.sql import SQLiteIO from beeref.items import BeePixmapItem @@ -33,7 +32,7 @@ __all__ = [ 'BeeFileIOError', ] -logger = logging.getLogger(constants.APPNAME) +logger = logging.getLogger(__name__) def load_bee(filename, scene, worker=None): diff --git a/beeref/fileio/sql.py b/beeref/fileio/sql.py index e69b8d9..aac258f 100644 --- a/beeref/fileio/sql.py +++ b/beeref/fileio/sql.py @@ -30,12 +30,11 @@ import sqlite3 from PyQt6 import QtGui from beeref.items import BeePixmapItem -from beeref import constants from .errors import BeeFileIOError from .schema import SCHEMA -logger = logging.getLogger(constants.APPNAME) +logger = logging.getLogger(__name__) def handle_sqlite_errors(func): diff --git a/beeref/gui.py b/beeref/gui.py index 147de1f..db0a8a3 100644 --- a/beeref/gui.py +++ b/beeref/gui.py @@ -20,9 +20,10 @@ from PyQt6 import QtWidgets from PyQt6.QtCore import Qt from beeref import constants +from beeref.config import logfile_name -logger = logging.getLogger(constants.APPNAME) +logger = logging.getLogger(__name__) class WelcomeOverlay(QtWidgets.QWidget): @@ -78,9 +79,44 @@ class HelpDialog(QtWidgets.QDialog): with open(os.path.join(docdir, 'controls.html')) as f: controls_txt = f.read() controls = QtWidgets.QLabel(controls_txt) + controls.setTextInteractionFlags( + Qt.TextInteractionFlag.TextSelectableByMouse) tabs.addTab(controls, '&Controls') layout = QtWidgets.QVBoxLayout() self.setLayout(layout) layout.addWidget(tabs) self.show() + + +class DebugLogDialog(QtWidgets.QDialog): + def __init__(self, parent): + super().__init__(parent) + self.setWindowTitle(f'{constants.APPNAME} Debug Log') + with open(logfile_name) as f: + self.log_txt = f.read() + + log = QtWidgets.QLabel(self.log_txt) + log.setTextInteractionFlags( + Qt.TextInteractionFlag.TextSelectableByMouse) + scroll = QtWidgets.QScrollArea(self) + scroll.setWidgetResizable(True) + scroll.setWidget(log) + + buttons = QtWidgets.QDialogButtonBox( + QtWidgets.QDialogButtonBox.StandardButton.Close) + buttons.rejected.connect(self.reject) + copy_button = QtWidgets.QPushButton('Co&py To Clipboard') + copy_button.released.connect(self.copy_to_clipboard) + buttons.addButton( + copy_button, QtWidgets.QDialogButtonBox.ButtonRole.ActionRole) + + layout = QtWidgets.QVBoxLayout() + self.setLayout(layout) + layout.addWidget(scroll) + layout.addWidget(buttons) + self.show() + + def copy_to_clipboard(self): + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(self.log_txt) diff --git a/beeref/items.py b/beeref/items.py index df150b4..862279b 100644 --- a/beeref/items.py +++ b/beeref/items.py @@ -21,11 +21,10 @@ import logging from PyQt6 import QtCore, QtGui, QtWidgets -from beeref import constants from beeref.selection import SelectableMixin -logger = logging.getLogger(constants.APPNAME) +logger = logging.getLogger(__name__) class BeePixmapItem(SelectableMixin, QtWidgets.QGraphicsPixmapItem): diff --git a/beeref/scene.py b/beeref/scene.py index b646640..af63d97 100644 --- a/beeref/scene.py +++ b/beeref/scene.py @@ -23,11 +23,10 @@ from PyQt6.QtCore import Qt import rpack from beeref import commands -from beeref import constants from beeref.selection import MultiSelectItem, RubberbandItem -logger = logging.getLogger(constants.APPNAME) +logger = logging.getLogger(__name__) class BeeGraphicsScene(QtWidgets.QGraphicsScene): diff --git a/beeref/selection.py b/beeref/selection.py index 712ef6f..98f827f 100644 --- a/beeref/selection.py +++ b/beeref/selection.py @@ -25,12 +25,11 @@ from PyQt6.QtWidgets import QGraphicsItem from beeref.assets import BeeAssets from beeref import commands from beeref.config import CommandlineArgs -from beeref import constants from beeref import utils commandline_args = CommandlineArgs() -logger = logging.getLogger(constants.APPNAME) +logger = logging.getLogger(__name__) SELECT_COLOR = QtGui.QColor(116, 234, 231, 255) diff --git a/beeref/utils.py b/beeref/utils.py index 5a551b6..339519e 100644 --- a/beeref/utils.py +++ b/beeref/utils.py @@ -13,6 +13,9 @@ # You should have received a copy of the GNU General Public License # along with BeeRef. If not, see . +import logging.handlers +import os +import os.path from PyQt6 import QtCore @@ -37,3 +40,11 @@ def round_to(number, base): """ return base * round(number / base) + + +class BeeRotatingFileHandler(logging.handlers.RotatingFileHandler): + """RotatingFileHandler that creates log directory if necessary.""" + + def __init__(self, filename, **kwargs): + os.makedirs(os.path.dirname(filename), exist_ok=True) + super().__init__(filename, **kwargs) diff --git a/beeref/view.py b/beeref/view.py index 5491818..8ed88bc 100644 --- a/beeref/view.py +++ b/beeref/view.py @@ -24,13 +24,13 @@ from beeref import commands from beeref.config import CommandlineArgs, BeeSettings from beeref import constants from beeref import fileio -from beeref.gui import BeeProgressDialog, WelcomeOverlay, HelpDialog +from beeref import gui from beeref.items import BeePixmapItem from beeref.scene import BeeGraphicsScene commandline_args = CommandlineArgs() -logger = logging.getLogger(constants.APPNAME) +logger = logging.getLogger(__name__) class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): @@ -72,7 +72,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): self.customContextMenuRequested.connect(self.on_context_menu) self.context_menu = self.build_menu_and_actions() - self.welcome_overlay = WelcomeOverlay(self) + self.welcome_overlay = gui.WelcomeOverlay(self) # Load file given via command line if commandline_args.filename: @@ -279,7 +279,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): fileio.load_bee, filename, self.scene) self.worker.progress.connect(self.on_items_loaded) self.worker.finished.connect(self.on_loading_finished) - self.progress = BeeProgressDialog( + self.progress = gui.BeeProgressDialog( 'Loading %s' % filename, worker=self.worker, parent=self) @@ -311,7 +311,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): self.worker = fileio.ThreadedIO( fileio.save_bee, filename, self.scene, create_new=create_new) self.worker.finished.connect(self.on_saving_finished) - self.progress = BeeProgressDialog( + self.progress = gui.BeeProgressDialog( 'Saving %s' % filename, worker=self.worker, parent=self) @@ -336,7 +336,10 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): self.app.quit() def on_action_help(self): - HelpDialog(self) + gui.HelpDialog(self) + + def on_action_debuglog(self): + gui.DebugLogDialog(self) def on_insert_images_finished(self, filename, errors): if errors: @@ -369,7 +372,7 @@ class BeeGraphicsView(QtWidgets.QGraphicsView, ActionsMixin): self.scene) self.worker.progress.connect(self.on_items_loaded) self.worker.finished.connect(self.on_insert_images_finished) - self.progress = BeeProgressDialog( + self.progress = gui.BeeProgressDialog( 'Loading images', worker=self.worker, parent=self) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..bbd9e92 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,12 @@ +from unittest.mock import MagicMock + + +def pytest_configure(config): + # Ignore logging configuration for BeeRef during test runs. This + # avoids logging to the regular log file and spamming test output + # with debug messages. + # + # This needs to be done before the application code is even loaded since + # logging configuration happens on module level + import logging.config + logging.config.dictConfig = MagicMock diff --git a/tests/test_utils.py b/tests/test_utils.py index b72e086..46965bc 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,4 +1,6 @@ +import os.path import pytest +import tempfile from PyQt6 import QtCore @@ -32,3 +34,22 @@ class GetRectFromPointsTestCase(BeeTestCase): (3.1, 0.5, 3.0)]) def test_round_to(number, base, expected): assert utils.round_to(number, base) == expected + + +class BeeRotatingFileHandler(BeeTestCase): + + def test_creates_new_dir(self): + with tempfile.TemporaryDirectory() as tmpdir: + logfile = os.path.join(tmpdir, 'foo', 'bar.log') + handler = utils.BeeRotatingFileHandler(logfile) + handler.emit('foo') + handler.close() + assert os.path.exists(logfile) + + def test_uses_existing_dir(self): + with tempfile.TemporaryDirectory() as tmpdir: + logfile = os.path.join(tmpdir, 'bar.log') + handler = utils.BeeRotatingFileHandler(logfile) + handler.emit('foo') + handler.close() + assert os.path.exists(logfile) diff --git a/tests/test_view.py b/tests/test_view.py index e8db80f..17c714a 100644 --- a/tests/test_view.py +++ b/tests/test_view.py @@ -284,6 +284,11 @@ class BeeGraphicsViewTestCase(ViewBaseTestCase): self.view.on_action_help() show_mock.assert_called_once() + @patch('beeref.gui.DebugLogDialog.show') + def test_on_action_debuglog(self, show_mock): + self.view.on_action_debuglog() + show_mock.assert_called_once() + @patch('beeref.scene.BeeGraphicsScene.clearSelection') @patch('PyQt6.QtWidgets.QFileDialog.getOpenFileNames') def test_on_action_insert_images(self, dialog_mock, clear_mock):