Source code for magicclass.widgets.sequence

from __future__ import annotations
from typing import Any, Iterable, TypeVar, overload, Iterator, Tuple, ForwardRef, Sequence, List
from typing_extensions import get_args, get_origin
import inspect
from magicgui.widgets import create_widget, Container, PushButton
from magicgui.types import WidgetOptions
from magicgui.widgets._bases.value_widget import UNSET, ValueWidget, _Unset
from magicgui.widgets._concrete import merge_super_sigs

_V = TypeVar("_V")


@merge_super_sigs
class ListEdit(Container):
    """A widget to represent a list of values.

    A ListEdit container can create a list with multiple objects of same type. It
    will contain many child widgets and their value is represented as a Python list
    object. If a list is given as the initial value, types of child widgets are
    determined from the contents. Number of contents can be adjusted with +/-
    buttons.

    Parameters
    ----------
    options: WidgetOptions, optional
        Widget options of child widgets.
    """

    def __init__(
        self,
        value: Iterable[_V] | _Unset = UNSET,
        layout: str = "horizontal",
        options: WidgetOptions = None,
        **kwargs,
    ):
        self._args_type: type | None = None
        super().__init__(layout=layout, labels=False, **kwargs)
        self.margins = (0, 0, 0, 0)

        if not isinstance(value, _Unset):
            types = {type(a) for a in value}
            if len(types) == 1:
                self._args_type = types.pop()
            else:
                raise TypeError("values have inconsistent type.")
            _value: Iterable[_V] = value
        else:
            _value = []

        self._child_options = options or {}

        button_plus = PushButton(text="+", name="plus")
        button_plus.changed.connect(lambda: self._append_value())

        button_minus = PushButton(text="-", name="minus")
        button_minus.changed.connect(self._pop_value)

        if layout == "horizontal":
            button_plus.max_width = 40
            button_minus.max_width = 40

        self.append(button_plus)
        self.append(button_minus)

        for a in _value:
            self._append_value(a)

        self.btn_plus = button_plus
        self.btn_minus = button_minus

    @property
    def annotation(self):
        """Return type annotation for the parameter represented by the widget.

        ForwardRefs will be resolve when setting the annotation. For ListEdit,
        annotation will be like 'list[str]'.
        """
        return self._annotation

    @annotation.setter
    def annotation(self, value):
        if isinstance(value, (str, ForwardRef)):
            raise TypeError(
                "annotation using str or forward reference is not supported in "
                f"{type(self).__name__}"
            )

        arg: type | None = None

        if value and value is not inspect.Parameter.empty:
            from magicgui.type_map import _is_subclass

            orig = get_origin(value)
            if not (_is_subclass(orig, list) or isinstance(orig, list)):
                raise TypeError(
                    f"cannot set annotation {value} to {type(self).__name__}."
                )
            args = get_args(value)
            if len(args) > 0:
                _arg = args[0]
            else:
                _arg = None
            if isinstance(_arg, (str, ForwardRef)):
                from magicgui.type_map import _evaluate_forwardref

                arg = _evaluate_forwardref(_arg)
                if not isinstance(arg, type):
                    raise TypeError(f"could not resolve type {arg!r}.")

                value = List[arg]  # type: ignore
            else:
                arg = _arg

        self._annotation = value
        self._args_type = arg

    def _append_value(self, value=UNSET):
        """Create a new child value widget and append it."""
        i = len(self) - 2

        widget = create_widget(
            annotation=self._args_type,
            name=f"value_{i}",
            options=self._child_options,
        )
        self.insert(i, widget)

        # Value must be set after new widget is inserted because it could be
        # valid only after same parent is shared between widgets.
        if value is UNSET and i > 0:
            value = self[i - 1].value  # type: ignore
        if value is not UNSET:
            widget.value = value

    def _pop_value(self):
        """Delete last child value widget."""
        try:
            self.pop(-3)
        except IndexError:
            pass

    @property
    def value(self) -> ListDataView:
        """Return a data view of current value."""
        return ListDataView(self)

    @value.setter
    def value(self, vals: Iterable[_V]):
        del self[:-2]
        for v in vals:
            self._append_value(v)


