Skip to content
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
87 changes: 22 additions & 65 deletions rascal2/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import site
import sys

from rascal2.settings import Settings, get_global_settings
from rascal2.settings import get_global_settings

if getattr(sys, "frozen", False):
# we are running in a bundle
Expand All @@ -24,6 +24,7 @@
IMAGES_PATH = STATIC_PATH / "images"
MATLAB_ARCH_FILE = pathlib.Path(SITE_PATH) / "matlab/engine/_arch.txt"
EXAMPLES_TEMP_PATH = pathlib.Path(get_global_settings().fileName()).parent / "examples"
LOGGER = logging.getLogger("rascal2")


def handle_scaling():
Expand All @@ -50,84 +51,41 @@ def path_for(filename: str):
return (IMAGES_PATH / filename).as_posix()


def setup_settings(project_path: str | os.PathLike) -> Settings:
"""Set up the Settings object for the project.

Parameters
----------
project_path : str or PathLike
The path to the current RasCAL-2 project.

Returns
-------
Settings
If a settings.json file already exists in the
RasCAL-2 project, returns a Settings object with
the settings defined there. Otherwise, returns a
(global) default Settings object.

"""
filepath: pathlib.Path = pathlib.Path(project_path, "settings.json")
if filepath.is_file():
json = filepath.read_text()
return Settings.model_validate_json(json)
return Settings()
def log_uncaught_exceptions(exc_type, exc_value, exc_traceback):
"""Qt slots swallows exceptions but this ensures exceptions are logged."""
logging.critical("An unhandled exception occurred!", exc_info=(exc_type, exc_value, exc_traceback))
logging.shutdown()
sys.exit(1)


def setup_logging(log_path: str | os.PathLike, terminal=None, level: int = logging.INFO) -> logging.Logger:
def setup_logging(level: int = logging.INFO) -> None:
"""Set up logging for the project.

The default logging path and level are defined in the settings.

Parameters
----------
log_path : str | PathLike
The path to where the log file will be written.
terminal : Optional[TerminalWidget]
The TerminalWidget instance which acts as an IO stream.
level : int, default logging.INFO
The debug level for the logger.

"""
path = pathlib.Path(log_path)
logger = logging.getLogger("rascal_log")
path = pathlib.Path(get_global_settings().fileName()).parent
path.mkdir(parents=True, exist_ok=True)

logger = logging.getLogger()
logger.setLevel(level)
logger.handlers.clear()

log_filehandler = logging.FileHandler(path)
log_file_handler = logging.FileHandler(path / "rascal.log")
file_formatting = logging.Formatter("%(asctime)s - %(threadName)s - %(name)s - %(levelname)s - %(message)s")
log_filehandler.setFormatter(file_formatting)
logger.addHandler(log_filehandler)

if terminal is not None:
# handler that logs to terminal widget
log_termhandler = logging.StreamHandler(stream=terminal)
term_formatting = logging.Formatter("%(levelname)s - %(message)s")
log_termhandler.setFormatter(term_formatting)
logger.addHandler(log_termhandler)

return logger


def get_logger():
"""Get the RasCAL logger, and set up a backup logger if it hasn't been set up yet."""
logger = logging.getLogger("rascal_log")
if not logger.handlers:
# Backup in case the crash happens before the local logger setup
path = pathlib.Path(get_global_settings().fileName()).parent
path.mkdir(parents=True, exist_ok=True)
setup_logging(path / "rascal.log")
log_file_handler.setFormatter(file_formatting)
logger.addHandler(log_file_handler)

return logger
# handler that logs to terminal widget
log_term_handler = logging.StreamHandler()
term_formatting = logging.Formatter("%(levelname)s - %(message)s")
log_term_handler.setFormatter(term_formatting)
logger.addHandler(log_term_handler)


