Source code for orangewidget.utils.signals

import copy
import itertools
import warnings
from collections import defaultdict
from functools import singledispatch
import inspect
from typing import (
    NamedTuple, Union, Optional, Iterable, Dict, Tuple, Any, Sequence,
    Callable
)

from AnyQt.QtCore import Qt, pyqtSignal, QPoint
from AnyQt.QtWidgets import QWidgetAction, QMenu, QWidget, QLabel, QVBoxLayout

from orangecanvas.registry.description import (
    InputSignal, OutputSignal, Single, Multiple, Default, NonDefault,
    Explicit, Dynamic
)
# imported here for easier use by widgets, pylint: disable=unused-import
from orangecanvas.scheme.signalmanager import LazyValue

from orangewidget.utils.messagewidget import MessagesWidget
from orangewidget.workflow.utils import WeakKeyDefaultDict


# increasing counter for ensuring the order of Input/Output definitions
# is preserved when going through the unordered class namespace of
# WidgetSignalsMixin.Inputs/Outputs.
_counter = itertools.count()


class PartialSummary(NamedTuple):
    summary: Union[None, str, int] = None
    details: Optional[str] = None
    preview_func: Optional[Callable[[], QWidget]] = None


def base_summarize(_) -> PartialSummary:
    return PartialSummary()


summarize = singledispatch(base_summarize)
summarize.__doc__ = """
Function for summarizing the input or output data.

The function must be decorated with `@summarize.register`. It accepts an
argument of arbitrary type and returns a `PartialSummary`, which is a tuple
consisting of two strings: a short summary (usually a number) and details.
"""


SUMMARY_STYLE = """
<style>
    ul {
        margin-left: 4px;
        margin-top: 2px;
        -qt-list-indent:1
    }

    li {
        margin-bottom: 3px;
    }

    th {
        text-align: right;
    }
</style>
"""


def can_summarize(type_, name, explicit):
    if explicit is not None:
        return explicit
    if not isinstance(type_, tuple):
        type_ = (type_, )
    instr = f"To silence this warning, set auto_summary of '{name}' to False."
    for a_type in type_:
        if isinstance(a_type, str):
            warnings.warn(
                f"Output is specified with qualified name ({a_type}). "
                "To enable auto summary, set auto_summary to True. "
                + instr, UserWarning)
            return False
        if summarize.dispatch(a_type) is base_summarize:
            warnings.warn(
                f"register 'summarize' function for type {a_type.__name__}. "
                + instr, UserWarning, stacklevel=4)
            return False
    return True


Closed = type(
    "Closed", (object,), {
        "__doc__": "Explicit connection closing sentinel.",
        "__repr__": lambda self: "Closed",
        "__str__": lambda self: "Closed",
    }
)()


class _Signal:
    @staticmethod
    def get_flags(multiple, default, explicit, dynamic):
        """Compute flags from arguments"""

        return (Multiple if multiple else Single) | \
                (Default if default else NonDefault) | \
                (explicit and Explicit) | \
                (dynamic and Dynamic)

    def bound_signal(self, widget):
        """
        Return a copy of the signal bound to a widget.

        Called from `WidgetSignalsMixin.__init__`
        """
        new_signal = copy.copy(self)
        new_signal.widget = widget
        return new_signal


def getsignals(signals_cls):
    # This function is preferred over getmembers because it returns the signals
    # in order of appearance
    return [(k, v)
            for cls in reversed(inspect.getmro(signals_cls))
            for k, v in cls.__dict__.items()
            if isinstance(v, _Signal)]


