from __future__ import annotations
from typing import Any, TYPE_CHECKING, Callable, TypeVar
from dataclasses import Field, MISSING
from magicgui.type_map import get_widget_class
from magicgui.widgets import create_widget
from magicgui.widgets._bases import Widget
from magicgui.widgets._bases.value_widget import UNSET, ValueWidget
from .widgets import NotInstalled
if TYPE_CHECKING:
from magicgui.widgets._protocols import WidgetProtocol
from magicgui.types import WidgetOptions
X = TypeVar("X")
[docs]class MagicField(Field):
"""
Field class for magicgui construction. This object is compatible with dataclass.
"""
def __init__(self, default=MISSING, default_factory=MISSING, metadata: dict = {},
name: str = None, record: bool = True):
metadata = metadata.copy()
if default is MISSING:
default = metadata.pop("value", MISSING)
super().__init__(default=default, default_factory=default_factory, init=True, repr=True,
hash=False, compare=False, metadata=metadata)
self.callbacks: list[Callable] = []
self.guis: dict[int, X] = {}
self.name = name
self.record = record
self.parent_class = None
def __repr__(self):
return self.__class__.__name__.rstrip("Field") + super().__repr__()
[docs] def as_getter(self, obj: X):
"""
Make a function that get the value of Widget.
"""
return lambda widget: self.get_widget(obj).value
def __get__(self, obj: X, objtype=None):
if obj is None:
return self
return self.get_widget(obj)
def __set__(self, obj: X, value) -> None:
raise AttributeError(f"Cannot set value to {self.__class__.__name__}.")
[docs] def ready(self) -> bool:
return not self.not_ready()
[docs] def not_ready(self) -> bool:
return self.default is MISSING and self.default_factory is MISSING
[docs] 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
@property
def value(self) -> Any:
return UNSET if self.default is MISSING else self.default
@property
def annotation(self):
return None if self.default_factory is MISSING else self.default_factory
@property
def options(self) -> dict:
return self.metadata.get("options", {})
@property
def widget_type(self) -> str:
if self.default_factory is not MISSING and issubclass(self.default_factory, Widget):
wcls = self.default_factory
else:
wcls = get_widget_class(value=self.value, annotation=self.annotation)
return wcls.__name__
[docs]class MagicValueField(MagicField):
def __get__(self, obj: X, objtype=None):
if obj is None:
return self
return self.get_widget(obj).value
def __set__(self, obj: X, value) -> None:
if obj is None:
raise AttributeError(f"Cannot set {self.__class__.__name__}.")
self.get_widget(obj).value = value
[docs]def field(obj: Any = MISSING,
*,
name: str = "",
widget_type: str | type[WidgetProtocol] | None = None,
options: WidgetOptions = {},
record: bool = True
) -> MagicField:
"""
Make a MagicField object.
>>> i = field(1)
>>> i = field(widget_type="Slider")
Parameters
----------
obj : Any, default is MISSING
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, default is ""
Name 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 always be used in ``widget(**options)`` form.
record : bool, default is True
Record value changes as macro.
Returns
-------
MagicField
"""
return _get_field(obj, name, widget_type, options, record, MagicField)
[docs]def vfield(obj: Any = MISSING,
*,
name: str = "",
widget_type: str | type[WidgetProtocol] | None = None,
options: WidgetOptions = {},
record: bool = True,
) -> MagicValueField:
"""
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 MISSING
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, default is ""
Name 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 always be used in ``widget(**options)`` form.
Returns
-------
MagicValueField
"""
return _get_field(obj, name, widget_type, options, record, MagicValueField)
def _get_field(obj,
name: str,
widget_type: str | type[WidgetProtocol] | None,
options: WidgetOptions,
record: bool,
field_class: type[MagicField]
):
options = options.copy()
metadata = dict(widget_type=widget_type, options=options)
if isinstance(obj, type):
f = field_class(default_factory=obj, metadata=metadata, name=name, record=record)
elif obj is MISSING:
f = field_class(metadata=metadata, name=name, record=record)
else:
f = field_class(default=obj, metadata=metadata, name=name, record=record)
return f