Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a method to temporarily convert GUI error mode. #111

Merged
merged 1 commit into from
Jul 29, 2023
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
45 changes: 45 additions & 0 deletions magicclass/_gui/_base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from __future__ import annotations
from contextlib import contextmanager
import functools
from typing import (
Any,
ContextManager,
Union,
Callable,
TYPE_CHECKING,
Expand Down Expand Up @@ -672,6 +674,49 @@ def vfield(
fld.set_destination(cls)
return fld

@contextmanager
def config_context(
self,
error_mode=None,
close_on_run=None,
recursive: bool = True,
):
"""
Context manager to temporarily change the configuration of the widget.

Parameters
----------
error_mode : ErrorMode, optional
Error mode to use.
"""
if error_mode is not None:
old_error_mode = self._error_mode
self._error_mode = ErrorMode(error_mode)
else:
old_error_mode = self._popup_mode
if close_on_run is not None:
old_close_on_run = self._close_on_run
else:
old_close_on_run = self._close_on_run

child_contexts: list[ContextManager] = []
try:
if recursive:
for child in self.__magicclass_children__:
ctx = child.config_context(
error_mode=error_mode,
close_on_run=close_on_run,
recursive=True,
)
ctx.__enter__()
child_contexts.append(ctx)
yield
finally:
for ctx in child_contexts:
ctx.__exit__(None, None, None)
self._error_mode = old_error_mode
self._close_on_run = old_close_on_run

def _convert_attributes_into_widgets(self):
"""
This function is called in dynamically created __init__. Methods, fields and nested
Expand Down
25 changes: 14 additions & 11 deletions magicclass/_gui/_gui_modes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

from types import TracebackType
from contextlib import contextmanager
from typing import Callable
from typing import Callable, TYPE_CHECKING
from enum import Enum
import functools
from magicgui.widgets import Widget
from .mgui_ext import FunctionGuiPlus
from magicclass._exceptions import Canceled

if TYPE_CHECKING:
from ._base import BaseGui


class PopUpMode(Enum):
"""Define how to popup FunctionGui."""
Expand Down Expand Up @@ -65,11 +68,7 @@ def _msgbox_raising(e: Exception, parent: Widget):


def _stderr_raising(e: Exception, parent: Widget):
pass


def _stdout_raising(e: Exception, parent: Widget):
print(f"{e.__class__.__name__}: {e}")
raise e


def _napari_notification_raising(e: Exception, parent: Widget):
Expand All @@ -94,17 +93,19 @@ class ErrorMode(Enum):
stdout = "stdout"
napari = "napari"
debug = "debug"
ignore = "ignore"

def get_handler(self):
"""Get error handler."""
return ErrorModeHandlers[self]

def wrap_handler(self, func: Callable, parent):
@classmethod
def wrap_handler(cls, func: Callable, parent: BaseGui):
"""Wrap function with the error handler."""
handler = self.get_handler()

@functools.wraps(func)
def wrapped_func(*args, **kwargs):
handler = parent._error_mode.get_handler()
try:
out = func(*args, **kwargs)
except Canceled as e:
Expand All @@ -117,21 +118,23 @@ def wrapped_func(*args, **kwargs):

return wrapped_func

@classmethod
@contextmanager
def raise_with_handler(self, parent):
def raise_with_handler(cls, parent: BaseGui):
"""Raise error with the error handler in this context."""
try:
yield
except Exception as e:
self.get_handler()(e, parent=parent)
parent._error_mode.get_handler()(e, parent=parent)


ErrorModeHandlers = {
ErrorMode.msgbox: _msgbox_raising,
ErrorMode.stderr: _stderr_raising,
ErrorMode.stdout: _stdout_raising,
ErrorMode.stdout: lambda e, parent: print(f"{e.__class__.__name__}: {e}"),
ErrorMode.napari: _napari_notification_raising,
ErrorMode.debug: _debug_raising,
ErrorMode.ignore: lambda e, parent: None,
}


Expand Down
27 changes: 18 additions & 9 deletions magicclass/testing/_function_gui_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def __init__(self, method: Callable[_P, _R]):
self._fgui = get_function_gui(method)
# NOTE: if the widget is in napari etc., choices depend on the parent.
ui: BaseGui = method.__self__
self._magicclass_gui = ui
self._fgui.native.setParent(ui.native, self._fgui.native.windowFlags())
self._fgui.reset_choices()
self._method = method
Expand All @@ -66,13 +67,16 @@ def __init__(self, method: Callable[_P, _R]):

@property
def has_preview(self) -> bool:
"""True if the method has preview function."""
return self._prev_func is not None

@property
def has_confirmation(self) -> bool:
"""True if the method has confirmation function."""
return self._conf_dict is not None

def click_preview(self):
"""Emulate the preview button click."""
if not self.has_preview:
raise RuntimeError("No preview function found.")
if self._fgui._auto_call:
Expand All @@ -88,25 +92,30 @@ def click_preview(self):
prev_widget.changed.emit(True)

def update_parameters(self, **kwargs):
"""Update the parameters of the function GUI."""
self._fgui.update(**kwargs)

def call(self, *args: _P.args, **kwargs: _P.kwargs):
if not self.has_confirmation:
return self._fgui(*args, **kwargs)
cb = self._conf_dict["callback"]
self._conf_dict["callback"] = self._mock_confirmation
try:
out = self._fgui(*args, **kwargs)
finally:
self._conf_dict["callback"] = cb
return out
"""Call the method as if it is called from the GUI."""
with self._magicclass_gui.config_context(error_mode="stderr"):
if not self.has_confirmation:
return self._fgui(*args, **kwargs)
cb = self._conf_dict["callback"]
self._conf_dict["callback"] = self._mock_confirmation
try:
out = self._fgui(*args, **kwargs)
finally:
self._conf_dict["callback"] = cb
return out

@property
def confirm_count(self) -> int:
"""Number of times the confirmation is called."""
return self._n_confirm

@property
def call_count(self) -> int:
"""Number of times the method is called."""
return self._fgui.call_count

def _mock_confirmation(self, *_, **__):
Expand Down
12 changes: 12 additions & 0 deletions tests/test_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,15 @@ def _preview(self, x: int):
assert hist == [0, 1, 0, 1]
testf.update_parameters(x=4)
assert hist == [0, 1, 0, 1]

def test_error_detectable():
@magicclass
class A:
def f(self, raises=True):
if raises:
raise ValueError("error")

ui = A()
testf = FunctionGuiTester(ui.f)
with pytest.raises(ValueError):
testf.call()