class Input(InputSignal, _Signal):
    """
    Description of an input signal.

    The class is used to declare input signals for a widget as follows
    (the example is taken from the widget Test & Score)::

        class Inputs:
            train_data = Input("Data", Table, default=True)
            test_data = Input("Test Data", Table)
            learner = Input("Learner", Learner, multiple=True)
            preprocessor = Input("Preprocessor", Preprocess)

    Every input signal must be used to decorate exactly one method that
    serves as the input handler, for instance::

        @Inputs.train_data
        def set_train_data(self, data):
            ...

    Parameters
    ----------
    name (str):
        signal name
    type (type):
        signal type
    id (str):
        a unique id of the signal
    doc (str, optional):
        signal documentation
    replaces (list of str):
        a list with names of signals replaced by this signal
    multiple (bool, optional):
        if set, multiple signals can be connected to this output
        (default: `False`)
    default (bool, optional):
        when the widget accepts multiple signals of the same type, one of them
        can set this flag to act as the default (default: `False`)
    explicit (bool, optional):
        if set, this signal is only used when it is the only option or when
        explicitly connected in the dialog (default: `False`)
    auto_summary (bool, optional):
        by default, the input is reflected in widget's summary for all types
        with registered `summarize` function. This can be overridden by
        explicitly setting `auto_summary` to `False` or `True`. Explicitly
        setting this argument will also silence warnings for types without
        the summary function and for types defined with a fully qualified
        string instead of an actual type object.
    """
    Closed = Closed

    def __init__(self, name, type, id=None, doc=None, replaces=None, *,
                 multiple=False, default=False, explicit=False,
                 auto_summary=None, closing_sentinel=None):
        flags = self.get_flags(multiple, default, explicit, False)
        super().__init__(name, type, "", flags, id, doc, replaces or [])
        self.auto_summary = can_summarize(type, name, auto_summary)
        self._seq_id = next(_counter)
        self.closing_sentinel = closing_sentinel

    def __call__(self, method):
        """
        Decorator that stores decorated method's name in the signal's
        `handler` attribute. The method is returned unchanged.
        """
        if self.flags & Multiple:
            def summarize_wrapper(widget, value, id=None):
                # If this method is overridden, don't summarize
                if summarize_wrapper is getattr(type(widget), method.__name__):
                    widget.set_partial_input_summary(
                        self.name, summarize(value), id=id)
                method(widget, value, id)
        else:
            def summarize_wrapper(widget, value):
                if summarize_wrapper is getattr(type(widget), method.__name__):
                    widget.set_partial_input_summary(
                        self.name, summarize(value))
                method(widget, value)

        # Re-binding with the same name can happen in derived classes
        # We do not allow re-binding to a different name; for the same class
        # it wouldn't work, in derived class it could mislead into thinking
        # that the signal is passed to two different methods
        if self.handler and self.handler != method.__name__:
            raise ValueError("Input {} is already bound to method {}".
                             format(self.name, self.handler))
        self.handler = method.__name__
        return summarize_wrapper if self.auto_summary else method


class MultiInput(Input):
    """
    A special multiple input descriptor.

    This type of input has explicit set/insert/remove interface to maintain
    fully ordered sequence input. This should be preferred to the
    plain `Input(..., multiple=True)` descriptor.

    This input type must register three methods in the widget implementation
    class corresponding to the insert, set/update and remove input commands::

        class Inputs:
            values = MultiInput("Values", object)
        ...
        @Inputs.values
        def set_value(self, index: int, value: object):
            "Set/update the value at index"
            ...
        @Inputs.values.insert
        def insert_value(self, index: int, value: object):
            "Insert value at specified index"
            ...
        @Inputs.values.remove
        def remove_value(self, index: int):
            "Remove value at index"
            ...

    Parameters
    ----------
    filter_none: bool
        If `True` any `None` values sent by workflow execution
        are implicitly converted to 'remove' notifications. When the value
        again changes to non-None the input is re-inserted into its proper
        position.


    .. versionadded:: 4.13.0
    """
    insert_handler: str = None
    remove_handler: str = None

    def __init__(self, *args, filter_none=False, **kwargs):
        multiple = kwargs.pop("multiple", True)
        if not multiple:
            raise ValueError("multiple cannot be set to False")
        super().__init__(*args, multiple=True, **kwargs)
        self.filter_none = filter_none
        self.closing_sentinel = Closed

    __summary_ids_mapping = WeakKeyDefaultDict(dict)
    __id_gen = itertools.count()

    def __get_summary_ids(self, widget: 'WidgetSignalsMixin'):
        ids = self.__summary_ids_mapping[widget]
        return ids.setdefault(self.name, [])

    def __call__(self, method):
        def summarize_wrapper(widget, index, value):
            # If this method is overridden, don't summarize
            if summarize_wrapper is getattr(type(widget), method.__name__):
                ids = self.__get_summary_ids(widget)
                widget.set_partial_input_summary(
                    self.name, summarize(value), id=ids[index], index=index)
            method(widget, index, value)
        _ = super().__call__(method)
        return summarize_wrapper if self.auto_summary else method

    def insert(self, method):
        """Register the method as the insert handler"""
        def summarize_wrapper(widget, index, value):
            if summarize_wrapper is getattr(type(widget), method.__name__):
                ids = self.__get_summary_ids(widget)
                ids.insert(index, next(self.__id_gen))
                widget.set_partial_input_summary(
                    self.name, summarize(value), id=ids[index], index=index)
            method(widget, index, value)
        self.insert_handler = method.__name__
        return summarize_wrapper if self.auto_summary else method

    def remove(self, method):
        """"Register the method as the remove handler"""
        def summarize_wrapper(widget, index):
            if summarize_wrapper is getattr(type(widget), method.__name__):
                ids = self.__get_summary_ids(widget)
                id_ = ids.pop(index)
                widget.set_partial_input_summary(
                    self.name, summarize(None), id=id_)
            method(widget, index)
        self.remove_handler = method.__name__
        return summarize_wrapper if self.auto_summary else method

    def bound_signal(self, widget):
        if self.insert_handler is None:
            raise RuntimeError('insert_handler is not set')
        if self.remove_handler is None:
            raise RuntimeError('remove_handler is not set')
        return super().bound_signal(widget)


