from __future__ import annotations
import inspect
from typing import Any, Callable, Iterable, Union, TYPE_CHECKING, TypeVar, overload
import warnings
from magicgui.widgets import FunctionGui
from .utils import show_messagebox
from .types import Color
from .signature import get_additional_option, upgrade_signature
if TYPE_CHECKING:
from ._gui import BaseGui
nStrings = Union[str, Iterable[str]]
R = TypeVar("R")
T = TypeVar("T")
F = TypeVar("F", bound=Callable)
[docs]def set_options(
layout: str = "vertical",
labels: bool = True,
call_button: bool | str | None = None,
auto_call: bool = False,
**options,
) -> Callable[[F], F]:
"""
Set MagicSignature to functions.
By decorating a method with this function, ``magicgui`` will create a widget with these
options. These codes are similar in appearance.
.. code-block:: python
# A magicgui way
@magicgui(a={...})
def func(a):
...
# A magicclass way
@magicclass
class A:
@set_options(a={...})
def func(self, a):
...
Parameters
----------
layout : str, default is "vertical"
The type of layout to use in FunctionGui. Must be one of {'horizontal', 'vertical'}.
labels : bool, default is True
Whether labels are shown in the FunctionGui.
call_button : bool or str, optional
If ``True``, create an additional button that calls the original
function when clicked. If a ``str``, set the button text. If None (the
default), it defaults to True when ``auto_call`` is False, and False
otherwise.
auto_call : bool, optional
If ``True``, changing any parameter in either the GUI or the widget attributes
will call the original function with the current settings. by default False
options : dict
Parameter options.
"""
def wrapper(func: F) -> F:
sig = inspect.signature(func)
rem = options.keys() - sig.parameters.keys()
if rem:
warnings.warn(
f"Unknown arguments found in set_options of {func.__name__}: {rem}",
UserWarning,
)
upgrade_signature(
func,
gui_options=options,
additional_options={
"call_button": call_button,
"layout": layout,
"labels": labels,
"auto_call": auto_call,
},
)
return func
return wrapper
[docs]def set_design(
width: int | None = None,
height: int | None = None,
min_width: int | None = None,
min_height: int | None = None,
max_width: int | None = None,
max_height: int | None = None,
text: str | None = None,
icon: str | None = None,
font_size: int | None = None,
font_family: int | None = None,
font_color: Color | None = None,
background_color: Color | None = None,
visible: bool | None = None,
) -> Callable[[type[T]], type[T]] | Callable[[F], F]:
"""
Change button/action design by calling setter when the widget is created.
Parameters
----------
width : int, optional
Button width. Call ``button.width = width``.
height : int, optional
Button height. Call ``button.height = height``.
min_width : int, optional
Button minimum width. Call ``button.min_width = min_width``.
min_height : int, optional
Button minimum height. Call ``button.min_height = min_height``.
max_width : int, optional
Button maximum width. Call ``button.max_width = max_width``.
max_height : int, optional
Button maximum height. Call ``button.max_height = max_height``.
text : str, optional
Button text. Call ``button.text = text``.
icon : str, optional
Path to icon file. ``min_width`` and ``min_height`` will be automatically set to the icon size
if not given.
font_size : int, optional
Font size of the text.
visible : bool default is True
Button visibility.
"""
caller_options = locals()
caller_options = {k: v for k, v in caller_options.items() if v is not None}
def wrapper(obj):
if isinstance(obj, type):
_post_init = getattr(obj, "__post_init__", lambda self: None)
def __post_init__(self):
_post_init(self)
for k, v in caller_options.items():
setattr(self, k, v)
obj.__post_init__ = __post_init__
else:
upgrade_signature(obj, caller_options=caller_options)
return obj
return wrapper
[docs]def do_not_record(method: F) -> F:
"""Wrapped method will not be recorded in macro."""
upgrade_signature(method, additional_options={"record": False})
return method
[docs]def bind_key(*key) -> Callable[[F], F]:
"""
Define a keybinding to a button or an action.
This function accepts several styles of shortcut expression.
>>> @bind_key("Ctrl-A") # napari style
>>> @bind_key("Ctrl", "A") # separately
>>> @bind_key(Key.Ctrl + Key.A) # use Key class
>>> @bind_key(Key.Ctrl, Key.A) # use Key class separately
"""
if isinstance(key[0], tuple):
key = key[0]
def wrapper(method: F) -> F:
upgrade_signature(method, additional_options={"keybinding": key})
return method
return wrapper
[docs]class Canceled(RuntimeError):
"""Raised when a function is canceled"""
@overload
def confirm(
*,
text: str | None,
condition: Callable[..., bool] | str | None,
callback: Callable[[str, BaseGui], None] | None = None,
) -> Callable[[F], F]:
...
@overload
def confirm(
f: F,
*,
text: str | None,
condition: Callable[..., bool] | str | None,
callback: Callable[[str, BaseGui], None] | None = None,
) -> F:
...
[docs]def confirm(
f: F | None = None,
*,
text: str | None = None,
condition: Callable[[BaseGui], bool] | str = None,
callback: Callable[[str, BaseGui], None] | None = None,
):
"""
Confirm if it is OK to run function in GUI.
Useful when the function will irreversibly delete or update something in GUI.
Confirmation will be executed only when function is called in GUI.
Parameters
----------
text : str, optional
Confirmation message, such as "Are you sure to run this function?". Format
string can also be used here, in which case arguments will be passed. For
instance, to execute confirmation on function ``f(a, b)``, you can use
format string ``"Running with a = {a} and b = {b}"`` then confirmation
message will be "Running with a = 1, b = 2" if ``f(1, 2)`` is called.
By default, message will be "Do you want to run {name}?" where "name" is
the function name.
condition : callable or str, optional
Condition of when confirmation will show up. If callable, it must accept
``condition(self)`` and return boolean object. If string, it must be
evaluable as literal with input arguments as local namespace. For instance,
function ``f(a, b)`` decorated by ``confirm(condition="a < b + 1")`` will
evaluate ``a < b + 1`` to check if confirmation is needed. Always true by
default.
callback : callable, optional
Callback function when confirmation is needed. Must take a ``str`` and a
``BaseGui`` object as inputs. By default, message box will be shown. Useful
for testing.
"""
if condition is None:
condition = lambda x: True
if callback is None:
callback = _default_confirmation
def _decorator(method: F) -> F:
_name = method.__name__
# set text
if text is None:
_text = f"Do you want to run {_name}?"
elif isinstance(text, str):
_text = text
else:
raise TypeError(
f"The first argument of 'confirm' must be a str but got {type(text)}."
)
upgrade_signature(
method,
additional_options={
"confirm": {
"text": _text,
"condition": condition,
"callback": callback,
}
},
)
return method
if f is not None:
return _decorator(f)
return _decorator
def _default_confirmation(text: str, gui: BaseGui):
ok = show_messagebox(
mode="question",
title="Confirmation",
text=text,
parent=gui.native,
)
if not ok:
raise Canceled("Canceled")
[docs]def nogui(method: F) -> F:
"""Wrapped method will not be converted into a widget."""
upgrade_signature(method, additional_options={"gui": False})
return method
[docs]def mark_preview(function: Callable, text: str = "Preview") -> Callable[[F], F]:
"""
Define a preview of a function.
This decorator is useful for advanced magicgui creation. A "Preview" button
appears in the bottom of the widget built from the input function and the
decorated function will be called with the same arguments.
Following example shows how to define a previewer that prints the content of
the selected file.
.. code-block:: python
def func(self, path: Path):
...
@mark_preview(func)
def _func_prev(self, path: Path):
with open(path, mode="r") as f:
print(f.read())
Parameters
----------
function : callable
To which function previewer will be defined.
text : str, optional
Text of preview button.
"""
def _wrapper(preview: F) -> F:
sig_preview = inspect.signature(preview)
sig_func = inspect.signature(function)
params_preview = sig_preview.parameters
params_func = sig_func.parameters
less = len(params_func) - len(params_preview)
if less == 0:
if params_preview.keys() != params_func.keys():
raise TypeError(
f"Arguments mismatch between {sig_preview!r} and {sig_func!r}."
)
# If argument names are identical, input arguments don't have to be filtered.
_filter = lambda a: a
elif less > 0:
idx: list[int] = []
for i, param in enumerate(params_func.keys()):
if param in params_preview:
idx.append(i)
# If argument names are not identical, input arguments have to be filtered so
# that arguments match the inputs.
_filter = lambda _args: (a for i, a in enumerate(_args) if i in idx)
else:
raise TypeError(
f"Number of arguments of preview function {preview!r} must be equal"
f"or smaller than that of running function {function!r}."
)
def _preview(*args):
# find proper parent instance in the case of classes being nested
from ._gui import BaseGui
if len(args) > 0 and isinstance(args[0], BaseGui):
ins = args[0]
prev_ns = preview.__qualname__.split(".")[-2]
while ins.__class__.__name__ != prev_ns:
ins = ins.__magicclass_parent__
args = (ins,) + args[1:]
# filter input arguments
return preview(*_filter(args))
if not isinstance(function, FunctionGui):
upgrade_signature(
function, additional_options={"preview": (text, _preview)}
)
else:
from ._gui._function_gui import append_preview
append_preview(function, _preview, text=text)
return preview
return _wrapper
_Fn = TypeVar("_Fn", bound=Callable[[FunctionGui], Any])
[docs]def mark_on_calling(function: Callable) -> Callable[[_Fn], _Fn]:
def _wrapper(on_calling: _Fn) -> _Fn:
if opt := get_additional_option(function, "on_calling", None):
opt.append(on_calling)
else:
upgrade_signature(function, additional_options={"on_calling": [on_calling]})
return on_calling
return _wrapper
[docs]def mark_on_called(function: Callable) -> Callable[[_Fn], _Fn]:
def _wrapper(on_called: _Fn) -> _Fn:
if opt := get_additional_option(function, "on_called", None):
opt.append(on_called)
else:
upgrade_signature(function, additional_options={"on_called": [on_called]})
return on_called
return _wrapper