[docs]class ListDataView: """Data view of ListEdit.""" def __init__(self, widget: ListEdit): self.widget: list[ValueWidget] = list(widget[:-2]) # type: ignore def __repr__(self): """Convert to a string as a list.""" return repr([w.value for w in self.widget]) def __str__(self): """Convert to a string as a list.""" return str([w.value for w in self.widget]) def __len__(self): """Length as a list.""" return len(self.widget) def __eq__(self, other): """Compare as a list.""" return [w.value for w in self.widget] == other @overload def __getitem__(self, i: int) -> _V: # noqa ... @overload def __getitem__(self, key: slice) -> list[_V]: # noqa ... def __getitem__(self, key): """Slice as a list.""" if isinstance(key, int): return self.widget[key].value elif isinstance(key, slice): return [w.value for w in self.widget[key]] else: raise TypeError( f"list indices must be integers or slices, not {type(key).__name__}" ) @overload def __setitem__(self, key: int, value: _V) -> None: # noqa ... @overload def __setitem__(self, key: slice, value: _V | Iterable[_V]) -> None: # noqa ... def __setitem__(self, key, value): """Update widget value.""" if isinstance(key, int): self.widget[key].value = value elif isinstance(key, slice): if isinstance(value, type(self.widget[0].value)): for w in self.widget[key]: w.value = value else: for w, v in zip(self.widget[key], value): w.value = v else: raise TypeError( f"list indices must be integers or slices, not {type(key).__name__}" ) def __iter__(self) -> Iterator[_V]: """Iterate over values of child widgets.""" for w in self.widget: yield w.value
@merge_super_sigs class TupleEdit(Container): """A widget to represent a tuple of values. A TupleEdit container has several child widgets of different type. Their value is represented as a Python tuple object. If a tuple is given as the initial value, types of child widgets are determined one by one. Unlike ListEdit, number of contents is not editable. Parameters ---------- options: WidgetOptions, optional Widget options of child widgets. """ def __init__( self, value: Iterable[_V] | _Unset = UNSET, layout: str = "horizontal", options: WidgetOptions = None, **kwargs, ): self._args_types: tuple[type, ...] | None = None super().__init__(layout=layout, labels=False, **kwargs) self._child_options = options or {} self.margins = (0, 0, 0, 0) if not isinstance(value, _Unset): self._args_types = tuple(type(a) for a in value) _value: Iterable[Any] = value elif self._args_types is not None: _value = (UNSET,) * len(self._args_types) else: raise ValueError( "Either 'value' or 'annotation' must be specified in " f"{type(self).__name__}." ) for i, a in enumerate(_value): i = len(self) widget = create_widget( value=a, annotation=self._args_types[i], name=f"value_{i}", options=self._child_options, ) self.insert(i, widget) def __iter__(self) -> Iterator[ValueWidget]: """Just for typing.""" return super().__iter__() # type: ignore @property def annotation(self): """Return type annotation for the parameter represented by the widget. ForwardRefs will be resolve when setting the annotation. For TupleEdit, annotation will be like 'tuple[str, int]'. """ return self._annotation @annotation.setter def annotation(self, value): if isinstance(value, (str, ForwardRef)): raise TypeError( "annotation using str or forward reference is not supported in " f"{type(self).__name__}" ) args: tuple[type, ...] | None = None if value and value is not inspect.Parameter.empty: from magicgui.type_map import _is_subclass orig = get_origin(value) if not (_is_subclass(orig, tuple) or isinstance(orig, tuple)): raise TypeError( f"cannot set annotation {value} to {type(self).__name__}." ) _args: list[type] = [] for arg in get_args(value): if isinstance(arg, (str, ForwardRef)): from magicgui.type_map import _evaluate_forwardref arg = _evaluate_forwardref(arg) if not isinstance(arg, type): raise TypeError(f"could not resolve type {arg!r}.") _args.append(arg) args = tuple(_args) value = Tuple[args] self._annotation = value self._args_types = args @property def value(self) -> tuple: """Return current value as a tuple.""" return tuple(w.value for w in self) @value.setter def value(self, vals: Sequence): if len(vals) != len(self): raise ValueError("Length of tuple does not match.") for w, v in zip(self, vals): w.value = v # type: ignore