_not_set = object()


def _parse_call_id_arg(id=_not_set):
    if id is _not_set:
        return None
    else:
        warnings.warn(
            "`id` parameter is deprecated and will be removed in the "
            "future", FutureWarning, stacklevel=3,
        )
        return id


class Output(OutputSignal, _Signal):
    """
    Description of an output signal.

    The class is used to declare output signals for a widget as follows
    (the example is taken from the widget Test & Score)::

        class Outputs:
            predictions = Output("Predictions", Table)
            evaluations_results = Output("Evaluation Results", Results)

    The signal is then transmitted by, for instance::

        self.Outputs.predictions.send(predictions)

    Parameters
    ----------
    name (str):
        signal name
    type (type):
        signal type
    id (str):
        a unique id of the signal
    doc (str, optional):
        signal documentation
    replaces (list of str):
        a list with names of signals replaced by this signal
    default (bool, optional):
        when the widget accepts multiple signals of the same type, one of them
        can set this flag to act as the default (default: `False`)
    explicit (bool, optional):
        if set, this signal is only used when it is the only option or when
        explicitly connected in the dialog (default: `False`)
    dynamic (bool, optional):
        Specifies that the instances on the output will in general be subtypes
        of the declared type and that the output can be connected to any input
        signal which can accept a subtype of the declared output type
        (default: `True`)
    auto_summary (bool, optional):
        by default, the output is reflected in widget's summary for all types
        with registered `summarize` function. This can be overridden by
        explicitly setting `auto_summary` to `False` or `True`. Explicitly
        setting this argument will also silence warnings for types without
        the summary function and for types defined with a fully qualified
        string instead of an actual type object.
    """
    def __init__(self, name, type, id=None, doc=None, replaces=None, *,
                 default=False, explicit=False, dynamic=True,
                 auto_summary=None):
        flags = self.get_flags(False, default, explicit, dynamic)
        super().__init__(name, type, flags, id, doc, replaces or [])
        self.auto_summary = can_summarize(type, name, auto_summary)
        self.widget = None
        self._seq_id = next(_counter)

    def send(self, value, *args, **kwargs):
        """Emit the signal through signal manager."""
        assert self.widget is not None
        id = _parse_call_id_arg(*args, **kwargs)
        signal_manager = self.widget.signalManager
        if signal_manager is not None:
            if id is not None:
                extra_args = (id,)
            else:
                extra_args = ()
            signal_manager.send(self.widget, self.name, value, *extra_args)
        if self.auto_summary:
            self.widget.set_partial_output_summary(
                self.name, summarize(value), id=id)

    def invalidate(self):
        """Invalidate the current output value on the signal"""
        assert self.widget is not None
        signal_manager = self.widget.signalManager
        if signal_manager is not None:
            signal_manager.invalidate(self.widget, self.name)


