from __future__ import annotations
from typing import (
Any,
TYPE_CHECKING,
Callable,
TypeVar,
overload,
Generic,
Union,
)
from typing_extensions import Literal, _AnnotatedAlias
from pathlib import Path
import datetime
import sys
from enum import Enum
from magicgui.widgets import create_widget
from magicgui.widgets._bases import Widget, ValueWidget, ContainerWidget
from magicgui.widgets._bases.value_widget import UNSET
from ..utils import (
argcount,
is_instance_method,
method_as_getter,
eval_attribute,
)
from .._gui.mgui_ext import Action, WidgetAction
if TYPE_CHECKING:
from magicgui.widgets._protocols import WidgetProtocol
from typing_extensions import Self
from .._gui._base import MagicTemplate
from .._gui.mgui_ext import AbstractAction
_M = TypeVar("_M", bound=MagicTemplate)
if sys.version_info >= (3, 10):
from typing import _BaseGenericAlias
else:
from typing_extensions import _BaseGenericAlias
class _FieldObject:
name: str
tooltip: str
def get_widget(self, obj: Any) -> Widget:
raise NotImplementedError()
_W = TypeVar("_W", bound=Widget)
_V = TypeVar("_V", bound=object)
class MagicField(_FieldObject, Generic[_W, _V]):
"""
Field class for magicgui construction.
This object is compatible with dataclass. MagicField object is in "ready for
widget construction" state.
"""
default_object = object()
def __init__(
self,
value: Any = UNSET,
name: str | None = None,
label: str | None = None,
annotation: Any = None,
widget_type: type | str | None = None,
options: dict[str, Any] | None = None,
record: bool = True,
constructor: Callable[..., Widget] | None = None,
):
if options is None:
options = {}
if value is UNSET:
value = options.pop("value", UNSET)
if constructor is None:
def _create_widget(obj):
return create_widget(
self.value,
name=self.name,
label=self.label,
annotation=self.annotation,
widget_type=self.widget_type,
options=self.options,
)
constructor = _create_widget
self.value = value
self.name = name
self.label = label
self.annotation = annotation
self.options = options
self._widget_type = widget_type
self._constructor = constructor
self._callbacks: list[Callable] = []
self._guis: dict[int, _M] = {}
self._record = record
# MagicField has to remenber the first class that referred to itself so
# that it can "know" the namespace it belongs to.
self._parent_class: type | None = None
def __repr__(self):
attrs = ["value", "name", "widget_type", "record", "options"]
kw = ", ".join(f"{a}={getattr(self, a)!r}" for a in attrs)
return f"{self.__class__.__name__}({kw})"
def __set_name__(self, owner: type, name: str) -> None:
self._parent_class = owner
if self.name is None:
self.name = name
@property
def constructor(self) -> Callable[..., Widget]:
"""Get widget constructor."""
return self._constructor
@property
def callbacks(self) -> tuple[Callable, ...]:
"""Return callbacks in an immutable way."""
return tuple(self._callbacks)
@property
def record(self) -> bool:
return self._record
def construct(self, obj) -> Widget:
"""Construct a widget."""
constructor = self.constructor
_arg_choices = self.options.get("choices", None)
if isinstance(_arg_choices, str):
_arg_choices = eval_attribute(type(obj), _arg_choices)
if is_instance_method(_arg_choices):
self.options["choices"] = method_as_getter(obj, _arg_choices)
try:
if _is_subclass(constructor, Widget):
widget = constructor(**self.options)
else:
widget = constructor(obj)
if not isinstance(widget, Widget):
raise TypeError(
f"{self.__class__.__name__} {self.name} created non-widget "
f"object {type(widget)}."
)
finally:
if _arg_choices is not None:
self.options["choices"] = _arg_choices
return widget
def copy(self) -> Self[_W, _V]:
"""Copy object."""
return self.__class__(
value=self.value,
name=self.name,
label=self.label,
annotation=self.annotation,
widget_type=self.widget_type,
options=self.options,
record=self.record,
constructor=self.constructor,
)
@classmethod
def from_callable(cls, func: Callable[..., _W]) -> Self[_W, Any]:
"""Use a function as the constructor of MagicField."""
return cls(constructor=func)
def get_widget(self, obj: Any) -> _W:
"""
Get a widget from ``obj``. This function will be called every time MagicField is referred
by ``obj.field``.
"""
from .._gui import MagicTemplate
obj_id = id(obj)
if obj_id in self._guis.keys():
widget = self._guis[obj_id]
else:
widget = self.construct(obj)
widget.name = self.name
self._guis[obj_id] = widget
if isinstance(widget, (ValueWidget, ContainerWidget)):
if isinstance(obj, MagicTemplate):
_def = _define_callback_gui
else:
_def = _define_callback
for callback in self._callbacks:
# funcname = callback.__name__
widget.changed.connect(_def(obj, callback))
return widget
def get_action(self, obj: Any) -> AbstractAction:
"""
Get an action from ``obj``. This function will be called every time MagicField is referred
by ``obj.field``.
"""
from .._gui import MagicTemplate
obj_id = id(obj)
if obj_id in self._guis.keys():
action = self._guis[obj_id]
else:
if type(self.value) is bool or self.annotation is bool:
# we should not use "isinstance" or "issubclass" because subclass
# may be mapped to different widget by users.
value = False if self.value is UNSET else self.value
action = Action(
checkable=True,
checked=value,
text=self.name.replace("_", " "),
name=self.name,
)
for k, v in self.options.items():
setattr(action, k, v)
else:
widget = self.construct(obj)
widget.name = self.name
action = WidgetAction(widget)
self._guis[obj_id] = action
if action.support_value:
if isinstance(obj, MagicTemplate):
_def = _define_callback_gui
else:
_def = _define_callback
for callback in self._callbacks:
# funcname = callback.__name__
action.changed.connect(_def(obj, callback))
return action
def as_getter(self, obj: Any) -> Callable[[Any], _V]:
"""Make a function that get the value of Widget or Action."""
return lambda w: self._guis[id(obj)].value
def as_remote_getter(self, obj: Any):
"""Called when a MagicField is used in Bound method."""
qualname = self._parent_class.__qualname__
_LOCALS = "<locals>."
if _LOCALS in qualname:
qualname = qualname.split(_LOCALS)[-1]
clsnames = qualname.split(".")
def _func(w):
# First we have to know where (which instance) MagicField came from.
if obj.__class__.__name__ not in clsnames:
ns = ".".join(clsnames)
raise ValueError(
f"Method {self.name!r} is in namespace {ns!r}, so it is invisible "
f"from magicclass {obj.__class__.__qualname__!r}."
)
i = clsnames.index(type(obj).__name__) + 1
ins = obj
for clsname in clsnames[i:]:
ins = getattr(ins, clsname, ins)
# Now, ins is an instance of parent class.
# Extract correct widget from MagicField
_field_widget = self.get_widget(ins)
if not hasattr(_field_widget, "value"):
raise TypeError(
f"MagicField {self.name} does not return ValueWidget "
"thus cannot be used as a bound value."
)
return self.as_getter(ins)(w)
return _func
@overload
def __get__(self, obj: Literal[None], objtype=None) -> MagicField[_W, _V]:
...
@overload
def __get__(self, obj: Any, objtype=None) -> _W:
...
def __get__(self, obj, objtype=None):
"""Get widget for the object."""
if obj is None:
return self
return self.get_widget(obj)
def __set__(self, obj, value) -> None:
raise AttributeError(f"Cannot set value to {self.__class__.__name__}.")
def ready(self) -> bool:
"""Return true if field is ready to create widgets."""
return not self.not_ready()
def not_ready(self) -> bool:
return (
self.value is UNSET
and self.annotation is None
and self.widget_type is None
and "choices" not in self.options
)
def to_widget(self) -> _W:
"""
Create a widget from the field.
Returns
-------
Widget
Widget object that is ready to be inserted into Container.
Raises
------
ValueError
If there is not enough information to build a widget.
"""
return self.get_widget(self.default_object)
def to_action(self) -> Action | WidgetAction[_W]:
"""
Create a menu action or a menu widget action from the field.
Returns
-------
Action or WidgetAction
Object that can be added to menu.
Raises
------
ValueError
If there is not enough information to build an action.
"""
return self.get_action(self.default_object)
def connect(self, func: Callable) -> Callable:
"""Set callback function to "ready to connect" state."""
if not callable(func):
raise TypeError("Cannot connect non-callable object")
self._callbacks.append(func)
return func
def disconnect(self, func: Callable) -> None:
"""
Disconnect callback from the field.
This method does NOT disconnect callbacks from widgets that are
already created.
"""
i = self._callbacks.index(func)
self._callbacks.pop(i)
return None
def wraps(
self,
method: Callable | None = None,
*,
template: Callable | None = None,
copy: bool = False,
):
"""
Call the ``wraps`` class method of magic class.
This method is needed when a child magic class is defined outside the main magic
class, and integrated into the main magic class by ``field`` function, like below
.. code-block:: python
@magicclass
class B:
def func(self): ... # pre-definition
@magicclass
class A:
b = field(B)
@b.wraps
def func(self):
# do something
Parameters
----------
method : Callable, optional
Method of parent class.
template : Callable, optional
Function template for signature.
copy: bool, default is False
If true, wrapped method is still enabled.
Returns
-------
Callable
Same method as input, but has updated signature.
"""
from .._gui import BaseGui
cls = self.constructor
if not (isinstance(cls, type) and issubclass(cls, BaseGui)):
raise TypeError(
"The wraps method cannot be used for any objects but magic class."
)
return cls.wraps(method=method, template=template, copy=copy)
@property
def tooltip(self) -> str:
"""Get tooltip of returned widgets."""
return self.options.get("tooltip", "")
@tooltip.setter
def tooltip(self, value):
self.options.update(tooltip=value)
@property
def enabled(self) -> bool:
"""Get interactivity of returned widgets."""
return self.options.get("enabled", True)
@enabled.setter
def enabled(self, value: bool):
self.options.update(enabled=value)
@property
def visible(self) -> bool:
"""Get visibility of returned widgets."""
return self.options.get("visible", True)
@visible.setter
def visible(self, value: bool):
self.options.update(visible=value)
@property
def widget_type(self) -> type[Widget]:
"""Return type of the resulting widget."""
return self._widget_type
class MagicValueField(MagicField[_W, _V]):
"""
Field class for magicgui construction. Unlike MagicField, object of this class
always returns value itself.
"""
@overload
def __get__(self, obj: Literal[None], objtype=None) -> MagicValueField[_W, _V] | _V:
...
@overload
def __get__(self, obj: Any, objtype=None) -> _V:
...
def __get__(self, obj, objtype=None):
if obj is None:
return self
return self._postgethook(obj, self.get_widget(obj).value)
def __set__(self, obj: _M, value: _V) -> None:
if obj is None:
raise AttributeError(f"Cannot set {self.__class__.__name__}.")
self.get_widget(obj).value = self._presethook(obj, value)
def post_get_hook(self, hook: Callable[[Any, _V], Any] | Callable[[_V], Any]):
"""
Define a post-get hook for the field.
If a post-get hook is set, value will always be converted before returned.
Following example shows how to convert ``x`` to a float every time it gets
accessed.
>>> class A:
>>> x = vfield(int)
>>> @x.post_get_hook
>>> def _x_get(self, value):
>>> return float(int)
Parameters
----------
hook : callable
Post-get hook function.
"""
if not callable(hook):
raise TypeError("Post-get hook must be callable.")
if is_instance_method(hook):
self._postgethook = hook
else:
self._postgethook = lambda _, x: hook(x)
return hook
def pre_set_hook(self, hook: Callable[[Any, Any], _V] | Callable[[Any], _V]):
"""
Define a pre-set hook for the field.
If a pre-set hook is set, value will always be converted before being set
to the widget value. Following example shows how to convert ``x`` to a
string before setting the value
>>> class A:
>>> x = vfield(str)
>>> @x.pre_set_hook
>>> def _x_set(self, value):
>>> return str(value)
Parameters
----------
hook : callable
Pre-set hook function.
"""
if not callable(hook):
raise TypeError("Pre-set hook must be callable.")
if is_instance_method(hook):
self._presethook = hook
else:
self._presethook = lambda _, x: hook(x)
return hook
def _postgethook(self, obj, value):
return value
def _presethook(self, obj, value):
return value
# magicgui symple types
_X = TypeVar(
"_X",
bound=Union[
int,
float,
bool,
str,
Path,
datetime.datetime,
datetime.date,
datetime.time,
Enum,
range,
slice,
list,
tuple,
],
)
@overload
def field(
obj: _X,
*,
name: str | None = None,
label: str | None = None,
widget_type: str | type[WidgetProtocol] | type[Widget] | None = None,
options: dict[str, Any] = {},
record: bool = True,
) -> MagicField[ValueWidget, _X]:
...
@overload
def field(
widget_type: type[_W],
*,
name: str | None = None,
label: str | None = None,
options: dict[str, Any] = {},
record: bool = True,
) -> MagicField[_W, Any]:
...
@overload
def field(
obj: type[_X],
*,
name: str | None = None,
label: str | None = None,
widget_type: str | type[WidgetProtocol] | type[Widget] | None = None,
options: dict[str, Any] = {},
record: bool = True,
) -> MagicField[ValueWidget, _X]:
...
@overload
def field(
gui_class: type[_M],
*,
name: str | None = None,
label: str | None = None,
widget_type: str | type[WidgetProtocol] | type[Widget] | None = None,
options: dict[str, Any] = {},
record: bool = True,
) -> MagicField[_M, Any]:
...
@overload
def field(
obj: Any,
*,
name: str | None = None,
label: str | None = None,
widget_type: type[_W] = None,
options: dict[str, Any] = {},
record: bool = True,
) -> MagicField[_W, Any]:
...
@overload
def field(
obj: Any,
*,
name: str | None = None,
label: str | None = None,
widget_type: str | type[WidgetProtocol] | None = None,
options: dict[str, Any] = {},
record: bool = True,
) -> MagicField[Widget, Any]:
...
[docs]def field(
obj: Any = UNSET,
*,
name: str | None = None,
label: str | None = None,
widget_type: str | type[WidgetProtocol] | None = None,
options: dict[str, Any] = {},
record: bool = True,
) -> MagicField[Widget, Any]:
"""
Make a MagicField object.
>>> i = field(1)
>>> i = field(widget_type="Slider")
Parameters
----------
obj : Any, default is UNSET
Reference to determine what type of widget will be created. If Widget
subclass is given, it will be used as is. If other type of class is given,
it will used as type annotation. If an object (not type) is given, it will
be assumed to be the default value.
name : str, optional
Name of the widget.
label : str, optional
Label of the widget.
widget_type : str, optional
Widget type. This argument will be sent to ``create_widget`` function.
options : WidgetOptions, optional
Widget options. This parameter will be passed to the ``options`` keyword
argument of ``create_widget``.
record : bool, default is True
A magic-class specific parameter. If true, record value changes as macro.
Returns
-------
MagicField
"""
return _get_field(obj, name, label, widget_type, options, record, MagicField)
@overload
def vfield(
obj: _X,
*,
name: str | None = None,
label: str | None = None,
widget_type: str | type[WidgetProtocol] | type[Widget] | None = None,
options: dict[str, Any] = {},
record: bool = True,
) -> MagicValueField[ValueWidget, _X]:
...
@overload
def vfield(
widget_type: type[_W],
*,
name: str | None = None,
label: str | None = None,
options: dict[str, Any] = {},
record: bool = True,
) -> MagicValueField[_W, Any]:
...
@overload
def vfield(
annotation: type[_X],
*,
name: str | None = None,
label: str | None = None,
widget_type: str | type[WidgetProtocol] | type[Widget] | None = None,
options: dict[str, Any] = {},
record: bool = True,
) -> MagicValueField[ValueWidget, _X]:
...
@overload
def vfield(
obj: Any,
*,
name: str | None = None,
label: str | None = None,
widget_type: type[_W] = None,
options: dict[str, Any] = {},
record: bool = True,
) -> MagicValueField[_W, Any]:
...
@overload
def vfield(
obj: Any,
*,
name: str | None = None,
label: str | None = None,
widget_type: str | type[WidgetProtocol] | type[Widget] | None = None,
options: dict[str, Any] = {},
record: bool = True,
) -> MagicValueField[Widget, Any]:
...
[docs]def vfield(
obj: Any = UNSET,
*,
name: str | None = None,
label: str | None = None,
widget_type: str | type[WidgetProtocol] | None = None,
options: dict[str, Any] = {},
record: bool = True,
) -> MagicValueField[Widget, Any]:
"""
Make a MagicValueField object.
>>> i = vfield(1)
>>> i = vfield(widget_type="Slider")
Unlike MagicField, value itself can be accessed.
>>> ui.i # int is returned
>>> ui.i = 3 # set value to the widget.
Parameters
----------
obj : Any, default is UNSET
Reference to determine what type of widget will be created. If Widget
subclass is given, it will be used as is. If other type of class is given,
it will used as type annotation. If an object (not type) is given, it will
be assumed to be the default value.
name : str, optional
Name of the widget.
label : str, optional
Label of the widget.
widget_type : str, optional
Widget type. This argument will be sent to ``create_widget`` function.
options : WidgetOptions, optional
Widget options. This parameter will be passed to the ``options`` keyword
argument of ``create_widget``.
record : bool, default is True
A magic-class specific parameter. If true, record value changes as macro.
Returns
-------
MagicValueField
"""
return _get_field(obj, name, label, widget_type, options, record, MagicValueField)
def _get_field(
obj,
name: str,
label: str,
widget_type: str | type[WidgetProtocol] | None,
options: dict[str, Any],
record: bool,
field_class: type[MagicField],
) -> MagicField:
if not isinstance(options, dict):
raise TypeError(f"Field options must be a dict, got {type(options)}")
options = options.copy()
kwargs = dict(
name=name, label=label, record=record, annotation=None, options=options
)
if isinstance(obj, (type, _BaseGenericAlias)):
if isinstance(obj, _AnnotatedAlias):
from magicgui.signature import split_annotated_type
tp, widget_option = split_annotated_type(obj)
kwargs.update(annotation=tp)
options.update(**widget_option)
if _is_subclass(obj, Widget):
if widget_type is not None:
raise ValueError("Cannot specify Widget type twice.")
f = field_class(constructor=obj, widget_type=obj, **kwargs)
else:
if kwargs["annotation"] is None:
kwargs.update(annotation=obj)
f = field_class(widget_type=widget_type, **kwargs)
elif obj is UNSET:
f = field_class(widget_type=widget_type, **kwargs)
else:
f = field_class(value=obj, widget_type=widget_type, **kwargs)
return f
def _is_subclass(obj: Any, class_or_tuple):
try:
return issubclass(obj, class_or_tuple)
except Exception:
return False
def _define_callback(self: Any, callback: Callable):
"""Define a callback function from a method."""
return callback.__get__(self)
def _define_callback_gui(self: MagicTemplate, callback: Callable):
"""Define a callback function from a method of a magic-class."""
*_, clsname, funcname = callback.__qualname__.split(".")
mro = self.__class__.__mro__
for base in mro:
if base.__name__ == clsname:
_func: Callable = getattr(base, funcname).__get__(self)
_func = _normalize_argcount(_func)
def _callback(v):
with self.macro.blocked():
_func(v)
return None
break
else:
def _callback(v):
# search for parent instances that have the same name.
current_self = self
while not (
hasattr(current_self, funcname)
and current_self.__class__.__qualname__.split(".")[-1] == clsname
):
current_self = current_self.__magicclass_parent__
_func = _normalize_argcount(getattr(current_self, funcname))
with self.macro.blocked():
_func(v)
return None
return _callback
def _normalize_argcount(func: Callable) -> Callable[[Any], Any]:
if argcount(func) == 0:
return lambda v: func()
return func