Skip to content

Commit

Permalink
Merge pull request #116 from YektaY/error
Browse files Browse the repository at this point in the history
ENH: More Extensive Error Pop-up Handling
  • Loading branch information
roussel-ryan authored Oct 30, 2024
2 parents c8e4633 + 3ce7a0b commit 19c1eb5
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 32 deletions.
17 changes: 13 additions & 4 deletions src/badger/core_subprocess.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import logging
import time

import traceback
import pkg_resources
import torch # noqa: F401. For converting dtype str to torch object.
from pandas import concat, DataFrame
import multiprocessing as mp

from badger.db import load_routine
from badger.errors import BadgerRunTerminated, BadgerError
from badger.errors import BadgerRunTerminated
from badger.logger import _get_default_logger
from badger.logger.event import Events
from badger.routine import Routine
Expand Down Expand Up @@ -94,7 +94,13 @@ def run_routine_subprocess(
print(f"Error in subprocess: {type(e).__name__}, {str(e)}")

# set required arguments
routine, _ = load_routine(args["routine_id"])
try:
routine, _ = load_routine(args["routine_id"])
except Exception as e:
error_title = f"{type(e).__name__}: {e}"
error_traceback = traceback.format_exc()
queue.put((error_title, error_traceback))
raise e

# TODO look into this bug with serializing of turbo. Fix might be needed in Xopt
# Patch for converting dtype str to torch object
Expand Down Expand Up @@ -222,5 +228,8 @@ def run_routine_subprocess(
evaluate_queue[0].close()
except Exception as e:
opt_logger.update(Events.OPTIMIZATION_END, solution_meta)
error_title = f"{type(e).__name__}: {e}"
error_traceback = traceback.format_exc()
queue.put((error_title, error_traceback))
evaluate_queue[0].close()
raise BadgerError(f"An error occurred: {str(e)}")
raise e
40 changes: 16 additions & 24 deletions src/badger/errors.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
from PyQt5.QtWidgets import QMessageBox
import traceback
import sys
from PyQt5.QtWidgets import QApplication


class BadgerError(Exception):
def __init__(self, message="", detailed_text=None):
if QApplication.instance() is None:
self.app = QApplication([])
else:
self.app = QApplication.instance()

if detailed_text is None:
detailed_text = self.capture_traceback_or_stack()

Expand Down Expand Up @@ -46,62 +40,60 @@ def capture_traceback_or_stack(self):
return "".join(traceback.format_stack())


class BadgerConfigError(BadgerError):
class BadgerConfigError(Exception):
pass


class VariableRangeError(BadgerError):
class VariableRangeError(Exception):
pass


class BadgerNotImplementedError(BadgerError):
class BadgerNotImplementedError(Exception):
pass


class BadgerDBError(BadgerError):
class BadgerDBError(Exception):
pass


class BadgerEnvVarError(BadgerError):
class BadgerEnvVarError(Exception):
pass


class BadgerEnvObsError(BadgerError):
class BadgerEnvObsError(Exception):
pass


class BadgerNoInterfaceError(BadgerError):
def __init__(self, detailed_text=None):
super().__init__(
message="Must provide an interface!", detailed_text=detailed_text
)
class BadgerNoInterfaceError(Exception):
def __init__(self, message="Must provide an interface!"):
super().__init__(message)


class BadgerInterfaceChannelError(BadgerError):
class BadgerInterfaceChannelError(Exception):
pass


class BadgerInvalidPluginError(BadgerError):
class BadgerInvalidPluginError(Exception):
pass


class BadgerPluginNotFoundError(BadgerError):
class BadgerPluginNotFoundError(Exception):
pass


class BadgerInvalidDocsError(BadgerError):
class BadgerInvalidDocsError(Exception):
pass


class BadgerLogbookError(BadgerError):
class BadgerLogbookError(Exception):
pass


class BadgerLoadConfigError(BadgerError):
class BadgerLoadConfigError(Exception):
pass


class BadgerRoutineError(BadgerError):
class BadgerRoutineError(Exception):
pass


Expand Down
32 changes: 31 additions & 1 deletion src/badger/gui/default/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import signal
import sys
import time

from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QFont, QIcon
from PyQt5 import QtCore
Expand All @@ -11,6 +10,11 @@
from badger.settings import init_settings
from badger.gui.default.windows.main_window import BadgerMainWindow

import traceback
from badger.errors import BadgerError
from types import TracebackType
from typing import Type, NoReturn

# Fix the scaling issue on multiple monitors w/ different scaling settings
# if sys.platform == 'win32':
# ctypes.windll.shcore.SetProcessDpiAwareness(1)
Expand Down Expand Up @@ -49,7 +53,33 @@ def on_timeout():
print("Timeout, resume the operation...")


def error_handler(
etype: Type[BaseException], value: BaseException, tb: TracebackType
) -> NoReturn:
"""
Custom exception handler that formats uncaught exceptions and raises a BadgerError.
Parameters
----------
etype : Type[BaseException]
The class of the exception that was raised.
value : BaseException
The exception instance.
tb : TracebackType
The traceback object associated with the exception.
Raises
------
BadgerError
An exception that includes the error title and detailed traceback.
"""
error_msg = "".join(traceback.format_exception(etype, value, tb))
error_title = f"{etype.__name__}: {value}"
raise BadgerError(error_title, error_msg)


def launch_gui():
sys.excepthook = error_handler
app = QApplication(sys.argv)
config_singleton = init_settings()
# Set app metainfo
Expand Down
12 changes: 9 additions & 3 deletions src/badger/gui/default/components/routine_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from badger.settings import init_settings
from badger.gui.default.components.process_manager import ProcessManager
from badger.routine import Routine
from badger.errors import BadgerError

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -69,7 +70,7 @@ def __init__(
)
self.start_time = None # track the time cost of the run
self.last_dump_time = None # track the time the run data got dumped
self.data_queue = None
self.data_and_error_queue = None
self.stop_event = None
self.pause_event = None
self.routine_process = None
Expand Down Expand Up @@ -140,7 +141,7 @@ def run(self) -> None:
self.routine_process = process_with_args["process"]
self.stop_event = process_with_args["stop_event"]
self.pause_event = process_with_args["pause_event"]
self.data_queue = process_with_args["data_queue"]
self.data_and_error_queue = process_with_args["data_queue"]
self.evaluate_queue = process_with_args["evaluate_queue"]
self.wait_event = process_with_args["wait_event"]

Expand All @@ -154,7 +155,7 @@ def run(self) -> None:
"start_time": self.start_time,
}

self.data_queue.put(arg_dict)
self.data_and_error_queue.put(arg_dict)
self.wait_event.set()
self.pause_event.set()
self.setup_timer()
Expand Down Expand Up @@ -208,13 +209,18 @@ def setup_timer(self) -> None:
def check_queue(self) -> None:
"""
This method checks the subprocess for updates in the evaluate_queue.
It also checks the self.data_and_error_queue to see if an exception was thrown during the routine.
It is called by a QTimer every 100 miliseconds.
"""
if self.evaluate_queue[1].poll():
while self.evaluate_queue[1].poll():
results = self.evaluate_queue[1].recv()
self.after_evaluate(results)

if not self.data_and_error_queue.empty():
error_title, error_traceback = self.data_and_error_queue.get()
BadgerError(error_title, error_traceback)

if not self.routine_process.is_alive():
self.close()
self.evaluate_queue[1].close()
Expand Down
3 changes: 3 additions & 0 deletions src/badger/tests/test_routine_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from PyQt5.QtCore import QEventLoop, Qt, QTimer
from PyQt5.QtTest import QSignalSpy
from PyQt5.QtWidgets import QApplication
from unittest.mock import Mock


class TestRoutineRunner:
Expand Down Expand Up @@ -75,6 +76,7 @@ def test_after_evaluate(self, instance):
def test_check_queue(self, instance):
sig_finished_spy = QSignalSpy(instance.signals.finished)
instance.run()
instance.data_and_error_queue.empty = Mock(return_value=True)
instance.check_queue()
instance.stop_routine()
assert len(sig_finished_spy) == 1
Expand Down Expand Up @@ -172,6 +174,7 @@ def inner(ins, event):
)

monitor.run_until_action.trigger()
monitor.routine_runner.data_and_error_queue.empty = Mock(return_value=True)

# Wait until the run is done
while monitor.running:
Expand Down

0 comments on commit 19c1eb5

Please sign in to comment.