class WidgetSignalsMixin:
    """Mixin for managing widget's input and output signals"""
    class Inputs:
        pass

    class Outputs:
        pass

    def __init__(self):
        self.input_summaries = {}
        self.output_summaries: Dict[str, PartialSummary] = {}
        self._bind_signals()

    def _bind_signals(self):
        for direction, summaries in (("Inputs", self.input_summaries),
                                     ("Outputs", self.output_summaries)):
            bound_cls = getattr(self, direction)
            bound_signals = bound_cls()
            for name, signal in getsignals(bound_cls):
                setattr(bound_signals, name, signal.bound_signal(self))
                if signal.auto_summary:
                    summaries[signal.name] = {}
            setattr(self, direction, bound_signals)

    def send(self, signalName, value, *args, **kwargs):
        """
        Send a `value` on the `signalName` widget output.

        An output with `signalName` must be defined in the class ``outputs``
        list.
        """
        id = _parse_call_id_arg(*args, **kwargs)
        if not any(s.name == signalName for s in self.outputs):
            raise ValueError('{} is not a valid output signal for widget {}'.format(
                signalName, self.name))

        if self.signalManager is not None:
            if id is not None:
                extra_args = (id,)
            else:
                extra_args = ()
            self.signalManager.send(self, signalName, value, *extra_args)

    def handleNewSignals(self):
        """
        Invoked by the workflow signal propagation manager after all
        signals handlers have been called.
        Reimplement this method in order to coalesce updates from
        multiple updated inputs.
        """
        pass

    # Methods used by the meta class
    @classmethod
    def convert_signals(cls):
        """
        Maintenance and sanity checks for signals.

        - Convert tuple descriptions into old-style signals for backwards compatibility
        - For new-style in classes, copy attribute name to id, if id is not set explicitly
        - Check that all input signals have handlers
        - Check that the same name and/or does not refer to different signals.

        This method is called from the meta-class.
        """
        def signal_from_args(args, signal_type):
            if isinstance(args, tuple):
                return signal_type(*args)
            elif isinstance(args, signal_type):
                return copy.copy(args)

        if hasattr(cls, "inputs") and cls.inputs:
            cls.inputs = [signal_from_args(input_, InputSignal)
                          for input_ in cls.inputs]
        if hasattr(cls, "outputs") and cls.outputs:
            cls.outputs = [signal_from_args(output, OutputSignal)
                           for output in cls.outputs]

        for direction in ("Inputs", "Outputs"):
            klass = getattr(cls, direction, None)
            if klass is None:
                continue
            for name, signal in klass.__dict__.items():
                if isinstance(signal, (_Signal)) and signal.id is None:
                    signal.id = name

        cls._check_input_handlers()
        cls._check_ids_unique()

    @classmethod
    def _check_input_handlers(cls):
        unbound = [signal.name
                   for _, signal in getsignals(cls.Inputs)
                   if not signal.handler]
        if unbound:
            raise ValueError("unbound signal(s) in {}: {}".
                             format(cls.__name__, ", ".join(unbound)))

        missing_handlers = [signal.handler for signal in cls.inputs
                            if not hasattr(cls, signal.handler)]
        if missing_handlers:
            raise ValueError("missing handlers in {}: {}".
                             format(cls.__name__, ", ".join(missing_handlers)))

    @classmethod
    def _check_ids_unique(cls):
        for direction in ("input", "output"):
            # Collect signals by name and by id, check for duplicates
            by_name = {}
            by_id = {}
            for signal in cls.get_signals(direction + "s"):
                if signal.name in by_name:
                    raise RuntimeError(
                        f"Name {signal.name} refers to different {direction} "
                        f"signals of {cls.__name__}" )
                by_name[signal.name] = signal
                if signal.id is not None:
                    if signal.id in by_id:
                        raise RuntimeError(
                            f"Id {signal.id} refers to different {direction} "
                            f"signals of {cls.__name__}" )
                    by_id[signal.id] = signal

            # Warn the same name and id refer to different signal
            for name in set(by_name) & set(by_id):
                if by_name[name] is not by_id[name]:
                    warnings.warn(
                        f"{name} appears as a name and an id of two different "
                        f"{direction} signals in {cls.__name__}")

    @classmethod
    def get_signals(cls, direction, ignore_old_style=False):
        """
        Return a list of `InputSignal` or `OutputSignal` needed for the
        widget description. For old-style signals, the method returns the
        original list. New-style signals are collected into a list.

        Parameters
        ----------
        direction (str): `"inputs"` or `"outputs"`

        Returns
        -------
        list of `InputSignal` or `OutputSignal`
        """
        old_style = cls.__dict__.get(direction, None)
        if old_style and not ignore_old_style:
            return old_style

        signal_class = getattr(cls, direction.title())
        signals = [signal for _, signal in getsignals(signal_class)]
        return list(sorted(signals, key=lambda s: s._seq_id))

    def update_summaries(self):
        self._update_summary(self.input_summaries)
        self._update_summary(self.output_summaries)

    def set_partial_input_summary(self, name, partial_summary, *,
                                  id=None, index=None):
        self.__set_part_summary(self.input_summaries[name], id, partial_summary, index=index)
        self._update_summary(self.input_summaries)

    def set_partial_output_summary(self, name, partial_summary, *, id=None):
        self.__set_part_summary(self.output_summaries[name], id, partial_summary)
        self._update_summary(self.output_summaries)

    @staticmethod
    def __set_part_summary(summary, id, partial_summary, index=None):
        if partial_summary.summary is None:
            if id in summary:
                del summary[id]
        else:
            if index is None or id in summary:
                summary[id] = partial_summary
            else:
                # Insert inplace at specified index
                items = list(summary.items())
                items.insert(index, (id, partial_summary))
                summary.clear()
                summary.update(items)

    def _update_summary(self, summaries):
        from orangewidget.widget import StateInfo

        def format_short(partial):
            summary = partial.summary
            if summary is None:
                return "-"
            if isinstance(summary, int):
                return StateInfo.format_number(summary)
            if isinstance(summary, str):
                return summary
            raise ValueError("summary must be None, string or int; "
                             f"got {type(summary).__name__}")

        def format_detail(partial):
            if partial.summary is None:
                return "-"
            return str(partial.details or partial.summary)

        def join_multiples(partials):
            if not partials:
                return "-", "-"
            shorts = " ".join(map(format_short, partials.values()))
            details = "<br/>".join(format_detail(partial) for partial in partials.values())
            return shorts, details

        info = self.info
        is_input = summaries is self.input_summaries
        assert is_input or summaries is self.output_summaries

        if not summaries:
            return
        if not any(summaries.values()):
            summary = info.NoInput if is_input else info.NoOutput
            detail = ""
        else:
            summary, details = zip(*map(join_multiples, summaries.values()))
            summary = " | ".join(summary)
            detail = "<hr/><table>" \
                     + "".join(f"<tr><th><nobr>{name}</nobr>: "
                               f"</th><td>{detail}</td></tr>"
                               for name, detail in zip(summaries, details)) \
                     + "</table>"

        setter = info.set_input_summary if is_input else info.set_output_summary
        if detail:
            setter(summary, SUMMARY_STYLE + detail, format=Qt.RichText)
        else:
            setter(summary)

    def show_preview(self, summaries):
        view = QWidget(self)
        view.setLayout(QVBoxLayout())

        for name, summary in summaries.items():
            if not summary:
                view.layout().addWidget(QLabel("<hr/><table>"
                           f"<tr><th><nobr>{name}</nobr>: "
                           f"</th><td>-</td></tr>"
                           "</table>"))
            for i, part in enumerate(summary.values(), start=1):
                part_no = f" ({i})" if len(summary) > 1 else ""
                detail = str(part.details or part.summary) or "-"
                view.layout().addWidget(
                    QLabel("<hr/><table>"
                           f"<tr><th><nobr>{name}{part_no}</nobr>: "
                           f"</th><td>{detail}</td></tr>"
                           "</table>")
                )
                if part.preview_func:
                    preview = part.preview_func()
                    view.layout().addWidget(preview, 1)
        if view.layout().isEmpty():
            return
        view.layout().addStretch()

        screen = self.windowHandle().screen()
        geometry = screen.availableGeometry()
        preview = QMenu(self)
        wid, hei = geometry.width(), geometry.height()
        preview.setFixedSize(wid // 2, hei // 2)
        view.setFixedSize(wid // 2 - 4, hei // 2 - 4)
        action = QWidgetAction(preview)
        action.setDefaultWidget(view)
        preview.addAction(action)
        preview.popup(QPoint(wid // 4, hei // 4), action)


def get_input_meta(widget: WidgetSignalsMixin, name: str) -> Optional[Input]:
    """
    Return the named input meta description from widget (if it exists).
    """
    def as_input(obj):
        if isinstance(obj, Input):
            return obj
        elif isinstance(obj, InputSignal):
            rval = Input(obj.name, obj.type, obj.id, obj.doc, obj.replaces,
                         multiple=not obj.single, default=obj.default,
                         explicit=obj.explicit)
            rval.handler = obj.handler
            return rval
        elif isinstance(obj, tuple):
            return as_input(InputSignal(*obj))
        else:
            raise TypeError

    inputs: Iterable[Input] = map(as_input, widget.get_signals("inputs"))
    for input_ in inputs:
        if input_.name == name:
            return input_
    return None


def get_widget_inputs(
        widget: WidgetSignalsMixin
) -> Dict[str, Sequence[Tuple[Any, Any]]]:
    state: Dict[str, Sequence[Tuple[Any, Any]]]
    state = widget.__dict__.setdefault(
        "_WidgetSignalsMixin__input_state", {}
    )
    return state


@singledispatch
def notify_input_helper(
        input: Input, widget: WidgetSignalsMixin, obj, key=None, index=-1
) -> None:
    """
    Set the input to the `widget` in a way appropriate for the `input` type.
    """
    raise NotImplementedError


@notify_input_helper.register(Input)
def set_input_helper(
        input: Input, widget: WidgetSignalsMixin, obj, key=None, index=-1
):
    handler = getattr(widget, input.handler)
    if input.single:
        args = (obj,)
    else:
        args = (obj, key)
    handler(*args)


@notify_input_helper.register(MultiInput)
def set_multi_input_helper(
        input: MultiInput, widget: WidgetSignalsMixin, obj, key=None, index=-1,
):
    """
    Set/update widget's input for a `MultiInput` input to obj.

    `key` must be a unique for an input slot to update.
    `index` defines the position where a new input (key that did not
    previously exist) is inserted. The default -1 indicates that the
    new input should be appended to the end. An input is removed by using
    inout.closing_sentinel as the obj.
    """
    inputs_ = get_widget_inputs(widget)
    inputs = inputs_.setdefault(input.name, ())
    filter_none = input.filter_none

    signal_old = None
    key_to_pos = {key: i for i, (key, _) in enumerate(inputs)}
    update = key in key_to_pos
    new = key not in key_to_pos
    remove = obj is input.closing_sentinel
    if new:
        if not 0 <= index < len(inputs):
            index = len(inputs)
    else:
        index = key_to_pos.get(key)
        assert index is not None

    inputs_updated = list(inputs)
    if new:
        inputs_updated.insert(index, (key, obj))
    elif remove:
        signal_old = inputs_updated.pop(index)
    else:
        signal_old = inputs_updated[index]
        inputs_updated[index] = (key, obj)

    inputs_[input.name] = tuple(inputs_updated)

    if filter_none:
        def filter_f(obj):
            return obj is None
    else:
        filter_f = None

    def local_index(
            key: Any, inputs: Sequence[Tuple[Any, Any]],
            filter: Optional[Callable[[Any], bool]] = None,
    ) -> Optional[int]:
        i = 0
        for k, obj in inputs:
            if key == k:
                return i
            elif filter is not None:
                i += int(not filter(obj))
            else:
                i += 1
        return None

    if filter_none:
        # normalize signal.value is None to Close signal.
        filtered = filter_f(obj)
        if new and filtered:
            # insert in inputs only (done above)
            return
        elif new:
            # Some inputs before this might be filtered invalidating the
            # effective index. Find appropriate index for insertion
            index = len([obj for _, obj in inputs[:index] if not filter_f(obj)])
        elif remove:
            if filter_f(signal_old[1]):
                # was already notified as removed, only remove from inputs (done above)
                return
            else:
                index = local_index(key, inputs, filter_f)
        elif update and filtered:
            if filter_f(signal_old[1]):
                # did not change; remains filtered
                return
            else:
                # remove it
                remove = True
                new = False
                index = local_index(key, inputs, filter_f)
                assert index is not None
        elif update:
            index = local_index(key, inputs, filter_f)

        if signal_old is not None and filter_f(signal_old[1]) and not filtered:
            # update with non-none value, substitute as new signal
            new = True
            remove = False
            index = local_index(key, inputs, filter_f)

    if new:
        handler = input.insert_handler
        args = (index, obj)
    elif remove:
        handler = input.remove_handler
        args = (index, )
    else:
        handler = input.handler
        args = (index, obj)
    assert index is not None
    handler = getattr(widget, handler)
    handler(*args)