def log_uncaught_exceptions(exc_type, exc_value, exc_traceback):
"""Qt slots swallows exceptions but this ensures exceptions are logged."""
logger = get_logger()
logger.addHandler(logging.StreamHandler(stream=sys.stderr)) # print emergency crashes to terminal
logger.critical("An unhandled exception occurred!", exc_info=(exc_type, exc_value, exc_traceback))
logging.shutdown()
sys.exit(1)
sys.excepthook = log_uncaught_exceptions


def run_matlab(ready_event, close_event, engine_output):
Expand Down Expand Up @@ -280,6 +238,5 @@ def get_matlab_path(self):
if error:
self.engine_output[:] = []
self.engine_output.append(Exception(error))
logger = logging.getLogger("rascal_log")
logger.error(f"{error}. Attempt to read MATLAB _arch file failed {MATLAB_ARCH_FILE}.")
LOGGER.error(f"{error}. Attempt to read MATLAB _arch file failed {MATLAB_ARCH_FILE}.")
return str(install_dir)
5 changes: 3 additions & 2 deletions rascal2/core/commands.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
"""File for Qt commands."""

import copy
import logging
from collections.abc import Callable
from enum import IntEnum, unique

import ratapi
from PyQt6 import QtGui
from ratapi import ClassList

from rascal2.config import LOGGER


@unique
class CommandID(IntEnum):
Expand Down Expand Up @@ -69,7 +70,7 @@ def redo(self):
except Exception as ex:
self.new_result = self.old_result
message = f"Error occurred when generating result preview:\n\n{ex}"
logging.error(message, exc_info=ex)
LOGGER.error(message, exc_info=ex)
self.presenter.view.terminal_widget.write(message)
self.presenter.model.update_results(self.new_result)
else:
Expand Down
12 changes: 4 additions & 8 deletions rascal2/dialogs/custom_file_editor.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
"""Dialogs for editing custom files."""

import logging
from pathlib import Path

from PyQt6 import Qsci, QtGui, QtWidgets
from ratapi.utils.enums import Languages

from rascal2.config import EXAMPLES_PATH, MatlabHelper
from rascal2.config import EXAMPLES_PATH, LOGGER, MatlabHelper


