Skip to content

Commit

Permalink
Improve error reporting in the Graphical User Interface
Browse files Browse the repository at this point in the history
  • Loading branch information
bartfeenstra committed Feb 23, 2024
1 parent 5a9bf6b commit 2e453b3
Show file tree
Hide file tree
Showing 11 changed files with 151 additions and 66 deletions.
5 changes: 4 additions & 1 deletion betty/assets/betty.pot
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Betty VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-02-17 18:26+0000\n"
"POT-Creation-Date: 2024-02-20 23:54+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
Expand Down Expand Up @@ -243,6 +243,9 @@ msgstr ""
msgid "Clear all caches"
msgstr ""

msgid "Close"
msgstr ""

msgid "Conference"
msgstr ""

Expand Down
5 changes: 4 additions & 1 deletion betty/assets/locale/de-DE/betty.po
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Betty VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-02-17 18:26+0000\n"
"POT-Creation-Date: 2024-02-20 23:54+0000\n"
"PO-Revision-Date: 2024-02-08 13:24+0000\n"
"Last-Translator: Bart Feenstra <bart@bartfeenstra.com>\n"
"Language: de\n"
Expand Down Expand Up @@ -357,6 +357,9 @@ msgstr "Saubere URLs"
msgid "Clear all caches"
msgstr "Alle Caches löschen"

msgid "Close"
msgstr "Schließen"

msgid "Conference"
msgstr "Konferenz"

Expand Down
5 changes: 4 additions & 1 deletion betty/assets/locale/fr-FR/betty.po
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-02-17 18:26+0000\n"
"POT-Creation-Date: 2024-02-20 23:54+0000\n"
"PO-Revision-Date: 2024-02-08 13:24+0000\n"
"Last-Translator: Bart Feenstra <bart@bartfeenstra.com>\n"
"Language: fr\n"
Expand Down Expand Up @@ -297,6 +297,9 @@ msgstr ""
msgid "Clear all caches"
msgstr ""

msgid "Close"
msgstr ""

msgid "Conference"
msgstr ""

Expand Down
5 changes: 4 additions & 1 deletion betty/assets/locale/nl-NL/betty.po
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-02-17 18:26+0000\n"
"POT-Creation-Date: 2024-02-20 23:54+0000\n"
"PO-Revision-Date: 2024-02-11 15:31+0000\n"
"Last-Translator: Bart Feenstra <bart@bartfeenstra.com>\n"
"Language: nl\n"
Expand Down Expand Up @@ -350,6 +350,9 @@ msgstr "Nette URL's"
msgid "Clear all caches"
msgstr "Leeg alle caches"

msgid "Close"
msgstr "Sluiten"

msgid "Conference"
msgstr "Conferentie"

Expand Down
5 changes: 4 additions & 1 deletion betty/assets/locale/uk/betty.po
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Betty VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-02-17 18:26+0000\n"
"POT-Creation-Date: 2024-02-20 23:54+0000\n"
"PO-Revision-Date: 2024-02-08 13:08+0000\n"
"Last-Translator: Rainer Thieringer <rainerthi@gmail.com>\n"
"Language: uk\n"
Expand Down Expand Up @@ -297,6 +297,9 @@ msgstr ""
msgid "Clear all caches"
msgstr ""

msgid "Close"
msgstr ""

#, fuzzy
msgid "Conference"
msgstr "Референції"
Expand Down
36 changes: 26 additions & 10 deletions betty/gui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
from PyQt6.QtWidgets import QApplication, QWidget

from betty.app import App
from betty.error import UserFacingError
from betty.gui.error import ExceptionError, UnexpectedExceptionError
from betty.gui.error import ExceptionError, _UnexpectedExceptionError
from betty.locale import Str
from betty.serde.format import FormatRepository

