From 7125ec50c40996f098159e9e1de05a914dd3df67 Mon Sep 17 00:00:00 2001 From: selfkilla666 Date: Thu, 21 Dec 2023 18:11:33 +0200 Subject: [PATCH] Add settings & Refactor code --- crasher/application.py | 27 ++------- crasher/classes/settings.py | 89 ++++++++++++++++++++++++++++ crasher/utils/json.py | 1 + crasher/utils/validate.py | 9 ++- crasher/widgets/application.py | 102 ++++++++++++++++++++++++--------- crasher/widgets/window.py | 8 +-- 6 files changed, 180 insertions(+), 56 deletions(-) create mode 100644 crasher/classes/settings.py diff --git a/crasher/application.py b/crasher/application.py index a3c45ad..0c0e64d 100644 --- a/crasher/application.py +++ b/crasher/application.py @@ -4,14 +4,10 @@ from __future__ import annotations +from crasher.widgets.application import QCrasherApplication + import sys import argparse -import platform - -from loguru import logger - -from crasher.widgets.application import QCrasherApplication -from crasher.utils.path import get_user_local_directory def get_run_arguments() -> argparse.Namespace: @@ -32,22 +28,11 @@ def run_application() -> None: args = get_run_arguments() - # Setup logger - logger.add( - str(get_user_local_directory()) + r"\logs\log_{time}.log", - format="{time:HH:mm:ss.SS} ({file}) [{level}] {message}", - colorize=True - ) - - logger.info("Application starts") - - # Log debug data about user computer - logger.debug(f"Platform: {platform.system()} {platform.release()} ({platform.architecture()[0]})") - if sys.argv[1:]: - logger.debug(f"Running application with arguments: {sys.argv[1:]}") - # Setup application - application: QCrasherApplication = QCrasherApplication(sys.argv, arguments_=args, logger_=logger) + application: QCrasherApplication = QCrasherApplication(sys.argv, arguments_=args) + + # Set the global exception handler + sys.excepthook = application.handle_exception # Execute application application.exec() diff --git a/crasher/classes/settings.py b/crasher/classes/settings.py new file mode 100644 index 0000000..0740996 --- /dev/null +++ b/crasher/classes/settings.py @@ -0,0 +1,89 @@ +# Code by @selfkilla666 +# https://github.com/witch-software/crasher +# MIT License + +from __future__ import annotations + +from typing import Any, Optional +from pathlib import Path + +from crasher.utils.validate import are_keys_present, add_missing_values + +import os.path +import toml +import loguru + + +DEFAULT_SETTINGS: dict[str, Any] = { + "General": { + "Interface": { + "Theme": "Light" + }, + "Language": "en_US" + } +} + + +class QCrasherApplicationSettings: + + path: Path + data: dict[Any, Any] + logger: Optional[loguru.Logger] + + def __init__(self, path: Path, *, logger: Optional[loguru.Logger] = None) -> None: + self.path = path + self.logger = logger + + def load_settings(self, default_settings=DEFAULT_SETTINGS) -> None: + + if self.logger: + self.logger.info(f"Try to load settings from \"{self.path}\"...") + + loaded_toml_data = default_settings + + if os.path.exists(self.path): + try: + loaded_toml_data = toml.load(self.path) + + if not are_keys_present(loaded_toml_data, default_settings): + + if self.logger: + self.logger.warning( + "Settings do not contain the required values. They will be replaced by default values") + + loaded_toml_data = add_missing_values(loaded_toml_data, default_settings) + + except Exception as e: + if self.logger: + self.logger.error(f"Failed to load settings from \"{self.path}\": {e}") + + self.data = loaded_toml_data + + def save_settings(self) -> None: + with open(self.path, "w") as toml_file: + toml.dump(self.data, toml_file) + + if self.logger: + self.logger.success(f"Settings were successfully saved to a file \"{self.path}\"!") + + def get_value(self, key: Any, default: Any = None) -> Any: + keys = key.split('.') + current_dict = self.data + + # Traverse the dictionary to get the value + for k in keys: + current_dict = current_dict.get(k, {}) + if not current_dict: + return default + + return current_dict or default + + def set_value(self, key: Any, value: Any) -> None: + keys = key.split('.') + current_dict = self.data + + # Traverse the dictionary to set the value and create missing sections + for k in keys[:-1]: + current_dict = current_dict.setdefault(k, {}) + + current_dict[keys[-1]] = value \ No newline at end of file diff --git a/crasher/utils/json.py b/crasher/utils/json.py index ea91a7b..e4942f5 100644 --- a/crasher/utils/json.py +++ b/crasher/utils/json.py @@ -21,6 +21,7 @@ def load_json_file(path: Path) -> dict[Any, Any]: with open(path, 'r') as file: return load(file) + def save_json_file(dictionary: dict[Any, Any], path: Path) -> None: """ Save dictionary to the JSON file. diff --git a/crasher/utils/validate.py b/crasher/utils/validate.py index 6dbd224..e1bcc9d 100644 --- a/crasher/utils/validate.py +++ b/crasher/utils/validate.py @@ -20,11 +20,18 @@ def are_keys_present(dictionary_to_check: dict[Any, Any], dictionary_pattern: di """ for key, value in dictionary_pattern.items(): - if key not in dictionary_to_check or not are_keys_present(dictionary_to_check[key], value): + if key not in dictionary_to_check: + return False + + if isinstance(value, dict): + if not isinstance(dictionary_to_check[key], dict) or not are_keys_present(dictionary_to_check[key], value): + return False + elif value is not None and dictionary_to_check[key] != value: return False return True + def add_missing_values(dictionary_to_check: dict[Any, Any], dictionary_pattern: dict[Any, Any]) -> dict[Any, Any]: """ Add missing keys and their corresponding values from `dictionary_pattern` to diff --git a/crasher/widgets/application.py b/crasher/widgets/application.py index ab2b3a6..84ddd96 100644 --- a/crasher/widgets/application.py +++ b/crasher/widgets/application.py @@ -8,12 +8,15 @@ from pathlib import Path -from PySide6.QtWidgets import QApplication, QMainWindow +from PySide6.QtWidgets import QApplication from PySide6.QtGui import QIcon from crasher.widgets.window import QCrasherWindow +from crasher.classes.settings import QCrasherApplicationSettings +from crasher.utils.path import get_user_local_directory import loguru +import platform __all__ = ["QCrasherApplication"] @@ -22,52 +25,65 @@ class QCrasherApplication(QApplication): """ Custom PySide6.QApplication implementation for Crasher """ - APPLICATION_TITLE: str = "Crasher" + APPLICATION_NAME: str = "Crasher" APPLICATION_VERSION: str = "1.0.0b" APPLICATION_ORG_NAME: str = "Witch Software" APPLICATION_ORG_DOMAIN: str = "witch-software.com" run_arguments: Namespace logger: loguru.Logger - window: QMainWindow + settings: QCrasherApplicationSettings + window: QCrasherWindow - def __init__(self, argv: list[str], *, arguments_: Namespace, logger_: loguru.Logger) -> None: + debug: bool = True + + def __init__(self, argv: list[str], *, arguments_: Namespace) -> None: """ Initialize the QCrasherApplication. - :type argv: list[str] - :type arguments_: argparse.Namespace - :type logger_: loguru.Logger - :param argv: Command-line arguments passed to the application. :param arguments_: Parsed command-line arguments for configuration. - :param logger_: An instance of the logger for logging application events. - - :rtype: None """ - # Todo: Add settings config + self.initialize_logger() + self.logger.info("Application starts") super(QCrasherApplication, self).__init__(argv) self.run_arguments = arguments_ - self.logger = logger_ + self.debug = self.run_arguments.debug or True - self.initialize_application() + self.log_debug_info(argv) + + self.logger.info("Start application initialization...") + + # Initialize application parts + self.initialize_application_information() + self.initialize_settings() self.initialize_window() - def initialize_application(self) -> None: - """ - Method to initialize the application. + self.logger.success("Application is fully initialized!") - :rtype: None - """ + def initialize_logger(self) -> None: + """ Method to initialize application logger """ + + self.logger: loguru.Logger = loguru.logger + + # Setup logger + self.logger.add( + str(get_user_local_directory()) + r"\logs\log_{time}.log", + format="{time:HH:mm:ss.SS} ({file}) [{level}] {message}", + colorize=True + ) + + def initialize_application_information(self) -> None: + """ Method to initialize application information """ # Connect application events self.aboutToQuit.connect(self.on_close_event) # Set application metadata - self.setApplicationName(self.APPLICATION_TITLE) + self.setApplicationName(self.APPLICATION_NAME) self.setApplicationVersion(self.APPLICATION_VERSION) self.setOrganizationName(self.APPLICATION_ORG_NAME) self.setOrganizationDomain(self.APPLICATION_ORG_DOMAIN) @@ -76,24 +92,54 @@ def initialize_application(self) -> None: icon = QIcon(str(Path("./static/gui/icons/icon.png"))) self.setWindowIcon(icon) + def initialize_settings(self) -> None: + """ Method to initialize application settings """ + + self.logger.info("Initialize application settings...") + + self.settings: QCrasherApplicationSettings = QCrasherApplicationSettings( + Path(f"{get_user_local_directory()}\\settings.toml"), + logger=self.logger + ) + self.settings.load_settings() + def initialize_window(self) -> None: + """ Method to initialize application window """ + + self.logger.info("Initialize application window...") if not self.run_arguments.windowless: + self.window: QCrasherWindow = QCrasherWindow(application=self) self.window.show() + + self.logger.success("Window initialized!") + else: self.logger.info("Application is running in windowless mode") - def set_window(self, window: QMainWindow) -> None: - self.window = window + def log_debug_info(self, argv: list[str]): + """ Logging some values that might help for debugging """ + + if self.debug: + self.logger.debug("Application running in debug mode.") + + self.logger.debug(f"Application version: {self.APPLICATION_VERSION}") + + # Debug data about user system + self.logger.debug(f"Platform: {platform.system()} {platform.release()} ({platform.architecture()[0]})") + + if len(argv) > 1: + self.logger.debug(f"Running application with arguments: {' '.join(argv[1:])}") + + def handle_exception(self, exc_type, exc_value, exc_traceback) -> None: + self.logger.exception("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) def on_close_event(self) -> None: - """ - Event called when closing the application. + """ Event called when closing the application """ - :rtype: None - """ + self.logger.info("Saving application settings...") - # Todo: Add saving settings config + self.settings.save_settings() - self.logger.success("Application was successfully closed.") + self.logger.success("Application was successfully closed!") diff --git a/crasher/widgets/window.py b/crasher/widgets/window.py index f6576dd..bf600f8 100644 --- a/crasher/widgets/window.py +++ b/crasher/widgets/window.py @@ -32,14 +32,10 @@ def __init__(self, *, application: QApplication) -> None: self.application: QApplication = application self.logger: loguru.Logger = self.application.logger # type: ignore[attr-defined] - - self.logger.info("Initialize application window...") - - self.initialize_ui() - - self.logger.success("Window initialized!") + self.initialize_ui() # TODO: Fix this typehint sometime in future ¯\_(ツ)_/¯ def initialize_ui(self) -> None: + self.logger.info("Initializing UI...") # Set window info