Skip to content

Commit

Permalink
Simplify catching exceptions in the Graphical User Interface. (#1295)
Browse files Browse the repository at this point in the history
  • Loading branch information
bartfeenstra authored Feb 21, 2024
1 parent 3ae82b5 commit 72ea42a
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 164 deletions.
24 changes: 12 additions & 12 deletions betty/extension/gramps/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from betty.extension import Gramps
from betty.extension.gramps.config import FamilyTreeConfiguration
from betty.gui import mark_valid, mark_invalid
from betty.gui.error import catch_exceptions
from betty.gui.error import ExceptionCatcher
from betty.gui.locale import LocalizedObject
from betty.gui.text import Text
from betty.gui.window import BettyMainWindow
Expand Down Expand Up @@ -116,15 +116,15 @@ def _update_configuration_file_path(file_path: str) -> None:
file_path_layout = QHBoxLayout()
file_path_layout.addWidget(self._file_path)

@catch_exceptions
def find_family_tree_file_path() -> None:
found_family_tree_file_path, __ = QFileDialog.getOpenFileName(
self._widget,
self._app.localizer._('Load the family tree from...'),
directory=self._file_path.text(),
)
if '' != found_family_tree_file_path:
self._file_path.setText(found_family_tree_file_path)
with ExceptionCatcher(self):
found_family_tree_file_path, __ = QFileDialog.getOpenFileName(
self._widget,
self._app.localizer._('Load the family tree from...'),
directory=self._file_path.text(),
)
if '' != found_family_tree_file_path:
self._file_path.setText(found_family_tree_file_path)
self._file_path_find = QPushButton('...')
self._file_path_find.released.connect(find_family_tree_file_path)
file_path_layout.addWidget(self._file_path_find)
Expand All @@ -134,10 +134,10 @@ def find_family_tree_file_path() -> None:
buttons_layout = QHBoxLayout()
self._layout.addRow(buttons_layout)

@catch_exceptions
def save_and_close_family_tree() -> None:
self._app.extensions[Gramps].configuration.family_trees.append(self._family_tree)
self.close()
with ExceptionCatcher(self):
self._app.extensions[Gramps].configuration.family_trees.append(self._family_tree)
self.close()
self._save_and_close = QPushButton()
self._save_and_close.setDisabled(True)
self._save_and_close.released.connect(save_and_close_family_tree)
Expand Down
10 changes: 5 additions & 5 deletions betty/extension/nginx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from betty.extension.nginx.artifact import generate_configuration_file, generate_dockerfile_file
from betty.generate import Generator, GenerationContext
from betty.gui import GuiBuilder
from betty.gui.error import catch_exceptions
from betty.gui.error import ExceptionCatcher
from betty.locale import Str
from betty.serde.dump import Dump, VoidableDump, minimize, Void, VoidableDictDump
from betty.serde.load import Asserter, Fields, OptionalField, Assertions
Expand Down Expand Up @@ -171,11 +171,11 @@ def _update_configuration_www_directory_path(www_directory_path: str) -> None:
www_directory_path_layout = QHBoxLayout()
www_directory_path_layout.addWidget(self._nginx_www_directory_path)

@catch_exceptions
def find_www_directory_path() -> None:
found_www_directory_path = QFileDialog.getExistingDirectory(self, 'Serve your site from...', directory=self._nginx_www_directory_path.text())
if '' != found_www_directory_path:
self._nginx_www_directory_path.setText(found_www_directory_path)
with ExceptionCatcher(self):
found_www_directory_path = QFileDialog.getExistingDirectory(self, 'Serve your site from...', directory=self._nginx_www_directory_path.text())
if '' != found_www_directory_path:
self._nginx_www_directory_path.setText(found_www_directory_path)
self._nginx_www_directory_path_find = QPushButton('...')
self._nginx_www_directory_path_find.released.connect(find_www_directory_path)
www_directory_path_layout.addWidget(self._nginx_www_directory_path_find)
Expand Down
116 changes: 58 additions & 58 deletions betty/gui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from betty.app import App
from betty.asyncio import sync, wait
from betty.gui import get_configuration_file_filter
from betty.gui.error import catch_exceptions
from betty.gui.error import ExceptionCatcher
from betty.gui.locale import TranslationsLocaleCollector
from betty.gui.serve import ServeDemoWindow, ServeDocsWindow
from betty.gui.text import Text
Expand Down Expand Up @@ -125,9 +125,9 @@ def _set_translatables(self) -> None:
self._docs_action.setText(self._app.localizer._('View documentation'))
self.about_action.setText(self._app.localizer._('About Betty'))

@catch_exceptions
def report_bug(self) -> None:
body = f'''
with ExceptionCatcher(self):
body = f'''
## Summary
## Steps to reproduce
Expand All @@ -139,83 +139,83 @@ def report_bug(self) -> None:
{report()}
```
'''.strip()
webbrowser.open_new_tab('https://github.com/bartfeenstra/betty/issues/new?' + urlencode({
'body': body,
'labels': 'bug',
}))
webbrowser.open_new_tab('https://github.com/bartfeenstra/betty/issues/new?' + urlencode({
'body': body,
'labels': 'bug',
}))

@catch_exceptions
def request_feature(self) -> None:
body = '''
with ExceptionCatcher(self):
body = '''
## Summary
## Expected behavior
'''.strip()
webbrowser.open_new_tab('https://github.com/bartfeenstra/betty/issues/new?' + urlencode({
'body': body,
'labels': 'enhancement',
}))
webbrowser.open_new_tab('https://github.com/bartfeenstra/betty/issues/new?' + urlencode({
'body': body,
'labels': 'enhancement',
}))

@catch_exceptions
def _docs(self) -> None:
serve_window = ServeDocsWindow(self._app, parent=self)
serve_window.show()
with ExceptionCatcher(self):
serve_window = ServeDocsWindow(self._app, parent=self)
serve_window.show()

@catch_exceptions
def _about_betty(self) -> None:
about_window = _AboutBettyWindow(self._app, parent=self)
about_window.show()
with ExceptionCatcher(self):
about_window = _AboutBettyWindow(self._app, parent=self)
about_window.show()

@catch_exceptions
def open_project(self) -> None:
from betty.gui.project import ProjectWindow
with ExceptionCatcher(self):
from betty.gui.project import ProjectWindow

configuration_file_path_str, __ = QFileDialog.getOpenFileName(
self,
self._app.localizer._('Open your project from...'),
'',
get_configuration_file_filter().localize(self._app.localizer),
)
if not configuration_file_path_str:
return
wait(self._app.project.configuration.read(Path(configuration_file_path_str)))
project_window = ProjectWindow(self._app)
project_window.show()
self.close()

configuration_file_path_str, __ = QFileDialog.getOpenFileName(
self,
self._app.localizer._('Open your project from...'),
'',
get_configuration_file_filter().localize(self._app.localizer),
)
if not configuration_file_path_str:
return
wait(self._app.project.configuration.read(Path(configuration_file_path_str)))
project_window = ProjectWindow(self._app)
project_window.show()
self.close()

@catch_exceptions
def new_project(self) -> None:
from betty.gui.project import ProjectWindow
with ExceptionCatcher(self):
from betty.gui.project import ProjectWindow

configuration_file_path_str, __ = QFileDialog.getSaveFileName(
self,
self._app.localizer._('Save your new project to...'),
'',
get_configuration_file_filter().localize(self._app.localizer),
)
if not configuration_file_path_str:
return
configuration = ProjectConfiguration()
wait(configuration.write(Path(configuration_file_path_str)))
project_window = ProjectWindow(self._app)
project_window.show()
self.close()

configuration_file_path_str, __ = QFileDialog.getSaveFileName(
self,
self._app.localizer._('Save your new project to...'),
'',
get_configuration_file_filter().localize(self._app.localizer),
)
if not configuration_file_path_str:
return
configuration = ProjectConfiguration()
wait(configuration.write(Path(configuration_file_path_str)))
project_window = ProjectWindow(self._app)
project_window.show()
self.close()

@catch_exceptions
def _demo(self) -> None:
serve_window = ServeDemoWindow(self._app, parent=self)
serve_window.show()
with ExceptionCatcher(self):
serve_window = ServeDemoWindow(self._app, parent=self)
serve_window.show()

@catch_exceptions
@sync
async def clear_caches(self) -> None:
await self._app.cache.clear()
async with ExceptionCatcher(self):
await self._app.cache.clear()

@catch_exceptions
def open_application_configuration(self) -> None:
window = ApplicationConfiguration(self._app, parent=self)
window.show()
with ExceptionCatcher(self):
window = ApplicationConfiguration(self._app, parent=self)
window.show()


class _WelcomeText(Text):
Expand Down
72 changes: 33 additions & 39 deletions betty/gui/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
"""
from __future__ import annotations

import functools
import traceback
from asyncio import CancelledError
from types import TracebackType
from typing import Callable, Any, TypeVar, Generic, TYPE_CHECKING, ParamSpec
from typing import Any, TypeVar, Generic, ParamSpec

from PyQt6.QtCore import QMetaObject, Qt, Q_ARG, QObject
from PyQt6.QtGui import QCloseEvent, QIcon
Expand All @@ -15,68 +15,62 @@
from betty.app import App
from betty.gui.locale import LocalizedObject

if TYPE_CHECKING:
from betty.gui import QWidgetT


T = TypeVar('T')
P = ParamSpec('P')

BaseExceptionT = TypeVar('BaseExceptionT', bound=BaseException)


class ExceptionCatcher(Generic[P, T]):
"""
Catch any exception and show an error window instead.
"""

_SUPPRESS_EXCEPTION_TYPES = (
CancelledError,
)

class _ExceptionCatcher(Generic[P, T]):
def __init__(
self,
f: Callable[P, T] | None = None,
parent: QWidget | None = None,
close_parent: bool = False,
instance: QWidget | None = None,
self,
parent: QObject,
*,
close_parent: bool = False,
):
if f:
functools.update_wrapper(self, f)
self._f = f
if close_parent and not parent:
raise ValueError('No parent was given to close.')
self._parent = instance if parent is None else parent
self._parent = parent
self._close_parent = close_parent
self._instance = instance

def __get__(self, instance: QWidgetT | None, owner: type[QWidgetT] | None = None) -> Any:
if instance is None:
return self
assert isinstance(instance, QWidget)
return type(self)(self._f, instance, self._close_parent, instance)

def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
if not self._f:
raise RuntimeError('This exception catcher is not callable, but you can use it as a context manager instead using a `with` statement.')
if self._instance is not None:
args = (self._instance, *args) # type: ignore[assignment]
with self:
return self._f(*args, **kwargs)

def __enter__(self) -> None:
pass

def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None) -> bool | None:
return self._catch(exc_type, exc_val)

async def __aenter__(self) -> None:
pass

async def __aexit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None) -> bool | None:
return self._catch(exc_type, exc_val)

def _catch(self, exception_type: type[BaseExceptionT] | None, exception: BaseExceptionT | None) -> bool | None:
from betty.gui import BettyApplication

if exc_val is None:
if exception_type is None or exception is None:
return None

if isinstance(exception, self._SUPPRESS_EXCEPTION_TYPES):
return None

QMetaObject.invokeMethod(
BettyApplication.instance(),
'_catch_exception',
Qt.ConnectionType.QueuedConnection,
Q_ARG(Exception, exc_val),
Q_ARG(Exception, exception),
Q_ARG(QObject, self._parent),
Q_ARG(bool, self._close_parent),
)
return True


# Alias the class so its original name follows the PEP code style, but the alias follows the decorator code style.
catch_exceptions = _ExceptionCatcher


class Error(LocalizedObject, QMessageBox):
def __init__(
self,
Expand Down
Loading

0 comments on commit 72ea42a

Please sign in to comment.