Skip to content

Commit cf00a6a

Browse files
committed
Refactor the Error Reporting system
- Move original `preditor.debug.BlurExcepthook` class to `preditor.excepthooks.PreditorExceptHook` and refactor removing most of the legacy blur specific customization. - You can now configure the excepthook system using `preditor.configure` - Removed blur specific code from `ErrorDialog`. - Named all widgets in `ErrorDialog` so subclasses can easily customize it.
1 parent c5cfef7 commit cf00a6a

File tree

10 files changed

+220
-258
lines changed

10 files changed

+220
-258
lines changed

examples/add_to_app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ def raise_error():
4141
# specific to this application.
4242
'add_to_app',
4343
)
44-
import preditor.debug
44+
import preditor.excepthooks
4545

46-
preditor.debug.BlurExcepthook.install()
46+
preditor.excepthooks.PreditorExceptHook.install()
4747

4848
# Create a Gui Application allowing the user to show PrEditor
4949
app = QApplication(sys.argv)

preditor/config.py

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,17 @@ def __init__(self):
4949
self._logging = False
5050
self._headless_callback = None
5151
self._parent_callback = None
52+
self._error_dialog_class = True
53+
self._excepthooks = []
5254

5355
def dump(self, indent=0):
5456
"""A convenient way to inspect the current configuration."""
5557
ret = [
5658
f"{' ' * indent}name: {self.name}",
5759
f"{' ' * indent}streams: {self.streams}",
5860
f"{' ' * indent}excepthook: {self.excepthook!r}",
61+
f"{' ' * indent}excepthooks: {self.excepthooks!r}",
62+
f"{' ' * indent}error_dialog_class: {self.error_dialog_class!r}",
5963
f"{' ' * indent}logging: {self.logging}",
6064
f"{' ' * indent}headless_callback: {self.headless_callback!r}",
6165
f"{' ' * indent}parent_callback: {self.parent_callback!r}",
@@ -83,14 +87,37 @@ def is_locked(self, value, track_state=False):
8387
return self._locks.get(value, False)
8488
return False
8589

90+
@property
91+
def error_dialog_class(self):
92+
"""Dialog class shown if PrEditor isn't visible and a error happens.
93+
94+
This dialog is used to prompt the user to show PrEditor and can be
95+
sub-classed to add extra functionality. This is called by
96+
`PreditorExceptHook.ask_to_show_logger` method when added to
97+
`preditor.config.excepthooks`.
98+
99+
If set to `True` then `blurdev.gui.errordialog.ErrorDialog` is used. When
100+
replacing this, it should be a sub-class of that class or re-implement
101+
its api.
102+
103+
You can use an EntryPoint string like `preditor.gui.errordialog:ErrorDialog`
104+
instead of passing the actual class object. This lets you delay the import
105+
until actually needed.
106+
"""
107+
return self._error_dialog_class
108+
109+
@error_dialog_class.setter
110+
def error_dialog_class(self, cls):
111+
self._error_dialog_class = cls
112+
86113
@property
87114
def excepthook(self):
88115
"""Installs a `sys.excepthook` handler when first set to True.
89116
90117
Replaces `sys.excepthook` with a interactive exception handler that
91118
prompts the user to show PrEditor when an python exception is raised.
92119
It is recommended that you only add the excepthook once the Qt UI is
93-
initialized. See: :py:class:`preditor.debug.BlurExcepthook`.
120+
initialized. See: :py:class:`preditor.excepthooks.PreditorExceptHook`.
94121
"""
95122
return self._locks.get("excepthook", False)
96123

@@ -100,19 +127,30 @@ def excepthook(self, value):
100127
if not value:
101128
return
102129

103-
# Install the excepthook
104-
import preditor.debug
130+
# Install the excepthook:
131+
import preditor.excepthooks
105132

106133
# Note: install checks if the current excepthook is a instance of this
107134
# class and prevents installing a second time.
108-
preditor.debug.BlurExcepthook.install()
135+
preditor.excepthooks.PreditorExceptHook.install()
109136

110-
# Disable future setting via `set_if_unlocked`, the BlurExcepthook is a
111-
# chaining except hook that calls the previous excepthook when called. We
112-
# don't want to install multiple automatically, the user can define that
137+
# Disable future setting via `set_if_unlocked`, the `PreditorExceptHook`
138+
# is a chaining except hook that calls the previous excepthook when called.
139+
# We don't want to install multiple automatically, the user can define that
113140
# logic if required.
114141
self._locks["excepthook"] = True
115142

143+
@property
144+
def excepthooks(self):
145+
"""A list of callables that are called when an exception is handled.
146+
147+
If `excepthook` is enabled installing `PreditorExceptHook` then it will
148+
call each item in this list. The signature of the function should be
149+
`callable(*args)`. If not configured it will automatically install default
150+
callables. You can add `None` to disable this.
151+
"""
152+
return self._excepthooks
153+
116154
@property
117155
def headless_callback(self):
118156
"""A pointer to a method that is called by `is_headless`.

preditor/cores/core.py

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
from Qt.QtCore import QObject
44

5-
from .. import config
6-
75

86
class Core(QObject):
97
"""
@@ -20,32 +18,3 @@ def __init__(self, objectName=None):
2018
# Paths in this variable will be removed in
2119
# preditor.osystem.subprocessEnvironment
2220
self._removeFromPATHEnv = set()
23-
24-
def shouldReportException(self, exc_type, exc_value, exc_traceback, actions=None):
25-
"""
26-
Allow core to control how exceptions are handled. Currently being used
27-
by `BlurExcepthook`, informing which excepthooks should or should not
28-
be executed.
29-
30-
Args:
31-
exc_type (type): exception type class object
32-
exc_value (Exception): class instance of exception parameter
33-
exc_traceback (traceback): encapsulation of call stack for exception
34-
actions (dict, optional): default values for the returned dict. A copy
35-
of this dict is returned with standard defaults applied.
36-
37-
Returns:
38-
dict: Boolean values representing whether to perform excepthook
39-
action, keyed to the name of the excepthook
40-
"""
41-
if actions is None:
42-
actions = {}
43-
# Create a shallow copy so we don't modify the passed in dict and don't
44-
# need to use a default value of None
45-
actions = actions.copy()
46-
47-
# provide the expected default values
48-
actions.setdefault('email', True)
49-
# If blurdev is running headless, there is no way to show a gui prompt
50-
actions.setdefault('prompt', not config.is_headless())
51-
return actions

preditor/debug.py

Lines changed: 0 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,6 @@
44
import inspect
55
import logging
66
import sys
7-
import traceback
8-
9-
from Qt import QtCompat
10-
11-
from . import config, core
12-
from .contexts import ErrorReport
137

148
logger = logging.getLogger(__name__)
159

@@ -77,150 +71,6 @@ def logToFile(path, stdout=True, stderr=True, useOldStd=True, clearLog=True):
7771
StreamHandlerHelper.replace_stream(sys.stderr._stdhandle, sys.stderr)
7872

7973

80-
# --------------------------------------------------------------------------------
81-
82-
83-
class BlurExcepthook(object):
84-
"""
85-
Blur's excepthook override allowing for granular error handling
86-
customization.
87-
88-
Stacked atop the standard library excepthook (by default), catches any
89-
unhandled exceptions and conditionally passes them to the following custom
90-
excepthooks:
91-
92-
- *`call_base_excepthook`*
93-
excepthook callable supplied at initialization; if not supplied or
94-
invalid, executes standard library excepthook.
95-
96-
- *`send_exception_email`*
97-
email notification.
98-
99-
- *`send_logger_error`*
100-
logger console.
101-
102-
Arguments:
103-
ehook (callable): An excepthook callable compatible with signature of
104-
sys.excepthook; defaults to original startup excepthook
105-
"""
106-
107-
def __init__(self, base_excepthook=None):
108-
self.base_excepthook = base_excepthook or sys.__excepthook__
109-
# We can't show the prompt if running headless.
110-
self.actions = dict(email=True, prompt=not config.is_headless())
111-
112-
def __call__(self, *exc_info):
113-
"""
114-
Executes overriden execpthook.
115-
116-
Checks the results from the core's `shouldReportException` function as
117-
to if the current exception should be reported. (Why? Nuke, for
118-
example, uses exceptions to signal tradionally non-exception worthy
119-
events, such as when a user cancels an Open File dialog window.)
120-
"""
121-
self.actions = core.shouldReportException(*exc_info, actions=self.actions)
122-
123-
self.call_base_excepthook(exc_info)
124-
self.send_exception_email(exc_info)
125-
self.send_logger_error(exc_info)
126-
127-
ErrorReport.clearReports()
128-
129-
def call_base_excepthook(self, exc_info):
130-
"""
131-
Process base excepthook supplied during object instantiation.
132-
133-
A newline is printed pre-traceback to ensure the first line of output
134-
is not printed in-line with the prompt. This also provides visual
135-
separation between tracebacks, when recieved consecutively.
136-
"""
137-
print("")
138-
try:
139-
self.base_excepthook(*exc_info)
140-
except (TypeError, NameError):
141-
sys.__excepthook__(*exc_info)
142-
143-
def send_exception_email(self, exc_info):
144-
"""
145-
Conditionally sends an exception email.
146-
"""
147-
if not self.actions.get("email", False):
148-
return
149-
150-
# email_addresses = os.getenv('BDEV_ERROR_EMAIL')
151-
# if email_addresses:
152-
# from .utils.error import ErrorEmail
153-
# mailer = ErrorEmail(*exc_info)
154-
# mailer.send(email_addresses)
155-
156-
def send_logger_error(self, exc_info):
157-
"""
158-
Shows logger prompt.
159-
"""
160-
if not self.actions.get("prompt", False):
161-
return
162-
163-
from .gui.console import ConsolePrEdit
164-
from .gui.errordialog import ErrorDialog
165-
from .gui.loggerwindow import LoggerWindow
166-
167-
instance = LoggerWindow.instance(create=False)
168-
169-
if instance:
170-
# logger reference deleted, fallback and print to console
171-
if not QtCompat.isValid(instance):
172-
print("[LoggerWindow] LoggerWindow object has been deleted.")
173-
print(traceback)
174-
return
175-
176-
# logger is visible and check if it was minimized on windows
177-
if instance.isVisible() and not instance.isMinimized():
178-
if instance.uiAutoPromptACT.isChecked():
179-
instance.console().startInputLine()
180-
return
181-
182-
# error already prompted
183-
if ConsolePrEdit._errorPrompted:
184-
return
185-
186-
# Preemptively marking error as "prompted" (handled) to avoid errors
187-
# from being raised multiple times due to C++ and/or threading error
188-
# processing.
189-
try:
190-
ConsolePrEdit._errorPrompted = True
191-
errorDialog = ErrorDialog(config.root_window())
192-
errorDialog.setText(exc_info)
193-
errorDialog.exec_()
194-
195-
# interruptted until dialog closed
196-
finally:
197-
ConsolePrEdit._errorPrompted = False
198-
199-
@classmethod
200-
def install(cls, force=False):
201-
"""
202-
Install Blur excepthook override, returing previously implemented
203-
excepthook function.
204-
205-
Arguments:
206-
force (bool): force reinstallation of excepthook override when
207-
already previously implemented.
208-
209-
Returns:
210-
func: pre-override excepthook function
211-
"""
212-
ErrorReport.enabled = True
213-
prev_excepthook = sys.excepthook
214-
215-
if not isinstance(prev_excepthook, BlurExcepthook) or force:
216-
sys.excepthook = cls(prev_excepthook)
217-
218-
return prev_excepthook
219-
220-
221-
# --------------------------------------------------------------------------------
222-
223-
22474
def printCallingFunction(compact=False):
22575
"""Prints and returns info about the calling function
22676

0 commit comments

Comments
 (0)