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

Enable option to raise exception if magicgui cannot determine widget for provided value/annotation #476

Merged
merged 3 commits into from
Oct 24, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 4 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
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
2 changes: 1 addition & 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