def edit_file(filename: str, language: Languages, parent: QtWidgets.QWidget):
Expand All @@ -24,8 +23,7 @@ def edit_file(filename: str, language: Languages, parent: QtWidgets.QWidget):
"""
file = Path(filename)
if not file.is_file():
logger = logging.getLogger("rascal_log")
logger.error("Attempted to edit a custom file which does not exist!")
LOGGER.error("Attempted to edit a custom file which does not exist!")
return

dialog = CustomFileEditorDialog(file, language, parent)
Expand All @@ -37,8 +35,7 @@ def edit_file_matlab(filename: str):
try:
engine = MatlabHelper().get_local_engine()
except Exception as ex:
logger = logging.getLogger("rascal_log")
logger.error("Attempted to edit a file in MATLAB engine" + repr(ex))
LOGGER.error("Attempted to edit a file in MATLAB engine", exc_info=ex)
return

engine.edit(str(filename))
Expand Down Expand Up @@ -131,7 +128,6 @@ def save_file(self):
self.file.write_text(self.editor.text())
self.accept()
except OSError as ex:
logger = logging.getLogger("rascal_log")
message = f"Failed to save custom file to {self.file}.\n"
logger.error(message, exc_info=ex)
LOGGER.error(message, exc_info=ex)
QtWidgets.QMessageBox.critical(self, "Save File", message, QtWidgets.QMessageBox.StandardButton.Ok)
8 changes: 3 additions & 5 deletions rascal2/dialogs/settings_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from PyQt6 import QtCore, QtWidgets

from rascal2.config import MATLAB_ARCH_FILE, MatlabHelper
from rascal2.settings import Settings, SettingsGroups, delete_local_settings
from rascal2.settings import SettingsGroups
from rascal2.widgets.inputs import get_validated_input


Expand Down Expand Up @@ -61,15 +61,13 @@ def __init__(self, parent):
def update_settings(self) -> None:
"""Accept the changed settings."""
self.parent().settings = self.settings
if self.parent().presenter.model.save_path:
self.parent().settings.save(self.parent().presenter.model.save_path)
self.parent().settings.set_global_settings()
self.matlab_tab.set_matlab_paths()
self.accept()

def reset_default_settings(self) -> None:
"""Reset the settings to the global defaults."""
delete_local_settings(self.parent().presenter.model.save_path)
self.parent().settings = Settings()
self.parent().settings.reset_global_settings()
self.accept()


Expand Down
5 changes: 2 additions & 3 deletions rascal2/dialogs/startup_dialog.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import logging
import os
from pathlib import Path

from PyQt6 import QtCore, QtWidgets

from rascal2.config import EXAMPLES_PATH
from rascal2.config import EXAMPLES_PATH, LOGGER
from rascal2.core.worker import Worker
from rascal2.settings import update_recent_projects

Expand Down Expand Up @@ -183,7 +182,7 @@ def project_start_failed(self, exception, args):
folder_name = args[0]
error = str(exception).strip().replace("\n", "")
message = f"The Project ({folder_name}) could not be opened because:\n\n{error}"
logging.error(message, exc_info=exception)
LOGGER.error(message, exc_info=exception)
QtWidgets.QMessageBox.critical(self, self.windowTitle(), message)


Expand Down
6 changes: 4 additions & 2 deletions rascal2/main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import logging
import multiprocessing
import re
import sys
from contextlib import suppress

from PyQt6 import QtGui, QtWidgets

from rascal2.config import IMAGES_PATH, STATIC_PATH, MatlabHelper, handle_scaling, log_uncaught_exceptions, path_for
from rascal2.config import IMAGES_PATH, STATIC_PATH, MatlabHelper, handle_scaling, path_for, setup_logging
from rascal2.ui.view import MainWindowView


Expand Down Expand Up @@ -42,10 +43,11 @@ def main():
"""Entry point function for starting RasCAL."""
multiprocessing.freeze_support()
multiprocessing.set_start_method("spawn", force=True)
sys.excepthook = log_uncaught_exceptions
setup_logging()
matlab_helper = MatlabHelper()
exit_code = ui_execute()
matlab_helper.close_event.set()
logging.shutdown()
sys.exit(exit_code)


Expand Down
21 changes: 13 additions & 8 deletions rascal2/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ class SettingsGroups(StrEnum):
Logging = "Logging"
Plotting = "Plotting"
Terminal = "Terminal"
Windows = "Windows"


class Styles(StrEnum):
Expand Down Expand Up @@ -131,10 +130,6 @@ class Settings(BaseModel, validate_assignment=True, arbitrary_types_allowed=True
default=True, title=SettingsGroups.Terminal, description="Clear Terminal when Run Starts"
)
terminal_fontsize: int = Field(default=12, title=SettingsGroups.Terminal, description="Terminal Font Size", gt=0)

mdi_defaults: MDIGeometries = Field(
default=None, title=SettingsGroups.Windows, description="Default Window Geometries"
)
export_background_colour: BackgroundColour = Field(
default=BackgroundColour.White, title=SettingsGroups.Plotting, description="Background colour of exported plot"
)
Expand All @@ -145,7 +140,6 @@ def model_post_init(self, __context: Any):
for setting in unset_settings:
if global_name(setting) in global_settings.allKeys():
setattr(self, setting, global_settings.value(global_name(setting)))
self.model_fields_set.remove(setting) # we don't want global defaults to count as manually set!

def save(self, path: str | PathLike):
"""Save settings to a JSON file in the given path.
Expand All @@ -163,6 +157,17 @@ def set_global_settings(self):
global_settings = get_global_settings()
for setting in self.model_fields_set:
global_settings.setValue(global_name(setting), getattr(self, setting))
global_settings.sync()