Expand Down Expand Up @@ -75,6 +74,10 @@ def _stylesheet(self) -> str:
margin-bottom: 0.3em;
}}
Code {{
font-family: monospace;
}}
QLineEdit[invalid="true"] {{
border: 1px solid red;
color: red;
Expand Down Expand Up @@ -130,21 +133,34 @@ def __init__(self, *args: Any, app: App, **kwargs: Any):
@pyqtSlot(
type,
bytes,
str,
QObject,
bool,
)
def _catch_error(
def _show_user_facing_error(
self,
error_type: type[Exception],
pickled_error_message: bytes,
error_traceback: str | None,
parent: QWidget,
parent: QObject,
close_parent: bool,
) -> None:
error_message = pickle.loads(pickled_error_message)
if issubclass(error_type, UserFacingError):
window = ExceptionError(parent, self._app, error_type, error_message, close_parent=close_parent)
else:
window = UnexpectedExceptionError(parent, self._app, error_type, error_message, error_traceback, close_parent=close_parent)
window = ExceptionError(self._app, error_message, error_type, parent=parent, close_parent=close_parent)
window.show()

@pyqtSlot(
type,
str,
str,
QObject,
bool,
)
def _show_unexpected_exception(
self,
error_type: type[Exception],
error_message: str,
error_traceback: str,
parent: QObject,
close_parent: bool,
) -> None:
window = _UnexpectedExceptionError(self._app, error_type, error_message, error_traceback, parent=parent, close_parent=close_parent)
window.show()
5 changes: 2 additions & 3 deletions betty/gui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ class BettyPrimaryWindow(BettyMainWindow):
def __init__(
self,
app: App,
*,
parent: QObject | None = None,
/,
):
super().__init__(app, parent=parent)
super().__init__(app)
self.setWindowIcon(QIcon(path.join(path.dirname(__file__), 'assets', 'public', 'static', 'betty-512x512.png')))

menu_bar = self.menuBar()
Expand Down
133 changes: 87 additions & 46 deletions betty/gui/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@
from __future__ import annotations

import pickle
import traceback
from asyncio import CancelledError
from logging import getLogger
from traceback import format_exception
from types import TracebackType
from typing import TypeVar, Generic, ParamSpec

from PyQt6.QtCore import QMetaObject, Qt, Q_ARG, QObject
from PyQt6.QtGui import QCloseEvent, QIcon
from PyQt6.QtWidgets import QMessageBox, QWidget
from PyQt6.QtGui import QCloseEvent
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QScrollArea, QFrame

from betty.app import App
from betty.error import UserFacingError
from betty.gui.locale import LocalizedObject
from betty.gui.text import Code, Text
from betty.gui.window import BettyMainWindow
from betty.locale import Str, Localizable

T = TypeVar('T')
Expand Down Expand Up @@ -63,42 +65,73 @@ def _catch(self, exception_type: type[BaseExceptionT] | None, exception: BaseExc
if isinstance(exception, self._SUPPRESS_EXCEPTION_TYPES):
return None

QMetaObject.invokeMethod(
BettyApplication.instance(),
'_catch_error',
Qt.ConnectionType.QueuedConnection,
Q_ARG(type, type(exception)),
Q_ARG(bytes, pickle.dumps(exception if isinstance(exception, UserFacingError) else Str.plain(str(exception)))),
Q_ARG(str, ''.join(traceback.format_exception(exception))),
Q_ARG(QObject, self._parent),
Q_ARG(bool, self._close_parent),
)
if isinstance(exception, UserFacingError):
QMetaObject.invokeMethod(
BettyApplication.instance(),
'_show_user_facing_error',
Qt.ConnectionType.QueuedConnection,
Q_ARG(type, exception_type),
Q_ARG(bytes, pickle.dumps(exception)),
Q_ARG(QObject, self._parent),
Q_ARG(bool, self._close_parent),
)
else:
getLogger(__name__).exception(exception)
QMetaObject.invokeMethod(
BettyApplication.instance(),
'_show_unexpected_exception',
Qt.ConnectionType.QueuedConnection,
Q_ARG(type, exception_type),
Q_ARG(str, str(exception)),
Q_ARG(str, ''.join(format_exception(exception))),
Q_ARG(QObject, self._parent),
Q_ARG(bool, self._close_parent),
)
return True


class Error(LocalizedObject, QMessageBox):
class Error(BettyMainWindow):
window_height = 300
window_width = 500

def __init__(
self,
parent: QObject,
app: App,
message: Localizable,
*,
parent: QObject,
close_parent: bool = False,
):
super().__init__(app, parent)
self._message = message
super().__init__(app, parent=parent)
self._message_localizable = message
if close_parent and not isinstance(parent, QWidget):
raise ValueError('If `close_parent` is true, `parent` must be `QWidget`.')
self._close_parent = close_parent
self.setWindowModality(Qt.WindowModality.WindowModal)

central_widget = QWidget()
self._central_layout = QVBoxLayout()
central_widget.setLayout(self._central_layout)
self.setCentralWidget(central_widget)

self._message = Text()
self._central_layout.addWidget(self._message)

standard_button_type = QMessageBox.StandardButton.Close
self.setStandardButtons(standard_button_type)
close_button = self.button(standard_button_type)
assert close_button is not None
close_button.setIcon(QIcon())
self.setDefaultButton(standard_button_type)
self.setEscapeButton(standard_button_type)
close_button.clicked.connect(self.close)
self._controls = QHBoxLayout()
self._central_layout.addLayout(self._controls)

self._dismiss = QPushButton()
self._dismiss.released.connect(self.close)
self._controls.addWidget(self._dismiss)

@property
def window_title(self) -> Localizable:
return Str.plain('{error} - Betty', error=Str._('Error'))

def _set_translatables(self) -> None:
super()._set_translatables()
self._message.setText(self._message_localizable.localize(self._app.localizer))
self._dismiss.setText(self._app.localizer._('Close'))

def closeEvent(self, a0: QCloseEvent | None) -> None:
if self._close_parent:
Expand All @@ -107,44 +140,52 @@ def closeEvent(self, a0: QCloseEvent | None) -> None:
parent.close()
super().closeEvent(a0)

def _set_translatables(self) -> None:
super()._set_translatables()
self.setWindowTitle('{error} - Betty'.format(error=self._app.localizer._("Error")))
self.setText(self._message.localize(self._app.localizer))


ErrorT = TypeVar('ErrorT', bound=Error)


class ExceptionError(Error):
def __init__(
self,
parent: QWidget,
app: App,
message: Localizable,
error_type: type[BaseException],
error_message: Localizable,
*,
parent: QObject,
close_parent: bool = False,
):
super().__init__(parent, app, error_message, close_parent=close_parent)
super().__init__(app, message, parent=parent, close_parent=close_parent)
self.error_type = error_type


class UnexpectedExceptionError(ExceptionError):
class _UnexpectedExceptionError(ExceptionError):
def __init__(
self,
parent: QWidget,
app: App,
error_type: type[BaseException],
error_message: Localizable,
error_traceback: str | None,
error_type: type[Exception],
error_message: str,
error_traceback: str,
*,
parent: QObject,
close_parent: bool = False,
):
super().__init__(parent, app, error_type, error_message, close_parent=close_parent)
self.setText(self._app.localizer._('An unexpected error occurred and Betty could not complete the task. Please <a href="{report_url}">report this problem</a> and include the following details, so the team behind Betty can address it.').format(
report_url='https://github.com/bartfeenstra/betty/issues',
))
self.setTextFormat(Qt.TextFormat.RichText)
if error_traceback:
self.setDetailedText(error_traceback)
super().__init__(
app,
Str._(
'An unexpected error occurred and Betty could not complete the task. Please <a href="{report_url}">report this problem</a> and include the following details, so the team behind Betty can address it.',
report_url='https://github.com/bartfeenstra/betty/issues',
),
error_type,
parent=parent,
close_parent=close_parent,
)

if error_message:
self._exception_message = Code(error_message)
self._central_layout.addWidget(self._exception_message)

self._exception_details = QScrollArea()
self._exception_details.setFrameShape(QFrame.Shape.NoFrame)
self._exception_details.setWidget(Code(error_traceback + error_traceback + error_traceback + error_traceback))
self._exception_details.setWidgetResizable(True)
self._central_layout.addWidget(self._exception_details)
3 changes: 3 additions & 0 deletions betty/gui/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,9 @@ def __init__(
self._serve_button.released.connect(self._serve)
button_layout.addWidget(self._serve_button)

self._log_record_viewer = LogRecordViewer()
central_layout.addWidget(self._log_record_viewer)

self._logging_handler = LogRecordViewerHandler(self._log_record_viewer)
load.getLogger().addHandler(self._logging_handler)
generate.getLogger().addHandler(self._logging_handler)
Expand Down
10 changes: 10 additions & 0 deletions betty/gui/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,13 @@ def __init__(self, text: str | None = None):
super().__init__(text)
font = QFont()
self.setFont(font)


class Code(Text):
def __init__(self, text: str | None = None):
super().__init__(text)
font = QFont()
self.setFont(font)

def setText(self, a0: str | None) -> None:
super().setText(f'<pre>{a0}</pre>' if a0 else a0)
Loading

0 comments on commit 2e453b3

Please sign in to comment.