1
0
Fork 0
mirror of synced 2024-06-29 19:51:02 +12:00
Rare/rare/widgets/rare_app.py
loathingKernel 9d4e0995fd
RareAppException: Create exception handler that can show a dialog
The reason is that `sys.excepthook` is a global attribute which we
have to unset for threads because we show a Qt dialog in it and we
can't do that from threads. Before this change, we used to unset
it in threads, but since it is a global attr, that was unsetting it
for the whole application. We cannot reliably reset it because we
can have multiple threads executing and there will be race conditions.

To fix this situation, `RareAppException` implements a callback to
be patched into `sys.excepthook` which emits a signal to be serviced
by the the `RareAppException` instance in the main thread.
`RareAppException` can be subclassed to implement the
`RareAppException._handler` method for domain specific handling.

The `RareApp` base class instantiates its own `RareAppException`
instance for early basic handling. `RareAppException` is subclassed
into `RareException` and `RareLauncherExcpetion` in `Rare` and `RareLauncher`
respectively to implement the aforemention domain specific handling.
Each of these classes deletes the previous instance and replace it
with their specialized handlers.
2023-03-07 17:11:21 +02:00

140 lines
5.7 KiB
Python

import logging
import os
import platform
import sys
import time
import traceback
from argparse import Namespace
import legendary
from PyQt5.QtCore import QSettings, QTranslator, QT_VERSION_STR, PYQT_VERSION_STR, QObject, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QMessageBox
import rare.resources.resources
from rare.utils import paths
from rare.utils.misc import set_color_pallete, set_style_sheet, get_static_style
class RareAppException(QObject):
exception = pyqtSignal(object, object, object)
def __init__(self, parent=None):
self.logger = logging.getLogger(type(self).__name__)
super(RareAppException, self).__init__(parent=parent)
sys.excepthook = self._excepthook
self.exception.connect(self._on_exception)
def _excepthook(self, exc_type: object, exc_value: object, exc_tb: object):
self.exception.emit(exc_type, exc_value, exc_tb)
def _handler(self, exc_type, exc_value, exc_tb) -> bool:
return False
@pyqtSlot(object, object, object)
def _on_exception(self, exc_type, exc_value, exc_tb):
message = "".join(traceback.format_exception(exc_type, exc_value, exc_tb))
if self._handler(exc_type, exc_value, exc_tb):
return
self.logger.fatal(message)
QMessageBox.warning(None, exc_type.__name__, message)
QApplication.exit(1)
class RareApp(QApplication):
def __init__(self, args: Namespace, log_file: str):
self.logger = logging.getLogger(type(self).__name__)
super(RareApp, self).__init__(sys.argv)
self._hook = RareAppException(self)
self.setQuitOnLastWindowClosed(False)
self.setApplicationName("Rare")
self.setOrganizationName("Rare")
# Create directories after QStandardPaths has been initialized
paths.create_dirs()
# Clean any existing logging handlers from library imports
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)
start_time = time.strftime("%y-%m-%d--%H-%M") # year-month-day-hour-minute
file_handler = logging.FileHandler(
filename=os.path.join(paths.log_dir(), log_file.format(start_time)),
encoding="utf-8",
)
file_handler.setFormatter(fmt=logging.Formatter("[%(name)s] %(levelname)s: %(message)s"))
# Set up common logging channel to stderr
if args.debug:
logging.basicConfig(
format="[%(name)s] %(levelname)s: %(message)s",
level=logging.DEBUG,
stream=sys.stderr,
)
file_handler.setLevel(logging.DEBUG)
logging.root.addHandler(file_handler)
logging.getLogger().setLevel(level=logging.DEBUG)
# keep requests, asyncio and pillow quiet
logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("asyncio").setLevel(logging.WARNING)
self.logger.info(
f"Launching Rare version {rare.__version__} Codename: {rare.code_name}\n"
f" - Using Legendary {legendary.__version__} Codename: {legendary.__codename__} as backend\n"
f" - Operating System: {platform.system()}, Python version: {platform.python_version()}\n"
f" - Running {sys.executable} {' '.join(sys.argv)}\n"
f" - Qt version: {QT_VERSION_STR}, PyQt version: {PYQT_VERSION_STR}"
)
else:
logging.basicConfig(
format="[%(name)s] %(levelname)s: %(message)s",
level=logging.INFO,
stream=sys.stderr,
)
file_handler.setLevel(logging.DEBUG)
logging.root.addHandler(file_handler)
self.logger.info(f"Launching Rare version {rare.__version__}")
self.logger.info(f"Operating System: {platform.system()}")
self.settings = QSettings()
# Translator
self.translator = QTranslator()
self.qt_translator = QTranslator()
# Style
# lk: this is a bit silly but serves well until we have a class
# lk: store the default qt style name from the system on startup as a property for later reference
self.setProperty("rareDefaultQtStyle", self.style().objectName())
if (
self.settings.value("color_scheme", None) is None
and self.settings.value("style_sheet", None) is None
):
self.settings.setValue("color_scheme", "")
self.settings.setValue("style_sheet", "RareStyle")
if color_scheme := self.settings.value("color_scheme", False):
self.settings.setValue("style_sheet", "")
set_color_pallete(color_scheme)
elif style_sheet := self.settings.value("style_sheet", False):
self.settings.setValue("color_scheme", "")
set_style_sheet(style_sheet)
else:
self.setStyleSheet(get_static_style())
self.setWindowIcon(QIcon(":/images/Rare.png"))
def load_translator(self, lang: str):
if os.path.isfile(f := os.path.join(paths.resources_path, "languages", f"{lang}.qm")):
self.translator.load(f)
self.logger.info(f"Your language is supported: {lang}")
elif not lang == "en":
self.logger.info("Your language is not supported")
self.installTranslator(self.translator)
# translator for qt stuff
if os.path.isfile(f := os.path.join(paths.resources_path, f"qt_{lang}.qm")):
self.qt_translator = QTranslator()
self.qt_translator.load(f)
self.installTranslator(self.qt_translator)