def reset_global_settings(self):
"""Reset the local and global settings to default."""
for field, field_info in self.model_fields.items():
setattr(self, field, field_info.default)

global_settings = get_global_settings()
for group in SettingsGroups:
global_settings.remove(group)
global_settings.sync()


def global_name(key: str) -> str:
Expand Down Expand Up @@ -202,7 +207,7 @@ def update_recent_projects(path: str | None = None) -> list[str]:

"""
settings = get_global_settings()
recent_projects: list[str] = settings.value("internal/recent_projects")
recent_projects: list[str] = settings.value("recent_projects")
if not recent_projects:
recent_projects = []

Expand All @@ -213,6 +218,6 @@ def update_recent_projects(path: str | None = None) -> list[str]:

new_recent_projects = new_recent_projects[:10]

settings.setValue("internal/recent_projects", new_recent_projects)
settings.setValue("recent_projects", new_recent_projects)
settings.sync()
return new_recent_projects
15 changes: 7 additions & 8 deletions rascal2/ui/presenter.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import ratapi as rat
import ratapi.wrappers

from rascal2.config import MatlabHelper, get_matlab_engine
from rascal2.config import LOGGER, MatlabHelper, get_matlab_engine
from rascal2.core import commands
from rascal2.core.enums import UnsavedReply
from rascal2.core.runner import LogData, RATRunner
Expand Down Expand Up @@ -71,11 +71,10 @@ def load_r1_project(self, load_path: str):

def initialise_ui(self):
"""Initialise UI for a project."""
suffix = " [Example]" if self.model.is_project_example() else ""
suffix = " [Example]" if self.model.is_project_example() else f"[{self.model.save_path}]"
self.view.setWindowTitle(
self.view.windowTitle().split(" - ")[0] + " - " + self.model.project.name + suffix,
)
self.view.init_settings_and_log(self.model.save_path)
self.view.setup_mdi()
self.view.plot_widget.update_plots()
self.view.handle_results(self.model.results)
Expand Down Expand Up @@ -124,7 +123,7 @@ def save_project(self, save_as: bool = False):
try:
self.model.save_project(to_path)
except OSError as err:
self.view.logging.error(f"Failed to save project to {to_path}.\n", exc_info=err)
LOGGER.error(f"Failed to save project to {to_path}.\n", exc_info=err)
else:
update_recent_projects(self.model.save_path)
self.view.undo_stack.setClean()
Expand Down Expand Up @@ -158,7 +157,7 @@ def export_fits(self):
try:
write_result_to_zipped_csvs(save_file, results)
except OSError as err:
self.view.logging.error(f"Failed to save fits to {save_file}.\n", exc_info=err)
LOGGER.error(f"Failed to save fits to {save_file}.\n", exc_info=err)

def interrupt_terminal(self):
"""Send an interrupt signal to the RAT runner."""
Expand Down Expand Up @@ -232,9 +231,9 @@ def handle_results(self):
def handle_interrupt(self):
"""Handle a RAT run being interrupted."""
if self.runner.error is None:
self.view.logging.info("RAT run interrupted!")
LOGGER.info("RAT run interrupted!")
else:
self.view.logging.error("RAT run failed with exception.\n", exc_info=self.runner.error)
LOGGER.error("RAT run failed with exception.\n", exc_info=self.runner.error)
self.view.handle_results()
self.model.controls.delete_IPC()

Expand All @@ -252,7 +251,7 @@ def handle_event(self):
case rat.events.PlotEventData():
self.view.plot_widget.plot_with_blit(event)
case LogData():
self.view.logging.log(event.level, event.msg)
LOGGER.log(event.level, event.msg)

def edit_project(self, updated_project: dict, preview: bool = True) -> None:
"""Edit the Project with a dictionary of attributes.
Expand Down
Loading
Loading