Skip to content

Commit

Permalink
Enable option to raise excpetion if magicgui cannot determine widget …
Browse files Browse the repository at this point in the history
…for provided value/annotation (#476)

* initial implementation

* add tests

* add argument to magic factory
  • Loading branch information
Czaki authored Oct 24, 2022
1 parent 9e29739 commit 12d788b
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 10 deletions.
5 changes: 5 additions & 0 deletions magicgui/_magicgui.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def magicgui(
main_window: bool = False,
app: AppRef = None,
persist: bool = False,
raise_on_unknown: bool = False,
**param_options: dict,
):
"""Return a :class:`FunctionGui` for ``function``.
Expand Down Expand Up @@ -65,6 +66,9 @@ def magicgui(
disk and restored when the widget is loaded again with ``persist = True``.
Call ``magicgui._util.user_cache_dir()`` to get the default cache location.
By default False.
raise_on_unknown : bool, optional
If ``True``, raise an error if magicgui cannot determine widget for function
argument or return type. If ``False``, ignore unknown types. By default False.
**param_options : dict of dict
Any additional keyword arguments will be used as parameter-specific options.
Expand Down Expand Up @@ -108,6 +112,7 @@ def magic_factory(
app: AppRef = None,
persist: bool = False,
widget_init: Callable[[FunctionGui], None] | None = None,
raise_on_unknown: bool = False,
**param_options: dict,
):
"""Return a :class:`MagicFactory` for ``function``."""
Expand Down
8 changes: 8 additions & 0 deletions magicgui/_magicgui.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def magicgui( # noqa
main_window: Literal[False] = False,
app: AppRef = None,
persist: bool = False,
raise_on_unknown: bool = False,
**param_options: dict,
) -> FunctionGui[_R]: ...
@overload # noqa: E302
Expand All @@ -51,6 +52,7 @@ def magicgui( # noqa
main_window: Literal[False] = False,
app: AppRef = None,
persist: bool = False,
raise_on_unknown: bool = False,
**param_options: dict,
) -> Callable[[Callable[..., _R]], FunctionGui[_R]]: ...
@overload # noqa: E302
Expand All @@ -67,6 +69,7 @@ def magicgui( # noqa
main_window: Literal[True],
app: AppRef = None,
persist: bool = False,
raise_on_unknown: bool = False,
**param_options: dict,
) -> MainFunctionGui[_R]: ...
@overload # noqa: E302
Expand All @@ -83,6 +86,7 @@ def magicgui( # noqa
main_window: Literal[True],
app: AppRef = None,
persist: bool = False,
raise_on_unknown: bool = False,
**param_options: dict,
) -> Callable[[Callable[..., _R]], MainFunctionGui[_R]]: ...
@overload # noqa: E302
Expand All @@ -100,6 +104,7 @@ def magic_factory( # noqa
app: AppRef = None,
persist: bool = False,
widget_init: Callable[[FunctionGui], None] | None = None,
raise_on_unknown: bool = False,
**param_options: dict,
) -> MagicFactory[FunctionGui[_R]]: ...
@overload # noqa: E302
Expand All @@ -117,6 +122,7 @@ def magic_factory( # noqa
app: AppRef = None,
persist: bool = False,
widget_init: Callable[[FunctionGui], None] | None = None,
raise_on_unknown: bool = False,
**param_options: dict,
) -> Callable[[Callable[..., _R]], MagicFactory[FunctionGui[_R]]]: ...
@overload # noqa: E302
Expand All @@ -134,6 +140,7 @@ def magic_factory( # noqa
app: AppRef = None,
persist: bool = False,
widget_init: Callable[[FunctionGui], None] | None = None,
raise_on_unknown: bool = False,
**param_options: dict,
) -> MagicFactory[MainFunctionGui[_R]]: ...
@overload # noqa: E302
Expand All @@ -151,5 +158,6 @@ def magic_factory( # noqa
app: AppRef = None,
persist: bool = False,
widget_init: Callable[[FunctionGui], None] | None = None,
raise_on_unknown: bool = False,
**param_options: dict,
) -> Callable[[Callable[..., _R]], MagicFactory[MainFunctionGui[_R]]]: ...
33 changes: 27 additions & 6 deletions magicgui/signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,11 @@ def __init__(
default: Any = inspect.Parameter.empty,
annotation: Any = inspect.Parameter.empty,
gui_options: dict = None,
raise_on_unknown: bool = False,
):
_annotation = make_annotated(annotation, gui_options)
super().__init__(name, kind, default=default, annotation=_annotation)
self.raise_on_unknown = raise_on_unknown

@property
def options(self) -> WidgetOptions:
Expand All @@ -118,7 +120,7 @@ def options(self) -> WidgetOptions:

def __repr__(self) -> str:
"""Return __repr__, replacing NoneType if present."""
rep = super().__repr__()[:-1] + f" {self.options}>"
rep = f"{super().__repr__()[:-1]} {self.options}>"
rep = rep.replace(": NoneType = ", "=")
return rep

Expand All @@ -144,6 +146,7 @@ def to_widget(self, app: AppRef = None) -> Widget:
annotation=annotation,
app=app,
options=options,
raise_on_unknown=self.raise_on_unknown,
)
widget.param_kind = self.kind
return widget
Expand All @@ -161,7 +164,10 @@ def from_widget(cls, widget: Widget) -> MagicParameter:

@classmethod
def from_parameter(
cls, param: inspect.Parameter, gui_options: dict = None
cls,
param: inspect.Parameter,
gui_options: dict = None,
raise_on_unknown: bool = False,
) -> MagicParameter:
"""Create MagicParameter from an inspect.Parameter."""
if isinstance(param, MagicParameter):
Expand All @@ -172,6 +178,7 @@ def from_parameter(
default=param.default,
annotation=param.annotation,
gui_options=gui_options,
raise_on_unknown=raise_on_unknown,
)


Expand All @@ -198,15 +205,20 @@ def __init__(
*,
return_annotation=inspect.Signature.empty,
gui_options: dict[str, dict] = None,
raise_on_unknown: bool = False,
):
params = [
MagicParameter.from_parameter(p, (gui_options or {}).get(p.name))
MagicParameter.from_parameter(
p, (gui_options or {}).get(p.name), raise_on_unknown
)
for p in parameters or []
]
super().__init__(params, return_annotation=return_annotation)

@classmethod
def from_signature(cls, sig: inspect.Signature, gui_options=None) -> MagicSignature:
def from_signature(
cls, sig: inspect.Signature, gui_options=None, raise_on_unknown=False
) -> MagicSignature:
"""Convert regular inspect.Signature to MagicSignature."""
if type(sig) is cls:
return cast(MagicSignature, sig)
Expand All @@ -216,6 +228,7 @@ def from_signature(cls, sig: inspect.Signature, gui_options=None) -> MagicSignat
list(sig.parameters.values()),
return_annotation=sig.return_annotation,
gui_options=gui_options,
raise_on_unknown=raise_on_unknown,
)

def widgets(self, app: AppRef = None) -> MappingProxyType:
Expand Down Expand Up @@ -254,7 +267,11 @@ def replace(


def magic_signature(
obj: Callable, *, gui_options: dict[str, dict] = None, follow_wrapped: bool = True
obj: Callable,
*,
gui_options: dict[str, dict] = None,
follow_wrapped: bool = True,
raise_on_unknown: bool = False,
) -> MagicSignature:
"""Create a MagicSignature from a callable object.
Expand All @@ -270,6 +287,8 @@ def magic_signature(
Will be passed to `MagicSignature.from_signature` by default None
follow_wrapped : bool, optional
passed to inspect.signature, by default True
raise_on_unknown : bool, optional
If True, raise an error if a parameter annotation is not recognized.
Returns
-------
Expand Down Expand Up @@ -297,4 +316,6 @@ def magic_signature(
s = "s" if len(bad) > 1 else ""
raise TypeError(f"Value for parameter{s} {bad} must be a dict")

return MagicSignature.from_signature(sig, gui_options=gui_options)
return MagicSignature.from_signature(
sig, gui_options=gui_options, raise_on_unknown=raise_on_unknown
)
13 changes: 12 additions & 1 deletion magicgui/type_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ def pick_widget_type(
annotation: type[Any] | None = None,
options: WidgetOptions | None = None,
is_result: bool = False,
raise_on_unknown: bool = True,
) -> WidgetTuple:
"""Pick the appropriate widget type for ``value`` with ``annotation``."""
if is_result and annotation is inspect.Parameter.empty:
Expand Down Expand Up @@ -208,6 +209,11 @@ def pick_widget_type(
_cls, opts = _widget_type
return _cls, {**options, **opts} # type: ignore

if raise_on_unknown:
raise ValueError(
f"No widget found for type {_type} and annotation {annotation}"
)

return widgets.EmptyWidget, {"visible": False}


Expand All @@ -216,6 +222,7 @@ def get_widget_class(
annotation: type[Any] | None = None,
options: WidgetOptions | None = None,
is_result: bool = False,
raise_on_unknown: bool = True,
) -> tuple[WidgetClass, WidgetOptions]:
"""Return a WidgetClass appropriate for the given parameters.
Expand All @@ -231,6 +238,8 @@ def get_widget_class(
is_result : bool, optional
Identifies whether the returned widget should be tailored to
an input or to an output.
raise_on_unknown : bool, optional
Raise exception if no widget is found for the given type, by default True
Returns
-------
Expand All @@ -240,7 +249,9 @@ def get_widget_class(
"""
_options = cast(WidgetOptions, options)

widget_type, _options = pick_widget_type(value, annotation, _options, is_result)
widget_type, _options = pick_widget_type(
value, annotation, _options, is_result, raise_on_unknown
)

if isinstance(widget_type, str):
widget_class: WidgetClass = _import_class(widget_type)
Expand Down
8 changes: 7 additions & 1 deletion magicgui/widgets/_bases/create_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def create_widget(
widget_type: str | type[_protocols.WidgetProtocol] | None = None,
options: WidgetOptions = dict(),
is_result: bool = False,
raise_on_unknown: bool = True,
):
"""Create and return appropriate widget subclass.
Expand Down Expand Up @@ -62,6 +63,8 @@ def create_widget(
is_result : boolean, optional
Whether the widget belongs to an input or an output. By defult, an input
is assumed.
raise_on_unknown : bool, optional
Raise exception if no widget is found for the given type, by default True
Returns
-------
Expand All @@ -77,6 +80,7 @@ def create_widget(
options = options.copy()
kwargs = locals().copy()
_kind = kwargs.pop("param_kind", None)
kwargs.pop("raise_on_unknown")
_is_result = kwargs.pop("is_result", None)
_app = use_app(kwargs.pop("app"))
assert _app.native
Expand All @@ -87,7 +91,9 @@ def create_widget(

if widget_type:
options["widget_type"] = widget_type
wdg_class, opts = get_widget_class(value, annotation, options, is_result)
wdg_class, opts = get_widget_class(
value, annotation, options, is_result, raise_on_unknown
)

if issubclass(wdg_class, Widget):
opts.update(kwargs.pop("options"))
Expand Down
6 changes: 5 additions & 1 deletion magicgui/widgets/_function_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ def __init__(
param_options: dict[str, dict] | None = None,
name: str = None,
persist: bool = False,
raise_on_unknown=False,
**kwargs,
):
if not callable(function):
Expand All @@ -151,7 +152,9 @@ def __init__(
elif not isinstance(param_options, dict):
raise TypeError("'param_options' must be a dict of dicts")

sig = magic_signature(function, gui_options=param_options)
sig = magic_signature(
function, gui_options=param_options, raise_on_unknown=raise_on_unknown
)
self.return_annotation = sig.return_annotation
self._tooltips = tooltips
if tooltips:
Expand Down Expand Up @@ -221,6 +224,7 @@ def _disable_button_and_call():
annotation=self._return_annotation,
gui_only=True,
is_result=True,
raise_on_unknown=raise_on_unknown,
)
self.append(self._result_widget)

Expand Down
30 changes: 29 additions & 1 deletion tests/test_magicgui.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ def get_layout_items(gui):

gui = magicgui(func, labels=labels)
assert get_layout_items(gui) == ["a", "b", "c", "call_button"]
gui.insert(1, widgets.create_widget(name="new"))
gui.insert(1, widgets.create_widget(name="new", raise_on_unknown=False))
assert get_layout_items(gui) == ["a", "new", "b", "c", "call_button"]


Expand Down Expand Up @@ -842,3 +842,31 @@ def test_nonscrollable(a: int = 1, y: str = "a"):

assert test_nonscrollable.native is test_nonscrollable.root_native_widget
assert not isinstance(test_nonscrollable.native, QScrollArea)


def test_unknown_exception_magicgui():
"""Test that an unknown type is raised as a RuntimeError."""

class A:
pass

with pytest.raises(ValueError, match="No widget found for type"):

@magicgui(raise_on_unknown=True)
def func(a: A):
print(a)


def test_unknown_exception_create_widget():
"""Test that an unknown type is raised as a RuntimeError."""

class A:
pass

with pytest.raises(ValueError, match="No widget found for type"):
widgets.create_widget(A, raise_on_unknown=True)
with pytest.raises(ValueError, match="No widget found for type"):
widgets.create_widget(A)
assert isinstance(
widgets.create_widget(A, raise_on_unknown=False), widgets.EmptyWidget
)

0 comments on commit 12d788b

Please sign in to comment.