{
    "file_diffs": [
        {
            "old_file_content": "\"\"\"\nEdit Domain\n-----------\n\nA widget for manual editing of a domain's attributes.\n\n\"\"\"\nimport warnings\nfrom xml.sax.saxutils import escape\nfrom itertools import zip_longest, repeat, chain\nfrom contextlib import contextmanager\nfrom collections import namedtuple, Counter\nfrom functools import singledispatch, partial\nfrom typing import (\n    Tuple, List, Any, Optional, Union, Dict, Sequence, Iterable, NamedTuple,\n    FrozenSet, Type, Callable, TypeVar, Mapping, Hashable, cast\n)\n\nimport numpy as np\nimport pandas as pd\nfrom AnyQt.QtWidgets import (\n    QWidget, QListView, QTreeView, QVBoxLayout, QHBoxLayout, QFormLayout,\n    QLineEdit, QAction, QActionGroup, QGroupBox,\n    QStyledItemDelegate, QStyleOptionViewItem, QStyle, QSizePolicy,\n    QDialogButtonBox, QPushButton, QCheckBox, QComboBox, QStackedLayout,\n    QDialog, QRadioButton, QGridLayout, QLabel, QSpinBox, QDoubleSpinBox,\n    QAbstractItemView, QMenu, QToolTip\n)\nfrom AnyQt.QtGui import (\n    QStandardItemModel, QStandardItem, QKeySequence, QIcon, QBrush, QPalette,\n    QHelpEvent\n)\nfrom AnyQt.QtCore import (\n    Qt, QSize, QModelIndex, QAbstractItemModel, QPersistentModelIndex, QRect,\n    QPoint,\n)\nfrom AnyQt.QtCore import pyqtSignal as Signal, pyqtSlot as Slot\n\nimport Orange.data\n\nfrom Orange.preprocess.transformation import (\n    Transformation, Identity, Lookup, MappingTransform\n)\nfrom Orange.misc.collections import DictMissingConst\nfrom Orange.util import frompyfunc\nfrom Orange.widgets import widget, gui, settings\nfrom Orange.widgets.utils import itemmodels, ftry\nfrom Orange.widgets.utils.buttons import FixedSizeButton\nfrom Orange.widgets.utils.itemmodels import signal_blocking\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.widget import Input, Output\n\nndarray = np.ndarray  # pylint: disable=invalid-name\nMArray = np.ma.MaskedArray\nDType = Union[np.dtype, type]\n\nV = TypeVar(\"V\", bound=Orange.data.Variable)  # pylint: disable=invalid-name\nH = TypeVar(\"H\", bound=Hashable)  # pylint: disable=invalid-name\n\n\ndef unique(sequence: Iterable[H]) -> Iterable[H]:\n    \"\"\"\n    Return unique elements in `sequence`, preserving their (first seen) order.\n    \"\"\"\n    # depending on Python >= 3.6 'ordered' dict implementation detail.\n    return iter(dict.fromkeys(sequence))\n\n\nclass _DataType:\n    def __eq__(self, other):\n        \"\"\"Equal if `other` has the same type and all elements compare equal.\"\"\"\n        if type(self) is not type(other):\n            return False\n        return super().__eq__(other)\n\n    def __ne__(self, other):\n        return not self == other\n\n    def __hash__(self):\n        return hash((type(self), super().__hash__()))\n\n    def name_type(self):\n        \"\"\"\n        Returns a tuple with name and type of the variable.\n        It is used since it is forbidden to use names of variables in settings.\n        \"\"\"\n        type_number = {\n            \"Categorical\": 0,\n            \"Real\": 2,\n            \"Time\": 3,\n            \"String\": 4\n        }\n        return self.name, type_number[type(self).__name__]\n\n\n#: An ordered sequence of key, value pairs (variable annotations)\nAnnotationsType = Tuple[Tuple[str, str], ...]\n\n\n# Define abstract representation of the variable types edited\n\nclass Categorical(\n    _DataType, NamedTuple(\"Categorical\", [\n        (\"name\", str),\n        (\"categories\", Tuple[str, ...]),\n        (\"annotations\", AnnotationsType),\n        (\"linked\", bool)\n    ])): pass\n\n\nclass Real(\n    _DataType, NamedTuple(\"Real\", [\n        (\"name\", str),\n        # a precision (int, and a format specifier('f', 'g', or '')\n        (\"format\", Tuple[int, str]),\n        (\"annotations\", AnnotationsType),\n        (\"linked\", bool)\n    ])): pass\n\n\nclass String(\n    _DataType, NamedTuple(\"String\", [\n        (\"name\", str),\n        (\"annotations\", AnnotationsType),\n        (\"linked\", bool)\n    ])): pass\n\n\nclass Time(\n    _DataType, NamedTuple(\"Time\", [\n        (\"name\", str),\n        (\"annotations\", AnnotationsType),\n        (\"linked\", bool)\n    ])): pass\n\n\nVariable = Union[Categorical, Real, Time, String]\nVariableTypes = (Categorical, Real, Time, String)\n\n\n# Define variable transformations.\n\nclass Rename(_DataType, namedtuple(\"Rename\", [\"name\"])):\n    \"\"\"\n    Rename a variable.\n\n    Parameters\n    ----------\n    name : str\n        The new name\n    \"\"\"\n    def __call__(self, var):\n        # type: (Variable) -> Variable\n        return var._replace(name=self.name)\n\n\n#: Mapping of categories.\n#: A list of pairs with the first element the original value and the second\n#: element the new value. If the first element is None then a category level\n#: is added. If the second element is None than the corresponding first level\n#: is dropped. If there are duplicated elements on the right the corresponding\n#: categories on the left are merged.\n#: The mapped order is defined by the translated elements after None removal\n#: and merges (the first occurrence of a multiplied elements defines its\n#: position):\nCategoriesMappingType = List[Tuple[Optional[str], Optional[str]]]\n\n\nclass CategoriesMapping(_DataType, namedtuple(\"CategoriesMapping\", [\"mapping\"])):\n    \"\"\"\n    Change categories of a categorical variable.\n\n    Parameters\n    ----------\n    mapping : CategoriesMappingType\n    \"\"\"\n    def __call__(self, var):\n        # type: (Categorical) -> Categorical\n        cat = tuple(unique(cj for _, cj in self.mapping if cj is not None))\n        return var._replace(categories=cat)\n\n\nclass Annotate(_DataType, namedtuple(\"Annotate\", [\"annotations\"])):\n    \"\"\"\n    Replace variable annotations.\n    \"\"\"\n    def __call__(self, var):\n        return var._replace(annotations=self.annotations)\n\n\nclass Unlink(_DataType, namedtuple(\"Unlink\", [])):\n    \"\"\"Unlink variable from its source, that is, remove compute_value\"\"\"\n\n\nclass StrpTime(_DataType, namedtuple(\"StrpTime\", [\"label\", \"formats\", \"have_date\", \"have_time\"])):\n    \"\"\"Use format on variable interpreted as time\"\"\"\n\n\nTransform = Union[Rename, CategoriesMapping, Annotate, Unlink, StrpTime]\nTransformTypes = (Rename, CategoriesMapping, Annotate, Unlink, StrpTime)\n\nCategoricalTransformTypes = (CategoriesMapping, Unlink)\n\n\n# Reinterpret vector transformations.\nclass CategoricalVector(\n    _DataType, NamedTuple(\"CategoricalVector\", [\n        (\"vtype\", Categorical),\n        (\"data\", Callable[[], MArray]),\n    ])): ...\n\n\nclass RealVector(\n    _DataType, NamedTuple(\"RealVector\", [\n        (\"vtype\", Real),\n        (\"data\", Callable[[], MArray]),\n    ])): ...\n\n\nclass StringVector(\n    _DataType, NamedTuple(\"StringVector\", [\n        (\"vtype\", String),\n        (\"data\", Callable[[], MArray]),\n    ])): ...\n\n\nclass TimeVector(\n    _DataType, NamedTuple(\"TimeVector\", [\n        (\"vtype\", Time),\n        (\"data\", Callable[[], MArray]),\n    ])): ...\n\n\nDataVector = Union[CategoricalVector, RealVector, StringVector, TimeVector]\nDataVectorTypes = (CategoricalVector, RealVector, StringVector, TimeVector)\n\n\nclass AsString(_DataType, NamedTuple(\"AsString\", [])):\n    \"\"\"Reinterpret a data vector as a string.\"\"\"\n    def __call__(self, vector: DataVector) -> StringVector:\n        var, _ = vector\n        if isinstance(var, String):\n            return vector\n        return StringVector(\n            String(var.name, var.annotations, False),\n            lambda: as_string(vector.data()),\n        )\n\n\nclass AsContinuous(_DataType, NamedTuple(\"AsContinuous\", [])):\n    \"\"\"\n    Reinterpret as a continuous variable (values that do not parse as\n    float are NaN).\n    \"\"\"\n    def __call__(self, vector: DataVector) -> RealVector:\n        var, _ = vector\n        if isinstance(var, Real):\n            return vector\n        elif isinstance(var, Categorical):\n            def data() -> MArray:\n                d = vector.data()\n                a = categorical_to_string_vector(d, var.values)\n                return MArray(as_float_or_nan(a, where=a.mask), mask=a.mask)\n            return RealVector(\n                Real(var.name, (6, 'g'), var.annotations, var.linked), data\n            )\n        elif isinstance(var, Time):\n            return RealVector(\n                Real(var.name, (6, 'g'), var.annotations, var.linked),\n                lambda: vector.data().astype(float)\n            )\n        elif isinstance(var, String):\n            def data():\n                s = vector.data()\n                return MArray(as_float_or_nan(s, where=s.mask), mask=s.mask)\n            return RealVector(\n                Real(var.name, (6, \"g\"), var.annotations, var.linked), data\n            )\n        raise AssertionError\n\n\nclass AsCategorical(_DataType, namedtuple(\"AsCategorical\", [])):\n    \"\"\"Reinterpret as a categorical variable\"\"\"\n    def __call__(self, vector: DataVector) -> CategoricalVector:\n        # this is the main complication in type transformation since we need\n        # the data and not just the variable description\n        var, _ = vector\n        if isinstance(var, Categorical):\n            return vector\n        if isinstance(var, (Real, Time, String)):\n            data, values = categorical_from_vector(vector.data())\n            return CategoricalVector(\n                Categorical(var.name, values, var.annotations, var.linked),\n                lambda: data\n            )\n        raise AssertionError\n\n\nclass AsTime(_DataType, namedtuple(\"AsTime\", [])):\n    \"\"\"Reinterpret as a datetime vector\"\"\"\n    def __call__(self, vector: DataVector) -> TimeVector:\n        var, _ = vector\n        if isinstance(var, Time):\n            return vector\n        elif isinstance(var, Real):\n            return TimeVector(\n                Time(var.name, var.annotations, var.linked),\n                lambda: vector.data().astype(\"M8[us]\")\n            )\n        elif isinstance(var, Categorical):\n            def data():\n                d = vector.data()\n                s = categorical_to_string_vector(d, var.values)\n                dt = pd.to_datetime(s, errors=\"coerce\").values.astype(\"M8[us]\")\n                return MArray(dt, mask=d.mask)\n            return TimeVector(\n                Time(var.name, var.annotations, var.linked), data\n            )\n        elif isinstance(var, String):\n            def data():\n                s = vector.data()\n                dt = pd.to_datetime(s, errors=\"coerce\").values.astype(\"M8[us]\")\n                return MArray(dt, mask=s.mask)\n            return TimeVector(\n                Time(var.name, var.annotations, var.linked), data\n            )\n        raise AssertionError\n\n\nReinterpretTransform = Union[AsCategorical, AsContinuous, AsTime, AsString]\nReinterpretTransformTypes = (AsCategorical, AsContinuous, AsTime, AsString)\n\n\ndef deconstruct(obj):\n    # type: (tuple) -> Tuple[str, Tuple[Any, ...]]\n    \"\"\"\n    Deconstruct a tuple subclass to its class name and its contents.\n\n    Parameters\n    ----------\n    obj : A tuple\n\n    Returns\n    -------\n    value: Tuple[str, Tuple[Any, ...]]\n    \"\"\"\n    cname = type(obj).__name__\n    args = tuple(obj)\n    return cname, args\n\n\ndef reconstruct(tname, args):\n    # type: (str, Tuple[Any, ...]) -> Tuple[Any, ...]\n    \"\"\"\n    Reconstruct a tuple subclass (inverse of deconstruct).\n\n    Parameters\n    ----------\n    tname : str\n        Type name\n    args : Tuple[Any, ...]\n\n    Returns\n    -------\n    rval: Tuple[Any, ...]\n    \"\"\"\n    try:\n        constructor = globals()[tname]\n    except KeyError:\n        raise NameError(tname)\n    return constructor(*args)\n\n\ndef formatter_for_dtype(dtype: np.dtype) -> Callable[[Any], str]:\n    if dtype.metadata is None:\n        return str\n    else:\n        return dtype.metadata.get(\"__formatter\", str)  # metadata abuse\n\n\ndef masked_unique(data: MArray) -> Tuple[MArray, ndarray]:\n    if not np.any(data.mask):\n        return np.ma.unique(data, return_inverse=True)\n    elif data.dtype.kind == \"O\":\n        # np.ma.unique does not work for object arrays\n        # (no ma.minimum_fill_value for object arrays)\n        # maybe sorted(set(data.data[...]))\n        unq = np.unique(data.data[~data.mask])\n        mapper = make_dict_mapper(\n            DictMissingConst(len(unq), ((v, i) for i, v in enumerate(unq)))\n        )\n        index = mapper(data.data)\n        unq = np.array(unq.tolist() + [data.fill_value], dtype=data.dtype)\n        unq_mask = [False] * unq.size\n        unq_mask[-1] = True\n        unq = MArray(unq, mask=unq_mask)\n        return unq, index\n    else:\n        unq, index = np.ma.unique(data, return_inverse=True)\n        assert not np.any(unq.mask[:-1]), \\\n            \"masked value if present must be in last position\"\n        return unq, index\n\n\ndef categorical_from_vector(data: MArray) -> Tuple[MArray, Tuple[str, ...]]:\n    formatter = formatter_for_dtype(data.dtype)\n    unq, index = categorize_unique(data)\n    if formatter is not str:\n        # str(np.array([0], \"M8[s]\")[0]) is different then\n        # str(np.array([0], \"M8[s]\").astype(object)[0]) which is what\n        # as_string is doing\n        names = tuple(map(formatter, unq.astype(object)))\n    else:\n        names = tuple(as_string(unq))\n    data = MArray(\n        index, mask=data.mask,\n        dtype=np.dtype(int, metadata={\n            \"__formater\": lambda i: names[i] if 0 <= i < unq.size else \"?\"\n        })\n    )\n    return data, names\n\n\ndef categorize_unique(data: MArray) -> Tuple[ndarray, MArray]:\n    unq, index = masked_unique(data)\n    if np.any(unq.mask):\n        unq = unq[:-1]\n        assert not np.any(unq.mask), \"masked value if present must be last\"\n    unq = unq.data\n    index[data.mask] = -1\n    index = MArray(index, mask=data.mask)\n    return unq, index\n\n\ndef categorical_to_string_vector(data: MArray, values: Tuple[str, ...]) -> MArray:\n    lookup = np.asarray(values, object)\n    out = np.full(data.shape, \"\", dtype=object)\n    mask_ = ~data.mask\n    out[mask_] = lookup[data.data[mask_]]\n    return MArray(out, mask=data.mask, fill_value=\"\")\n\n\n# Item models\n\n\nclass DictItemsModel(QStandardItemModel):\n    \"\"\"A Qt Item Model class displaying the contents of a python\n    dictionary.\n\n    \"\"\"\n    # Implement a proper model with in-place editing.\n    # (Maybe it should be a TableModel with 2 columns)\n    def __init__(self, parent=None, a_dict=None):\n        super().__init__(parent)\n        self._dict = {}\n        self.setHorizontalHeaderLabels([\"Key\", \"Value\"])\n        if a_dict is not None:\n            self.set_dict(a_dict)\n\n    def set_dict(self, a_dict):\n        # type: (Dict[str, str]) -> None\n        self._dict = a_dict\n        self.setRowCount(0)\n        for key, value in sorted(a_dict.items()):\n            key_item = QStandardItem(key)\n            value_item = QStandardItem(value)\n            key_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)\n            value_item.setFlags(value_item.flags() | Qt.ItemIsEditable)\n            self.appendRow([key_item, value_item])\n\n    def get_dict(self):\n        # type: () -> Dict[str, str]\n        rval = {}\n        for row in range(self.rowCount()):\n            key_item = self.item(row, 0)\n            value_item = self.item(row, 1)\n            rval[key_item.text()] = value_item.text()\n        return rval\n\n\nclass VariableEditor(QWidget):\n    \"\"\"\n    An editor widget for a variable.\n\n    Can edit the variable name, and its attributes dictionary.\n    \"\"\"\n    variable_changed = Signal()\n\n    def __init__(self, parent=None, **kwargs):\n        super().__init__(parent, **kwargs)\n        self.var = None  # type: Optional[Variable]\n\n        layout = QVBoxLayout()\n        self.setLayout(layout)\n\n        self.form = form = QFormLayout(\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,\n            objectName=\"editor-form-layout\"\n        )\n        layout.addLayout(self.form)\n\n        self.name_edit = QLineEdit(objectName=\"name-editor\")\n        self.name_edit.editingFinished.connect(\n            lambda: self.name_edit.isModified() and self.on_name_changed()\n        )\n        form.addRow(\"Name:\", self.name_edit)\n\n        self.unlink_var_cb = QCheckBox(\n            \"Unlink variable from its source variable\", self,\n            toolTip=\"Make Orange forget that the variable is derived from \"\n                    \"another.\\n\"\n                    \"Use this for instance when you want to consider variables \"\n                    \"with the same name but from different sources as the same \"\n                    \"variable.\"\n        )\n        self.unlink_var_cb.toggled.connect(self._set_unlink)\n        form.addRow(\"\", self.unlink_var_cb)\n\n        vlayout = QVBoxLayout(spacing=1)\n        self.labels_edit = view = QTreeView(\n            objectName=\"annotation-pairs-edit\",\n            rootIsDecorated=False,\n            editTriggers=QTreeView.DoubleClicked | QTreeView.EditKeyPressed,\n        )\n        self.labels_model = model = DictItemsModel()\n        view.setModel(model)\n\n        view.selectionModel().selectionChanged.connect(\n            self.on_label_selection_changed)\n\n        agrp = QActionGroup(view, objectName=\"annotate-action-group\")\n        action_add = QAction(\n            \"+\", self, objectName=\"action-add-label\",\n            toolTip=\"Add a new label.\",\n            shortcut=QKeySequence(QKeySequence.New),\n            shortcutContext=Qt.WidgetShortcut\n        )\n        action_delete = QAction(\n            \"\\N{MINUS SIGN}\", self, objectName=\"action-delete-label\",\n            toolTip=\"Remove selected label.\",\n            shortcut=QKeySequence(QKeySequence.Delete),\n            shortcutContext=Qt.WidgetShortcut\n        )\n        agrp.addAction(action_add)\n        agrp.addAction(action_delete)\n        view.addActions([action_add, action_delete])\n\n        def add_label():\n            row = [QStandardItem(), QStandardItem()]\n            model.appendRow(row)\n            idx = model.index(model.rowCount() - 1, 0)\n            view.setCurrentIndex(idx)\n            view.edit(idx)\n\n        def remove_label():\n            rows = view.selectionModel().selectedRows(0)\n            if rows:\n                assert len(rows) == 1\n                idx = rows[0].row()\n                model.removeRow(idx)\n\n        action_add.triggered.connect(add_label)\n        action_delete.triggered.connect(remove_label)\n        agrp.setEnabled(False)\n\n        self.add_label_action = action_add\n        self.remove_label_action = action_delete\n\n        # Necessary signals to know when the labels change\n        model.dataChanged.connect(self.on_labels_changed)\n        model.rowsInserted.connect(self.on_labels_changed)\n        model.rowsRemoved.connect(self.on_labels_changed)\n\n        vlayout.addWidget(self.labels_edit)\n        hlayout = QHBoxLayout()\n        hlayout.setContentsMargins(0, 0, 0, 0)\n        button = FixedSizeButton(\n            self, defaultAction=self.add_label_action,\n            accessibleName=\"Add\",\n        )\n        hlayout.addWidget(button)\n\n        button = FixedSizeButton(\n            self, defaultAction=self.remove_label_action,\n            accessibleName=\"Remove\",\n        )\n\n        hlayout.addWidget(button)\n        hlayout.addStretch(10)\n        vlayout.addLayout(hlayout)\n        form.addRow(\"Labels:\", vlayout)\n\n    def set_data(self, var, transform=()):\n        # type: (Optional[Variable], Sequence[Transform]) -> None\n        \"\"\"\n        Set the variable to edit.\n        \"\"\"\n        self.clear()\n        self.var = var\n        if var is not None:\n            name = var.name\n            annotations = var.annotations\n            unlink = False\n            for tr in transform:\n                if isinstance(tr, Rename):\n                    name = tr.name\n                elif isinstance(tr, Annotate):\n                    annotations = tr.annotations\n                elif isinstance(tr, Unlink):\n                    unlink = True\n            self.name_edit.setText(name)\n            self.labels_model.set_dict(dict(annotations))\n            self.add_label_action.actionGroup().setEnabled(True)\n            self.unlink_var_cb.setChecked(unlink)\n        else:\n            self.add_label_action.actionGroup().setEnabled(False)\n\n        self.unlink_var_cb.setDisabled(var is None or not var.linked)\n\n    def get_data(self):\n        \"\"\"Retrieve the modified variable.\n        \"\"\"\n        if self.var is None:\n            return None, []\n        name = self.name_edit.text().strip()\n        labels = tuple(sorted(self.labels_model.get_dict().items()))\n        tr = []\n        if self.var.name != name:\n            tr.append(Rename(name))\n        if self.var.annotations != labels:\n            tr.append(Annotate(labels))\n        if self.var.linked and self.unlink_var_cb.isChecked():\n            tr.append(Unlink())\n        return self.var, tr\n\n    def clear(self):\n        \"\"\"Clear the editor state.\n        \"\"\"\n        self.var = None\n        self.name_edit.setText(\"\")\n        self.labels_model.setRowCount(0)\n        self.unlink_var_cb.setChecked(False)\n\n    @Slot()\n    def on_name_changed(self):\n        self.variable_changed.emit()\n\n    @Slot()\n    def on_labels_changed(self):\n        self.variable_changed.emit()\n\n    @Slot()\n    def on_label_selection_changed(self):\n        selected = self.labels_edit.selectionModel().selectedRows()\n        self.remove_label_action.setEnabled(bool(len(selected)))\n\n    def _set_unlink(self, unlink):\n        self.unlink_var_cb.setChecked(unlink)\n        self.variable_changed.emit()\n\n\nclass GroupItemsDialog(QDialog):\n    \"\"\"\n    A dialog for group less frequent values.\n    \"\"\"\n    DEFAULT_LABEL = \"other\"\n\n    def __init__(\n            self, variable: Categorical,\n            data: Union[np.ndarray, List, MArray],\n            selected_attributes: List[str], dialog_settings: Dict[str, Any],\n            parent: QWidget = None, flags: Qt.WindowFlags = Qt.Dialog, **kwargs\n    ) -> None:\n        super().__init__(parent, flags, **kwargs)\n        self.variable = variable\n        self.data = data\n        self.selected_attributes = selected_attributes\n\n        # grouping strategy\n        self.selected_radio = radio1 = QRadioButton(\"Group selected values\")\n        self.frequent_abs_radio = radio2 = QRadioButton(\n            \"Group values with less than\"\n        )\n        self.frequent_rel_radio = radio3 = QRadioButton(\n            \"Group values with less than\"\n        )\n        self.n_values_radio = radio4 = QRadioButton(\n            \"Group all except\"\n        )\n\n        # if selected attributes available check the first radio button,\n        # otherwise disable it\n        if selected_attributes:\n            radio1.setChecked(True)\n        else:\n            radio1.setEnabled(False)\n            # they are remembered by number since radio button instance is\n            # new object for each dialog\n            checked = dialog_settings.get(\"selected_radio\", 0)\n            [radio2, radio3, radio4][checked].setChecked(True)\n\n        label2 = QLabel(\"occurrences\")\n        label3 = QLabel(\"occurrences\")\n        label4 = QLabel(\"most frequent values\")\n\n        self.frequent_abs_spin = spin2 = QSpinBox()\n        max_val = len(data)\n        spin2.setMinimum(1)\n        spin2.setMaximum(max_val)\n        spin2.setValue(dialog_settings.get(\"frequent_abs_spin\", 10))\n        spin2.setMinimumWidth(\n            self.fontMetrics().horizontalAdvance(\"X\") * (len(str(max_val)) + 1) + 20\n        )\n        spin2.valueChanged.connect(self._frequent_abs_spin_changed)\n\n        self.frequent_rel_spin = spin3 = QDoubleSpinBox()\n        spin3.setMinimum(0)\n        spin3.setDecimals(1)\n        spin3.setSingleStep(0.1)\n        spin3.setMaximum(100)\n        spin3.setValue(dialog_settings.get(\"frequent_rel_spin\", 10))\n        spin3.setMinimumWidth(self.fontMetrics().horizontalAdvance(\"X\") * (2 + 1) + 20)\n        spin3.setSuffix(\" %\")\n        spin3.valueChanged.connect(self._frequent_rel_spin_changed)\n\n        self.n_values_spin = spin4 = QSpinBox()\n        spin4.setMinimum(0)\n        spin4.setMaximum(len(variable.categories))\n        spin4.setValue(\n            dialog_settings.get(\n                \"n_values_spin\", min(10, len(variable.categories))\n            )\n        )\n        spin4.setMinimumWidth(\n            self.fontMetrics().horizontalAdvance(\"X\") * (len(str(max_val)) + 1) + 20\n        )\n        spin4.valueChanged.connect(self._n_values_spin_spin_changed)\n\n        grid_layout = QGridLayout()\n        # first row\n        grid_layout.addWidget(radio1, 0, 0, 1, 2)\n        # second row\n        grid_layout.addWidget(radio2, 1, 0, 1, 2)\n        grid_layout.addWidget(spin2, 1, 2)\n        grid_layout.addWidget(label2, 1, 3)\n        # third row\n        grid_layout.addWidget(radio3, 2, 0, 1, 2)\n        grid_layout.addWidget(spin3, 2, 2)\n        grid_layout.addWidget(label3, 2, 3)\n        # fourth row\n        grid_layout.addWidget(radio4, 3, 0)\n        grid_layout.addWidget(spin4, 3, 1)\n        grid_layout.addWidget(label4, 3, 2, 1, 2)\n\n        group_box = QGroupBox()\n        group_box.setLayout(grid_layout)\n\n        # grouped variable name\n        new_name_label = QLabel(\"New value name: \")\n        self.new_name_line_edit = n_line_edit = QLineEdit(\n            dialog_settings.get(\"name_line_edit\", self.DEFAULT_LABEL)\n        )\n        # it is shown gray when user removes the text and let user know that\n        # word others is default one\n        n_line_edit.setPlaceholderText(self.DEFAULT_LABEL)\n        name_hlayout = QHBoxLayout()\n        name_hlayout.addWidget(new_name_label)\n        name_hlayout.addWidget(n_line_edit)\n\n        # confirm_button = QPushButton(\"Apply\")\n        # cancel_button = QPushButton(\"Cancel\")\n        buttons = QDialogButtonBox(\n            orientation=Qt.Horizontal,\n            standardButtons=(QDialogButtonBox.Ok | QDialogButtonBox.Cancel),\n            objectName=\"dialog-button-box\",\n        )\n        buttons.accepted.connect(self.accept)\n        buttons.rejected.connect(self.reject)\n\n        # join components\n        self.setLayout(QVBoxLayout())\n        self.layout().addWidget(group_box)\n        self.layout().addLayout(name_hlayout)\n        self.layout().addWidget(buttons)\n        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)\n\n    def _frequent_abs_spin_changed(self) -> None:\n        self.frequent_abs_radio.setChecked(True)\n\n    def _n_values_spin_spin_changed(self) -> None:\n        self.n_values_radio.setChecked(True)\n\n    def _frequent_rel_spin_changed(self) -> None:\n        self.frequent_rel_radio.setChecked(True)\n\n    def get_merge_attributes(self) -> List[str]:\n        \"\"\"\n        Returns attributes that will be merged\n\n        Returns\n        -------\n        List of attributes' to be merged names\n        \"\"\"\n        if self.selected_radio.isChecked():\n            return self.selected_attributes\n\n        if isinstance(self.data, MArray):\n            non_nan = self.data[~self.data.mask]\n        elif isinstance(self.data, np.ndarray):\n            non_nan = self.data[~np.isnan(self.data)]\n        else:  # list\n            non_nan = [x for x in self.data if x is not None]\n\n        counts = Counter(non_nan)\n        if self.n_values_radio.isChecked():\n            keep_values = self.n_values_spin.value()\n            values = counts.most_common()[keep_values:]\n            indices = [i for i, _ in values]\n        elif self.frequent_abs_radio.isChecked():\n            indices = [v for v, c in counts.most_common()\n                       if c < self.frequent_abs_spin.value()]\n        else:  # self.frequent_rel_radio.isChecked():\n            n_all = sum(counts.values())\n            indices = [v for v, c in counts.most_common()\n                       if c / n_all * 100 < self.frequent_rel_spin.value()]\n\n        indices = np.array(indices, dtype=int)  # indices must be ints\n        return np.array(self.variable.categories)[indices].tolist()\n\n    def get_merged_value_name(self) -> str:\n        \"\"\"\n        Returns\n        -------\n        New label of merged values\n        \"\"\"\n        return self.new_name_line_edit.text() or self.DEFAULT_LABEL\n\n    def get_dialog_settings(self) -> Dict[str, Any]:\n        \"\"\"\n        Returns\n        -------\n        Return the dictionary with vlues set by user in each of the line edits\n        and selected radio button.\n        \"\"\"\n        settings_dict = {\n            \"frequent_abs_spin\": self.frequent_abs_spin.value(),\n            \"frequent_rel_spin\": self.frequent_rel_spin.value(),\n            \"n_values_spin\": self.n_values_spin.value(),\n            \"name_line_edit\": self.new_name_line_edit.text()\n        }\n        checked = [\n            i for i, s in enumerate(\n                [self.frequent_abs_radio,\n                 self.frequent_rel_radio,\n                 self.n_values_radio]\n            ) if s.isChecked()]\n        # when checked empty radio button for selected values is selected\n        # it is not stored in setting since its selection depends on users\n        # selection of values in list\n        if checked:\n            settings_dict[\"selected_radio\"] = checked[0]\n        return settings_dict\n\n\n@contextmanager\ndef disconnected(signal, slot, connection_type=Qt.AutoConnection):\n    signal.disconnect(slot)\n    try:\n        yield\n    finally:\n        signal.connect(slot, connection_type)\n\n\n#: In 'reordable' models holds the original position of the item\n#: (if applicable).\nSourcePosRole = Qt.UserRole\n#: The original name\nSourceNameRole = Qt.UserRole + 2\n\n#: The added/dropped state (type is ItemEditState)\nEditStateRole = Qt.UserRole + 1\n\n\nclass ItemEditState:\n    NoState = 0\n    Dropped = 1\n    Added = 2\n\n\n#: Role used to retrieve the count of 'key' values in the model.\nMultiplicityRole = Qt.UserRole + 0x67\n\n\nclass CountedListModel(itemmodels.PyListModel):\n    \"\"\"\n    A list model counting how many times unique `key` values appear in\n    the list.\n\n    The counts are cached and invalidated on any change to the model involving\n    the changes to `keyRoles`.\n    \"\"\"\n    #: cached counts\n    __counts_cache = None  # type: Optional[Counter]\n\n    def data(self, index, role=Qt.DisplayRole):\n        # type: (QModelIndex, int) -> Any\n        if role == MultiplicityRole:\n            key = self.key(index)\n            counts = self.__counts()\n            return counts.get(key, 1)\n        return super().data(index, role)\n\n    def setData(self, index, value, role=Qt.EditRole):\n        # type: (QModelIndex, Any, int)-> bool\n        rval = super().setData(index, value, role)\n        if role in self.keyRoles():\n            self.invalidateCounts()\n        return rval\n\n    def setItemData(self, index, data):\n        # type: (QModelIndex, Dict[int, Any]) -> bool\n        rval = super().setItemData(index, data)\n        if self.keyRoles().intersection(set(data.keys())):\n            self.invalidateCounts()\n        return rval\n\n    def endInsertRows(self):\n        super().endInsertRows()\n        self.invalidateCounts()\n\n    def endRemoveRows(self):\n        super().endRemoveRows()\n        self.invalidateCounts()\n\n    def endResetModel(self) -> None:\n        super().endResetModel()\n        self.invalidateCounts()\n\n    def invalidateCounts(self) -> None:\n        \"\"\"\n        Invalidate the cached counts.\n        \"\"\"\n        self.__counts_cache = None\n        # emit the change for the whole model\n        self.dataChanged.emit(\n            self.index(0), self.index(self.rowCount() - 1), [MultiplicityRole]\n        )\n\n    def __counts(self):\n        # type: () -> Counter\n        if self.__counts_cache is not None:\n            return self.__counts_cache\n        counts = Counter()\n        for index in map(self.index, range(self.rowCount())):\n            key = self.key(index)\n            try:\n                counts[key] += 1\n            except TypeError:  # pragma: no cover\n                warnings.warn(f\"key value '{key}' is not hashable\")\n        self.__counts_cache = counts\n        return self.__counts_cache\n\n    def key(self, index):\n        # type: (QModelIndex) -> Any\n        \"\"\"\n        Return the 'key' value that is to be counted.\n\n        The default implementation returns Qt.EditRole value for the index\n\n        Parameters\n        ----------\n        index : QModelIndex\n            The model index.\n\n        Returns\n        -------\n        key : Any\n        \"\"\"\n        return self.data(index, Qt.EditRole)\n\n    def keyRoles(self):\n        # type: () -> FrozenSet[int]\n        \"\"\"\n        Return a set of item roles on which `key` depends.\n\n        The counts are invalidated and recomputed whenever any of the roles in\n        this set changes.\n\n        By default the only role returned is Qt.EditRole\n        \"\"\"\n        return frozenset({Qt.EditRole})\n\n\nclass CountedStateModel(CountedListModel):\n    \"\"\"\n    Count by EditRole (name) and EditStateRole (ItemEditState)\n    \"\"\"\n    # The purpose is to count the items with target name only for\n    # ItemEditState.NoRole, i.e. excluding added/dropped values.\n    #\n    def key(self, index):  # type: (QModelIndex) -> Tuple[Any, Any]\n        # reimplemented\n        return self.data(index, Qt.EditRole), self.data(index, EditStateRole)\n\n    def keyRoles(self):  # type: () -> FrozenSet[int]\n        # reimplemented\n        return frozenset({Qt.EditRole, EditStateRole})\n\n\ndef mapRectTo(widget: QWidget, parent: QWidget, rect: QRect) -> QRect:  # pylint: disable=redefined-outer-name\n    return QRect(widget.mapTo(parent, rect.topLeft()), rect.size())\n\n\ndef mapRectToGlobal(widget: QWidget, rect: QRect) -> QRect:  # pylint: disable=redefined-outer-name\n    return QRect(widget.mapToGlobal(rect.topLeft()), rect.size())\n\n\nclass CategoriesEditDelegate(QStyledItemDelegate):\n    \"\"\"\n    Display delegate for editing categories.\n\n    Displayed items are styled for add, remove, merge and rename operations.\n    \"\"\"\n    def initStyleOption(self, option, index):\n        # type: (QStyleOptionViewItem, QModelIndex)-> None\n        super().initStyleOption(option, index)\n        text = str(index.data(Qt.EditRole))\n        sourcename = str(index.data(SourceNameRole))\n        editstate = index.data(EditStateRole)\n        counts = index.data(MultiplicityRole)\n        if not isinstance(counts, int):\n            counts = 1\n        suffix = None\n        if editstate == ItemEditState.Dropped:\n            option.state &= ~QStyle.State_Enabled\n            option.font.setStrikeOut(True)\n            text = sourcename\n            suffix = \"(dropped)\"\n        elif editstate == ItemEditState.Added:\n            suffix = \"(added)\"\n        else:\n            text = f\"{sourcename} \\N{RIGHTWARDS ARROW} {text}\"\n            if counts > 1:\n                suffix = \"(merged)\"\n        if suffix is not None:\n            text = text + \" \" + suffix\n        option.text = text\n\n    class CatEditComboBox(QComboBox):\n        prows: List[QPersistentModelIndex]\n\n    def createEditor(\n            self, parent: QWidget, option: 'QStyleOptionViewItem',\n            index: QModelIndex\n    ) -> QWidget:\n        view = option.widget\n        assert isinstance(view, QAbstractItemView)\n        selmodel = view.selectionModel()\n        rows = selmodel.selectedRows(0)\n        if len(rows) < 2:\n            return super().createEditor(parent, option, index)\n        # edit multiple selection\n        cb = CategoriesEditDelegate.CatEditComboBox(\n            editable=True, insertPolicy=QComboBox.InsertAtBottom)\n        cb.setParent(view, Qt.Popup)\n        cb.addItems(\n            list(unique(str(row.data(Qt.EditRole)) for row in rows)))\n        prows = [QPersistentModelIndex(row) for row in rows]\n        cb.prows = prows\n        return cb\n\n    def updateEditorGeometry(\n            self, editor: QWidget, option: 'QStyleOptionViewItem',\n            index: QModelIndex\n    ) -> None:\n        if isinstance(editor, CategoriesEditDelegate.CatEditComboBox):\n            view = cast(QAbstractItemView, option.widget)\n            view.scrollTo(index)\n            vport = view.viewport()\n            vrect = view.visualRect(index)\n            vrect = mapRectTo(vport, view, vrect)\n            vrect = vrect.intersected(vport.geometry())\n            vrect = mapRectToGlobal(vport, vrect)\n            size = editor.sizeHint().expandedTo(vrect.size())\n            editor.resize(size)\n            editor.move(vrect.topLeft())\n        else:\n            super().updateEditorGeometry(editor, option, index)\n\n    def setModelData(\n            self, editor: QWidget, model: QAbstractItemModel, index: QModelIndex\n    ) -> None:\n        if isinstance(editor, CategoriesEditDelegate.CatEditComboBox):\n            text = editor.currentText()\n            with signal_blocking(model):\n                for prow in editor.prows:\n                    if prow.isValid():\n                        model.setData(QModelIndex(prow), text, Qt.EditRole)\n            # this could be better\n            model.dataChanged.emit(\n                model.index(0, 0), model.index(model.rowCount() - 1, 0),\n                (Qt.EditRole,)\n            )\n        else:\n            super().setModelData(editor, model, index)\n\n\nclass DiscreteVariableEditor(VariableEditor):\n    \"\"\"An editor widget for editing a discrete variable.\n\n    Extends the :class:`VariableEditor` to enable editing of\n    variables values.\n    \"\"\"\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.merge_dialog_settings = {}\n        self._values = None\n\n        form = self.layout().itemAt(0)\n        assert isinstance(form, QFormLayout)\n        #: A list model of discrete variable's values.\n        self.values_model = CountedStateModel(\n            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable\n        )\n\n        vlayout = QVBoxLayout(spacing=1)\n        self.values_edit = QListView(\n            editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed,\n            selectionMode=QListView.ExtendedSelection,\n            uniformItemSizes=True,\n        )\n        self.values_edit.setItemDelegate(CategoriesEditDelegate(self))\n        self.values_edit.setModel(self.values_model)\n        self.values_model.dataChanged.connect(self.on_values_changed)\n\n        self.values_edit.selectionModel().selectionChanged.connect(\n            self.on_value_selection_changed)\n        self.values_model.layoutChanged.connect(self.on_value_selection_changed)\n        self.values_model.rowsMoved.connect(self.on_value_selection_changed)\n\n        vlayout.addWidget(self.values_edit)\n        hlayout = QHBoxLayout(spacing=1)\n\n        self.categories_action_group = group = QActionGroup(\n            self, objectName=\"action-group-categories\", enabled=False\n        )\n        self.move_value_up = QAction(\n            \"Move up\", group,\n            iconText=\"\\N{UPWARDS ARROW}\",\n            toolTip=\"Move the selected item up.\",\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier |\n                                  Qt.Key_BracketLeft),\n            shortcutContext=Qt.WidgetShortcut,\n        )\n        self.move_value_up.triggered.connect(self.move_up)\n\n        self.move_value_down = QAction(\n            \"Move down\", group,\n            iconText=\"\\N{DOWNWARDS ARROW}\",\n            toolTip=\"Move the selected item down.\",\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier |\n                                  Qt.Key_BracketRight),\n            shortcutContext=Qt.WidgetShortcut,\n        )\n        self.move_value_down.triggered.connect(self.move_down)\n\n        self.add_new_item = QAction(\n            \"Add\", group,\n            iconText=\"+\",\n            objectName=\"action-add-item\",\n            toolTip=\"Append a new item.\",\n            shortcut=QKeySequence(QKeySequence.New),\n            shortcutContext=Qt.WidgetShortcut,\n        )\n        self.remove_item = QAction(\n            \"Remove item\", group,\n            iconText=\"\\N{MINUS SIGN}\",\n            objectName=\"action-remove-item\",\n            toolTip=\"Delete the selected item.\",\n            shortcut=QKeySequence(QKeySequence.Delete),\n            shortcutContext=Qt.WidgetShortcut,\n        )\n        self.rename_selected_items = QAction(\n            \"Rename selected items\", group,\n            iconText=\"=\",\n            objectName=\"action-rename-selected-items\",\n            toolTip=\"Rename selected items.\",\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_Equal),\n            shortcutContext=Qt.WidgetShortcut,\n        )\n        self.merge_items = QAction(\n            \"Merge\", group,\n            iconText=\"M\",\n            objectName=\"action-activate-merge-dialog\",\n            toolTip=\"Merge infrequent items.\",\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.MetaModifier | Qt.Key_Equal),\n            shortcutContext=Qt.WidgetShortcut\n        )\n\n        self.add_new_item.triggered.connect(self._add_category)\n        self.remove_item.triggered.connect(self._remove_category)\n        self.rename_selected_items.triggered.connect(self._rename_selected_categories)\n        self.merge_items.triggered.connect(self._merge_categories)\n\n        button1 = FixedSizeButton(\n            self, defaultAction=self.move_value_up,\n            accessibleName=\"Move up\"\n        )\n        button2 = FixedSizeButton(\n            self, defaultAction=self.move_value_down,\n            accessibleName=\"Move down\"\n        )\n        button3 = FixedSizeButton(\n            self, defaultAction=self.add_new_item,\n            accessibleName=\"Add\"\n        )\n        button4 = FixedSizeButton(\n            self, defaultAction=self.remove_item,\n            accessibleName=\"Remove\"\n        )\n        button5 = FixedSizeButton(\n            self, defaultAction=self.rename_selected_items,\n            accessibleName=\"Merge selected items\"\n        )\n        button6 = FixedSizeButton(\n            self, defaultAction=self.merge_items,\n            accessibleName=\"Merge infrequent\",\n        )\n\n        self.values_edit.addActions([\n            self.move_value_up, self.move_value_down,\n            self.add_new_item, self.remove_item, self.rename_selected_items\n        ])\n        self.values_edit.setContextMenuPolicy(Qt.CustomContextMenu)\n\n        def context_menu(pos: QPoint):\n            viewport = self.values_edit.viewport()\n            menu = QMenu(self.values_edit)\n            menu.setAttribute(Qt.WA_DeleteOnClose)\n            menu.addActions([self.rename_selected_items, self.remove_item])\n            menu.popup(viewport.mapToGlobal(pos))\n        self.values_edit.customContextMenuRequested.connect(context_menu)\n\n        hlayout.addWidget(button1)\n        hlayout.addWidget(button2)\n        hlayout.addSpacing(3)\n        hlayout.addWidget(button3)\n        hlayout.addWidget(button4)\n        hlayout.addSpacing(3)\n        hlayout.addWidget(button5)\n        hlayout.addWidget(button6)\n\n        hlayout.addStretch(10)\n        vlayout.addLayout(hlayout)\n\n        form.insertRow(2, \"Values:\", vlayout)\n\n        QWidget.setTabOrder(self.name_edit, self.values_edit)\n        QWidget.setTabOrder(self.values_edit, button1)\n        QWidget.setTabOrder(button1, button2)\n        QWidget.setTabOrder(button2, button3)\n        QWidget.setTabOrder(button3, button4)\n        QWidget.setTabOrder(button4, button5)\n        QWidget.setTabOrder(button5, button6)\n\n    def set_data(self, var, transform=()):\n        raise NotImplementedError\n\n    def set_data_categorical(self, var, values, transform=()):\n        # type: (Optional[Categorical], Optional[Sequence[float]], Sequence[Transform]) -> None\n        \"\"\"\n        Set the variable to edit.\n\n        `values` is needed for categorical features to perform grouping.\n        \"\"\"\n        # pylint: disable=too-many-branches\n        super().set_data(var, transform=transform)\n        self._values = values\n        tr = None  # type: Optional[CategoriesMapping]\n        for tr_ in transform:\n            if isinstance(tr_, CategoriesMapping):\n                tr = tr_\n\n        items = []\n        if tr is not None:\n            ci_index = {c: i for i, c in enumerate(var.categories)}\n            for ci, cj in tr.mapping:\n                if ci is None and cj is not None:\n                    # level added\n                    item = {\n                        Qt.EditRole: cj,\n                        EditStateRole: ItemEditState.Added,\n                        SourcePosRole: None\n                    }\n                elif ci is not None and cj is None:\n                    # ci level dropped\n                    item = {\n                        Qt.EditRole: ci,\n                        EditStateRole: ItemEditState.Dropped,\n                        SourcePosRole: ci_index[ci],\n                        SourceNameRole: ci\n                    }\n                elif ci is not None and cj is not None:\n                    # rename or reorder\n                    item = {\n                        Qt.EditRole: cj,\n                        EditStateRole: ItemEditState.NoState,\n                        SourcePosRole: ci_index[ci],\n                        SourceNameRole: ci\n                    }\n                else:\n                    assert False, \"invalid mapping: {!r}\".format(tr.mapping)\n                items.append(item)\n        elif var is not None:\n            items = [\n                {Qt.EditRole: c,\n                 EditStateRole: ItemEditState.NoState,\n                 SourcePosRole: i,\n                 SourceNameRole: c}\n                for i, c in enumerate(var.categories)\n            ]\n        else:\n            items = []\n\n        with disconnected(self.values_model.dataChanged,\n                          self.on_values_changed):\n            self.values_model.clear()\n            self.values_model.insertRows(0, len(items))\n            for i, item in enumerate(items):\n                self.values_model.setItemData(\n                    self.values_model.index(i, 0),\n                    item\n                )\n\n        self.add_new_item.actionGroup().setEnabled(var is not None)\n\n    def __categories_mapping(self):\n        # type: () -> CategoriesMappingType\n        \"\"\"\n        Encode and return the current state as a CategoriesMappingType\n        \"\"\"\n        model = self.values_model\n        source = self.var.categories\n\n        res = []  # type: CategoriesMappingType\n        for i in range(model.rowCount()):\n            midx = model.index(i, 0)\n            category = midx.data(Qt.EditRole)\n            source_pos = midx.data(SourcePosRole)  # type: Optional[int]\n            if source_pos is not None:\n                source_name = source[source_pos]\n            else:\n                source_name = None\n            state = midx.data(EditStateRole)\n            if state == ItemEditState.Dropped:\n                res.append((source_name, None))\n            elif state == ItemEditState.Added:\n                res.append((None, category))\n            else:\n                res.append((source_name, category))\n        return res\n\n    def get_data(self):\n        \"\"\"Retrieve the modified variable\n        \"\"\"\n        var, tr = super().get_data()\n        if var is None:\n            return var, tr\n        mapping = self.__categories_mapping()\n        assert len(mapping) >= len(var.categories), f'{mapping}, {var}'\n        if any(_1 != _2 or _2 != _3\n               for (_1, _2), _3 in zip_longest(mapping, var.categories)):\n            tr.append(CategoriesMapping(mapping))\n        return var, tr\n\n    def clear(self):\n        \"\"\"Clear the model state.\n        \"\"\"\n        super().clear()\n        self.values_model.clear()\n\n    def move_rows(self, rows, offset):\n        if not rows:\n            return\n        assert len(rows) == 1\n        i = rows[0].row()\n        if offset > 0:\n            offset += 1\n        self.values_model.moveRows(QModelIndex(), i, 1, QModelIndex(), i + offset)\n        self.variable_changed.emit()\n\n    def move_up(self):\n        rows = self.values_edit.selectionModel().selectedRows()\n        self.move_rows(rows, -1)\n\n    def move_down(self):\n        rows = self.values_edit.selectionModel().selectedRows()\n        self.move_rows(rows, 1)\n\n    @Slot()\n    def on_values_changed(self):\n        self.variable_changed.emit()\n\n    @Slot()\n    def on_value_selection_changed(self):\n        rows = self.values_edit.selectionModel().selectedRows()\n        if len(rows) == 1:\n            i = rows[0].row()\n            self.move_value_up.setEnabled(i != 0)\n            self.move_value_down.setEnabled(i != self.values_model.rowCount() - 1)\n        else:\n            self.move_value_up.setEnabled(False)\n            self.move_value_down.setEnabled(False)\n\n    def _remove_category(self):\n        \"\"\"\n        Remove the current selected category.\n\n        If the item is an existing category present in the source variable it\n        is marked as removed in the view. But if it was added in the set\n        transformation it is removed entirely from the model and view.\n        \"\"\"\n        view = self.values_edit\n        rows = view.selectionModel().selectedRows(0)\n        if not rows:\n            return  # pragma: no cover\n        for index in rows:\n            model = index.model()\n            state = index.data(EditStateRole)\n            pos = index.data(SourcePosRole)\n            if pos is not None and pos >= 0:\n                # existing level -> only mark/toggle its dropped state,\n                model.setData(\n                    index,\n                    ItemEditState.Dropped if state != ItemEditState.Dropped\n                    else ItemEditState.NoState,\n                    EditStateRole)\n            elif state == ItemEditState.Added:\n                # new level -> remove it\n                model.removeRow(index.row())\n            else:\n                assert False, \"invalid state '{}' for {}\" \\\n                    .format(state, index.row())\n\n    def _add_category(self):\n        \"\"\"\n        Add a new category\n        \"\"\"\n        view = self.values_edit\n        model = view.model()\n\n        with disconnected(model.dataChanged, self.on_values_changed,\n                          Qt.UniqueConnection):\n            row = model.rowCount()\n            if not model.insertRow(model.rowCount()):\n                return\n            index = model.index(row, 0)\n            model.setItemData(\n                index, {\n                    Qt.EditRole: \"\",\n                    SourcePosRole: None,\n                    EditStateRole: ItemEditState.Added\n                }\n            )\n            view.setCurrentIndex(index)\n            view.edit(index)\n        self.on_values_changed()\n\n    def _merge_categories(self) -> None:\n        \"\"\"\n        Merge less common categories into one with the dialog for merge\n        selection.\n        \"\"\"\n        view = self.values_edit\n        model = view.model()  # type: QAbstractItemModel\n\n        selected_attributes = [ind.data(SourceNameRole)\n                               for ind in view.selectedIndexes()]\n        dlg = GroupItemsDialog(\n            self.var, self._values, selected_attributes,\n            self.merge_dialog_settings.get(self.var, {}), self,\n            windowTitle=\"Import Options\",\n            sizeGripEnabled=True,\n        )\n        dlg.setWindowModality(Qt.WindowModal)\n        status = dlg.exec()\n        dlg.deleteLater()\n        self.merge_dialog_settings[self.var] = dlg.get_dialog_settings()\n\n        rows = (model.index(i, 0) for i in range(model.rowCount()))\n\n        def complete_merge(text, merge_attributes):\n            # write the new text for edit role in all rows\n            with disconnected(model.dataChanged, self.on_values_changed):\n                for row in rows:\n                    if row.data(SourceNameRole) in merge_attributes:\n                        model.setData(row, text, Qt.EditRole)\n            self.variable_changed.emit()\n\n        if status == QDialog.Accepted:\n            complete_merge(\n                dlg.get_merged_value_name(), dlg.get_merge_attributes()\n            )\n\n    def _rename_selected_categories(self):\n        \"\"\"\n        Rename selected categories and merging them.\n\n        Popup an editable combo box for selection/edit of a new value.\n        \"\"\"\n        view = self.values_edit\n        selmodel = view.selectionModel()\n        index = view.currentIndex()\n        if not selmodel.isSelected(index):\n            indices = selmodel.selectedRows(0)\n            if indices:\n                index = indices[0]\n        # delegate to the CategoriesEditDelegate\n        view.edit(index)\n\n\nclass ContinuousVariableEditor(VariableEditor):\n    # TODO: enable editing of display format...\n    pass\n\n\nclass TimeVariableEditor(VariableEditor):\n    def __init__(self, parent=None, **kwargs):\n        super().__init__(parent, **kwargs)\n        form = self.layout().itemAt(0)\n\n        self.format_cb = QComboBox()\n        for item, data in [(\"Detect automatically\", (None, 1, 1))] + list(\n            Orange.data.TimeVariable.ADDITIONAL_FORMATS.items()\n        ):\n            self.format_cb.addItem(item, StrpTime(item, *data))\n        self.format_cb.currentIndexChanged.connect(self.variable_changed)\n        form.insertRow(2, \"Format:\", self.format_cb)\n\n    def set_data(self, var, transform=()):\n        super().set_data(var, transform)\n        if self.parent() is not None and isinstance(self.parent().var, Time):\n            # when transforming from time to time disable format selection combo\n            self.format_cb.setEnabled(False)\n        else:\n            # select the format from StrpTime transform\n            for tr in transform:\n                if isinstance(tr, StrpTime):\n                    index = self.format_cb.findText(tr.label)\n                    self.format_cb.setCurrentIndex(index)\n            self.format_cb.setEnabled(True)\n\n    def get_data(self):\n        var, tr = super().get_data()\n        if var is not None and (self.parent() is None or not isinstance(self.parent().var, Time)):\n            # do not add StrpTime when transforming from time to time\n            tr.insert(0, self.format_cb.currentData())\n        return var, tr\n\n\ndef variable_icon(var):\n    # type: (Union[Variable, Type[Variable], ReinterpretTransform]) -> QIcon\n    if not isinstance(var, type):\n        var = type(var)\n\n    if issubclass(var, (Categorical, AsCategorical)):\n        return gui.attributeIconDict[1]\n    elif issubclass(var, (Real, AsContinuous)):\n        return gui.attributeIconDict[2]\n    elif issubclass(var, (String, AsString)):\n        return gui.attributeIconDict[3]\n    elif issubclass(var, (Time, AsTime)):\n        return gui.attributeIconDict[4]\n    else:\n        return gui.attributeIconDict[-1]\n\n\n#: ItemDataRole storing the data vector transform\n#: (`List[Union[ReinterpretTransform, Transform]]`)\nTransformRole = Qt.UserRole + 42\n\n\nclass VariableEditDelegate(QStyledItemDelegate):\n    ReinterpretNames = {\n        AsCategorical: \"categorical\", AsContinuous: \"numeric\",\n        AsString: \"string\", AsTime: \"time\"\n    }\n\n    def initStyleOption(self, option, index):\n        # type: (QStyleOptionViewItem, QModelIndex) -> None\n        super().initStyleOption(option, index)\n        item = index.data(Qt.EditRole)\n        var = tr = None\n        if isinstance(item, DataVectorTypes):\n            var = item.vtype\n            option.icon = variable_icon(var)\n        if isinstance(item, VariableTypes):\n            var = item\n            option.icon = variable_icon(item)\n        elif isinstance(item, Orange.data.Variable):\n            var = item\n            option.icon = gui.attributeIconDict[var]\n\n        transform = index.data(TransformRole)\n        if not isinstance(transform, list):\n            transform = []\n\n        if transform and isinstance(transform[0], ReinterpretTransformTypes):\n            option.icon = variable_icon(transform[0])\n\n        if not option.icon.isNull():\n            option.features |= QStyleOptionViewItem.HasDecoration\n\n        if var is not None:\n            text = var.name\n            for tr in transform:\n                if isinstance(tr, Rename):\n                    text = (\"{} \\N{RIGHTWARDS ARROW} {}\"\n                            .format(var.name, tr.name))\n            for tr in transform:\n                if isinstance(tr, ReinterpretTransformTypes):\n                    text += f\" (reinterpreted as \" \\\n                            f\"{self.ReinterpretNames[type(tr)]})\"\n            option.text = text\n        if transform:\n            # mark as changed (maybe also change color, add text, ...)\n            option.font.setItalic(True)\n\n        multiplicity = index.data(MultiplicityRole)\n        if isinstance(multiplicity, int) and multiplicity > 1:\n            option.palette.setBrush(QPalette.Text, QBrush(Qt.red))\n            option.palette.setBrush(QPalette.HighlightedText, QBrush(Qt.red))\n\n    def helpEvent(self, event: QHelpEvent, view: QAbstractItemView,\n                  option: QStyleOptionViewItem, index: QModelIndex) -> bool:\n        multiplicity = index.data(MultiplicityRole)\n        name = VariableListModel.effective_name(index)\n        if isinstance(multiplicity, int) and multiplicity > 1 \\\n                and name is not None:\n            QToolTip.showText(\n                event.globalPos(), f\"Name `{name}` is duplicated\",\n                view.viewport()\n            )\n            return True\n        else:  # pragma: no cover\n            return super().helpEvent(event, view, option, index)\n\n\n# Item model for edited variables (Variable). Define a display role to be the\n# source variable name. This is used only in keyboard search. The display is\n# otherwise completely handled by a delegate.\nclass VariableListModel(CountedListModel):\n    def data(self, index, role=Qt.DisplayRole):\n        # type: (QModelIndex, Qt.ItemDataRole) -> Any\n        row = index.row()\n        if not index.isValid() or not 0 <= row < self.rowCount():\n            return None\n        if role == Qt.DisplayRole:\n            item = self[row]\n            if isinstance(item, VariableTypes):\n                return item.name\n            if isinstance(item, DataVectorTypes):\n                return item.vtype.name\n        return super().data(index, role)\n\n    def key(self, index):\n        return VariableListModel.effective_name(index)\n\n    def keyRoles(self):  # type: () -> FrozenSet[int]\n        return frozenset((Qt.DisplayRole, Qt.EditRole, TransformRole))\n\n    @staticmethod\n    def effective_name(index) -> Optional[str]:\n        item = index.data(Qt.EditRole)\n        if isinstance(item, DataVectorTypes):\n            var = item.vtype\n        elif isinstance(item, VariableTypes):\n            var = item\n        else:\n            return None\n        tr = index.data(TransformRole)\n        return effective_name(var, tr or [])\n\n\ndef effective_name(var: Variable, tr: Sequence[Transform]) -> str:\n    name = var.name\n    for t in tr:\n        if isinstance(t, Rename):\n            name = t.name\n    return name\n\n\nclass ReinterpretVariableEditor(VariableEditor):\n    \"\"\"\n    A 'compound' variable editor capable of variable type reinterpretations.\n    \"\"\"\n    _editors = {\n        Categorical: 0,\n        Real: 1,\n        String: 2,\n        Time: 3,\n        type(None): -1,\n    }\n\n    def __init__(self, parent=None, **kwargs):\n        # Explicitly skip VariableEditor's __init__, this is ugly but we have\n        # a completely different layout/logic as a compound editor (should\n        # really not subclass VariableEditor).\n        super(VariableEditor, self).__init__(parent, **kwargs)  # pylint: disable=bad-super-call\n        self.var = None  # type: Optional[Variable]\n        self.__transform = None  # type: Optional[ReinterpretTransform]\n        self.__data = None  # type: Optional[DataVector]\n        #: Stored transform state indexed by variable. Used to preserve state\n        #: between type switches.\n        self.__history = {}  # type: Dict[Variable, List[Transform]]\n\n        self.setLayout(QStackedLayout())\n\n        def decorate(editor: VariableEditor) -> VariableEditor:\n            \"\"\"insert an type combo box into a `editor`'s layout.\"\"\"\n            form = editor.layout().itemAt(0)\n            assert isinstance(form, QFormLayout)\n            typecb = QComboBox(objectName=\"type-combo\")\n            typecb.addItem(variable_icon(Categorical), \"Categorical\", Categorical)\n            typecb.addItem(variable_icon(Real), \"Numeric\", Real)\n            typecb.addItem(variable_icon(String), \"Text\", String)\n            typecb.addItem(variable_icon(Time), \"Time\", Time)\n            typecb.activated[int].connect(self.__reinterpret_activated)\n            form.insertRow(1, \"Type:\", typecb)\n            # Insert the typecb after name edit in the focus chain\n            name_edit = editor.findChild(QLineEdit, )\n            if name_edit is not None:\n                QWidget.setTabOrder(name_edit, typecb)\n            return editor\n        # This is ugly. Create an editor for each type and insert a type\n        # selection combo box into its layout. Switch between widgets\n        # on type change.\n        self.disc_edit = dedit = decorate(DiscreteVariableEditor())\n        cedit = decorate(ContinuousVariableEditor())\n        tedit = decorate(TimeVariableEditor())\n        sedit = decorate(VariableEditor())\n\n        for ed in [dedit, cedit, tedit, sedit]:\n            ed.variable_changed.connect(self.variable_changed)\n\n        self.layout().addWidget(dedit)\n        self.layout().addWidget(cedit)\n        self.layout().addWidget(sedit)\n        self.layout().addWidget(tedit)\n\n    def set_data(self, data, transform=()):  # pylint: disable=arguments-differ\n        # type: (Optional[DataVector], Sequence[Transform]) -> None\n        \"\"\"\n        Set the editor data.\n\n        Note\n        ----\n        This must be a `DataVector` as the vector's values are needed for type\n        reinterpretation/casts.\n\n        If the `transform` sequence contains ReinterpretTransform then it\n        must be in the first position.\n        \"\"\"\n        type_transform = None  # type: Optional[ReinterpretTransform]\n        if transform:\n            _tr = transform[0]\n            if isinstance(_tr, ReinterpretTransformTypes):\n                type_transform = _tr\n                transform = transform[1:]\n            assert not any(isinstance(t, ReinterpretTransformTypes)\n                           for t in transform)\n        self.__transform = type_transform\n        self.__data = data\n        self.var = data.vtype if data is not None else None\n\n        if type_transform is not None and data is not None:\n            data = type_transform(data)\n        if data is not None:\n            var = data.vtype\n        else:\n            var = None\n        index = self._editors.get(type(var), -1)\n        self.layout().setCurrentIndex(index)\n        if index != -1:\n            w = self.layout().currentWidget()\n            assert isinstance(w, VariableEditor)\n            if isinstance(var, Categorical):\n                w.set_data_categorical(var, data.data(), transform=transform)\n            else:\n                w.set_data(var, transform=transform)\n            self.__history[var] = tuple(transform)\n            cb = w.findChild(QComboBox, \"type-combo\")\n            cb.setCurrentIndex(index)\n\n    def get_data(self):\n        # type: () -> Tuple[Variable, Sequence[Transform]]\n        editor = self.layout().currentWidget()  # type: VariableEditor\n        var, tr = editor.get_data()\n        if type(var) != type(self.var):  # pylint: disable=unidiomatic-typecheck\n            assert self.__transform is not None\n            var = self.var\n            tr = [self.__transform, *tr]\n        return var, tr\n\n    def __reinterpret_activated(self, index):\n        layout = self.layout()\n        assert isinstance(layout, QStackedLayout)\n        if index == layout.currentIndex():\n            return\n        current = layout.currentWidget()\n        assert isinstance(current, VariableEditor)\n        Specific = {\n            Categorical: CategoricalTransformTypes\n        }\n        _var, _tr = current.get_data()\n        if _var is not None:\n            self.__history[_var] = _tr\n\n        var = self.var\n        transform = self.__transform\n        # take/preserve the general transforms that apply to all types\n        specific = Specific.get(type(var), ())\n        _tr = [t for t in _tr if not isinstance(t, specific)]\n\n        layout.setCurrentIndex(index)\n        w = layout.currentWidget()\n        cb = w.findChild(QComboBox, \"type-combo\")\n        cb.setCurrentIndex(index)\n        cb.setFocus()\n        target = cb.itemData(index, Qt.UserRole)\n        assert issubclass(target, VariableTypes)\n        if not isinstance(var, target):\n            if target == Real:\n                transform = AsContinuous()\n            elif target == Categorical:\n                transform = AsCategorical()\n            elif target == Time:\n                transform = AsTime()\n            elif target == String:\n                transform = AsString()\n        else:\n            transform = None\n            var = self.var\n\n        self.__transform = transform\n        data = None\n        if transform is not None and self.__data is not None:\n            data = transform(self.__data)\n            var = data.vtype\n\n        if var in self.__history:\n            tr = self.__history[var]\n        else:\n            tr = []\n        # type specific transform\n        specific = Specific.get(type(var), ())\n        # merge tr and _tr\n        tr = _tr + [t for t in tr if isinstance(t, specific)]\n        with disconnected(\n                w.variable_changed, self.variable_changed,\n                Qt.UniqueConnection\n        ):\n            if isinstance(w, DiscreteVariableEditor):\n                data = data or self.__data\n                w.set_data_categorical(var, data.data(), transform=tr)\n            else:\n                w.set_data(var, transform=tr)\n        self.variable_changed.emit()\n\n    def set_merge_context(self, merge_context):\n        self.disc_edit.merge_dialog_settings = merge_context\n\n    def get_merge_context(self):\n        return self.disc_edit.merge_dialog_settings\n\n\nclass OWEditDomain(widget.OWWidget):\n    name = \"Edit Domain\"\n    description = \"Rename variables, edit categories and variable annotations.\"\n    icon = \"icons/EditDomain.svg\"\n    priority = 3125\n    keywords = [\"rename\", \"drop\", \"reorder\", \"order\"]\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        data = Output(\"Data\", Orange.data.Table)\n\n    class Error(widget.OWWidget.Error):\n        duplicate_var_name = widget.Msg(\"A variable name is duplicated.\")\n\n    settingsHandler = settings.DomainContextHandler()\n    settings_version = 2\n\n    _domain_change_store = settings.ContextSetting({})\n    _selected_item = settings.ContextSetting(None)  # type: Optional[Tuple[str, int]]\n    _merge_dialog_settings = settings.ContextSetting({})\n    output_table_name = settings.ContextSetting(\"\")\n\n    want_main_area = False\n\n    def __init__(self):\n        super().__init__()\n        self.data = None  # type: Optional[Orange.data.Table]\n        #: The current selected variable index\n        self.selected_index = -1\n        self._invalidated = False\n        self.typeindex = 0\n\n        main = gui.hBox(self.controlArea, spacing=6)\n        box = gui.vBox(main, \"Variables\")\n\n        self.variables_model = VariableListModel(parent=self)\n        self.variables_view = self.domain_view = QListView(\n            selectionMode=QListView.SingleSelection,\n            uniformItemSizes=True,\n        )\n        self.variables_view.setItemDelegate(VariableEditDelegate(self))\n        self.variables_view.setModel(self.variables_model)\n        self.variables_view.selectionModel().selectionChanged.connect(\n            self._on_selection_changed\n        )\n        box.layout().addWidget(self.variables_view)\n\n        box = gui.vBox(main, \"Edit\")\n        self._editor = ReinterpretVariableEditor()\n        box.layout().addWidget(self._editor)\n\n        self.le_output_name = gui.lineEdit(\n            self.buttonsArea, self, \"output_table_name\", \"Output table name: \",\n            orientation=Qt.Horizontal)\n\n        gui.rubber(self.buttonsArea)\n\n        bbox = gui.hBox(self.buttonsArea)\n        breset_all = gui.button(\n            bbox, self, \"Reset All\",\n            objectName=\"button-reset-all\",\n            toolTip=\"Reset all variables to their input state.\",\n            autoDefault=False,\n            callback=self.reset_all\n        )\n        breset = gui.button(\n            bbox, self, \"Reset Selected\",\n            objectName=\"button-reset\",\n            toolTip=\"Rest selected variable to its input state.\",\n            autoDefault=False,\n            callback=self.reset_selected\n        )\n        bapply = gui.button(\n            bbox, self, \"Apply\",\n            objectName=\"button-apply\",\n            toolTip=\"Apply changes and commit data on output.\",\n            default=True,\n            autoDefault=False,\n            callback=self.commit\n        )\n\n        self.variables_view.setFocus(Qt.NoFocusReason)  # initial focus\n\n    @Inputs.data\n    def set_data(self, data):\n        \"\"\"Set input dataset.\"\"\"\n        self.closeContext()\n        self.clear()\n        self.data = data\n\n        if self.data is not None:\n            self.setup_model(data)\n            self.le_output_name.setPlaceholderText(data.name)\n            self.openContext(self.data)\n            self._editor.set_merge_context(self._merge_dialog_settings)\n            self._restore()\n        else:\n            self.le_output_name.setPlaceholderText(\"\")\n\n        self.commit()\n\n    def clear(self):\n        \"\"\"Clear the widget state.\"\"\"\n        self.data = None\n        self.variables_model.clear()\n        self.clear_editor()\n        assert self.selected_index == -1\n        self.selected_index = -1\n\n        self._selected_item = None\n        self._domain_change_store = {}\n        self._merge_dialog_settings = {}\n\n    def reset_selected(self):\n        \"\"\"Reset the currently selected variable to its original state.\"\"\"\n        ind = self.selected_var_index()\n        if ind >= 0:\n            model = self.variables_model\n            midx = model.index(ind)\n            var = midx.data(Qt.EditRole)\n            tr = midx.data(TransformRole)\n            if not tr:\n                return  # nothing to reset\n            editor = self._editor\n            with disconnected(editor.variable_changed,\n                              self._on_variable_changed):\n                model.setData(midx, [], TransformRole)\n                editor.set_data(var, transform=[])\n            self._invalidate()\n\n    def reset_all(self):\n        \"\"\"Reset all variables to their original state.\"\"\"\n        self._domain_change_store = {}\n        if self.data is not None:\n            model = self.variables_model\n            for i in range(model.rowCount()):\n                midx = model.index(i)\n                model.setData(midx, [], TransformRole)\n            index = self.selected_var_index()\n            if index >= 0:\n                self.open_editor(index)\n            self._invalidate()\n\n    def selected_var_index(self):\n        \"\"\"Return the current selected variable index.\"\"\"\n        rows = self.variables_view.selectedIndexes()\n        assert len(rows) <= 1\n        return rows[0].row() if rows else -1\n\n    def setup_model(self, data: Orange.data.Table):\n        model = self.variables_model\n        vars_ = []\n        columns = []\n        for i, _, var, coldata in enumerate_columns(data):\n            var = abstract(var)\n            vars_.append(var)\n            if isinstance(var, Categorical):\n                data = CategoricalVector(var, coldata)\n            elif isinstance(var, Real):\n                data = RealVector(var, coldata)\n            elif isinstance(var, Time):\n                data = TimeVector(var, coldata)\n            elif isinstance(var, String):\n                data = StringVector(var, coldata)\n            columns.append(data)\n\n        model[:] = vars_\n        for i, d in enumerate(columns):\n            model.setData(model.index(i), d, Qt.EditRole)\n\n    def _restore(self, ):\n        \"\"\"\n        Restore the edit transform from saved state.\n        \"\"\"\n        model = self.variables_model\n        for i in range(model.rowCount()):\n            midx = model.index(i, 0)\n            coldesc = model.data(midx, Qt.EditRole)  # type: DataVector\n            tr = self._restore_transform(coldesc.vtype)\n            if tr:\n                model.setData(midx, tr, TransformRole)\n\n        # Restore the current variable selection\n        i = -1\n        if self._selected_item is not None:\n            for i, vec in enumerate(model):\n                if vec.vtype.name_type() == self._selected_item:\n                    break\n        if i == -1 and model.rowCount():\n            i = 0\n\n        if i != -1:\n            itemmodels.select_row(self.variables_view, i)\n\n    def _on_selection_changed(self):\n        self.selected_index = self.selected_var_index()\n        if self.selected_index != -1:\n            self._selected_item = self.variables_model[self.selected_index].vtype.name_type()\n        else:\n            self._selected_item = None\n        self.open_editor(self.selected_index)\n\n    def open_editor(self, index):\n        # type: (int) -> None\n        self.clear_editor()\n        model = self.variables_model\n        if not 0 <= index < model.rowCount():\n            return\n        idx = model.index(index, 0)\n        vector = model.data(idx, Qt.EditRole)\n        tr = model.data(idx, TransformRole)\n        if tr is None:\n            tr = []\n        editor = self._editor\n        editor.set_data(vector, transform=tr)\n        editor.variable_changed.connect(\n            self._on_variable_changed, Qt.UniqueConnection\n        )\n\n    def clear_editor(self):\n        current = self._editor\n        try:\n            current.variable_changed.disconnect(self._on_variable_changed)\n        except TypeError:\n            pass\n        current.set_data(None)\n        current.layout().currentWidget().clear()\n\n    @Slot()\n    def _on_variable_changed(self):\n        \"\"\"User edited the current variable in editor.\"\"\"\n        assert 0 <= self.selected_index <= len(self.variables_model)\n        editor = self._editor\n        var, transform = editor.get_data()\n        model = self.variables_model\n        midx = model.index(self.selected_index, 0)\n        model.setData(midx, transform, TransformRole)\n        self._store_transform(var, transform)\n        self._invalidate()\n\n    def _store_transform(self, var, transform):\n        # type: (Variable, List[Transform]) -> None\n        self._domain_change_store[deconstruct(var)] = [deconstruct(t) for t in transform]\n\n    def _restore_transform(self, var):\n        # type: (Variable) -> List[Transform]\n        tr_ = self._domain_change_store.get(deconstruct(var), [])\n        tr = []\n\n        for t in tr_:\n            try:\n                tr.append(reconstruct(*t))\n            except (NameError, TypeError) as err:\n                warnings.warn(\n                    \"Failed to restore transform: {}, {!r}\".format(t, err),\n                    UserWarning, stacklevel=2\n                )\n        return tr\n\n    def _invalidate(self):\n        self._set_modified(True)\n\n    def _set_modified(self, state):\n        self._invalidated = state\n        b = self.findChild(QPushButton, \"button-apply\")\n        if isinstance(b, QPushButton):\n            f = b.font()\n            f.setItalic(state)\n            b.setFont(f)\n\n    def commit(self):\n        \"\"\"\n        Apply the changes to the input data and send the changed data to output.\n        \"\"\"\n        self._set_modified(False)\n        self.Error.duplicate_var_name.clear()\n\n        data = self.data\n        if data is None:\n            self.Outputs.data.send(None)\n            return\n        model = self.variables_model\n\n        def state(i):\n            # type: (int) -> Tuple[DataVector, List[Transform]]\n            midx = self.variables_model.index(i, 0)\n            return (model.data(midx, Qt.EditRole),\n                    model.data(midx, TransformRole))\n\n        state = [state(i) for i in range(model.rowCount())]\n        input_vars = data.domain.variables + data.domain.metas\n        if self.output_table_name in (\"\", data.name) \\\n                and not any(requires_transform(var, trs)\n                            for var, (_, trs) in zip(input_vars, state)):\n            self.Outputs.data.send(data)\n            return\n\n        assert all(v_.vtype.name == v.name\n                   for v, (v_, _) in zip(input_vars, state))\n        output_vars = []\n        unlinked_vars = []\n        unlink_domain = False\n        for (_, tr), v in zip(state, input_vars):\n            if tr:\n                var = apply_transform(v, data, tr)\n                if requires_unlink(v, tr):\n                    unlinked_var = var.copy(compute_value=None)\n                    unlink_domain = True\n                else:\n                    unlinked_var = var\n            else:\n                unlinked_var = var = v\n            output_vars.append(var)\n            unlinked_vars.append(unlinked_var)\n\n        if len(output_vars) != len({v.name for v in output_vars}):\n            self.Error.duplicate_var_name()\n            self.Outputs.data.send(None)\n            return\n\n        domain = data.domain\n        nx = len(domain.attributes)\n        ny = len(domain.class_vars)\n\n        def construct_domain(vars_list):\n            # Move non primitive Xs, Ys to metas (if they were changed)\n            Xs = [v for v in vars_list[:nx] if v.is_primitive()]\n            Ys = [v for v in vars_list[nx: nx + ny] if v.is_primitive()]\n            Ms = vars_list[nx + ny:] + \\\n                 [v for v in vars_list[:nx + ny] if not  v.is_primitive()]\n            return Orange.data.Domain(Xs, Ys, Ms)\n\n        domain = construct_domain(output_vars)\n        new_data = data.transform(domain)\n        if unlink_domain:\n            unlinked_domain = construct_domain(unlinked_vars)\n            new_data = new_data.from_numpy(\n                unlinked_domain,\n                new_data.X, new_data.Y, new_data.metas, new_data.W,\n                new_data.attributes, new_data.ids\n            )\n        if self.output_table_name:\n            new_data.name = self.output_table_name\n        self.Outputs.data.send(new_data)\n\n    def sizeHint(self):\n        sh = super().sizeHint()\n        return sh.expandedTo(QSize(660, 550))\n\n    def storeSpecificSettings(self):\n        \"\"\"\n        Update setting before context closes - also when widget closes.\n        \"\"\"\n        self._merge_dialog_settings = self._editor.get_merge_context()\n\n    def send_report(self):\n\n        if self.data is not None:\n            model = self.variables_model\n            state = ((model.data(midx, Qt.EditRole),\n                      model.data(midx, TransformRole))\n                     for i in range(model.rowCount())\n                     for midx in [model.index(i)])\n            parts = []\n            for vector, trs in state:\n                if trs:\n                    parts.append(report_transform(vector.vtype, trs))\n            if parts:\n                html = (\"<ul>\" +\n                        \"\".join(map(\"<li>{}</li>\".format, parts)) +\n                        \"</ul>\")\n            else:\n                html = \"No changes\"\n            self.report_raw(\"\", html)\n        else:\n            self.report_data(None)\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        # pylint: disable=bad-continuation\n        if version is None or version <= 1:\n            hints_ = context.values.get(\"domain_change_hints\", ({}, -2))[0]\n            store = []\n            ns = \"Orange.data.variable\"\n            mapping = {\n                \"DiscreteVariable\":\n                    lambda name, args, attrs:\n                        (\"Categorical\", (name, tuple(args[0][1]), ())),\n                \"TimeVariable\":\n                    lambda name, _, attrs:\n                        (\"Time\", (name, ())),\n                \"ContinuousVariable\":\n                    lambda name, _, attrs:\n                        (\"Real\", (name, (3, \"f\"), ())),\n                \"StringVariable\":\n                    lambda name, _, attrs:\n                        (\"String\", (name, ())),\n            }\n            for (module, class_name, *rest), target in hints_.items():\n                if module != ns:\n                    continue\n                f = mapping.get(class_name)\n                if f is None:\n                    continue\n                trs = []\n                key_mapped = f(*rest)\n                item_mapped = f(*target[2:])\n                src = reconstruct(*key_mapped)   # type: Variable\n                dst = reconstruct(*item_mapped)  # type: Variable\n                if src.name != dst.name:\n                    trs.append(Rename(dst.name))\n                if src.annotations != dst.annotations:\n                    trs.append(Annotate(dst.annotations))\n                if isinstance(src, Categorical):\n                    if src.categories != dst.categories:\n                        assert len(src.categories) == len(dst.categories)\n                        trs.append(CategoriesMapping(\n                            list(zip(src.categories, dst.categories))))\n                store.append((deconstruct(src), [deconstruct(tr) for tr in trs]))\n            context.values[\"_domain_change_store\"] = (dict(store), -2)\n\n\ndef enumerate_columns(\n        table: Orange.data.Table\n) -> Iterable[Tuple[int, str, Orange.data.Variable, Callable[[], ndarray]]]:\n    domain = table.domain\n    for i, (var, role) in enumerate(\n            chain(zip(domain.attributes, repeat(\"x\")),\n                  zip(domain.class_vars, repeat(\"y\")),\n                  zip(domain.metas, repeat(\"m\"))),\n    ):\n        if i >= len(domain.variables):\n            i = len(domain.variables) - i - 1\n        data = partial(table_column_data, table, i)\n        yield i, role, var, data\n\n\ndef table_column_data(\n        table: Orange.data.Table,\n        var: Union[Orange.data.Variable, int],\n        dtype=None\n) -> MArray:\n    col, copy = table.get_column_view(var)\n    var = table.domain[var]  # type: Orange.data.Variable\n    if var.is_primitive() and not np.issubdtype(col.dtype, np.inexact):\n        col = col.astype(float)\n        copy = True\n\n    if dtype is None:\n        if isinstance(var, Orange.data.TimeVariable):\n            dtype = np.dtype(\"M8[us]\")\n            col = col * 1e6\n        elif isinstance(var, Orange.data.ContinuousVariable):\n            dtype = np.dtype(float)\n        elif isinstance(var, Orange.data.DiscreteVariable):\n            _values = tuple(var.values)\n            _n_values = len(_values)\n            dtype = np.dtype(int, metadata={\n                \"__formatter\": lambda i: _values[i] if 0 <= i < _n_values else \"?\"\n            })\n        elif isinstance(var, Orange.data.StringVariable):\n            dtype = np.dtype(object)\n        else:\n            assert False\n    mask = orange_isna(var, col)\n\n    if dtype != col.dtype:\n        col = col.astype(dtype)\n        copy = True\n\n    if not copy:\n        col = col.copy()\n    return MArray(col, mask=mask)\n\n\ndef report_transform(var, trs):\n    # type: (Variable, List[Transform]) -> str\n    \"\"\"\n    Return a html fragment summarizing the changes applied by `trs` list.\n\n    Parameters\n    ----------\n    var : Variable\n        A variable descriptor no which trs operates\n    trs : List[Transform]\n        A non empty list of `Transform` instances.\n\n    Returns\n    -------\n    report : str\n    \"\"\"\n    # pylint: disable=too-many-branches\n    ReinterpretTypeCode = {\n        AsCategorical:  \"C\", AsContinuous: \"N\", AsString: \"S\", AsTime: \"T\",\n    }\n\n    def type_char(value: ReinterpretTransform) -> str:\n        return ReinterpretTypeCode.get(type(value), \"?\")\n\n    def strike(text):\n        return \"<s>{}</s>\".format(escape(text))\n\n    def i(text):\n        return \"<i>{}</i>\".format(escape(text))\n\n    def text(text):\n        return \"<span>{}</span>\".format(escape(text))\n    assert trs\n    rename = annotate = catmap = unlink = None\n    reinterpret = None\n\n    for tr in trs:\n        if isinstance(tr, Rename):\n            rename = tr\n        elif isinstance(tr, Annotate):\n            annotate = tr\n        elif isinstance(tr, CategoriesMapping):\n            catmap = tr\n        elif isinstance(tr, Unlink):\n            unlink = tr\n        elif isinstance(tr, ReinterpretTransformTypes):\n            reinterpret = tr\n\n    if reinterpret is not None:\n        header = \"{} → ({}) {}\".format(\n            var.name, type_char(reinterpret),\n            rename.name if rename is not None else var.name\n        )\n    elif rename is not None:\n        header = \"{} → {}\".format(var.name, rename.name)\n    else:\n        header = var.name\n    if unlink is not None:\n        header += \"(unlinked from source)\"\n\n    values_section = None\n    if catmap is not None:\n        values_section = (\"Values\", [])\n        lines = values_section[1]\n        for ci, cj in catmap.mapping:\n            if ci is None:\n                item = cj + (\"&nbsp;\" * 3) + \"(added)\"\n            elif cj is None:\n                item = strike(ci)\n            else:\n                item = ci + \" → \" + cj\n            lines.append(item)\n\n    annotate_section = None\n    if annotate is not None:\n        annotate_section = (\"Labels\", [])\n        lines = annotate_section[1]\n        old = dict(var.annotations)\n        new = dict(annotate.annotations)\n        for name in sorted(set(old) - set(new)):\n            lines.append(\n                \"<s>\" + i(name) + \" : \" + text(old[name]) + \"</s>\"\n            )\n        for name in sorted(set(new) - set(old)):\n            lines.append(\n                i(name) + \" : \" + text(new[name]) + \"&nbsp;\" * 3 + i(\"(new)\")\n            )\n\n        for name in sorted(set(new) & set(old)):\n            if new[name] != old[name]:\n                lines.append(\n                    i(name) + \" : \" + text(old[name]) + \" → \" + text(new[name])\n                )\n\n    html = [\"<div style='font-weight: bold;'>{}</div>\".format(header)]\n    for title, contents in filter(None, [values_section, annotate_section]):\n        section_header = \"<div>{}:</div>\".format(title)\n        section_contents = \"<br/>\\n\".join(contents)\n        html.append(section_header)\n        html.append(\n            \"<div style='padding-left: 1em;'>\" +\n            section_contents +\n            \"</div>\"\n        )\n    return \"\\n\".join(html)\n\n\ndef abstract(var):\n    # type: (Orange.data.Variable) -> Variable\n    \"\"\"\n    Return `Varaible` descriptor for an `Orange.data.Variable` instance.\n\n    Parameters\n    ----------\n    var : Orange.data.Variable\n\n    Returns\n    -------\n    var : Variable\n    \"\"\"\n    annotations = tuple(sorted(\n        (key, str(value))\n        for key, value in var.attributes.items()\n    ))\n    linked = var.compute_value is not None\n    if isinstance(var, Orange.data.DiscreteVariable):\n        return Categorical(var.name, tuple(var.values), annotations, linked)\n    elif isinstance(var, Orange.data.TimeVariable):\n        return Time(var.name, annotations, linked)\n    elif isinstance(var, Orange.data.ContinuousVariable):\n        return Real(var.name, (var.number_of_decimals, 'f'), annotations, linked)\n    elif isinstance(var, Orange.data.StringVariable):\n        return String(var.name, annotations, linked)\n    else:\n        raise TypeError\n\n\ndef _parse_attributes(mapping):\n    # type: (Iterable[Tuple[str, str]]) -> Dict[str, Any]\n    # Use the same functionality that parses attributes\n    # when reading text files\n    return Orange.data.Flags([\n        \"{}={}\".format(*item) for item in mapping\n    ]).attributes\n\n\ndef apply_transform(var, table, trs):\n    # type: (Orange.data.Variable, Orange.data.Table, List[Transform]) -> Orange.data.Variable\n    \"\"\"\n    Apply a list of `Transform` instances on an `Orange.data.Variable`.\n    \"\"\"\n    if trs and isinstance(trs[0], ReinterpretTransformTypes):\n        reinterpret, trs = trs[0], trs[1:]\n        coldata = table_column_data(table, var)\n        var = apply_reinterpret(var, reinterpret, coldata)\n    if trs:\n        return apply_transform_var(var, trs)\n    else:\n        return var\n\n\ndef requires_unlink(var: Orange.data.Variable, trs: List[Transform]) -> bool:\n    # Variable is only unlinked if it has compute_value  or if it has other\n    # transformations (that might had added compute_value)\n    return trs is not None \\\n           and any(isinstance(tr, Unlink) for tr in trs) \\\n           and (var.compute_value is not None or len(trs) > 1)\n\n\ndef requires_transform(var: Orange.data.Variable, trs: List[Transform]) -> bool:\n    # Unlink is treated separately: Unlink is required only if the variable\n    # has compute_value. Hence tranform is required if it has any\n    # transformations other than Unlink, or if unlink is indeed required.\n    return trs is not None and (\n        not all(isinstance(tr, Unlink) for tr in trs)\n        or requires_unlink(var, trs)\n    )\n\n\n@singledispatch\ndef apply_transform_var(var, trs):\n    # type: (Orange.data.Variable, List[Transform]) -> Orange.data.Variable\n    raise NotImplementedError\n\n\n@apply_transform_var.register(Orange.data.DiscreteVariable)\ndef apply_transform_discete(var, trs):\n    # type: (Orange.data.DiscreteVariable, List[Transform]) -> Orange.data.Variable\n    # pylint: disable=too-many-branches\n    name, annotations = var.name, var.attributes\n    mapping = None\n    for tr in trs:\n        if isinstance(tr, Rename):\n            name = tr.name\n        elif isinstance(tr, CategoriesMapping):\n            mapping = tr.mapping\n        elif isinstance(tr, Annotate):\n            annotations = _parse_attributes(tr.annotations)\n\n    source_values = var.values\n    if mapping is not None:\n        dest_values = list(unique(cj for ci, cj in mapping if cj is not None))\n    else:\n        dest_values = var.values\n\n    def positions(values):\n        rval = {c: i for i, c in enumerate(values)}\n        assert len(rval) == len(values)\n        return rval\n    source_codes = positions(source_values)\n    dest_codes = positions(dest_values)\n    if mapping is not None:\n        # construct a lookup table\n        lookup = np.full(len(source_values), np.nan, dtype=float)\n        for ci, cj in mapping:\n            if ci is not None and cj is not None:\n                i, j = source_codes[ci], dest_codes[cj]\n                lookup[i] = j\n        lookup = Lookup(var, lookup)\n    else:\n        lookup = Identity(var)\n    variable = Orange.data.DiscreteVariable(\n        name, values=dest_values, compute_value=lookup\n    )\n    variable.attributes.update(annotations)\n    return variable\n\n\n@apply_transform_var.register(Orange.data.ContinuousVariable)\ndef apply_transform_continuous(var, trs):\n    # type: (Orange.data.ContinuousVariable, List[Transform]) -> Orange.data.Variable\n    name, annotations = var.name, var.attributes\n    for tr in trs:\n        if isinstance(tr, Rename):\n            name = tr.name\n        elif isinstance(tr, Annotate):\n            annotations = _parse_attributes(tr.annotations)\n    variable = Orange.data.ContinuousVariable(\n        name=name, compute_value=Identity(var)\n    )\n    variable.attributes.update(annotations)\n    return variable\n\n\n@apply_transform_var.register(Orange.data.TimeVariable)\ndef apply_transform_time(var, trs):\n    # type: (Orange.data.TimeVariable, List[Transform]) -> Orange.data.Variable\n    name, annotations = var.name, var.attributes\n    for tr in trs:\n        if isinstance(tr, Rename):\n            name = tr.name\n        elif isinstance(tr, Annotate):\n            annotations = _parse_attributes(tr.annotations)\n    variable = Orange.data.TimeVariable(\n        name=name, have_date=var.have_date, have_time=var.have_time,\n        compute_value=Identity(var)\n    )\n    variable.attributes.update(annotations)\n    return variable\n\n\n@apply_transform_var.register(Orange.data.StringVariable)\ndef apply_transform_string(var, trs):\n    # type: (Orange.data.StringVariable, List[Transform]) -> Orange.data.Variable\n    name, annotations = var.name, var.attributes\n    out_type = Orange.data.StringVariable\n    compute_value = Identity\n    for tr in trs:\n        if isinstance(tr, Rename):\n            name = tr.name\n        elif isinstance(tr, Annotate):\n            annotations = _parse_attributes(tr.annotations)\n        elif isinstance(tr, StrpTime):\n            out_type = partial(\n                Orange.data.TimeVariable, have_date=tr.have_date, have_time=tr.have_time\n            )\n            compute_value = partial(ReparseTimeTransform, tr=tr)\n    variable = out_type(name=name, compute_value=compute_value(var))\n    variable.attributes.update(annotations)\n    return variable\n\n\ndef make_dict_mapper(\n        mapping: Mapping, dtype: Optional[DType] = None\n) -> Callable:\n    \"\"\"\n    Wrap a `mapping` into a callable ufunc-like function with\n    `out`, `dtype`, `where`, ... parameters. If `dtype` is passed to\n    `make_dict_mapper` it is used as a the default return dtype,\n    otherwise the default dtype is `object`.\n    \"\"\"\n    return frompyfunc(mapping.__getitem__, 1, 1, dtype)\n\n\nas_string = np.frompyfunc(str, 1, 1)\nparse_float = ftry(float, ValueError, float(\"nan\"))\n\n_parse_float = np.frompyfunc(parse_float, 1, 1)\n\n\ndef as_float_or_nan(\n        arr: ndarray, out: Optional[ndarray] = None, where: Optional[ndarray] = True,\n        dtype=None, **kwargs\n) -> ndarray:\n    \"\"\"\n    Convert elements of the input array using builtin `float`, fill elements\n    where conversion failed with NaN.\n    \"\"\"\n    if out is None:\n        out = np.full(arr.shape, np.nan, float if dtype is None else dtype)\n    if np.issubdtype(arr.dtype, np.inexact) or \\\n            np.issubdtype(arr.dtype, np.integer):\n        np.copyto(out, arr, casting=\"unsafe\", where=where)\n        return out\n    return _parse_float(arr, out, where=where, casting=\"unsafe\", **kwargs)\n\n\ndef copy_attributes(dst: V, src: Orange.data.Variable) -> V:\n    # copy `attributes` and `sparse` members from src to dst\n    dst.attributes = dict(src.attributes)\n    dst.sparse = src.sparse\n    return dst\n\n\n# Building (and applying) concrete type transformations on Table columns\n\n@singledispatch\ndef apply_reinterpret(var, tr, data):\n    # type: (Orange.data.Variable, ReinterpretTransform, MArray) -> Orange.data.Variable\n    \"\"\"\n    Apply a re-interpret transform to an `Orange.data.Table`'s column\n    \"\"\"\n    raise NotImplementedError\n\n\n@apply_reinterpret.register(Orange.data.DiscreteVariable)\ndef apply_reinterpret_d(var, tr, data):\n    # type: (Orange.data.DiscreteVariable, ReinterpretTransform, ndarray) -> Orange.data.Variable\n    if isinstance(tr, AsCategorical):\n        return var\n    elif isinstance(tr, (AsString, AsTime)):\n        # TimeVar will be interpreted by StrpTime later\n        f = Lookup(var, np.array(var.values, dtype=object), unknown=\"\")\n        rvar = Orange.data.StringVariable(name=var.name, compute_value=f)\n    elif isinstance(tr, AsContinuous):\n        f = Lookup(var, np.array(list(map(parse_float, var.values))),\n                   unknown=np.nan)\n        rvar = Orange.data.ContinuousVariable(\n            name=var.name, compute_value=f, sparse=var.sparse\n        )\n    else:\n        assert False\n    return copy_attributes(rvar, var)\n\n\n@apply_reinterpret.register(Orange.data.ContinuousVariable)\ndef apply_reinterpret_c(var, tr, data: MArray):\n    if isinstance(tr, AsCategorical):\n        # This is ill-defined and should not result in a 'compute_value'\n        # (post-hoc expunge from the domain once translated?)\n        values, index = categorize_unique(data)\n        coldata = index.astype(float)\n        coldata[index.mask] = np.nan\n        tr = LookupMappingTransform(\n            var, {v: i for i, v in enumerate(values)}, dtype=np.float64, unknown=np.nan\n        )\n        values = tuple(as_string(values))\n        rvar = Orange.data.DiscreteVariable(\n            name=var.name, values=values, compute_value=tr\n        )\n    elif isinstance(tr, AsContinuous):\n        return var\n    elif isinstance(tr, AsString):\n        # TimeVar will be interpreted by StrpTime later\n        tstr = ToStringTransform(var)\n        rvar = Orange.data.StringVariable(name=var.name, compute_value=tstr)\n    elif isinstance(tr, AsTime):\n        rvar = Orange.data.TimeVariable(name=var.name, compute_value=Identity(var))\n    else:\n        assert False\n    return copy_attributes(rvar, var)\n\n\n@apply_reinterpret.register(Orange.data.StringVariable)\ndef apply_reinterpret_s(var: Orange.data.StringVariable, tr, data: MArray):\n    if isinstance(tr, AsCategorical):\n        # This is ill-defined and should not result in a 'compute_value'\n        # (post-hoc expunge from the domain once translated?)\n        _, values = categorical_from_vector(data)\n        mapping = {v: float(i) for i, v in enumerate(values)}\n        tr = LookupMappingTransform(var, mapping)\n        rvar = Orange.data.DiscreteVariable(\n            name=var.name, values=values, compute_value=tr\n        )\n    elif isinstance(tr, AsContinuous):\n        rvar = Orange.data.ContinuousVariable(\n            var.name, compute_value=ToContinuousTransform(var)\n        )\n    elif isinstance(tr, (AsString, AsTime)):\n        # TimeVar will be interpreted by StrpTime later\n        return var\n    else:\n        assert False\n    return copy_attributes(rvar, var)\n\n\n@apply_reinterpret.register(Orange.data.TimeVariable)\ndef apply_reinterpret_t(var: Orange.data.TimeVariable, tr, data):\n    if isinstance(tr, AsCategorical):\n        values, _ = categorize_unique(data)\n        or_values = values.astype(float) / 1e6\n        mapping = {v: i for i, v in enumerate(or_values)}\n        tr = LookupMappingTransform(var, mapping)\n        values = tuple(as_string(values))\n        rvar = Orange.data.DiscreteVariable(\n            name=var.name, values=values, compute_value=tr\n        )\n    elif isinstance(tr, AsContinuous):\n        rvar = Orange.data.TimeVariable(\n            name=var.name, compute_value=Identity(var)\n        )\n    elif isinstance(tr, AsString):\n        rvar = Orange.data.StringVariable(\n            name=var.name, compute_value=ToStringTransform(var)\n        )\n    elif isinstance(tr, AsTime):\n        return var\n    else:\n        assert False\n    return copy_attributes(rvar, var)\n\n\ndef orange_isna(variable: Orange.data.Variable, data: ndarray) -> ndarray:\n    \"\"\"\n    Return a bool mask masking N/A elements in `data` for the `variable`.\n    \"\"\"\n    if variable.is_primitive():\n        return np.isnan(data)\n    else:\n        return data == variable.Unknown\n\n\nclass ToStringTransform(Transformation):\n    \"\"\"\n    Transform a variable to string.\n    \"\"\"\n    def transform(self, c):\n        if self.variable.is_string:\n            return c\n        elif self.variable.is_discrete or self.variable.is_time:\n            r = column_str_repr(self.variable, c)\n        elif self.variable.is_continuous:\n            r = as_string(c)\n        mask = orange_isna(self.variable, c)\n        return np.where(mask, \"\", r)\n\n\nclass ToContinuousTransform(Transformation):\n    def transform(self, c):\n        if self.variable.is_time:\n            return c\n        elif self.variable.is_continuous:\n            return c\n        elif self.variable.is_discrete:\n            lookup = Lookup(\n                self.variable, as_float_or_nan(self.variable.values),\n                unknown=np.nan\n            )\n            return lookup.transform(c)\n        elif self.variable.is_string:\n            return as_float_or_nan(c)\n        else:\n            raise TypeError\n\n\ndef datetime_to_epoch(dti: pd.DatetimeIndex, only_time) -> np.ndarray:\n    \"\"\"Convert datetime to epoch\"\"\"\n    delta = dti - (dti.normalize() if only_time else pd.Timestamp(\"1970-01-01\"))\n    return (delta / pd.Timedelta(\"1s\")).values\n\n\nclass ReparseTimeTransform(Transformation):\n    \"\"\"\n    Re-parse the column's string repr as datetime.\n    \"\"\"\n    def __init__(self, variable, tr):\n        super().__init__(variable)\n        self.tr = tr\n\n    def transform(self, c):\n        # if self.formats is none guess format option is selected\n        formats = self.tr.formats if self.tr.formats is not None else [None]\n        for f in formats:\n            d = pd.to_datetime(c, errors=\"coerce\", format=f)\n            if pd.notnull(d).any():\n                return datetime_to_epoch(d, only_time=not self.tr.have_date)\n        return np.nan\n\n\n# Alias for back compatibility (unpickling transforms)\nLookupMappingTransform = MappingTransform\n\n\n@singledispatch\ndef column_str_repr(var: Orange.data.Variable, coldata: ndarray) -> ndarray:\n    \"\"\"Return a array of str representations of coldata for the `variable.\"\"\"\n    _f = np.frompyfunc(var.repr_val, 1, 1)\n    return _f(coldata)\n\n\n@column_str_repr.register(Orange.data.DiscreteVariable)\ndef column_str_repr_discrete(\n        var: Orange.data.DiscreteVariable, coldata: ndarray\n) -> ndarray:\n    values = np.array(var.values, dtype=object)\n    lookup = Lookup(var, values, \"?\")\n    return lookup.transform(coldata)\n\n\n@column_str_repr.register(Orange.data.StringVariable)\ndef column_str_repr_string(\n        var: Orange.data.StringVariable, coldata: ndarray\n) -> ndarray:\n    return np.where(coldata == var.Unknown, \"?\", coldata)\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWEditDomain).run(Orange.data.Table(\"iris\"))\n",
            "new_file_content": "\"\"\"\nEdit Domain\n-----------\n\nA widget for manual editing of a domain's attributes.\n\n\"\"\"\nimport warnings\nfrom xml.sax.saxutils import escape\nfrom itertools import zip_longest, repeat, chain\nfrom contextlib import contextmanager\nfrom collections import namedtuple, Counter\nfrom functools import singledispatch, partial\nfrom typing import (\n    Tuple, List, Any, Optional, Union, Dict, Sequence, Iterable, NamedTuple,\n    FrozenSet, Type, Callable, TypeVar, Mapping, Hashable, cast\n)\n\nimport numpy as np\nimport pandas as pd\nfrom AnyQt.QtWidgets import (\n    QWidget, QListView, QTreeView, QVBoxLayout, QHBoxLayout, QFormLayout,\n    QLineEdit, QAction, QActionGroup, QGroupBox,\n    QStyledItemDelegate, QStyleOptionViewItem, QStyle, QSizePolicy,\n    QDialogButtonBox, QPushButton, QCheckBox, QComboBox, QStackedLayout,\n    QDialog, QRadioButton, QGridLayout, QLabel, QSpinBox, QDoubleSpinBox,\n    QAbstractItemView, QMenu, QToolTip\n)\nfrom AnyQt.QtGui import (\n    QStandardItemModel, QStandardItem, QKeySequence, QIcon, QBrush, QPalette,\n    QHelpEvent\n)\nfrom AnyQt.QtCore import (\n    Qt, QSize, QModelIndex, QAbstractItemModel, QPersistentModelIndex, QRect,\n    QPoint,\n)\nfrom AnyQt.QtCore import pyqtSignal as Signal, pyqtSlot as Slot\n\nimport Orange.data\n\nfrom Orange.preprocess.transformation import (\n    Transformation, Identity, Lookup, MappingTransform\n)\nfrom Orange.misc.collections import DictMissingConst\nfrom Orange.util import frompyfunc\nfrom Orange.widgets import widget, gui, settings\nfrom Orange.widgets.utils import itemmodels, ftry\nfrom Orange.widgets.utils.buttons import FixedSizeButton\nfrom Orange.widgets.utils.itemmodels import signal_blocking\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.widget import Input, Output\n\nndarray = np.ndarray  # pylint: disable=invalid-name\nMArray = np.ma.MaskedArray\nDType = Union[np.dtype, type]\n\nV = TypeVar(\"V\", bound=Orange.data.Variable)  # pylint: disable=invalid-name\nH = TypeVar(\"H\", bound=Hashable)  # pylint: disable=invalid-name\n\n\ndef unique(sequence: Iterable[H]) -> Iterable[H]:\n    \"\"\"\n    Return unique elements in `sequence`, preserving their (first seen) order.\n    \"\"\"\n    # depending on Python >= 3.6 'ordered' dict implementation detail.\n    return iter(dict.fromkeys(sequence))\n\n\nclass _DataType:\n    def __eq__(self, other):\n        \"\"\"Equal if `other` has the same type and all elements compare equal.\"\"\"\n        if type(self) is not type(other):\n            return False\n        return super().__eq__(other)\n\n    def __ne__(self, other):\n        return not self == other\n\n    def __hash__(self):\n        return hash((type(self), super().__hash__()))\n\n    def name_type(self):\n        \"\"\"\n        Returns a tuple with name and type of the variable.\n        It is used since it is forbidden to use names of variables in settings.\n        \"\"\"\n        type_number = {\n            \"Categorical\": 0,\n            \"Real\": 2,\n            \"Time\": 3,\n            \"String\": 4\n        }\n        return self.name, type_number[type(self).__name__]\n\n\n#: An ordered sequence of key, value pairs (variable annotations)\nAnnotationsType = Tuple[Tuple[str, str], ...]\n\n\n# Define abstract representation of the variable types edited\n\nclass Categorical(\n    _DataType, NamedTuple(\"Categorical\", [\n        (\"name\", str),\n        (\"categories\", Tuple[str, ...]),\n        (\"annotations\", AnnotationsType),\n        (\"linked\", bool)\n    ])): pass\n\n\nclass Real(\n    _DataType, NamedTuple(\"Real\", [\n        (\"name\", str),\n        # a precision (int, and a format specifier('f', 'g', or '')\n        (\"format\", Tuple[int, str]),\n        (\"annotations\", AnnotationsType),\n        (\"linked\", bool)\n    ])): pass\n\n\nclass String(\n    _DataType, NamedTuple(\"String\", [\n        (\"name\", str),\n        (\"annotations\", AnnotationsType),\n        (\"linked\", bool)\n    ])): pass\n\n\nclass Time(\n    _DataType, NamedTuple(\"Time\", [\n        (\"name\", str),\n        (\"annotations\", AnnotationsType),\n        (\"linked\", bool)\n    ])): pass\n\n\nVariable = Union[Categorical, Real, Time, String]\nVariableTypes = (Categorical, Real, Time, String)\n\n\n# Define variable transformations.\n\nclass Rename(_DataType, namedtuple(\"Rename\", [\"name\"])):\n    \"\"\"\n    Rename a variable.\n\n    Parameters\n    ----------\n    name : str\n        The new name\n    \"\"\"\n    def __call__(self, var):\n        # type: (Variable) -> Variable\n        return var._replace(name=self.name)\n\n\n#: Mapping of categories.\n#: A list of pairs with the first element the original value and the second\n#: element the new value. If the first element is None then a category level\n#: is added. If the second element is None than the corresponding first level\n#: is dropped. If there are duplicated elements on the right the corresponding\n#: categories on the left are merged.\n#: The mapped order is defined by the translated elements after None removal\n#: and merges (the first occurrence of a multiplied elements defines its\n#: position):\nCategoriesMappingType = List[Tuple[Optional[str], Optional[str]]]\n\n\nclass CategoriesMapping(_DataType, namedtuple(\"CategoriesMapping\", [\"mapping\"])):\n    \"\"\"\n    Change categories of a categorical variable.\n\n    Parameters\n    ----------\n    mapping : CategoriesMappingType\n    \"\"\"\n    def __call__(self, var):\n        # type: (Categorical) -> Categorical\n        cat = tuple(unique(cj for _, cj in self.mapping if cj is not None))\n        return var._replace(categories=cat)\n\n\nclass Annotate(_DataType, namedtuple(\"Annotate\", [\"annotations\"])):\n    \"\"\"\n    Replace variable annotations.\n    \"\"\"\n    def __call__(self, var):\n        return var._replace(annotations=self.annotations)\n\n\nclass Unlink(_DataType, namedtuple(\"Unlink\", [])):\n    \"\"\"Unlink variable from its source, that is, remove compute_value\"\"\"\n\n\nclass StrpTime(_DataType, namedtuple(\"StrpTime\", [\"label\", \"formats\", \"have_date\", \"have_time\"])):\n    \"\"\"Use format on variable interpreted as time\"\"\"\n\n\nTransform = Union[Rename, CategoriesMapping, Annotate, Unlink, StrpTime]\nTransformTypes = (Rename, CategoriesMapping, Annotate, Unlink, StrpTime)\n\nCategoricalTransformTypes = (CategoriesMapping, Unlink)\n\n\n# Reinterpret vector transformations.\nclass CategoricalVector(\n    _DataType, NamedTuple(\"CategoricalVector\", [\n        (\"vtype\", Categorical),\n        (\"data\", Callable[[], MArray]),\n    ])): ...\n\n\nclass RealVector(\n    _DataType, NamedTuple(\"RealVector\", [\n        (\"vtype\", Real),\n        (\"data\", Callable[[], MArray]),\n    ])): ...\n\n\nclass StringVector(\n    _DataType, NamedTuple(\"StringVector\", [\n        (\"vtype\", String),\n        (\"data\", Callable[[], MArray]),\n    ])): ...\n\n\nclass TimeVector(\n    _DataType, NamedTuple(\"TimeVector\", [\n        (\"vtype\", Time),\n        (\"data\", Callable[[], MArray]),\n    ])): ...\n\n\nDataVector = Union[CategoricalVector, RealVector, StringVector, TimeVector]\nDataVectorTypes = (CategoricalVector, RealVector, StringVector, TimeVector)\n\n\nclass AsString(_DataType, NamedTuple(\"AsString\", [])):\n    \"\"\"Reinterpret a data vector as a string.\"\"\"\n    def __call__(self, vector: DataVector) -> StringVector:\n        var, _ = vector\n        if isinstance(var, String):\n            return vector\n        return StringVector(\n            String(var.name, var.annotations, False),\n            lambda: as_string(vector.data()),\n        )\n\n\nclass AsContinuous(_DataType, NamedTuple(\"AsContinuous\", [])):\n    \"\"\"\n    Reinterpret as a continuous variable (values that do not parse as\n    float are NaN).\n    \"\"\"\n    def __call__(self, vector: DataVector) -> RealVector:\n        var, _ = vector\n        if isinstance(var, Real):\n            return vector\n        elif isinstance(var, Categorical):\n            def data() -> MArray:\n                d = vector.data()\n                a = categorical_to_string_vector(d, var.values)\n                return MArray(as_float_or_nan(a, where=a.mask), mask=a.mask)\n            return RealVector(\n                Real(var.name, (6, 'g'), var.annotations, var.linked), data\n            )\n        elif isinstance(var, Time):\n            return RealVector(\n                Real(var.name, (6, 'g'), var.annotations, var.linked),\n                lambda: vector.data().astype(float)\n            )\n        elif isinstance(var, String):\n            def data():\n                s = vector.data()\n                return MArray(as_float_or_nan(s, where=s.mask), mask=s.mask)\n            return RealVector(\n                Real(var.name, (6, \"g\"), var.annotations, var.linked), data\n            )\n        raise AssertionError\n\n\nclass AsCategorical(_DataType, namedtuple(\"AsCategorical\", [])):\n    \"\"\"Reinterpret as a categorical variable\"\"\"\n    def __call__(self, vector: DataVector) -> CategoricalVector:\n        # this is the main complication in type transformation since we need\n        # the data and not just the variable description\n        var, _ = vector\n        if isinstance(var, Categorical):\n            return vector\n        if isinstance(var, (Real, Time, String)):\n            data, values = categorical_from_vector(vector.data())\n            return CategoricalVector(\n                Categorical(var.name, values, var.annotations, var.linked),\n                lambda: data\n            )\n        raise AssertionError\n\n\nclass AsTime(_DataType, namedtuple(\"AsTime\", [])):\n    \"\"\"Reinterpret as a datetime vector\"\"\"\n    def __call__(self, vector: DataVector) -> TimeVector:\n        var, _ = vector\n        if isinstance(var, Time):\n            return vector\n        elif isinstance(var, Real):\n            return TimeVector(\n                Time(var.name, var.annotations, var.linked),\n                lambda: vector.data().astype(\"M8[us]\")\n            )\n        elif isinstance(var, Categorical):\n            def data():\n                d = vector.data()\n                s = categorical_to_string_vector(d, var.values)\n                dt = pd.to_datetime(s, errors=\"coerce\").values.astype(\"M8[us]\")\n                return MArray(dt, mask=d.mask)\n            return TimeVector(\n                Time(var.name, var.annotations, var.linked), data\n            )\n        elif isinstance(var, String):\n            def data():\n                s = vector.data()\n                dt = pd.to_datetime(s, errors=\"coerce\").values.astype(\"M8[us]\")\n                return MArray(dt, mask=s.mask)\n            return TimeVector(\n                Time(var.name, var.annotations, var.linked), data\n            )\n        raise AssertionError\n\n\nReinterpretTransform = Union[AsCategorical, AsContinuous, AsTime, AsString]\nReinterpretTransformTypes = (AsCategorical, AsContinuous, AsTime, AsString)\n\n\ndef deconstruct(obj):\n    # type: (tuple) -> Tuple[str, Tuple[Any, ...]]\n    \"\"\"\n    Deconstruct a tuple subclass to its class name and its contents.\n\n    Parameters\n    ----------\n    obj : A tuple\n\n    Returns\n    -------\n    value: Tuple[str, Tuple[Any, ...]]\n    \"\"\"\n    cname = type(obj).__name__\n    args = tuple(obj)\n    return cname, args\n\n\ndef reconstruct(tname, args):\n    # type: (str, Tuple[Any, ...]) -> Tuple[Any, ...]\n    \"\"\"\n    Reconstruct a tuple subclass (inverse of deconstruct).\n\n    Parameters\n    ----------\n    tname : str\n        Type name\n    args : Tuple[Any, ...]\n\n    Returns\n    -------\n    rval: Tuple[Any, ...]\n    \"\"\"\n    try:\n        constructor = globals()[tname]\n    except KeyError:\n        raise NameError(tname)\n    return constructor(*args)\n\n\ndef formatter_for_dtype(dtype: np.dtype) -> Callable[[Any], str]:\n    if dtype.metadata is None:\n        return str\n    else:\n        return dtype.metadata.get(\"__formatter\", str)  # metadata abuse\n\n\ndef masked_unique(data: MArray) -> Tuple[MArray, ndarray]:\n    if not np.any(data.mask):\n        return np.ma.unique(data, return_inverse=True)\n    elif data.dtype.kind == \"O\":\n        # np.ma.unique does not work for object arrays\n        # (no ma.minimum_fill_value for object arrays)\n        # maybe sorted(set(data.data[...]))\n        unq = np.unique(data.data[~data.mask])\n        mapper = make_dict_mapper(\n            DictMissingConst(len(unq), ((v, i) for i, v in enumerate(unq)))\n        )\n        index = mapper(data.data)\n        unq = np.array(unq.tolist() + [data.fill_value], dtype=data.dtype)\n        unq_mask = [False] * unq.size\n        unq_mask[-1] = True\n        unq = MArray(unq, mask=unq_mask)\n        return unq, index\n    else:\n        unq, index = np.ma.unique(data, return_inverse=True)\n        assert not np.any(unq.mask[:-1]), \\\n            \"masked value if present must be in last position\"\n        return unq, index\n\n\ndef categorical_from_vector(data: MArray) -> Tuple[MArray, Tuple[str, ...]]:\n    formatter = formatter_for_dtype(data.dtype)\n    unq, index = categorize_unique(data)\n    if formatter is not str:\n        # str(np.array([0], \"M8[s]\")[0]) is different then\n        # str(np.array([0], \"M8[s]\").astype(object)[0]) which is what\n        # as_string is doing\n        names = tuple(map(formatter, unq.astype(object)))\n    else:\n        names = tuple(as_string(unq))\n    data = MArray(\n        index, mask=data.mask,\n        dtype=np.dtype(int, metadata={\n            \"__formater\": lambda i: names[i] if 0 <= i < unq.size else \"?\"\n        })\n    )\n    return data, names\n\n\ndef categorize_unique(data: MArray) -> Tuple[ndarray, MArray]:\n    unq, index = masked_unique(data)\n    if np.any(unq.mask):\n        unq = unq[:-1]\n        assert not np.any(unq.mask), \"masked value if present must be last\"\n    unq = unq.data\n    index[data.mask] = -1\n    index = MArray(index, mask=data.mask)\n    return unq, index\n\n\ndef categorical_to_string_vector(data: MArray, values: Tuple[str, ...]) -> MArray:\n    lookup = np.asarray(values, object)\n    out = np.full(data.shape, \"\", dtype=object)\n    mask_ = ~data.mask\n    out[mask_] = lookup[data.data[mask_]]\n    return MArray(out, mask=data.mask, fill_value=\"\")\n\n\n# Item models\n\n\nclass DictItemsModel(QStandardItemModel):\n    \"\"\"A Qt Item Model class displaying the contents of a python\n    dictionary.\n\n    \"\"\"\n    # Implement a proper model with in-place editing.\n    # (Maybe it should be a TableModel with 2 columns)\n    def __init__(self, parent=None, a_dict=None):\n        super().__init__(parent)\n        self._dict = {}\n        self.setHorizontalHeaderLabels([\"Key\", \"Value\"])\n        if a_dict is not None:\n            self.set_dict(a_dict)\n\n    def set_dict(self, a_dict):\n        # type: (Dict[str, str]) -> None\n        self._dict = a_dict\n        self.setRowCount(0)\n        for key, value in sorted(a_dict.items()):\n            key_item = QStandardItem(key)\n            value_item = QStandardItem(value)\n            key_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)\n            value_item.setFlags(value_item.flags() | Qt.ItemIsEditable)\n            self.appendRow([key_item, value_item])\n\n    def get_dict(self):\n        # type: () -> Dict[str, str]\n        rval = {}\n        for row in range(self.rowCount()):\n            key_item = self.item(row, 0)\n            value_item = self.item(row, 1)\n            rval[key_item.text()] = value_item.text()\n        return rval\n\n\nclass VariableEditor(QWidget):\n    \"\"\"\n    An editor widget for a variable.\n\n    Can edit the variable name, and its attributes dictionary.\n    \"\"\"\n    variable_changed = Signal()\n\n    def __init__(self, parent=None, **kwargs):\n        super().__init__(parent, **kwargs)\n        self.var = None  # type: Optional[Variable]\n\n        layout = QVBoxLayout()\n        self.setLayout(layout)\n\n        self.form = form = QFormLayout(\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,\n            objectName=\"editor-form-layout\"\n        )\n        layout.addLayout(self.form)\n\n        self.name_edit = QLineEdit(objectName=\"name-editor\")\n        self.name_edit.editingFinished.connect(\n            lambda: self.name_edit.isModified() and self.on_name_changed()\n        )\n        form.addRow(\"Name:\", self.name_edit)\n\n        self.unlink_var_cb = QCheckBox(\n            \"Unlink variable from its source variable\", self,\n            toolTip=\"Make Orange forget that the variable is derived from \"\n                    \"another.\\n\"\n                    \"Use this for instance when you want to consider variables \"\n                    \"with the same name but from different sources as the same \"\n                    \"variable.\"\n        )\n        self.unlink_var_cb.toggled.connect(self._set_unlink)\n        form.addRow(\"\", self.unlink_var_cb)\n\n        vlayout = QVBoxLayout(spacing=1)\n        self.labels_edit = view = QTreeView(\n            objectName=\"annotation-pairs-edit\",\n            rootIsDecorated=False,\n            editTriggers=QTreeView.DoubleClicked | QTreeView.EditKeyPressed,\n        )\n        self.labels_model = model = DictItemsModel()\n        view.setModel(model)\n\n        view.selectionModel().selectionChanged.connect(\n            self.on_label_selection_changed)\n\n        agrp = QActionGroup(view, objectName=\"annotate-action-group\")\n        action_add = QAction(\n            \"+\", self, objectName=\"action-add-label\",\n            toolTip=\"Add a new label.\",\n            shortcut=QKeySequence(QKeySequence.New),\n            shortcutContext=Qt.WidgetShortcut\n        )\n        action_delete = QAction(\n            \"\\N{MINUS SIGN}\", self, objectName=\"action-delete-label\",\n            toolTip=\"Remove selected label.\",\n            shortcut=QKeySequence(QKeySequence.Delete),\n            shortcutContext=Qt.WidgetShortcut\n        )\n        agrp.addAction(action_add)\n        agrp.addAction(action_delete)\n        view.addActions([action_add, action_delete])\n\n        def add_label():\n            row = [QStandardItem(), QStandardItem()]\n            model.appendRow(row)\n            idx = model.index(model.rowCount() - 1, 0)\n            view.setCurrentIndex(idx)\n            view.edit(idx)\n\n        def remove_label():\n            rows = view.selectionModel().selectedRows(0)\n            if rows:\n                assert len(rows) == 1\n                idx = rows[0].row()\n                model.removeRow(idx)\n\n        action_add.triggered.connect(add_label)\n        action_delete.triggered.connect(remove_label)\n        agrp.setEnabled(False)\n\n        self.add_label_action = action_add\n        self.remove_label_action = action_delete\n\n        # Necessary signals to know when the labels change\n        model.dataChanged.connect(self.on_labels_changed)\n        model.rowsInserted.connect(self.on_labels_changed)\n        model.rowsRemoved.connect(self.on_labels_changed)\n\n        vlayout.addWidget(self.labels_edit)\n        hlayout = QHBoxLayout()\n        hlayout.setContentsMargins(0, 0, 0, 0)\n        button = FixedSizeButton(\n            self, defaultAction=self.add_label_action,\n            accessibleName=\"Add\",\n        )\n        hlayout.addWidget(button)\n\n        button = FixedSizeButton(\n            self, defaultAction=self.remove_label_action,\n            accessibleName=\"Remove\",\n        )\n\n        hlayout.addWidget(button)\n        hlayout.addStretch(10)\n        vlayout.addLayout(hlayout)\n        form.addRow(\"Labels:\", vlayout)\n\n    def set_data(self, var, transform=()):\n        # type: (Optional[Variable], Sequence[Transform]) -> None\n        \"\"\"\n        Set the variable to edit.\n        \"\"\"\n        self.clear()\n        self.var = var\n        if var is not None:\n            name = var.name\n            annotations = var.annotations\n            unlink = False\n            for tr in transform:\n                if isinstance(tr, Rename):\n                    name = tr.name\n                elif isinstance(tr, Annotate):\n                    annotations = tr.annotations\n                elif isinstance(tr, Unlink):\n                    unlink = True\n            self.name_edit.setText(name)\n            self.labels_model.set_dict(dict(annotations))\n            self.add_label_action.actionGroup().setEnabled(True)\n            self.unlink_var_cb.setChecked(unlink)\n        else:\n            self.add_label_action.actionGroup().setEnabled(False)\n\n        self.unlink_var_cb.setDisabled(var is None or not var.linked)\n\n    def get_data(self):\n        \"\"\"Retrieve the modified variable.\n        \"\"\"\n        if self.var is None:\n            return None, []\n        name = self.name_edit.text().strip()\n        labels = tuple(sorted(self.labels_model.get_dict().items()))\n        tr = []\n        if self.var.name != name:\n            tr.append(Rename(name))\n        if self.var.annotations != labels:\n            tr.append(Annotate(labels))\n        if self.var.linked and self.unlink_var_cb.isChecked():\n            tr.append(Unlink())\n        return self.var, tr\n\n    def clear(self):\n        \"\"\"Clear the editor state.\n        \"\"\"\n        self.var = None\n        self.name_edit.setText(\"\")\n        self.labels_model.setRowCount(0)\n        self.unlink_var_cb.setChecked(False)\n\n    @Slot()\n    def on_name_changed(self):\n        self.variable_changed.emit()\n\n    @Slot()\n    def on_labels_changed(self):\n        self.variable_changed.emit()\n\n    @Slot()\n    def on_label_selection_changed(self):\n        selected = self.labels_edit.selectionModel().selectedRows()\n        self.remove_label_action.setEnabled(bool(len(selected)))\n\n    def _set_unlink(self, unlink):\n        self.unlink_var_cb.setChecked(unlink)\n        self.variable_changed.emit()\n\n\nclass GroupItemsDialog(QDialog):\n    \"\"\"\n    A dialog for group less frequent values.\n    \"\"\"\n    DEFAULT_LABEL = \"other\"\n\n    def __init__(\n            self, variable: Categorical,\n            data: Union[np.ndarray, List, MArray],\n            selected_attributes: List[str], dialog_settings: Dict[str, Any],\n            parent: QWidget = None, flags: Qt.WindowFlags = Qt.Dialog, **kwargs\n    ) -> None:\n        super().__init__(parent, flags, **kwargs)\n        self.variable = variable\n        self.data = data\n        self.selected_attributes = selected_attributes\n\n        # grouping strategy\n        self.selected_radio = radio1 = QRadioButton(\"Group selected values\")\n        self.frequent_abs_radio = radio2 = QRadioButton(\n            \"Group values with less than\"\n        )\n        self.frequent_rel_radio = radio3 = QRadioButton(\n            \"Group values with less than\"\n        )\n        self.n_values_radio = radio4 = QRadioButton(\n            \"Group all except\"\n        )\n\n        # if selected attributes available check the first radio button,\n        # otherwise disable it\n        if selected_attributes:\n            radio1.setChecked(True)\n        else:\n            radio1.setEnabled(False)\n            # they are remembered by number since radio button instance is\n            # new object for each dialog\n            checked = dialog_settings.get(\"selected_radio\", 0)\n            [radio2, radio3, radio4][checked].setChecked(True)\n\n        label2 = QLabel(\"occurrences\")\n        label3 = QLabel(\"occurrences\")\n        label4 = QLabel(\"most frequent values\")\n\n        self.frequent_abs_spin = spin2 = QSpinBox()\n        max_val = len(data)\n        spin2.setMinimum(1)\n        spin2.setMaximum(max_val)\n        spin2.setValue(dialog_settings.get(\"frequent_abs_spin\", 10))\n        spin2.setMinimumWidth(\n            self.fontMetrics().horizontalAdvance(\"X\") * (len(str(max_val)) + 1) + 20\n        )\n        spin2.valueChanged.connect(self._frequent_abs_spin_changed)\n\n        self.frequent_rel_spin = spin3 = QDoubleSpinBox()\n        spin3.setMinimum(0)\n        spin3.setDecimals(1)\n        spin3.setSingleStep(0.1)\n        spin3.setMaximum(100)\n        spin3.setValue(dialog_settings.get(\"frequent_rel_spin\", 10))\n        spin3.setMinimumWidth(self.fontMetrics().horizontalAdvance(\"X\") * (2 + 1) + 20)\n        spin3.setSuffix(\" %\")\n        spin3.valueChanged.connect(self._frequent_rel_spin_changed)\n\n        self.n_values_spin = spin4 = QSpinBox()\n        spin4.setMinimum(0)\n        spin4.setMaximum(len(variable.categories))\n        spin4.setValue(\n            dialog_settings.get(\n                \"n_values_spin\", min(10, len(variable.categories))\n            )\n        )\n        spin4.setMinimumWidth(\n            self.fontMetrics().horizontalAdvance(\"X\") * (len(str(max_val)) + 1) + 20\n        )\n        spin4.valueChanged.connect(self._n_values_spin_spin_changed)\n\n        grid_layout = QGridLayout()\n        # first row\n        grid_layout.addWidget(radio1, 0, 0, 1, 2)\n        # second row\n        grid_layout.addWidget(radio2, 1, 0, 1, 2)\n        grid_layout.addWidget(spin2, 1, 2)\n        grid_layout.addWidget(label2, 1, 3)\n        # third row\n        grid_layout.addWidget(radio3, 2, 0, 1, 2)\n        grid_layout.addWidget(spin3, 2, 2)\n        grid_layout.addWidget(label3, 2, 3)\n        # fourth row\n        grid_layout.addWidget(radio4, 3, 0)\n        grid_layout.addWidget(spin4, 3, 1)\n        grid_layout.addWidget(label4, 3, 2, 1, 2)\n\n        group_box = QGroupBox()\n        group_box.setLayout(grid_layout)\n\n        # grouped variable name\n        new_name_label = QLabel(\"New value name: \")\n        self.new_name_line_edit = n_line_edit = QLineEdit(\n            dialog_settings.get(\"name_line_edit\", self.DEFAULT_LABEL)\n        )\n        # it is shown gray when user removes the text and let user know that\n        # word others is default one\n        n_line_edit.setPlaceholderText(self.DEFAULT_LABEL)\n        name_hlayout = QHBoxLayout()\n        name_hlayout.addWidget(new_name_label)\n        name_hlayout.addWidget(n_line_edit)\n\n        # confirm_button = QPushButton(\"Apply\")\n        # cancel_button = QPushButton(\"Cancel\")\n        buttons = QDialogButtonBox(\n            orientation=Qt.Horizontal,\n            standardButtons=(QDialogButtonBox.Ok | QDialogButtonBox.Cancel),\n            objectName=\"dialog-button-box\",\n        )\n        buttons.accepted.connect(self.accept)\n        buttons.rejected.connect(self.reject)\n\n        # join components\n        self.setLayout(QVBoxLayout())\n        self.layout().addWidget(group_box)\n        self.layout().addLayout(name_hlayout)\n        self.layout().addWidget(buttons)\n        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)\n\n    def _frequent_abs_spin_changed(self) -> None:\n        self.frequent_abs_radio.setChecked(True)\n\n    def _n_values_spin_spin_changed(self) -> None:\n        self.n_values_radio.setChecked(True)\n\n    def _frequent_rel_spin_changed(self) -> None:\n        self.frequent_rel_radio.setChecked(True)\n\n    def get_merge_attributes(self) -> List[str]:\n        \"\"\"\n        Returns attributes that will be merged\n\n        Returns\n        -------\n        List of attributes' to be merged names\n        \"\"\"\n        if self.selected_radio.isChecked():\n            return self.selected_attributes\n\n        if isinstance(self.data, MArray):\n            non_nan = self.data[~self.data.mask]\n        elif isinstance(self.data, np.ndarray):\n            non_nan = self.data[~np.isnan(self.data)]\n        else:  # list\n            non_nan = [x for x in self.data if x is not None]\n\n        counts = Counter(non_nan)\n        if self.n_values_radio.isChecked():\n            keep_values = self.n_values_spin.value()\n            values = counts.most_common()[keep_values:]\n            indices = [i for i, _ in values]\n        elif self.frequent_abs_radio.isChecked():\n            indices = [v for v, c in counts.most_common()\n                       if c < self.frequent_abs_spin.value()]\n        else:  # self.frequent_rel_radio.isChecked():\n            n_all = sum(counts.values())\n            indices = [v for v, c in counts.most_common()\n                       if c / n_all * 100 < self.frequent_rel_spin.value()]\n\n        indices = np.array(indices, dtype=int)  # indices must be ints\n        return np.array(self.variable.categories)[indices].tolist()\n\n    def get_merged_value_name(self) -> str:\n        \"\"\"\n        Returns\n        -------\n        New label of merged values\n        \"\"\"\n        return self.new_name_line_edit.text() or self.DEFAULT_LABEL\n\n    def get_dialog_settings(self) -> Dict[str, Any]:\n        \"\"\"\n        Returns\n        -------\n        Return the dictionary with vlues set by user in each of the line edits\n        and selected radio button.\n        \"\"\"\n        settings_dict = {\n            \"frequent_abs_spin\": self.frequent_abs_spin.value(),\n            \"frequent_rel_spin\": self.frequent_rel_spin.value(),\n            \"n_values_spin\": self.n_values_spin.value(),\n            \"name_line_edit\": self.new_name_line_edit.text()\n        }\n        checked = [\n            i for i, s in enumerate(\n                [self.frequent_abs_radio,\n                 self.frequent_rel_radio,\n                 self.n_values_radio]\n            ) if s.isChecked()]\n        # when checked empty radio button for selected values is selected\n        # it is not stored in setting since its selection depends on users\n        # selection of values in list\n        if checked:\n            settings_dict[\"selected_radio\"] = checked[0]\n        return settings_dict\n\n\n@contextmanager\ndef disconnected(signal, slot, connection_type=Qt.AutoConnection):\n    signal.disconnect(slot)\n    try:\n        yield\n    finally:\n        signal.connect(slot, connection_type)\n\n\n#: In 'reordable' models holds the original position of the item\n#: (if applicable).\nSourcePosRole = Qt.UserRole\n#: The original name\nSourceNameRole = Qt.UserRole + 2\n\n#: The added/dropped state (type is ItemEditState)\nEditStateRole = Qt.UserRole + 1\n\n\nclass ItemEditState:\n    NoState = 0\n    Dropped = 1\n    Added = 2\n\n\n#: Role used to retrieve the count of 'key' values in the model.\nMultiplicityRole = Qt.UserRole + 0x67\n\n\nclass CountedListModel(itemmodels.PyListModel):\n    \"\"\"\n    A list model counting how many times unique `key` values appear in\n    the list.\n\n    The counts are cached and invalidated on any change to the model involving\n    the changes to `keyRoles`.\n    \"\"\"\n    #: cached counts\n    __counts_cache = None  # type: Optional[Counter]\n\n    def data(self, index, role=Qt.DisplayRole):\n        # type: (QModelIndex, int) -> Any\n        if role == MultiplicityRole:\n            key = self.key(index)\n            counts = self.__counts()\n            return counts.get(key, 1)\n        return super().data(index, role)\n\n    def setData(self, index, value, role=Qt.EditRole):\n        # type: (QModelIndex, Any, int)-> bool\n        rval = super().setData(index, value, role)\n        if role in self.keyRoles():\n            self.invalidateCounts()\n        return rval\n\n    def setItemData(self, index, data):\n        # type: (QModelIndex, Dict[int, Any]) -> bool\n        rval = super().setItemData(index, data)\n        if self.keyRoles().intersection(set(data.keys())):\n            self.invalidateCounts()\n        return rval\n\n    def endInsertRows(self):\n        super().endInsertRows()\n        self.invalidateCounts()\n\n    def endRemoveRows(self):\n        super().endRemoveRows()\n        self.invalidateCounts()\n\n    def endResetModel(self) -> None:\n        super().endResetModel()\n        self.invalidateCounts()\n\n    def invalidateCounts(self) -> None:\n        \"\"\"\n        Invalidate the cached counts.\n        \"\"\"\n        self.__counts_cache = None\n        # emit the change for the whole model\n        self.dataChanged.emit(\n            self.index(0), self.index(self.rowCount() - 1), [MultiplicityRole]\n        )\n\n    def __counts(self):\n        # type: () -> Counter\n        if self.__counts_cache is not None:\n            return self.__counts_cache\n        counts = Counter()\n        for index in map(self.index, range(self.rowCount())):\n            key = self.key(index)\n            try:\n                counts[key] += 1\n            except TypeError:  # pragma: no cover\n                warnings.warn(f\"key value '{key}' is not hashable\")\n        self.__counts_cache = counts\n        return self.__counts_cache\n\n    def key(self, index):\n        # type: (QModelIndex) -> Any\n        \"\"\"\n        Return the 'key' value that is to be counted.\n\n        The default implementation returns Qt.EditRole value for the index\n\n        Parameters\n        ----------\n        index : QModelIndex\n            The model index.\n\n        Returns\n        -------\n        key : Any\n        \"\"\"\n        return self.data(index, Qt.EditRole)\n\n    def keyRoles(self):\n        # type: () -> FrozenSet[int]\n        \"\"\"\n        Return a set of item roles on which `key` depends.\n\n        The counts are invalidated and recomputed whenever any of the roles in\n        this set changes.\n\n        By default the only role returned is Qt.EditRole\n        \"\"\"\n        return frozenset({Qt.EditRole})\n\n\nclass CountedStateModel(CountedListModel):\n    \"\"\"\n    Count by EditRole (name) and EditStateRole (ItemEditState)\n    \"\"\"\n    # The purpose is to count the items with target name only for\n    # ItemEditState.NoRole, i.e. excluding added/dropped values.\n    #\n    def key(self, index):  # type: (QModelIndex) -> Tuple[Any, Any]\n        # reimplemented\n        return self.data(index, Qt.EditRole), self.data(index, EditStateRole)\n\n    def keyRoles(self):  # type: () -> FrozenSet[int]\n        # reimplemented\n        return frozenset({Qt.EditRole, EditStateRole})\n\n\ndef mapRectTo(widget: QWidget, parent: QWidget, rect: QRect) -> QRect:  # pylint: disable=redefined-outer-name\n    return QRect(widget.mapTo(parent, rect.topLeft()), rect.size())\n\n\ndef mapRectToGlobal(widget: QWidget, rect: QRect) -> QRect:  # pylint: disable=redefined-outer-name\n    return QRect(widget.mapToGlobal(rect.topLeft()), rect.size())\n\n\nclass CategoriesEditDelegate(QStyledItemDelegate):\n    \"\"\"\n    Display delegate for editing categories.\n\n    Displayed items are styled for add, remove, merge and rename operations.\n    \"\"\"\n    def initStyleOption(self, option, index):\n        # type: (QStyleOptionViewItem, QModelIndex)-> None\n        super().initStyleOption(option, index)\n        text = str(index.data(Qt.EditRole))\n        sourcename = str(index.data(SourceNameRole))\n        editstate = index.data(EditStateRole)\n        counts = index.data(MultiplicityRole)\n        if not isinstance(counts, int):\n            counts = 1\n        suffix = None\n        if editstate == ItemEditState.Dropped:\n            option.state &= ~QStyle.State_Enabled\n            option.font.setStrikeOut(True)\n            text = sourcename\n            suffix = \"(dropped)\"\n        elif editstate == ItemEditState.Added:\n            suffix = \"(added)\"\n        else:\n            text = f\"{sourcename} \\N{RIGHTWARDS ARROW} {text}\"\n            if counts > 1:\n                suffix = \"(merged)\"\n        if suffix is not None:\n            text = text + \" \" + suffix\n        option.text = text\n\n    class CatEditComboBox(QComboBox):\n        prows: List[QPersistentModelIndex]\n\n    def createEditor(\n            self, parent: QWidget, option: 'QStyleOptionViewItem',\n            index: QModelIndex\n    ) -> QWidget:\n        view = option.widget\n        assert isinstance(view, QAbstractItemView)\n        selmodel = view.selectionModel()\n        rows = selmodel.selectedRows(0)\n        if len(rows) < 2:\n            return super().createEditor(parent, option, index)\n        # edit multiple selection\n        cb = CategoriesEditDelegate.CatEditComboBox(\n            editable=True, insertPolicy=QComboBox.InsertAtBottom)\n        cb.setParent(view, Qt.Popup)\n        cb.addItems(\n            list(unique(str(row.data(Qt.EditRole)) for row in rows)))\n        prows = [QPersistentModelIndex(row) for row in rows]\n        cb.prows = prows\n        return cb\n\n    def updateEditorGeometry(\n            self, editor: QWidget, option: 'QStyleOptionViewItem',\n            index: QModelIndex\n    ) -> None:\n        if isinstance(editor, CategoriesEditDelegate.CatEditComboBox):\n            view = cast(QAbstractItemView, option.widget)\n            view.scrollTo(index)\n            vport = view.viewport()\n            vrect = view.visualRect(index)\n            vrect = mapRectTo(vport, view, vrect)\n            vrect = vrect.intersected(vport.geometry())\n            vrect = mapRectToGlobal(vport, vrect)\n            size = editor.sizeHint().expandedTo(vrect.size())\n            editor.resize(size)\n            editor.move(vrect.topLeft())\n        else:\n            super().updateEditorGeometry(editor, option, index)\n\n    def setModelData(\n            self, editor: QWidget, model: QAbstractItemModel, index: QModelIndex\n    ) -> None:\n        if isinstance(editor, CategoriesEditDelegate.CatEditComboBox):\n            text = editor.currentText()\n            with signal_blocking(model):\n                for prow in editor.prows:\n                    if prow.isValid():\n                        model.setData(QModelIndex(prow), text, Qt.EditRole)\n            # this could be better\n            model.dataChanged.emit(\n                model.index(0, 0), model.index(model.rowCount() - 1, 0),\n                (Qt.EditRole,)\n            )\n        else:\n            super().setModelData(editor, model, index)\n\n\nclass DiscreteVariableEditor(VariableEditor):\n    \"\"\"An editor widget for editing a discrete variable.\n\n    Extends the :class:`VariableEditor` to enable editing of\n    variables values.\n    \"\"\"\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.merge_dialog_settings = {}\n        self._values = None\n\n        form = self.layout().itemAt(0)\n        assert isinstance(form, QFormLayout)\n        #: A list model of discrete variable's values.\n        self.values_model = CountedStateModel(\n            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable\n        )\n\n        vlayout = QVBoxLayout(spacing=1)\n        self.values_edit = QListView(\n            editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed,\n            selectionMode=QListView.ExtendedSelection,\n            uniformItemSizes=True,\n        )\n        self.values_edit.setItemDelegate(CategoriesEditDelegate(self))\n        self.values_edit.setModel(self.values_model)\n        self.values_model.dataChanged.connect(self.on_values_changed)\n\n        self.values_edit.selectionModel().selectionChanged.connect(\n            self.on_value_selection_changed)\n        self.values_model.layoutChanged.connect(self.on_value_selection_changed)\n        self.values_model.rowsMoved.connect(self.on_value_selection_changed)\n\n        vlayout.addWidget(self.values_edit)\n        hlayout = QHBoxLayout(spacing=1)\n\n        self.categories_action_group = group = QActionGroup(\n            self, objectName=\"action-group-categories\", enabled=False\n        )\n        self.move_value_up = QAction(\n            \"Move up\", group,\n            iconText=\"\\N{UPWARDS ARROW}\",\n            toolTip=\"Move the selected item up.\",\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier |\n                                  Qt.Key_BracketLeft),\n            shortcutContext=Qt.WidgetShortcut,\n        )\n        self.move_value_up.triggered.connect(self.move_up)\n\n        self.move_value_down = QAction(\n            \"Move down\", group,\n            iconText=\"\\N{DOWNWARDS ARROW}\",\n            toolTip=\"Move the selected item down.\",\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier |\n                                  Qt.Key_BracketRight),\n            shortcutContext=Qt.WidgetShortcut,\n        )\n        self.move_value_down.triggered.connect(self.move_down)\n\n        self.add_new_item = QAction(\n            \"Add\", group,\n            iconText=\"+\",\n            objectName=\"action-add-item\",\n            toolTip=\"Append a new item.\",\n            shortcut=QKeySequence(QKeySequence.New),\n            shortcutContext=Qt.WidgetShortcut,\n        )\n        self.remove_item = QAction(\n            \"Remove item\", group,\n            iconText=\"\\N{MINUS SIGN}\",\n            objectName=\"action-remove-item\",\n            toolTip=\"Delete the selected item.\",\n            shortcut=QKeySequence(QKeySequence.Delete),\n            shortcutContext=Qt.WidgetShortcut,\n        )\n        self.rename_selected_items = QAction(\n            \"Rename selected items\", group,\n            iconText=\"=\",\n            objectName=\"action-rename-selected-items\",\n            toolTip=\"Rename selected items.\",\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_Equal),\n            shortcutContext=Qt.WidgetShortcut,\n        )\n        self.merge_items = QAction(\n            \"Merge\", group,\n            iconText=\"M\",\n            objectName=\"action-activate-merge-dialog\",\n            toolTip=\"Merge infrequent items.\",\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.MetaModifier | Qt.Key_Equal),\n            shortcutContext=Qt.WidgetShortcut\n        )\n\n        self.add_new_item.triggered.connect(self._add_category)\n        self.remove_item.triggered.connect(self._remove_category)\n        self.rename_selected_items.triggered.connect(self._rename_selected_categories)\n        self.merge_items.triggered.connect(self._merge_categories)\n\n        button1 = FixedSizeButton(\n            self, defaultAction=self.move_value_up,\n            accessibleName=\"Move up\"\n        )\n        button2 = FixedSizeButton(\n            self, defaultAction=self.move_value_down,\n            accessibleName=\"Move down\"\n        )\n        button3 = FixedSizeButton(\n            self, defaultAction=self.add_new_item,\n            accessibleName=\"Add\"\n        )\n        button4 = FixedSizeButton(\n            self, defaultAction=self.remove_item,\n            accessibleName=\"Remove\"\n        )\n        button5 = FixedSizeButton(\n            self, defaultAction=self.rename_selected_items,\n            accessibleName=\"Merge selected items\"\n        )\n        button6 = FixedSizeButton(\n            self, defaultAction=self.merge_items,\n            accessibleName=\"Merge infrequent\",\n        )\n\n        self.values_edit.addActions([\n            self.move_value_up, self.move_value_down,\n            self.add_new_item, self.remove_item, self.rename_selected_items\n        ])\n        self.values_edit.setContextMenuPolicy(Qt.CustomContextMenu)\n\n        def context_menu(pos: QPoint):\n            viewport = self.values_edit.viewport()\n            menu = QMenu(self.values_edit)\n            menu.setAttribute(Qt.WA_DeleteOnClose)\n            menu.addActions([self.rename_selected_items, self.remove_item])\n            menu.popup(viewport.mapToGlobal(pos))\n        self.values_edit.customContextMenuRequested.connect(context_menu)\n\n        hlayout.addWidget(button1)\n        hlayout.addWidget(button2)\n        hlayout.addSpacing(3)\n        hlayout.addWidget(button3)\n        hlayout.addWidget(button4)\n        hlayout.addSpacing(3)\n        hlayout.addWidget(button5)\n        hlayout.addWidget(button6)\n\n        hlayout.addStretch(10)\n        vlayout.addLayout(hlayout)\n\n        form.insertRow(2, \"Values:\", vlayout)\n\n        QWidget.setTabOrder(self.name_edit, self.values_edit)\n        QWidget.setTabOrder(self.values_edit, button1)\n        QWidget.setTabOrder(button1, button2)\n        QWidget.setTabOrder(button2, button3)\n        QWidget.setTabOrder(button3, button4)\n        QWidget.setTabOrder(button4, button5)\n        QWidget.setTabOrder(button5, button6)\n\n    def set_data(self, var, transform=()):\n        raise NotImplementedError\n\n    def set_data_categorical(self, var, values, transform=()):\n        # type: (Optional[Categorical], Optional[Sequence[float]], Sequence[Transform]) -> None\n        \"\"\"\n        Set the variable to edit.\n\n        `values` is needed for categorical features to perform grouping.\n        \"\"\"\n        # pylint: disable=too-many-branches\n        super().set_data(var, transform=transform)\n        self._values = values\n        tr = None  # type: Optional[CategoriesMapping]\n        for tr_ in transform:\n            if isinstance(tr_, CategoriesMapping):\n                tr = tr_\n\n        items = []\n        if tr is not None:\n            ci_index = {c: i for i, c in enumerate(var.categories)}\n            for ci, cj in tr.mapping:\n                if ci is None and cj is not None:\n                    # level added\n                    item = {\n                        Qt.EditRole: cj,\n                        EditStateRole: ItemEditState.Added,\n                        SourcePosRole: None\n                    }\n                elif ci is not None and cj is None:\n                    # ci level dropped\n                    item = {\n                        Qt.EditRole: ci,\n                        EditStateRole: ItemEditState.Dropped,\n                        SourcePosRole: ci_index[ci],\n                        SourceNameRole: ci\n                    }\n                elif ci is not None and cj is not None:\n                    # rename or reorder\n                    item = {\n                        Qt.EditRole: cj,\n                        EditStateRole: ItemEditState.NoState,\n                        SourcePosRole: ci_index[ci],\n                        SourceNameRole: ci\n                    }\n                else:\n                    assert False, \"invalid mapping: {!r}\".format(tr.mapping)\n                items.append(item)\n        elif var is not None:\n            items = [\n                {Qt.EditRole: c,\n                 EditStateRole: ItemEditState.NoState,\n                 SourcePosRole: i,\n                 SourceNameRole: c}\n                for i, c in enumerate(var.categories)\n            ]\n        else:\n            items = []\n\n        with disconnected(self.values_model.dataChanged,\n                          self.on_values_changed):\n            self.values_model.clear()\n            self.values_model.insertRows(0, len(items))\n            for i, item in enumerate(items):\n                self.values_model.setItemData(\n                    self.values_model.index(i, 0),\n                    item\n                )\n\n        self.add_new_item.actionGroup().setEnabled(var is not None)\n\n    def __categories_mapping(self):\n        # type: () -> CategoriesMappingType\n        \"\"\"\n        Encode and return the current state as a CategoriesMappingType\n        \"\"\"\n        model = self.values_model\n        source = self.var.categories\n\n        res = []  # type: CategoriesMappingType\n        for i in range(model.rowCount()):\n            midx = model.index(i, 0)\n            category = midx.data(Qt.EditRole)\n            source_pos = midx.data(SourcePosRole)  # type: Optional[int]\n            if source_pos is not None:\n                source_name = source[source_pos]\n            else:\n                source_name = None\n            state = midx.data(EditStateRole)\n            if state == ItemEditState.Dropped:\n                res.append((source_name, None))\n            elif state == ItemEditState.Added:\n                res.append((None, category))\n            else:\n                res.append((source_name, category))\n        return res\n\n    def get_data(self):\n        \"\"\"Retrieve the modified variable\n        \"\"\"\n        var, tr = super().get_data()\n        if var is None:\n            return var, tr\n        mapping = self.__categories_mapping()\n        assert len(mapping) >= len(var.categories), f'{mapping}, {var}'\n        if any(_1 != _2 or _2 != _3\n               for (_1, _2), _3 in zip_longest(mapping, var.categories)):\n            tr.append(CategoriesMapping(mapping))\n        return var, tr\n\n    def clear(self):\n        \"\"\"Clear the model state.\n        \"\"\"\n        super().clear()\n        self.values_model.clear()\n\n    def move_rows(self, rows, offset):\n        if not rows:\n            return\n        assert len(rows) == 1\n        i = rows[0].row()\n        if offset > 0:\n            offset += 1\n        self.values_model.moveRows(QModelIndex(), i, 1, QModelIndex(), i + offset)\n        self.variable_changed.emit()\n\n    def move_up(self):\n        rows = self.values_edit.selectionModel().selectedRows()\n        self.move_rows(rows, -1)\n\n    def move_down(self):\n        rows = self.values_edit.selectionModel().selectedRows()\n        self.move_rows(rows, 1)\n\n    @Slot()\n    def on_values_changed(self):\n        self.variable_changed.emit()\n\n    @Slot()\n    def on_value_selection_changed(self):\n        rows = self.values_edit.selectionModel().selectedRows()\n        if len(rows) == 1:\n            i = rows[0].row()\n            self.move_value_up.setEnabled(i != 0)\n            self.move_value_down.setEnabled(i != self.values_model.rowCount() - 1)\n        else:\n            self.move_value_up.setEnabled(False)\n            self.move_value_down.setEnabled(False)\n\n    def _remove_category(self):\n        \"\"\"\n        Remove the current selected category.\n\n        If the item is an existing category present in the source variable it\n        is marked as removed in the view. But if it was added in the set\n        transformation it is removed entirely from the model and view.\n        \"\"\"\n        view = self.values_edit\n        rows = view.selectionModel().selectedRows(0)\n        if not rows:\n            return  # pragma: no cover\n        for index in rows:\n            model = index.model()\n            state = index.data(EditStateRole)\n            pos = index.data(SourcePosRole)\n            if pos is not None and pos >= 0:\n                # existing level -> only mark/toggle its dropped state,\n                model.setData(\n                    index,\n                    ItemEditState.Dropped if state != ItemEditState.Dropped\n                    else ItemEditState.NoState,\n                    EditStateRole)\n            elif state == ItemEditState.Added:\n                # new level -> remove it\n                model.removeRow(index.row())\n            else:\n                assert False, \"invalid state '{}' for {}\" \\\n                    .format(state, index.row())\n\n    def _add_category(self):\n        \"\"\"\n        Add a new category\n        \"\"\"\n        view = self.values_edit\n        model = view.model()\n\n        with disconnected(model.dataChanged, self.on_values_changed,\n                          Qt.UniqueConnection):\n            row = model.rowCount()\n            if not model.insertRow(model.rowCount()):\n                return\n            index = model.index(row, 0)\n            model.setItemData(\n                index, {\n                    Qt.EditRole: \"\",\n                    SourcePosRole: None,\n                    EditStateRole: ItemEditState.Added\n                }\n            )\n            view.setCurrentIndex(index)\n            view.edit(index)\n        self.on_values_changed()\n\n    def _merge_categories(self) -> None:\n        \"\"\"\n        Merge less common categories into one with the dialog for merge\n        selection.\n        \"\"\"\n        view = self.values_edit\n        model = view.model()  # type: QAbstractItemModel\n\n        selected_attributes = [ind.data(SourceNameRole)\n                               for ind in view.selectedIndexes()]\n        dlg = GroupItemsDialog(\n            self.var, self._values, selected_attributes,\n            self.merge_dialog_settings.get(self.var, {}), self,\n            windowTitle=\"Import Options\",\n            sizeGripEnabled=True,\n        )\n        dlg.setWindowModality(Qt.WindowModal)\n        status = dlg.exec()\n        dlg.deleteLater()\n        self.merge_dialog_settings[self.var] = dlg.get_dialog_settings()\n\n        rows = (model.index(i, 0) for i in range(model.rowCount()))\n\n        def complete_merge(text, merge_attributes):\n            # write the new text for edit role in all rows\n            with disconnected(model.dataChanged, self.on_values_changed):\n                for row in rows:\n                    if row.data(SourceNameRole) in merge_attributes:\n                        model.setData(row, text, Qt.EditRole)\n            self.variable_changed.emit()\n\n        if status == QDialog.Accepted:\n            complete_merge(\n                dlg.get_merged_value_name(), dlg.get_merge_attributes()\n            )\n\n    def _rename_selected_categories(self):\n        \"\"\"\n        Rename selected categories and merging them.\n\n        Popup an editable combo box for selection/edit of a new value.\n        \"\"\"\n        view = self.values_edit\n        selmodel = view.selectionModel()\n        index = view.currentIndex()\n        if not selmodel.isSelected(index):\n            indices = selmodel.selectedRows(0)\n            if indices:\n                index = indices[0]\n        # delegate to the CategoriesEditDelegate\n        view.edit(index)\n\n\nclass ContinuousVariableEditor(VariableEditor):\n    # TODO: enable editing of display format...\n    pass\n\n\nclass TimeVariableEditor(VariableEditor):\n    def __init__(self, parent=None, **kwargs):\n        super().__init__(parent, **kwargs)\n        form = self.layout().itemAt(0)\n\n        self.format_cb = QComboBox()\n        for item, data in [(\"Detect automatically\", (None, 1, 1))] + list(\n            Orange.data.TimeVariable.ADDITIONAL_FORMATS.items()\n        ):\n            self.format_cb.addItem(item, StrpTime(item, *data))\n        self.format_cb.currentIndexChanged.connect(self.variable_changed)\n        form.insertRow(2, \"Format:\", self.format_cb)\n\n    def set_data(self, var, transform=()):\n        super().set_data(var, transform)\n        if self.parent() is not None and isinstance(self.parent().var, Time):\n            # when transforming from time to time disable format selection combo\n            self.format_cb.setEnabled(False)\n        else:\n            # select the format from StrpTime transform\n            for tr in transform:\n                if isinstance(tr, StrpTime):\n                    index = self.format_cb.findText(tr.label)\n                    self.format_cb.setCurrentIndex(index)\n            self.format_cb.setEnabled(True)\n\n    def get_data(self):\n        var, tr = super().get_data()\n        if var is not None and (self.parent() is None or not isinstance(self.parent().var, Time)):\n            # do not add StrpTime when transforming from time to time\n            tr.insert(0, self.format_cb.currentData())\n        return var, tr\n\n\ndef variable_icon(var):\n    # type: (Union[Variable, Type[Variable], ReinterpretTransform]) -> QIcon\n    if not isinstance(var, type):\n        var = type(var)\n\n    if issubclass(var, (Categorical, AsCategorical)):\n        return gui.attributeIconDict[1]\n    elif issubclass(var, (Real, AsContinuous)):\n        return gui.attributeIconDict[2]\n    elif issubclass(var, (String, AsString)):\n        return gui.attributeIconDict[3]\n    elif issubclass(var, (Time, AsTime)):\n        return gui.attributeIconDict[4]\n    else:\n        return gui.attributeIconDict[-1]\n\n\n#: ItemDataRole storing the data vector transform\n#: (`List[Union[ReinterpretTransform, Transform]]`)\nTransformRole = Qt.UserRole + 42\n\n\nclass VariableEditDelegate(QStyledItemDelegate):\n    ReinterpretNames = {\n        AsCategorical: \"categorical\", AsContinuous: \"numeric\",\n        AsString: \"string\", AsTime: \"time\"\n    }\n\n    def initStyleOption(self, option, index):\n        # type: (QStyleOptionViewItem, QModelIndex) -> None\n        super().initStyleOption(option, index)\n        item = index.data(Qt.EditRole)\n        var = tr = None\n        if isinstance(item, DataVectorTypes):\n            var = item.vtype\n            option.icon = variable_icon(var)\n        if isinstance(item, VariableTypes):\n            var = item\n            option.icon = variable_icon(item)\n        elif isinstance(item, Orange.data.Variable):\n            var = item\n            option.icon = gui.attributeIconDict[var]\n\n        transform = index.data(TransformRole)\n        if not isinstance(transform, list):\n            transform = []\n\n        if transform and isinstance(transform[0], ReinterpretTransformTypes):\n            option.icon = variable_icon(transform[0])\n\n        if not option.icon.isNull():\n            option.features |= QStyleOptionViewItem.HasDecoration\n\n        if var is not None:\n            text = var.name\n            for tr in transform:\n                if isinstance(tr, Rename):\n                    text = (\"{} \\N{RIGHTWARDS ARROW} {}\"\n                            .format(var.name, tr.name))\n            for tr in transform:\n                if isinstance(tr, ReinterpretTransformTypes):\n                    text += f\" (reinterpreted as \" \\\n                            f\"{self.ReinterpretNames[type(tr)]})\"\n            option.text = text\n        if transform:\n            # mark as changed (maybe also change color, add text, ...)\n            option.font.setItalic(True)\n\n        multiplicity = index.data(MultiplicityRole)\n        if isinstance(multiplicity, int) and multiplicity > 1:\n            option.palette.setBrush(QPalette.Text, QBrush(Qt.red))\n            option.palette.setBrush(QPalette.HighlightedText, QBrush(Qt.red))\n\n    def helpEvent(self, event: QHelpEvent, view: QAbstractItemView,\n                  option: QStyleOptionViewItem, index: QModelIndex) -> bool:\n        multiplicity = index.data(MultiplicityRole)\n        name = VariableListModel.effective_name(index)\n        if isinstance(multiplicity, int) and multiplicity > 1 \\\n                and name is not None:\n            QToolTip.showText(\n                event.globalPos(), f\"Name `{name}` is duplicated\",\n                view.viewport()\n            )\n            return True\n        else:  # pragma: no cover\n            return super().helpEvent(event, view, option, index)\n\n\n# Item model for edited variables (Variable). Define a display role to be the\n# source variable name. This is used only in keyboard search. The display is\n# otherwise completely handled by a delegate.\nclass VariableListModel(CountedListModel):\n    def data(self, index, role=Qt.DisplayRole):\n        # type: (QModelIndex, Qt.ItemDataRole) -> Any\n        row = index.row()\n        if not index.isValid() or not 0 <= row < self.rowCount():\n            return None\n        if role == Qt.DisplayRole:\n            item = self[row]\n            if isinstance(item, VariableTypes):\n                return item.name\n            if isinstance(item, DataVectorTypes):\n                return item.vtype.name\n        return super().data(index, role)\n\n    def key(self, index):\n        return VariableListModel.effective_name(index)\n\n    def keyRoles(self):  # type: () -> FrozenSet[int]\n        return frozenset((Qt.DisplayRole, Qt.EditRole, TransformRole))\n\n    @staticmethod\n    def effective_name(index) -> Optional[str]:\n        item = index.data(Qt.EditRole)\n        if isinstance(item, DataVectorTypes):\n            var = item.vtype\n        elif isinstance(item, VariableTypes):\n            var = item\n        else:\n            return None\n        tr = index.data(TransformRole)\n        return effective_name(var, tr or [])\n\n\ndef effective_name(var: Variable, tr: Sequence[Transform]) -> str:\n    name = var.name\n    for t in tr:\n        if isinstance(t, Rename):\n            name = t.name\n    return name\n\n\nclass ReinterpretVariableEditor(VariableEditor):\n    \"\"\"\n    A 'compound' variable editor capable of variable type reinterpretations.\n    \"\"\"\n    _editors = {\n        Categorical: 0,\n        Real: 1,\n        String: 2,\n        Time: 3,\n        type(None): -1,\n    }\n\n    def __init__(self, parent=None, **kwargs):\n        # Explicitly skip VariableEditor's __init__, this is ugly but we have\n        # a completely different layout/logic as a compound editor (should\n        # really not subclass VariableEditor).\n        super(VariableEditor, self).__init__(parent, **kwargs)  # pylint: disable=bad-super-call\n        self.var = None  # type: Optional[Variable]\n        self.__transform = None  # type: Optional[ReinterpretTransform]\n        self.__data = None  # type: Optional[DataVector]\n        #: Stored transform state indexed by variable. Used to preserve state\n        #: between type switches.\n        self.__history = {}  # type: Dict[Variable, List[Transform]]\n\n        self.setLayout(QStackedLayout())\n\n        def decorate(editor: VariableEditor) -> VariableEditor:\n            \"\"\"insert an type combo box into a `editor`'s layout.\"\"\"\n            form = editor.layout().itemAt(0)\n            assert isinstance(form, QFormLayout)\n            typecb = QComboBox(objectName=\"type-combo\")\n            typecb.addItem(variable_icon(Categorical), \"Categorical\", Categorical)\n            typecb.addItem(variable_icon(Real), \"Numeric\", Real)\n            typecb.addItem(variable_icon(String), \"Text\", String)\n            typecb.addItem(variable_icon(Time), \"Time\", Time)\n            typecb.activated[int].connect(self.__reinterpret_activated)\n            form.insertRow(1, \"Type:\", typecb)\n            # Insert the typecb after name edit in the focus chain\n            name_edit = editor.findChild(QLineEdit, )\n            if name_edit is not None:\n                QWidget.setTabOrder(name_edit, typecb)\n            return editor\n        # This is ugly. Create an editor for each type and insert a type\n        # selection combo box into its layout. Switch between widgets\n        # on type change.\n        self.disc_edit = dedit = decorate(DiscreteVariableEditor())\n        cedit = decorate(ContinuousVariableEditor())\n        tedit = decorate(TimeVariableEditor())\n        sedit = decorate(VariableEditor())\n\n        for ed in [dedit, cedit, tedit, sedit]:\n            ed.variable_changed.connect(self.variable_changed)\n\n        self.layout().addWidget(dedit)\n        self.layout().addWidget(cedit)\n        self.layout().addWidget(sedit)\n        self.layout().addWidget(tedit)\n\n    def set_data(self, data, transform=()):  # pylint: disable=arguments-differ\n        # type: (Optional[DataVector], Sequence[Transform]) -> None\n        \"\"\"\n        Set the editor data.\n\n        Note\n        ----\n        This must be a `DataVector` as the vector's values are needed for type\n        reinterpretation/casts.\n\n        If the `transform` sequence contains ReinterpretTransform then it\n        must be in the first position.\n        \"\"\"\n        type_transform = None  # type: Optional[ReinterpretTransform]\n        if transform:\n            _tr = transform[0]\n            if isinstance(_tr, ReinterpretTransformTypes):\n                type_transform = _tr\n                transform = transform[1:]\n            assert not any(isinstance(t, ReinterpretTransformTypes)\n                           for t in transform)\n        self.__transform = type_transform\n        self.__data = data\n        self.var = data.vtype if data is not None else None\n\n        if type_transform is not None and data is not None:\n            data = type_transform(data)\n        if data is not None:\n            var = data.vtype\n        else:\n            var = None\n        index = self._editors.get(type(var), -1)\n        self.layout().setCurrentIndex(index)\n        if index != -1:\n            w = self.layout().currentWidget()\n            assert isinstance(w, VariableEditor)\n            if isinstance(var, Categorical):\n                w.set_data_categorical(var, data.data(), transform=transform)\n            else:\n                w.set_data(var, transform=transform)\n            self.__history[var] = tuple(transform)\n            cb = w.findChild(QComboBox, \"type-combo\")\n            cb.setCurrentIndex(index)\n\n    def get_data(self):\n        # type: () -> Tuple[Variable, Sequence[Transform]]\n        editor = self.layout().currentWidget()  # type: VariableEditor\n        var, tr = editor.get_data()\n        if type(var) != type(self.var):  # pylint: disable=unidiomatic-typecheck\n            assert self.__transform is not None\n            var = self.var\n            tr = [self.__transform, *tr]\n        return var, tr\n\n    def __reinterpret_activated(self, index):\n        layout = self.layout()\n        assert isinstance(layout, QStackedLayout)\n        if index == layout.currentIndex():\n            return\n        current = layout.currentWidget()\n        assert isinstance(current, VariableEditor)\n        Specific = {\n            Categorical: CategoricalTransformTypes\n        }\n        _var, _tr = current.get_data()\n        if _var is not None:\n            self.__history[_var] = _tr\n\n        var = self.var\n        transform = self.__transform\n        # take/preserve the general transforms that apply to all types\n        specific = Specific.get(type(var), ())\n        _tr = [t for t in _tr if not isinstance(t, specific)]\n\n        layout.setCurrentIndex(index)\n        w = layout.currentWidget()\n        cb = w.findChild(QComboBox, \"type-combo\")\n        cb.setCurrentIndex(index)\n        cb.setFocus()\n        target = cb.itemData(index, Qt.UserRole)\n        assert issubclass(target, VariableTypes)\n        if not isinstance(var, target):\n            if target == Real:\n                transform = AsContinuous()\n            elif target == Categorical:\n                transform = AsCategorical()\n            elif target == Time:\n                transform = AsTime()\n            elif target == String:\n                transform = AsString()\n        else:\n            transform = None\n            var = self.var\n\n        self.__transform = transform\n        data = None\n        if transform is not None and self.__data is not None:\n            data = transform(self.__data)\n            var = data.vtype\n\n        if var in self.__history:\n            tr = self.__history[var]\n        else:\n            tr = []\n        # type specific transform\n        specific = Specific.get(type(var), ())\n        # merge tr and _tr\n        tr = _tr + [t for t in tr if isinstance(t, specific)]\n        with disconnected(\n                w.variable_changed, self.variable_changed,\n                Qt.UniqueConnection\n        ):\n            if isinstance(w, DiscreteVariableEditor):\n                data = data or self.__data\n                w.set_data_categorical(var, data.data(), transform=tr)\n            else:\n                w.set_data(var, transform=tr)\n        self.variable_changed.emit()\n\n    def set_merge_context(self, merge_context):\n        self.disc_edit.merge_dialog_settings = merge_context\n\n    def get_merge_context(self):\n        return self.disc_edit.merge_dialog_settings\n\n\nclass OWEditDomain(widget.OWWidget):\n    name = \"Edit Domain\"\n    description = \"Rename variables, edit categories and variable annotations.\"\n    icon = \"icons/EditDomain.svg\"\n    priority = 3125\n    keywords = [\"rename\", \"drop\", \"reorder\", \"order\"]\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        data = Output(\"Data\", Orange.data.Table)\n\n    class Error(widget.OWWidget.Error):\n        duplicate_var_name = widget.Msg(\"A variable name is duplicated.\")\n\n    settingsHandler = settings.DomainContextHandler()\n    settings_version = 2\n\n    _domain_change_store = settings.ContextSetting({})\n    _selected_item = settings.ContextSetting(None)  # type: Optional[Tuple[str, int]]\n    _merge_dialog_settings = settings.ContextSetting({})\n    output_table_name = settings.ContextSetting(\"\")\n\n    want_main_area = False\n\n    def __init__(self):\n        super().__init__()\n        self.data = None  # type: Optional[Orange.data.Table]\n        #: The current selected variable index\n        self.selected_index = -1\n        self._invalidated = False\n        self.typeindex = 0\n\n        main = gui.hBox(self.controlArea, spacing=6)\n        box = gui.vBox(main, \"Variables\")\n\n        self.variables_model = VariableListModel(parent=self)\n        self.variables_view = self.domain_view = QListView(\n            selectionMode=QListView.SingleSelection,\n            uniformItemSizes=True,\n        )\n        self.variables_view.setItemDelegate(VariableEditDelegate(self))\n        self.variables_view.setModel(self.variables_model)\n        self.variables_view.selectionModel().selectionChanged.connect(\n            self._on_selection_changed\n        )\n        box.layout().addWidget(self.variables_view)\n\n        box = gui.vBox(main, \"Edit\")\n        self._editor = ReinterpretVariableEditor()\n        box.layout().addWidget(self._editor)\n\n        self.le_output_name = gui.lineEdit(\n            self.buttonsArea, self, \"output_table_name\", \"Output table name: \",\n            orientation=Qt.Horizontal)\n\n        gui.rubber(self.buttonsArea)\n\n        bbox = gui.hBox(self.buttonsArea)\n        breset_all = gui.button(\n            bbox, self, \"Reset All\",\n            objectName=\"button-reset-all\",\n            toolTip=\"Reset all variables to their input state.\",\n            autoDefault=False,\n            callback=self.reset_all\n        )\n        breset = gui.button(\n            bbox, self, \"Reset Selected\",\n            objectName=\"button-reset\",\n            toolTip=\"Rest selected variable to its input state.\",\n            autoDefault=False,\n            callback=self.reset_selected\n        )\n        bapply = gui.button(\n            bbox, self, \"Apply\",\n            objectName=\"button-apply\",\n            toolTip=\"Apply changes and commit data on output.\",\n            default=True,\n            autoDefault=False,\n            callback=self.commit\n        )\n\n        self.variables_view.setFocus(Qt.NoFocusReason)  # initial focus\n\n    @Inputs.data\n    def set_data(self, data):\n        \"\"\"Set input dataset.\"\"\"\n        self.closeContext()\n        self.clear()\n        self.data = data\n\n        if self.data is not None:\n            self.setup_model(data)\n            self.le_output_name.setPlaceholderText(data.name)\n            self.openContext(self.data)\n            self._editor.set_merge_context(self._merge_dialog_settings)\n            self._restore()\n        else:\n            self.le_output_name.setPlaceholderText(\"\")\n\n        self.commit()\n\n    def clear(self):\n        \"\"\"Clear the widget state.\"\"\"\n        self.data = None\n        self.variables_model.clear()\n        self.clear_editor()\n        assert self.selected_index == -1\n        self.selected_index = -1\n\n        self._selected_item = None\n        self._domain_change_store = {}\n        self._merge_dialog_settings = {}\n\n    def reset_selected(self):\n        \"\"\"Reset the currently selected variable to its original state.\"\"\"\n        ind = self.selected_var_index()\n        if ind >= 0:\n            model = self.variables_model\n            midx = model.index(ind)\n            var = midx.data(Qt.EditRole)\n            tr = midx.data(TransformRole)\n            if not tr:\n                return  # nothing to reset\n            editor = self._editor\n            with disconnected(editor.variable_changed,\n                              self._on_variable_changed):\n                model.setData(midx, [], TransformRole)\n                editor.set_data(var, transform=[])\n            self._invalidate()\n\n    def reset_all(self):\n        \"\"\"Reset all variables to their original state.\"\"\"\n        self._domain_change_store = {}\n        if self.data is not None:\n            model = self.variables_model\n            for i in range(model.rowCount()):\n                midx = model.index(i)\n                model.setData(midx, [], TransformRole)\n            index = self.selected_var_index()\n            if index >= 0:\n                self.open_editor(index)\n            self._invalidate()\n\n    def selected_var_index(self):\n        \"\"\"Return the current selected variable index.\"\"\"\n        rows = self.variables_view.selectedIndexes()\n        assert len(rows) <= 1\n        return rows[0].row() if rows else -1\n\n    def setup_model(self, data: Orange.data.Table):\n        model = self.variables_model\n        vars_ = []\n        columns = []\n        for i, _, var, coldata in enumerate_columns(data):\n            var = abstract(var)\n            vars_.append(var)\n            if isinstance(var, Categorical):\n                data = CategoricalVector(var, coldata)\n            elif isinstance(var, Real):\n                data = RealVector(var, coldata)\n            elif isinstance(var, Time):\n                data = TimeVector(var, coldata)\n            elif isinstance(var, String):\n                data = StringVector(var, coldata)\n            columns.append(data)\n\n        model[:] = vars_\n        for i, d in enumerate(columns):\n            model.setData(model.index(i), d, Qt.EditRole)\n\n    def _restore(self, ):\n        \"\"\"\n        Restore the edit transform from saved state.\n        \"\"\"\n        model = self.variables_model\n        for i in range(model.rowCount()):\n            midx = model.index(i, 0)\n            coldesc = model.data(midx, Qt.EditRole)  # type: DataVector\n            tr = self._restore_transform(coldesc.vtype)\n            if tr:\n                model.setData(midx, tr, TransformRole)\n\n        # Restore the current variable selection\n        i = -1\n        if self._selected_item is not None:\n            for i, vec in enumerate(model):\n                if vec.vtype.name_type() == self._selected_item:\n                    break\n        if i == -1 and model.rowCount():\n            i = 0\n\n        if i != -1:\n            itemmodels.select_row(self.variables_view, i)\n\n    def _on_selection_changed(self):\n        self.selected_index = self.selected_var_index()\n        if self.selected_index != -1:\n            self._selected_item = self.variables_model[self.selected_index].vtype.name_type()\n        else:\n            self._selected_item = None\n        self.open_editor(self.selected_index)\n\n    def open_editor(self, index):\n        # type: (int) -> None\n        self.clear_editor()\n        model = self.variables_model\n        if not 0 <= index < model.rowCount():\n            return\n        idx = model.index(index, 0)\n        vector = model.data(idx, Qt.EditRole)\n        tr = model.data(idx, TransformRole)\n        if tr is None:\n            tr = []\n        editor = self._editor\n        editor.set_data(vector, transform=tr)\n        editor.variable_changed.connect(\n            self._on_variable_changed, Qt.UniqueConnection\n        )\n\n    def clear_editor(self):\n        current = self._editor\n        try:\n            current.variable_changed.disconnect(self._on_variable_changed)\n        except TypeError:\n            pass\n        current.set_data(None)\n        current.layout().currentWidget().clear()\n\n    @Slot()\n    def _on_variable_changed(self):\n        \"\"\"User edited the current variable in editor.\"\"\"\n        assert 0 <= self.selected_index <= len(self.variables_model)\n        editor = self._editor\n        var, transform = editor.get_data()\n        model = self.variables_model\n        midx = model.index(self.selected_index, 0)\n        model.setData(midx, transform, TransformRole)\n        self._store_transform(var, transform)\n        self._invalidate()\n\n    def _store_transform(self, var, transform):\n        # type: (Variable, List[Transform]) -> None\n        self._domain_change_store[deconstruct(var)] = [deconstruct(t) for t in transform]\n\n    def _restore_transform(self, var):\n        # type: (Variable) -> List[Transform]\n        tr_ = self._domain_change_store.get(deconstruct(var), [])\n        tr = []\n\n        for t in tr_:\n            try:\n                tr.append(reconstruct(*t))\n            except (NameError, TypeError) as err:\n                warnings.warn(\n                    \"Failed to restore transform: {}, {!r}\".format(t, err),\n                    UserWarning, stacklevel=2\n                )\n        return tr\n\n    def _invalidate(self):\n        self._set_modified(True)\n\n    def _set_modified(self, state):\n        self._invalidated = state\n        b = self.findChild(QPushButton, \"button-apply\")\n        if isinstance(b, QPushButton):\n            f = b.font()\n            f.setItalic(state)\n            b.setFont(f)\n\n    def commit(self):\n        \"\"\"\n        Apply the changes to the input data and send the changed data to output.\n        \"\"\"\n        self._set_modified(False)\n        self.Error.duplicate_var_name.clear()\n\n        data = self.data\n        if data is None:\n            self.Outputs.data.send(None)\n            return\n        model = self.variables_model\n\n        def state(i):\n            # type: (int) -> Tuple[DataVector, List[Transform]]\n            midx = self.variables_model.index(i, 0)\n            return (model.data(midx, Qt.EditRole),\n                    model.data(midx, TransformRole))\n\n        state = [state(i) for i in range(model.rowCount())]\n        input_vars = data.domain.variables + data.domain.metas\n        if self.output_table_name in (\"\", data.name) \\\n                and not any(requires_transform(var, trs)\n                            for var, (_, trs) in zip(input_vars, state)):\n            self.Outputs.data.send(data)\n            return\n\n        assert all(v_.vtype.name == v.name\n                   for v, (v_, _) in zip(input_vars, state))\n        output_vars = []\n        unlinked_vars = []\n        unlink_domain = False\n        for (_, tr), v in zip(state, input_vars):\n            if tr:\n                var = apply_transform(v, data, tr)\n                if requires_unlink(v, tr):\n                    unlinked_var = var.copy(compute_value=None)\n                    unlink_domain = True\n                else:\n                    unlinked_var = var\n            else:\n                unlinked_var = var = v\n            output_vars.append(var)\n            unlinked_vars.append(unlinked_var)\n\n        if len(output_vars) != len({v.name for v in output_vars}):\n            self.Error.duplicate_var_name()\n            self.Outputs.data.send(None)\n            return\n\n        domain = data.domain\n        nx = len(domain.attributes)\n        ny = len(domain.class_vars)\n\n        def construct_domain(vars_list):\n            # Move non primitive Xs, Ys to metas (if they were changed)\n            Xs = [v for v in vars_list[:nx] if v.is_primitive()]\n            Ys = [v for v in vars_list[nx: nx + ny] if v.is_primitive()]\n            Ms = vars_list[nx + ny:] + \\\n                 [v for v in vars_list[:nx + ny] if not  v.is_primitive()]\n            return Orange.data.Domain(Xs, Ys, Ms)\n\n        domain = construct_domain(output_vars)\n        new_data = data.transform(domain)\n        if unlink_domain:\n            unlinked_domain = construct_domain(unlinked_vars)\n            new_data = new_data.from_numpy(\n                unlinked_domain,\n                new_data.X, new_data.Y, new_data.metas, new_data.W,\n                new_data.attributes, new_data.ids\n            )\n        if self.output_table_name:\n            new_data.name = self.output_table_name\n        self.Outputs.data.send(new_data)\n\n    def sizeHint(self):\n        sh = super().sizeHint()\n        return sh.expandedTo(QSize(660, 550))\n\n    def storeSpecificSettings(self):\n        \"\"\"\n        Update setting before context closes - also when widget closes.\n        \"\"\"\n        self._merge_dialog_settings = self._editor.get_merge_context()\n\n    def send_report(self):\n\n        if self.data is not None:\n            model = self.variables_model\n            state = ((model.data(midx, Qt.EditRole),\n                      model.data(midx, TransformRole))\n                     for i in range(model.rowCount())\n                     for midx in [model.index(i)])\n            parts = []\n            for vector, trs in state:\n                if trs:\n                    parts.append(report_transform(vector.vtype, trs))\n            if parts:\n                html = (\"<ul>\" +\n                        \"\".join(map(\"<li>{}</li>\".format, parts)) +\n                        \"</ul>\")\n            else:\n                html = \"No changes\"\n            self.report_raw(\"\", html)\n        else:\n            self.report_data(None)\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        # pylint: disable=bad-continuation\n        if version is None or version <= 1:\n            hints_ = context.values.get(\"domain_change_hints\", ({}, -2))[0]\n            store = []\n            ns = \"Orange.data.variable\"\n            mapping = {\n                \"DiscreteVariable\":\n                    lambda name, args, attrs:\n                        (\"Categorical\", (name, tuple(args[0][1]), ())),\n                \"TimeVariable\":\n                    lambda name, _, attrs:\n                        (\"Time\", (name, ())),\n                \"ContinuousVariable\":\n                    lambda name, _, attrs:\n                        (\"Real\", (name, (3, \"f\"), ())),\n                \"StringVariable\":\n                    lambda name, _, attrs:\n                        (\"String\", (name, ())),\n            }\n            for (module, class_name, *rest), target in hints_.items():\n                if module != ns:\n                    continue\n                f = mapping.get(class_name)\n                if f is None:\n                    continue\n                trs = []\n                key_mapped = f(*rest)\n                item_mapped = f(*target[2:])\n                src = reconstruct(*key_mapped)   # type: Variable\n                dst = reconstruct(*item_mapped)  # type: Variable\n                if src.name != dst.name:\n                    trs.append(Rename(dst.name))\n                if src.annotations != dst.annotations:\n                    trs.append(Annotate(dst.annotations))\n                if isinstance(src, Categorical):\n                    if src.categories != dst.categories:\n                        assert len(src.categories) == len(dst.categories)\n                        trs.append(CategoriesMapping(\n                            list(zip(src.categories, dst.categories))))\n                store.append((deconstruct(src), [deconstruct(tr) for tr in trs]))\n            context.values[\"_domain_change_store\"] = (dict(store), -2)\n\n\ndef enumerate_columns(\n        table: Orange.data.Table\n) -> Iterable[Tuple[int, str, Orange.data.Variable, Callable[[], ndarray]]]:\n    domain = table.domain\n    for i, (var, role) in enumerate(\n            chain(zip(domain.attributes, repeat(\"x\")),\n                  zip(domain.class_vars, repeat(\"y\")),\n                  zip(domain.metas, repeat(\"m\"))),\n    ):\n        if i >= len(domain.variables):\n            i = len(domain.variables) - i - 1\n        data = partial(table_column_data, table, i)\n        yield i, role, var, data\n\n\ndef table_column_data(\n        table: Orange.data.Table,\n        var: Union[Orange.data.Variable, int],\n        dtype=None\n) -> MArray:\n    col, copy = table.get_column_view(var)\n    var = table.domain[var]  # type: Orange.data.Variable\n    if var.is_primitive() and not np.issubdtype(col.dtype, np.inexact):\n        col = col.astype(float)\n        copy = True\n\n    if dtype is None:\n        if isinstance(var, Orange.data.TimeVariable):\n            dtype = np.dtype(\"M8[us]\")\n            col = col * 1e6\n        elif isinstance(var, Orange.data.ContinuousVariable):\n            dtype = np.dtype(float)\n        elif isinstance(var, Orange.data.DiscreteVariable):\n            _values = tuple(var.values)\n            _n_values = len(_values)\n            dtype = np.dtype(int, metadata={\n                \"__formatter\": lambda i: _values[i] if 0 <= i < _n_values else \"?\"\n            })\n        elif isinstance(var, Orange.data.StringVariable):\n            dtype = np.dtype(object)\n        else:\n            assert False\n    mask = orange_isna(var, col)\n\n    if dtype != col.dtype:\n        col = col.astype(dtype)\n        copy = True\n\n    if not copy:\n        col = col.copy()\n    return MArray(col, mask=mask)\n\n\ndef report_transform(var, trs):\n    # type: (Variable, List[Transform]) -> str\n    \"\"\"\n    Return a html fragment summarizing the changes applied by `trs` list.\n\n    Parameters\n    ----------\n    var : Variable\n        A variable descriptor no which trs operates\n    trs : List[Transform]\n        A non empty list of `Transform` instances.\n\n    Returns\n    -------\n    report : str\n    \"\"\"\n    # pylint: disable=too-many-branches\n    ReinterpretTypeCode = {\n        AsCategorical:  \"C\", AsContinuous: \"N\", AsString: \"S\", AsTime: \"T\",\n    }\n\n    def type_char(value: ReinterpretTransform) -> str:\n        return ReinterpretTypeCode.get(type(value), \"?\")\n\n    def strike(text):\n        return \"<s>{}</s>\".format(escape(text))\n\n    def i(text):\n        return \"<i>{}</i>\".format(escape(text))\n\n    def text(text):\n        return \"<span>{}</span>\".format(escape(text))\n    assert trs\n    rename = annotate = catmap = unlink = None\n    reinterpret = None\n\n    for tr in trs:\n        if isinstance(tr, Rename):\n            rename = tr\n        elif isinstance(tr, Annotate):\n            annotate = tr\n        elif isinstance(tr, CategoriesMapping):\n            catmap = tr\n        elif isinstance(tr, Unlink):\n            unlink = tr\n        elif isinstance(tr, ReinterpretTransformTypes):\n            reinterpret = tr\n\n    if reinterpret is not None:\n        header = \"{} → ({}) {}\".format(\n            var.name, type_char(reinterpret),\n            rename.name if rename is not None else var.name\n        )\n    elif rename is not None:\n        header = \"{} → {}\".format(var.name, rename.name)\n    else:\n        header = var.name\n    if unlink is not None:\n        header += \"(unlinked from source)\"\n\n    values_section = None\n    if catmap is not None:\n        values_section = (\"Values\", [])\n        lines = values_section[1]\n        for ci, cj in catmap.mapping:\n            if ci is None:\n                item = cj + (\"&nbsp;\" * 3) + \"(added)\"\n            elif cj is None:\n                item = strike(ci)\n            else:\n                item = ci + \" → \" + cj\n            lines.append(item)\n\n    annotate_section = None\n    if annotate is not None:\n        annotate_section = (\"Labels\", [])\n        lines = annotate_section[1]\n        old = dict(var.annotations)\n        new = dict(annotate.annotations)\n        for name in sorted(set(old) - set(new)):\n            lines.append(\n                \"<s>\" + i(name) + \" : \" + text(old[name]) + \"</s>\"\n            )\n        for name in sorted(set(new) - set(old)):\n            lines.append(\n                i(name) + \" : \" + text(new[name]) + \"&nbsp;\" * 3 + i(\"(new)\")\n            )\n\n        for name in sorted(set(new) & set(old)):\n            if new[name] != old[name]:\n                lines.append(\n                    i(name) + \" : \" + text(old[name]) + \" → \" + text(new[name])\n                )\n\n    html = [\"<div style='font-weight: bold;'>{}</div>\".format(header)]\n    for title, contents in filter(None, [values_section, annotate_section]):\n        section_header = \"<div>{}:</div>\".format(title)\n        section_contents = \"<br/>\\n\".join(contents)\n        html.append(section_header)\n        html.append(\n            \"<div style='padding-left: 1em;'>\" +\n            section_contents +\n            \"</div>\"\n        )\n    return \"\\n\".join(html)\n\n\ndef abstract(var):\n    # type: (Orange.data.Variable) -> Variable\n    \"\"\"\n    Return `Varaible` descriptor for an `Orange.data.Variable` instance.\n\n    Parameters\n    ----------\n    var : Orange.data.Variable\n\n    Returns\n    -------\n    var : Variable\n    \"\"\"\n    annotations = tuple(sorted(\n        (key, str(value))\n        for key, value in var.attributes.items()\n    ))\n    linked = var.compute_value is not None\n    if isinstance(var, Orange.data.DiscreteVariable):\n        return Categorical(var.name, tuple(var.values), annotations, linked)\n    elif isinstance(var, Orange.data.TimeVariable):\n        return Time(var.name, annotations, linked)\n    elif isinstance(var, Orange.data.ContinuousVariable):\n        return Real(var.name, (var.number_of_decimals, 'f'), annotations, linked)\n    elif isinstance(var, Orange.data.StringVariable):\n        return String(var.name, annotations, linked)\n    else:\n        raise TypeError\n\n\ndef _parse_attributes(mapping):\n    # type: (Iterable[Tuple[str, str]]) -> Dict[str, Any]\n    # Use the same functionality that parses attributes\n    # when reading text files\n    return Orange.data.Flags([\n        \"{}={}\".format(*item) for item in mapping\n    ]).attributes\n\n\ndef apply_transform(var, table, trs):\n    # type: (Orange.data.Variable, Orange.data.Table, List[Transform]) -> Orange.data.Variable\n    \"\"\"\n    Apply a list of `Transform` instances on an `Orange.data.Variable`.\n    \"\"\"\n    if trs and isinstance(trs[0], ReinterpretTransformTypes):\n        reinterpret, trs = trs[0], trs[1:]\n        coldata = table_column_data(table, var)\n        var = apply_reinterpret(var, reinterpret, coldata)\n    if trs:\n        return apply_transform_var(var, trs)\n    else:\n        return var\n\n\ndef requires_unlink(var: Orange.data.Variable, trs: List[Transform]) -> bool:\n    # Variable is only unlinked if it has compute_value  or if it has other\n    # transformations (that might had added compute_value)\n    return trs is not None \\\n           and any(isinstance(tr, Unlink) for tr in trs) \\\n           and (var.compute_value is not None or len(trs) > 1)\n\n\ndef requires_transform(var: Orange.data.Variable, trs: List[Transform]) -> bool:\n    # Unlink is treated separately: Unlink is required only if the variable\n    # has compute_value. Hence tranform is required if it has any\n    # transformations other than Unlink, or if unlink is indeed required.\n    return trs is not None and (\n        not all(isinstance(tr, Unlink) for tr in trs)\n        or requires_unlink(var, trs)\n    )\n\n\n@singledispatch\ndef apply_transform_var(var, trs):\n    # type: (Orange.data.Variable, List[Transform]) -> Orange.data.Variable\n    raise NotImplementedError\n\n\n@apply_transform_var.register(Orange.data.DiscreteVariable)\ndef apply_transform_discete(var, trs):\n    # type: (Orange.data.DiscreteVariable, List[Transform]) -> Orange.data.Variable\n    # pylint: disable=too-many-branches\n    name, annotations = var.name, var.attributes\n    mapping = None\n    for tr in trs:\n        if isinstance(tr, Rename):\n            name = tr.name\n        elif isinstance(tr, CategoriesMapping):\n            mapping = tr.mapping\n        elif isinstance(tr, Annotate):\n            annotations = _parse_attributes(tr.annotations)\n\n    source_values = var.values\n    if mapping is not None:\n        dest_values = list(unique(cj for ci, cj in mapping if cj is not None))\n    else:\n        dest_values = var.values\n\n    def positions(values):\n        rval = {c: i for i, c in enumerate(values)}\n        assert len(rval) == len(values)\n        return rval\n    source_codes = positions(source_values)\n    dest_codes = positions(dest_values)\n    if mapping is not None:\n        # construct a lookup table\n        lookup = np.full(len(source_values), np.nan, dtype=float)\n        for ci, cj in mapping:\n            if ci is not None and cj is not None:\n                i, j = source_codes[ci], dest_codes[cj]\n                lookup[i] = j\n        lookup = Lookup(var, lookup)\n    else:\n        lookup = Identity(var)\n    variable = Orange.data.DiscreteVariable(\n        name, values=dest_values, compute_value=lookup\n    )\n    variable.attributes.update(annotations)\n    return variable\n\n\n@apply_transform_var.register(Orange.data.ContinuousVariable)\ndef apply_transform_continuous(var, trs):\n    # type: (Orange.data.ContinuousVariable, List[Transform]) -> Orange.data.Variable\n    name, annotations = var.name, var.attributes\n    for tr in trs:\n        if isinstance(tr, Rename):\n            name = tr.name\n        elif isinstance(tr, Annotate):\n            annotations = _parse_attributes(tr.annotations)\n    variable = Orange.data.ContinuousVariable(\n        name=name, compute_value=Identity(var)\n    )\n    variable.attributes.update(annotations)\n    return variable\n\n\n@apply_transform_var.register(Orange.data.TimeVariable)\ndef apply_transform_time(var, trs):\n    # type: (Orange.data.TimeVariable, List[Transform]) -> Orange.data.Variable\n    name, annotations = var.name, var.attributes\n    for tr in trs:\n        if isinstance(tr, Rename):\n            name = tr.name\n        elif isinstance(tr, Annotate):\n            annotations = _parse_attributes(tr.annotations)\n    variable = Orange.data.TimeVariable(\n        name=name, have_date=var.have_date, have_time=var.have_time,\n        compute_value=Identity(var)\n    )\n    variable.attributes.update(annotations)\n    return variable\n\n\n@apply_transform_var.register(Orange.data.StringVariable)\ndef apply_transform_string(var, trs):\n    # type: (Orange.data.StringVariable, List[Transform]) -> Orange.data.Variable\n    name, annotations = var.name, var.attributes\n    out_type = Orange.data.StringVariable\n    compute_value = Identity\n    for tr in trs:\n        if isinstance(tr, Rename):\n            name = tr.name\n        elif isinstance(tr, Annotate):\n            annotations = _parse_attributes(tr.annotations)\n        elif isinstance(tr, StrpTime):\n            out_type = partial(\n                Orange.data.TimeVariable, have_date=tr.have_date, have_time=tr.have_time\n            )\n            compute_value = partial(ReparseTimeTransform, tr=tr)\n    variable = out_type(name=name, compute_value=compute_value(var))\n    variable.attributes.update(annotations)\n    return variable\n\n\ndef make_dict_mapper(\n        mapping: Mapping, dtype: Optional[DType] = None\n) -> Callable:\n    \"\"\"\n    Wrap a `mapping` into a callable ufunc-like function with\n    `out`, `dtype`, `where`, ... parameters. If `dtype` is passed to\n    `make_dict_mapper` it is used as a the default return dtype,\n    otherwise the default dtype is `object`.\n    \"\"\"\n    return frompyfunc(mapping.__getitem__, 1, 1, dtype)\n\n\nas_string = np.frompyfunc(str, 1, 1)\nparse_float = ftry(float, ValueError, float(\"nan\"))\n\n_parse_float = np.frompyfunc(parse_float, 1, 1)\n\n\ndef as_float_or_nan(\n        arr: ndarray, out: Optional[ndarray] = None, where: Optional[ndarray] = True,\n        dtype=None, **kwargs\n) -> ndarray:\n    \"\"\"\n    Convert elements of the input array using builtin `float`, fill elements\n    where conversion failed with NaN.\n    \"\"\"\n    if out is None:\n        out = np.full(arr.shape, np.nan, float if dtype is None else dtype)\n    if np.issubdtype(arr.dtype, np.inexact) or \\\n            np.issubdtype(arr.dtype, np.integer):\n        np.copyto(out, arr, casting=\"unsafe\", where=where)\n        return out\n    return _parse_float(arr, out, where=where, casting=\"unsafe\", **kwargs)\n\n\ndef copy_attributes(dst: V, src: Orange.data.Variable) -> V:\n    # copy `attributes` and `sparse` members from src to dst\n    dst.attributes = dict(src.attributes)\n    dst.sparse = src.sparse\n    return dst\n\n\n# Building (and applying) concrete type transformations on Table columns\n\n@singledispatch\ndef apply_reinterpret(var, tr, data):\n    # type: (Orange.data.Variable, ReinterpretTransform, MArray) -> Orange.data.Variable\n    \"\"\"\n    Apply a re-interpret transform to an `Orange.data.Table`'s column\n    \"\"\"\n    raise NotImplementedError\n\n\n@apply_reinterpret.register(Orange.data.DiscreteVariable)\ndef apply_reinterpret_d(var, tr, data):\n    # type: (Orange.data.DiscreteVariable, ReinterpretTransform, ndarray) -> Orange.data.Variable\n    if isinstance(tr, AsCategorical):\n        return var\n    elif isinstance(tr, (AsString, AsTime)):\n        # TimeVar will be interpreted by StrpTime later\n        f = Lookup(var, np.array(var.values, dtype=object), unknown=\"\")\n        rvar = Orange.data.StringVariable(name=var.name, compute_value=f)\n    elif isinstance(tr, AsContinuous):\n        f = Lookup(var, np.array(list(map(parse_float, var.values))),\n                   unknown=np.nan)\n        rvar = Orange.data.ContinuousVariable(\n            name=var.name, compute_value=f, sparse=var.sparse\n        )\n    else:\n        assert False\n    return copy_attributes(rvar, var)\n\n\n@apply_reinterpret.register(Orange.data.ContinuousVariable)\ndef apply_reinterpret_c(var, tr, data: MArray):\n    if isinstance(tr, AsCategorical):\n        # This is ill-defined and should not result in a 'compute_value'\n        # (post-hoc expunge from the domain once translated?)\n        values, index = categorize_unique(data)\n        coldata = index.astype(float)\n        coldata[index.mask] = np.nan\n        tr = LookupMappingTransform(\n            var, {v: i for i, v in enumerate(values)}, dtype=np.float64, unknown=np.nan\n        )\n        values = tuple(as_string(values))\n        rvar = Orange.data.DiscreteVariable(\n            name=var.name, values=values, compute_value=tr\n        )\n    elif isinstance(tr, AsContinuous):\n        return var\n    elif isinstance(tr, AsString):\n        # TimeVar will be interpreted by StrpTime later\n        tstr = ToStringTransform(var)\n        rvar = Orange.data.StringVariable(name=var.name, compute_value=tstr)\n    elif isinstance(tr, AsTime):\n        rvar = Orange.data.TimeVariable(name=var.name, compute_value=Identity(var))\n    else:\n        assert False\n    return copy_attributes(rvar, var)\n\n\n@apply_reinterpret.register(Orange.data.StringVariable)\ndef apply_reinterpret_s(var: Orange.data.StringVariable, tr, data: MArray):\n    if isinstance(tr, AsCategorical):\n        # This is ill-defined and should not result in a 'compute_value'\n        # (post-hoc expunge from the domain once translated?)\n        _, values = categorical_from_vector(data)\n        mapping = {v: float(i) for i, v in enumerate(values)}\n        tr = LookupMappingTransform(var, mapping)\n        rvar = Orange.data.DiscreteVariable(\n            name=var.name, values=values, compute_value=tr\n        )\n    elif isinstance(tr, AsContinuous):\n        rvar = Orange.data.ContinuousVariable(\n            var.name, compute_value=ToContinuousTransform(var)\n        )\n    elif isinstance(tr, (AsString, AsTime)):\n        # TimeVar will be interpreted by StrpTime later\n        return var\n    else:\n        assert False\n    return copy_attributes(rvar, var)\n\n\n@apply_reinterpret.register(Orange.data.TimeVariable)\ndef apply_reinterpret_t(var: Orange.data.TimeVariable, tr, data):\n    if isinstance(tr, AsCategorical):\n        values, _ = categorize_unique(data)\n        or_values = values.astype(float) / 1e6\n        mapping = {v: i for i, v in enumerate(or_values)}\n        tr = LookupMappingTransform(var, mapping)\n        values = tuple(as_string(values))\n        rvar = Orange.data.DiscreteVariable(\n            name=var.name, values=values, compute_value=tr\n        )\n    elif isinstance(tr, AsContinuous):\n        rvar = Orange.data.TimeVariable(\n            name=var.name, compute_value=Identity(var)\n        )\n    elif isinstance(tr, AsString):\n        rvar = Orange.data.StringVariable(\n            name=var.name, compute_value=ToStringTransform(var)\n        )\n    elif isinstance(tr, AsTime):\n        return var\n    else:\n        assert False\n    return copy_attributes(rvar, var)\n\n\ndef orange_isna(variable: Orange.data.Variable, data: ndarray) -> ndarray:\n    \"\"\"\n    Return a bool mask masking N/A elements in `data` for the `variable`.\n    \"\"\"\n    if variable.is_primitive():\n        return np.isnan(data)\n    else:\n        return data == variable.Unknown\n\n\nclass ToStringTransform(Transformation):\n    \"\"\"\n    Transform a variable to string.\n    \"\"\"\n    def transform(self, c):\n        if self.variable.is_string:\n            return c\n        elif self.variable.is_discrete or self.variable.is_time:\n            r = column_str_repr(self.variable, c)\n        elif self.variable.is_continuous:\n            r = as_string(c)\n        mask = orange_isna(self.variable, c)\n        return np.where(mask, \"\", r)\n\n\nclass ToContinuousTransform(Transformation):\n    def transform(self, c):\n        if self.variable.is_time:\n            return c\n        elif self.variable.is_continuous:\n            return c\n        elif self.variable.is_discrete:\n            lookup = Lookup(\n                self.variable, as_float_or_nan(self.variable.values),\n                unknown=np.nan\n            )\n            return lookup.transform(c)\n        elif self.variable.is_string:\n            return as_float_or_nan(c)\n        else:\n            raise TypeError\n\n\ndef datetime_to_epoch(dti: pd.DatetimeIndex, only_time) -> np.ndarray:\n    \"\"\"Convert datetime to epoch\"\"\"\n    # when dti has timezone info also the subtracted timestamp must have it\n    # otherwise subtracting fails\n    initial_ts = pd.Timestamp(\"1970-01-01\", tz=None if dti.tz is None else \"UTC\")\n    # pandas in versions before 1.4 don't support subtracting different timezones\n    # remove next two lines when read-the-docs start supporting config files\n    # for subprojects, or they change default python version to 3.8\n    if dti.tz is not None:\n        dti = dti.tz_convert(\"UTC\")\n    delta = dti - (dti.normalize() if only_time else initial_ts)\n    return (delta / pd.Timedelta(\"1s\")).values\n\n\nclass ReparseTimeTransform(Transformation):\n    \"\"\"\n    Re-parse the column's string repr as datetime.\n    \"\"\"\n    def __init__(self, variable, tr):\n        super().__init__(variable)\n        self.tr = tr\n\n    def transform(self, c):\n        # if self.formats is none guess format option is selected\n        formats = self.tr.formats if self.tr.formats is not None else [None]\n        for f in formats:\n            d = pd.to_datetime(c, errors=\"coerce\", format=f)\n            if pd.notnull(d).any():\n                return datetime_to_epoch(d, only_time=not self.tr.have_date)\n        return np.nan\n\n\n# Alias for back compatibility (unpickling transforms)\nLookupMappingTransform = MappingTransform\n\n\n@singledispatch\ndef column_str_repr(var: Orange.data.Variable, coldata: ndarray) -> ndarray:\n    \"\"\"Return a array of str representations of coldata for the `variable.\"\"\"\n    _f = np.frompyfunc(var.repr_val, 1, 1)\n    return _f(coldata)\n\n\n@column_str_repr.register(Orange.data.DiscreteVariable)\ndef column_str_repr_discrete(\n        var: Orange.data.DiscreteVariable, coldata: ndarray\n) -> ndarray:\n    values = np.array(var.values, dtype=object)\n    lookup = Lookup(var, values, \"?\")\n    return lookup.transform(coldata)\n\n\n@column_str_repr.register(Orange.data.StringVariable)\ndef column_str_repr_string(\n        var: Orange.data.StringVariable, coldata: ndarray\n) -> ndarray:\n    return np.where(coldata == var.Unknown, \"?\", coldata)\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWEditDomain).run(Orange.data.Table(\"iris\"))\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/data/oweditdomain.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "ccb73061e",
                "new_commit_hash": "c69450f60",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/data/oweditdomain.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/data/oweditdomain.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 2828,
                            "length": 7
                        },
                        "new_range": {
                            "start": 2828,
                            "length": 15
                        },
                        "section": "class ToContinuousTransform(Transformation):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "def datetime_to_epoch(dti: pd.DatetimeIndex, only_time) -> np.ndarray:",
                                "type": "context"
                            },
                            {
                                "content": "    \"\"\"Convert datetime to epoch\"\"\"",
                                "type": "context"
                            },
                            {
                                "content": "    delta = dti - (dti.normalize() if only_time else pd.Timestamp(\"1970-01-01\"))",
                                "type": "deleted"
                            },
                            {
                                "content": "    # when dti has timezone info also the subtracted timestamp must have it",
                                "type": "added"
                            },
                            {
                                "content": "    # otherwise subtracting fails",
                                "type": "added"
                            },
                            {
                                "content": "    initial_ts = pd.Timestamp(\"1970-01-01\", tz=None if dti.tz is None else \"UTC\")",
                                "type": "added"
                            },
                            {
                                "content": "    # pandas in versions before 1.4 don't support subtracting different timezones",
                                "type": "added"
                            },
                            {
                                "content": "    # remove next two lines when read-the-docs start supporting config files",
                                "type": "added"
                            },
                            {
                                "content": "    # for subprojects, or they change default python version to 3.8",
                                "type": "added"
                            },
                            {
                                "content": "    if dti.tz is not None:",
                                "type": "added"
                            },
                            {
                                "content": "        dti = dti.tz_convert(\"UTC\")",
                                "type": "added"
                            },
                            {
                                "content": "    delta = dti - (dti.normalize() if only_time else initial_ts)",
                                "type": "added"
                            },
                            {
                                "content": "    return (delta / pd.Timedelta(\"1s\")).values",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/data/oweditdomain.py",
                            "type": "function",
                            "name": "datetime_to_epoch",
                            "content": "def datetime_to_epoch(dti: pd.DatetimeIndex, only_time) -> np.ndarray:\n    \"\"\"Convert datetime to epoch\"\"\"\n    delta = dti - (dti.normalize() if only_time else pd.Timestamp(\"1970-01-01\"))\n    return (delta / pd.Timedelta(\"1s\")).values",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 2829,
                            "end_lineno": 2832,
                            "parent": null
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        },
        {
            "old_file_content": "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=all\nimport pickle\nimport unittest\nfrom functools import partial\nfrom itertools import product, chain\nfrom unittest import TestCase\nfrom unittest.mock import Mock, patch\n\nimport numpy as np\nfrom numpy.testing import assert_array_equal\n\nfrom AnyQt.QtCore import QItemSelectionModel, Qt, QItemSelection, QPoint\nfrom AnyQt.QtGui import QPalette, QColor, QHelpEvent\nfrom AnyQt.QtWidgets import QAction, QComboBox, QLineEdit, \\\n    QStyleOptionViewItem, QDialog, QMenu, QToolTip, QListView\nfrom AnyQt.QtTest import QTest, QSignalSpy\n\nfrom orangewidget.tests.utils import simulate\n\nfrom Orange.data import (\n    ContinuousVariable, DiscreteVariable, StringVariable, TimeVariable,\n    Table, Domain\n)\nfrom Orange.preprocess.transformation import Identity, Lookup\nfrom Orange.widgets.data.oweditdomain import (\n    OWEditDomain,\n    ContinuousVariableEditor, DiscreteVariableEditor, VariableEditor,\n    TimeVariableEditor, Categorical, Real, Time, String,\n    Rename, Annotate, Unlink, CategoriesMapping, report_transform,\n    apply_transform, apply_transform_var, apply_reinterpret, MultiplicityRole,\n    AsString, AsCategorical, AsContinuous, AsTime,\n    table_column_data, ReinterpretVariableEditor, CategoricalVector,\n    VariableEditDelegate, TransformRole,\n    RealVector, TimeVector, StringVector, make_dict_mapper,\n    LookupMappingTransform, as_float_or_nan, column_str_repr,\n    GroupItemsDialog, VariableListModel, StrpTime\n)\nfrom Orange.widgets.data.owcolor import OWColor, ColorRole\nfrom Orange.widgets.tests.base import WidgetTest, GuiTest\nfrom Orange.widgets.tests.utils import contextMenu\nfrom Orange.widgets.utils import colorpalettes\nfrom Orange.tests import test_filename, assert_array_nanequal\n\nMArray = np.ma.MaskedArray\n\n\nclass TestReport(TestCase):\n    def test_rename(self):\n        var = Real(\"X\", (-1, \"\"), (), False)\n        tr = Rename(\"Y\")\n        val = report_transform(var, [tr])\n        self.assertIn(\"X\", val)\n        self.assertIn(\"Y\", val)\n\n    def test_annotate(self):\n        var = Real(\"X\", (-1, \"\"), ((\"a\", \"1\"), (\"b\", \"z\")), False)\n        tr = Annotate(((\"a\", \"2\"), (\"j\", \"z\")))\n        r = report_transform(var, [tr])\n        self.assertIn(\"a\", r)\n        self.assertIn(\"b\", r)\n\n    def test_unlinke(self):\n        var = Real(\"X\", (-1, \"\"), ((\"a\", \"1\"), (\"b\", \"z\")), True)\n        r = report_transform(var, [Unlink()])\n        self.assertIn(\"unlinked\", r)\n\n    def test_categories_mapping(self):\n        var = Categorical(\"C\", (\"a\", \"b\", \"c\"), (), False)\n        tr = CategoriesMapping(\n            ((\"a\", \"aa\"),\n             (\"b\", None),\n             (\"c\", \"cc\"),\n             (None, \"ee\")),\n        )\n        r = report_transform(var, [tr])\n        self.assertIn(\"a\", r)\n        self.assertIn(\"aa\", r)\n        self.assertIn(\"b\", r)\n        self.assertIn(\"<s>\", r)\n\n    def test_categorical_merge_mapping(self):\n        var = Categorical(\"C\", (\"a\", \"b1\", \"b2\"), (), False)\n        tr = CategoriesMapping(\n            ((\"a\", \"a\"),\n             (\"b1\", \"b\"),\n             (\"b2\", \"b\"),\n             (None, \"c\")),\n        )\n        r = report_transform(var, [tr])\n        self.assertIn('b', r)\n\n    def test_reinterpret(self):\n        var = String(\"T\", (), False)\n        for tr in (AsContinuous(), AsCategorical(), AsTime()):\n            t = report_transform(var, [tr])\n            self.assertIn(\"→ (\", t)\n\n\nclass TestOWEditDomain(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(OWEditDomain)\n        self.iris = Table(\"iris\")\n\n    def test_input_data(self):\n        \"\"\"Check widget's data with data on the input\"\"\"\n        self.assertEqual(self.widget.data, None)\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertEqual(self.widget.data, self.iris)\n\n    def test_input_data_disconnect(self):\n        \"\"\"Check widget's data after disconnecting data on the input\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertEqual(self.widget.data, self.iris)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(self.widget.data, None)\n\n    def test_widget_state(self):\n        \"\"\"Check if widget clears its state when the input is disconnected\"\"\"\n        editor = self.widget.findChild(ContinuousVariableEditor)\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertEqual(editor.name_edit.text(), \"sepal length\")\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(editor.name_edit.text(), \"\")\n        self.assertEqual(\n            self.widget.variables_model.index(0).data(Qt.EditRole), None)\n\n        editor = self.widget.findChild(DiscreteVariableEditor)\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        index = self.widget.domain_view.model().index(4)\n        self.widget.variables_view.setCurrentIndex(index)\n        self.assertEqual(editor.name_edit.text(), \"iris\")\n        self.assertEqual(editor.labels_model.get_dict(), {})\n        self.assertNotEqual(\n            self.widget.variables_model.index(0).data(Qt.EditRole), None)\n        model = editor.values_edit.selectionModel().model()\n        self.assertEqual(model.index(0).data(Qt.EditRole), \"Iris-setosa\")\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(editor.name_edit.text(), \"\")\n        self.assertEqual(editor.labels_model.get_dict(), {})\n        self.assertEqual(model.index(0).data(Qt.EditRole), None)\n        self.assertEqual(\n            self.widget.variables_model.index(0).data(Qt.EditRole), None)\n\n        editor = self.widget.findChild(TimeVariableEditor)\n        table = Table(test_filename(\"datasets/cyber-security-breaches.tab\"))\n        self.send_signal(self.widget.Inputs.data, table)\n        index = self.widget.domain_view.model().index(4)\n        self.widget.variables_view.setCurrentIndex(index)\n        self.assertEqual(editor.name_edit.text(), \"Date_Posted_or_Updated\")\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(editor.name_edit.text(), \"\")\n        self.assertEqual(\n            self.widget.variables_model.index(0).data(Qt.EditRole), None)\n\n        editor = self.widget.findChild(VariableEditor)\n        self.send_signal(self.widget.Inputs.data, table)\n        index = self.widget.domain_view.model().index(8)\n        self.widget.variables_view.setCurrentIndex(index)\n        self.assertEqual(editor.var.name, \"Business_Associate_Involved\")\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(editor.var, None)\n        self.assertEqual(\n            self.widget.variables_model.index(0).data(Qt.EditRole), None)\n\n    def test_output_data(self):\n        \"\"\"Check data on the output after apply\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        output = self.get_output(self.widget.Outputs.data)\n        np.testing.assert_array_equal(output.X, self.iris.X)\n        np.testing.assert_array_equal(output.Y, self.iris.Y)\n        self.assertEqual(output.domain, self.iris.domain)\n\n        self.widget.output_table_name = \"Iris 2\"\n        self.widget.commit()\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(output.name, \"Iris 2\")\n\n    def test_input_from_owcolor(self):\n        \"\"\"Check widget's data sent from OWColor widget\"\"\"\n        owcolor = self.create_widget(OWColor)\n        self.send_signal(\"Data\", self.iris, widget=owcolor)\n        disc_model = owcolor.disc_model\n        disc_model.setData(disc_model.index(0, 1), (1, 2, 3), ColorRole)\n        cont_model = owcolor.cont_model\n        palette = list(colorpalettes.ContinuousPalettes.values())[-1]\n        cont_model.setData(cont_model.index(1, 1), palette, ColorRole)\n        owcolor_output = self.get_output(\"Data\", owcolor)\n        self.send_signal(\"Data\", owcolor_output)\n        self.assertEqual(self.widget.data, owcolor_output)\n        np.testing.assert_equal(self.widget.data.domain.class_var.colors[0],\n                                (1, 2, 3))\n        self.assertIs(self.widget.data.domain.attributes[1].palette, palette)\n\n    def test_list_attributes_remain_lists(self):\n        a = ContinuousVariable(\"a\")\n        a.attributes[\"list\"] = [1, 2, 3]\n        d = Domain([a])\n        t = Table.from_domain(d)\n\n        self.send_signal(self.widget.Inputs.data, t)\n\n        assert isinstance(self.widget, OWEditDomain)\n        # select first variable\n        idx = self.widget.domain_view.model().index(0)\n        self.widget.domain_view.setCurrentIndex(idx)\n\n        # change first attribute value\n        editor = self.widget.findChild(ContinuousVariableEditor)\n        assert isinstance(editor, ContinuousVariableEditor)\n        idx = editor.labels_model.index(0, 1)\n        editor.labels_model.setData(idx, \"[1, 2, 4]\", Qt.EditRole)\n\n        self.widget.commit()\n        t2 = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(t2.domain[\"a\"].attributes[\"list\"], [1, 2, 4])\n\n    def test_annotation_bool(self):\n        \"\"\"Check if bool labels remain bool\"\"\"\n        a = ContinuousVariable(\"a\")\n        a.attributes[\"hidden\"] = True\n        d = Domain([a])\n        t = Table.from_domain(d)\n\n        self.send_signal(self.widget.Inputs.data, t)\n\n        assert isinstance(self.widget, OWEditDomain)\n        # select first variable\n        idx = self.widget.domain_view.model().index(0)\n        self.widget.domain_view.setCurrentIndex(idx)\n\n        # change first attribute value\n        editor = self.widget.findChild(ContinuousVariableEditor)\n        assert isinstance(editor, ContinuousVariableEditor)\n        idx = editor.labels_model.index(0, 1)\n        editor.labels_model.setData(idx, \"False\", Qt.EditRole)\n\n        self.widget.commit()\n        t2 = self.get_output(self.widget.Outputs.data)\n        self.assertFalse(t2.domain[\"a\"].attributes[\"hidden\"])\n\n    def test_duplicate_names(self):\n        \"\"\"\n        Tests if widget shows error when duplicate name is entered.\n        And tests if widget sends None data when error is shown.\n        GH-2143\n        GH-2146\n        \"\"\"\n        table = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertFalse(self.widget.Error.duplicate_var_name.is_shown())\n\n        idx = self.widget.domain_view.model().index(0)\n        self.widget.domain_view.setCurrentIndex(idx)\n        editor = self.widget.findChild(ContinuousVariableEditor)\n\n        def enter_text(widget, text):\n            # type: (QLineEdit, str) -> None\n            widget.selectAll()\n            QTest.keyClick(widget, Qt.Key_Delete)\n            QTest.keyClicks(widget, text)\n            QTest.keyClick(widget, Qt.Key_Return)\n\n        enter_text(editor.name_edit, \"iris\")\n        self.widget.commit()\n        self.assertTrue(self.widget.Error.duplicate_var_name.is_shown())\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertIsNone(output)\n\n        enter_text(editor.name_edit, \"sepal height\")\n        self.widget.commit()\n        self.assertFalse(self.widget.Error.duplicate_var_name.is_shown())\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertIsInstance(output, Table)\n\n    def test_unlink(self):\n        var0, var1, var2 = [ContinuousVariable(\"x\", compute_value=Mock()),\n                            ContinuousVariable(\"y\", compute_value=Mock()),\n                            ContinuousVariable(\"z\")]\n        domain = Domain([var0, var1, var2], None)\n        table = Table.from_numpy(domain, np.zeros((5, 3)), np.zeros((5, 0)))\n        self.send_signal(self.widget.Inputs.data, table)\n\n        index = self.widget.domain_view.model().index\n        for i in range(3):\n            self.widget.domain_view.setCurrentIndex(index(i))\n            editor = self.widget.findChild(ContinuousVariableEditor)\n            self.assertIs(editor.unlink_var_cb.isEnabled(), i < 2)\n            editor._set_unlink(i == 1)\n\n        self.widget.commit()\n        out = self.get_output(self.widget.Outputs.data)\n        out0, out1, out2 = out.domain.variables\n        self.assertIs(out0, domain[0])\n        self.assertIsNot(out1, domain[1])\n        self.assertIs(out2, domain[2])\n\n        self.assertIsNotNone(out0.compute_value)\n        self.assertIsNone(out1.compute_value)\n        self.assertIsNone(out2.compute_value)\n\n    def test_time_variable_preservation(self):\n        \"\"\"Test if time variables preserve format specific attributes\"\"\"\n        table = Table(test_filename(\"datasets/cyber-security-breaches.tab\"))\n        self.send_signal(self.widget.Inputs.data, table)\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(str(table[0, 4]), str(output[0, 4]))\n        view = self.widget.variables_view\n        view.setCurrentIndex(view.model().index(4))\n\n        editor = self.widget.findChild(TimeVariableEditor)\n        editor.name_edit.setText(\"Date\")\n        editor.variable_changed.emit()\n        self.widget.commit()\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(str(table[0, 4]), str(output[0, 4]))\n\n    def test_restore(self):\n        iris = self.iris\n        viris = (\n            \"Categorical\",\n            (\"iris\", (\"Iris-setosa\", \"Iris-versicolor\", \"Iris-virginica\"), (),\n             False)\n        )\n        w = self.widget\n\n        def restore(state):\n            w._domain_change_store = state\n            w._restore()\n\n        model = w.variables_model\n        self.send_signal(w.Inputs.data, iris, widget=w)\n        restore({viris: [(\"Rename\", (\"Z\",))]})\n        tr = model.data(model.index(4), TransformRole)\n        self.assertEqual(tr, [Rename(\"Z\")])\n\n        restore({viris: [(\"AsString\", ()), (\"Rename\", (\"Z\",))]})\n        tr = model.data(model.index(4), TransformRole)\n        self.assertEqual(tr, [AsString(), Rename(\"Z\")])\n\n\nclass TestEditors(GuiTest):\n    def test_variable_editor(self):\n        w = VariableEditor()\n        self.assertEqual(w.get_data(), (None, []))\n\n        v = String(\"S\", ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        w.set_data(v, [])\n\n        self.assertEqual(w.name_edit.text(), v.name)\n        self.assertEqual(w.labels_model.get_dict(),\n                         {\"A\": \"1\", \"B\": \"b\"})\n        self.assertEqual(w.get_data(), (v, []))\n\n        w.set_data(None)\n        self.assertEqual(w.name_edit.text(), \"\")\n        self.assertEqual(w.labels_model.get_dict(), {})\n        self.assertEqual(w.get_data(), (None, []))\n\n        w.set_data(v, [Rename(\"T\"), Annotate(((\"a\", \"1\"), (\"b\", \"2\")))])\n        self.assertEqual(w.name_edit.text(), \"T\")\n        self.assertEqual(w.labels_model.rowCount(), 2)\n        add = w.findChild(QAction, \"action-add-label\")\n        add.trigger()\n        remove = w.findChild(QAction, \"action-delete-label\")\n        remove.trigger()\n\n    def test_continuous_editor(self):\n        w = ContinuousVariableEditor()\n        self.assertEqual(w.get_data(), (None, []))\n\n        v = Real(\"X\", (-1, \"\"), ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        w.set_data(v, [])\n\n        self.assertEqual(w.name_edit.text(), v.name)\n        self.assertEqual(w.labels_model.get_dict(), dict(v.annotations))\n\n        w.set_data(None)\n        self.assertEqual(w.name_edit.text(), \"\")\n        self.assertEqual(w.labels_model.get_dict(), {})\n        self.assertEqual(w.get_data(), (None, []))\n\n    def test_discrete_editor(self):\n        w = DiscreteVariableEditor()\n        self.assertEqual(w.get_data(), (None, []))\n\n        v = Categorical(\"C\", (\"a\", \"b\", \"c\"), ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        values = [0, 0, 0, 1, 1, 2]\n        w.set_data_categorical(v, values)\n\n        self.assertEqual(w.name_edit.text(), v.name)\n        self.assertEqual(w.labels_model.get_dict(), dict(v.annotations))\n        self.assertEqual(w.get_data(), (v, []))\n        w.set_data_categorical(None, None)\n        self.assertEqual(w.name_edit.text(), \"\")\n        self.assertEqual(w.labels_model.get_dict(), {})\n        self.assertEqual(w.get_data(), (None, []))\n        mapping = [\n            (\"c\", \"C\"),\n            (\"a\", \"A\"),\n            (\"b\", None),\n            (None, \"b\")\n        ]\n        w.set_data_categorical(v, values, [CategoriesMapping(mapping)])\n        w.grab()  # run delegate paint method\n        self.assertEqual(w.get_data(), (v, [CategoriesMapping(mapping)]))\n\n        # test selection/deselection in the view\n        w.set_data_categorical(v, values)\n        view = w.values_edit\n        model = view.model()\n        assert model.rowCount()\n        sel_model = view.selectionModel()\n        model = sel_model.model()\n        sel_model.select(model.index(0, 0), QItemSelectionModel.Select)\n        sel_model.select(model.index(0, 0), QItemSelectionModel.Deselect)\n\n        # merge mapping\n        mapping = [\n            (\"a\", \"a\"),\n            (\"b\", \"b\"),\n            (\"c\", \"b\")\n        ]\n        w.set_data_categorical(v, values, [CategoriesMapping(mapping)])\n        self.assertEqual(w.get_data()[1], [CategoriesMapping(mapping)])\n        self.assertEqual(model.data(model.index(0, 0), MultiplicityRole), 1)\n        self.assertEqual(model.data(model.index(1, 0), MultiplicityRole), 2)\n        self.assertEqual(model.data(model.index(2, 0), MultiplicityRole), 2)\n        w.grab()\n        model.setData(model.index(0, 0), \"b\", Qt.EditRole)\n        self.assertEqual(model.data(model.index(0, 0), MultiplicityRole), 3)\n        self.assertEqual(model.data(model.index(1, 0), MultiplicityRole), 3)\n        self.assertEqual(model.data(model.index(2, 0), MultiplicityRole), 3)\n        w.grab()\n\n    def test_discrete_editor_add_remove_action(self):\n        w = DiscreteVariableEditor()\n        v = Categorical(\"C\", (\"a\", \"b\", \"c\"),\n                        ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        values = [0, 0, 0, 1, 1, 2]\n        w.set_data_categorical(v, values)\n        action_add = w.add_new_item\n        action_remove = w.remove_item\n        view = w.values_edit\n        model, selection = view.model(), view.selectionModel()\n        selection.clear()\n\n        action_add.trigger()\n        self.assertTrue(view.state() == view.EditingState)\n        editor = view.focusWidget()\n        assert isinstance(editor, QLineEdit)\n        spy = QSignalSpy(model.dataChanged)\n        QTest.keyClick(editor, Qt.Key_D)\n        QTest.keyClick(editor, Qt.Key_Return)\n        self.assertTrue(model.rowCount() == 4)\n        # The commit to model is executed via a queued invoke\n        self.assertTrue(bool(spy) or spy.wait())\n        self.assertEqual(model.index(3, 0).data(Qt.EditRole), \"d\")\n        # remove it\n        spy = QSignalSpy(model.rowsRemoved)\n        action_remove.trigger()\n        self.assertEqual(model.rowCount(), 3)\n        self.assertEqual(len(spy), 1)\n        _, first, last = spy[0]\n        self.assertEqual((first, last), (3, 3))\n        # remove/drop and existing value\n        selection.select(model.index(1, 0), QItemSelectionModel.ClearAndSelect)\n        removespy = QSignalSpy(model.rowsRemoved)\n        changedspy = QSignalSpy(model.dataChanged)\n        action_remove.trigger()\n        self.assertEqual(len(removespy), 0, \"Should only mark item as removed\")\n        self.assertGreaterEqual(len(changedspy), 1, \"Did not change data\")\n        w.grab()\n\n    # mocking exec make dialog never show - dialog blocks running until closed\n    @patch(\n        \"Orange.widgets.data.oweditdomain.GroupItemsDialog.exec\",\n        Mock(side_effect=lambda: QDialog.Accepted)\n    )\n    def test_discrete_editor_merge_action(self):\n        \"\"\"\n        This function check whether results of dialog have effect on\n        merging the attributes. The dialog itself is tested separately.\n        \"\"\"\n        w = DiscreteVariableEditor()\n        v = Categorical(\"C\", (\"a\", \"b\", \"c\"),\n                        ((\"A\", \"1\"), (\"B\", \"b\")), False)\n\n        w.set_data_categorical(\n            v, [0, 0, 0, 1, 1, 2],\n            [CategoriesMapping([\n                (\"a\", \"AA\"), (\"b\", \"BB\"), (\"c\", \"CC\"),\n            ])]\n        )\n\n        view = w.values_edit\n        model = view.model()\n        selmodel = view.selectionModel()  # type: QItemSelectionModel\n        selmodel.select(\n            QItemSelection(model.index(0, 0), model.index(1, 0)),\n            QItemSelectionModel.ClearAndSelect\n        )\n        spy = QSignalSpy(w.variable_changed)\n        w.merge_items.trigger()\n\n        self.assertEqual(model.index(0, 0).data(Qt.EditRole), \"other\")\n        self.assertEqual(model.index(1, 0).data(Qt.EditRole), \"other\")\n        self.assertEqual(model.index(2, 0).data(Qt.EditRole), \"CC\")\n\n        self.assertSequenceEqual(\n            list(spy), [[]], 'variable_changed should emit exactly once'\n        )\n\n    def test_discrete_editor_rename_selected_items_action(self):\n        w = DiscreteVariableEditor()\n        v = Categorical(\"C\", (\"a\", \"b\", \"c\"),\n                        ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        w.set_data_categorical(v, [])\n        action = w.rename_selected_items\n        view = w.values_edit\n        model = view.model()\n        selmodel = view.selectionModel()  # type: QItemSelectionModel\n        selmodel.select(\n            QItemSelection(model.index(0, 0), model.index(1, 0)),\n            QItemSelectionModel.ClearAndSelect\n        )\n        # trigger the action, then find the active popup, and simulate entry\n        spy = QSignalSpy(w.variable_changed)\n        with patch.object(QComboBox, \"setVisible\", return_value=None) as m:\n            action.trigger()\n            m.assert_called()\n        cb = view.findChild(QComboBox)\n        cb.setCurrentText(\"BA\")\n        view.commitData(cb)\n        self.assertEqual(model.index(0, 0).data(Qt.EditRole), \"BA\")\n        self.assertEqual(model.index(1, 0).data(Qt.EditRole), \"BA\")\n        self.assertSequenceEqual(\n            list(spy), [[]], 'variable_changed should emit exactly once'\n        )\n\n    def test_discrete_editor_context_menu(self):\n        w = DiscreteVariableEditor()\n        v = Categorical(\"C\", (\"a\", \"b\", \"c\"),\n                        ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        w.set_data_categorical(v, [])\n        view = w.values_edit\n        model = view.model()\n\n        pos = view.visualRect(model.index(0, 0)).center()\n        with patch.object(QMenu, \"setVisible\", return_value=None) as m:\n            contextMenu(view.viewport(), pos)\n            m.assert_called()\n\n        menu = view.findChild(QMenu)\n        self.assertIsNotNone(menu)\n        menu.close()\n\n    def test_time_editor(self):\n        w = TimeVariableEditor()\n        self.assertEqual(w.get_data(), (None, []))\n\n        v = Time(\"T\", ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        w.set_data(v,)\n\n        self.assertEqual(w.name_edit.text(), v.name)\n        self.assertEqual(w.labels_model.get_dict(), dict(v.annotations))\n\n        w.set_data(None)\n        self.assertEqual(w.name_edit.text(), \"\")\n        self.assertEqual(w.labels_model.get_dict(), {})\n        self.assertEqual(w.get_data(), (None, []))\n\n    DataVectors = [\n        CategoricalVector(\n            Categorical(\"A\", (\"a\", \"aa\"), (), False), lambda:\n                MArray([0, 1, 2], mask=[False, False, True])\n        ),\n        RealVector(\n            Real(\"B\", (6, \"f\"), (), False), lambda:\n                MArray([0.1, 0.2, 0.3], mask=[True, False, True])\n        ),\n        TimeVector(\n            Time(\"T\", (), False), lambda:\n                MArray([0, 100, 200], dtype=\"M8[us]\", mask=[True, False, True])\n        ),\n        StringVector(\n            String(\"S\", (), False), lambda:\n                MArray([\"0\", \"1\", \"2\"], dtype=object, mask=[True, False, True])\n        ),\n    ]\n    ReinterpretTransforms = {\n        Categorical: [AsCategorical], Real: [AsContinuous],\n        Time: [AsTime, partial(StrpTime, 'Detect automatically', None, 1, 1)],\n        String: [AsString]\n    }\n\n    def test_reinterpret_editor(self):\n        w = ReinterpretVariableEditor()\n        self.assertEqual(w.get_data(), (None, []))\n        data = self.DataVectors[0]\n        w.set_data(data, )\n        self.assertEqual(w.get_data(), (data.vtype, []))\n        w.set_data(data, [Rename(\"Z\")])\n        self.assertEqual(w.get_data(), (data.vtype, [Rename(\"Z\")]))\n\n        for vec, tr in product(self.DataVectors, self.ReinterpretTransforms.values()):\n            w.set_data(vec, [t() for t in tr])\n            v, tr_ = w.get_data()\n            self.assertEqual(v, vec.vtype)\n            if not tr_:\n                self.assertEqual(tr, self.ReinterpretTransforms[type(v)])\n            else:\n                self.assertListEqual(tr_, [t() for t in tr])\n\n    def test_reinterpret_editor_simulate(self):\n        w = ReinterpretVariableEditor()\n        tc = w.findChild(QComboBox, name=\"type-combo\")\n\n        def cb():\n            var, tr = w.get_data()\n            type_ = tc.currentData()\n            if type_ is not type(var):\n                self.assertEqual(\n                    tr, [t() for t in self.ReinterpretTransforms[type_]] + [Rename(\"Z\")]\n                )\n            else:\n                self.assertEqual(tr, [Rename(\"Z\")])\n\n        for vec in self.DataVectors:\n            w.set_data(vec, [Rename(\"Z\")])\n            simulate.combobox_run_through_all(tc, callback=cb)\n\n    def test_unlink(self):\n        w = ContinuousVariableEditor()\n        cbox = w.unlink_var_cb\n        self.assertEqual(w.get_data(), (None, []))\n\n        v = Real(\"X\", (-1, \"\"), ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        w.set_data(v, [])\n        self.assertFalse(cbox.isEnabled())\n\n        v = Real(\"X\", (-1, \"\"), ((\"A\", \"1\"), (\"B\", \"b\")), True)\n        w.set_data(v, [Unlink()])\n        self.assertTrue(cbox.isEnabled())\n        self.assertTrue(cbox.isChecked())\n\n        v = Real(\"X\", (-1, \"\"), ((\"A\", \"1\"), (\"B\", \"b\")), True)\n        w.set_data(v, [])\n        self.assertTrue(cbox.isEnabled())\n        self.assertFalse(cbox.isChecked())\n\n        cbox.setChecked(True)\n        self.assertEqual(w.get_data()[1], [Unlink()])\n\n        w.set_data(v, [Unlink()])\n        self.assertTrue(cbox.isChecked())\n\n        cbox.setChecked(False)\n        self.assertEqual(w.get_data()[1], [])\n\n        cbox.setChecked(True)\n        w.clear()\n        self.assertFalse(cbox.isChecked())\n        self.assertEqual(w.get_data()[1], [])\n\n        w._set_unlink(True)\n        self.assertTrue(cbox.isChecked())\n        w._set_unlink(False)\n        self.assertFalse(cbox.isChecked())\n\n\nclass TestModels(GuiTest):\n    def test_variable_model(self):\n        model = VariableListModel()\n        self.assertEqual(model.effective_name(model.index(-1, -1)), None)\n\n        def data(row, role):\n            return model.data(model.index(row,), role)\n\n        def set_data(row, data, role):\n            model.setData(model.index(row), data, role)\n\n        model[:] = [\n            RealVector(Real(\"A\", (3, \"g\"), (), False), lambda: MArray([])),\n            RealVector(Real(\"B\", (3, \"g\"), (), False), lambda: MArray([])),\n        ]\n        self.assertEqual(data(0, Qt.DisplayRole), \"A\")\n        self.assertEqual(data(1, Qt.DisplayRole), \"B\")\n        self.assertEqual(model.effective_name(model.index(1)), \"B\")\n        set_data(1, [Rename(\"A\")], TransformRole)\n        self.assertEqual(model.effective_name(model.index(1)), \"A\")\n        self.assertEqual(data(0, MultiplicityRole), 2)\n        self.assertEqual(data(1, MultiplicityRole), 2)\n        set_data(1, [], TransformRole)\n        self.assertEqual(data(0, MultiplicityRole), 1)\n        self.assertEqual(data(1, MultiplicityRole), 1)\n\n\nclass TestDelegates(GuiTest):\n    def test_delegate(self):\n        model = VariableListModel([None, None])\n\n        def set_item(row: int, v: dict):\n            model.setItemData(model.index(row),  v)\n\n        def get_style_option(row: int) -> QStyleOptionViewItem:\n            opt = QStyleOptionViewItem()\n            delegate.initStyleOption(opt, model.index(row))\n            return opt\n\n        set_item(0, {Qt.EditRole: Categorical(\"a\", (), (), False)})\n        delegate = VariableEditDelegate()\n        opt = get_style_option(0)\n        self.assertEqual(opt.text, \"a\")\n        self.assertFalse(opt.font.italic())\n        set_item(0, {TransformRole: [Rename(\"b\")]})\n        opt = get_style_option(0)\n        self.assertEqual(opt.text, \"a \\N{RIGHTWARDS ARROW} b\")\n        self.assertTrue(opt.font.italic())\n\n        set_item(0, {TransformRole: [AsString()]})\n        opt = get_style_option(0)\n        self.assertIn(\"reinterpreted\", opt.text)\n        self.assertTrue(opt.font.italic())\n        set_item(1, {\n            Qt.EditRole: String(\"b\", (), False),\n            TransformRole: [Rename(\"a\")]\n        })\n        opt = get_style_option(1)\n        self.assertEqual(opt.palette.color(QPalette.Text), QColor(Qt.red))\n        view = QListView()\n        with patch.object(QToolTip, \"showText\") as p:\n            delegate.helpEvent(\n                QHelpEvent(QHelpEvent.ToolTip, QPoint(0, 0), QPoint(0, 0)),\n                view, opt, model.index(1),\n            )\n            p.assert_called_once()\n\n\nclass TestTransforms(TestCase):\n    def _test_common(self, var):\n        tr = [Rename(var.name + \"_copy\"), Annotate(((\"A\", \"1\"),))]\n        XX = apply_transform_var(var, tr)\n        self.assertEqual(XX.name, var.name + \"_copy\")\n        self.assertEqual(XX.attributes, {\"A\": 1})\n        self.assertIsInstance(XX.compute_value, Identity)\n        self.assertIs(XX.compute_value.variable, var)\n\n    def test_continous(self):\n        X = ContinuousVariable(\"X\")\n        self._test_common(X)\n\n    def test_string(self):\n        X = StringVariable(\"S\")\n        self._test_common(X)\n\n    def test_time(self):\n        X = TimeVariable(\"X\")\n        self._test_common(X)\n\n    def test_discrete(self):\n        D = DiscreteVariable(\"D\", values=(\"a\", \"b\"))\n        self._test_common(D)\n\n    def test_discrete_rename(self):\n        D = DiscreteVariable(\"D\", values=(\"a\", \"b\"))\n        DD = apply_transform_var(D, [CategoriesMapping(((\"a\", \"A\"), (\"b\", \"B\")))])\n        self.assertSequenceEqual(DD.values, [\"A\", \"B\"])\n        self.assertIs(DD.compute_value.variable, D)\n\n    def test_discrete_reorder(self):\n        D = DiscreteVariable(\"D\", values=(\"2\", \"3\", \"1\", \"0\"))\n        DD = apply_transform_var(D, [CategoriesMapping(((\"0\", \"0\"), (\"1\", \"1\"),\n                                                    (\"2\", \"2\"), (\"3\", \"3\")))])\n        self.assertSequenceEqual(DD.values, [\"0\", \"1\", \"2\", \"3\"])\n        self._assertLookupEquals(\n            DD.compute_value, Lookup(D, np.array([2, 3, 1, 0]))\n        )\n\n    def test_discrete_add_drop(self):\n        D = DiscreteVariable(\"D\", values=(\"2\", \"3\", \"1\", \"0\"))\n        mapping = (\n            (\"0\", None),\n            (\"1\", \"1\"),\n            (\"2\", \"2\"),\n            (\"3\", None),\n            (None, \"A\"),\n        )\n        tr = [CategoriesMapping(mapping)]\n        DD = apply_transform_var(D, tr)\n        self.assertSequenceEqual(DD.values, [\"1\", \"2\", \"A\"])\n        self._assertLookupEquals(\n            DD.compute_value, Lookup(D, np.array([1, np.nan, 0, np.nan]))\n        )\n\n    def test_discrete_merge(self):\n        D = DiscreteVariable(\"D\", values=(\"2\", \"3\", \"1\", \"0\"))\n        mapping = (\n            (\"0\", \"x\"),\n            (\"1\", \"y\"),\n            (\"2\", \"x\"),\n            (\"3\", \"y\"),\n        )\n        tr = [CategoriesMapping(mapping)]\n        DD = apply_transform_var(D, tr)\n        self.assertSequenceEqual(DD.values, [\"x\", \"y\"])\n        self._assertLookupEquals(\n            DD.compute_value, Lookup(D, np.array([0, 1, 1, 0]))\n        )\n\n    def _assertLookupEquals(self, first, second):\n        self.assertIsInstance(first, Lookup)\n        self.assertIsInstance(second, Lookup)\n        self.assertIs(first.variable, second.variable)\n        assert_array_equal(first.lookup_table, second.lookup_table)\n\n\nclass TestReinterpretTransforms(TestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        super().setUpClass()\n        domain = Domain([\n            DiscreteVariable(\"A\", values=(\"a\", \"b\", \"c\")),\n            DiscreteVariable(\"B\", values=(\"0\", \"1\", \"2\")),\n            ContinuousVariable(\"C\"),\n            TimeVariable(\"D\", have_time=True),\n        ],\n        metas=[\n            StringVariable(\"S\")\n        ])\n        cls.data = Table.from_list(\n            domain, [\n                [0, 2, 0.25, 180],\n                [1, 1, 1.25, 360],\n                [2, 0, 0.20, 720],\n                [1, 0, 0.00, 000],\n            ]\n        )\n        cls.data_str = Table.from_list(\n            Domain([], [], metas=[\n                StringVariable(\"S\"),\n                StringVariable(\"T\")\n            ]),\n            [[\"0.1\", \"2010\"],\n             [\"1.0\", \"2020\"]]\n        )\n\n    def test_as_string(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsString()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        ttable = table.transform(Domain([], [], dtr))\n        assert_array_equal(\n            ttable.metas,\n            np.array([\n                [\"a\", \"2\", \"0.25\", \"00:03:00\"],\n                [\"b\", \"1\", \"1.25\", \"00:06:00\"],\n                [\"c\", \"0\", \"0.2\", \"00:12:00\"],\n                [\"b\", \"0\", \"0.0\", \"00:00:00\"],\n            ], dtype=object)\n        )\n\n    def test_as_discrete(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsCategorical()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        tdomain = Domain(dtr)\n        ttable = table.transform(tdomain)\n        assert_array_equal(\n            ttable.X,\n            np.array([\n                [0, 2, 2, 1],\n                [1, 1, 3, 2],\n                [2, 0, 1, 3],\n                [1, 0, 0, 0],\n            ], dtype=float)\n        )\n        self.assertEqual(tdomain[\"A\"].values, (\"a\", \"b\", \"c\"))\n        self.assertEqual(tdomain[\"B\"].values, (\"0\", \"1\", \"2\"))\n        self.assertEqual(tdomain[\"C\"].values, (\"0.0\", \"0.2\", \"0.25\", \"1.25\"))\n        self.assertEqual(\n            tdomain[\"D\"].values,\n            (\"1970-01-01 00:00:00\", \"1970-01-01 00:03:00\",\n             \"1970-01-01 00:06:00\", \"1970-01-01 00:12:00\")\n        )\n\n    def test_as_continuous(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsContinuous()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        ttable = table.transform(Domain(dtr))\n        assert_array_equal(\n            ttable.X,\n            np.array([\n                [np.nan, 2, 0.25, 180],\n                [np.nan, 1, 1.25, 360],\n                [np.nan, 0, 0.20, 720],\n                [np.nan, 0, 0.00, 000],\n            ], dtype=float)\n        )\n\n    def test_as_time(self):\n        # this test only test type of format that can be string, continuous and discrete\n        # correctness of time formats is already tested in TimeVariable module\n        d = TimeVariable(\"_\").parse_exact_iso\n        times = (\n            [\"07.02.2022\", \"18.04.2021\"],  # date only\n            [\"07.02.2022 01:02:03\", \"18.04.2021 01:02:03\"],  # datetime\n            [\"010203\", \"010203\"],  # time\n            [\"02-07\", \"04-18\"],\n        )\n        formats = [\"25.11.2021\", \"25.11.2021 00:00:00\", \"000000\", \"11-25\"]\n        expected = [\n            [d(\"2022-02-07\"), d(\"2021-04-18\")],\n            [d(\"2022-02-07 01:02:03\"), d(\"2021-04-18 01:02:03\")],\n            [d(\"01:02:03\"), d(\"01:02:03\")],\n            [d(\"1900-02-07\"), d(\"1900-04-18\")],\n        ]\n        variables = [StringVariable(f\"s{i}\") for i in range(len(times))]\n        variables += [DiscreteVariable(f\"d{i}\", values=t) for i, t in enumerate(times)]\n        domain = Domain([], metas=variables)\n        metas = [t for t in times] + [list(range(len(x))) for x in times]\n        table = Table(domain, np.empty((len(times[0]), 0)), metas=np.array(metas).transpose())\n\n        tr = AsTime()\n        dtr = []\n        for v, f in zip(domain.metas, chain(formats, formats)):\n            strp = StrpTime(f, *TimeVariable.ADDITIONAL_FORMATS[f])\n            vtr = apply_transform_var(\n                apply_reinterpret(v, tr, table_column_data(table, v)), [strp]\n            )\n            dtr.append(vtr)\n\n        ttable = table.transform(Domain([], metas=dtr))\n        assert_array_equal(\n            ttable.metas,\n            np.array(list(chain(expected, expected)), dtype=float).transpose()\n        )\n\n    def test_reinterpret_string(self):\n        table = self.data_str\n        domain = table.domain\n        tvars = []\n        for v in domain.metas:\n            for i, tr in enumerate(\n                [AsContinuous(), AsCategorical(), AsTime(), AsString()]\n            ):\n                vtr = apply_reinterpret(v, tr, table_column_data(table, v)).renamed(\n                    f\"{v.name}_{i}\"\n                )\n                if isinstance(tr, AsTime):\n                    strp = StrpTime(\"Detect automatically\", None, 1, 1)\n                    vtr = apply_transform_var(vtr, [strp])\n                tvars.append(vtr)\n        tdomain = Domain([], metas=tvars)\n        ttable = table.transform(tdomain)\n        assert_array_nanequal(\n            ttable.metas,\n            np.array([\n                [0.1, 0., np.nan, \"0.1\", 2010., 0., 1262304000., \"2010\"],\n                [1.0, 1., np.nan, \"1.0\", 2020., 1., 1577836800., \"2020\"],\n            ], dtype=object)\n        )\n\n    def test_compound_transform(self):\n        table = self.data_str\n        domain = table.domain\n        v1 = domain.metas[0]\n        v1.attributes[\"a\"] = \"a\"\n        tv1 = apply_transform(v1, table, [AsContinuous(), Rename(\"Z1\")])\n        tv2 = apply_transform(v1, table, [AsContinuous(), Rename(\"Z2\"), Annotate(((\"a\", \"b\"),))])\n\n        self.assertIsInstance(tv1, ContinuousVariable)\n        self.assertEqual(tv1.name, \"Z1\")\n        self.assertEqual(tv1.attributes, {\"a\": \"a\"})\n\n        self.assertIsInstance(tv2, ContinuousVariable)\n        self.assertEqual(tv2.name, \"Z2\")\n        self.assertEqual(tv2.attributes, {\"a\": \"b\"})\n\n        tdomain = Domain([], metas=[tv1, tv2])\n        ttable = table.transform(tdomain)\n\n        assert_array_nanequal(\n            ttable.metas,\n            np.array([\n                [0.1, 0.1],\n                [1.0, 1.0],\n            ], dtype=object)\n        )\n\n    def test_null_transform(self):\n        table = self.data_str\n        domain = table.domain\n        v = apply_transform(domain.metas[0],table, [])\n        self.assertIs(v, domain.metas[0])\n\n\nclass TestUtils(TestCase):\n    def test_mapper(self):\n        mapper = make_dict_mapper({\"a\": 1, \"b\": 2})\n        r = mapper([\"a\", \"a\", \"b\"])\n        assert_array_equal(r, [1, 1, 2])\n        self.assertEqual(r.dtype, np.dtype(\"O\"))\n        r = mapper([\"a\", \"a\", \"b\"], dtype=float)\n        assert_array_equal(r, [1, 1, 2])\n        self.assertEqual(r.dtype, np.dtype(float))\n        r = mapper([\"a\", \"a\", \"b\"], dtype=int)\n        self.assertEqual(r.dtype, np.dtype(int))\n\n        mapper = make_dict_mapper({\"a\": 1, \"b\": 2}, dtype=int)\n        r = mapper([\"a\", \"a\", \"b\"])\n        self.assertEqual(r.dtype, np.dtype(int))\n\n        r = np.full(3, -1, dtype=float)\n        r_ = mapper([\"a\", \"a\", \"b\"], out=r)\n        self.assertIs(r, r_)\n        assert_array_equal(r, [1, 1, 2])\n\n    def test_as_float_or_nan(self):\n        a = np.array([\"a\", \"1.1\", \".2\", \"NaN\"], object)\n        r = as_float_or_nan(a)\n        assert_array_equal(r, [np.nan, 1.1, .2, np.nan])\n\n        a = np.array([1, 2, 3], dtype=int)\n        r = as_float_or_nan(a)\n        assert_array_equal(r, [1., 2., 3.])\n\n        r = as_float_or_nan(r, dtype=np.float32)\n        assert_array_equal(r, [1., 2., 3.])\n        self.assertEqual(r.dtype, np.dtype(np.float32))\n\n    def test_column_str_repr(self):\n        v = StringVariable(\"S\")\n        d = column_str_repr(v, np.array([\"A\", \"\", \"B\"]))\n        assert_array_equal(d, [\"A\", \"?\", \"B\"])\n        v = ContinuousVariable(\"C\")\n        d = column_str_repr(v, np.array([0.1, np.nan, 1.0]))\n        assert_array_equal(d, [\"0.1\", \"?\", \"1\"])\n        v = DiscreteVariable(\"D\", (\"a\", \"b\"))\n        d = column_str_repr(v, np.array([0., np.nan, 1.0]))\n        assert_array_equal(d, [\"a\", \"?\", \"b\"])\n        v = TimeVariable(\"T\", have_date=False, have_time=True)\n        d = column_str_repr(v, np.array([0., np.nan, 1.0]))\n        assert_array_equal(d, [\"00:00:00\", \"?\", \"00:00:01\"])\n\n\nclass TestLookupMappingTransform(TestCase):\n    def setUp(self) -> None:\n        self.lookup = LookupMappingTransform(\n            StringVariable(\"S\"),\n            {\"\": np.nan, \"a\": 0, \"b\": 1},\n            dtype=float,\n        )\n\n    def test_transform(self):\n        r = self.lookup.transform(np.array([\"\", \"a\", \"b\", \"c\"]))\n        assert_array_equal(r, [np.nan, 0, 1, np.nan])\n\n    def test_pickle(self):\n        lookup = self.lookup\n        lookup_ = pickle.loads(pickle.dumps(lookup))\n        c = np.array([\"\", \"a\", \"b\", \"c\"])\n        r = lookup.transform(c)\n        assert_array_equal(r, [np.nan, 0, 1, np.nan])\n        r_ = lookup_.transform(c)\n        assert_array_equal(r_, [np.nan, 0, 1, np.nan])\n\n    def test_equality(self):\n        v1 = DiscreteVariable(\"v1\", values=tuple(\"abc\"))\n        v2 = DiscreteVariable(\"v1\", values=tuple(\"abc\"))\n        v3 = DiscreteVariable(\"v3\", values=tuple(\"abc\"))\n\n        map1 = {\"a\": 2, \"b\": 0, \"c\": 1}\n        map2 = {\"a\": 2, \"b\": 0, \"c\": 1}\n        map3 = {\"a\": 2, \"b\": 0, \"c\": 1}\n\n        t1 = LookupMappingTransform(v1, map1, float)\n        t1a = LookupMappingTransform(v2, map2, float)\n        t2 = LookupMappingTransform(v3, map3, float)\n        self.assertEqual(t1, t1)\n        self.assertEqual(t1, t1a)\n        self.assertNotEqual(t1, t2)\n\n        self.assertEqual(hash(t1), hash(t1a))\n        self.assertNotEqual(hash(t1), hash(t2))\n\n        map1a = {\"a\": 2, \"b\": 1, \"c\": 0}\n        t1 = LookupMappingTransform(v1, map1, float)\n        t1a = LookupMappingTransform(v1, map1a, float)\n        self.assertNotEqual(t1, t1a)\n        self.assertNotEqual(hash(t1), hash(t1a))\n\n        map1a = {\"a\": 2, \"b\": 0, \"c\": 1}\n        t1 = LookupMappingTransform(v1, map1, float)\n        t1a = LookupMappingTransform(v1, map1a, float, unknown=2)\n        self.assertNotEqual(t1, t1a)\n        self.assertNotEqual(hash(t1), hash(t1a))\n\n        t1 = LookupMappingTransform(v1, map1, float)\n        t1a = LookupMappingTransform(v1, map1, int)\n        self.assertNotEqual(t1, t1a)\n        self.assertNotEqual(hash(t1), hash(t1a))\n\n\nclass TestGroupLessFrequentItemsDialog(GuiTest):\n    def setUp(self) -> None:\n        self.v = Categorical(\"C\", (\"a\", \"b\", \"c\"),\n                        ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        self.data = [0, 0, 0, 1, 1, 2]\n\n    def test_dialog_open(self):\n        dialog = GroupItemsDialog(self.v, self.data, [\"a\", \"b\"], {})\n        self.assertTrue(dialog.selected_radio.isChecked())\n        self.assertFalse(dialog.frequent_abs_radio.isChecked())\n        self.assertFalse(dialog.frequent_rel_radio.isChecked())\n        self.assertFalse(dialog.n_values_radio.isChecked())\n\n        dialog = GroupItemsDialog(self.v, self.data, [], {})\n        self.assertFalse(dialog.selected_radio.isChecked())\n        self.assertTrue(dialog.frequent_abs_radio.isChecked())\n        self.assertFalse(dialog.frequent_rel_radio.isChecked())\n        self.assertFalse(dialog.n_values_radio.isChecked())\n\n    def test_group_selected(self):\n        dialog = GroupItemsDialog(self.v, self.data, [\"a\", \"b\"], {})\n        dialog.selected_radio.setChecked(True)\n        dialog.new_name_line_edit.setText(\"BA\")\n\n        self.assertListEqual(dialog.get_merge_attributes(), [\"a\", \"b\"])\n        self.assertEqual(dialog.get_merged_value_name(), \"BA\")\n\n    def test_group_less_frequent_abs(self):\n        dialog = GroupItemsDialog(self.v, self.data, [\"a\", \"b\"], {})\n        dialog.frequent_abs_radio.setChecked(True)\n        dialog.frequent_abs_spin.setValue(3)\n        dialog.new_name_line_edit.setText(\"BA\")\n\n        self.assertListEqual(dialog.get_merge_attributes(), [\"b\", \"c\"])\n        self.assertEqual(dialog.get_merged_value_name(), \"BA\")\n\n        dialog.frequent_abs_spin.setValue(2)\n        self.assertListEqual(dialog.get_merge_attributes(), [\"c\"])\n\n        dialog.frequent_abs_spin.setValue(1)\n        self.assertListEqual(dialog.get_merge_attributes(), [])\n\n    def test_group_less_frequent_rel(self):\n        dialog = GroupItemsDialog(self.v, self.data, [\"a\", \"b\"], {})\n        dialog.frequent_rel_radio.setChecked(True)\n        dialog.frequent_rel_spin.setValue(50)\n        dialog.new_name_line_edit.setText(\"BA\")\n\n        self.assertListEqual(dialog.get_merge_attributes(), [\"b\", \"c\"])\n        self.assertEqual(dialog.get_merged_value_name(), \"BA\")\n\n        dialog.frequent_rel_spin.setValue(20)\n        self.assertListEqual(dialog.get_merge_attributes(), [\"c\"])\n\n        dialog.frequent_rel_spin.setValue(15)\n        self.assertListEqual(dialog.get_merge_attributes(), [])\n\n    def test_group_keep_n(self):\n        dialog = GroupItemsDialog(self.v, self.data, [\"a\", \"b\"], {})\n        dialog.n_values_radio.setChecked(True)\n        dialog.n_values_spin.setValue(1)\n        dialog.new_name_line_edit.setText(\"BA\")\n\n        self.assertListEqual(dialog.get_merge_attributes(), [\"b\", \"c\"])\n        self.assertEqual(dialog.get_merged_value_name(), \"BA\")\n\n        dialog.n_values_spin.setValue(2)\n        self.assertListEqual(dialog.get_merge_attributes(), [\"c\"])\n\n        dialog.n_values_spin.setValue(3)\n        self.assertListEqual(dialog.get_merge_attributes(), [])\n\n    def test_group_less_frequent_missing(self):\n        \"\"\"\n        Widget gives MaskedArray to GroupItemsDialog which can have missing\n        values.\n        gh-4599\n        \"\"\"\n        def _test_correctness():\n            dialog.frequent_abs_radio.setChecked(True)\n            dialog.frequent_abs_spin.setValue(3)\n            self.assertListEqual(dialog.get_merge_attributes(), [\"b\", \"c\"])\n\n            dialog.frequent_rel_radio.setChecked(True)\n            dialog.frequent_rel_spin.setValue(50)\n            self.assertListEqual(dialog.get_merge_attributes(), [\"b\", \"c\"])\n\n            dialog.n_values_radio.setChecked(True)\n            dialog.n_values_spin.setValue(1)\n            self.assertListEqual(dialog.get_merge_attributes(), [\"b\", \"c\"])\n\n        # masked array\n        data_masked = np.ma.array(\n            [0, 0, np.nan, 0, 1, 1, 2], mask=[0, 0, 1, 0, 0, 0, 0]\n        )\n        dialog = GroupItemsDialog(self.v, data_masked, [], {})\n        _test_correctness()\n\n        data_array = np.array([0, 0, np.nan, 0, 1, 1, 2])\n        dialog = GroupItemsDialog(self.v, data_array, [], {})\n        _test_correctness()\n\n        data_list = [0, 0, None, 0, 1, 1, 2]\n        dialog = GroupItemsDialog(self.v, data_list, [], {})\n        _test_correctness()\n\n\nif __name__ == '__main__':\n    unittest.main()\n",
            "new_file_content": "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=all\nimport pickle\nimport unittest\nfrom functools import partial\nfrom itertools import product, chain\nfrom unittest import TestCase\nfrom unittest.mock import Mock, patch\n\nimport numpy as np\nfrom numpy.testing import assert_array_equal\n\nfrom AnyQt.QtCore import QItemSelectionModel, Qt, QItemSelection, QPoint\nfrom AnyQt.QtGui import QPalette, QColor, QHelpEvent\nfrom AnyQt.QtWidgets import QAction, QComboBox, QLineEdit, \\\n    QStyleOptionViewItem, QDialog, QMenu, QToolTip, QListView\nfrom AnyQt.QtTest import QTest, QSignalSpy\n\nfrom orangewidget.tests.utils import simulate\n\nfrom Orange.data import (\n    ContinuousVariable, DiscreteVariable, StringVariable, TimeVariable,\n    Table, Domain\n)\nfrom Orange.preprocess.transformation import Identity, Lookup\nfrom Orange.widgets.data.oweditdomain import (\n    OWEditDomain,\n    ContinuousVariableEditor, DiscreteVariableEditor, VariableEditor,\n    TimeVariableEditor, Categorical, Real, Time, String,\n    Rename, Annotate, Unlink, CategoriesMapping, report_transform,\n    apply_transform, apply_transform_var, apply_reinterpret, MultiplicityRole,\n    AsString, AsCategorical, AsContinuous, AsTime,\n    table_column_data, ReinterpretVariableEditor, CategoricalVector,\n    VariableEditDelegate, TransformRole,\n    RealVector, TimeVector, StringVector, make_dict_mapper,\n    LookupMappingTransform, as_float_or_nan, column_str_repr,\n    GroupItemsDialog, VariableListModel, StrpTime\n)\nfrom Orange.widgets.data.owcolor import OWColor, ColorRole\nfrom Orange.widgets.tests.base import WidgetTest, GuiTest\nfrom Orange.widgets.tests.utils import contextMenu\nfrom Orange.widgets.utils import colorpalettes\nfrom Orange.tests import test_filename, assert_array_nanequal\n\nMArray = np.ma.MaskedArray\n\n\nclass TestReport(TestCase):\n    def test_rename(self):\n        var = Real(\"X\", (-1, \"\"), (), False)\n        tr = Rename(\"Y\")\n        val = report_transform(var, [tr])\n        self.assertIn(\"X\", val)\n        self.assertIn(\"Y\", val)\n\n    def test_annotate(self):\n        var = Real(\"X\", (-1, \"\"), ((\"a\", \"1\"), (\"b\", \"z\")), False)\n        tr = Annotate(((\"a\", \"2\"), (\"j\", \"z\")))\n        r = report_transform(var, [tr])\n        self.assertIn(\"a\", r)\n        self.assertIn(\"b\", r)\n\n    def test_unlinke(self):\n        var = Real(\"X\", (-1, \"\"), ((\"a\", \"1\"), (\"b\", \"z\")), True)\n        r = report_transform(var, [Unlink()])\n        self.assertIn(\"unlinked\", r)\n\n    def test_categories_mapping(self):\n        var = Categorical(\"C\", (\"a\", \"b\", \"c\"), (), False)\n        tr = CategoriesMapping(\n            ((\"a\", \"aa\"),\n             (\"b\", None),\n             (\"c\", \"cc\"),\n             (None, \"ee\")),\n        )\n        r = report_transform(var, [tr])\n        self.assertIn(\"a\", r)\n        self.assertIn(\"aa\", r)\n        self.assertIn(\"b\", r)\n        self.assertIn(\"<s>\", r)\n\n    def test_categorical_merge_mapping(self):\n        var = Categorical(\"C\", (\"a\", \"b1\", \"b2\"), (), False)\n        tr = CategoriesMapping(\n            ((\"a\", \"a\"),\n             (\"b1\", \"b\"),\n             (\"b2\", \"b\"),\n             (None, \"c\")),\n        )\n        r = report_transform(var, [tr])\n        self.assertIn('b', r)\n\n    def test_reinterpret(self):\n        var = String(\"T\", (), False)\n        for tr in (AsContinuous(), AsCategorical(), AsTime()):\n            t = report_transform(var, [tr])\n            self.assertIn(\"→ (\", t)\n\n\nclass TestOWEditDomain(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(OWEditDomain)\n        self.iris = Table(\"iris\")\n\n    def test_input_data(self):\n        \"\"\"Check widget's data with data on the input\"\"\"\n        self.assertEqual(self.widget.data, None)\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertEqual(self.widget.data, self.iris)\n\n    def test_input_data_disconnect(self):\n        \"\"\"Check widget's data after disconnecting data on the input\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertEqual(self.widget.data, self.iris)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(self.widget.data, None)\n\n    def test_widget_state(self):\n        \"\"\"Check if widget clears its state when the input is disconnected\"\"\"\n        editor = self.widget.findChild(ContinuousVariableEditor)\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertEqual(editor.name_edit.text(), \"sepal length\")\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(editor.name_edit.text(), \"\")\n        self.assertEqual(\n            self.widget.variables_model.index(0).data(Qt.EditRole), None)\n\n        editor = self.widget.findChild(DiscreteVariableEditor)\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        index = self.widget.domain_view.model().index(4)\n        self.widget.variables_view.setCurrentIndex(index)\n        self.assertEqual(editor.name_edit.text(), \"iris\")\n        self.assertEqual(editor.labels_model.get_dict(), {})\n        self.assertNotEqual(\n            self.widget.variables_model.index(0).data(Qt.EditRole), None)\n        model = editor.values_edit.selectionModel().model()\n        self.assertEqual(model.index(0).data(Qt.EditRole), \"Iris-setosa\")\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(editor.name_edit.text(), \"\")\n        self.assertEqual(editor.labels_model.get_dict(), {})\n        self.assertEqual(model.index(0).data(Qt.EditRole), None)\n        self.assertEqual(\n            self.widget.variables_model.index(0).data(Qt.EditRole), None)\n\n        editor = self.widget.findChild(TimeVariableEditor)\n        table = Table(test_filename(\"datasets/cyber-security-breaches.tab\"))\n        self.send_signal(self.widget.Inputs.data, table)\n        index = self.widget.domain_view.model().index(4)\n        self.widget.variables_view.setCurrentIndex(index)\n        self.assertEqual(editor.name_edit.text(), \"Date_Posted_or_Updated\")\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(editor.name_edit.text(), \"\")\n        self.assertEqual(\n            self.widget.variables_model.index(0).data(Qt.EditRole), None)\n\n        editor = self.widget.findChild(VariableEditor)\n        self.send_signal(self.widget.Inputs.data, table)\n        index = self.widget.domain_view.model().index(8)\n        self.widget.variables_view.setCurrentIndex(index)\n        self.assertEqual(editor.var.name, \"Business_Associate_Involved\")\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(editor.var, None)\n        self.assertEqual(\n            self.widget.variables_model.index(0).data(Qt.EditRole), None)\n\n    def test_output_data(self):\n        \"\"\"Check data on the output after apply\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        output = self.get_output(self.widget.Outputs.data)\n        np.testing.assert_array_equal(output.X, self.iris.X)\n        np.testing.assert_array_equal(output.Y, self.iris.Y)\n        self.assertEqual(output.domain, self.iris.domain)\n\n        self.widget.output_table_name = \"Iris 2\"\n        self.widget.commit()\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(output.name, \"Iris 2\")\n\n    def test_input_from_owcolor(self):\n        \"\"\"Check widget's data sent from OWColor widget\"\"\"\n        owcolor = self.create_widget(OWColor)\n        self.send_signal(\"Data\", self.iris, widget=owcolor)\n        disc_model = owcolor.disc_model\n        disc_model.setData(disc_model.index(0, 1), (1, 2, 3), ColorRole)\n        cont_model = owcolor.cont_model\n        palette = list(colorpalettes.ContinuousPalettes.values())[-1]\n        cont_model.setData(cont_model.index(1, 1), palette, ColorRole)\n        owcolor_output = self.get_output(\"Data\", owcolor)\n        self.send_signal(\"Data\", owcolor_output)\n        self.assertEqual(self.widget.data, owcolor_output)\n        np.testing.assert_equal(self.widget.data.domain.class_var.colors[0],\n                                (1, 2, 3))\n        self.assertIs(self.widget.data.domain.attributes[1].palette, palette)\n\n    def test_list_attributes_remain_lists(self):\n        a = ContinuousVariable(\"a\")\n        a.attributes[\"list\"] = [1, 2, 3]\n        d = Domain([a])\n        t = Table.from_domain(d)\n\n        self.send_signal(self.widget.Inputs.data, t)\n\n        assert isinstance(self.widget, OWEditDomain)\n        # select first variable\n        idx = self.widget.domain_view.model().index(0)\n        self.widget.domain_view.setCurrentIndex(idx)\n\n        # change first attribute value\n        editor = self.widget.findChild(ContinuousVariableEditor)\n        assert isinstance(editor, ContinuousVariableEditor)\n        idx = editor.labels_model.index(0, 1)\n        editor.labels_model.setData(idx, \"[1, 2, 4]\", Qt.EditRole)\n\n        self.widget.commit()\n        t2 = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(t2.domain[\"a\"].attributes[\"list\"], [1, 2, 4])\n\n    def test_annotation_bool(self):\n        \"\"\"Check if bool labels remain bool\"\"\"\n        a = ContinuousVariable(\"a\")\n        a.attributes[\"hidden\"] = True\n        d = Domain([a])\n        t = Table.from_domain(d)\n\n        self.send_signal(self.widget.Inputs.data, t)\n\n        assert isinstance(self.widget, OWEditDomain)\n        # select first variable\n        idx = self.widget.domain_view.model().index(0)\n        self.widget.domain_view.setCurrentIndex(idx)\n\n        # change first attribute value\n        editor = self.widget.findChild(ContinuousVariableEditor)\n        assert isinstance(editor, ContinuousVariableEditor)\n        idx = editor.labels_model.index(0, 1)\n        editor.labels_model.setData(idx, \"False\", Qt.EditRole)\n\n        self.widget.commit()\n        t2 = self.get_output(self.widget.Outputs.data)\n        self.assertFalse(t2.domain[\"a\"].attributes[\"hidden\"])\n\n    def test_duplicate_names(self):\n        \"\"\"\n        Tests if widget shows error when duplicate name is entered.\n        And tests if widget sends None data when error is shown.\n        GH-2143\n        GH-2146\n        \"\"\"\n        table = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertFalse(self.widget.Error.duplicate_var_name.is_shown())\n\n        idx = self.widget.domain_view.model().index(0)\n        self.widget.domain_view.setCurrentIndex(idx)\n        editor = self.widget.findChild(ContinuousVariableEditor)\n\n        def enter_text(widget, text):\n            # type: (QLineEdit, str) -> None\n            widget.selectAll()\n            QTest.keyClick(widget, Qt.Key_Delete)\n            QTest.keyClicks(widget, text)\n            QTest.keyClick(widget, Qt.Key_Return)\n\n        enter_text(editor.name_edit, \"iris\")\n        self.widget.commit()\n        self.assertTrue(self.widget.Error.duplicate_var_name.is_shown())\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertIsNone(output)\n\n        enter_text(editor.name_edit, \"sepal height\")\n        self.widget.commit()\n        self.assertFalse(self.widget.Error.duplicate_var_name.is_shown())\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertIsInstance(output, Table)\n\n    def test_unlink(self):\n        var0, var1, var2 = [ContinuousVariable(\"x\", compute_value=Mock()),\n                            ContinuousVariable(\"y\", compute_value=Mock()),\n                            ContinuousVariable(\"z\")]\n        domain = Domain([var0, var1, var2], None)\n        table = Table.from_numpy(domain, np.zeros((5, 3)), np.zeros((5, 0)))\n        self.send_signal(self.widget.Inputs.data, table)\n\n        index = self.widget.domain_view.model().index\n        for i in range(3):\n            self.widget.domain_view.setCurrentIndex(index(i))\n            editor = self.widget.findChild(ContinuousVariableEditor)\n            self.assertIs(editor.unlink_var_cb.isEnabled(), i < 2)\n            editor._set_unlink(i == 1)\n\n        self.widget.commit()\n        out = self.get_output(self.widget.Outputs.data)\n        out0, out1, out2 = out.domain.variables\n        self.assertIs(out0, domain[0])\n        self.assertIsNot(out1, domain[1])\n        self.assertIs(out2, domain[2])\n\n        self.assertIsNotNone(out0.compute_value)\n        self.assertIsNone(out1.compute_value)\n        self.assertIsNone(out2.compute_value)\n\n    def test_time_variable_preservation(self):\n        \"\"\"Test if time variables preserve format specific attributes\"\"\"\n        table = Table(test_filename(\"datasets/cyber-security-breaches.tab\"))\n        self.send_signal(self.widget.Inputs.data, table)\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(str(table[0, 4]), str(output[0, 4]))\n        view = self.widget.variables_view\n        view.setCurrentIndex(view.model().index(4))\n\n        editor = self.widget.findChild(TimeVariableEditor)\n        editor.name_edit.setText(\"Date\")\n        editor.variable_changed.emit()\n        self.widget.commit()\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(str(table[0, 4]), str(output[0, 4]))\n\n    def test_restore(self):\n        iris = self.iris\n        viris = (\n            \"Categorical\",\n            (\"iris\", (\"Iris-setosa\", \"Iris-versicolor\", \"Iris-virginica\"), (),\n             False)\n        )\n        w = self.widget\n\n        def restore(state):\n            w._domain_change_store = state\n            w._restore()\n\n        model = w.variables_model\n        self.send_signal(w.Inputs.data, iris, widget=w)\n        restore({viris: [(\"Rename\", (\"Z\",))]})\n        tr = model.data(model.index(4), TransformRole)\n        self.assertEqual(tr, [Rename(\"Z\")])\n\n        restore({viris: [(\"AsString\", ()), (\"Rename\", (\"Z\",))]})\n        tr = model.data(model.index(4), TransformRole)\n        self.assertEqual(tr, [AsString(), Rename(\"Z\")])\n\n\nclass TestEditors(GuiTest):\n    def test_variable_editor(self):\n        w = VariableEditor()\n        self.assertEqual(w.get_data(), (None, []))\n\n        v = String(\"S\", ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        w.set_data(v, [])\n\n        self.assertEqual(w.name_edit.text(), v.name)\n        self.assertEqual(w.labels_model.get_dict(),\n                         {\"A\": \"1\", \"B\": \"b\"})\n        self.assertEqual(w.get_data(), (v, []))\n\n        w.set_data(None)\n        self.assertEqual(w.name_edit.text(), \"\")\n        self.assertEqual(w.labels_model.get_dict(), {})\n        self.assertEqual(w.get_data(), (None, []))\n\n        w.set_data(v, [Rename(\"T\"), Annotate(((\"a\", \"1\"), (\"b\", \"2\")))])\n        self.assertEqual(w.name_edit.text(), \"T\")\n        self.assertEqual(w.labels_model.rowCount(), 2)\n        add = w.findChild(QAction, \"action-add-label\")\n        add.trigger()\n        remove = w.findChild(QAction, \"action-delete-label\")\n        remove.trigger()\n\n    def test_continuous_editor(self):\n        w = ContinuousVariableEditor()\n        self.assertEqual(w.get_data(), (None, []))\n\n        v = Real(\"X\", (-1, \"\"), ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        w.set_data(v, [])\n\n        self.assertEqual(w.name_edit.text(), v.name)\n        self.assertEqual(w.labels_model.get_dict(), dict(v.annotations))\n\n        w.set_data(None)\n        self.assertEqual(w.name_edit.text(), \"\")\n        self.assertEqual(w.labels_model.get_dict(), {})\n        self.assertEqual(w.get_data(), (None, []))\n\n    def test_discrete_editor(self):\n        w = DiscreteVariableEditor()\n        self.assertEqual(w.get_data(), (None, []))\n\n        v = Categorical(\"C\", (\"a\", \"b\", \"c\"), ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        values = [0, 0, 0, 1, 1, 2]\n        w.set_data_categorical(v, values)\n\n        self.assertEqual(w.name_edit.text(), v.name)\n        self.assertEqual(w.labels_model.get_dict(), dict(v.annotations))\n        self.assertEqual(w.get_data(), (v, []))\n        w.set_data_categorical(None, None)\n        self.assertEqual(w.name_edit.text(), \"\")\n        self.assertEqual(w.labels_model.get_dict(), {})\n        self.assertEqual(w.get_data(), (None, []))\n        mapping = [\n            (\"c\", \"C\"),\n            (\"a\", \"A\"),\n            (\"b\", None),\n            (None, \"b\")\n        ]\n        w.set_data_categorical(v, values, [CategoriesMapping(mapping)])\n        w.grab()  # run delegate paint method\n        self.assertEqual(w.get_data(), (v, [CategoriesMapping(mapping)]))\n\n        # test selection/deselection in the view\n        w.set_data_categorical(v, values)\n        view = w.values_edit\n        model = view.model()\n        assert model.rowCount()\n        sel_model = view.selectionModel()\n        model = sel_model.model()\n        sel_model.select(model.index(0, 0), QItemSelectionModel.Select)\n        sel_model.select(model.index(0, 0), QItemSelectionModel.Deselect)\n\n        # merge mapping\n        mapping = [\n            (\"a\", \"a\"),\n            (\"b\", \"b\"),\n            (\"c\", \"b\")\n        ]\n        w.set_data_categorical(v, values, [CategoriesMapping(mapping)])\n        self.assertEqual(w.get_data()[1], [CategoriesMapping(mapping)])\n        self.assertEqual(model.data(model.index(0, 0), MultiplicityRole), 1)\n        self.assertEqual(model.data(model.index(1, 0), MultiplicityRole), 2)\n        self.assertEqual(model.data(model.index(2, 0), MultiplicityRole), 2)\n        w.grab()\n        model.setData(model.index(0, 0), \"b\", Qt.EditRole)\n        self.assertEqual(model.data(model.index(0, 0), MultiplicityRole), 3)\n        self.assertEqual(model.data(model.index(1, 0), MultiplicityRole), 3)\n        self.assertEqual(model.data(model.index(2, 0), MultiplicityRole), 3)\n        w.grab()\n\n    def test_discrete_editor_add_remove_action(self):\n        w = DiscreteVariableEditor()\n        v = Categorical(\"C\", (\"a\", \"b\", \"c\"),\n                        ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        values = [0, 0, 0, 1, 1, 2]\n        w.set_data_categorical(v, values)\n        action_add = w.add_new_item\n        action_remove = w.remove_item\n        view = w.values_edit\n        model, selection = view.model(), view.selectionModel()\n        selection.clear()\n\n        action_add.trigger()\n        self.assertTrue(view.state() == view.EditingState)\n        editor = view.focusWidget()\n        assert isinstance(editor, QLineEdit)\n        spy = QSignalSpy(model.dataChanged)\n        QTest.keyClick(editor, Qt.Key_D)\n        QTest.keyClick(editor, Qt.Key_Return)\n        self.assertTrue(model.rowCount() == 4)\n        # The commit to model is executed via a queued invoke\n        self.assertTrue(bool(spy) or spy.wait())\n        self.assertEqual(model.index(3, 0).data(Qt.EditRole), \"d\")\n        # remove it\n        spy = QSignalSpy(model.rowsRemoved)\n        action_remove.trigger()\n        self.assertEqual(model.rowCount(), 3)\n        self.assertEqual(len(spy), 1)\n        _, first, last = spy[0]\n        self.assertEqual((first, last), (3, 3))\n        # remove/drop and existing value\n        selection.select(model.index(1, 0), QItemSelectionModel.ClearAndSelect)\n        removespy = QSignalSpy(model.rowsRemoved)\n        changedspy = QSignalSpy(model.dataChanged)\n        action_remove.trigger()\n        self.assertEqual(len(removespy), 0, \"Should only mark item as removed\")\n        self.assertGreaterEqual(len(changedspy), 1, \"Did not change data\")\n        w.grab()\n\n    # mocking exec make dialog never show - dialog blocks running until closed\n    @patch(\n        \"Orange.widgets.data.oweditdomain.GroupItemsDialog.exec\",\n        Mock(side_effect=lambda: QDialog.Accepted)\n    )\n    def test_discrete_editor_merge_action(self):\n        \"\"\"\n        This function check whether results of dialog have effect on\n        merging the attributes. The dialog itself is tested separately.\n        \"\"\"\n        w = DiscreteVariableEditor()\n        v = Categorical(\"C\", (\"a\", \"b\", \"c\"),\n                        ((\"A\", \"1\"), (\"B\", \"b\")), False)\n\n        w.set_data_categorical(\n            v, [0, 0, 0, 1, 1, 2],\n            [CategoriesMapping([\n                (\"a\", \"AA\"), (\"b\", \"BB\"), (\"c\", \"CC\"),\n            ])]\n        )\n\n        view = w.values_edit\n        model = view.model()\n        selmodel = view.selectionModel()  # type: QItemSelectionModel\n        selmodel.select(\n            QItemSelection(model.index(0, 0), model.index(1, 0)),\n            QItemSelectionModel.ClearAndSelect\n        )\n        spy = QSignalSpy(w.variable_changed)\n        w.merge_items.trigger()\n\n        self.assertEqual(model.index(0, 0).data(Qt.EditRole), \"other\")\n        self.assertEqual(model.index(1, 0).data(Qt.EditRole), \"other\")\n        self.assertEqual(model.index(2, 0).data(Qt.EditRole), \"CC\")\n\n        self.assertSequenceEqual(\n            list(spy), [[]], 'variable_changed should emit exactly once'\n        )\n\n    def test_discrete_editor_rename_selected_items_action(self):\n        w = DiscreteVariableEditor()\n        v = Categorical(\"C\", (\"a\", \"b\", \"c\"),\n                        ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        w.set_data_categorical(v, [])\n        action = w.rename_selected_items\n        view = w.values_edit\n        model = view.model()\n        selmodel = view.selectionModel()  # type: QItemSelectionModel\n        selmodel.select(\n            QItemSelection(model.index(0, 0), model.index(1, 0)),\n            QItemSelectionModel.ClearAndSelect\n        )\n        # trigger the action, then find the active popup, and simulate entry\n        spy = QSignalSpy(w.variable_changed)\n        with patch.object(QComboBox, \"setVisible\", return_value=None) as m:\n            action.trigger()\n            m.assert_called()\n        cb = view.findChild(QComboBox)\n        cb.setCurrentText(\"BA\")\n        view.commitData(cb)\n        self.assertEqual(model.index(0, 0).data(Qt.EditRole), \"BA\")\n        self.assertEqual(model.index(1, 0).data(Qt.EditRole), \"BA\")\n        self.assertSequenceEqual(\n            list(spy), [[]], 'variable_changed should emit exactly once'\n        )\n\n    def test_discrete_editor_context_menu(self):\n        w = DiscreteVariableEditor()\n        v = Categorical(\"C\", (\"a\", \"b\", \"c\"),\n                        ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        w.set_data_categorical(v, [])\n        view = w.values_edit\n        model = view.model()\n\n        pos = view.visualRect(model.index(0, 0)).center()\n        with patch.object(QMenu, \"setVisible\", return_value=None) as m:\n            contextMenu(view.viewport(), pos)\n            m.assert_called()\n\n        menu = view.findChild(QMenu)\n        self.assertIsNotNone(menu)\n        menu.close()\n\n    def test_time_editor(self):\n        w = TimeVariableEditor()\n        self.assertEqual(w.get_data(), (None, []))\n\n        v = Time(\"T\", ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        w.set_data(v,)\n\n        self.assertEqual(w.name_edit.text(), v.name)\n        self.assertEqual(w.labels_model.get_dict(), dict(v.annotations))\n\n        w.set_data(None)\n        self.assertEqual(w.name_edit.text(), \"\")\n        self.assertEqual(w.labels_model.get_dict(), {})\n        self.assertEqual(w.get_data(), (None, []))\n\n    DataVectors = [\n        CategoricalVector(\n            Categorical(\"A\", (\"a\", \"aa\"), (), False), lambda:\n                MArray([0, 1, 2], mask=[False, False, True])\n        ),\n        RealVector(\n            Real(\"B\", (6, \"f\"), (), False), lambda:\n                MArray([0.1, 0.2, 0.3], mask=[True, False, True])\n        ),\n        TimeVector(\n            Time(\"T\", (), False), lambda:\n                MArray([0, 100, 200], dtype=\"M8[us]\", mask=[True, False, True])\n        ),\n        StringVector(\n            String(\"S\", (), False), lambda:\n                MArray([\"0\", \"1\", \"2\"], dtype=object, mask=[True, False, True])\n        ),\n    ]\n    ReinterpretTransforms = {\n        Categorical: [AsCategorical], Real: [AsContinuous],\n        Time: [AsTime, partial(StrpTime, 'Detect automatically', None, 1, 1)],\n        String: [AsString]\n    }\n\n    def test_reinterpret_editor(self):\n        w = ReinterpretVariableEditor()\n        self.assertEqual(w.get_data(), (None, []))\n        data = self.DataVectors[0]\n        w.set_data(data, )\n        self.assertEqual(w.get_data(), (data.vtype, []))\n        w.set_data(data, [Rename(\"Z\")])\n        self.assertEqual(w.get_data(), (data.vtype, [Rename(\"Z\")]))\n\n        for vec, tr in product(self.DataVectors, self.ReinterpretTransforms.values()):\n            w.set_data(vec, [t() for t in tr])\n            v, tr_ = w.get_data()\n            self.assertEqual(v, vec.vtype)\n            if not tr_:\n                self.assertEqual(tr, self.ReinterpretTransforms[type(v)])\n            else:\n                self.assertListEqual(tr_, [t() for t in tr])\n\n    def test_reinterpret_editor_simulate(self):\n        w = ReinterpretVariableEditor()\n        tc = w.findChild(QComboBox, name=\"type-combo\")\n\n        def cb():\n            var, tr = w.get_data()\n            type_ = tc.currentData()\n            if type_ is not type(var):\n                self.assertEqual(\n                    tr, [t() for t in self.ReinterpretTransforms[type_]] + [Rename(\"Z\")]\n                )\n            else:\n                self.assertEqual(tr, [Rename(\"Z\")])\n\n        for vec in self.DataVectors:\n            w.set_data(vec, [Rename(\"Z\")])\n            simulate.combobox_run_through_all(tc, callback=cb)\n\n    def test_unlink(self):\n        w = ContinuousVariableEditor()\n        cbox = w.unlink_var_cb\n        self.assertEqual(w.get_data(), (None, []))\n\n        v = Real(\"X\", (-1, \"\"), ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        w.set_data(v, [])\n        self.assertFalse(cbox.isEnabled())\n\n        v = Real(\"X\", (-1, \"\"), ((\"A\", \"1\"), (\"B\", \"b\")), True)\n        w.set_data(v, [Unlink()])\n        self.assertTrue(cbox.isEnabled())\n        self.assertTrue(cbox.isChecked())\n\n        v = Real(\"X\", (-1, \"\"), ((\"A\", \"1\"), (\"B\", \"b\")), True)\n        w.set_data(v, [])\n        self.assertTrue(cbox.isEnabled())\n        self.assertFalse(cbox.isChecked())\n\n        cbox.setChecked(True)\n        self.assertEqual(w.get_data()[1], [Unlink()])\n\n        w.set_data(v, [Unlink()])\n        self.assertTrue(cbox.isChecked())\n\n        cbox.setChecked(False)\n        self.assertEqual(w.get_data()[1], [])\n\n        cbox.setChecked(True)\n        w.clear()\n        self.assertFalse(cbox.isChecked())\n        self.assertEqual(w.get_data()[1], [])\n\n        w._set_unlink(True)\n        self.assertTrue(cbox.isChecked())\n        w._set_unlink(False)\n        self.assertFalse(cbox.isChecked())\n\n\nclass TestModels(GuiTest):\n    def test_variable_model(self):\n        model = VariableListModel()\n        self.assertEqual(model.effective_name(model.index(-1, -1)), None)\n\n        def data(row, role):\n            return model.data(model.index(row,), role)\n\n        def set_data(row, data, role):\n            model.setData(model.index(row), data, role)\n\n        model[:] = [\n            RealVector(Real(\"A\", (3, \"g\"), (), False), lambda: MArray([])),\n            RealVector(Real(\"B\", (3, \"g\"), (), False), lambda: MArray([])),\n        ]\n        self.assertEqual(data(0, Qt.DisplayRole), \"A\")\n        self.assertEqual(data(1, Qt.DisplayRole), \"B\")\n        self.assertEqual(model.effective_name(model.index(1)), \"B\")\n        set_data(1, [Rename(\"A\")], TransformRole)\n        self.assertEqual(model.effective_name(model.index(1)), \"A\")\n        self.assertEqual(data(0, MultiplicityRole), 2)\n        self.assertEqual(data(1, MultiplicityRole), 2)\n        set_data(1, [], TransformRole)\n        self.assertEqual(data(0, MultiplicityRole), 1)\n        self.assertEqual(data(1, MultiplicityRole), 1)\n\n\nclass TestDelegates(GuiTest):\n    def test_delegate(self):\n        model = VariableListModel([None, None])\n\n        def set_item(row: int, v: dict):\n            model.setItemData(model.index(row),  v)\n\n        def get_style_option(row: int) -> QStyleOptionViewItem:\n            opt = QStyleOptionViewItem()\n            delegate.initStyleOption(opt, model.index(row))\n            return opt\n\n        set_item(0, {Qt.EditRole: Categorical(\"a\", (), (), False)})\n        delegate = VariableEditDelegate()\n        opt = get_style_option(0)\n        self.assertEqual(opt.text, \"a\")\n        self.assertFalse(opt.font.italic())\n        set_item(0, {TransformRole: [Rename(\"b\")]})\n        opt = get_style_option(0)\n        self.assertEqual(opt.text, \"a \\N{RIGHTWARDS ARROW} b\")\n        self.assertTrue(opt.font.italic())\n\n        set_item(0, {TransformRole: [AsString()]})\n        opt = get_style_option(0)\n        self.assertIn(\"reinterpreted\", opt.text)\n        self.assertTrue(opt.font.italic())\n        set_item(1, {\n            Qt.EditRole: String(\"b\", (), False),\n            TransformRole: [Rename(\"a\")]\n        })\n        opt = get_style_option(1)\n        self.assertEqual(opt.palette.color(QPalette.Text), QColor(Qt.red))\n        view = QListView()\n        with patch.object(QToolTip, \"showText\") as p:\n            delegate.helpEvent(\n                QHelpEvent(QHelpEvent.ToolTip, QPoint(0, 0), QPoint(0, 0)),\n                view, opt, model.index(1),\n            )\n            p.assert_called_once()\n\n\nclass TestTransforms(TestCase):\n    def _test_common(self, var):\n        tr = [Rename(var.name + \"_copy\"), Annotate(((\"A\", \"1\"),))]\n        XX = apply_transform_var(var, tr)\n        self.assertEqual(XX.name, var.name + \"_copy\")\n        self.assertEqual(XX.attributes, {\"A\": 1})\n        self.assertIsInstance(XX.compute_value, Identity)\n        self.assertIs(XX.compute_value.variable, var)\n\n    def test_continous(self):\n        X = ContinuousVariable(\"X\")\n        self._test_common(X)\n\n    def test_string(self):\n        X = StringVariable(\"S\")\n        self._test_common(X)\n\n    def test_time(self):\n        X = TimeVariable(\"X\")\n        self._test_common(X)\n\n    def test_discrete(self):\n        D = DiscreteVariable(\"D\", values=(\"a\", \"b\"))\n        self._test_common(D)\n\n    def test_discrete_rename(self):\n        D = DiscreteVariable(\"D\", values=(\"a\", \"b\"))\n        DD = apply_transform_var(D, [CategoriesMapping(((\"a\", \"A\"), (\"b\", \"B\")))])\n        self.assertSequenceEqual(DD.values, [\"A\", \"B\"])\n        self.assertIs(DD.compute_value.variable, D)\n\n    def test_discrete_reorder(self):\n        D = DiscreteVariable(\"D\", values=(\"2\", \"3\", \"1\", \"0\"))\n        DD = apply_transform_var(D, [CategoriesMapping(((\"0\", \"0\"), (\"1\", \"1\"),\n                                                    (\"2\", \"2\"), (\"3\", \"3\")))])\n        self.assertSequenceEqual(DD.values, [\"0\", \"1\", \"2\", \"3\"])\n        self._assertLookupEquals(\n            DD.compute_value, Lookup(D, np.array([2, 3, 1, 0]))\n        )\n\n    def test_discrete_add_drop(self):\n        D = DiscreteVariable(\"D\", values=(\"2\", \"3\", \"1\", \"0\"))\n        mapping = (\n            (\"0\", None),\n            (\"1\", \"1\"),\n            (\"2\", \"2\"),\n            (\"3\", None),\n            (None, \"A\"),\n        )\n        tr = [CategoriesMapping(mapping)]\n        DD = apply_transform_var(D, tr)\n        self.assertSequenceEqual(DD.values, [\"1\", \"2\", \"A\"])\n        self._assertLookupEquals(\n            DD.compute_value, Lookup(D, np.array([1, np.nan, 0, np.nan]))\n        )\n\n    def test_discrete_merge(self):\n        D = DiscreteVariable(\"D\", values=(\"2\", \"3\", \"1\", \"0\"))\n        mapping = (\n            (\"0\", \"x\"),\n            (\"1\", \"y\"),\n            (\"2\", \"x\"),\n            (\"3\", \"y\"),\n        )\n        tr = [CategoriesMapping(mapping)]\n        DD = apply_transform_var(D, tr)\n        self.assertSequenceEqual(DD.values, [\"x\", \"y\"])\n        self._assertLookupEquals(\n            DD.compute_value, Lookup(D, np.array([0, 1, 1, 0]))\n        )\n\n    def _assertLookupEquals(self, first, second):\n        self.assertIsInstance(first, Lookup)\n        self.assertIsInstance(second, Lookup)\n        self.assertIs(first.variable, second.variable)\n        assert_array_equal(first.lookup_table, second.lookup_table)\n\n\nclass TestReinterpretTransforms(TestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        super().setUpClass()\n        domain = Domain([\n            DiscreteVariable(\"A\", values=(\"a\", \"b\", \"c\")),\n            DiscreteVariable(\"B\", values=(\"0\", \"1\", \"2\")),\n            ContinuousVariable(\"C\"),\n            TimeVariable(\"D\", have_time=True),\n        ],\n        metas=[\n            StringVariable(\"S\")\n        ])\n        cls.data = Table.from_list(\n            domain, [\n                [0, 2, 0.25, 180],\n                [1, 1, 1.25, 360],\n                [2, 0, 0.20, 720],\n                [1, 0, 0.00, 000],\n            ]\n        )\n        cls.data_str = Table.from_list(\n            Domain([], [], metas=[\n                StringVariable(\"S\"),\n                StringVariable(\"T\")\n            ]),\n            [[\"0.1\", \"2010\"],\n             [\"1.0\", \"2020\"]]\n        )\n\n    def test_as_string(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsString()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        ttable = table.transform(Domain([], [], dtr))\n        assert_array_equal(\n            ttable.metas,\n            np.array([\n                [\"a\", \"2\", \"0.25\", \"00:03:00\"],\n                [\"b\", \"1\", \"1.25\", \"00:06:00\"],\n                [\"c\", \"0\", \"0.2\", \"00:12:00\"],\n                [\"b\", \"0\", \"0.0\", \"00:00:00\"],\n            ], dtype=object)\n        )\n\n    def test_as_discrete(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsCategorical()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        tdomain = Domain(dtr)\n        ttable = table.transform(tdomain)\n        assert_array_equal(\n            ttable.X,\n            np.array([\n                [0, 2, 2, 1],\n                [1, 1, 3, 2],\n                [2, 0, 1, 3],\n                [1, 0, 0, 0],\n            ], dtype=float)\n        )\n        self.assertEqual(tdomain[\"A\"].values, (\"a\", \"b\", \"c\"))\n        self.assertEqual(tdomain[\"B\"].values, (\"0\", \"1\", \"2\"))\n        self.assertEqual(tdomain[\"C\"].values, (\"0.0\", \"0.2\", \"0.25\", \"1.25\"))\n        self.assertEqual(\n            tdomain[\"D\"].values,\n            (\"1970-01-01 00:00:00\", \"1970-01-01 00:03:00\",\n             \"1970-01-01 00:06:00\", \"1970-01-01 00:12:00\")\n        )\n\n    def test_as_continuous(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsContinuous()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        ttable = table.transform(Domain(dtr))\n        assert_array_equal(\n            ttable.X,\n            np.array([\n                [np.nan, 2, 0.25, 180],\n                [np.nan, 1, 1.25, 360],\n                [np.nan, 0, 0.20, 720],\n                [np.nan, 0, 0.00, 000],\n            ], dtype=float)\n        )\n\n    def test_as_time(self):\n        # this test only test type of format that can be string, continuous and discrete\n        # correctness of time formats is already tested in TimeVariable module\n        d = TimeVariable(\"_\").parse_exact_iso\n        times = (\n            [\"07.02.2022\", \"18.04.2021\"],  # date only\n            [\"07.02.2022 01:02:03\", \"18.04.2021 01:02:03\"],  # datetime\n            # datetime with timezone\n            [\"2021-02-08 01:02:03+01:00\", \"2021-02-07 01:02:03+01:00\"],\n            [\"010203\", \"010203\"],  # time\n            [\"02-07\", \"04-18\"],\n        )\n        formats = [\n            \"25.11.2021\", \"25.11.2021 00:00:00\", \"2021-11-25 00:00:00\", \"000000\", \"11-25\"\n        ]\n        expected = [\n            [d(\"2022-02-07\"), d(\"2021-04-18\")],\n            [d(\"2022-02-07 01:02:03\"), d(\"2021-04-18 01:02:03\")],\n            [d(\"2021-02-08 01:02:03+0100\"), d(\"2021-02-07 01:02:03+0100\")],\n            [d(\"01:02:03\"), d(\"01:02:03\")],\n            [d(\"1900-02-07\"), d(\"1900-04-18\")],\n        ]\n        variables = [StringVariable(f\"s{i}\") for i in range(len(times))]\n        variables += [DiscreteVariable(f\"d{i}\", values=t) for i, t in enumerate(times)]\n        domain = Domain([], metas=variables)\n        metas = [t for t in times] + [list(range(len(x))) for x in times]\n        table = Table(domain, np.empty((len(times[0]), 0)), metas=np.array(metas).transpose())\n\n        tr = AsTime()\n        dtr = []\n        for v, f in zip(domain.metas, chain(formats, formats)):\n            strp = StrpTime(f, *TimeVariable.ADDITIONAL_FORMATS[f])\n            vtr = apply_transform_var(\n                apply_reinterpret(v, tr, table_column_data(table, v)), [strp]\n            )\n            dtr.append(vtr)\n\n        ttable = table.transform(Domain([], metas=dtr))\n        assert_array_equal(\n            ttable.metas,\n            np.array(list(chain(expected, expected)), dtype=float).transpose()\n        )\n\n    def test_raise_pandas_version(self):\n        \"\"\"\n        When this test start to fail:\n        - remove this test\n        - remove if clause in datetime_to_epoch function and supporting comments\n        - set pandas dependency version to pandas>=1.4\n        \"\"\"\n        from datetime import datetime\n        self.assertLess(datetime.today(), datetime(2023, 1, 1))\n\n    def test_reinterpret_string(self):\n        table = self.data_str\n        domain = table.domain\n        tvars = []\n        for v in domain.metas:\n            for i, tr in enumerate(\n                [AsContinuous(), AsCategorical(), AsTime(), AsString()]\n            ):\n                vtr = apply_reinterpret(v, tr, table_column_data(table, v)).renamed(\n                    f\"{v.name}_{i}\"\n                )\n                if isinstance(tr, AsTime):\n                    strp = StrpTime(\"Detect automatically\", None, 1, 1)\n                    vtr = apply_transform_var(vtr, [strp])\n                tvars.append(vtr)\n        tdomain = Domain([], metas=tvars)\n        ttable = table.transform(tdomain)\n        assert_array_nanequal(\n            ttable.metas,\n            np.array([\n                [0.1, 0., np.nan, \"0.1\", 2010., 0., 1262304000., \"2010\"],\n                [1.0, 1., np.nan, \"1.0\", 2020., 1., 1577836800., \"2020\"],\n            ], dtype=object)\n        )\n\n    def test_compound_transform(self):\n        table = self.data_str\n        domain = table.domain\n        v1 = domain.metas[0]\n        v1.attributes[\"a\"] = \"a\"\n        tv1 = apply_transform(v1, table, [AsContinuous(), Rename(\"Z1\")])\n        tv2 = apply_transform(v1, table, [AsContinuous(), Rename(\"Z2\"), Annotate(((\"a\", \"b\"),))])\n\n        self.assertIsInstance(tv1, ContinuousVariable)\n        self.assertEqual(tv1.name, \"Z1\")\n        self.assertEqual(tv1.attributes, {\"a\": \"a\"})\n\n        self.assertIsInstance(tv2, ContinuousVariable)\n        self.assertEqual(tv2.name, \"Z2\")\n        self.assertEqual(tv2.attributes, {\"a\": \"b\"})\n\n        tdomain = Domain([], metas=[tv1, tv2])\n        ttable = table.transform(tdomain)\n\n        assert_array_nanequal(\n            ttable.metas,\n            np.array([\n                [0.1, 0.1],\n                [1.0, 1.0],\n            ], dtype=object)\n        )\n\n    def test_null_transform(self):\n        table = self.data_str\n        domain = table.domain\n        v = apply_transform(domain.metas[0],table, [])\n        self.assertIs(v, domain.metas[0])\n\n\nclass TestUtils(TestCase):\n    def test_mapper(self):\n        mapper = make_dict_mapper({\"a\": 1, \"b\": 2})\n        r = mapper([\"a\", \"a\", \"b\"])\n        assert_array_equal(r, [1, 1, 2])\n        self.assertEqual(r.dtype, np.dtype(\"O\"))\n        r = mapper([\"a\", \"a\", \"b\"], dtype=float)\n        assert_array_equal(r, [1, 1, 2])\n        self.assertEqual(r.dtype, np.dtype(float))\n        r = mapper([\"a\", \"a\", \"b\"], dtype=int)\n        self.assertEqual(r.dtype, np.dtype(int))\n\n        mapper = make_dict_mapper({\"a\": 1, \"b\": 2}, dtype=int)\n        r = mapper([\"a\", \"a\", \"b\"])\n        self.assertEqual(r.dtype, np.dtype(int))\n\n        r = np.full(3, -1, dtype=float)\n        r_ = mapper([\"a\", \"a\", \"b\"], out=r)\n        self.assertIs(r, r_)\n        assert_array_equal(r, [1, 1, 2])\n\n    def test_as_float_or_nan(self):\n        a = np.array([\"a\", \"1.1\", \".2\", \"NaN\"], object)\n        r = as_float_or_nan(a)\n        assert_array_equal(r, [np.nan, 1.1, .2, np.nan])\n\n        a = np.array([1, 2, 3], dtype=int)\n        r = as_float_or_nan(a)\n        assert_array_equal(r, [1., 2., 3.])\n\n        r = as_float_or_nan(r, dtype=np.float32)\n        assert_array_equal(r, [1., 2., 3.])\n        self.assertEqual(r.dtype, np.dtype(np.float32))\n\n    def test_column_str_repr(self):\n        v = StringVariable(\"S\")\n        d = column_str_repr(v, np.array([\"A\", \"\", \"B\"]))\n        assert_array_equal(d, [\"A\", \"?\", \"B\"])\n        v = ContinuousVariable(\"C\")\n        d = column_str_repr(v, np.array([0.1, np.nan, 1.0]))\n        assert_array_equal(d, [\"0.1\", \"?\", \"1\"])\n        v = DiscreteVariable(\"D\", (\"a\", \"b\"))\n        d = column_str_repr(v, np.array([0., np.nan, 1.0]))\n        assert_array_equal(d, [\"a\", \"?\", \"b\"])\n        v = TimeVariable(\"T\", have_date=False, have_time=True)\n        d = column_str_repr(v, np.array([0., np.nan, 1.0]))\n        assert_array_equal(d, [\"00:00:00\", \"?\", \"00:00:01\"])\n\n\nclass TestLookupMappingTransform(TestCase):\n    def setUp(self) -> None:\n        self.lookup = LookupMappingTransform(\n            StringVariable(\"S\"),\n            {\"\": np.nan, \"a\": 0, \"b\": 1},\n            dtype=float,\n        )\n\n    def test_transform(self):\n        r = self.lookup.transform(np.array([\"\", \"a\", \"b\", \"c\"]))\n        assert_array_equal(r, [np.nan, 0, 1, np.nan])\n\n    def test_pickle(self):\n        lookup = self.lookup\n        lookup_ = pickle.loads(pickle.dumps(lookup))\n        c = np.array([\"\", \"a\", \"b\", \"c\"])\n        r = lookup.transform(c)\n        assert_array_equal(r, [np.nan, 0, 1, np.nan])\n        r_ = lookup_.transform(c)\n        assert_array_equal(r_, [np.nan, 0, 1, np.nan])\n\n    def test_equality(self):\n        v1 = DiscreteVariable(\"v1\", values=tuple(\"abc\"))\n        v2 = DiscreteVariable(\"v1\", values=tuple(\"abc\"))\n        v3 = DiscreteVariable(\"v3\", values=tuple(\"abc\"))\n\n        map1 = {\"a\": 2, \"b\": 0, \"c\": 1}\n        map2 = {\"a\": 2, \"b\": 0, \"c\": 1}\n        map3 = {\"a\": 2, \"b\": 0, \"c\": 1}\n\n        t1 = LookupMappingTransform(v1, map1, float)\n        t1a = LookupMappingTransform(v2, map2, float)\n        t2 = LookupMappingTransform(v3, map3, float)\n        self.assertEqual(t1, t1)\n        self.assertEqual(t1, t1a)\n        self.assertNotEqual(t1, t2)\n\n        self.assertEqual(hash(t1), hash(t1a))\n        self.assertNotEqual(hash(t1), hash(t2))\n\n        map1a = {\"a\": 2, \"b\": 1, \"c\": 0}\n        t1 = LookupMappingTransform(v1, map1, float)\n        t1a = LookupMappingTransform(v1, map1a, float)\n        self.assertNotEqual(t1, t1a)\n        self.assertNotEqual(hash(t1), hash(t1a))\n\n        map1a = {\"a\": 2, \"b\": 0, \"c\": 1}\n        t1 = LookupMappingTransform(v1, map1, float)\n        t1a = LookupMappingTransform(v1, map1a, float, unknown=2)\n        self.assertNotEqual(t1, t1a)\n        self.assertNotEqual(hash(t1), hash(t1a))\n\n        t1 = LookupMappingTransform(v1, map1, float)\n        t1a = LookupMappingTransform(v1, map1, int)\n        self.assertNotEqual(t1, t1a)\n        self.assertNotEqual(hash(t1), hash(t1a))\n\n\nclass TestGroupLessFrequentItemsDialog(GuiTest):\n    def setUp(self) -> None:\n        self.v = Categorical(\"C\", (\"a\", \"b\", \"c\"),\n                        ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        self.data = [0, 0, 0, 1, 1, 2]\n\n    def test_dialog_open(self):\n        dialog = GroupItemsDialog(self.v, self.data, [\"a\", \"b\"], {})\n        self.assertTrue(dialog.selected_radio.isChecked())\n        self.assertFalse(dialog.frequent_abs_radio.isChecked())\n        self.assertFalse(dialog.frequent_rel_radio.isChecked())\n        self.assertFalse(dialog.n_values_radio.isChecked())\n\n        dialog = GroupItemsDialog(self.v, self.data, [], {})\n        self.assertFalse(dialog.selected_radio.isChecked())\n        self.assertTrue(dialog.frequent_abs_radio.isChecked())\n        self.assertFalse(dialog.frequent_rel_radio.isChecked())\n        self.assertFalse(dialog.n_values_radio.isChecked())\n\n    def test_group_selected(self):\n        dialog = GroupItemsDialog(self.v, self.data, [\"a\", \"b\"], {})\n        dialog.selected_radio.setChecked(True)\n        dialog.new_name_line_edit.setText(\"BA\")\n\n        self.assertListEqual(dialog.get_merge_attributes(), [\"a\", \"b\"])\n        self.assertEqual(dialog.get_merged_value_name(), \"BA\")\n\n    def test_group_less_frequent_abs(self):\n        dialog = GroupItemsDialog(self.v, self.data, [\"a\", \"b\"], {})\n        dialog.frequent_abs_radio.setChecked(True)\n        dialog.frequent_abs_spin.setValue(3)\n        dialog.new_name_line_edit.setText(\"BA\")\n\n        self.assertListEqual(dialog.get_merge_attributes(), [\"b\", \"c\"])\n        self.assertEqual(dialog.get_merged_value_name(), \"BA\")\n\n        dialog.frequent_abs_spin.setValue(2)\n        self.assertListEqual(dialog.get_merge_attributes(), [\"c\"])\n\n        dialog.frequent_abs_spin.setValue(1)\n        self.assertListEqual(dialog.get_merge_attributes(), [])\n\n    def test_group_less_frequent_rel(self):\n        dialog = GroupItemsDialog(self.v, self.data, [\"a\", \"b\"], {})\n        dialog.frequent_rel_radio.setChecked(True)\n        dialog.frequent_rel_spin.setValue(50)\n        dialog.new_name_line_edit.setText(\"BA\")\n\n        self.assertListEqual(dialog.get_merge_attributes(), [\"b\", \"c\"])\n        self.assertEqual(dialog.get_merged_value_name(), \"BA\")\n\n        dialog.frequent_rel_spin.setValue(20)\n        self.assertListEqual(dialog.get_merge_attributes(), [\"c\"])\n\n        dialog.frequent_rel_spin.setValue(15)\n        self.assertListEqual(dialog.get_merge_attributes(), [])\n\n    def test_group_keep_n(self):\n        dialog = GroupItemsDialog(self.v, self.data, [\"a\", \"b\"], {})\n        dialog.n_values_radio.setChecked(True)\n        dialog.n_values_spin.setValue(1)\n        dialog.new_name_line_edit.setText(\"BA\")\n\n        self.assertListEqual(dialog.get_merge_attributes(), [\"b\", \"c\"])\n        self.assertEqual(dialog.get_merged_value_name(), \"BA\")\n\n        dialog.n_values_spin.setValue(2)\n        self.assertListEqual(dialog.get_merge_attributes(), [\"c\"])\n\n        dialog.n_values_spin.setValue(3)\n        self.assertListEqual(dialog.get_merge_attributes(), [])\n\n    def test_group_less_frequent_missing(self):\n        \"\"\"\n        Widget gives MaskedArray to GroupItemsDialog which can have missing\n        values.\n        gh-4599\n        \"\"\"\n        def _test_correctness():\n            dialog.frequent_abs_radio.setChecked(True)\n            dialog.frequent_abs_spin.setValue(3)\n            self.assertListEqual(dialog.get_merge_attributes(), [\"b\", \"c\"])\n\n            dialog.frequent_rel_radio.setChecked(True)\n            dialog.frequent_rel_spin.setValue(50)\n            self.assertListEqual(dialog.get_merge_attributes(), [\"b\", \"c\"])\n\n            dialog.n_values_radio.setChecked(True)\n            dialog.n_values_spin.setValue(1)\n            self.assertListEqual(dialog.get_merge_attributes(), [\"b\", \"c\"])\n\n        # masked array\n        data_masked = np.ma.array(\n            [0, 0, np.nan, 0, 1, 1, 2], mask=[0, 0, 1, 0, 0, 0, 0]\n        )\n        dialog = GroupItemsDialog(self.v, data_masked, [], {})\n        _test_correctness()\n\n        data_array = np.array([0, 0, np.nan, 0, 1, 1, 2])\n        dialog = GroupItemsDialog(self.v, data_array, [], {})\n        _test_correctness()\n\n        data_list = [0, 0, None, 0, 1, 1, 2]\n        dialog = GroupItemsDialog(self.v, data_list, [], {})\n        _test_correctness()\n\n\nif __name__ == '__main__':\n    unittest.main()\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/data/tests/test_oweditdomain.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "b4a7f7342",
                "new_commit_hash": "01b9d5fa4",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/data/tests/test_oweditdomain.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/data/tests/test_oweditdomain.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 921,
                            "length": 13
                        },
                        "new_range": {
                            "start": 921,
                            "length": 18
                        },
                        "section": "class TestReinterpretTransforms(TestCase):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        times = (",
                                "type": "context"
                            },
                            {
                                "content": "            [\"07.02.2022\", \"18.04.2021\"],  # date only",
                                "type": "context"
                            },
                            {
                                "content": "            [\"07.02.2022 01:02:03\", \"18.04.2021 01:02:03\"],  # datetime",
                                "type": "context"
                            },
                            {
                                "content": "            # datetime with timezone",
                                "type": "added"
                            },
                            {
                                "content": "            [\"2021-02-08 01:02:03+01:00\", \"2021-02-07 01:02:03+01:00\"],",
                                "type": "added"
                            },
                            {
                                "content": "            [\"010203\", \"010203\"],  # time",
                                "type": "context"
                            },
                            {
                                "content": "            [\"02-07\", \"04-18\"],",
                                "type": "context"
                            },
                            {
                                "content": "        )",
                                "type": "context"
                            },
                            {
                                "content": "        formats = [\"25.11.2021\", \"25.11.2021 00:00:00\", \"000000\", \"11-25\"]",
                                "type": "deleted"
                            },
                            {
                                "content": "        formats = [",
                                "type": "added"
                            },
                            {
                                "content": "            \"25.11.2021\", \"25.11.2021 00:00:00\", \"2021-11-25 00:00:00\", \"000000\", \"11-25\"",
                                "type": "added"
                            },
                            {
                                "content": "        ]",
                                "type": "added"
                            },
                            {
                                "content": "        expected = [",
                                "type": "context"
                            },
                            {
                                "content": "            [d(\"2022-02-07\"), d(\"2021-04-18\")],",
                                "type": "context"
                            },
                            {
                                "content": "            [d(\"2022-02-07 01:02:03\"), d(\"2021-04-18 01:02:03\")],",
                                "type": "context"
                            },
                            {
                                "content": "            [d(\"2021-02-08 01:02:03+0100\"), d(\"2021-02-07 01:02:03+0100\")],",
                                "type": "added"
                            },
                            {
                                "content": "            [d(\"01:02:03\"), d(\"01:02:03\")],",
                                "type": "context"
                            },
                            {
                                "content": "            [d(\"1900-02-07\"), d(\"1900-04-18\")],",
                                "type": "context"
                            },
                            {
                                "content": "        ]",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/data/tests/test_oweditdomain.py",
                            "type": "class",
                            "name": "TestReinterpretTransforms",
                            "content": "class TestReinterpretTransforms(TestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        super().setUpClass()\n        domain = Domain([\n            DiscreteVariable(\"A\", values=(\"a\", \"b\", \"c\")),\n            DiscreteVariable(\"B\", values=(\"0\", \"1\", \"2\")),\n            ContinuousVariable(\"C\"),\n            TimeVariable(\"D\", have_time=True),\n        ],\n        metas=[\n            StringVariable(\"S\")\n        ])\n        cls.data = Table.from_list(\n            domain, [\n                [0, 2, 0.25, 180],\n                [1, 1, 1.25, 360],\n                [2, 0, 0.20, 720],\n                [1, 0, 0.00, 000],\n            ]\n        )\n        cls.data_str = Table.from_list(\n            Domain([], [], metas=[\n                StringVariable(\"S\"),\n                StringVariable(\"T\")\n            ]),\n            [[\"0.1\", \"2010\"],\n             [\"1.0\", \"2020\"]]\n        )\n\n    def test_as_string(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsString()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        ttable = table.transform(Domain([], [], dtr))\n        assert_array_equal(\n            ttable.metas,\n            np.array([\n                [\"a\", \"2\", \"0.25\", \"00:03:00\"],\n                [\"b\", \"1\", \"1.25\", \"00:06:00\"],\n                [\"c\", \"0\", \"0.2\", \"00:12:00\"],\n                [\"b\", \"0\", \"0.0\", \"00:00:00\"],\n            ], dtype=object)\n        )\n\n    def test_as_discrete(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsCategorical()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        tdomain = Domain(dtr)\n        ttable = table.transform(tdomain)\n        assert_array_equal(\n            ttable.X,\n            np.array([\n                [0, 2, 2, 1],\n                [1, 1, 3, 2],\n                [2, 0, 1, 3],\n                [1, 0, 0, 0],\n            ], dtype=float)\n        )\n        self.assertEqual(tdomain[\"A\"].values, (\"a\", \"b\", \"c\"))\n        self.assertEqual(tdomain[\"B\"].values, (\"0\", \"1\", \"2\"))\n        self.assertEqual(tdomain[\"C\"].values, (\"0.0\", \"0.2\", \"0.25\", \"1.25\"))\n        self.assertEqual(\n            tdomain[\"D\"].values,\n            (\"1970-01-01 00:00:00\", \"1970-01-01 00:03:00\",\n             \"1970-01-01 00:06:00\", \"1970-01-01 00:12:00\")\n        )\n\n    def test_as_continuous(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsContinuous()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        ttable = table.transform(Domain(dtr))\n        assert_array_equal(\n            ttable.X,\n            np.array([\n                [np.nan, 2, 0.25, 180],\n                [np.nan, 1, 1.25, 360],\n                [np.nan, 0, 0.20, 720],\n                [np.nan, 0, 0.00, 000],\n            ], dtype=float)\n        )\n\n    def test_as_time(self):\n        # this test only test type of format that can be string, continuous and discrete\n        # correctness of time formats is already tested in TimeVariable module\n        d = TimeVariable(\"_\").parse_exact_iso\n        times = (\n            [\"07.02.2022\", \"18.04.2021\"],  # date only\n            [\"07.02.2022 01:02:03\", \"18.04.2021 01:02:03\"],  # datetime\n            [\"010203\", \"010203\"],  # time\n            [\"02-07\", \"04-18\"],\n        )\n        formats = [\"25.11.2021\", \"25.11.2021 00:00:00\", \"000000\", \"11-25\"]\n        expected = [\n            [d(\"2022-02-07\"), d(\"2021-04-18\")],\n            [d(\"2022-02-07 01:02:03\"), d(\"2021-04-18 01:02:03\")],\n            [d(\"01:02:03\"), d(\"01:02:03\")],\n            [d(\"1900-02-07\"), d(\"1900-04-18\")],\n        ]\n        variables = [StringVariable(f\"s{i}\") for i in range(len(times))]\n        variables += [DiscreteVariable(f\"d{i}\", values=t) for i, t in enumerate(times)]\n        domain = Domain([], metas=variables)\n        metas = [t for t in times] + [list(range(len(x))) for x in times]\n        table = Table(domain, np.empty((len(times[0]), 0)), metas=np.array(metas).transpose())\n\n        tr = AsTime()\n        dtr = []\n        for v, f in zip(domain.metas, chain(formats, formats)):\n            strp = StrpTime(f, *TimeVariable.ADDITIONAL_FORMATS[f])\n            vtr = apply_transform_var(\n                apply_reinterpret(v, tr, table_column_data(table, v)), [strp]\n            )\n            dtr.append(vtr)\n\n        ttable = table.transform(Domain([], metas=dtr))\n        assert_array_equal(\n            ttable.metas,\n            np.array(list(chain(expected, expected)), dtype=float).transpose()\n        )\n\n    def test_reinterpret_string(self):\n        table = self.data_str\n        domain = table.domain\n        tvars = []\n        for v in domain.metas:\n            for i, tr in enumerate(\n                [AsContinuous(), AsCategorical(), AsTime(), AsString()]\n            ):\n                vtr = apply_reinterpret(v, tr, table_column_data(table, v)).renamed(\n                    f\"{v.name}_{i}\"\n                )\n                if isinstance(tr, AsTime):\n                    strp = StrpTime(\"Detect automatically\", None, 1, 1)\n                    vtr = apply_transform_var(vtr, [strp])\n                tvars.append(vtr)\n        tdomain = Domain([], metas=tvars)\n        ttable = table.transform(tdomain)\n        assert_array_nanequal(\n            ttable.metas,\n            np.array([\n                [0.1, 0., np.nan, \"0.1\", 2010., 0., 1262304000., \"2010\"],\n                [1.0, 1., np.nan, \"1.0\", 2020., 1., 1577836800., \"2020\"],\n            ], dtype=object)\n        )\n\n    def test_compound_transform(self):\n        table = self.data_str\n        domain = table.domain\n        v1 = domain.metas[0]\n        v1.attributes[\"a\"] = \"a\"\n        tv1 = apply_transform(v1, table, [AsContinuous(), Rename(\"Z1\")])\n        tv2 = apply_transform(v1, table, [AsContinuous(), Rename(\"Z2\"), Annotate(((\"a\", \"b\"),))])\n\n        self.assertIsInstance(tv1, ContinuousVariable)\n        self.assertEqual(tv1.name, \"Z1\")\n        self.assertEqual(tv1.attributes, {\"a\": \"a\"})\n\n        self.assertIsInstance(tv2, ContinuousVariable)\n        self.assertEqual(tv2.name, \"Z2\")\n        self.assertEqual(tv2.attributes, {\"a\": \"b\"})\n\n        tdomain = Domain([], metas=[tv1, tv2])\n        ttable = table.transform(tdomain)\n\n        assert_array_nanequal(\n            ttable.metas,\n            np.array([\n                [0.1, 0.1],\n                [1.0, 1.0],\n            ], dtype=object)\n        )\n\n    def test_null_transform(self):\n        table = self.data_str\n        domain = table.domain\n        v = apply_transform(domain.metas[0],table, [])\n        self.assertIs(v, domain.metas[0])",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 818,
                            "end_lineno": 1011,
                            "parent": null
                        },
                        {
                            "file_name": "Orange/widgets/data/tests/test_oweditdomain.py",
                            "type": "method",
                            "name": "TestReinterpretTransforms.test_as_time",
                            "content": "class TestReinterpretTransforms(TestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        super().setUpClass()\n        domain = Domain([\n            DiscreteVariable(\"A\", values=(\"a\", \"b\", \"c\")),\n            DiscreteVariable(\"B\", values=(\"0\", \"1\", \"2\")),\n            ContinuousVariable(\"C\"),\n            TimeVariable(\"D\", have_time=True),\n        ],\n        metas=[\n            StringVariable(\"S\")\n        ])\n        cls.data = Table.from_list(\n            domain, [\n                [0, 2, 0.25, 180],\n                [1, 1, 1.25, 360],\n                [2, 0, 0.20, 720],\n                [1, 0, 0.00, 000],\n            ]\n        )\n        cls.data_str = Table.from_list(\n            Domain([], [], metas=[\n                StringVariable(\"S\"),\n                StringVariable(\"T\")\n            ]),\n            [[\"0.1\", \"2010\"],\n             [\"1.0\", \"2020\"]]\n        )\n\n    def test_as_string(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsString()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        ttable = table.transform(Domain([], [], dtr))\n        assert_array_equal(\n            ttable.metas,\n            np.array([\n                [\"a\", \"2\", \"0.25\", \"00:03:00\"],\n                [\"b\", \"1\", \"1.25\", \"00:06:00\"],\n                [\"c\", \"0\", \"0.2\", \"00:12:00\"],\n                [\"b\", \"0\", \"0.0\", \"00:00:00\"],\n            ], dtype=object)\n        )\n\n    def test_as_discrete(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsCategorical()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        tdomain = Domain(dtr)\n        ttable = table.transform(tdomain)\n        assert_array_equal(\n            ttable.X,\n            np.array([\n                [0, 2, 2, 1],\n                [1, 1, 3, 2],\n                [2, 0, 1, 3],\n                [1, 0, 0, 0],\n            ], dtype=float)\n        )\n        self.assertEqual(tdomain[\"A\"].values, (\"a\", \"b\", \"c\"))\n        self.assertEqual(tdomain[\"B\"].values, (\"0\", \"1\", \"2\"))\n        self.assertEqual(tdomain[\"C\"].values, (\"0.0\", \"0.2\", \"0.25\", \"1.25\"))\n        self.assertEqual(\n            tdomain[\"D\"].values,\n            (\"1970-01-01 00:00:00\", \"1970-01-01 00:03:00\",\n             \"1970-01-01 00:06:00\", \"1970-01-01 00:12:00\")\n        )\n\n    def test_as_continuous(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsContinuous()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        ttable = table.transform(Domain(dtr))\n        assert_array_equal(\n            ttable.X,\n            np.array([\n                [np.nan, 2, 0.25, 180],\n                [np.nan, 1, 1.25, 360],\n                [np.nan, 0, 0.20, 720],\n                [np.nan, 0, 0.00, 000],\n            ], dtype=float)\n        )\n\n    def test_as_time(self):\n        # this test only test type of format that can be string, continuous and discrete\n        # correctness of time formats is already tested in TimeVariable module\n        d = TimeVariable(\"_\").parse_exact_iso\n        times = (\n            [\"07.02.2022\", \"18.04.2021\"],  # date only\n            [\"07.02.2022 01:02:03\", \"18.04.2021 01:02:03\"],  # datetime\n            [\"010203\", \"010203\"],  # time\n            [\"02-07\", \"04-18\"],\n        )\n        formats = [\"25.11.2021\", \"25.11.2021 00:00:00\", \"000000\", \"11-25\"]\n        expected = [\n            [d(\"2022-02-07\"), d(\"2021-04-18\")],\n            [d(\"2022-02-07 01:02:03\"), d(\"2021-04-18 01:02:03\")],\n            [d(\"01:02:03\"), d(\"01:02:03\")],\n            [d(\"1900-02-07\"), d(\"1900-04-18\")],\n        ]\n        variables = [StringVariable(f\"s{i}\") for i in range(len(times))]\n        variables += [DiscreteVariable(f\"d{i}\", values=t) for i, t in enumerate(times)]\n        domain = Domain([], metas=variables)\n        metas = [t for t in times] + [list(range(len(x))) for x in times]\n        table = Table(domain, np.empty((len(times[0]), 0)), metas=np.array(metas).transpose())\n\n        tr = AsTime()\n        dtr = []\n        for v, f in zip(domain.metas, chain(formats, formats)):\n            strp = StrpTime(f, *TimeVariable.ADDITIONAL_FORMATS[f])\n            vtr = apply_transform_var(\n                apply_reinterpret(v, tr, table_column_data(table, v)), [strp]\n            )\n            dtr.append(vtr)\n\n        ttable = table.transform(Domain([], metas=dtr))\n        assert_array_equal(\n            ttable.metas,\n            np.array(list(chain(expected, expected)), dtype=float).transpose()\n        )\n\n    def test_reinterpret_string(self):\n        table = self.data_str\n        domain = table.domain\n        tvars = []\n        for v in domain.metas:\n            for i, tr in enumerate(\n                [AsContinuous(), AsCategorical(), AsTime(), AsString()]\n            ):\n                vtr = apply_reinterpret(v, tr, table_column_data(table, v)).renamed(\n                    f\"{v.name}_{i}\"\n                )\n                if isinstance(tr, AsTime):\n                    strp = StrpTime(\"Detect automatically\", None, 1, 1)\n                    vtr = apply_transform_var(vtr, [strp])\n                tvars.append(vtr)\n        tdomain = Domain([], metas=tvars)\n        ttable = table.transform(tdomain)\n        assert_array_nanequal(\n            ttable.metas,\n            np.array([\n                [0.1, 0., np.nan, \"0.1\", 2010., 0., 1262304000., \"2010\"],\n                [1.0, 1., np.nan, \"1.0\", 2020., 1., 1577836800., \"2020\"],\n            ], dtype=object)\n        )\n\n    def test_compound_transform(self):\n        table = self.data_str\n        domain = table.domain\n        v1 = domain.metas[0]\n        v1.attributes[\"a\"] = \"a\"\n        tv1 = apply_transform(v1, table, [AsContinuous(), Rename(\"Z1\")])\n        tv2 = apply_transform(v1, table, [AsContinuous(), Rename(\"Z2\"), Annotate(((\"a\", \"b\"),))])\n\n        self.assertIsInstance(tv1, ContinuousVariable)\n        self.assertEqual(tv1.name, \"Z1\")\n        self.assertEqual(tv1.attributes, {\"a\": \"a\"})\n\n        self.assertIsInstance(tv2, ContinuousVariable)\n        self.assertEqual(tv2.name, \"Z2\")\n        self.assertEqual(tv2.attributes, {\"a\": \"b\"})\n\n        tdomain = Domain([], metas=[tv1, tv2])\n        ttable = table.transform(tdomain)\n\n        assert_array_nanequal(\n            ttable.metas,\n            np.array([\n                [0.1, 0.1],\n                [1.0, 1.0],\n            ], dtype=object)\n        )\n\n    def test_null_transform(self):\n        table = self.data_str\n        domain = table.domain\n        v = apply_transform(domain.metas[0],table, [])\n        self.assertIs(v, domain.metas[0])",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 917,
                            "end_lineno": 953,
                            "parent": {
                                "file_name": "Orange/widgets/data/tests/test_oweditdomain.py",
                                "type": "class",
                                "name": "TestReinterpretTransforms",
                                "content": "class TestReinterpretTransforms(TestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        super().setUpClass()\n        domain = Domain([\n            DiscreteVariable(\"A\", values=(\"a\", \"b\", \"c\")),\n            DiscreteVariable(\"B\", values=(\"0\", \"1\", \"2\")),\n            ContinuousVariable(\"C\"),\n            TimeVariable(\"D\", have_time=True),\n        ],\n        metas=[\n            StringVariable(\"S\")\n        ])\n        cls.data = Table.from_list(\n            domain, [\n                [0, 2, 0.25, 180],\n                [1, 1, 1.25, 360],\n                [2, 0, 0.20, 720],\n                [1, 0, 0.00, 000],\n            ]\n        )\n        cls.data_str = Table.from_list(\n            Domain([], [], metas=[\n                StringVariable(\"S\"),\n                StringVariable(\"T\")\n            ]),\n            [[\"0.1\", \"2010\"],\n             [\"1.0\", \"2020\"]]\n        )\n\n    def test_as_string(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsString()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        ttable = table.transform(Domain([], [], dtr))\n        assert_array_equal(\n            ttable.metas,\n            np.array([\n                [\"a\", \"2\", \"0.25\", \"00:03:00\"],\n                [\"b\", \"1\", \"1.25\", \"00:06:00\"],\n                [\"c\", \"0\", \"0.2\", \"00:12:00\"],\n                [\"b\", \"0\", \"0.0\", \"00:00:00\"],\n            ], dtype=object)\n        )\n\n    def test_as_discrete(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsCategorical()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        tdomain = Domain(dtr)\n        ttable = table.transform(tdomain)\n        assert_array_equal(\n            ttable.X,\n            np.array([\n                [0, 2, 2, 1],\n                [1, 1, 3, 2],\n                [2, 0, 1, 3],\n                [1, 0, 0, 0],\n            ], dtype=float)\n        )\n        self.assertEqual(tdomain[\"A\"].values, (\"a\", \"b\", \"c\"))\n        self.assertEqual(tdomain[\"B\"].values, (\"0\", \"1\", \"2\"))\n        self.assertEqual(tdomain[\"C\"].values, (\"0.0\", \"0.2\", \"0.25\", \"1.25\"))\n        self.assertEqual(\n            tdomain[\"D\"].values,\n            (\"1970-01-01 00:00:00\", \"1970-01-01 00:03:00\",\n             \"1970-01-01 00:06:00\", \"1970-01-01 00:12:00\")\n        )\n\n    def test_as_continuous(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsContinuous()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        ttable = table.transform(Domain(dtr))\n        assert_array_equal(\n            ttable.X,\n            np.array([\n                [np.nan, 2, 0.25, 180],\n                [np.nan, 1, 1.25, 360],\n                [np.nan, 0, 0.20, 720],\n                [np.nan, 0, 0.00, 000],\n            ], dtype=float)\n        )\n\n    def test_as_time(self):\n        # this test only test type of format that can be string, continuous and discrete\n        # correctness of time formats is already tested in TimeVariable module\n        d = TimeVariable(\"_\").parse_exact_iso\n        times = (\n            [\"07.02.2022\", \"18.04.2021\"],  # date only\n            [\"07.02.2022 01:02:03\", \"18.04.2021 01:02:03\"],  # datetime\n            [\"010203\", \"010203\"],  # time\n            [\"02-07\", \"04-18\"],\n        )\n        formats = [\"25.11.2021\", \"25.11.2021 00:00:00\", \"000000\", \"11-25\"]\n        expected = [\n            [d(\"2022-02-07\"), d(\"2021-04-18\")],\n            [d(\"2022-02-07 01:02:03\"), d(\"2021-04-18 01:02:03\")],\n            [d(\"01:02:03\"), d(\"01:02:03\")],\n            [d(\"1900-02-07\"), d(\"1900-04-18\")],\n        ]\n        variables = [StringVariable(f\"s{i}\") for i in range(len(times))]\n        variables += [DiscreteVariable(f\"d{i}\", values=t) for i, t in enumerate(times)]\n        domain = Domain([], metas=variables)\n        metas = [t for t in times] + [list(range(len(x))) for x in times]\n        table = Table(domain, np.empty((len(times[0]), 0)), metas=np.array(metas).transpose())\n\n        tr = AsTime()\n        dtr = []\n        for v, f in zip(domain.metas, chain(formats, formats)):\n            strp = StrpTime(f, *TimeVariable.ADDITIONAL_FORMATS[f])\n            vtr = apply_transform_var(\n                apply_reinterpret(v, tr, table_column_data(table, v)), [strp]\n            )\n            dtr.append(vtr)\n\n        ttable = table.transform(Domain([], metas=dtr))\n        assert_array_equal(\n            ttable.metas,\n            np.array(list(chain(expected, expected)), dtype=float).transpose()\n        )\n\n    def test_reinterpret_string(self):\n        table = self.data_str\n        domain = table.domain\n        tvars = []\n        for v in domain.metas:\n            for i, tr in enumerate(\n                [AsContinuous(), AsCategorical(), AsTime(), AsString()]\n            ):\n                vtr = apply_reinterpret(v, tr, table_column_data(table, v)).renamed(\n                    f\"{v.name}_{i}\"\n                )\n                if isinstance(tr, AsTime):\n                    strp = StrpTime(\"Detect automatically\", None, 1, 1)\n                    vtr = apply_transform_var(vtr, [strp])\n                tvars.append(vtr)\n        tdomain = Domain([], metas=tvars)\n        ttable = table.transform(tdomain)\n        assert_array_nanequal(\n            ttable.metas,\n            np.array([\n                [0.1, 0., np.nan, \"0.1\", 2010., 0., 1262304000., \"2010\"],\n                [1.0, 1., np.nan, \"1.0\", 2020., 1., 1577836800., \"2020\"],\n            ], dtype=object)\n        )\n\n    def test_compound_transform(self):\n        table = self.data_str\n        domain = table.domain\n        v1 = domain.metas[0]\n        v1.attributes[\"a\"] = \"a\"\n        tv1 = apply_transform(v1, table, [AsContinuous(), Rename(\"Z1\")])\n        tv2 = apply_transform(v1, table, [AsContinuous(), Rename(\"Z2\"), Annotate(((\"a\", \"b\"),))])\n\n        self.assertIsInstance(tv1, ContinuousVariable)\n        self.assertEqual(tv1.name, \"Z1\")\n        self.assertEqual(tv1.attributes, {\"a\": \"a\"})\n\n        self.assertIsInstance(tv2, ContinuousVariable)\n        self.assertEqual(tv2.name, \"Z2\")\n        self.assertEqual(tv2.attributes, {\"a\": \"b\"})\n\n        tdomain = Domain([], metas=[tv1, tv2])\n        ttable = table.transform(tdomain)\n\n        assert_array_nanequal(\n            ttable.metas,\n            np.array([\n                [0.1, 0.1],\n                [1.0, 1.0],\n            ], dtype=object)\n        )\n\n    def test_null_transform(self):\n        table = self.data_str\n        domain = table.domain\n        v = apply_transform(domain.metas[0],table, [])\n        self.assertIs(v, domain.metas[0])",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 818,
                                "end_lineno": 1011,
                                "parent": null
                            }
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 952,
                            "length": 6
                        },
                        "new_range": {
                            "start": 957,
                            "length": 16
                        },
                        "section": "class TestReinterpretTransforms(TestCase):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "            np.array(list(chain(expected, expected)), dtype=float).transpose()",
                                "type": "context"
                            },
                            {
                                "content": "        )",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    def test_raise_pandas_version(self):",
                                "type": "added"
                            },
                            {
                                "content": "        \"\"\"",
                                "type": "added"
                            },
                            {
                                "content": "        When this test start to fail:",
                                "type": "added"
                            },
                            {
                                "content": "        - remove this test",
                                "type": "added"
                            },
                            {
                                "content": "        - remove if clause in datetime_to_epoch function and supporting comments",
                                "type": "added"
                            },
                            {
                                "content": "        - set pandas dependency version to pandas>=1.4",
                                "type": "added"
                            },
                            {
                                "content": "        \"\"\"",
                                "type": "added"
                            },
                            {
                                "content": "        from datetime import datetime",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertLess(datetime.today(), datetime(2023, 1, 1))",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    def test_reinterpret_string(self):",
                                "type": "context"
                            },
                            {
                                "content": "        table = self.data_str",
                                "type": "context"
                            },
                            {
                                "content": "        domain = table.domain",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/data/tests/test_oweditdomain.py",
                            "type": "class",
                            "name": "TestReinterpretTransforms",
                            "content": "class TestReinterpretTransforms(TestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        super().setUpClass()\n        domain = Domain([\n            DiscreteVariable(\"A\", values=(\"a\", \"b\", \"c\")),\n            DiscreteVariable(\"B\", values=(\"0\", \"1\", \"2\")),\n            ContinuousVariable(\"C\"),\n            TimeVariable(\"D\", have_time=True),\n        ],\n        metas=[\n            StringVariable(\"S\")\n        ])\n        cls.data = Table.from_list(\n            domain, [\n                [0, 2, 0.25, 180],\n                [1, 1, 1.25, 360],\n                [2, 0, 0.20, 720],\n                [1, 0, 0.00, 000],\n            ]\n        )\n        cls.data_str = Table.from_list(\n            Domain([], [], metas=[\n                StringVariable(\"S\"),\n                StringVariable(\"T\")\n            ]),\n            [[\"0.1\", \"2010\"],\n             [\"1.0\", \"2020\"]]\n        )\n\n    def test_as_string(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsString()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        ttable = table.transform(Domain([], [], dtr))\n        assert_array_equal(\n            ttable.metas,\n            np.array([\n                [\"a\", \"2\", \"0.25\", \"00:03:00\"],\n                [\"b\", \"1\", \"1.25\", \"00:06:00\"],\n                [\"c\", \"0\", \"0.2\", \"00:12:00\"],\n                [\"b\", \"0\", \"0.0\", \"00:00:00\"],\n            ], dtype=object)\n        )\n\n    def test_as_discrete(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsCategorical()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        tdomain = Domain(dtr)\n        ttable = table.transform(tdomain)\n        assert_array_equal(\n            ttable.X,\n            np.array([\n                [0, 2, 2, 1],\n                [1, 1, 3, 2],\n                [2, 0, 1, 3],\n                [1, 0, 0, 0],\n            ], dtype=float)\n        )\n        self.assertEqual(tdomain[\"A\"].values, (\"a\", \"b\", \"c\"))\n        self.assertEqual(tdomain[\"B\"].values, (\"0\", \"1\", \"2\"))\n        self.assertEqual(tdomain[\"C\"].values, (\"0.0\", \"0.2\", \"0.25\", \"1.25\"))\n        self.assertEqual(\n            tdomain[\"D\"].values,\n            (\"1970-01-01 00:00:00\", \"1970-01-01 00:03:00\",\n             \"1970-01-01 00:06:00\", \"1970-01-01 00:12:00\")\n        )\n\n    def test_as_continuous(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsContinuous()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        ttable = table.transform(Domain(dtr))\n        assert_array_equal(\n            ttable.X,\n            np.array([\n                [np.nan, 2, 0.25, 180],\n                [np.nan, 1, 1.25, 360],\n                [np.nan, 0, 0.20, 720],\n                [np.nan, 0, 0.00, 000],\n            ], dtype=float)\n        )\n\n    def test_as_time(self):\n        # this test only test type of format that can be string, continuous and discrete\n        # correctness of time formats is already tested in TimeVariable module\n        d = TimeVariable(\"_\").parse_exact_iso\n        times = (\n            [\"07.02.2022\", \"18.04.2021\"],  # date only\n            [\"07.02.2022 01:02:03\", \"18.04.2021 01:02:03\"],  # datetime\n            # datetime with timezone\n            [\"2021-02-08 01:02:03+01:00\", \"2021-02-07 01:02:03+01:00\"],\n            [\"010203\", \"010203\"],  # time\n            [\"02-07\", \"04-18\"],\n        )\n        formats = [\n            \"25.11.2021\", \"25.11.2021 00:00:00\", \"2021-11-25 00:00:00\", \"000000\", \"11-25\"\n        ]\n        expected = [\n            [d(\"2022-02-07\"), d(\"2021-04-18\")],\n            [d(\"2022-02-07 01:02:03\"), d(\"2021-04-18 01:02:03\")],\n            [d(\"2021-02-08 01:02:03+0100\"), d(\"2021-02-07 01:02:03+0100\")],\n            [d(\"01:02:03\"), d(\"01:02:03\")],\n            [d(\"1900-02-07\"), d(\"1900-04-18\")],\n        ]\n        variables = [StringVariable(f\"s{i}\") for i in range(len(times))]\n        variables += [DiscreteVariable(f\"d{i}\", values=t) for i, t in enumerate(times)]\n        domain = Domain([], metas=variables)\n        metas = [t for t in times] + [list(range(len(x))) for x in times]\n        table = Table(domain, np.empty((len(times[0]), 0)), metas=np.array(metas).transpose())\n\n        tr = AsTime()\n        dtr = []\n        for v, f in zip(domain.metas, chain(formats, formats)):\n            strp = StrpTime(f, *TimeVariable.ADDITIONAL_FORMATS[f])\n            vtr = apply_transform_var(\n                apply_reinterpret(v, tr, table_column_data(table, v)), [strp]\n            )\n            dtr.append(vtr)\n\n        ttable = table.transform(Domain([], metas=dtr))\n        assert_array_equal(\n            ttable.metas,\n            np.array(list(chain(expected, expected)), dtype=float).transpose()\n        )\n\n    def test_raise_pandas_version(self):\n        \"\"\"\n        When this test start to fail:\n        - remove this test\n        - remove if clause in datetime_to_epoch function and supporting comments\n        - set pandas dependency version to pandas>=1.4\n        \"\"\"\n        from datetime import datetime\n        self.assertLess(datetime.today(), datetime(2023, 1, 1))\n\n    def test_reinterpret_string(self):\n        table = self.data_str\n        domain = table.domain\n        tvars = []\n        for v in domain.metas:\n            for i, tr in enumerate(\n                [AsContinuous(), AsCategorical(), AsTime(), AsString()]\n            ):\n                vtr = apply_reinterpret(v, tr, table_column_data(table, v)).renamed(\n                    f\"{v.name}_{i}\"\n                )\n                if isinstance(tr, AsTime):\n                    strp = StrpTime(\"Detect automatically\", None, 1, 1)\n                    vtr = apply_transform_var(vtr, [strp])\n                tvars.append(vtr)\n        tdomain = Domain([], metas=tvars)\n        ttable = table.transform(tdomain)\n        assert_array_nanequal(\n            ttable.metas,\n            np.array([\n                [0.1, 0., np.nan, \"0.1\", 2010., 0., 1262304000., \"2010\"],\n                [1.0, 1., np.nan, \"1.0\", 2020., 1., 1577836800., \"2020\"],\n            ], dtype=object)\n        )\n\n    def test_compound_transform(self):\n        table = self.data_str\n        domain = table.domain\n        v1 = domain.metas[0]\n        v1.attributes[\"a\"] = \"a\"\n        tv1 = apply_transform(v1, table, [AsContinuous(), Rename(\"Z1\")])\n        tv2 = apply_transform(v1, table, [AsContinuous(), Rename(\"Z2\"), Annotate(((\"a\", \"b\"),))])\n\n        self.assertIsInstance(tv1, ContinuousVariable)\n        self.assertEqual(tv1.name, \"Z1\")\n        self.assertEqual(tv1.attributes, {\"a\": \"a\"})\n\n        self.assertIsInstance(tv2, ContinuousVariable)\n        self.assertEqual(tv2.name, \"Z2\")\n        self.assertEqual(tv2.attributes, {\"a\": \"b\"})\n\n        tdomain = Domain([], metas=[tv1, tv2])\n        ttable = table.transform(tdomain)\n\n        assert_array_nanequal(\n            ttable.metas,\n            np.array([\n                [0.1, 0.1],\n                [1.0, 1.0],\n            ], dtype=object)\n        )\n\n    def test_null_transform(self):\n        table = self.data_str\n        domain = table.domain\n        v = apply_transform(domain.metas[0],table, [])\n        self.assertIs(v, domain.metas[0])",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 818,
                            "end_lineno": 1026,
                            "parent": null
                        }
                    ],
                    "added_entities": [
                        {
                            "file_name": "Orange/widgets/data/tests/test_oweditdomain.py",
                            "type": "method",
                            "name": "TestReinterpretTransforms.test_raise_pandas_version",
                            "content": "class TestReinterpretTransforms(TestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        super().setUpClass()\n        domain = Domain([\n            DiscreteVariable(\"A\", values=(\"a\", \"b\", \"c\")),\n            DiscreteVariable(\"B\", values=(\"0\", \"1\", \"2\")),\n            ContinuousVariable(\"C\"),\n            TimeVariable(\"D\", have_time=True),\n        ],\n        metas=[\n            StringVariable(\"S\")\n        ])\n        cls.data = Table.from_list(\n            domain, [\n                [0, 2, 0.25, 180],\n                [1, 1, 1.25, 360],\n                [2, 0, 0.20, 720],\n                [1, 0, 0.00, 000],\n            ]\n        )\n        cls.data_str = Table.from_list(\n            Domain([], [], metas=[\n                StringVariable(\"S\"),\n                StringVariable(\"T\")\n            ]),\n            [[\"0.1\", \"2010\"],\n             [\"1.0\", \"2020\"]]\n        )\n\n    def test_as_string(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsString()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        ttable = table.transform(Domain([], [], dtr))\n        assert_array_equal(\n            ttable.metas,\n            np.array([\n                [\"a\", \"2\", \"0.25\", \"00:03:00\"],\n                [\"b\", \"1\", \"1.25\", \"00:06:00\"],\n                [\"c\", \"0\", \"0.2\", \"00:12:00\"],\n                [\"b\", \"0\", \"0.0\", \"00:00:00\"],\n            ], dtype=object)\n        )\n\n    def test_as_discrete(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsCategorical()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        tdomain = Domain(dtr)\n        ttable = table.transform(tdomain)\n        assert_array_equal(\n            ttable.X,\n            np.array([\n                [0, 2, 2, 1],\n                [1, 1, 3, 2],\n                [2, 0, 1, 3],\n                [1, 0, 0, 0],\n            ], dtype=float)\n        )\n        self.assertEqual(tdomain[\"A\"].values, (\"a\", \"b\", \"c\"))\n        self.assertEqual(tdomain[\"B\"].values, (\"0\", \"1\", \"2\"))\n        self.assertEqual(tdomain[\"C\"].values, (\"0.0\", \"0.2\", \"0.25\", \"1.25\"))\n        self.assertEqual(\n            tdomain[\"D\"].values,\n            (\"1970-01-01 00:00:00\", \"1970-01-01 00:03:00\",\n             \"1970-01-01 00:06:00\", \"1970-01-01 00:12:00\")\n        )\n\n    def test_as_continuous(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsContinuous()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        ttable = table.transform(Domain(dtr))\n        assert_array_equal(\n            ttable.X,\n            np.array([\n                [np.nan, 2, 0.25, 180],\n                [np.nan, 1, 1.25, 360],\n                [np.nan, 0, 0.20, 720],\n                [np.nan, 0, 0.00, 000],\n            ], dtype=float)\n        )\n\n    def test_as_time(self):\n        # this test only test type of format that can be string, continuous and discrete\n        # correctness of time formats is already tested in TimeVariable module\n        d = TimeVariable(\"_\").parse_exact_iso\n        times = (\n            [\"07.02.2022\", \"18.04.2021\"],  # date only\n            [\"07.02.2022 01:02:03\", \"18.04.2021 01:02:03\"],  # datetime\n            # datetime with timezone\n            [\"2021-02-08 01:02:03+01:00\", \"2021-02-07 01:02:03+01:00\"],\n            [\"010203\", \"010203\"],  # time\n            [\"02-07\", \"04-18\"],\n        )\n        formats = [\n            \"25.11.2021\", \"25.11.2021 00:00:00\", \"2021-11-25 00:00:00\", \"000000\", \"11-25\"\n        ]\n        expected = [\n            [d(\"2022-02-07\"), d(\"2021-04-18\")],\n            [d(\"2022-02-07 01:02:03\"), d(\"2021-04-18 01:02:03\")],\n            [d(\"2021-02-08 01:02:03+0100\"), d(\"2021-02-07 01:02:03+0100\")],\n            [d(\"01:02:03\"), d(\"01:02:03\")],\n            [d(\"1900-02-07\"), d(\"1900-04-18\")],\n        ]\n        variables = [StringVariable(f\"s{i}\") for i in range(len(times))]\n        variables += [DiscreteVariable(f\"d{i}\", values=t) for i, t in enumerate(times)]\n        domain = Domain([], metas=variables)\n        metas = [t for t in times] + [list(range(len(x))) for x in times]\n        table = Table(domain, np.empty((len(times[0]), 0)), metas=np.array(metas).transpose())\n\n        tr = AsTime()\n        dtr = []\n        for v, f in zip(domain.metas, chain(formats, formats)):\n            strp = StrpTime(f, *TimeVariable.ADDITIONAL_FORMATS[f])\n            vtr = apply_transform_var(\n                apply_reinterpret(v, tr, table_column_data(table, v)), [strp]\n            )\n            dtr.append(vtr)\n\n        ttable = table.transform(Domain([], metas=dtr))\n        assert_array_equal(\n            ttable.metas,\n            np.array(list(chain(expected, expected)), dtype=float).transpose()\n        )\n\n    def test_raise_pandas_version(self):\n        \"\"\"\n        When this test start to fail:\n        - remove this test\n        - remove if clause in datetime_to_epoch function and supporting comments\n        - set pandas dependency version to pandas>=1.4\n        \"\"\"\n        from datetime import datetime\n        self.assertLess(datetime.today(), datetime(2023, 1, 1))\n\n    def test_reinterpret_string(self):\n        table = self.data_str\n        domain = table.domain\n        tvars = []\n        for v in domain.metas:\n            for i, tr in enumerate(\n                [AsContinuous(), AsCategorical(), AsTime(), AsString()]\n            ):\n                vtr = apply_reinterpret(v, tr, table_column_data(table, v)).renamed(\n                    f\"{v.name}_{i}\"\n                )\n                if isinstance(tr, AsTime):\n                    strp = StrpTime(\"Detect automatically\", None, 1, 1)\n                    vtr = apply_transform_var(vtr, [strp])\n                tvars.append(vtr)\n        tdomain = Domain([], metas=tvars)\n        ttable = table.transform(tdomain)\n        assert_array_nanequal(\n            ttable.metas,\n            np.array([\n                [0.1, 0., np.nan, \"0.1\", 2010., 0., 1262304000., \"2010\"],\n                [1.0, 1., np.nan, \"1.0\", 2020., 1., 1577836800., \"2020\"],\n            ], dtype=object)\n        )\n\n    def test_compound_transform(self):\n        table = self.data_str\n        domain = table.domain\n        v1 = domain.metas[0]\n        v1.attributes[\"a\"] = \"a\"\n        tv1 = apply_transform(v1, table, [AsContinuous(), Rename(\"Z1\")])\n        tv2 = apply_transform(v1, table, [AsContinuous(), Rename(\"Z2\"), Annotate(((\"a\", \"b\"),))])\n\n        self.assertIsInstance(tv1, ContinuousVariable)\n        self.assertEqual(tv1.name, \"Z1\")\n        self.assertEqual(tv1.attributes, {\"a\": \"a\"})\n\n        self.assertIsInstance(tv2, ContinuousVariable)\n        self.assertEqual(tv2.name, \"Z2\")\n        self.assertEqual(tv2.attributes, {\"a\": \"b\"})\n\n        tdomain = Domain([], metas=[tv1, tv2])\n        ttable = table.transform(tdomain)\n\n        assert_array_nanequal(\n            ttable.metas,\n            np.array([\n                [0.1, 0.1],\n                [1.0, 1.0],\n            ], dtype=object)\n        )\n\n    def test_null_transform(self):\n        table = self.data_str\n        domain = table.domain\n        v = apply_transform(domain.metas[0],table, [])\n        self.assertIs(v, domain.metas[0])",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 960,
                            "end_lineno": 968,
                            "parent": {
                                "file_name": "Orange/widgets/data/tests/test_oweditdomain.py",
                                "type": "class",
                                "name": "TestReinterpretTransforms",
                                "content": "class TestReinterpretTransforms(TestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        super().setUpClass()\n        domain = Domain([\n            DiscreteVariable(\"A\", values=(\"a\", \"b\", \"c\")),\n            DiscreteVariable(\"B\", values=(\"0\", \"1\", \"2\")),\n            ContinuousVariable(\"C\"),\n            TimeVariable(\"D\", have_time=True),\n        ],\n        metas=[\n            StringVariable(\"S\")\n        ])\n        cls.data = Table.from_list(\n            domain, [\n                [0, 2, 0.25, 180],\n                [1, 1, 1.25, 360],\n                [2, 0, 0.20, 720],\n                [1, 0, 0.00, 000],\n            ]\n        )\n        cls.data_str = Table.from_list(\n            Domain([], [], metas=[\n                StringVariable(\"S\"),\n                StringVariable(\"T\")\n            ]),\n            [[\"0.1\", \"2010\"],\n             [\"1.0\", \"2020\"]]\n        )\n\n    def test_as_string(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsString()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        ttable = table.transform(Domain([], [], dtr))\n        assert_array_equal(\n            ttable.metas,\n            np.array([\n                [\"a\", \"2\", \"0.25\", \"00:03:00\"],\n                [\"b\", \"1\", \"1.25\", \"00:06:00\"],\n                [\"c\", \"0\", \"0.2\", \"00:12:00\"],\n                [\"b\", \"0\", \"0.0\", \"00:00:00\"],\n            ], dtype=object)\n        )\n\n    def test_as_discrete(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsCategorical()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        tdomain = Domain(dtr)\n        ttable = table.transform(tdomain)\n        assert_array_equal(\n            ttable.X,\n            np.array([\n                [0, 2, 2, 1],\n                [1, 1, 3, 2],\n                [2, 0, 1, 3],\n                [1, 0, 0, 0],\n            ], dtype=float)\n        )\n        self.assertEqual(tdomain[\"A\"].values, (\"a\", \"b\", \"c\"))\n        self.assertEqual(tdomain[\"B\"].values, (\"0\", \"1\", \"2\"))\n        self.assertEqual(tdomain[\"C\"].values, (\"0.0\", \"0.2\", \"0.25\", \"1.25\"))\n        self.assertEqual(\n            tdomain[\"D\"].values,\n            (\"1970-01-01 00:00:00\", \"1970-01-01 00:03:00\",\n             \"1970-01-01 00:06:00\", \"1970-01-01 00:12:00\")\n        )\n\n    def test_as_continuous(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsContinuous()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        ttable = table.transform(Domain(dtr))\n        assert_array_equal(\n            ttable.X,\n            np.array([\n                [np.nan, 2, 0.25, 180],\n                [np.nan, 1, 1.25, 360],\n                [np.nan, 0, 0.20, 720],\n                [np.nan, 0, 0.00, 000],\n            ], dtype=float)\n        )\n\n    def test_as_time(self):\n        # this test only test type of format that can be string, continuous and discrete\n        # correctness of time formats is already tested in TimeVariable module\n        d = TimeVariable(\"_\").parse_exact_iso\n        times = (\n            [\"07.02.2022\", \"18.04.2021\"],  # date only\n            [\"07.02.2022 01:02:03\", \"18.04.2021 01:02:03\"],  # datetime\n            # datetime with timezone\n            [\"2021-02-08 01:02:03+01:00\", \"2021-02-07 01:02:03+01:00\"],\n            [\"010203\", \"010203\"],  # time\n            [\"02-07\", \"04-18\"],\n        )\n        formats = [\n            \"25.11.2021\", \"25.11.2021 00:00:00\", \"2021-11-25 00:00:00\", \"000000\", \"11-25\"\n        ]\n        expected = [\n            [d(\"2022-02-07\"), d(\"2021-04-18\")],\n            [d(\"2022-02-07 01:02:03\"), d(\"2021-04-18 01:02:03\")],\n            [d(\"2021-02-08 01:02:03+0100\"), d(\"2021-02-07 01:02:03+0100\")],\n            [d(\"01:02:03\"), d(\"01:02:03\")],\n            [d(\"1900-02-07\"), d(\"1900-04-18\")],\n        ]\n        variables = [StringVariable(f\"s{i}\") for i in range(len(times))]\n        variables += [DiscreteVariable(f\"d{i}\", values=t) for i, t in enumerate(times)]\n        domain = Domain([], metas=variables)\n        metas = [t for t in times] + [list(range(len(x))) for x in times]\n        table = Table(domain, np.empty((len(times[0]), 0)), metas=np.array(metas).transpose())\n\n        tr = AsTime()\n        dtr = []\n        for v, f in zip(domain.metas, chain(formats, formats)):\n            strp = StrpTime(f, *TimeVariable.ADDITIONAL_FORMATS[f])\n            vtr = apply_transform_var(\n                apply_reinterpret(v, tr, table_column_data(table, v)), [strp]\n            )\n            dtr.append(vtr)\n\n        ttable = table.transform(Domain([], metas=dtr))\n        assert_array_equal(\n            ttable.metas,\n            np.array(list(chain(expected, expected)), dtype=float).transpose()\n        )\n\n    def test_raise_pandas_version(self):\n        \"\"\"\n        When this test start to fail:\n        - remove this test\n        - remove if clause in datetime_to_epoch function and supporting comments\n        - set pandas dependency version to pandas>=1.4\n        \"\"\"\n        from datetime import datetime\n        self.assertLess(datetime.today(), datetime(2023, 1, 1))\n\n    def test_reinterpret_string(self):\n        table = self.data_str\n        domain = table.domain\n        tvars = []\n        for v in domain.metas:\n            for i, tr in enumerate(\n                [AsContinuous(), AsCategorical(), AsTime(), AsString()]\n            ):\n                vtr = apply_reinterpret(v, tr, table_column_data(table, v)).renamed(\n                    f\"{v.name}_{i}\"\n                )\n                if isinstance(tr, AsTime):\n                    strp = StrpTime(\"Detect automatically\", None, 1, 1)\n                    vtr = apply_transform_var(vtr, [strp])\n                tvars.append(vtr)\n        tdomain = Domain([], metas=tvars)\n        ttable = table.transform(tdomain)\n        assert_array_nanequal(\n            ttable.metas,\n            np.array([\n                [0.1, 0., np.nan, \"0.1\", 2010., 0., 1262304000., \"2010\"],\n                [1.0, 1., np.nan, \"1.0\", 2020., 1., 1577836800., \"2020\"],\n            ], dtype=object)\n        )\n\n    def test_compound_transform(self):\n        table = self.data_str\n        domain = table.domain\n        v1 = domain.metas[0]\n        v1.attributes[\"a\"] = \"a\"\n        tv1 = apply_transform(v1, table, [AsContinuous(), Rename(\"Z1\")])\n        tv2 = apply_transform(v1, table, [AsContinuous(), Rename(\"Z2\"), Annotate(((\"a\", \"b\"),))])\n\n        self.assertIsInstance(tv1, ContinuousVariable)\n        self.assertEqual(tv1.name, \"Z1\")\n        self.assertEqual(tv1.attributes, {\"a\": \"a\"})\n\n        self.assertIsInstance(tv2, ContinuousVariable)\n        self.assertEqual(tv2.name, \"Z2\")\n        self.assertEqual(tv2.attributes, {\"a\": \"b\"})\n\n        tdomain = Domain([], metas=[tv1, tv2])\n        ttable = table.transform(tdomain)\n\n        assert_array_nanequal(\n            ttable.metas,\n            np.array([\n                [0.1, 0.1],\n                [1.0, 1.0],\n            ], dtype=object)\n        )\n\n    def test_null_transform(self):\n        table = self.data_str\n        domain = table.domain\n        v = apply_transform(domain.metas[0],table, [])\n        self.assertIs(v, domain.metas[0])",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 818,
                                "end_lineno": 1026,
                                "parent": null
                            }
                        }
                    ],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "7b40090a9ccd235bd84fa3eda1feed0e259621ab^",
    "new_commit_hash": "7b40090a9ccd235bd84fa3eda1feed0e259621ab",
    "commit_message": "Edit domain - fix converting to time variable when timezone set",
    "commit_date": "2022-09-26T16:45:52+02:00",
    "metadata": {}
}
{
    "repo_name": "orange3",
    "new_commit_hash": "7b40090a9ccd235bd84fa3eda1feed0e259621ab",
    "test_file_codes": [
        "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=all\nimport pickle\nimport unittest\nfrom functools import partial\nfrom itertools import product, chain\nfrom unittest import TestCase\nfrom unittest.mock import Mock, patch\n\nimport numpy as np\nfrom numpy.testing import assert_array_equal\n\nfrom AnyQt.QtCore import QItemSelectionModel, Qt, QItemSelection, QPoint\nfrom AnyQt.QtGui import QPalette, QColor, QHelpEvent\nfrom AnyQt.QtWidgets import QAction, QComboBox, QLineEdit, \\\n    QStyleOptionViewItem, QDialog, QMenu, QToolTip, QListView\nfrom AnyQt.QtTest import QTest, QSignalSpy\n\nfrom orangewidget.tests.utils import simulate\n\nfrom Orange.data import (\n    ContinuousVariable, DiscreteVariable, StringVariable, TimeVariable,\n    Table, Domain\n)\nfrom Orange.preprocess.transformation import Identity, Lookup\nfrom Orange.widgets.data.oweditdomain import (\n    OWEditDomain,\n    ContinuousVariableEditor, DiscreteVariableEditor, VariableEditor,\n    TimeVariableEditor, Categorical, Real, Time, String,\n    Rename, Annotate, Unlink, CategoriesMapping, report_transform,\n    apply_transform, apply_transform_var, apply_reinterpret, MultiplicityRole,\n    AsString, AsCategorical, AsContinuous, AsTime,\n    table_column_data, ReinterpretVariableEditor, CategoricalVector,\n    VariableEditDelegate, TransformRole,\n    RealVector, TimeVector, StringVector, make_dict_mapper,\n    LookupMappingTransform, as_float_or_nan, column_str_repr,\n    GroupItemsDialog, VariableListModel, StrpTime\n)\nfrom Orange.widgets.data.owcolor import OWColor, ColorRole\nfrom Orange.widgets.tests.base import WidgetTest, GuiTest\nfrom Orange.widgets.tests.utils import contextMenu\nfrom Orange.widgets.utils import colorpalettes\nfrom Orange.tests import test_filename, assert_array_nanequal\n\nMArray = np.ma.MaskedArray\n\n\nclass TestReport(TestCase):\n    def test_rename(self):\n        var = Real(\"X\", (-1, \"\"), (), False)\n        tr = Rename(\"Y\")\n        val = report_transform(var, [tr])\n        self.assertIn(\"X\", val)\n        self.assertIn(\"Y\", val)\n\n    def test_annotate(self):\n        var = Real(\"X\", (-1, \"\"), ((\"a\", \"1\"), (\"b\", \"z\")), False)\n        tr = Annotate(((\"a\", \"2\"), (\"j\", \"z\")))\n        r = report_transform(var, [tr])\n        self.assertIn(\"a\", r)\n        self.assertIn(\"b\", r)\n\n    def test_unlinke(self):\n        var = Real(\"X\", (-1, \"\"), ((\"a\", \"1\"), (\"b\", \"z\")), True)\n        r = report_transform(var, [Unlink()])\n        self.assertIn(\"unlinked\", r)\n\n    def test_categories_mapping(self):\n        var = Categorical(\"C\", (\"a\", \"b\", \"c\"), (), False)\n        tr = CategoriesMapping(\n            ((\"a\", \"aa\"),\n             (\"b\", None),\n             (\"c\", \"cc\"),\n             (None, \"ee\")),\n        )\n        r = report_transform(var, [tr])\n        self.assertIn(\"a\", r)\n        self.assertIn(\"aa\", r)\n        self.assertIn(\"b\", r)\n        self.assertIn(\"<s>\", r)\n\n    def test_categorical_merge_mapping(self):\n        var = Categorical(\"C\", (\"a\", \"b1\", \"b2\"), (), False)\n        tr = CategoriesMapping(\n            ((\"a\", \"a\"),\n             (\"b1\", \"b\"),\n             (\"b2\", \"b\"),\n             (None, \"c\")),\n        )\n        r = report_transform(var, [tr])\n        self.assertIn('b', r)\n\n    def test_reinterpret(self):\n        var = String(\"T\", (), False)\n        for tr in (AsContinuous(), AsCategorical(), AsTime()):\n            t = report_transform(var, [tr])\n            self.assertIn(\"→ (\", t)\n\n\nclass TestOWEditDomain(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(OWEditDomain)\n        self.iris = Table(\"iris\")\n\n    def test_input_data(self):\n        \"\"\"Check widget's data with data on the input\"\"\"\n        self.assertEqual(self.widget.data, None)\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertEqual(self.widget.data, self.iris)\n\n    def test_input_data_disconnect(self):\n        \"\"\"Check widget's data after disconnecting data on the input\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertEqual(self.widget.data, self.iris)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(self.widget.data, None)\n\n    def test_widget_state(self):\n        \"\"\"Check if widget clears its state when the input is disconnected\"\"\"\n        editor = self.widget.findChild(ContinuousVariableEditor)\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.assertEqual(editor.name_edit.text(), \"sepal length\")\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(editor.name_edit.text(), \"\")\n        self.assertEqual(\n            self.widget.variables_model.index(0).data(Qt.EditRole), None)\n\n        editor = self.widget.findChild(DiscreteVariableEditor)\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        index = self.widget.domain_view.model().index(4)\n        self.widget.variables_view.setCurrentIndex(index)\n        self.assertEqual(editor.name_edit.text(), \"iris\")\n        self.assertEqual(editor.labels_model.get_dict(), {})\n        self.assertNotEqual(\n            self.widget.variables_model.index(0).data(Qt.EditRole), None)\n        model = editor.values_edit.selectionModel().model()\n        self.assertEqual(model.index(0).data(Qt.EditRole), \"Iris-setosa\")\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(editor.name_edit.text(), \"\")\n        self.assertEqual(editor.labels_model.get_dict(), {})\n        self.assertEqual(model.index(0).data(Qt.EditRole), None)\n        self.assertEqual(\n            self.widget.variables_model.index(0).data(Qt.EditRole), None)\n\n        editor = self.widget.findChild(TimeVariableEditor)\n        table = Table(test_filename(\"datasets/cyber-security-breaches.tab\"))\n        self.send_signal(self.widget.Inputs.data, table)\n        index = self.widget.domain_view.model().index(4)\n        self.widget.variables_view.setCurrentIndex(index)\n        self.assertEqual(editor.name_edit.text(), \"Date_Posted_or_Updated\")\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(editor.name_edit.text(), \"\")\n        self.assertEqual(\n            self.widget.variables_model.index(0).data(Qt.EditRole), None)\n\n        editor = self.widget.findChild(VariableEditor)\n        self.send_signal(self.widget.Inputs.data, table)\n        index = self.widget.domain_view.model().index(8)\n        self.widget.variables_view.setCurrentIndex(index)\n        self.assertEqual(editor.var.name, \"Business_Associate_Involved\")\n        self.send_signal(self.widget.Inputs.data, None)\n        self.assertEqual(editor.var, None)\n        self.assertEqual(\n            self.widget.variables_model.index(0).data(Qt.EditRole), None)\n\n    def test_output_data(self):\n        \"\"\"Check data on the output after apply\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        output = self.get_output(self.widget.Outputs.data)\n        np.testing.assert_array_equal(output.X, self.iris.X)\n        np.testing.assert_array_equal(output.Y, self.iris.Y)\n        self.assertEqual(output.domain, self.iris.domain)\n\n        self.widget.output_table_name = \"Iris 2\"\n        self.widget.commit()\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(output.name, \"Iris 2\")\n\n    def test_input_from_owcolor(self):\n        \"\"\"Check widget's data sent from OWColor widget\"\"\"\n        owcolor = self.create_widget(OWColor)\n        self.send_signal(\"Data\", self.iris, widget=owcolor)\n        disc_model = owcolor.disc_model\n        disc_model.setData(disc_model.index(0, 1), (1, 2, 3), ColorRole)\n        cont_model = owcolor.cont_model\n        palette = list(colorpalettes.ContinuousPalettes.values())[-1]\n        cont_model.setData(cont_model.index(1, 1), palette, ColorRole)\n        owcolor_output = self.get_output(\"Data\", owcolor)\n        self.send_signal(\"Data\", owcolor_output)\n        self.assertEqual(self.widget.data, owcolor_output)\n        np.testing.assert_equal(self.widget.data.domain.class_var.colors[0],\n                                (1, 2, 3))\n        self.assertIs(self.widget.data.domain.attributes[1].palette, palette)\n\n    def test_list_attributes_remain_lists(self):\n        a = ContinuousVariable(\"a\")\n        a.attributes[\"list\"] = [1, 2, 3]\n        d = Domain([a])\n        t = Table.from_domain(d)\n\n        self.send_signal(self.widget.Inputs.data, t)\n\n        assert isinstance(self.widget, OWEditDomain)\n        # select first variable\n        idx = self.widget.domain_view.model().index(0)\n        self.widget.domain_view.setCurrentIndex(idx)\n\n        # change first attribute value\n        editor = self.widget.findChild(ContinuousVariableEditor)\n        assert isinstance(editor, ContinuousVariableEditor)\n        idx = editor.labels_model.index(0, 1)\n        editor.labels_model.setData(idx, \"[1, 2, 4]\", Qt.EditRole)\n\n        self.widget.commit()\n        t2 = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(t2.domain[\"a\"].attributes[\"list\"], [1, 2, 4])\n\n    def test_annotation_bool(self):\n        \"\"\"Check if bool labels remain bool\"\"\"\n        a = ContinuousVariable(\"a\")\n        a.attributes[\"hidden\"] = True\n        d = Domain([a])\n        t = Table.from_domain(d)\n\n        self.send_signal(self.widget.Inputs.data, t)\n\n        assert isinstance(self.widget, OWEditDomain)\n        # select first variable\n        idx = self.widget.domain_view.model().index(0)\n        self.widget.domain_view.setCurrentIndex(idx)\n\n        # change first attribute value\n        editor = self.widget.findChild(ContinuousVariableEditor)\n        assert isinstance(editor, ContinuousVariableEditor)\n        idx = editor.labels_model.index(0, 1)\n        editor.labels_model.setData(idx, \"False\", Qt.EditRole)\n\n        self.widget.commit()\n        t2 = self.get_output(self.widget.Outputs.data)\n        self.assertFalse(t2.domain[\"a\"].attributes[\"hidden\"])\n\n    def test_duplicate_names(self):\n        \"\"\"\n        Tests if widget shows error when duplicate name is entered.\n        And tests if widget sends None data when error is shown.\n        GH-2143\n        GH-2146\n        \"\"\"\n        table = Table(\"iris\")\n        self.send_signal(self.widget.Inputs.data, table)\n        self.assertFalse(self.widget.Error.duplicate_var_name.is_shown())\n\n        idx = self.widget.domain_view.model().index(0)\n        self.widget.domain_view.setCurrentIndex(idx)\n        editor = self.widget.findChild(ContinuousVariableEditor)\n\n        def enter_text(widget, text):\n            # type: (QLineEdit, str) -> None\n            widget.selectAll()\n            QTest.keyClick(widget, Qt.Key_Delete)\n            QTest.keyClicks(widget, text)\n            QTest.keyClick(widget, Qt.Key_Return)\n\n        enter_text(editor.name_edit, \"iris\")\n        self.widget.commit()\n        self.assertTrue(self.widget.Error.duplicate_var_name.is_shown())\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertIsNone(output)\n\n        enter_text(editor.name_edit, \"sepal height\")\n        self.widget.commit()\n        self.assertFalse(self.widget.Error.duplicate_var_name.is_shown())\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertIsInstance(output, Table)\n\n    def test_unlink(self):\n        var0, var1, var2 = [ContinuousVariable(\"x\", compute_value=Mock()),\n                            ContinuousVariable(\"y\", compute_value=Mock()),\n                            ContinuousVariable(\"z\")]\n        domain = Domain([var0, var1, var2], None)\n        table = Table.from_numpy(domain, np.zeros((5, 3)), np.zeros((5, 0)))\n        self.send_signal(self.widget.Inputs.data, table)\n\n        index = self.widget.domain_view.model().index\n        for i in range(3):\n            self.widget.domain_view.setCurrentIndex(index(i))\n            editor = self.widget.findChild(ContinuousVariableEditor)\n            self.assertIs(editor.unlink_var_cb.isEnabled(), i < 2)\n            editor._set_unlink(i == 1)\n\n        self.widget.commit()\n        out = self.get_output(self.widget.Outputs.data)\n        out0, out1, out2 = out.domain.variables\n        self.assertIs(out0, domain[0])\n        self.assertIsNot(out1, domain[1])\n        self.assertIs(out2, domain[2])\n\n        self.assertIsNotNone(out0.compute_value)\n        self.assertIsNone(out1.compute_value)\n        self.assertIsNone(out2.compute_value)\n\n    def test_time_variable_preservation(self):\n        \"\"\"Test if time variables preserve format specific attributes\"\"\"\n        table = Table(test_filename(\"datasets/cyber-security-breaches.tab\"))\n        self.send_signal(self.widget.Inputs.data, table)\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(str(table[0, 4]), str(output[0, 4]))\n        view = self.widget.variables_view\n        view.setCurrentIndex(view.model().index(4))\n\n        editor = self.widget.findChild(TimeVariableEditor)\n        editor.name_edit.setText(\"Date\")\n        editor.variable_changed.emit()\n        self.widget.commit()\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(str(table[0, 4]), str(output[0, 4]))\n\n    def test_restore(self):\n        iris = self.iris\n        viris = (\n            \"Categorical\",\n            (\"iris\", (\"Iris-setosa\", \"Iris-versicolor\", \"Iris-virginica\"), (),\n             False)\n        )\n        w = self.widget\n\n        def restore(state):\n            w._domain_change_store = state\n            w._restore()\n\n        model = w.variables_model\n        self.send_signal(w.Inputs.data, iris, widget=w)\n        restore({viris: [(\"Rename\", (\"Z\",))]})\n        tr = model.data(model.index(4), TransformRole)\n        self.assertEqual(tr, [Rename(\"Z\")])\n\n        restore({viris: [(\"AsString\", ()), (\"Rename\", (\"Z\",))]})\n        tr = model.data(model.index(4), TransformRole)\n        self.assertEqual(tr, [AsString(), Rename(\"Z\")])\n\n\nclass TestEditors(GuiTest):\n    def test_variable_editor(self):\n        w = VariableEditor()\n        self.assertEqual(w.get_data(), (None, []))\n\n        v = String(\"S\", ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        w.set_data(v, [])\n\n        self.assertEqual(w.name_edit.text(), v.name)\n        self.assertEqual(w.labels_model.get_dict(),\n                         {\"A\": \"1\", \"B\": \"b\"})\n        self.assertEqual(w.get_data(), (v, []))\n\n        w.set_data(None)\n        self.assertEqual(w.name_edit.text(), \"\")\n        self.assertEqual(w.labels_model.get_dict(), {})\n        self.assertEqual(w.get_data(), (None, []))\n\n        w.set_data(v, [Rename(\"T\"), Annotate(((\"a\", \"1\"), (\"b\", \"2\")))])\n        self.assertEqual(w.name_edit.text(), \"T\")\n        self.assertEqual(w.labels_model.rowCount(), 2)\n        add = w.findChild(QAction, \"action-add-label\")\n        add.trigger()\n        remove = w.findChild(QAction, \"action-delete-label\")\n        remove.trigger()\n\n    def test_continuous_editor(self):\n        w = ContinuousVariableEditor()\n        self.assertEqual(w.get_data(), (None, []))\n\n        v = Real(\"X\", (-1, \"\"), ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        w.set_data(v, [])\n\n        self.assertEqual(w.name_edit.text(), v.name)\n        self.assertEqual(w.labels_model.get_dict(), dict(v.annotations))\n\n        w.set_data(None)\n        self.assertEqual(w.name_edit.text(), \"\")\n        self.assertEqual(w.labels_model.get_dict(), {})\n        self.assertEqual(w.get_data(), (None, []))\n\n    def test_discrete_editor(self):\n        w = DiscreteVariableEditor()\n        self.assertEqual(w.get_data(), (None, []))\n\n        v = Categorical(\"C\", (\"a\", \"b\", \"c\"), ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        values = [0, 0, 0, 1, 1, 2]\n        w.set_data_categorical(v, values)\n\n        self.assertEqual(w.name_edit.text(), v.name)\n        self.assertEqual(w.labels_model.get_dict(), dict(v.annotations))\n        self.assertEqual(w.get_data(), (v, []))\n        w.set_data_categorical(None, None)\n        self.assertEqual(w.name_edit.text(), \"\")\n        self.assertEqual(w.labels_model.get_dict(), {})\n        self.assertEqual(w.get_data(), (None, []))\n        mapping = [\n            (\"c\", \"C\"),\n            (\"a\", \"A\"),\n            (\"b\", None),\n            (None, \"b\")\n        ]\n        w.set_data_categorical(v, values, [CategoriesMapping(mapping)])\n        w.grab()  # run delegate paint method\n        self.assertEqual(w.get_data(), (v, [CategoriesMapping(mapping)]))\n\n        # test selection/deselection in the view\n        w.set_data_categorical(v, values)\n        view = w.values_edit\n        model = view.model()\n        assert model.rowCount()\n        sel_model = view.selectionModel()\n        model = sel_model.model()\n        sel_model.select(model.index(0, 0), QItemSelectionModel.Select)\n        sel_model.select(model.index(0, 0), QItemSelectionModel.Deselect)\n\n        # merge mapping\n        mapping = [\n            (\"a\", \"a\"),\n            (\"b\", \"b\"),\n            (\"c\", \"b\")\n        ]\n        w.set_data_categorical(v, values, [CategoriesMapping(mapping)])\n        self.assertEqual(w.get_data()[1], [CategoriesMapping(mapping)])\n        self.assertEqual(model.data(model.index(0, 0), MultiplicityRole), 1)\n        self.assertEqual(model.data(model.index(1, 0), MultiplicityRole), 2)\n        self.assertEqual(model.data(model.index(2, 0), MultiplicityRole), 2)\n        w.grab()\n        model.setData(model.index(0, 0), \"b\", Qt.EditRole)\n        self.assertEqual(model.data(model.index(0, 0), MultiplicityRole), 3)\n        self.assertEqual(model.data(model.index(1, 0), MultiplicityRole), 3)\n        self.assertEqual(model.data(model.index(2, 0), MultiplicityRole), 3)\n        w.grab()\n\n    def test_discrete_editor_add_remove_action(self):\n        w = DiscreteVariableEditor()\n        v = Categorical(\"C\", (\"a\", \"b\", \"c\"),\n                        ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        values = [0, 0, 0, 1, 1, 2]\n        w.set_data_categorical(v, values)\n        action_add = w.add_new_item\n        action_remove = w.remove_item\n        view = w.values_edit\n        model, selection = view.model(), view.selectionModel()\n        selection.clear()\n\n        action_add.trigger()\n        self.assertTrue(view.state() == view.EditingState)\n        editor = view.focusWidget()\n        assert isinstance(editor, QLineEdit)\n        spy = QSignalSpy(model.dataChanged)\n        QTest.keyClick(editor, Qt.Key_D)\n        QTest.keyClick(editor, Qt.Key_Return)\n        self.assertTrue(model.rowCount() == 4)\n        # The commit to model is executed via a queued invoke\n        self.assertTrue(bool(spy) or spy.wait())\n        self.assertEqual(model.index(3, 0).data(Qt.EditRole), \"d\")\n        # remove it\n        spy = QSignalSpy(model.rowsRemoved)\n        action_remove.trigger()\n        self.assertEqual(model.rowCount(), 3)\n        self.assertEqual(len(spy), 1)\n        _, first, last = spy[0]\n        self.assertEqual((first, last), (3, 3))\n        # remove/drop and existing value\n        selection.select(model.index(1, 0), QItemSelectionModel.ClearAndSelect)\n        removespy = QSignalSpy(model.rowsRemoved)\n        changedspy = QSignalSpy(model.dataChanged)\n        action_remove.trigger()\n        self.assertEqual(len(removespy), 0, \"Should only mark item as removed\")\n        self.assertGreaterEqual(len(changedspy), 1, \"Did not change data\")\n        w.grab()\n\n    # mocking exec make dialog never show - dialog blocks running until closed\n    @patch(\n        \"Orange.widgets.data.oweditdomain.GroupItemsDialog.exec\",\n        Mock(side_effect=lambda: QDialog.Accepted)\n    )\n    def test_discrete_editor_merge_action(self):\n        \"\"\"\n        This function check whether results of dialog have effect on\n        merging the attributes. The dialog itself is tested separately.\n        \"\"\"\n        w = DiscreteVariableEditor()\n        v = Categorical(\"C\", (\"a\", \"b\", \"c\"),\n                        ((\"A\", \"1\"), (\"B\", \"b\")), False)\n\n        w.set_data_categorical(\n            v, [0, 0, 0, 1, 1, 2],\n            [CategoriesMapping([\n                (\"a\", \"AA\"), (\"b\", \"BB\"), (\"c\", \"CC\"),\n            ])]\n        )\n\n        view = w.values_edit\n        model = view.model()\n        selmodel = view.selectionModel()  # type: QItemSelectionModel\n        selmodel.select(\n            QItemSelection(model.index(0, 0), model.index(1, 0)),\n            QItemSelectionModel.ClearAndSelect\n        )\n        spy = QSignalSpy(w.variable_changed)\n        w.merge_items.trigger()\n\n        self.assertEqual(model.index(0, 0).data(Qt.EditRole), \"other\")\n        self.assertEqual(model.index(1, 0).data(Qt.EditRole), \"other\")\n        self.assertEqual(model.index(2, 0).data(Qt.EditRole), \"CC\")\n\n        self.assertSequenceEqual(\n            list(spy), [[]], 'variable_changed should emit exactly once'\n        )\n\n    def test_discrete_editor_rename_selected_items_action(self):\n        w = DiscreteVariableEditor()\n        v = Categorical(\"C\", (\"a\", \"b\", \"c\"),\n                        ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        w.set_data_categorical(v, [])\n        action = w.rename_selected_items\n        view = w.values_edit\n        model = view.model()\n        selmodel = view.selectionModel()  # type: QItemSelectionModel\n        selmodel.select(\n            QItemSelection(model.index(0, 0), model.index(1, 0)),\n            QItemSelectionModel.ClearAndSelect\n        )\n        # trigger the action, then find the active popup, and simulate entry\n        spy = QSignalSpy(w.variable_changed)\n        with patch.object(QComboBox, \"setVisible\", return_value=None) as m:\n            action.trigger()\n            m.assert_called()\n        cb = view.findChild(QComboBox)\n        cb.setCurrentText(\"BA\")\n        view.commitData(cb)\n        self.assertEqual(model.index(0, 0).data(Qt.EditRole), \"BA\")\n        self.assertEqual(model.index(1, 0).data(Qt.EditRole), \"BA\")\n        self.assertSequenceEqual(\n            list(spy), [[]], 'variable_changed should emit exactly once'\n        )\n\n    def test_discrete_editor_context_menu(self):\n        w = DiscreteVariableEditor()\n        v = Categorical(\"C\", (\"a\", \"b\", \"c\"),\n                        ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        w.set_data_categorical(v, [])\n        view = w.values_edit\n        model = view.model()\n\n        pos = view.visualRect(model.index(0, 0)).center()\n        with patch.object(QMenu, \"setVisible\", return_value=None) as m:\n            contextMenu(view.viewport(), pos)\n            m.assert_called()\n\n        menu = view.findChild(QMenu)\n        self.assertIsNotNone(menu)\n        menu.close()\n\n    def test_time_editor(self):\n        w = TimeVariableEditor()\n        self.assertEqual(w.get_data(), (None, []))\n\n        v = Time(\"T\", ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        w.set_data(v,)\n\n        self.assertEqual(w.name_edit.text(), v.name)\n        self.assertEqual(w.labels_model.get_dict(), dict(v.annotations))\n\n        w.set_data(None)\n        self.assertEqual(w.name_edit.text(), \"\")\n        self.assertEqual(w.labels_model.get_dict(), {})\n        self.assertEqual(w.get_data(), (None, []))\n\n    DataVectors = [\n        CategoricalVector(\n            Categorical(\"A\", (\"a\", \"aa\"), (), False), lambda:\n                MArray([0, 1, 2], mask=[False, False, True])\n        ),\n        RealVector(\n            Real(\"B\", (6, \"f\"), (), False), lambda:\n                MArray([0.1, 0.2, 0.3], mask=[True, False, True])\n        ),\n        TimeVector(\n            Time(\"T\", (), False), lambda:\n                MArray([0, 100, 200], dtype=\"M8[us]\", mask=[True, False, True])\n        ),\n        StringVector(\n            String(\"S\", (), False), lambda:\n                MArray([\"0\", \"1\", \"2\"], dtype=object, mask=[True, False, True])\n        ),\n    ]\n    ReinterpretTransforms = {\n        Categorical: [AsCategorical], Real: [AsContinuous],\n        Time: [AsTime, partial(StrpTime, 'Detect automatically', None, 1, 1)],\n        String: [AsString]\n    }\n\n    def test_reinterpret_editor(self):\n        w = ReinterpretVariableEditor()\n        self.assertEqual(w.get_data(), (None, []))\n        data = self.DataVectors[0]\n        w.set_data(data, )\n        self.assertEqual(w.get_data(), (data.vtype, []))\n        w.set_data(data, [Rename(\"Z\")])\n        self.assertEqual(w.get_data(), (data.vtype, [Rename(\"Z\")]))\n\n        for vec, tr in product(self.DataVectors, self.ReinterpretTransforms.values()):\n            w.set_data(vec, [t() for t in tr])\n            v, tr_ = w.get_data()\n            self.assertEqual(v, vec.vtype)\n            if not tr_:\n                self.assertEqual(tr, self.ReinterpretTransforms[type(v)])\n            else:\n                self.assertListEqual(tr_, [t() for t in tr])\n\n    def test_reinterpret_editor_simulate(self):\n        w = ReinterpretVariableEditor()\n        tc = w.findChild(QComboBox, name=\"type-combo\")\n\n        def cb():\n            var, tr = w.get_data()\n            type_ = tc.currentData()\n            if type_ is not type(var):\n                self.assertEqual(\n                    tr, [t() for t in self.ReinterpretTransforms[type_]] + [Rename(\"Z\")]\n                )\n            else:\n                self.assertEqual(tr, [Rename(\"Z\")])\n\n        for vec in self.DataVectors:\n            w.set_data(vec, [Rename(\"Z\")])\n            simulate.combobox_run_through_all(tc, callback=cb)\n\n    def test_unlink(self):\n        w = ContinuousVariableEditor()\n        cbox = w.unlink_var_cb\n        self.assertEqual(w.get_data(), (None, []))\n\n        v = Real(\"X\", (-1, \"\"), ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        w.set_data(v, [])\n        self.assertFalse(cbox.isEnabled())\n\n        v = Real(\"X\", (-1, \"\"), ((\"A\", \"1\"), (\"B\", \"b\")), True)\n        w.set_data(v, [Unlink()])\n        self.assertTrue(cbox.isEnabled())\n        self.assertTrue(cbox.isChecked())\n\n        v = Real(\"X\", (-1, \"\"), ((\"A\", \"1\"), (\"B\", \"b\")), True)\n        w.set_data(v, [])\n        self.assertTrue(cbox.isEnabled())\n        self.assertFalse(cbox.isChecked())\n\n        cbox.setChecked(True)\n        self.assertEqual(w.get_data()[1], [Unlink()])\n\n        w.set_data(v, [Unlink()])\n        self.assertTrue(cbox.isChecked())\n\n        cbox.setChecked(False)\n        self.assertEqual(w.get_data()[1], [])\n\n        cbox.setChecked(True)\n        w.clear()\n        self.assertFalse(cbox.isChecked())\n        self.assertEqual(w.get_data()[1], [])\n\n        w._set_unlink(True)\n        self.assertTrue(cbox.isChecked())\n        w._set_unlink(False)\n        self.assertFalse(cbox.isChecked())\n\n\nclass TestModels(GuiTest):\n    def test_variable_model(self):\n        model = VariableListModel()\n        self.assertEqual(model.effective_name(model.index(-1, -1)), None)\n\n        def data(row, role):\n            return model.data(model.index(row,), role)\n\n        def set_data(row, data, role):\n            model.setData(model.index(row), data, role)\n\n        model[:] = [\n            RealVector(Real(\"A\", (3, \"g\"), (), False), lambda: MArray([])),\n            RealVector(Real(\"B\", (3, \"g\"), (), False), lambda: MArray([])),\n        ]\n        self.assertEqual(data(0, Qt.DisplayRole), \"A\")\n        self.assertEqual(data(1, Qt.DisplayRole), \"B\")\n        self.assertEqual(model.effective_name(model.index(1)), \"B\")\n        set_data(1, [Rename(\"A\")], TransformRole)\n        self.assertEqual(model.effective_name(model.index(1)), \"A\")\n        self.assertEqual(data(0, MultiplicityRole), 2)\n        self.assertEqual(data(1, MultiplicityRole), 2)\n        set_data(1, [], TransformRole)\n        self.assertEqual(data(0, MultiplicityRole), 1)\n        self.assertEqual(data(1, MultiplicityRole), 1)\n\n\nclass TestDelegates(GuiTest):\n    def test_delegate(self):\n        model = VariableListModel([None, None])\n\n        def set_item(row: int, v: dict):\n            model.setItemData(model.index(row),  v)\n\n        def get_style_option(row: int) -> QStyleOptionViewItem:\n            opt = QStyleOptionViewItem()\n            delegate.initStyleOption(opt, model.index(row))\n            return opt\n\n        set_item(0, {Qt.EditRole: Categorical(\"a\", (), (), False)})\n        delegate = VariableEditDelegate()\n        opt = get_style_option(0)\n        self.assertEqual(opt.text, \"a\")\n        self.assertFalse(opt.font.italic())\n        set_item(0, {TransformRole: [Rename(\"b\")]})\n        opt = get_style_option(0)\n        self.assertEqual(opt.text, \"a \\N{RIGHTWARDS ARROW} b\")\n        self.assertTrue(opt.font.italic())\n\n        set_item(0, {TransformRole: [AsString()]})\n        opt = get_style_option(0)\n        self.assertIn(\"reinterpreted\", opt.text)\n        self.assertTrue(opt.font.italic())\n        set_item(1, {\n            Qt.EditRole: String(\"b\", (), False),\n            TransformRole: [Rename(\"a\")]\n        })\n        opt = get_style_option(1)\n        self.assertEqual(opt.palette.color(QPalette.Text), QColor(Qt.red))\n        view = QListView()\n        with patch.object(QToolTip, \"showText\") as p:\n            delegate.helpEvent(\n                QHelpEvent(QHelpEvent.ToolTip, QPoint(0, 0), QPoint(0, 0)),\n                view, opt, model.index(1),\n            )\n            p.assert_called_once()\n\n\nclass TestTransforms(TestCase):\n    def _test_common(self, var):\n        tr = [Rename(var.name + \"_copy\"), Annotate(((\"A\", \"1\"),))]\n        XX = apply_transform_var(var, tr)\n        self.assertEqual(XX.name, var.name + \"_copy\")\n        self.assertEqual(XX.attributes, {\"A\": 1})\n        self.assertIsInstance(XX.compute_value, Identity)\n        self.assertIs(XX.compute_value.variable, var)\n\n    def test_continous(self):\n        X = ContinuousVariable(\"X\")\n        self._test_common(X)\n\n    def test_string(self):\n        X = StringVariable(\"S\")\n        self._test_common(X)\n\n    def test_time(self):\n        X = TimeVariable(\"X\")\n        self._test_common(X)\n\n    def test_discrete(self):\n        D = DiscreteVariable(\"D\", values=(\"a\", \"b\"))\n        self._test_common(D)\n\n    def test_discrete_rename(self):\n        D = DiscreteVariable(\"D\", values=(\"a\", \"b\"))\n        DD = apply_transform_var(D, [CategoriesMapping(((\"a\", \"A\"), (\"b\", \"B\")))])\n        self.assertSequenceEqual(DD.values, [\"A\", \"B\"])\n        self.assertIs(DD.compute_value.variable, D)\n\n    def test_discrete_reorder(self):\n        D = DiscreteVariable(\"D\", values=(\"2\", \"3\", \"1\", \"0\"))\n        DD = apply_transform_var(D, [CategoriesMapping(((\"0\", \"0\"), (\"1\", \"1\"),\n                                                    (\"2\", \"2\"), (\"3\", \"3\")))])\n        self.assertSequenceEqual(DD.values, [\"0\", \"1\", \"2\", \"3\"])\n        self._assertLookupEquals(\n            DD.compute_value, Lookup(D, np.array([2, 3, 1, 0]))\n        )\n\n    def test_discrete_add_drop(self):\n        D = DiscreteVariable(\"D\", values=(\"2\", \"3\", \"1\", \"0\"))\n        mapping = (\n            (\"0\", None),\n            (\"1\", \"1\"),\n            (\"2\", \"2\"),\n            (\"3\", None),\n            (None, \"A\"),\n        )\n        tr = [CategoriesMapping(mapping)]\n        DD = apply_transform_var(D, tr)\n        self.assertSequenceEqual(DD.values, [\"1\", \"2\", \"A\"])\n        self._assertLookupEquals(\n            DD.compute_value, Lookup(D, np.array([1, np.nan, 0, np.nan]))\n        )\n\n    def test_discrete_merge(self):\n        D = DiscreteVariable(\"D\", values=(\"2\", \"3\", \"1\", \"0\"))\n        mapping = (\n            (\"0\", \"x\"),\n            (\"1\", \"y\"),\n            (\"2\", \"x\"),\n            (\"3\", \"y\"),\n        )\n        tr = [CategoriesMapping(mapping)]\n        DD = apply_transform_var(D, tr)\n        self.assertSequenceEqual(DD.values, [\"x\", \"y\"])\n        self._assertLookupEquals(\n            DD.compute_value, Lookup(D, np.array([0, 1, 1, 0]))\n        )\n\n    def _assertLookupEquals(self, first, second):\n        self.assertIsInstance(first, Lookup)\n        self.assertIsInstance(second, Lookup)\n        self.assertIs(first.variable, second.variable)\n        assert_array_equal(first.lookup_table, second.lookup_table)\n\n\nclass TestReinterpretTransforms(TestCase):\n    @classmethod\n    def setUpClass(cls) -> None:\n        super().setUpClass()\n        domain = Domain([\n            DiscreteVariable(\"A\", values=(\"a\", \"b\", \"c\")),\n            DiscreteVariable(\"B\", values=(\"0\", \"1\", \"2\")),\n            ContinuousVariable(\"C\"),\n            TimeVariable(\"D\", have_time=True),\n        ],\n        metas=[\n            StringVariable(\"S\")\n        ])\n        cls.data = Table.from_list(\n            domain, [\n                [0, 2, 0.25, 180],\n                [1, 1, 1.25, 360],\n                [2, 0, 0.20, 720],\n                [1, 0, 0.00, 000],\n            ]\n        )\n        cls.data_str = Table.from_list(\n            Domain([], [], metas=[\n                StringVariable(\"S\"),\n                StringVariable(\"T\")\n            ]),\n            [[\"0.1\", \"2010\"],\n             [\"1.0\", \"2020\"]]\n        )\n\n    def test_as_string(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsString()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        ttable = table.transform(Domain([], [], dtr))\n        assert_array_equal(\n            ttable.metas,\n            np.array([\n                [\"a\", \"2\", \"0.25\", \"00:03:00\"],\n                [\"b\", \"1\", \"1.25\", \"00:06:00\"],\n                [\"c\", \"0\", \"0.2\", \"00:12:00\"],\n                [\"b\", \"0\", \"0.0\", \"00:00:00\"],\n            ], dtype=object)\n        )\n\n    def test_as_discrete(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsCategorical()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        tdomain = Domain(dtr)\n        ttable = table.transform(tdomain)\n        assert_array_equal(\n            ttable.X,\n            np.array([\n                [0, 2, 2, 1],\n                [1, 1, 3, 2],\n                [2, 0, 1, 3],\n                [1, 0, 0, 0],\n            ], dtype=float)\n        )\n        self.assertEqual(tdomain[\"A\"].values, (\"a\", \"b\", \"c\"))\n        self.assertEqual(tdomain[\"B\"].values, (\"0\", \"1\", \"2\"))\n        self.assertEqual(tdomain[\"C\"].values, (\"0.0\", \"0.2\", \"0.25\", \"1.25\"))\n        self.assertEqual(\n            tdomain[\"D\"].values,\n            (\"1970-01-01 00:00:00\", \"1970-01-01 00:03:00\",\n             \"1970-01-01 00:06:00\", \"1970-01-01 00:12:00\")\n        )\n\n    def test_as_continuous(self):\n        table = self.data\n        domain = table.domain\n\n        tr = AsContinuous()\n        dtr = []\n        for v in domain.variables:\n            vtr = apply_reinterpret(v, tr, table_column_data(table, v))\n            dtr.append(vtr)\n        ttable = table.transform(Domain(dtr))\n        assert_array_equal(\n            ttable.X,\n            np.array([\n                [np.nan, 2, 0.25, 180],\n                [np.nan, 1, 1.25, 360],\n                [np.nan, 0, 0.20, 720],\n                [np.nan, 0, 0.00, 000],\n            ], dtype=float)\n        )\n\n    def test_as_time(self):\n        # this test only test type of format that can be string, continuous and discrete\n        # correctness of time formats is already tested in TimeVariable module\n        d = TimeVariable(\"_\").parse_exact_iso\n        times = (\n            [\"07.02.2022\", \"18.04.2021\"],  # date only\n            [\"07.02.2022 01:02:03\", \"18.04.2021 01:02:03\"],  # datetime\n            # datetime with timezone\n            [\"2021-02-08 01:02:03+01:00\", \"2021-02-07 01:02:03+01:00\"],\n            [\"010203\", \"010203\"],  # time\n            [\"02-07\", \"04-18\"],\n        )\n        formats = [\n            \"25.11.2021\", \"25.11.2021 00:00:00\", \"2021-11-25 00:00:00\", \"000000\", \"11-25\"\n        ]\n        expected = [\n            [d(\"2022-02-07\"), d(\"2021-04-18\")],\n            [d(\"2022-02-07 01:02:03\"), d(\"2021-04-18 01:02:03\")],\n            [d(\"2021-02-08 01:02:03+0100\"), d(\"2021-02-07 01:02:03+0100\")],\n            [d(\"01:02:03\"), d(\"01:02:03\")],\n            [d(\"1900-02-07\"), d(\"1900-04-18\")],\n        ]\n        variables = [StringVariable(f\"s{i}\") for i in range(len(times))]\n        variables += [DiscreteVariable(f\"d{i}\", values=t) for i, t in enumerate(times)]\n        domain = Domain([], metas=variables)\n        metas = [t for t in times] + [list(range(len(x))) for x in times]\n        table = Table(domain, np.empty((len(times[0]), 0)), metas=np.array(metas).transpose())\n\n        tr = AsTime()\n        dtr = []\n        for v, f in zip(domain.metas, chain(formats, formats)):\n            strp = StrpTime(f, *TimeVariable.ADDITIONAL_FORMATS[f])\n            vtr = apply_transform_var(\n                apply_reinterpret(v, tr, table_column_data(table, v)), [strp]\n            )\n            dtr.append(vtr)\n\n        ttable = table.transform(Domain([], metas=dtr))\n        assert_array_equal(\n            ttable.metas,\n            np.array(list(chain(expected, expected)), dtype=float).transpose()\n        )\n\n    def test_raise_pandas_version(self):\n        \"\"\"\n        When this test start to fail:\n        - remove this test\n        - remove if clause in datetime_to_epoch function and supporting comments\n        - set pandas dependency version to pandas>=1.4\n        \"\"\"\n        from datetime import datetime\n        self.assertLess(datetime.today(), datetime(2023, 1, 1))\n\n    def test_reinterpret_string(self):\n        table = self.data_str\n        domain = table.domain\n        tvars = []\n        for v in domain.metas:\n            for i, tr in enumerate(\n                [AsContinuous(), AsCategorical(), AsTime(), AsString()]\n            ):\n                vtr = apply_reinterpret(v, tr, table_column_data(table, v)).renamed(\n                    f\"{v.name}_{i}\"\n                )\n                if isinstance(tr, AsTime):\n                    strp = StrpTime(\"Detect automatically\", None, 1, 1)\n                    vtr = apply_transform_var(vtr, [strp])\n                tvars.append(vtr)\n        tdomain = Domain([], metas=tvars)\n        ttable = table.transform(tdomain)\n        assert_array_nanequal(\n            ttable.metas,\n            np.array([\n                [0.1, 0., np.nan, \"0.1\", 2010., 0., 1262304000., \"2010\"],\n                [1.0, 1., np.nan, \"1.0\", 2020., 1., 1577836800., \"2020\"],\n            ], dtype=object)\n        )\n\n    def test_compound_transform(self):\n        table = self.data_str\n        domain = table.domain\n        v1 = domain.metas[0]\n        v1.attributes[\"a\"] = \"a\"\n        tv1 = apply_transform(v1, table, [AsContinuous(), Rename(\"Z1\")])\n        tv2 = apply_transform(v1, table, [AsContinuous(), Rename(\"Z2\"), Annotate(((\"a\", \"b\"),))])\n\n        self.assertIsInstance(tv1, ContinuousVariable)\n        self.assertEqual(tv1.name, \"Z1\")\n        self.assertEqual(tv1.attributes, {\"a\": \"a\"})\n\n        self.assertIsInstance(tv2, ContinuousVariable)\n        self.assertEqual(tv2.name, \"Z2\")\n        self.assertEqual(tv2.attributes, {\"a\": \"b\"})\n\n        tdomain = Domain([], metas=[tv1, tv2])\n        ttable = table.transform(tdomain)\n\n        assert_array_nanequal(\n            ttable.metas,\n            np.array([\n                [0.1, 0.1],\n                [1.0, 1.0],\n            ], dtype=object)\n        )\n\n    def test_null_transform(self):\n        table = self.data_str\n        domain = table.domain\n        v = apply_transform(domain.metas[0],table, [])\n        self.assertIs(v, domain.metas[0])\n\n\nclass TestUtils(TestCase):\n    def test_mapper(self):\n        mapper = make_dict_mapper({\"a\": 1, \"b\": 2})\n        r = mapper([\"a\", \"a\", \"b\"])\n        assert_array_equal(r, [1, 1, 2])\n        self.assertEqual(r.dtype, np.dtype(\"O\"))\n        r = mapper([\"a\", \"a\", \"b\"], dtype=float)\n        assert_array_equal(r, [1, 1, 2])\n        self.assertEqual(r.dtype, np.dtype(float))\n        r = mapper([\"a\", \"a\", \"b\"], dtype=int)\n        self.assertEqual(r.dtype, np.dtype(int))\n\n        mapper = make_dict_mapper({\"a\": 1, \"b\": 2}, dtype=int)\n        r = mapper([\"a\", \"a\", \"b\"])\n        self.assertEqual(r.dtype, np.dtype(int))\n\n        r = np.full(3, -1, dtype=float)\n        r_ = mapper([\"a\", \"a\", \"b\"], out=r)\n        self.assertIs(r, r_)\n        assert_array_equal(r, [1, 1, 2])\n\n    def test_as_float_or_nan(self):\n        a = np.array([\"a\", \"1.1\", \".2\", \"NaN\"], object)\n        r = as_float_or_nan(a)\n        assert_array_equal(r, [np.nan, 1.1, .2, np.nan])\n\n        a = np.array([1, 2, 3], dtype=int)\n        r = as_float_or_nan(a)\n        assert_array_equal(r, [1., 2., 3.])\n\n        r = as_float_or_nan(r, dtype=np.float32)\n        assert_array_equal(r, [1., 2., 3.])\n        self.assertEqual(r.dtype, np.dtype(np.float32))\n\n    def test_column_str_repr(self):\n        v = StringVariable(\"S\")\n        d = column_str_repr(v, np.array([\"A\", \"\", \"B\"]))\n        assert_array_equal(d, [\"A\", \"?\", \"B\"])\n        v = ContinuousVariable(\"C\")\n        d = column_str_repr(v, np.array([0.1, np.nan, 1.0]))\n        assert_array_equal(d, [\"0.1\", \"?\", \"1\"])\n        v = DiscreteVariable(\"D\", (\"a\", \"b\"))\n        d = column_str_repr(v, np.array([0., np.nan, 1.0]))\n        assert_array_equal(d, [\"a\", \"?\", \"b\"])\n        v = TimeVariable(\"T\", have_date=False, have_time=True)\n        d = column_str_repr(v, np.array([0., np.nan, 1.0]))\n        assert_array_equal(d, [\"00:00:00\", \"?\", \"00:00:01\"])\n\n\nclass TestLookupMappingTransform(TestCase):\n    def setUp(self) -> None:\n        self.lookup = LookupMappingTransform(\n            StringVariable(\"S\"),\n            {\"\": np.nan, \"a\": 0, \"b\": 1},\n            dtype=float,\n        )\n\n    def test_transform(self):\n        r = self.lookup.transform(np.array([\"\", \"a\", \"b\", \"c\"]))\n        assert_array_equal(r, [np.nan, 0, 1, np.nan])\n\n    def test_pickle(self):\n        lookup = self.lookup\n        lookup_ = pickle.loads(pickle.dumps(lookup))\n        c = np.array([\"\", \"a\", \"b\", \"c\"])\n        r = lookup.transform(c)\n        assert_array_equal(r, [np.nan, 0, 1, np.nan])\n        r_ = lookup_.transform(c)\n        assert_array_equal(r_, [np.nan, 0, 1, np.nan])\n\n    def test_equality(self):\n        v1 = DiscreteVariable(\"v1\", values=tuple(\"abc\"))\n        v2 = DiscreteVariable(\"v1\", values=tuple(\"abc\"))\n        v3 = DiscreteVariable(\"v3\", values=tuple(\"abc\"))\n\n        map1 = {\"a\": 2, \"b\": 0, \"c\": 1}\n        map2 = {\"a\": 2, \"b\": 0, \"c\": 1}\n        map3 = {\"a\": 2, \"b\": 0, \"c\": 1}\n\n        t1 = LookupMappingTransform(v1, map1, float)\n        t1a = LookupMappingTransform(v2, map2, float)\n        t2 = LookupMappingTransform(v3, map3, float)\n        self.assertEqual(t1, t1)\n        self.assertEqual(t1, t1a)\n        self.assertNotEqual(t1, t2)\n\n        self.assertEqual(hash(t1), hash(t1a))\n        self.assertNotEqual(hash(t1), hash(t2))\n\n        map1a = {\"a\": 2, \"b\": 1, \"c\": 0}\n        t1 = LookupMappingTransform(v1, map1, float)\n        t1a = LookupMappingTransform(v1, map1a, float)\n        self.assertNotEqual(t1, t1a)\n        self.assertNotEqual(hash(t1), hash(t1a))\n\n        map1a = {\"a\": 2, \"b\": 0, \"c\": 1}\n        t1 = LookupMappingTransform(v1, map1, float)\n        t1a = LookupMappingTransform(v1, map1a, float, unknown=2)\n        self.assertNotEqual(t1, t1a)\n        self.assertNotEqual(hash(t1), hash(t1a))\n\n        t1 = LookupMappingTransform(v1, map1, float)\n        t1a = LookupMappingTransform(v1, map1, int)\n        self.assertNotEqual(t1, t1a)\n        self.assertNotEqual(hash(t1), hash(t1a))\n\n\nclass TestGroupLessFrequentItemsDialog(GuiTest):\n    def setUp(self) -> None:\n        self.v = Categorical(\"C\", (\"a\", \"b\", \"c\"),\n                        ((\"A\", \"1\"), (\"B\", \"b\")), False)\n        self.data = [0, 0, 0, 1, 1, 2]\n\n    def test_dialog_open(self):\n        dialog = GroupItemsDialog(self.v, self.data, [\"a\", \"b\"], {})\n        self.assertTrue(dialog.selected_radio.isChecked())\n        self.assertFalse(dialog.frequent_abs_radio.isChecked())\n        self.assertFalse(dialog.frequent_rel_radio.isChecked())\n        self.assertFalse(dialog.n_values_radio.isChecked())\n\n        dialog = GroupItemsDialog(self.v, self.data, [], {})\n        self.assertFalse(dialog.selected_radio.isChecked())\n        self.assertTrue(dialog.frequent_abs_radio.isChecked())\n        self.assertFalse(dialog.frequent_rel_radio.isChecked())\n        self.assertFalse(dialog.n_values_radio.isChecked())\n\n    def test_group_selected(self):\n        dialog = GroupItemsDialog(self.v, self.data, [\"a\", \"b\"], {})\n        dialog.selected_radio.setChecked(True)\n        dialog.new_name_line_edit.setText(\"BA\")\n\n        self.assertListEqual(dialog.get_merge_attributes(), [\"a\", \"b\"])\n        self.assertEqual(dialog.get_merged_value_name(), \"BA\")\n\n    def test_group_less_frequent_abs(self):\n        dialog = GroupItemsDialog(self.v, self.data, [\"a\", \"b\"], {})\n        dialog.frequent_abs_radio.setChecked(True)\n        dialog.frequent_abs_spin.setValue(3)\n        dialog.new_name_line_edit.setText(\"BA\")\n\n        self.assertListEqual(dialog.get_merge_attributes(), [\"b\", \"c\"])\n        self.assertEqual(dialog.get_merged_value_name(), \"BA\")\n\n        dialog.frequent_abs_spin.setValue(2)\n        self.assertListEqual(dialog.get_merge_attributes(), [\"c\"])\n\n        dialog.frequent_abs_spin.setValue(1)\n        self.assertListEqual(dialog.get_merge_attributes(), [])\n\n    def test_group_less_frequent_rel(self):\n        dialog = GroupItemsDialog(self.v, self.data, [\"a\", \"b\"], {})\n        dialog.frequent_rel_radio.setChecked(True)\n        dialog.frequent_rel_spin.setValue(50)\n        dialog.new_name_line_edit.setText(\"BA\")\n\n        self.assertListEqual(dialog.get_merge_attributes(), [\"b\", \"c\"])\n        self.assertEqual(dialog.get_merged_value_name(), \"BA\")\n\n        dialog.frequent_rel_spin.setValue(20)\n        self.assertListEqual(dialog.get_merge_attributes(), [\"c\"])\n\n        dialog.frequent_rel_spin.setValue(15)\n        self.assertListEqual(dialog.get_merge_attributes(), [])\n\n    def test_group_keep_n(self):\n        dialog = GroupItemsDialog(self.v, self.data, [\"a\", \"b\"], {})\n        dialog.n_values_radio.setChecked(True)\n        dialog.n_values_spin.setValue(1)\n        dialog.new_name_line_edit.setText(\"BA\")\n\n        self.assertListEqual(dialog.get_merge_attributes(), [\"b\", \"c\"])\n        self.assertEqual(dialog.get_merged_value_name(), \"BA\")\n\n        dialog.n_values_spin.setValue(2)\n        self.assertListEqual(dialog.get_merge_attributes(), [\"c\"])\n\n        dialog.n_values_spin.setValue(3)\n        self.assertListEqual(dialog.get_merge_attributes(), [])\n\n    def test_group_less_frequent_missing(self):\n        \"\"\"\n        Widget gives MaskedArray to GroupItemsDialog which can have missing\n        values.\n        gh-4599\n        \"\"\"\n        def _test_correctness():\n            dialog.frequent_abs_radio.setChecked(True)\n            dialog.frequent_abs_spin.setValue(3)\n            self.assertListEqual(dialog.get_merge_attributes(), [\"b\", \"c\"])\n\n            dialog.frequent_rel_radio.setChecked(True)\n            dialog.frequent_rel_spin.setValue(50)\n            self.assertListEqual(dialog.get_merge_attributes(), [\"b\", \"c\"])\n\n            dialog.n_values_radio.setChecked(True)\n            dialog.n_values_spin.setValue(1)\n            self.assertListEqual(dialog.get_merge_attributes(), [\"b\", \"c\"])\n\n        # masked array\n        data_masked = np.ma.array(\n            [0, 0, np.nan, 0, 1, 1, 2], mask=[0, 0, 1, 0, 0, 0, 0]\n        )\n        dialog = GroupItemsDialog(self.v, data_masked, [], {})\n        _test_correctness()\n\n        data_array = np.array([0, 0, np.nan, 0, 1, 1, 2])\n        dialog = GroupItemsDialog(self.v, data_array, [], {})\n        _test_correctness()\n\n        data_list = [0, 0, None, 0, 1, 1, 2]\n        dialog = GroupItemsDialog(self.v, data_list, [], {})\n        _test_correctness()\n\n\nif __name__ == '__main__':\n    unittest.main()\n"
    ],
    "test_file_names": [
        "test_1.py"
    ],
    "setup_res_code": 0,
    "setup_res_stdout": "Starting Orange installation attempts...\nAttempting installation with Python 3.7...\nrunning build_ext\nCompiling Orange/classification/_tree_scorers.pyx because it changed.\nCompiling Orange/data/_contingency.pyx because it changed.\nCompiling Orange/data/_io.pyx because it changed.\nCompiling Orange/data/_valuecount.pyx because it changed.\nCompiling Orange/data/_variable.pyx because it changed.\nCompiling Orange/distance/_distance.pyx because it changed.\nCompiling Orange/preprocess/_discretize.pyx because it changed.\nCompiling Orange/preprocess/_relieff.pyx because it changed.\nCompiling Orange/projection/_som.pyx because it changed.\n[1/9] Cythonizing Orange/classification/_tree_scorers.pyx\n[2/9] Cythonizing Orange/data/_contingency.pyx\n[3/9] Cythonizing Orange/data/_io.pyx\n[4/9] Cythonizing Orange/data/_valuecount.pyx\n[5/9] Cythonizing Orange/data/_variable.pyx\n[6/9] Cythonizing Orange/distance/_distance.pyx\n[7/9] Cythonizing Orange/preprocess/_discretize.pyx\n[8/9] Cythonizing Orange/preprocess/_relieff.pyx\n[9/9] Cythonizing Orange/projection/_som.pyx\nbuilding 'Orange.classification._tree_scorers' extension\ncreating build\ncreating build/temp.linux-x86_64-3.7\ncreating build/temp.linux-x86_64-3.7/Orange\ncreating build/temp.linux-x86_64-3.7/Orange/classification\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/classification/_tree_scorers.c -o build/temp.linux-x86_64-3.7/Orange/classification/_tree_scorers.o\ncreating build/lib.linux-x86_64-3.7\ncreating build/lib.linux-x86_64-3.7/Orange\ncreating build/lib.linux-x86_64-3.7/Orange/classification\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/classification/_tree_scorers.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/classification/_tree_scorers.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._contingency' extension\ncreating build/temp.linux-x86_64-3.7/Orange/data\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/data/_contingency.c -o build/temp.linux-x86_64-3.7/Orange/data/_contingency.o\ncreating build/lib.linux-x86_64-3.7/Orange/data\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_contingency.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/data/_contingency.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._io' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/data/_io.c -o build/temp.linux-x86_64-3.7/Orange/data/_io.o\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_io.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/data/_io.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._valuecount' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/data/_valuecount.c -o build/temp.linux-x86_64-3.7/Orange/data/_valuecount.o\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_valuecount.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/data/_valuecount.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._variable' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/data/_variable.c -o build/temp.linux-x86_64-3.7/Orange/data/_variable.o\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/data/_variable.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/data/_variable.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.distance._distance' extension\ncreating build/temp.linux-x86_64-3.7/Orange/distance\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/distance/_distance.c -o build/temp.linux-x86_64-3.7/Orange/distance/_distance.o\ncreating build/lib.linux-x86_64-3.7/Orange/distance\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/distance/_distance.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/distance/_distance.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._discretize' extension\ncreating build/temp.linux-x86_64-3.7/Orange/preprocess\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/preprocess/_discretize.c -o build/temp.linux-x86_64-3.7/Orange/preprocess/_discretize.o\ncreating build/lib.linux-x86_64-3.7/Orange/preprocess\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/preprocess/_discretize.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/preprocess/_discretize.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._relieff' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/preprocess/_relieff.cpp -o build/temp.linux-x86_64-3.7/Orange/preprocess/_relieff.o\nc++ -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/preprocess/_relieff.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/preprocess/_relieff.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.projection._som' extension\ncreating build/temp.linux-x86_64-3.7/Orange/projection\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/projection/_som.c -o build/temp.linux-x86_64-3.7/Orange/projection/_som.o\ncreating build/lib.linux-x86_64-3.7/Orange/projection\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/projection/_som.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/projection/_som.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.classification._simple_tree' extension\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/classification/_simple_tree.c -o build/temp.linux-x86_64-3.7/Orange/classification/_simple_tree.o\ncc -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/classification/_simple_tree.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/classification/_simple_tree.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.widgets.utils._grid_density' extension\ncreating build/temp.linux-x86_64-3.7/Orange/widgets\ncreating build/temp.linux-x86_64-3.7/Orange/widgets/utils\ncc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c Orange/widgets/utils/_grid_density.cpp -o build/temp.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.o\ncreating build/lib.linux-x86_64-3.7/Orange/widgets\ncreating build/lib.linux-x86_64-3.7/Orange/widgets/utils\nc++ -pthread -shared -L/tools/deps/lib build/temp.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.o -L/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/lib -lm -lpython3.7m -o build/lib.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.cpython-37m-x86_64-linux-gnu.so\ncopying build/lib.linux-x86_64-3.7/Orange/classification/_tree_scorers.cpython-37m-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-3.7/Orange/data/_contingency.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_io.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_valuecount.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_variable.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/distance/_distance.cpython-37m-x86_64-linux-gnu.so -> Orange/distance\ncopying build/lib.linux-x86_64-3.7/Orange/preprocess/_discretize.cpython-37m-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-3.7/Orange/preprocess/_relieff.cpython-37m-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-3.7/Orange/projection/_som.cpython-37m-x86_64-linux-gnu.so -> Orange/projection\ncopying build/lib.linux-x86_64-3.7/Orange/classification/_simple_tree.cpython-37m-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.cpython-37m-x86_64-linux-gnu.so -> Orange/widgets/utils\nrunning develop\nrunning egg_info\ncreating Orange3.egg-info\nwriting Orange3.egg-info/PKG-INFO\nwriting dependency_links to Orange3.egg-info/dependency_links.txt\nwriting entry points to Orange3.egg-info/entry_points.txt\nwriting requirements to Orange3.egg-info/requires.txt\nwriting top-level names to Orange3.egg-info/top_level.txt\nwriting manifest file 'Orange3.egg-info/SOURCES.txt'\nreading manifest file 'Orange3.egg-info/SOURCES.txt'\nreading manifest template 'MANIFEST.in'\nadding license file 'LICENSE'\nwriting manifest file 'Orange3.egg-info/SOURCES.txt'\nrunning build_ext\ncopying build/lib.linux-x86_64-3.7/Orange/classification/_tree_scorers.cpython-37m-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-3.7/Orange/data/_contingency.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_io.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_valuecount.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/data/_variable.cpython-37m-x86_64-linux-gnu.so -> Orange/data\ncopying build/lib.linux-x86_64-3.7/Orange/distance/_distance.cpython-37m-x86_64-linux-gnu.so -> Orange/distance\ncopying build/lib.linux-x86_64-3.7/Orange/preprocess/_discretize.cpython-37m-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-3.7/Orange/preprocess/_relieff.cpython-37m-x86_64-linux-gnu.so -> Orange/preprocess\ncopying build/lib.linux-x86_64-3.7/Orange/projection/_som.cpython-37m-x86_64-linux-gnu.so -> Orange/projection\ncopying build/lib.linux-x86_64-3.7/Orange/classification/_simple_tree.cpython-37m-x86_64-linux-gnu.so -> Orange/classification\ncopying build/lib.linux-x86_64-3.7/Orange/widgets/utils/_grid_density.cpython-37m-x86_64-linux-gnu.so -> Orange/widgets/utils\nCreating /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/Orange3.egg-link (link to .)\nAdding Orange3 3.33.0.dev0+7b40090 to easy-install.pth file\nInstalling orange-canvas script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab\nProcessing dependencies for Orange3==3.33.0.dev0+7b40090\nSearching for XlsxWriter==3.2.0\nBest match: XlsxWriter 3.2.0\nAdding XlsxWriter 3.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for xlrd==2.0.1\nBest match: xlrd 2.0.1\nAdding xlrd 2.0.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for setuptools==59.8.0\nBest match: setuptools 59.8.0\nAdding setuptools 59.8.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for serverfiles==0.3.1\nBest match: serverfiles 0.3.1\nAdding serverfiles 0.3.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for scipy==1.7.3\nBest match: scipy 1.7.3\nAdding scipy 1.7.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for scikit-learn==1.0.2\nBest match: scikit-learn 1.0.2\nAdding scikit-learn 1.0.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for requests==2.31.0\nBest match: requests 2.31.0\nAdding requests 2.31.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for qtconsole==5.4.4\nBest match: qtconsole 5.4.4\nAdding qtconsole 5.4.4 to easy-install.pth file\nInstalling jupyter-qtconsole script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for PyYAML==6.0.1\nBest match: PyYAML 6.0.1\nAdding PyYAML 6.0.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for python-louvain==0.16\nBest match: python-louvain 0.16\nAdding python-louvain 0.16 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for pyqtgraph==0.12.3\nBest match: pyqtgraph 0.12.3\nAdding pyqtgraph 0.12.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for pygments==2.17.2\nBest match: pygments 2.17.2\nAdding pygments 2.17.2 to easy-install.pth file\nInstalling pygmentize script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for pip==24.0\nBest match: pip 24.0\nAdding pip 24.0 to easy-install.pth file\nInstalling pip script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\nInstalling pip3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\nInstalling pip3.10 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for pandas==1.3.5\nBest match: pandas 1.3.5\nAdding pandas 1.3.5 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for orange-widget-base==4.22.0\nBest match: orange-widget-base 4.22.0\nAdding orange-widget-base 4.22.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for orange-canvas-core==0.1.35\nBest match: orange-canvas-core 0.1.35\nAdding orange-canvas-core 0.1.35 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for openpyxl==3.1.3\nBest match: openpyxl 3.1.3\nAdding openpyxl 3.1.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for openTSNE==1.0.1\nBest match: openTSNE 1.0.1\nAdding openTSNE 1.0.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for numpy==1.17.5\nBest match: numpy 1.17.5\nAdding numpy 1.17.5 to easy-install.pth file\nInstalling f2py script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\nInstalling f2py3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\nInstalling f2py3.7 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for networkx==2.6.3\nBest match: networkx 2.6.3\nAdding networkx 2.6.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for matplotlib==3.5.3\nBest match: matplotlib 3.5.3\nAdding matplotlib 3.5.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for keyrings.alt==4.2.0\nBest match: keyrings.alt 4.2.0\nAdding keyrings.alt 4.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for keyring==24.1.1\nBest match: keyring 24.1.1\nAdding keyring 24.1.1 to easy-install.pth file\nInstalling keyring script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for joblib==1.3.2\nBest match: joblib 1.3.2\nAdding joblib 1.3.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for httpx==0.24.1\nBest match: httpx 0.24.1\nAdding httpx 0.24.1 to easy-install.pth file\nInstalling httpx script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for chardet==5.2.0\nBest match: chardet 5.2.0\nAdding chardet 5.2.0 to easy-install.pth file\nInstalling chardetect script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for Bottleneck==1.4.0\nBest match: Bottleneck 1.4.0\nAdding Bottleneck 1.4.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for baycomp==1.0.3\nBest match: baycomp 1.0.3\nAdding baycomp 1.0.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for PyQtWebEngine==5.15.6\nBest match: PyQtWebEngine 5.15.6\nAdding PyQtWebEngine 5.15.6 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for PyQt5==5.15.10\nBest match: PyQt5 5.15.10\nAdding PyQt5 5.15.10 to easy-install.pth file\nInstalling pylupdate5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\nInstalling pyrcc5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\nInstalling pyuic5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for AnyQt==0.2.0\nBest match: AnyQt 0.2.0\nAdding AnyQt 0.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for threadpoolctl==3.1.0\nBest match: threadpoolctl 3.1.0\nAdding threadpoolctl 3.1.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for idna==3.10\nBest match: idna 3.10\nAdding idna 3.10 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for charset-normalizer==3.4.1\nBest match: charset-normalizer 3.4.1\nAdding charset-normalizer 3.4.1 to easy-install.pth file\nInstalling normalizer script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for urllib3==2.0.7\nBest match: urllib3 2.0.7\nAdding urllib3 2.0.7 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for certifi==2024.12.14\nBest match: certifi 2024.12.14\nAdding certifi 2024.12.14 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for traitlets==5.9.0\nBest match: traitlets 5.9.0\nAdding traitlets 5.9.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for ipykernel==6.16.2\nBest match: ipykernel 6.16.2\nAdding ipykernel 6.16.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for QtPy==2.4.2\nBest match: QtPy 2.4.2\nAdding QtPy 2.4.2 to easy-install.pth file\nInstalling qtpy script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for packaging==24.0\nBest match: packaging 24.0\nAdding packaging 24.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for jupyter-core==4.12.0\nBest match: jupyter-core 4.12.0\nAdding jupyter-core 4.12.0 to easy-install.pth file\nInstalling jupyter script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\nInstalling jupyter-migrate script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\nInstalling jupyter-troubleshoot script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for jupyter-client==7.4.9\nBest match: jupyter-client 7.4.9\nAdding jupyter-client 7.4.9 to easy-install.pth file\nInstalling jupyter-kernel script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\nInstalling jupyter-kernelspec script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\nInstalling jupyter-run script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for pyzmq==26.2.0\nBest match: pyzmq 26.2.0\nAdding pyzmq 26.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for ipython-genutils==0.2.0\nBest match: ipython-genutils 0.2.0\nAdding ipython-genutils 0.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for pytz==2024.2\nBest match: pytz 2024.2\nAdding pytz 2024.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for python-dateutil==2.9.0.post0\nBest match: python-dateutil 2.9.0.post0\nAdding python-dateutil 2.9.0.post0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for typing-extensions==4.7.1\nBest match: typing-extensions 4.7.1\nAdding typing-extensions 4.7.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for commonmark==0.9.1\nBest match: commonmark 0.9.1\nAdding commonmark 0.9.1 to easy-install.pth file\nInstalling cmark script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for qasync==0.25.0\nBest match: qasync 0.25.0\nAdding qasync 0.25.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for requests-cache==1.1.1\nBest match: requests-cache 1.1.1\nAdding requests-cache 1.1.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for docutils==0.20.1\nBest match: docutils 0.20.1\nAdding docutils 0.20.1 to easy-install.pth file\nInstalling docutils script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for dictdiffer==0.9.0\nBest match: dictdiffer 0.9.0\nAdding dictdiffer 0.9.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for importlib-metadata==6.7.0\nBest match: importlib-metadata 6.7.0\nAdding importlib-metadata 6.7.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for et-xmlfile==1.1.0\nBest match: et-xmlfile 1.1.0\nAdding et-xmlfile 1.1.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for pyparsing==3.1.4\nBest match: pyparsing 3.1.4\nAdding pyparsing 3.1.4 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for cycler==0.11.0\nBest match: cycler 0.11.0\nAdding cycler 0.11.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for kiwisolver==1.4.5\nBest match: kiwisolver 1.4.5\nAdding kiwisolver 1.4.5 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for Pillow==9.5.0\nBest match: Pillow 9.5.0\nAdding Pillow 9.5.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for fonttools==4.38.0\nBest match: fonttools 4.38.0\nAdding fonttools 4.38.0 to easy-install.pth file\nInstalling fonttools script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\nInstalling pyftmerge script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\nInstalling pyftsubset script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\nInstalling ttx script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for jaraco.classes==3.2.3\nBest match: jaraco.classes 3.2.3\nAdding jaraco.classes 3.2.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for importlib-resources==5.12.0\nBest match: importlib-resources 5.12.0\nAdding importlib-resources 5.12.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for jeepney==0.8.0\nBest match: jeepney 0.8.0\nAdding jeepney 0.8.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for SecretStorage==3.3.3\nBest match: SecretStorage 3.3.3\nAdding SecretStorage 3.3.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for httpcore==0.17.3\nBest match: httpcore 0.17.3\nAdding httpcore 0.17.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for sniffio==1.3.1\nBest match: sniffio 1.3.1\nAdding sniffio 1.3.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for PyQt5-sip==12.13.0\nBest match: PyQt5-sip 12.13.0\nAdding PyQt5-sip 12.13.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for PyQtWebEngine-Qt5==5.15.16\nBest match: PyQtWebEngine-Qt5 5.15.16\nAdding PyQtWebEngine-Qt5 5.15.16 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for PyQt5-Qt5==5.15.16\nBest match: PyQt5-Qt5 5.15.16\nAdding PyQt5-Qt5 5.15.16 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for debugpy==1.7.0\nBest match: debugpy 1.7.0\nAdding debugpy 1.7.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for ipython==7.34.0\nBest match: ipython 7.34.0\nAdding ipython 7.34.0 to easy-install.pth file\nInstalling iptest script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\nInstalling iptest3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\nInstalling ipython script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\nInstalling ipython3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for tornado==6.2\nBest match: tornado 6.2\nAdding tornado 6.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for matplotlib-inline==0.1.6\nBest match: matplotlib-inline 0.1.6\nAdding matplotlib-inline 0.1.6 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for psutil==6.1.1\nBest match: psutil 6.1.1\nAdding psutil 6.1.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for nest-asyncio==1.6.0\nBest match: nest-asyncio 1.6.0\nAdding nest-asyncio 1.6.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for entrypoints==0.4\nBest match: entrypoints 0.4\nAdding entrypoints 0.4 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for six==1.17.0\nBest match: six 1.17.0\nAdding six 1.17.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for url-normalize==1.4.3\nBest match: url-normalize 1.4.3\nAdding url-normalize 1.4.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for attrs==24.2.0\nBest match: attrs 24.2.0\nAdding attrs 24.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for cattrs==23.1.2\nBest match: cattrs 23.1.2\nAdding cattrs 23.1.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for platformdirs==4.0.0\nBest match: platformdirs 4.0.0\nAdding platformdirs 4.0.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for zipp==3.15.0\nBest match: zipp 3.15.0\nAdding zipp 3.15.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for more-itertools==9.1.0\nBest match: more-itertools 9.1.0\nAdding more-itertools 9.1.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for cryptography==44.0.0\nBest match: cryptography 44.0.0\nAdding cryptography 44.0.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for h11==0.14.0\nBest match: h11 0.14.0\nAdding h11 0.14.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for anyio==3.7.1\nBest match: anyio 3.7.1\nAdding anyio 3.7.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for pexpect==4.9.0\nBest match: pexpect 4.9.0\nAdding pexpect 4.9.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for prompt-toolkit==3.0.48\nBest match: prompt-toolkit 3.0.48\nAdding prompt-toolkit 3.0.48 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for decorator==5.1.1\nBest match: decorator 5.1.1\nAdding decorator 5.1.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for backcall==0.2.0\nBest match: backcall 0.2.0\nAdding backcall 0.2.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for jedi==0.19.2\nBest match: jedi 0.19.2\nAdding jedi 0.19.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for pickleshare==0.7.5\nBest match: pickleshare 0.7.5\nAdding pickleshare 0.7.5 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for exceptiongroup==1.2.2\nBest match: exceptiongroup 1.2.2\nAdding exceptiongroup 1.2.2 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for cffi==1.15.1\nBest match: cffi 1.15.1\nAdding cffi 1.15.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for ptyprocess==0.7.0\nBest match: ptyprocess 0.7.0\nAdding ptyprocess 0.7.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for wcwidth==0.2.13\nBest match: wcwidth 0.2.13\nAdding wcwidth 0.2.13 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for parso==0.8.4\nBest match: parso 0.8.4\nAdding parso 0.8.4 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nSearching for pycparser==2.21\nBest match: pycparser 2.21\nAdding pycparser 2.21 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages\nFinished processing dependencies for Orange3==3.33.0.dev0+7b40090\nVerifying Orange installation...\n✅ Orange installation successful!\nSuccessfully installed orange using Python 3.7\n",
    "setup_res_stderr": "Using CPython 3.7.9\nCreating virtual environment at: .venv\nActivate with: source .venv/bin/activate\nResolved 18 packages in 101ms\nPrepared 18 packages in 8ms\nInstalled 18 packages in 78ms\n + cython==0.29.37\n + exceptiongroup==1.2.2\n + importlib-metadata==6.7.0\n + iniconfig==2.0.0\n + numpy==1.17.5\n + packaging==24.0\n + pluggy==1.2.0\n + pyqt5==5.15.10\n + pyqt5-qt5==5.15.16\n + pyqt5-sip==12.13.0\n + pyqtwebengine==5.15.6\n + pyqtwebengine-qt5==5.15.16\n + pytest==7.4.4\n + setuptools==59.8.0\n + tomli==2.0.1\n + typing-extensions==4.7.1\n + wheel==0.42.0\n + zipp==3.15.0\nResolved 54 packages in 390ms\nInstalled 47 packages in 884ms\n + anyio==3.7.1\n + baycomp==1.0.3\n + bottleneck==1.4.0\n + certifi==2024.12.14\n + cffi==1.15.1\n + chardet==5.2.0\n + charset-normalizer==3.4.1\n + cryptography==44.0.0\n + cycler==0.11.0\n + et-xmlfile==1.1.0\n + fonttools==4.38.0\n + h11==0.14.0\n + httpcore==0.17.3\n + httpx==0.24.1\n + idna==3.10\n + importlib-resources==5.12.0\n + jaraco-classes==3.2.3\n + jeepney==0.8.0\n + joblib==1.3.2\n + keyring==24.1.1\n + keyrings-alt==4.2.0\n + kiwisolver==1.4.5\n + matplotlib==3.5.3\n + more-itertools==9.1.0\n + networkx==2.6.3\n + openpyxl==3.1.3\n + opentsne==1.0.1\n + pandas==1.3.5\n + pillow==9.5.0\n + pip==24.0\n + pycparser==2.21\n + pyparsing==3.1.4\n + python-dateutil==2.9.0.post0\n + python-louvain==0.16\n + pytz==2024.2\n + pyyaml==6.0.1\n + requests==2.31.0\n + scikit-learn==1.0.2\n + scipy==1.7.3\n + secretstorage==3.3.3\n + serverfiles==0.3.1\n + six==1.17.0\n + sniffio==1.3.1\n + threadpoolctl==3.1.0\n + urllib3==2.0.7\n + xlrd==2.0.1\n + xlsxwriter==3.2.0\nResolved 59 packages in 359ms\nInstalled 38 packages in 296ms\n + anyqt==0.2.0\n + attrs==24.2.0\n + backcall==0.2.0\n + cattrs==23.1.2\n + commonmark==0.9.1\n + debugpy==1.7.0\n + decorator==5.1.1\n + dictdiffer==0.9.0\n + docutils==0.20.1\n + entrypoints==0.4\n + ipykernel==6.16.2\n + ipython==7.34.0\n + ipython-genutils==0.2.0\n + jedi==0.19.2\n + jupyter-client==7.4.9\n + jupyter-core==4.12.0\n + matplotlib-inline==0.1.6\n + nest-asyncio==1.6.0\n + orange-canvas-core==0.1.35\n + orange-widget-base==4.22.0\n + parso==0.8.4\n + pexpect==4.9.0\n + pickleshare==0.7.5\n + platformdirs==4.0.0\n + prompt-toolkit==3.0.48\n + psutil==6.1.1\n + ptyprocess==0.7.0\n + pygments==2.17.2\n + pyqtgraph==0.12.3\n + pyzmq==26.2.0\n + qasync==0.25.0\n + qtconsole==5.4.4\n + qtpy==2.4.2\n + requests-cache==1.1.1\n + tornado==6.2\n + traitlets==5.9.0\n + url-normalize==1.4.3\n + wcwidth==0.2.13\nResolved 2 packages in 4ms\nInstalled 2 packages in 5ms\n + psycopg2==2.9.9\n + pymssql==2.3.2\nResolved 19 packages in 76ms\nInstalled 5 packages in 1.22s\n + catboost==1.2.7\n + graphviz==0.20.1\n + plotly==5.18.0\n + tenacity==8.2.3\n + xgboost==1.6.2\nAudited 2 packages in 1ms\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/classification/_tree_scorers.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_2find_threshold_entropy’:\nOrange/classification/_tree_scorers.c:3427:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 3427 |   __pyx_t_7 = ((__pyx_v_N <= __pyx_v_min_leaf) != 0);\n      |                           ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_6find_threshold_MSE’:\nOrange/classification/_tree_scorers.c:4883:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 4883 |   __pyx_t_1 = ((__pyx_v_N <= __pyx_v_min_leaf) != 0);\n      |                           ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_8find_binarization_MSE’:\nOrange/classification/_tree_scorers.c:5629:43: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5629 |           for (__pyx_t_19 = 0; __pyx_t_19 < __pyx_t_18; __pyx_t_19+=1) {\n      |                                           ^\nOrange/classification/_tree_scorers.c:5750:39: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5750 |           __pyx_t_20 = ((__pyx_v_left >= __pyx_v_min_leaf) != 0);\n      |                                       ^~\nOrange/classification/_tree_scorers.c:5756:53: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5756 |           __pyx_t_20 = (((__pyx_v_N - __pyx_v_left) >= __pyx_v_min_leaf) != 0);\n      |                                                     ^~\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_14compute_predictions_csr’:\nOrange/classification/_tree_scorers.c:7217:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7217 |         for (__pyx_t_12 = 0; __pyx_t_12 < __pyx_t_11; __pyx_t_12+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_16compute_predictions_csc’:\nOrange/classification/_tree_scorers.c:7874:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7874 |         for (__pyx_t_12 = 0; __pyx_t_12 < __pyx_t_11; __pyx_t_12+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c:7934:150: warning: comparison of integer expressions of different signedness: ‘__pyx_t_5numpy_int32_t’ {aka ‘int’} and ‘unsigned int’ [-Wsign-compare]\n 7934 |               __pyx_t_16 = (((*((__pyx_t_5numpy_int32_t const  *) ( /* dim=0 */ (__pyx_v_indices.data + __pyx_t_15 * __pyx_v_indices.strides[0]) ))) != __pyx_v_i) != 0);\n      |                                                                                                                                                      ^~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/data/_contingency.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nOrange/data/_io.c: In function ‘__pyx_pf_6Orange_4data_3_io_sparse_read_float.constprop’:\nOrange/data/_io.c:4120:43: warning: ‘__pyx_v_decs’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4120 |           __pyx_v_value = ((__pyx_v_value * __pyx_v_decs) + (__pyx_v_c & 0xf));\n      |                            ~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/data/_io.c:4318:24: warning: ‘__pyx_v_value’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4318 |           __pyx_t_10 = PyFloat_FromDouble(__pyx_v_value); if (unlikely(!__pyx_t_10)) __PYX_ERR(0, 223, __pyx_L5_error)\n      |                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:683:40: warning: ‘__pyx_v_attr_index’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n  683 |   #define PyInt_FromLong               PyLong_FromLong\n      |                                        ^~~~~~~~~~~~~~~\nOrange/data/_io.c:1590:7: note: ‘__pyx_v_attr_index’ was declared here\n 1590 |   int __pyx_v_attr_index;\n      |       ^~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:3183:25: warning: ‘__pyx_v_atomp’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3183 |           __pyx_v_atomp = (__pyx_v_atomp + 1);\n      |           ~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/data/_valuecount.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/distance/_distance.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/preprocess/_discretize.c:774:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/preprocess/_relieff.cpp:790:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/projection/_som.c:775:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning \"Using deprecated NumPy API, disable it with \" \"#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION\" [-Wcpp]\n   17 | #warning \"Using deprecated NumPy API, disable it with \" \\\n      |  ^~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_5update_hex’:\nOrange/projection/_som.c:3863:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3863 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:3862:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3862 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pf_6Orange_10projection_4_som_8update_sparse.constprop’:\nOrange/projection/_som.c:4934:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4934 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:4933:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4933 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_7get_winners_sparse’:\nOrange/projection/_som.c:4732:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4732 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_26, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_27, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4307:7: note: ‘__pyx_v_win_y’ was declared here\n 4307 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:4721:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4721 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_27, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_26, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4306:7: note: ‘__pyx_v_win_x’ was declared here\n 4306 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pf_6Orange_10projection_4_som_10update_sparse_hex.constprop’:\nOrange/projection/_som.c:5682:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5682 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:5681:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5681 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_1get_winners’:\nOrange/projection/_som.c:3184:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3184 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_22, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_23, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:2887:7: note: ‘__pyx_v_win_y’ was declared here\n 2887 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:3173:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3173 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_23, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_22, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:2886:7: note: ‘__pyx_v_win_x’ was declared here\n 2886 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_3update’:\nOrange/projection/_som.c:3357:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3357 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/classification/_simple_tree.c: In function ‘gain_ratio_c’:\nOrange/classification/_simple_tree.c:63:9: warning: implicit declaration of function ‘qsort_r’; did you mean ‘qsort’? [-Wimplicit-function-declaration]\n   63 |         qsort_r(base, nel, size, compar, thunk)\n      |         ^~~~~~~\nOrange/classification/_simple_tree.c:147:9: note: in expansion of macro ‘QSORT_R_FUNC’\n  147 |         QSORT_R_FUNC(examples, size, sizeof(struct Example), (void*) &compar_attr, compar_examples);\n      |         ^~~~~~~~~~~~\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/setuptools/command/easy_install.py:159: EasyInstallDeprecationWarning: easy_install command is deprecated. Use build and pip and other standards-based tools.\n  EasyInstallDeprecationWarning,\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/.venv/lib/python3.7/site-packages/setuptools/command/install.py:37: SetuptoolsDeprecationWarning: setup.py install is deprecated. Use build and pip and other standards-based tools.\n  setuptools.SetuptoolsDeprecationWarning,\nwarning: no files found matching 'README*' under directory 'Orange'\nwarning: no files found matching 'LICENSE' under directory 'Orange'\nwarning: no files found matching '*.basket' under directory 'Orange/datasets'\nwarning: no files found matching '*.svg' under directory 'Orange/canvas'\nwarning: no files found matching '*.js' under directory 'Orange/widgets'\nwarning: no files found matching '*.css' under directory 'Orange/widgets'\nwarning: no files found matching '*.html' under directory 'Orange/widgets'\nwarning: no files found matching '*.svg' under directory 'distribute'\nwarning: no files found matching 'COPYING'\n",
    "new_commit_res_code": 1,
    "new_commit_res_stdout": "============================= test session starts ==============================\nplatform linux -- Python 3.7.9, pytest-7.4.4, pluggy-1.2.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab\nplugins: anyio-3.7.1\ncollected 65 items\n\nr2e_tests/test_1.py sssE.............F.................................F [ 80%]\n.............                                                            [100%]\n\n==================================== ERRORS ====================================\n_______________________ ERROR at setup of test_filename ________________________\nfile /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/Orange/tests/__init__.py, line 68\n  def test_filename(path):\nE       fixture 'path' not found\n>       available fixtures: anyio_backend, anyio_backend_name, anyio_backend_options, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory\n>       use 'pytest --fixtures [testpath]' for help on them.\n\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/Orange/tests/__init__.py:68\n=================================== FAILURES ===================================\n______________________ TestOWEditDomain.test_minimum_size ______________________\n\nself = <r2e_tests.test_1.TestOWEditDomain testMethod=test_minimum_size>\n\n    def test_minimum_size(self):\n        widget = getattr(self, \"widget\", None)\n        if widget is None:\n            self.skipTest(\"minimum size not tested as .widget was not set\")\n>       self.check_minimum_size(widget)\n\n.venv/lib/python3.7/site-packages/orangewidget/tests/base.py:242: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n.venv/lib/python3.7/site-packages/orangewidget/tests/base.py:615: in check_minimum_size\n    self.assertLess(min_size.width(), 800)\nE   AssertionError: 1178 not less than 800\n_____________ TestReinterpretTransforms.test_raise_pandas_version ______________\n\nself = <r2e_tests.test_1.TestReinterpretTransforms testMethod=test_raise_pandas_version>\n\n    def test_raise_pandas_version(self):\n        \"\"\"\n        When this test start to fail:\n        - remove this test\n        - remove if clause in datetime_to_epoch function and supporting comments\n        - set pandas dependency version to pandas>=1.4\n        \"\"\"\n        from datetime import datetime\n>       self.assertLess(datetime.today(), datetime(2023, 1, 1))\nE       AssertionError: datetime.datetime(2025, 1, 9, 9, 28, 48, 128944) not less than datetime.datetime(2023, 1, 1, 0, 0)\n\nr2e_tests/test_1.py:968: AssertionError\n==================================== PASSES ====================================\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestReport::test_annotate\nPASSED r2e_tests/test_1.py::TestReport::test_categorical_merge_mapping\nPASSED r2e_tests/test_1.py::TestReport::test_categories_mapping\nPASSED r2e_tests/test_1.py::TestReport::test_reinterpret\nPASSED r2e_tests/test_1.py::TestReport::test_rename\nPASSED r2e_tests/test_1.py::TestReport::test_unlinke\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_annotation_bool\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_duplicate_names\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_image_export\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_input_data\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_input_data_disconnect\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_input_from_owcolor\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_list_attributes_remain_lists\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_output_data\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_restore\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_time_variable_preservation\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_unlink\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_widget_state\nPASSED r2e_tests/test_1.py::TestEditors::test_continuous_editor\nPASSED r2e_tests/test_1.py::TestEditors::test_discrete_editor\nPASSED r2e_tests/test_1.py::TestEditors::test_discrete_editor_add_remove_action\nPASSED r2e_tests/test_1.py::TestEditors::test_discrete_editor_context_menu\nPASSED r2e_tests/test_1.py::TestEditors::test_discrete_editor_merge_action\nPASSED r2e_tests/test_1.py::TestEditors::test_discrete_editor_rename_selected_items_action\nPASSED r2e_tests/test_1.py::TestEditors::test_reinterpret_editor\nPASSED r2e_tests/test_1.py::TestEditors::test_reinterpret_editor_simulate\nPASSED r2e_tests/test_1.py::TestEditors::test_time_editor\nPASSED r2e_tests/test_1.py::TestEditors::test_unlink\nPASSED r2e_tests/test_1.py::TestEditors::test_variable_editor\nPASSED r2e_tests/test_1.py::TestModels::test_variable_model\nPASSED r2e_tests/test_1.py::TestDelegates::test_delegate\nPASSED r2e_tests/test_1.py::TestTransforms::test_continous\nPASSED r2e_tests/test_1.py::TestTransforms::test_discrete\nPASSED r2e_tests/test_1.py::TestTransforms::test_discrete_add_drop\nPASSED r2e_tests/test_1.py::TestTransforms::test_discrete_merge\nPASSED r2e_tests/test_1.py::TestTransforms::test_discrete_rename\nPASSED r2e_tests/test_1.py::TestTransforms::test_discrete_reorder\nPASSED r2e_tests/test_1.py::TestTransforms::test_string\nPASSED r2e_tests/test_1.py::TestTransforms::test_time\nPASSED r2e_tests/test_1.py::TestReinterpretTransforms::test_as_continuous\nPASSED r2e_tests/test_1.py::TestReinterpretTransforms::test_as_discrete\nPASSED r2e_tests/test_1.py::TestReinterpretTransforms::test_as_string\nPASSED r2e_tests/test_1.py::TestReinterpretTransforms::test_as_time\nPASSED r2e_tests/test_1.py::TestReinterpretTransforms::test_compound_transform\nPASSED r2e_tests/test_1.py::TestReinterpretTransforms::test_null_transform\nPASSED r2e_tests/test_1.py::TestReinterpretTransforms::test_reinterpret_string\nPASSED r2e_tests/test_1.py::TestUtils::test_as_float_or_nan\nPASSED r2e_tests/test_1.py::TestUtils::test_column_str_repr\nPASSED r2e_tests/test_1.py::TestUtils::test_mapper\nPASSED r2e_tests/test_1.py::TestLookupMappingTransform::test_equality\nPASSED r2e_tests/test_1.py::TestLookupMappingTransform::test_pickle\nPASSED r2e_tests/test_1.py::TestLookupMappingTransform::test_transform\nPASSED r2e_tests/test_1.py::TestGroupLessFrequentItemsDialog::test_dialog_open\nPASSED r2e_tests/test_1.py::TestGroupLessFrequentItemsDialog::test_group_keep_n\nPASSED r2e_tests/test_1.py::TestGroupLessFrequentItemsDialog::test_group_less_frequent_abs\nPASSED r2e_tests/test_1.py::TestGroupLessFrequentItemsDialog::test_group_less_frequent_missing\nPASSED r2e_tests/test_1.py::TestGroupLessFrequentItemsDialog::test_group_less_frequent_rel\nPASSED r2e_tests/test_1.py::TestGroupLessFrequentItemsDialog::test_group_selected\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:244: image exporting not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:238: minimum size not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:250: msg base class not tested as .widget was not set\nERROR r2e_tests/test_1.py::test_filename\nFAILED r2e_tests/test_1.py::TestOWEditDomain::test_minimum_size - AssertionEr...\nFAILED r2e_tests/test_1.py::TestReinterpretTransforms::test_raise_pandas_version\n=============== 2 failed, 59 passed, 3 skipped, 1 error in 5.91s ===============\nWidgets left: 0    Max widgets: 2228 \n\n",
    "new_commit_res_stderr": "",
    "old_commit_res_code": 1,
    "old_commit_res_stdout": "============================= test session starts ==============================\nplatform linux -- Python 3.7.9, pytest-7.4.4, pluggy-1.2.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab\nplugins: anyio-3.7.1\ncollected 65 items\n\nr2e_tests/test_1.py sssE.............F..............................F..F [ 80%]\n.............                                                            [100%]\n\n==================================== ERRORS ====================================\n_______________________ ERROR at setup of test_filename ________________________\nfile /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/Orange/tests/__init__.py, line 68\n  def test_filename(path):\nE       fixture 'path' not found\n>       available fixtures: anyio_backend, anyio_backend_name, anyio_backend_options, cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory\n>       use 'pytest --fixtures [testpath]' for help on them.\n\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_7b40090a9ccd235bd84fa3eda1feed0e259621ab/Orange/tests/__init__.py:68\n=================================== FAILURES ===================================\n______________________ TestOWEditDomain.test_minimum_size ______________________\n\nself = <r2e_tests.test_1.TestOWEditDomain testMethod=test_minimum_size>\n\n    def test_minimum_size(self):\n        widget = getattr(self, \"widget\", None)\n        if widget is None:\n            self.skipTest(\"minimum size not tested as .widget was not set\")\n>       self.check_minimum_size(widget)\n\n.venv/lib/python3.7/site-packages/orangewidget/tests/base.py:242: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n.venv/lib/python3.7/site-packages/orangewidget/tests/base.py:615: in check_minimum_size\n    self.assertLess(min_size.width(), 800)\nE   AssertionError: 1178 not less than 800\n____________________ TestReinterpretTransforms.test_as_time ____________________\n\nself = <r2e_tests.test_1.TestReinterpretTransforms testMethod=test_as_time>\n\n    def test_as_time(self):\n        # this test only test type of format that can be string, continuous and discrete\n        # correctness of time formats is already tested in TimeVariable module\n        d = TimeVariable(\"_\").parse_exact_iso\n        times = (\n            [\"07.02.2022\", \"18.04.2021\"],  # date only\n            [\"07.02.2022 01:02:03\", \"18.04.2021 01:02:03\"],  # datetime\n            # datetime with timezone\n            [\"2021-02-08 01:02:03+01:00\", \"2021-02-07 01:02:03+01:00\"],\n            [\"010203\", \"010203\"],  # time\n            [\"02-07\", \"04-18\"],\n        )\n        formats = [\n            \"25.11.2021\", \"25.11.2021 00:00:00\", \"2021-11-25 00:00:00\", \"000000\", \"11-25\"\n        ]\n        expected = [\n            [d(\"2022-02-07\"), d(\"2021-04-18\")],\n            [d(\"2022-02-07 01:02:03\"), d(\"2021-04-18 01:02:03\")],\n            [d(\"2021-02-08 01:02:03+0100\"), d(\"2021-02-07 01:02:03+0100\")],\n            [d(\"01:02:03\"), d(\"01:02:03\")],\n            [d(\"1900-02-07\"), d(\"1900-04-18\")],\n        ]\n        variables = [StringVariable(f\"s{i}\") for i in range(len(times))]\n        variables += [DiscreteVariable(f\"d{i}\", values=t) for i, t in enumerate(times)]\n        domain = Domain([], metas=variables)\n        metas = [t for t in times] + [list(range(len(x))) for x in times]\n        table = Table(domain, np.empty((len(times[0]), 0)), metas=np.array(metas).transpose())\n    \n        tr = AsTime()\n        dtr = []\n        for v, f in zip(domain.metas, chain(formats, formats)):\n            strp = StrpTime(f, *TimeVariable.ADDITIONAL_FORMATS[f])\n            vtr = apply_transform_var(\n                apply_reinterpret(v, tr, table_column_data(table, v)), [strp]\n            )\n            dtr.append(vtr)\n    \n>       ttable = table.transform(Domain([], metas=dtr))\n\nr2e_tests/test_1.py:954: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \nOrange/data/table.py:844: in transform\n    return type(self).from_table(domain, self)\nOrange/data/table.py:770: in from_table\n    ...)\nOrange/data/table.py:305: in get_columns\n    col_array = match_density(col(sourceri))\nOrange/preprocess/transformation.py:64: in __call__\n    transformed = self.transform(col)\nOrange/widgets/data/oweditdomain.py:2849: in transform\n    return datetime_to_epoch(d, only_time=not self.tr.have_date)\nOrange/widgets/data/oweditdomain.py:2831: in datetime_to_epoch\n    delta = dti - (dti.normalize() if only_time else pd.Timestamp(\"1970-01-01\"))\n.venv/lib/python3.7/site-packages/pandas/core/indexes/extension.py:174: in method\n    result = meth(_maybe_unwrap_index(other))\n.venv/lib/python3.7/site-packages/pandas/core/ops/common.py:69: in new_method\n    return method(self, other)\n.venv/lib/python3.7/site-packages/pandas/core/arrays/datetimelike.py:1316: in __sub__\n    result = self._sub_datetimelike_scalar(other)\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <DatetimeArray>\n['2021-02-08 01:02:03+01:00', '2021-02-07 01:02:03+01:00']\nLength: 2, dtype: datetime64[ns, pytz.FixedOffset(60)]\nother = Timestamp('1970-01-01 00:00:00')\n\n    def _sub_datetimelike_scalar(self, other):\n        # subtract a datetime from myself, yielding a ndarray[timedelta64[ns]]\n        assert isinstance(other, (datetime, np.datetime64))\n        assert other is not NaT\n        other = Timestamp(other)\n        # error: Non-overlapping identity check (left operand type: \"Timestamp\",\n        # right operand type: \"NaTType\")\n        if other is NaT:  # type: ignore[comparison-overlap]\n            return self - NaT\n    \n        if not self._has_same_tz(other):\n            # require tz compat\n            raise TypeError(\n>               \"Timestamp subtraction must have the same timezones or no timezones\"\n            )\nE           TypeError: Timestamp subtraction must have the same timezones or no timezones\n\n.venv/lib/python3.7/site-packages/pandas/core/arrays/datetimes.py:765: TypeError\n_____________ TestReinterpretTransforms.test_raise_pandas_version ______________\n\nself = <r2e_tests.test_1.TestReinterpretTransforms testMethod=test_raise_pandas_version>\n\n    def test_raise_pandas_version(self):\n        \"\"\"\n        When this test start to fail:\n        - remove this test\n        - remove if clause in datetime_to_epoch function and supporting comments\n        - set pandas dependency version to pandas>=1.4\n        \"\"\"\n        from datetime import datetime\n>       self.assertLess(datetime.today(), datetime(2023, 1, 1))\nE       AssertionError: datetime.datetime(2025, 1, 9, 9, 28, 53, 276989) not less than datetime.datetime(2023, 1, 1, 0, 0)\n\nr2e_tests/test_1.py:968: AssertionError\n==================================== PASSES ====================================\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestReport::test_annotate\nPASSED r2e_tests/test_1.py::TestReport::test_categorical_merge_mapping\nPASSED r2e_tests/test_1.py::TestReport::test_categories_mapping\nPASSED r2e_tests/test_1.py::TestReport::test_reinterpret\nPASSED r2e_tests/test_1.py::TestReport::test_rename\nPASSED r2e_tests/test_1.py::TestReport::test_unlinke\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_annotation_bool\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_duplicate_names\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_image_export\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_input_data\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_input_data_disconnect\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_input_from_owcolor\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_list_attributes_remain_lists\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_output_data\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_restore\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_time_variable_preservation\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_unlink\nPASSED r2e_tests/test_1.py::TestOWEditDomain::test_widget_state\nPASSED r2e_tests/test_1.py::TestEditors::test_continuous_editor\nPASSED r2e_tests/test_1.py::TestEditors::test_discrete_editor\nPASSED r2e_tests/test_1.py::TestEditors::test_discrete_editor_add_remove_action\nPASSED r2e_tests/test_1.py::TestEditors::test_discrete_editor_context_menu\nPASSED r2e_tests/test_1.py::TestEditors::test_discrete_editor_merge_action\nPASSED r2e_tests/test_1.py::TestEditors::test_discrete_editor_rename_selected_items_action\nPASSED r2e_tests/test_1.py::TestEditors::test_reinterpret_editor\nPASSED r2e_tests/test_1.py::TestEditors::test_reinterpret_editor_simulate\nPASSED r2e_tests/test_1.py::TestEditors::test_time_editor\nPASSED r2e_tests/test_1.py::TestEditors::test_unlink\nPASSED r2e_tests/test_1.py::TestEditors::test_variable_editor\nPASSED r2e_tests/test_1.py::TestModels::test_variable_model\nPASSED r2e_tests/test_1.py::TestDelegates::test_delegate\nPASSED r2e_tests/test_1.py::TestTransforms::test_continous\nPASSED r2e_tests/test_1.py::TestTransforms::test_discrete\nPASSED r2e_tests/test_1.py::TestTransforms::test_discrete_add_drop\nPASSED r2e_tests/test_1.py::TestTransforms::test_discrete_merge\nPASSED r2e_tests/test_1.py::TestTransforms::test_discrete_rename\nPASSED r2e_tests/test_1.py::TestTransforms::test_discrete_reorder\nPASSED r2e_tests/test_1.py::TestTransforms::test_string\nPASSED r2e_tests/test_1.py::TestTransforms::test_time\nPASSED r2e_tests/test_1.py::TestReinterpretTransforms::test_as_continuous\nPASSED r2e_tests/test_1.py::TestReinterpretTransforms::test_as_discrete\nPASSED r2e_tests/test_1.py::TestReinterpretTransforms::test_as_string\nPASSED r2e_tests/test_1.py::TestReinterpretTransforms::test_compound_transform\nPASSED r2e_tests/test_1.py::TestReinterpretTransforms::test_null_transform\nPASSED r2e_tests/test_1.py::TestReinterpretTransforms::test_reinterpret_string\nPASSED r2e_tests/test_1.py::TestUtils::test_as_float_or_nan\nPASSED r2e_tests/test_1.py::TestUtils::test_column_str_repr\nPASSED r2e_tests/test_1.py::TestUtils::test_mapper\nPASSED r2e_tests/test_1.py::TestLookupMappingTransform::test_equality\nPASSED r2e_tests/test_1.py::TestLookupMappingTransform::test_pickle\nPASSED r2e_tests/test_1.py::TestLookupMappingTransform::test_transform\nPASSED r2e_tests/test_1.py::TestGroupLessFrequentItemsDialog::test_dialog_open\nPASSED r2e_tests/test_1.py::TestGroupLessFrequentItemsDialog::test_group_keep_n\nPASSED r2e_tests/test_1.py::TestGroupLessFrequentItemsDialog::test_group_less_frequent_abs\nPASSED r2e_tests/test_1.py::TestGroupLessFrequentItemsDialog::test_group_less_frequent_missing\nPASSED r2e_tests/test_1.py::TestGroupLessFrequentItemsDialog::test_group_less_frequent_rel\nPASSED r2e_tests/test_1.py::TestGroupLessFrequentItemsDialog::test_group_selected\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:244: image exporting not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:238: minimum size not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:250: msg base class not tested as .widget was not set\nERROR r2e_tests/test_1.py::test_filename\nFAILED r2e_tests/test_1.py::TestOWEditDomain::test_minimum_size - AssertionEr...\nFAILED r2e_tests/test_1.py::TestReinterpretTransforms::test_as_time - TypeErr...\nFAILED r2e_tests/test_1.py::TestReinterpretTransforms::test_raise_pandas_version\n=============== 3 failed, 58 passed, 3 skipped, 1 error in 4.25s ===============\nWidgets left: 0    Max widgets: 2228 \n\n",
    "old_commit_res_stderr": ""
}
Orange/widgets/data/oweditdomain.py Orange/widgets/data/tests/test_oweditdomain.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** Error When Converting Timezone-Aware Datetime to Epoch

**Description:**
When attempting to convert datetime objects that include timezone information to epoch values, a `TypeError` is raised. This issue arises because the subtraction operation fails when the datetime index has a timezone, but the initial timestamp does not.

**Example Buggy Code:**
```python
import pandas as pd
import numpy as np

def datetime_to_epoch(dti: pd.DatetimeIndex, only_time) -> np.ndarray:
    delta = dti - pd.Timestamp("1970-01-01")
    return (delta / pd.Timedelta("1s")).values

dti = pd.DatetimeIndex(["2021-02-08 01:02:03+01:00", "2021-02-07 01:02:03+01:00"])
epoch = datetime_to_epoch(dti, only_time=False)
```

**Expected Behavior:**
The function should correctly compute the epoch times for timezone-aware datetime objects without raising any errors.

**Actual Behavior:**
A `TypeError` is raised with the message:
```
TypeError: Timestamp subtraction must have the same timezones or no timezones
```
This prevents the successful conversion of timezone-aware datetime objects to epoch values.
[/ISSUE]
--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.

--- BEGIN FILE ---
```
### Orange/widgets/data/oweditdomain.py
"""
Edit Domain
-----------

A widget for manual editing of a domain's attributes.

"""
import warnings
from xml.sax.saxutils import escape
from itertools import zip_longest, repeat, chain
from contextlib import contextmanager
from collections import namedtuple, Counter
from functools import singledispatch, partial
from typing import (
    Tuple, List, Any, Optional, Union, Dict, Sequence, Iterable, NamedTuple,
    FrozenSet, Type, Callable, TypeVar, Mapping, Hashable, cast
)

import numpy as np
import pandas as pd
from AnyQt.QtWidgets import (
    QWidget, QListView, QTreeView, QVBoxLayout, QHBoxLayout, QFormLayout,
    QLineEdit, QAction, QActionGroup, QGroupBox,
    QStyledItemDelegate, QStyleOptionViewItem, QStyle, QSizePolicy,
    QDialogButtonBox, QPushButton, QCheckBox, QComboBox, QStackedLayout,
    QDialog, QRadioButton, QGridLayout, QLabel, QSpinBox, QDoubleSpinBox,
    QAbstractItemView, QMenu, QToolTip
)
from AnyQt.QtGui import (
    QStandardItemModel, QStandardItem, QKeySequence, QIcon, QBrush, QPalette,
    QHelpEvent
)
from AnyQt.QtCore import (
    Qt, QSize, QModelIndex, QAbstractItemModel, QPersistentModelIndex, QRect,
    QPoint,
)
from AnyQt.QtCore import pyqtSignal as Signal, pyqtSlot as Slot

import Orange.data

from Orange.preprocess.transformation import (
    Transformation, Identity, Lookup, MappingTransform
)
from Orange.misc.collections import DictMissingConst
from Orange.util import frompyfunc
from Orange.widgets import widget, gui, settings
from Orange.widgets.utils import itemmodels, ftry
from Orange.widgets.utils.buttons import FixedSizeButton
from Orange.widgets.utils.itemmodels import signal_blocking
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.widget import Input, Output

ndarray = np.ndarray  # pylint: disable=invalid-name
MArray = np.ma.MaskedArray
DType = Union[np.dtype, type]

V = TypeVar("V", bound=Orange.data.Variable)  # pylint: disable=invalid-name
H = TypeVar("H", bound=Hashable)  # pylint: disable=invalid-name


def unique(sequence: Iterable[H]) -> Iterable[H]:
    """
    Return unique elements in `sequence`, preserving their (first seen) order.
    """
    # depending on Python >= 3.6 'ordered' dict implementation detail.
    return iter(dict.fromkeys(sequence))


class _DataType:
    def __eq__(self, other):
        """Equal if `other` has the same type and all elements compare equal."""
        if type(self) is not type(other):
            return False
        return super().__eq__(other)

    def __ne__(self, other):
        return not self == other

    def __hash__(self):
        return hash((type(self), super().__hash__()))

    def name_type(self):
        """
        Returns a tuple with name and type of the variable.
        It is used since it is forbidden to use names of variables in settings.
        """
        type_number = {
            "Categorical": 0,
            "Real": 2,
            "Time": 3,
            "String": 4
        }
        return self.name, type_number[type(self).__name__]


#: An ordered sequence of key, value pairs (variable annotations)
AnnotationsType = Tuple[Tuple[str, str], ...]


# Define abstract representation of the variable types edited

class Categorical(
    _DataType, NamedTuple("Categorical", [
        ("name", str),
        ("categories", Tuple[str, ...]),
        ("annotations", AnnotationsType),
        ("linked", bool)
    ])): pass


class Real(
    _DataType, NamedTuple("Real", [
        ("name", str),
        # a precision (int, and a format specifier('f', 'g', or '')
        ("format", Tuple[int, str]),
        ("annotations", AnnotationsType),
        ("linked", bool)
    ])): pass


class String(
    _DataType, NamedTuple("String", [
        ("name", str),
        ("annotations", AnnotationsType),
        ("linked", bool)
    ])): pass


class Time(
    _DataType, NamedTuple("Time", [
        ("name", str),
        ("annotations", AnnotationsType),
        ("linked", bool)
    ])): pass


Variable = Union[Categorical, Real, Time, String]
VariableTypes = (Categorical, Real, Time, String)


# Define variable transformations.

class Rename(_DataType, namedtuple("Rename", ["name"])):
    """
    Rename a variable.

    Parameters
    ----------
    name : str
        The new name
    """
    def __call__(self, var):
        # type: (Variable) -> Variable
        return var._replace(name=self.name)


#: Mapping of categories.
#: A list of pairs with the first element the original value and the second
#: element the new value. If the first element is None then a category level
#: is added. If the second element is None than the corresponding first level
#: is dropped. If there are duplicated elements on the right the corresponding
#: categories on the left are merged.
#: The mapped order is defined by the translated elements after None removal
#: and merges (the first occurrence of a multiplied elements defines its
#: position):
CategoriesMappingType = List[Tuple[Optional[str], Optional[str]]]


class CategoriesMapping(_DataType, namedtuple("CategoriesMapping", ["mapping"])):
    """
    Change categories of a categorical variable.

    Parameters
    ----------
    mapping : CategoriesMappingType
    """
    def __call__(self, var):
        # type: (Categorical) -> Categorical
        cat = tuple(unique(cj for _, cj in self.mapping if cj is not None))
        return var._replace(categories=cat)


class Annotate(_DataType, namedtuple("Annotate", ["annotations"])):
    """
    Replace variable annotations.
    """
    def __call__(self, var):
        return var._replace(annotations=self.annotations)


class Unlink(_DataType, namedtuple("Unlink", [])):
    """Unlink variable from its source, that is, remove compute_value"""


class StrpTime(_DataType, namedtuple("StrpTime", ["label", "formats", "have_date", "have_time"])):
    """Use format on variable interpreted as time"""


Transform = Union[Rename, CategoriesMapping, Annotate, Unlink, StrpTime]
TransformTypes = (Rename, CategoriesMapping, Annotate, Unlink, StrpTime)

CategoricalTransformTypes = (CategoriesMapping, Unlink)


# Reinterpret vector transformations.
class CategoricalVector(
    _DataType, NamedTuple("CategoricalVector", [
        ("vtype", Categorical),
        ("data", Callable[[], MArray]),
    ])): ...


class RealVector(
    _DataType, NamedTuple("RealVector", [
        ("vtype", Real),
        ("data", Callable[[], MArray]),
    ])): ...


class StringVector(
    _DataType, NamedTuple("StringVector", [
        ("vtype", String),
        ("data", Callable[[], MArray]),
    ])): ...


class TimeVector(
    _DataType, NamedTuple("TimeVector", [
        ("vtype", Time),
        ("data", Callable[[], MArray]),
    ])): ...


DataVector = Union[CategoricalVector, RealVector, StringVector, TimeVector]
DataVectorTypes = (CategoricalVector, RealVector, StringVector, TimeVector)


class AsString(_DataType, NamedTuple("AsString", [])):
    """Reinterpret a data vector as a string."""
    def __call__(self, vector: DataVector) -> StringVector:
        var, _ = vector
        if isinstance(var, String):
            return vector
        return StringVector(
            String(var.name, var.annotations, False),
            lambda: as_string(vector.data()),
        )


class AsContinuous(_DataType, NamedTuple("AsContinuous", [])):
    """
    Reinterpret as a continuous variable (values that do not parse as
    float are NaN).
    """
    def __call__(self, vector: DataVector) -> RealVector:
        var, _ = vector
        if isinstance(var, Real):
            return vector
        elif isinstance(var, Categorical):
            def data() -> MArray:
                d = vector.data()
                a = categorical_to_string_vector(d, var.values)
                return MArray(as_float_or_nan(a, where=a.mask), mask=a.mask)
            return RealVector(
                Real(var.name, (6, 'g'), var.annotations, var.linked), data
            )
        elif isinstance(var, Time):
            return RealVector(
                Real(var.name, (6, 'g'), var.annotations, var.linked),
                lambda: vector.data().astype(float)
            )
        elif isinstance(var, String):
            def data():
                s = vector.data()
                return MArray(as_float_or_nan(s, where=s.mask), mask=s.mask)
            return RealVector(
                Real(var.name, (6, "g"), var.annotations, var.linked), data
            )
        raise AssertionError


class AsCategorical(_DataType, namedtuple("AsCategorical", [])):
    """Reinterpret as a categorical variable"""
    def __call__(self, vector: DataVector) -> CategoricalVector:
        # this is the main complication in type transformation since we need
        # the data and not just the variable description
        var, _ = vector
        if isinstance(var, Categorical):
            return vector
        if isinstance(var, (Real, Time, String)):
            data, values = categorical_from_vector(vector.data())
            return CategoricalVector(
                Categorical(var.name, values, var.annotations, var.linked),
                lambda: data
            )
        raise AssertionError


class AsTime(_DataType, namedtuple("AsTime", [])):
    """Reinterpret as a datetime vector"""
    def __call__(self, vector: DataVector) -> TimeVector:
        var, _ = vector
        if isinstance(var, Time):
            return vector
        elif isinstance(var, Real):
            return TimeVector(
                Time(var.name, var.annotations, var.linked),
                lambda: vector.data().astype("M8[us]")
            )
        elif isinstance(var, Categorical):
            def data():
                d = vector.data()
                s = categorical_to_string_vector(d, var.values)
                dt = pd.to_datetime(s, errors="coerce").values.astype("M8[us]")
                return MArray(dt, mask=d.mask)
            return TimeVector(
                Time(var.name, var.annotations, var.linked), data
            )
        elif isinstance(var, String):
            def data():
                s = vector.data()
                dt = pd.to_datetime(s, errors="coerce").values.astype("M8[us]")
                return MArray(dt, mask=s.mask)
            return TimeVector(
                Time(var.name, var.annotations, var.linked), data
            )
        raise AssertionError


ReinterpretTransform = Union[AsCategorical, AsContinuous, AsTime, AsString]
ReinterpretTransformTypes = (AsCategorical, AsContinuous, AsTime, AsString)


def deconstruct(obj):
    # type: (tuple) -> Tuple[str, Tuple[Any, ...]]
    """
    Deconstruct a tuple subclass to its class name and its contents.

    Parameters
    ----------
    obj : A tuple

    Returns
    -------
    value: Tuple[str, Tuple[Any, ...]]
    """
    cname = type(obj).__name__
    args = tuple(obj)
    return cname, args


def reconstruct(tname, args):
    # type: (str, Tuple[Any, ...]) -> Tuple[Any, ...]
    """
    Reconstruct a tuple subclass (inverse of deconstruct).

    Parameters
    ----------
    tname : str
        Type name
    args : Tuple[Any, ...]

    Returns
    -------
    rval: Tuple[Any, ...]
    """
    try:
        constructor = globals()[tname]
    except KeyError:
        raise NameError(tname)
    return constructor(*args)


def formatter_for_dtype(dtype: np.dtype) -> Callable[[Any], str]:
    if dtype.metadata is None:
        return str
    else:
        return dtype.metadata.get("__formatter", str)  # metadata abuse


def masked_unique(data: MArray) -> Tuple[MArray, ndarray]:
    if not np.any(data.mask):
        return np.ma.unique(data, return_inverse=True)
    elif data.dtype.kind == "O":
        # np.ma.unique does not work for object arrays
        # (no ma.minimum_fill_value for object arrays)
        # maybe sorted(set(data.data[...]))
        unq = np.unique(data.data[~data.mask])
        mapper = make_dict_mapper(
            DictMissingConst(len(unq), ((v, i) for i, v in enumerate(unq)))
        )
        index = mapper(data.data)
        unq = np.array(unq.tolist() + [data.fill_value], dtype=data.dtype)
        unq_mask = [False] * unq.size
        unq_mask[-1] = True
        unq = MArray(unq, mask=unq_mask)
        return unq, index
    else:
        unq, index = np.ma.unique(data, return_inverse=True)
        assert not np.any(unq.mask[:-1]), \
            "masked value if present must be in last position"
        return unq, index


def categorical_from_vector(data: MArray) -> Tuple[MArray, Tuple[str, ...]]:
    formatter = formatter_for_dtype(data.dtype)
    unq, index = categorize_unique(data)
    if formatter is not str:
        # str(np.array([0], "M8[s]")[0]) is different then
        # str(np.array([0], "M8[s]").astype(object)[0]) which is what
        # as_string is doing
        names = tuple(map(formatter, unq.astype(object)))
    else:
        names = tuple(as_string(unq))
    data = MArray(
        index, mask=data.mask,
        dtype=np.dtype(int, metadata={
            "__formater": lambda i: names[i] if 0 <= i < unq.size else "?"
        })
    )
    return data, names


def categorize_unique(data: MArray) -> Tuple[ndarray, MArray]:
    unq, index = masked_unique(data)
    if np.any(unq.mask):
        unq = unq[:-1]
        assert not np.any(unq.mask), "masked value if present must be last"
    unq = unq.data
    index[data.mask] = -1
    index = MArray(index, mask=data.mask)
    return unq, index


def categorical_to_string_vector(data: MArray, values: Tuple[str, ...]) -> MArray:
    lookup = np.asarray(values, object)
    out = np.full(data.shape, "", dtype=object)
    mask_ = ~data.mask
    out[mask_] = lookup[data.data[mask_]]
    return MArray(out, mask=data.mask, fill_value="")


# Item models


class DictItemsModel(QStandardItemModel):
    """A Qt Item Model class displaying the contents of a python
    dictionary.

    """
    # Implement a proper model with in-place editing.
    # (Maybe it should be a TableModel with 2 columns)
    def __init__(self, parent=None, a_dict=None):
        super().__init__(parent)
        self._dict = {}
        self.setHorizontalHeaderLabels(["Key", "Value"])
        if a_dict is not None:
            self.set_dict(a_dict)

    def set_dict(self, a_dict):
        # type: (Dict[str, str]) -> None
        self._dict = a_dict
        self.setRowCount(0)
        for key, value in sorted(a_dict.items()):
            key_item = QStandardItem(key)
            value_item = QStandardItem(value)
            key_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
            value_item.setFlags(value_item.flags() | Qt.ItemIsEditable)
            self.appendRow([key_item, value_item])

    def get_dict(self):
        # type: () -> Dict[str, str]
        rval = {}
        for row in range(self.rowCount()):
            key_item = self.item(row, 0)
            value_item = self.item(row, 1)
            rval[key_item.text()] = value_item.text()
        return rval


class VariableEditor(QWidget):
    """
    An editor widget for a variable.

    Can edit the variable name, and its attributes dictionary.
    """
    variable_changed = Signal()

    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)
        self.var = None  # type: Optional[Variable]

        layout = QVBoxLayout()
        self.setLayout(layout)

        self.form = form = QFormLayout(
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,
            objectName="editor-form-layout"
        )
        layout.addLayout(self.form)

        self.name_edit = QLineEdit(objectName="name-editor")
        self.name_edit.editingFinished.connect(
            lambda: self.name_edit.isModified() and self.on_name_changed()
        )
        form.addRow("Name:", self.name_edit)

        self.unlink_var_cb = QCheckBox(
            "Unlink variable from its source variable", self,
            toolTip="Make Orange forget that the variable is derived from "
                    "another.\n"
                    "Use this for instance when you want to consider variables "
                    "with the same name but from different sources as the same "
                    "variable."
        )
        self.unlink_var_cb.toggled.connect(self._set_unlink)
        form.addRow("", self.unlink_var_cb)

        vlayout = QVBoxLayout(spacing=1)
        self.labels_edit = view = QTreeView(
            objectName="annotation-pairs-edit",
            rootIsDecorated=False,
            editTriggers=QTreeView.DoubleClicked | QTreeView.EditKeyPressed,
        )
        self.labels_model = model = DictItemsModel()
        view.setModel(model)

        view.selectionModel().selectionChanged.connect(
            self.on_label_selection_changed)

        agrp = QActionGroup(view, objectName="annotate-action-group")
        action_add = QAction(
            "+", self, objectName="action-add-label",
            toolTip="Add a new label.",
            shortcut=QKeySequence(QKeySequence.New),
            shortcutContext=Qt.WidgetShortcut
        )
        action_delete = QAction(
            "\N{MINUS SIGN}", self, objectName="action-delete-label",
            toolTip="Remove selected label.",
            shortcut=QKeySequence(QKeySequence.Delete),
            shortcutContext=Qt.WidgetShortcut
        )
        agrp.addAction(action_add)
        agrp.addAction(action_delete)
        view.addActions([action_add, action_delete])

        def add_label():
            row = [QStandardItem(), QStandardItem()]
            model.appendRow(row)
            idx = model.index(model.rowCount() - 1, 0)
            view.setCurrentIndex(idx)
            view.edit(idx)

        def remove_label():
            rows = view.selectionModel().selectedRows(0)
            if rows:
                assert len(rows) == 1
                idx = rows[0].row()
                model.removeRow(idx)

        action_add.triggered.connect(add_label)
        action_delete.triggered.connect(remove_label)
        agrp.setEnabled(False)

        self.add_label_action = action_add
        self.remove_label_action = action_delete

        # Necessary signals to know when the labels change
        model.dataChanged.connect(self.on_labels_changed)
        model.rowsInserted.connect(self.on_labels_changed)
        model.rowsRemoved.connect(self.on_labels_changed)

        vlayout.addWidget(self.labels_edit)
        hlayout = QHBoxLayout()
        hlayout.setContentsMargins(0, 0, 0, 0)
        button = FixedSizeButton(
            self, defaultAction=self.add_label_action,
            accessibleName="Add",
        )
        hlayout.addWidget(button)

        button = FixedSizeButton(
            self, defaultAction=self.remove_label_action,
            accessibleName="Remove",
        )

        hlayout.addWidget(button)
        hlayout.addStretch(10)
        vlayout.addLayout(hlayout)
        form.addRow("Labels:", vlayout)

    def set_data(self, var, transform=()):
        # type: (Optional[Variable], Sequence[Transform]) -> None
        """
        Set the variable to edit.
        """
        self.clear()
        self.var = var
        if var is not None:
            name = var.name
            annotations = var.annotations
            unlink = False
            for tr in transform:
                if isinstance(tr, Rename):
                    name = tr.name
                elif isinstance(tr, Annotate):
                    annotations = tr.annotations
                elif isinstance(tr, Unlink):
                    unlink = True
            self.name_edit.setText(name)
            self.labels_model.set_dict(dict(annotations))
            self.add_label_action.actionGroup().setEnabled(True)
            self.unlink_var_cb.setChecked(unlink)
        else:
            self.add_label_action.actionGroup().setEnabled(False)

        self.unlink_var_cb.setDisabled(var is None or not var.linked)

    def get_data(self):
        """Retrieve the modified variable.
        """
        if self.var is None:
            return None, []
        name = self.name_edit.text().strip()
        labels = tuple(sorted(self.labels_model.get_dict().items()))
        tr = []
        if self.var.name != name:
            tr.append(Rename(name))
        if self.var.annotations != labels:
            tr.append(Annotate(labels))
        if self.var.linked and self.unlink_var_cb.isChecked():
            tr.append(Unlink())
        return self.var, tr

    def clear(self):
        """Clear the editor state.
        """
        self.var = None
        self.name_edit.setText("")
        self.labels_model.setRowCount(0)
        self.unlink_var_cb.setChecked(False)

    @Slot()
    def on_name_changed(self):
        self.variable_changed.emit()

    @Slot()
    def on_labels_changed(self):
        self.variable_changed.emit()

    @Slot()
    def on_label_selection_changed(self):
        selected = self.labels_edit.selectionModel().selectedRows()
        self.remove_label_action.setEnabled(bool(len(selected)))

    def _set_unlink(self, unlink):
        self.unlink_var_cb.setChecked(unlink)
        self.variable_changed.emit()


class GroupItemsDialog(QDialog):
    """
    A dialog for group less frequent values.
    """
    DEFAULT_LABEL = "other"

    def __init__(
            self, variable: Categorical,
            data: Union[np.ndarray, List, MArray],
            selected_attributes: List[str], dialog_settings: Dict[str, Any],
            parent: QWidget = None, flags: Qt.WindowFlags = Qt.Dialog, **kwargs
    ) -> None:
        super().__init__(parent, flags, **kwargs)
        self.variable = variable
        self.data = data
        self.selected_attributes = selected_attributes

        # grouping strategy
        self.selected_radio = radio1 = QRadioButton("Group selected values")
        self.frequent_abs_radio = radio2 = QRadioButton(
            "Group values with less than"
        )
        self.frequent_rel_radio = radio3 = QRadioButton(
            "Group values with less than"
        )
        self.n_values_radio = radio4 = QRadioButton(
            "Group all except"
        )

        # if selected attributes available check the first radio button,
        # otherwise disable it
        if selected_attributes:
            radio1.setChecked(True)
        else:
            radio1.setEnabled(False)
            # they are remembered by number since radio button instance is
            # new object for each dialog
            checked = dialog_settings.get("selected_radio", 0)
            [radio2, radio3, radio4][checked].setChecked(True)

        label2 = QLabel("occurrences")
        label3 = QLabel("occurrences")
        label4 = QLabel("most frequent values")

        self.frequent_abs_spin = spin2 = QSpinBox()
        max_val = len(data)
        spin2.setMinimum(1)
        spin2.setMaximum(max_val)
        spin2.setValue(dialog_settings.get("frequent_abs_spin", 10))
        spin2.setMinimumWidth(
            self.fontMetrics().horizontalAdvance("X") * (len(str(max_val)) + 1) + 20
        )
        spin2.valueChanged.connect(self._frequent_abs_spin_changed)

        self.frequent_rel_spin = spin3 = QDoubleSpinBox()
        spin3.setMinimum(0)
        spin3.setDecimals(1)
        spin3.setSingleStep(0.1)
        spin3.setMaximum(100)
        spin3.setValue(dialog_settings.get("frequent_rel_spin", 10))
        spin3.setMinimumWidth(self.fontMetrics().horizontalAdvance("X") * (2 + 1) + 20)
        spin3.setSuffix(" %")
        spin3.valueChanged.connect(self._frequent_rel_spin_changed)

        self.n_values_spin = spin4 = QSpinBox()
        spin4.setMinimum(0)
        spin4.setMaximum(len(variable.categories))
        spin4.setValue(
            dialog_settings.get(
                "n_values_spin", min(10, len(variable.categories))
            )
        )
        spin4.setMinimumWidth(
            self.fontMetrics().horizontalAdvance("X") * (len(str(max_val)) + 1) + 20
        )
        spin4.valueChanged.connect(self._n_values_spin_spin_changed)

        grid_layout = QGridLayout()
        # first row
        grid_layout.addWidget(radio1, 0, 0, 1, 2)
        # second row
        grid_layout.addWidget(radio2, 1, 0, 1, 2)
        grid_layout.addWidget(spin2, 1, 2)
        grid_layout.addWidget(label2, 1, 3)
        # third row
        grid_layout.addWidget(radio3, 2, 0, 1, 2)
        grid_layout.addWidget(spin3, 2, 2)
        grid_layout.addWidget(label3, 2, 3)
        # fourth row
        grid_layout.addWidget(radio4, 3, 0)
        grid_layout.addWidget(spin4, 3, 1)
        grid_layout.addWidget(label4, 3, 2, 1, 2)

        group_box = QGroupBox()
        group_box.setLayout(grid_layout)

        # grouped variable name
        new_name_label = QLabel("New value name: ")
        self.new_name_line_edit = n_line_edit = QLineEdit(
            dialog_settings.get("name_line_edit", self.DEFAULT_LABEL)
        )
        # it is shown gray when user removes the text and let user know that
        # word others is default one
        n_line_edit.setPlaceholderText(self.DEFAULT_LABEL)
        name_hlayout = QHBoxLayout()
        name_hlayout.addWidget(new_name_label)
        name_hlayout.addWidget(n_line_edit)

        # confirm_button = QPushButton("Apply")
        # cancel_button = QPushButton("Cancel")
        buttons = QDialogButtonBox(
            orientation=Qt.Horizontal,
            standardButtons=(QDialogButtonBox.Ok | QDialogButtonBox.Cancel),
            objectName="dialog-button-box",
        )
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)

        # join components
        self.setLayout(QVBoxLayout())
        self.layout().addWidget(group_box)
        self.layout().addLayout(name_hlayout)
        self.layout().addWidget(buttons)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

    def _frequent_abs_spin_changed(self) -> None:
        self.frequent_abs_radio.setChecked(True)

    def _n_values_spin_spin_changed(self) -> None:
        self.n_values_radio.setChecked(True)

    def _frequent_rel_spin_changed(self) -> None:
        self.frequent_rel_radio.setChecked(True)

    def get_merge_attributes(self) -> List[str]:
        """
        Returns attributes that will be merged

        Returns
        -------
        List of attributes' to be merged names
        """
        if self.selected_radio.isChecked():
            return self.selected_attributes

        if isinstance(self.data, MArray):
            non_nan = self.data[~self.data.mask]
        elif isinstance(self.data, np.ndarray):
            non_nan = self.data[~np.isnan(self.data)]
        else:  # list
            non_nan = [x for x in self.data if x is not None]

        counts = Counter(non_nan)
        if self.n_values_radio.isChecked():
            keep_values = self.n_values_spin.value()
            values = counts.most_common()[keep_values:]
            indices = [i for i, _ in values]
        elif self.frequent_abs_radio.isChecked():
            indices = [v for v, c in counts.most_common()
                       if c < self.frequent_abs_spin.value()]
        else:  # self.frequent_rel_radio.isChecked():
            n_all = sum(counts.values())
            indices = [v for v, c in counts.most_common()
                       if c / n_all * 100 < self.frequent_rel_spin.value()]

        indices = np.array(indices, dtype=int)  # indices must be ints
        return np.array(self.variable.categories)[indices].tolist()

    def get_merged_value_name(self) -> str:
        """
        Returns
        -------
        New label of merged values
        """
        return self.new_name_line_edit.text() or self.DEFAULT_LABEL

    def get_dialog_settings(self) -> Dict[str, Any]:
        """
        Returns
        -------
        Return the dictionary with vlues set by user in each of the line edits
        and selected radio button.
        """
        settings_dict = {
            "frequent_abs_spin": self.frequent_abs_spin.value(),
            "frequent_rel_spin": self.frequent_rel_spin.value(),
            "n_values_spin": self.n_values_spin.value(),
            "name_line_edit": self.new_name_line_edit.text()
        }
        checked = [
            i for i, s in enumerate(
                [self.frequent_abs_radio,
                 self.frequent_rel_radio,
                 self.n_values_radio]
            ) if s.isChecked()]
        # when checked empty radio button for selected values is selected
        # it is not stored in setting since its selection depends on users
        # selection of values in list
        if checked:
            settings_dict["selected_radio"] = checked[0]
        return settings_dict


@contextmanager
def disconnected(signal, slot, connection_type=Qt.AutoConnection):
    signal.disconnect(slot)
    try:
        yield
    finally:
        signal.connect(slot, connection_type)


#: In 'reordable' models holds the original position of the item
#: (if applicable).
SourcePosRole = Qt.UserRole
#: The original name
SourceNameRole = Qt.UserRole + 2

#: The added/dropped state (type is ItemEditState)
EditStateRole = Qt.UserRole + 1


class ItemEditState:
    NoState = 0
    Dropped = 1
    Added = 2


#: Role used to retrieve the count of 'key' values in the model.
MultiplicityRole = Qt.UserRole + 0x67


class CountedListModel(itemmodels.PyListModel):
    """
    A list model counting how many times unique `key` values appear in
    the list.

    The counts are cached and invalidated on any change to the model involving
    the changes to `keyRoles`.
    """
    #: cached counts
    __counts_cache = None  # type: Optional[Counter]

    def data(self, index, role=Qt.DisplayRole):
        # type: (QModelIndex, int) -> Any
        if role == MultiplicityRole:
            key = self.key(index)
            counts = self.__counts()
            return counts.get(key, 1)
        return super().data(index, role)

    def setData(self, index, value, role=Qt.EditRole):
        # type: (QModelIndex, Any, int)-> bool
        rval = super().setData(index, value, role)
        if role in self.keyRoles():
            self.invalidateCounts()
        return rval

    def setItemData(self, index, data):
        # type: (QModelIndex, Dict[int, Any]) -> bool
        rval = super().setItemData(index, data)
        if self.keyRoles().intersection(set(data.keys())):
            self.invalidateCounts()
        return rval

    def endInsertRows(self):
        super().endInsertRows()
        self.invalidateCounts()

    def endRemoveRows(self):
        super().endRemoveRows()
        self.invalidateCounts()

    def endResetModel(self) -> None:
        super().endResetModel()
        self.invalidateCounts()

    def invalidateCounts(self) -> None:
        """
        Invalidate the cached counts.
        """
        self.__counts_cache = None
        # emit the change for the whole model
        self.dataChanged.emit(
            self.index(0), self.index(self.rowCount() - 1), [MultiplicityRole]
        )

    def __counts(self):
        # type: () -> Counter
        if self.__counts_cache is not None:
            return self.__counts_cache
        counts = Counter()
        for index in map(self.index, range(self.rowCount())):
            key = self.key(index)
            try:
                counts[key] += 1
            except TypeError:  # pragma: no cover
                warnings.warn(f"key value '{key}' is not hashable")
        self.__counts_cache = counts
        return self.__counts_cache

    def key(self, index):
        # type: (QModelIndex) -> Any
        """
        Return the 'key' value that is to be counted.

        The default implementation returns Qt.EditRole value for the index

        Parameters
        ----------
        index : QModelIndex
            The model index.

        Returns
        -------
        key : Any
        """
        return self.data(index, Qt.EditRole)

    def keyRoles(self):
        # type: () -> FrozenSet[int]
        """
        Return a set of item roles on which `key` depends.

        The counts are invalidated and recomputed whenever any of the roles in
        this set changes.

        By default the only role returned is Qt.EditRole
        """
        return frozenset({Qt.EditRole})


class CountedStateModel(CountedListModel):
    """
    Count by EditRole (name) and EditStateRole (ItemEditState)
    """
    # The purpose is to count the items with target name only for
    # ItemEditState.NoRole, i.e. excluding added/dropped values.
    #
    def key(self, index):  # type: (QModelIndex) -> Tuple[Any, Any]
        # reimplemented
        return self.data(index, Qt.EditRole), self.data(index, EditStateRole)

    def keyRoles(self):  # type: () -> FrozenSet[int]
        # reimplemented
        return frozenset({Qt.EditRole, EditStateRole})


def mapRectTo(widget: QWidget, parent: QWidget, rect: QRect) -> QRect:  # pylint: disable=redefined-outer-name
    return QRect(widget.mapTo(parent, rect.topLeft()), rect.size())


def mapRectToGlobal(widget: QWidget, rect: QRect) -> QRect:  # pylint: disable=redefined-outer-name
    return QRect(widget.mapToGlobal(rect.topLeft()), rect.size())


class CategoriesEditDelegate(QStyledItemDelegate):
    """
    Display delegate for editing categories.

    Displayed items are styled for add, remove, merge and rename operations.
    """
    def initStyleOption(self, option, index):
        # type: (QStyleOptionViewItem, QModelIndex)-> None
        super().initStyleOption(option, index)
        text = str(index.data(Qt.EditRole))
        sourcename = str(index.data(SourceNameRole))
        editstate = index.data(EditStateRole)
        counts = index.data(MultiplicityRole)
        if not isinstance(counts, int):
            counts = 1
        suffix = None
        if editstate == ItemEditState.Dropped:
            option.state &= ~QStyle.State_Enabled
            option.font.setStrikeOut(True)
            text = sourcename
            suffix = "(dropped)"
        elif editstate == ItemEditState.Added:
            suffix = "(added)"
        else:
            text = f"{sourcename} \N{RIGHTWARDS ARROW} {text}"
            if counts > 1:
                suffix = "(merged)"
        if suffix is not None:
            text = text + " " + suffix
        option.text = text

    class CatEditComboBox(QComboBox):
        prows: List[QPersistentModelIndex]

    def createEditor(
            self, parent: QWidget, option: 'QStyleOptionViewItem',
            index: QModelIndex
    ) -> QWidget:
        view = option.widget
        assert isinstance(view, QAbstractItemView)
        selmodel = view.selectionModel()
        rows = selmodel.selectedRows(0)
        if len(rows) < 2:
            return super().createEditor(parent, option, index)
        # edit multiple selection
        cb = CategoriesEditDelegate.CatEditComboBox(
            editable=True, insertPolicy=QComboBox.InsertAtBottom)
        cb.setParent(view, Qt.Popup)
        cb.addItems(
            list(unique(str(row.data(Qt.EditRole)) for row in rows)))
        prows = [QPersistentModelIndex(row) for row in rows]
        cb.prows = prows
        return cb

    def updateEditorGeometry(
            self, editor: QWidget, option: 'QStyleOptionViewItem',
            index: QModelIndex
    ) -> None:
        if isinstance(editor, CategoriesEditDelegate.CatEditComboBox):
            view = cast(QAbstractItemView, option.widget)
            view.scrollTo(index)
            vport = view.viewport()
            vrect = view.visualRect(index)
            vrect = mapRectTo(vport, view, vrect)
            vrect = vrect.intersected(vport.geometry())
            vrect = mapRectToGlobal(vport, vrect)
            size = editor.sizeHint().expandedTo(vrect.size())
            editor.resize(size)
            editor.move(vrect.topLeft())
        else:
            super().updateEditorGeometry(editor, option, index)

    def setModelData(
            self, editor: QWidget, model: QAbstractItemModel, index: QModelIndex
    ) -> None:
        if isinstance(editor, CategoriesEditDelegate.CatEditComboBox):
            text = editor.currentText()
            with signal_blocking(model):
                for prow in editor.prows:
                    if prow.isValid():
                        model.setData(QModelIndex(prow), text, Qt.EditRole)
            # this could be better
            model.dataChanged.emit(
                model.index(0, 0), model.index(model.rowCount() - 1, 0),
                (Qt.EditRole,)
            )
        else:
            super().setModelData(editor, model, index)


class DiscreteVariableEditor(VariableEditor):
    """An editor widget for editing a discrete variable.

    Extends the :class:`VariableEditor` to enable editing of
    variables values.
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.merge_dialog_settings = {}
        self._values = None

        form = self.layout().itemAt(0)
        assert isinstance(form, QFormLayout)
        #: A list model of discrete variable's values.
        self.values_model = CountedStateModel(
            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
        )

        vlayout = QVBoxLayout(spacing=1)
        self.values_edit = QListView(
            editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed,
            selectionMode=QListView.ExtendedSelection,
            uniformItemSizes=True,
        )
        self.values_edit.setItemDelegate(CategoriesEditDelegate(self))
        self.values_edit.setModel(self.values_model)
        self.values_model.dataChanged.connect(self.on_values_changed)

        self.values_edit.selectionModel().selectionChanged.connect(
            self.on_value_selection_changed)
        self.values_model.layoutChanged.connect(self.on_value_selection_changed)
        self.values_model.rowsMoved.connect(self.on_value_selection_changed)

        vlayout.addWidget(self.values_edit)
        hlayout = QHBoxLayout(spacing=1)

        self.categories_action_group = group = QActionGroup(
            self, objectName="action-group-categories", enabled=False
        )
        self.move_value_up = QAction(
            "Move up", group,
            iconText="\N{UPWARDS ARROW}",
            toolTip="Move the selected item up.",
            shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier |
                                  Qt.Key_BracketLeft),
            shortcutContext=Qt.WidgetShortcut,
        )
        self.move_value_up.triggered.connect(self.move_up)

        self.move_value_down = QAction(
            "Move down", group,
            iconText="\N{DOWNWARDS ARROW}",
            toolTip="Move the selected item down.",
            shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier |
                                  Qt.Key_BracketRight),
            shortcutContext=Qt.WidgetShortcut,
        )
        self.move_value_down.triggered.connect(self.move_down)

        self.add_new_item = QAction(
            "Add", group,
            iconText="+",
            objectName="action-add-item",
            toolTip="Append a new item.",
            shortcut=QKeySequence(QKeySequence.New),
            shortcutContext=Qt.WidgetShortcut,
        )
        self.remove_item = QAction(
            "Remove item", group,
            iconText="\N{MINUS SIGN}",
            objectName="action-remove-item",
            toolTip="Delete the selected item.",
            shortcut=QKeySequence(QKeySequence.Delete),
            shortcutContext=Qt.WidgetShortcut,
        )
        self.rename_selected_items = QAction(
            "Rename selected items", group,
            iconText="=",
            objectName="action-rename-selected-items",
            toolTip="Rename selected items.",
            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_Equal),
            shortcutContext=Qt.WidgetShortcut,
        )
        self.merge_items = QAction(
            "Merge", group,
            iconText="M",
            objectName="action-activate-merge-dialog",
            toolTip="Merge infrequent items.",
            shortcut=QKeySequence(Qt.ControlModifier | Qt.MetaModifier | Qt.Key_Equal),
            shortcutContext=Qt.WidgetShortcut
        )

        self.add_new_item.triggered.connect(self._add_category)
        self.remove_item.triggered.connect(self._remove_category)
        self.rename_selected_items.triggered.connect(self._rename_selected_categories)
        self.merge_items.triggered.connect(self._merge_categories)

        button1 = FixedSizeButton(
            self, defaultAction=self.move_value_up,
            accessibleName="Move up"
        )
        button2 = FixedSizeButton(
            self, defaultAction=self.move_value_down,
            accessibleName="Move down"
        )
        button3 = FixedSizeButton(
            self, defaultAction=self.add_new_item,
            accessibleName="Add"
        )
        button4 = FixedSizeButton(
            self, defaultAction=self.remove_item,
            accessibleName="Remove"
        )
        button5 = FixedSizeButton(
            self, defaultAction=self.rename_selected_items,
            accessibleName="Merge selected items"
        )
        button6 = FixedSizeButton(
            self, defaultAction=self.merge_items,
            accessibleName="Merge infrequent",
        )

        self.values_edit.addActions([
            self.move_value_up, self.move_value_down,
            self.add_new_item, self.remove_item, self.rename_selected_items
        ])
        self.values_edit.setContextMenuPolicy(Qt.CustomContextMenu)

        def context_menu(pos: QPoint):
            viewport = self.values_edit.viewport()
            menu = QMenu(self.values_edit)
            menu.setAttribute(Qt.WA_DeleteOnClose)
            menu.addActions([self.rename_selected_items, self.remove_item])
            menu.popup(viewport.mapToGlobal(pos))
        self.values_edit.customContextMenuRequested.connect(context_menu)

        hlayout.addWidget(button1)
        hlayout.addWidget(button2)
        hlayout.addSpacing(3)
        hlayout.addWidget(button3)
        hlayout.addWidget(button4)
        hlayout.addSpacing(3)
        hlayout.addWidget(button5)
        hlayout.addWidget(button6)

        hlayout.addStretch(10)
        vlayout.addLayout(hlayout)

        form.insertRow(2, "Values:", vlayout)

        QWidget.setTabOrder(self.name_edit, self.values_edit)
        QWidget.setTabOrder(self.values_edit, button1)
        QWidget.setTabOrder(button1, button2)
        QWidget.setTabOrder(button2, button3)
        QWidget.setTabOrder(button3, button4)
        QWidget.setTabOrder(button4, button5)
        QWidget.setTabOrder(button5, button6)

    def set_data(self, var, transform=()):
        raise NotImplementedError

    def set_data_categorical(self, var, values, transform=()):
        # type: (Optional[Categorical], Optional[Sequence[float]], Sequence[Transform]) -> None
        """
        Set the variable to edit.

        `values` is needed for categorical features to perform grouping.
        """
        # pylint: disable=too-many-branches
        super().set_data(var, transform=transform)
        self._values = values
        tr = None  # type: Optional[CategoriesMapping]
        for tr_ in transform:
            if isinstance(tr_, CategoriesMapping):
                tr = tr_

        items = []
        if tr is not None:
            ci_index = {c: i for i, c in enumerate(var.categories)}
            for ci, cj in tr.mapping:
                if ci is None and cj is not None:
                    # level added
                    item = {
                        Qt.EditRole: cj,
                        EditStateRole: ItemEditState.Added,
                        SourcePosRole: None
                    }
                elif ci is not None and cj is None:
                    # ci level dropped
                    item = {
                        Qt.EditRole: ci,
                        EditStateRole: ItemEditState.Dropped,
                        SourcePosRole: ci_index[ci],
                        SourceNameRole: ci
                    }
                elif ci is not None and cj is not None:
                    # rename or reorder
                    item = {
                        Qt.EditRole: cj,
                        EditStateRole: ItemEditState.NoState,
                        SourcePosRole: ci_index[ci],
                        SourceNameRole: ci
                    }
                else:
                    assert False, "invalid mapping: {!r}".format(tr.mapping)
                items.append(item)
        elif var is not None:
            items = [
                {Qt.EditRole: c,
                 EditStateRole: ItemEditState.NoState,
                 SourcePosRole: i,
                 SourceNameRole: c}
                for i, c in enumerate(var.categories)
            ]
        else:
            items = []

        with disconnected(self.values_model.dataChanged,
                          self.on_values_changed):
            self.values_model.clear()
            self.values_model.insertRows(0, len(items))
            for i, item in enumerate(items):
                self.values_model.setItemData(
                    self.values_model.index(i, 0),
                    item
                )

        self.add_new_item.actionGroup().setEnabled(var is not None)

    def __categories_mapping(self):
        # type: () -> CategoriesMappingType
        """
        Encode and return the current state as a CategoriesMappingType
        """
        model = self.values_model
        source = self.var.categories

        res = []  # type: CategoriesMappingType
        for i in range(model.rowCount()):
            midx = model.index(i, 0)
            category = midx.data(Qt.EditRole)
            source_pos = midx.data(SourcePosRole)  # type: Optional[int]
            if source_pos is not None:
                source_name = source[source_pos]
            else:
                source_name = None
            state = midx.data(EditStateRole)
            if state == ItemEditState.Dropped:
                res.append((source_name, None))
            elif state == ItemEditState.Added:
                res.append((None, category))
            else:
                res.append((source_name, category))
        return res

    def get_data(self):
        """Retrieve the modified variable
        """
        var, tr = super().get_data()
        if var is None:
            return var, tr
        mapping = self.__categories_mapping()
        assert len(mapping) >= len(var.categories), f'{mapping}, {var}'
        if any(_1 != _2 or _2 != _3
               for (_1, _2), _3 in zip_longest(mapping, var.categories)):
            tr.append(CategoriesMapping(mapping))
        return var, tr

    def clear(self):
        """Clear the model state.
        """
        super().clear()
        self.values_model.clear()

    def move_rows(self, rows, offset):
        if not rows:
            return
        assert len(rows) == 1
        i = rows[0].row()
        if offset > 0:
            offset += 1
        self.values_model.moveRows(QModelIndex(), i, 1, QModelIndex(), i + offset)
        self.variable_changed.emit()

    def move_up(self):
        rows = self.values_edit.selectionModel().selectedRows()
        self.move_rows(rows, -1)

    def move_down(self):
        rows = self.values_edit.selectionModel().selectedRows()
        self.move_rows(rows, 1)

    @Slot()
    def on_values_changed(self):
        self.variable_changed.emit()

    @Slot()
    def on_value_selection_changed(self):
        rows = self.values_edit.selectionModel().selectedRows()
        if len(rows) == 1:
            i = rows[0].row()
            self.move_value_up.setEnabled(i != 0)
            self.move_value_down.setEnabled(i != self.values_model.rowCount() - 1)
        else:
            self.move_value_up.setEnabled(False)
            self.move_value_down.setEnabled(False)

    def _remove_category(self):
        """
        Remove the current selected category.

        If the item is an existing category present in the source variable it
        is marked as removed in the view. But if it was added in the set
        transformation it is removed entirely from the model and view.
        """
        view = self.values_edit
        rows = view.selectionModel().selectedRows(0)
        if not rows:
            return  # pragma: no cover
        for index in rows:
            model = index.model()
            state = index.data(EditStateRole)
            pos = index.data(SourcePosRole)
            if pos is not None and pos >= 0:
                # existing level -> only mark/toggle its dropped state,
                model.setData(
                    index,
                    ItemEditState.Dropped if state != ItemEditState.Dropped
                    else ItemEditState.NoState,
                    EditStateRole)
            elif state == ItemEditState.Added:
                # new level -> remove it
                model.removeRow(index.row())
            else:
                assert False, "invalid state '{}' for {}" \
                    .format(state, index.row())

    def _add_category(self):
        """
        Add a new category
        """
        view = self.values_edit
        model = view.model()

        with disconnected(model.dataChanged, self.on_values_changed,
                          Qt.UniqueConnection):
            row = model.rowCount()
            if not model.insertRow(model.rowCount()):
                return
            index = model.index(row, 0)
            model.setItemData(
                index, {
                    Qt.EditRole: "",
                    SourcePosRole: None,
                    EditStateRole: ItemEditState.Added
                }
            )
            view.setCurrentIndex(index)
            view.edit(index)
        self.on_values_changed()

    def _merge_categories(self) -> None:
        """
        Merge less common categories into one with the dialog for merge
        selection.
        """
        view = self.values_edit
        model = view.model()  # type: QAbstractItemModel

        selected_attributes = [ind.data(SourceNameRole)
                               for ind in view.selectedIndexes()]
        dlg = GroupItemsDialog(
            self.var, self._values, selected_attributes,
            self.merge_dialog_settings.get(self.var, {}), self,
            windowTitle="Import Options",
            sizeGripEnabled=True,
        )
        dlg.setWindowModality(Qt.WindowModal)
        status = dlg.exec()
        dlg.deleteLater()
        self.merge_dialog_settings[self.var] = dlg.get_dialog_settings()

        rows = (model.index(i, 0) for i in range(model.rowCount()))

        def complete_merge(text, merge_attributes):
            # write the new text for edit role in all rows
            with disconnected(model.dataChanged, self.on_values_changed):
                for row in rows:
                    if row.data(SourceNameRole) in merge_attributes:
                        model.setData(row, text, Qt.EditRole)
            self.variable_changed.emit()

        if status == QDialog.Accepted:
            complete_merge(
                dlg.get_merged_value_name(), dlg.get_merge_attributes()
            )

    def _rename_selected_categories(self):
        """
        Rename selected categories and merging them.

        Popup an editable combo box for selection/edit of a new value.
        """
        view = self.values_edit
        selmodel = view.selectionModel()
        index = view.currentIndex()
        if not selmodel.isSelected(index):
            indices = selmodel.selectedRows(0)
            if indices:
                index = indices[0]
        # delegate to the CategoriesEditDelegate
        view.edit(index)


class ContinuousVariableEditor(VariableEditor):
    # TODO: enable editing of display format...
    pass


class TimeVariableEditor(VariableEditor):
    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)
        form = self.layout().itemAt(0)

        self.format_cb = QComboBox()
        for item, data in [("Detect automatically", (None, 1, 1))] + list(
            Orange.data.TimeVariable.ADDITIONAL_FORMATS.items()
        ):
            self.format_cb.addItem(item, StrpTime(item, *data))
        self.format_cb.currentIndexChanged.connect(self.variable_changed)
        form.insertRow(2, "Format:", self.format_cb)

    def set_data(self, var, transform=()):
        super().set_data(var, transform)
        if self.parent() is not None and isinstance(self.parent().var, Time):
            # when transforming from time to time disable format selection combo
            self.format_cb.setEnabled(False)
        else:
            # select the format from StrpTime transform
            for tr in transform:
                if isinstance(tr, StrpTime):
                    index = self.format_cb.findText(tr.label)
                    self.format_cb.setCurrentIndex(index)
            self.format_cb.setEnabled(True)

    def get_data(self):
        var, tr = super().get_data()
        if var is not None and (self.parent() is None or not isinstance(self.parent().var, Time)):
            # do not add StrpTime when transforming from time to time
            tr.insert(0, self.format_cb.currentData())
        return var, tr


def variable_icon(var):
    # type: (Union[Variable, Type[Variable], ReinterpretTransform]) -> QIcon
    if not isinstance(var, type):
        var = type(var)

    if issubclass(var, (Categorical, AsCategorical)):
        return gui.attributeIconDict[1]
    elif issubclass(var, (Real, AsContinuous)):
        return gui.attributeIconDict[2]
    elif issubclass(var, (String, AsString)):
        return gui.attributeIconDict[3]
    elif issubclass(var, (Time, AsTime)):
        return gui.attributeIconDict[4]
    else:
        return gui.attributeIconDict[-1]


#: ItemDataRole storing the data vector transform
#: (`List[Union[ReinterpretTransform, Transform]]`)
TransformRole = Qt.UserRole + 42


class VariableEditDelegate(QStyledItemDelegate):
    ReinterpretNames = {
        AsCategorical: "categorical", AsContinuous: "numeric",
        AsString: "string", AsTime: "time"
    }

    def initStyleOption(self, option, index):
        # type: (QStyleOptionViewItem, QModelIndex) -> None
        super().initStyleOption(option, index)
        item = index.data(Qt.EditRole)
        var = tr = None
        if isinstance(item, DataVectorTypes):
            var = item.vtype
            option.icon = variable_icon(var)
        if isinstance(item, VariableTypes):
            var = item
            option.icon = variable_icon(item)
        elif isinstance(item, Orange.data.Variable):
            var = item
            option.icon = gui.attributeIconDict[var]

        transform = index.data(TransformRole)
        if not isinstance(transform, list):
            transform = []

        if transform and isinstance(transform[0], ReinterpretTransformTypes):
            option.icon = variable_icon(transform[0])

        if not option.icon.isNull():
            option.features |= QStyleOptionViewItem.HasDecoration

        if var is not None:
            text = var.name
            for tr in transform:
                if isinstance(tr, Rename):
                    text = ("{} \N{RIGHTWARDS ARROW} {}"
                            .format(var.name, tr.name))
            for tr in transform:
                if isinstance(tr, ReinterpretTransformTypes):
                    text += f" (reinterpreted as " \
                            f"{self.ReinterpretNames[type(tr)]})"
            option.text = text
        if transform:
            # mark as changed (maybe also change color, add text, ...)
            option.font.setItalic(True)

        multiplicity = index.data(MultiplicityRole)
        if isinstance(multiplicity, int) and multiplicity > 1:
            option.palette.setBrush(QPalette.Text, QBrush(Qt.red))
            option.palette.setBrush(QPalette.HighlightedText, QBrush(Qt.red))

    def helpEvent(self, event: QHelpEvent, view: QAbstractItemView,
                  option: QStyleOptionViewItem, index: QModelIndex) -> bool:
        multiplicity = index.data(MultiplicityRole)
        name = VariableListModel.effective_name(index)
        if isinstance(multiplicity, int) and multiplicity > 1 \
                and name is not None:
            QToolTip.showText(
                event.globalPos(), f"Name `{name}` is duplicated",
                view.viewport()
            )
            return True
        else:  # pragma: no cover
            return super().helpEvent(event, view, option, index)


# Item model for edited variables (Variable). Define a display role to be the
# source variable name. This is used only in keyboard search. The display is
# otherwise completely handled by a delegate.
class VariableListModel(CountedListModel):
    def data(self, index, role=Qt.DisplayRole):
        # type: (QModelIndex, Qt.ItemDataRole) -> Any
        row = index.row()
        if not index.isValid() or not 0 <= row < self.rowCount():
            return None
        if role == Qt.DisplayRole:
            item = self[row]
            if isinstance(item, VariableTypes):
                return item.name
            if isinstance(item, DataVectorTypes):
                return item.vtype.name
        return super().data(index, role)

    def key(self, index):
        return VariableListModel.effective_name(index)

    def keyRoles(self):  # type: () -> FrozenSet[int]
        return frozenset((Qt.DisplayRole, Qt.EditRole, TransformRole))

    @staticmethod
    def effective_name(index) -> Optional[str]:
        item = index.data(Qt.EditRole)
        if isinstance(item, DataVectorTypes):
            var = item.vtype
        elif isinstance(item, VariableTypes):
            var = item
        else:
            return None
        tr = index.data(TransformRole)
        return effective_name(var, tr or [])


def effective_name(var: Variable, tr: Sequence[Transform]) -> str:
    name = var.name
    for t in tr:
        if isinstance(t, Rename):
            name = t.name
    return name


class ReinterpretVariableEditor(VariableEditor):
    """
    A 'compound' variable editor capable of variable type reinterpretations.
    """
    _editors = {
        Categorical: 0,
        Real: 1,
        String: 2,
        Time: 3,
        type(None): -1,
    }

    def __init__(self, parent=None, **kwargs):
        # Explicitly skip VariableEditor's __init__, this is ugly but we have
        # a completely different layout/logic as a compound editor (should
        # really not subclass VariableEditor).
        super(VariableEditor, self).__init__(parent, **kwargs)  # pylint: disable=bad-super-call
        self.var = None  # type: Optional[Variable]
        self.__transform = None  # type: Optional[ReinterpretTransform]
        self.__data = None  # type: Optional[DataVector]
        #: Stored transform state indexed by variable. Used to preserve state
        #: between type switches.
        self.__history = {}  # type: Dict[Variable, List[Transform]]

        self.setLayout(QStackedLayout())

        def decorate(editor: VariableEditor) -> VariableEditor:
            """insert an type combo box into a `editor`'s layout."""
            form = editor.layout().itemAt(0)
            assert isinstance(form, QFormLayout)
            typecb = QComboBox(objectName="type-combo")
            typecb.addItem(variable_icon(Categorical), "Categorical", Categorical)
            typecb.addItem(variable_icon(Real), "Numeric", Real)
            typecb.addItem(variable_icon(String), "Text", String)
            typecb.addItem(variable_icon(Time), "Time", Time)
            typecb.activated[int].connect(self.__reinterpret_activated)
            form.insertRow(1, "Type:", typecb)
            # Insert the typecb after name edit in the focus chain
            name_edit = editor.findChild(QLineEdit, )
            if name_edit is not None:
                QWidget.setTabOrder(name_edit, typecb)
            return editor
        # This is ugly. Create an editor for each type and insert a type
        # selection combo box into its layout. Switch between widgets
        # on type change.
        self.disc_edit = dedit = decorate(DiscreteVariableEditor())
        cedit = decorate(ContinuousVariableEditor())
        tedit = decorate(TimeVariableEditor())
        sedit = decorate(VariableEditor())

        for ed in [dedit, cedit, tedit, sedit]:
            ed.variable_changed.connect(self.variable_changed)

        self.layout().addWidget(dedit)
        self.layout().addWidget(cedit)
        self.layout().addWidget(sedit)
        self.layout().addWidget(tedit)

    def set_data(self, data, transform=()):  # pylint: disable=arguments-differ
        # type: (Optional[DataVector], Sequence[Transform]) -> None
        """
        Set the editor data.

        Note
        ----
        This must be a `DataVector` as the vector's values are needed for type
        reinterpretation/casts.

        If the `transform` sequence contains ReinterpretTransform then it
        must be in the first position.
        """
        type_transform = None  # type: Optional[ReinterpretTransform]
        if transform:
            _tr = transform[0]
            if isinstance(_tr, ReinterpretTransformTypes):
                type_transform = _tr
                transform = transform[1:]
            assert not any(isinstance(t, ReinterpretTransformTypes)
                           for t in transform)
        self.__transform = type_transform
        self.__data = data
        self.var = data.vtype if data is not None else None

        if type_transform is not None and data is not None:
            data = type_transform(data)
        if data is not None:
            var = data.vtype
        else:
            var = None
        index = self._editors.get(type(var), -1)
        self.layout().setCurrentIndex(index)
        if index != -1:
            w = self.layout().currentWidget()
            assert isinstance(w, VariableEditor)
            if isinstance(var, Categorical):
                w.set_data_categorical(var, data.data(), transform=transform)
            else:
                w.set_data(var, transform=transform)
            self.__history[var] = tuple(transform)
            cb = w.findChild(QComboBox, "type-combo")
            cb.setCurrentIndex(index)

    def get_data(self):
        # type: () -> Tuple[Variable, Sequence[Transform]]
        editor = self.layout().currentWidget()  # type: VariableEditor
        var, tr = editor.get_data()
        if type(var) != type(self.var):  # pylint: disable=unidiomatic-typecheck
            assert self.__transform is not None
            var = self.var
            tr = [self.__transform, *tr]
        return var, tr

    def __reinterpret_activated(self, index):
        layout = self.layout()
        assert isinstance(layout, QStackedLayout)
        if index == layout.currentIndex():
            return
        current = layout.currentWidget()
        assert isinstance(current, VariableEditor)
        Specific = {
            Categorical: CategoricalTransformTypes
        }
        _var, _tr = current.get_data()
        if _var is not None:
            self.__history[_var] = _tr

        var = self.var
        transform = self.__transform
        # take/preserve the general transforms that apply to all types
        specific = Specific.get(type(var), ())
        _tr = [t for t in _tr if not isinstance(t, specific)]

        layout.setCurrentIndex(index)
        w = layout.currentWidget()
        cb = w.findChild(QComboBox, "type-combo")
        cb.setCurrentIndex(index)
        cb.setFocus()
        target = cb.itemData(index, Qt.UserRole)
        assert issubclass(target, VariableTypes)
        if not isinstance(var, target):
            if target == Real:
                transform = AsContinuous()
            elif target == Categorical:
                transform = AsCategorical()
            elif target == Time:
                transform = AsTime()
            elif target == String:
                transform = AsString()
        else:
            transform = None
            var = self.var

        self.__transform = transform
        data = None
        if transform is not None and self.__data is not None:
            data = transform(self.__data)
            var = data.vtype

        if var in self.__history:
            tr = self.__history[var]
        else:
            tr = []
        # type specific transform
        specific = Specific.get(type(var), ())
        # merge tr and _tr
        tr = _tr + [t for t in tr if isinstance(t, specific)]
        with disconnected(
                w.variable_changed, self.variable_changed,
                Qt.UniqueConnection
        ):
            if isinstance(w, DiscreteVariableEditor):
                data = data or self.__data
                w.set_data_categorical(var, data.data(), transform=tr)
            else:
                w.set_data(var, transform=tr)
        self.variable_changed.emit()

    def set_merge_context(self, merge_context):
        self.disc_edit.merge_dialog_settings = merge_context

    def get_merge_context(self):
        return self.disc_edit.merge_dialog_settings


class OWEditDomain(widget.OWWidget):
    name = "Edit Domain"
    description = "Rename variables, edit categories and variable annotations."
    icon = "icons/EditDomain.svg"
    priority = 3125
    keywords = ["rename", "drop", "reorder", "order"]

    class Inputs:
        data = Input("Data", Orange.data.Table)

    class Outputs:
        data = Output("Data", Orange.data.Table)

    class Error(widget.OWWidget.Error):
        duplicate_var_name = widget.Msg("A variable name is duplicated.")

    settingsHandler = settings.DomainContextHandler()
    settings_version = 2

    _domain_change_store = settings.ContextSetting({})
    _selected_item = settings.ContextSetting(None)  # type: Optional[Tuple[str, int]]
    _merge_dialog_settings = settings.ContextSetting({})
    output_table_name = settings.ContextSetting("")

    want_main_area = False

    def __init__(self):
        super().__init__()
        self.data = None  # type: Optional[Orange.data.Table]
        #: The current selected variable index
        self.selected_index = -1
        self._invalidated = False
        self.typeindex = 0

        main = gui.hBox(self.controlArea, spacing=6)
        box = gui.vBox(main, "Variables")

        self.variables_model = VariableListModel(parent=self)
        self.variables_view = self.domain_view = QListView(
            selectionMode=QListView.SingleSelection,
            uniformItemSizes=True,
        )
        self.variables_view.setItemDelegate(VariableEditDelegate(self))
        self.variables_view.setModel(self.variables_model)
        self.variables_view.selectionModel().selectionChanged.connect(
            self._on_selection_changed
        )
        box.layout().addWidget(self.variables_view)

        box = gui.vBox(main, "Edit")
        self._editor = ReinterpretVariableEditor()
        box.layout().addWidget(self._editor)

        self.le_output_name = gui.lineEdit(
            self.buttonsArea, self, "output_table_name", "Output table name: ",
            orientation=Qt.Horizontal)

        gui.rubber(self.buttonsArea)

        bbox = gui.hBox(self.buttonsArea)
        breset_all = gui.button(
            bbox, self, "Reset All",
            objectName="button-reset-all",
            toolTip="Reset all variables to their input state.",
            autoDefault=False,
            callback=self.reset_all
        )
        breset = gui.button(
            bbox, self, "Reset Selected",
            objectName="button-reset",
            toolTip="Rest selected variable to its input state.",
            autoDefault=False,
            callback=self.reset_selected
        )
        bapply = gui.button(
            bbox, self, "Apply",
            objectName="button-apply",
            toolTip="Apply changes and commit data on output.",
            default=True,
            autoDefault=False,
            callback=self.commit
        )

        self.variables_view.setFocus(Qt.NoFocusReason)  # initial focus

    @Inputs.data
    def set_data(self, data):
        """Set input dataset."""
        self.closeContext()
        self.clear()
        self.data = data

        if self.data is not None:
            self.setup_model(data)
            self.le_output_name.setPlaceholderText(data.name)
            self.openContext(self.data)
            self._editor.set_merge_context(self._merge_dialog_settings)
            self._restore()
        else:
            self.le_output_name.setPlaceholderText("")

        self.commit()

    def clear(self):
        """Clear the widget state."""
        self.data = None
        self.variables_model.clear()
        self.clear_editor()
        assert self.selected_index == -1
        self.selected_index = -1

        self._selected_item = None
        self._domain_change_store = {}
        self._merge_dialog_settings = {}

    def reset_selected(self):
        """Reset the currently selected variable to its original state."""
        ind = self.selected_var_index()
        if ind >= 0:
            model = self.variables_model
            midx = model.index(ind)
            var = midx.data(Qt.EditRole)
            tr = midx.data(TransformRole)
            if not tr:
                return  # nothing to reset
            editor = self._editor
            with disconnected(editor.variable_changed,
                              self._on_variable_changed):
                model.setData(midx, [], TransformRole)
                editor.set_data(var, transform=[])
            self._invalidate()

    def reset_all(self):
        """Reset all variables to their original state."""
        self._domain_change_store = {}
        if self.data is not None:
            model = self.variables_model
            for i in range(model.rowCount()):
                midx = model.index(i)
                model.setData(midx, [], TransformRole)
            index = self.selected_var_index()
            if index >= 0:
                self.open_editor(index)
            self._invalidate()

    def selected_var_index(self):
        """Return the current selected variable index."""
        rows = self.variables_view.selectedIndexes()
        assert len(rows) <= 1
        return rows[0].row() if rows else -1

    def setup_model(self, data: Orange.data.Table):
        model = self.variables_model
        vars_ = []
        columns = []
        for i, _, var, coldata in enumerate_columns(data):
            var = abstract(var)
            vars_.append(var)
            if isinstance(var, Categorical):
                data = CategoricalVector(var, coldata)
            elif isinstance(var, Real):
                data = RealVector(var, coldata)
            elif isinstance(var, Time):
                data = TimeVector(var, coldata)
            elif isinstance(var, String):
                data = StringVector(var, coldata)
            columns.append(data)

        model[:] = vars_
        for i, d in enumerate(columns):
            model.setData(model.index(i), d, Qt.EditRole)

    def _restore(self, ):
        """
        Restore the edit transform from saved state.
        """
        model = self.variables_model
        for i in range(model.rowCount()):
            midx = model.index(i, 0)
            coldesc = model.data(midx, Qt.EditRole)  # type: DataVector
            tr = self._restore_transform(coldesc.vtype)
            if tr:
                model.setData(midx, tr, TransformRole)

        # Restore the current variable selection
        i = -1
        if self._selected_item is not None:
            for i, vec in enumerate(model):
                if vec.vtype.name_type() == self._selected_item:
                    break
        if i == -1 and model.rowCount():
            i = 0

        if i != -1:
            itemmodels.select_row(self.variables_view, i)

    def _on_selection_changed(self):
        self.selected_index = self.selected_var_index()
        if self.selected_index != -1:
            self._selected_item = self.variables_model[self.selected_index].vtype.name_type()
        else:
            self._selected_item = None
        self.open_editor(self.selected_index)

    def open_editor(self, index):
        # type: (int) -> None
        self.clear_editor()
        model = self.variables_model
        if not 0 <= index < model.rowCount():
            return
        idx = model.index(index, 0)
        vector = model.data(idx, Qt.EditRole)
        tr = model.data(idx, TransformRole)
        if tr is None:
            tr = []
        editor = self._editor
        editor.set_data(vector, transform=tr)
        editor.variable_changed.connect(
            self._on_variable_changed, Qt.UniqueConnection
        )

    def clear_editor(self):
        current = self._editor
        try:
            current.variable_changed.disconnect(self._on_variable_changed)
        except TypeError:
            pass
        current.set_data(None)
        current.layout().currentWidget().clear()

    @Slot()
    def _on_variable_changed(self):
        """User edited the current variable in editor."""
        assert 0 <= self.selected_index <= len(self.variables_model)
        editor = self._editor
        var, transform = editor.get_data()
        model = self.variables_model
        midx = model.index(self.selected_index, 0)
        model.setData(midx, transform, TransformRole)
        self._store_transform(var, transform)
        self._invalidate()

    def _store_transform(self, var, transform):
        # type: (Variable, List[Transform]) -> None
        self._domain_change_store[deconstruct(var)] = [deconstruct(t) for t in transform]

    def _restore_transform(self, var):
        # type: (Variable) -> List[Transform]
        tr_ = self._domain_change_store.get(deconstruct(var), [])
        tr = []

        for t in tr_:
            try:
                tr.append(reconstruct(*t))
            except (NameError, TypeError) as err:
                warnings.warn(
                    "Failed to restore transform: {}, {!r}".format(t, err),
                    UserWarning, stacklevel=2
                )
        return tr

    def _invalidate(self):
        self._set_modified(True)

    def _set_modified(self, state):
        self._invalidated = state
        b = self.findChild(QPushButton, "button-apply")
        if isinstance(b, QPushButton):
            f = b.font()
            f.setItalic(state)
            b.setFont(f)

    def commit(self):
        """
        Apply the changes to the input data and send the changed data to output.
        """
        self._set_modified(False)
        self.Error.duplicate_var_name.clear()

        data = self.data
        if data is None:
            self.Outputs.data.send(None)
            return
        model = self.variables_model

        def state(i):
            # type: (int) -> Tuple[DataVector, List[Transform]]
            midx = self.variables_model.index(i, 0)
            return (model.data(midx, Qt.EditRole),
                    model.data(midx, TransformRole))

        state = [state(i) for i in range(model.rowCount())]
        input_vars = data.domain.variables + data.domain.metas
        if self.output_table_name in ("", data.name) \
                and not any(requires_transform(var, trs)
                            for var, (_, trs) in zip(input_vars, state)):
            self.Outputs.data.send(data)
            return

        assert all(v_.vtype.name == v.name
                   for v, (v_, _) in zip(input_vars, state))
        output_vars = []
        unlinked_vars = []
        unlink_domain = False
        for (_, tr), v in zip(state, input_vars):
            if tr:
                var = apply_transform(v, data, tr)
                if requires_unlink(v, tr):
                    unlinked_var = var.copy(compute_value=None)
                    unlink_domain = True
                else:
                    unlinked_var = var
            else:
                unlinked_var = var = v
            output_vars.append(var)
            unlinked_vars.append(unlinked_var)

        if len(output_vars) != len({v.name for v in output_vars}):
            self.Error.duplicate_var_name()
            self.Outputs.data.send(None)
            return

        domain = data.domain
        nx = len(domain.attributes)
        ny = len(domain.class_vars)

        def construct_domain(vars_list):
            # Move non primitive Xs, Ys to metas (if they were changed)
            Xs = [v for v in vars_list[:nx] if v.is_primitive()]
            Ys = [v for v in vars_list[nx: nx + ny] if v.is_primitive()]
            Ms = vars_list[nx + ny:] + \
                 [v for v in vars_list[:nx + ny] if not  v.is_primitive()]
            return Orange.data.Domain(Xs, Ys, Ms)

        domain = construct_domain(output_vars)
        new_data = data.transform(domain)
        if unlink_domain:
            unlinked_domain = construct_domain(unlinked_vars)
            new_data = new_data.from_numpy(
                unlinked_domain,
                new_data.X, new_data.Y, new_data.metas, new_data.W,
                new_data.attributes, new_data.ids
            )
        if self.output_table_name:
            new_data.name = self.output_table_name
        self.Outputs.data.send(new_data)

    def sizeHint(self):
        sh = super().sizeHint()
        return sh.expandedTo(QSize(660, 550))

    def storeSpecificSettings(self):
        """
        Update setting before context closes - also when widget closes.
        """
        self._merge_dialog_settings = self._editor.get_merge_context()

    def send_report(self):

        if self.data is not None:
            model = self.variables_model
            state = ((model.data(midx, Qt.EditRole),
                      model.data(midx, TransformRole))
                     for i in range(model.rowCount())
                     for midx in [model.index(i)])
            parts = []
            for vector, trs in state:
                if trs:
                    parts.append(report_transform(vector.vtype, trs))
            if parts:
                html = ("<ul>" +
                        "".join(map("<li>{}</li>".format, parts)) +
                        "</ul>")
            else:
                html = "No changes"
            self.report_raw("", html)
        else:
            self.report_data(None)

    @classmethod
    def migrate_context(cls, context, version):
        # pylint: disable=bad-continuation
        if version is None or version <= 1:
            hints_ = context.values.get("domain_change_hints", ({}, -2))[0]
            store = []
            ns = "Orange.data.variable"
            mapping = {
                "DiscreteVariable":
                    lambda name, args, attrs:
                        ("Categorical", (name, tuple(args[0][1]), ())),
                "TimeVariable":
                    lambda name, _, attrs:
                        ("Time", (name, ())),
                "ContinuousVariable":
                    lambda name, _, attrs:
                        ("Real", (name, (3, "f"), ())),
                "StringVariable":
                    lambda name, _, attrs:
                        ("String", (name, ())),
            }
            for (module, class_name, *rest), target in hints_.items():
                if module != ns:
                    continue
                f = mapping.get(class_name)
                if f is None:
                    continue
                trs = []
                key_mapped = f(*rest)
                item_mapped = f(*target[2:])
                src = reconstruct(*key_mapped)   # type: Variable
                dst = reconstruct(*item_mapped)  # type: Variable
                if src.name != dst.name:
                    trs.append(Rename(dst.name))
                if src.annotations != dst.annotations:
                    trs.append(Annotate(dst.annotations))
                if isinstance(src, Categorical):
                    if src.categories != dst.categories:
                        assert len(src.categories) == len(dst.categories)
                        trs.append(CategoriesMapping(
                            list(zip(src.categories, dst.categories))))
                store.append((deconstruct(src), [deconstruct(tr) for tr in trs]))
            context.values["_domain_change_store"] = (dict(store), -2)


def enumerate_columns(
        table: Orange.data.Table
) -> Iterable[Tuple[int, str, Orange.data.Variable, Callable[[], ndarray]]]:
    domain = table.domain
    for i, (var, role) in enumerate(
            chain(zip(domain.attributes, repeat("x")),
                  zip(domain.class_vars, repeat("y")),
                  zip(domain.metas, repeat("m"))),
    ):
        if i >= len(domain.variables):
            i = len(domain.variables) - i - 1
        data = partial(table_column_data, table, i)
        yield i, role, var, data


def table_column_data(
        table: Orange.data.Table,
        var: Union[Orange.data.Variable, int],
        dtype=None
) -> MArray:
    col, copy = table.get_column_view(var)
    var = table.domain[var]  # type: Orange.data.Variable
    if var.is_primitive() and not np.issubdtype(col.dtype, np.inexact):
        col = col.astype(float)
        copy = True

    if dtype is None:
        if isinstance(var, Orange.data.TimeVariable):
            dtype = np.dtype("M8[us]")
            col = col * 1e6
        elif isinstance(var, Orange.data.ContinuousVariable):
            dtype = np.dtype(float)
        elif isinstance(var, Orange.data.DiscreteVariable):
            _values = tuple(var.values)
            _n_values = len(_values)
            dtype = np.dtype(int, metadata={
                "__formatter": lambda i: _values[i] if 0 <= i < _n_values else "?"
            })
        elif isinstance(var, Orange.data.StringVariable):
            dtype = np.dtype(object)
        else:
            assert False
    mask = orange_isna(var, col)

    if dtype != col.dtype:
        col = col.astype(dtype)
        copy = True

    if not copy:
        col = col.copy()
    return MArray(col, mask=mask)


def report_transform(var, trs):
    # type: (Variable, List[Transform]) -> str
    """
    Return a html fragment summarizing the changes applied by `trs` list.

    Parameters
    ----------
    var : Variable
        A variable descriptor no which trs operates
    trs : List[Transform]
        A non empty list of `Transform` instances.

    Returns
    -------
    report : str
    """
    # pylint: disable=too-many-branches
    ReinterpretTypeCode = {
        AsCategorical:  "C", AsContinuous: "N", AsString: "S", AsTime: "T",
    }

    def type_char(value: ReinterpretTransform) -> str:
        return ReinterpretTypeCode.get(type(value), "?")

    def strike(text):
        return "<s>{}</s>".format(escape(text))

    def i(text):
        return "<i>{}</i>".format(escape(text))

    def text(text):
        return "<span>{}</span>".format(escape(text))
    assert trs
    rename = annotate = catmap = unlink = None
    reinterpret = None

    for tr in trs:
        if isinstance(tr, Rename):
            rename = tr
        elif isinstance(tr, Annotate):
            annotate = tr
        elif isinstance(tr, CategoriesMapping):
            catmap = tr
        elif isinstance(tr, Unlink):
            unlink = tr
        elif isinstance(tr, ReinterpretTransformTypes):
            reinterpret = tr

    if reinterpret is not None:
        header = "{} → ({}) {}".format(
            var.name, type_char(reinterpret),
            rename.name if rename is not None else var.name
        )
    elif rename is not None:
        header = "{} → {}".format(var.name, rename.name)
    else:
        header = var.name
    if unlink is not None:
        header += "(unlinked from source)"

    values_section = None
    if catmap is not None:
        values_section = ("Values", [])
        lines = values_section[1]
        for ci, cj in catmap.mapping:
            if ci is None:
                item = cj + ("&nbsp;" * 3) + "(added)"
            elif cj is None:
                item = strike(ci)
            else:
                item = ci + " → " + cj
            lines.append(item)

    annotate_section = None
    if annotate is not None:
        annotate_section = ("Labels", [])
        lines = annotate_section[1]
        old = dict(var.annotations)
        new = dict(annotate.annotations)
        for name in sorted(set(old) - set(new)):
            lines.append(
                "<s>" + i(name) + " : " + text(old[name]) + "</s>"
            )
        for name in sorted(set(new) - set(old)):
            lines.append(
                i(name) + " : " + text(new[name]) + "&nbsp;" * 3 + i("(new)")
            )

        for name in sorted(set(new) & set(old)):
            if new[name] != old[name]:
                lines.append(
                    i(name) + " : " + text(old[name]) + " → " + text(new[name])
                )

    html = ["<div style='font-weight: bold;'>{}</div>".format(header)]
    for title, contents in filter(None, [values_section, annotate_section]):
        section_header = "<div>{}:</div>".format(title)
        section_contents = "<br/>\n".join(contents)
        html.append(section_header)
        html.append(
            "<div style='padding-left: 1em;'>" +
            section_contents +
            "</div>"
        )
    return "\n".join(html)


def abstract(var):
    # type: (Orange.data.Variable) -> Variable
    """
    Return `Varaible` descriptor for an `Orange.data.Variable` instance.

    Parameters
    ----------
    var : Orange.data.Variable

    Returns
    -------
    var : Variable
    """
    annotations = tuple(sorted(
        (key, str(value))
        for key, value in var.attributes.items()
    ))
    linked = var.compute_value is not None
    if isinstance(var, Orange.data.DiscreteVariable):
        return Categorical(var.name, tuple(var.values), annotations, linked)
    elif isinstance(var, Orange.data.TimeVariable):
        return Time(var.name, annotations, linked)
    elif isinstance(var, Orange.data.ContinuousVariable):
        return Real(var.name, (var.number_of_decimals, 'f'), annotations, linked)
    elif isinstance(var, Orange.data.StringVariable):
        return String(var.name, annotations, linked)
    else:
        raise TypeError


def _parse_attributes(mapping):
    # type: (Iterable[Tuple[str, str]]) -> Dict[str, Any]
    # Use the same functionality that parses attributes
    # when reading text files
    return Orange.data.Flags([
        "{}={}".format(*item) for item in mapping
    ]).attributes


def apply_transform(var, table, trs):
    # type: (Orange.data.Variable, Orange.data.Table, List[Transform]) -> Orange.data.Variable
    """
    Apply a list of `Transform` instances on an `Orange.data.Variable`.
    """
    if trs and isinstance(trs[0], ReinterpretTransformTypes):
        reinterpret, trs = trs[0], trs[1:]
        coldata = table_column_data(table, var)
        var = apply_reinterpret(var, reinterpret, coldata)
    if trs:
        return apply_transform_var(var, trs)
    else:
        return var


def requires_unlink(var: Orange.data.Variable, trs: List[Transform]) -> bool:
    # Variable is only unlinked if it has compute_value  or if it has other
    # transformations (that might had added compute_value)
    return trs is not None \
           and any(isinstance(tr, Unlink) for tr in trs) \
           and (var.compute_value is not None or len(trs) > 1)


def requires_transform(var: Orange.data.Variable, trs: List[Transform]) -> bool:
    # Unlink is treated separately: Unlink is required only if the variable
    # has compute_value. Hence tranform is required if it has any
    # transformations other than Unlink, or if unlink is indeed required.
    return trs is not None and (
        not all(isinstance(tr, Unlink) for tr in trs)
        or requires_unlink(var, trs)
    )


@singledispatch
def apply_transform_var(var, trs):
    # type: (Orange.data.Variable, List[Transform]) -> Orange.data.Variable
    raise NotImplementedError


@apply_transform_var.register(Orange.data.DiscreteVariable)
def apply_transform_discete(var, trs):
    # type: (Orange.data.DiscreteVariable, List[Transform]) -> Orange.data.Variable
    # pylint: disable=too-many-branches
    name, annotations = var.name, var.attributes
    mapping = None
    for tr in trs:
        if isinstance(tr, Rename):
            name = tr.name
        elif isinstance(tr, CategoriesMapping):
            mapping = tr.mapping
        elif isinstance(tr, Annotate):
            annotations = _parse_attributes(tr.annotations)

    source_values = var.values
    if mapping is not None:
        dest_values = list(unique(cj for ci, cj in mapping if cj is not None))
    else:
        dest_values = var.values

    def positions(values):
        rval = {c: i for i, c in enumerate(values)}
        assert len(rval) == len(values)
        return rval
    source_codes = positions(source_values)
    dest_codes = positions(dest_values)
    if mapping is not None:
        # construct a lookup table
        lookup = np.full(len(source_values), np.nan, dtype=float)
        for ci, cj in mapping:
            if ci is not None and cj is not None:
                i, j = source_codes[ci], dest_codes[cj]
                lookup[i] = j
        lookup = Lookup(var, lookup)
    else:
        lookup = Identity(var)
    variable = Orange.data.DiscreteVariable(
        name, values=dest_values, compute_value=lookup
    )
    variable.attributes.update(annotations)
    return variable


@apply_transform_var.register(Orange.data.ContinuousVariable)
def apply_transform_continuous(var, trs):
    # type: (Orange.data.ContinuousVariable, List[Transform]) -> Orange.data.Variable
    name, annotations = var.name, var.attributes
    for tr in trs:
        if isinstance(tr, Rename):
            name = tr.name
        elif isinstance(tr, Annotate):
            annotations = _parse_attributes(tr.annotations)
    variable = Orange.data.ContinuousVariable(
        name=name, compute_value=Identity(var)
    )
    variable.attributes.update(annotations)
    return variable


@apply_transform_var.register(Orange.data.TimeVariable)
def apply_transform_time(var, trs):
    # type: (Orange.data.TimeVariable, List[Transform]) -> Orange.data.Variable
    name, annotations = var.name, var.attributes
    for tr in trs:
        if isinstance(tr, Rename):
            name = tr.name
        elif isinstance(tr, Annotate):
            annotations = _parse_attributes(tr.annotations)
    variable = Orange.data.TimeVariable(
        name=name, have_date=var.have_date, have_time=var.have_time,
        compute_value=Identity(var)
    )
    variable.attributes.update(annotations)
    return variable


@apply_transform_var.register(Orange.data.StringVariable)
def apply_transform_string(var, trs):
    # type: (Orange.data.StringVariable, List[Transform]) -> Orange.data.Variable
    name, annotations = var.name, var.attributes
    out_type = Orange.data.StringVariable
    compute_value = Identity
    for tr in trs:
        if isinstance(tr, Rename):
            name = tr.name
        elif isinstance(tr, Annotate):
            annotations = _parse_attributes(tr.annotations)
        elif isinstance(tr, StrpTime):
            out_type = partial(
                Orange.data.TimeVariable, have_date=tr.have_date, have_time=tr.have_time
            )
            compute_value = partial(ReparseTimeTransform, tr=tr)
    variable = out_type(name=name, compute_value=compute_value(var))
    variable.attributes.update(annotations)
    return variable


def make_dict_mapper(
        mapping: Mapping, dtype: Optional[DType] = None
) -> Callable:
    """
    Wrap a `mapping` into a callable ufunc-like function with
    `out`, `dtype`, `where`, ... parameters. If `dtype` is passed to
    `make_dict_mapper` it is used as a the default return dtype,
    otherwise the default dtype is `object`.
    """
    return frompyfunc(mapping.__getitem__, 1, 1, dtype)


as_string = np.frompyfunc(str, 1, 1)
parse_float = ftry(float, ValueError, float("nan"))

_parse_float = np.frompyfunc(parse_float, 1, 1)


def as_float_or_nan(
        arr: ndarray, out: Optional[ndarray] = None, where: Optional[ndarray] = True,
        dtype=None, **kwargs
) -> ndarray:
    """
    Convert elements of the input array using builtin `float`, fill elements
    where conversion failed with NaN.
    """
    if out is None:
        out = np.full(arr.shape, np.nan, float if dtype is None else dtype)
    if np.issubdtype(arr.dtype, np.inexact) or \
            np.issubdtype(arr.dtype, np.integer):
        np.copyto(out, arr, casting="unsafe", where=where)
        return out
    return _parse_float(arr, out, where=where, casting="unsafe", **kwargs)


def copy_attributes(dst: V, src: Orange.data.Variable) -> V:
    # copy `attributes` and `sparse` members from src to dst
    dst.attributes = dict(src.attributes)
    dst.sparse = src.sparse
    return dst


# Building (and applying) concrete type transformations on Table columns

@singledispatch
def apply_reinterpret(var, tr, data):
    # type: (Orange.data.Variable, ReinterpretTransform, MArray) -> Orange.data.Variable
    """
    Apply a re-interpret transform to an `Orange.data.Table`'s column
    """
    raise NotImplementedError


@apply_reinterpret.register(Orange.data.DiscreteVariable)
def apply_reinterpret_d(var, tr, data):
    # type: (Orange.data.DiscreteVariable, ReinterpretTransform, ndarray) -> Orange.data.Variable
    if isinstance(tr, AsCategorical):
        return var
    elif isinstance(tr, (AsString, AsTime)):
        # TimeVar will be interpreted by StrpTime later
        f = Lookup(var, np.array(var.values, dtype=object), unknown="")
        rvar = Orange.data.StringVariable(name=var.name, compute_value=f)
    elif isinstance(tr, AsContinuous):
        f = Lookup(var, np.array(list(map(parse_float, var.values))),
                   unknown=np.nan)
        rvar = Orange.data.ContinuousVariable(
            name=var.name, compute_value=f, sparse=var.sparse
        )
    else:
        assert False
    return copy_attributes(rvar, var)


@apply_reinterpret.register(Orange.data.ContinuousVariable)
def apply_reinterpret_c(var, tr, data: MArray):
    if isinstance(tr, AsCategorical):
        # This is ill-defined and should not result in a 'compute_value'
        # (post-hoc expunge from the domain once translated?)
        values, index = categorize_unique(data)
        coldata = index.astype(float)
        coldata[index.mask] = np.nan
        tr = LookupMappingTransform(
            var, {v: i for i, v in enumerate(values)}, dtype=np.float64, unknown=np.nan
        )
        values = tuple(as_string(values))
        rvar = Orange.data.DiscreteVariable(
            name=var.name, values=values, compute_value=tr
        )
    elif isinstance(tr, AsContinuous):
        return var
    elif isinstance(tr, AsString):
        # TimeVar will be interpreted by StrpTime later
        tstr = ToStringTransform(var)
        rvar = Orange.data.StringVariable(name=var.name, compute_value=tstr)
    elif isinstance(tr, AsTime):
        rvar = Orange.data.TimeVariable(name=var.name, compute_value=Identity(var))
    else:
        assert False
    return copy_attributes(rvar, var)


@apply_reinterpret.register(Orange.data.StringVariable)
def apply_reinterpret_s(var: Orange.data.StringVariable, tr, data: MArray):
    if isinstance(tr, AsCategorical):
        # This is ill-defined and should not result in a 'compute_value'
        # (post-hoc expunge from the domain once translated?)
        _, values = categorical_from_vector(data)
        mapping = {v: float(i) for i, v in enumerate(values)}
        tr = LookupMappingTransform(var, mapping)
        rvar = Orange.data.DiscreteVariable(
            name=var.name, values=values, compute_value=tr
        )
    elif isinstance(tr, AsContinuous):
        rvar = Orange.data.ContinuousVariable(
            var.name, compute_value=ToContinuousTransform(var)
        )
    elif isinstance(tr, (AsString, AsTime)):
        # TimeVar will be interpreted by StrpTime later
        return var
    else:
        assert False
    return copy_attributes(rvar, var)


@apply_reinterpret.register(Orange.data.TimeVariable)
def apply_reinterpret_t(var: Orange.data.TimeVariable, tr, data):
    if isinstance(tr, AsCategorical):
        values, _ = categorize_unique(data)
        or_values = values.astype(float) / 1e6
        mapping = {v: i for i, v in enumerate(or_values)}
        tr = LookupMappingTransform(var, mapping)
        values = tuple(as_string(values))
        rvar = Orange.data.DiscreteVariable(
            name=var.name, values=values, compute_value=tr
        )
    elif isinstance(tr, AsContinuous):
        rvar = Orange.data.TimeVariable(
            name=var.name, compute_value=Identity(var)
        )
    elif isinstance(tr, AsString):
        rvar = Orange.data.StringVariable(
            name=var.name, compute_value=ToStringTransform(var)
        )
    elif isinstance(tr, AsTime):
        return var
    else:
        assert False
    return copy_attributes(rvar, var)


def orange_isna(variable: Orange.data.Variable, data: ndarray) -> ndarray:
    """
    Return a bool mask masking N/A elements in `data` for the `variable`.
    """
    if variable.is_primitive():
        return np.isnan(data)
    else:
        return data == variable.Unknown


class ToStringTransform(Transformation):
    """
    Transform a variable to string.
    """
    def transform(self, c):
        if self.variable.is_string:
            return c
        elif self.variable.is_discrete or self.variable.is_time:
            r = column_str_repr(self.variable, c)
        elif self.variable.is_continuous:
            r = as_string(c)
        mask = orange_isna(self.variable, c)
        return np.where(mask, "", r)


class ToContinuousTransform(Transformation):
    def transform(self, c):
        if self.variable.is_time:
            return c
        elif self.variable.is_continuous:
            return c
        elif self.variable.is_discrete:
            lookup = Lookup(
                self.variable, as_float_or_nan(self.variable.values),
                unknown=np.nan
            )
            return lookup.transform(c)
        elif self.variable.is_string:
            return as_float_or_nan(c)
        else:
            raise TypeError


def datetime_to_epoch(dti: pd.DatetimeIndex, only_time) -> np.ndarray:
    """Convert datetime to epoch"""
    delta = dti - (dti.normalize() if only_time else pd.Timestamp("1970-01-01"))
    return (delta / pd.Timedelta("1s")).values


class ReparseTimeTransform(Transformation):
    """
    Re-parse the column's string repr as datetime.
    """
    def __init__(self, variable, tr):
        super().__init__(variable)
        self.tr = tr

    def transform(self, c):
        # if self.formats is none guess format option is selected
        formats = self.tr.formats if self.tr.formats is not None else [None]
        for f in formats:
            d = pd.to_datetime(c, errors="coerce", format=f)
            if pd.notnull(d).any():
                return datetime_to_epoch(d, only_time=not self.tr.have_date)
        return np.nan


# Alias for back compatibility (unpickling transforms)
LookupMappingTransform = MappingTransform


@singledispatch
def column_str_repr(var: Orange.data.Variable, coldata: ndarray) -> ndarray:
    """Return a array of str representations of coldata for the `variable."""
    _f = np.frompyfunc(var.repr_val, 1, 1)
    return _f(coldata)


@column_str_repr.register(Orange.data.DiscreteVariable)
def column_str_repr_discrete(
        var: Orange.data.DiscreteVariable, coldata: ndarray
) -> ndarray:
    values = np.array(var.values, dtype=object)
    lookup = Lookup(var, values, "?")
    return lookup.transform(coldata)


@column_str_repr.register(Orange.data.StringVariable)
def column_str_repr_string(
        var: Orange.data.StringVariable, coldata: ndarray
) -> ndarray:
    return np.where(coldata == var.Unknown, "?", coldata)


if __name__ == "__main__":  # pragma: no cover
    WidgetPreview(OWEditDomain).run(Orange.data.Table("iris"))

```
--- END FILE ---

Please first localize the bug based on the issue statement, and then generate *SEARCH/REPLACE* edits to fix the issue.

Every *SEARCH/REPLACE* edit must use this format:
1. The file path
2. The start of search block: <<<<<<< SEARCH
3. A contiguous chunk of lines to search for in the existing source code
4. The dividing line: =======
5. The lines to replace into the source code
6. The end of the replace block: >>>>>>> REPLACE

Here is an example:

```python
### mathweb/flask/app.py
<<<<<<< SEARCH
from flask import Flask
=======
import math
from flask import Flask
>>>>>>> REPLACE
```
</solution>

Please note that the *SEARCH/REPLACE* edit REQUIRES PROPER INDENTATION. If you would like to add the line '        print(x)', you must fully write that out, with all those spaces before the code!
Wrap each *SEARCH/REPLACE* edit in a code block as shown in the example above. If you have multiple *SEARCH/REPLACE* edits, use a separate code block for each one.
[ISSUE]
**Title:** Error When Converting Timezone-Aware Datetime to Epoch

**Description:**
When attempting to convert datetime objects that include timezone information to epoch values, a `TypeError` is raised. This issue arises because the subtraction operation fails when the datetime index has a timezone, but the initial timestamp does not.

**Example Buggy Code:**
```python
import pandas as pd
import numpy as np

def datetime_to_epoch(dti: pd.DatetimeIndex, only_time) -> np.ndarray:
    delta = dti - pd.Timestamp("1970-01-01")
    return (delta / pd.Timedelta("1s")).values

dti = pd.DatetimeIndex(["2021-02-08 01:02:03+01:00", "2021-02-07 01:02:03+01:00"])
epoch = datetime_to_epoch(dti, only_time=False)
```

**Expected Behavior:**
The function should correctly compute the epoch times for timezone-aware datetime objects without raising any errors.

**Actual Behavior:**
A `TypeError` is raised with the message:
```
TypeError: Timestamp subtraction must have the same timezones or no timezones
```
This prevents the successful conversion of timezone-aware datetime objects to epoch values.
[/ISSUE]
{
    "TestReport.test_annotate": "PASSED",
    "TestReport.test_categorical_merge_mapping": "PASSED",
    "TestReport.test_categories_mapping": "PASSED",
    "TestReport.test_reinterpret": "PASSED",
    "TestReport.test_rename": "PASSED",
    "TestReport.test_unlinke": "PASSED",
    "TestOWEditDomain.test_annotation_bool": "PASSED",
    "TestOWEditDomain.test_duplicate_names": "PASSED",
    "TestOWEditDomain.test_image_export": "PASSED",
    "TestOWEditDomain.test_input_data": "PASSED",
    "TestOWEditDomain.test_input_data_disconnect": "PASSED",
    "TestOWEditDomain.test_input_from_owcolor": "PASSED",
    "TestOWEditDomain.test_list_attributes_remain_lists": "PASSED",
    "TestOWEditDomain.test_msg_base_class": "PASSED",
    "TestOWEditDomain.test_output_data": "PASSED",
    "TestOWEditDomain.test_restore": "PASSED",
    "TestOWEditDomain.test_time_variable_preservation": "PASSED",
    "TestOWEditDomain.test_unlink": "PASSED",
    "TestOWEditDomain.test_widget_state": "PASSED",
    "TestEditors.test_continuous_editor": "PASSED",
    "TestEditors.test_discrete_editor": "PASSED",
    "TestEditors.test_discrete_editor_add_remove_action": "PASSED",
    "TestEditors.test_discrete_editor_context_menu": "PASSED",
    "TestEditors.test_discrete_editor_merge_action": "PASSED",
    "TestEditors.test_discrete_editor_rename_selected_items_action": "PASSED",
    "TestEditors.test_reinterpret_editor": "PASSED",
    "TestEditors.test_reinterpret_editor_simulate": "PASSED",
    "TestEditors.test_time_editor": "PASSED",
    "TestEditors.test_unlink": "PASSED",
    "TestEditors.test_variable_editor": "PASSED",
    "TestModels.test_variable_model": "PASSED",
    "TestDelegates.test_delegate": "PASSED",
    "TestTransforms.test_continous": "PASSED",
    "TestTransforms.test_discrete": "PASSED",
    "TestTransforms.test_discrete_add_drop": "PASSED",
    "TestTransforms.test_discrete_merge": "PASSED",
    "TestTransforms.test_discrete_rename": "PASSED",
    "TestTransforms.test_discrete_reorder": "PASSED",
    "TestTransforms.test_string": "PASSED",
    "TestTransforms.test_time": "PASSED",
    "TestReinterpretTransforms.test_as_continuous": "PASSED",
    "TestReinterpretTransforms.test_as_discrete": "PASSED",
    "TestReinterpretTransforms.test_as_string": "PASSED",
    "TestReinterpretTransforms.test_as_time": "PASSED",
    "TestReinterpretTransforms.test_compound_transform": "PASSED",
    "TestReinterpretTransforms.test_null_transform": "PASSED",
    "TestReinterpretTransforms.test_reinterpret_string": "PASSED",
    "TestUtils.test_as_float_or_nan": "PASSED",
    "TestUtils.test_column_str_repr": "PASSED",
    "TestUtils.test_mapper": "PASSED",
    "TestLookupMappingTransform.test_equality": "PASSED",
    "TestLookupMappingTransform.test_pickle": "PASSED",
    "TestLookupMappingTransform.test_transform": "PASSED",
    "TestGroupLessFrequentItemsDialog.test_dialog_open": "PASSED",
    "TestGroupLessFrequentItemsDialog.test_group_keep_n": "PASSED",
    "TestGroupLessFrequentItemsDialog.test_group_less_frequent_abs": "PASSED",
    "TestGroupLessFrequentItemsDialog.test_group_less_frequent_missing": "PASSED",
    "TestGroupLessFrequentItemsDialog.test_group_less_frequent_rel": "PASSED",
    "TestGroupLessFrequentItemsDialog.test_group_selected": "PASSED",
    "test_filename": "ERROR",
    "TestOWEditDomain.test_minimum_size": "FAILED",
    "TestReinterpretTransforms.test_raise_pandas_version": "FAILED"
}
diff --git a/Orange/widgets/data/oweditdomain.py b/Orange/widgets/data/oweditdomain.py
index ccb73061e..c69450f60 100644
--- a/Orange/widgets/data/oweditdomain.py
+++ b/Orange/widgets/data/oweditdomain.py
@@ -2828,7 +2828,15 @@ class ToContinuousTransform(Transformation):
 
 def datetime_to_epoch(dti: pd.DatetimeIndex, only_time) -> np.ndarray:
     """Convert datetime to epoch"""
-    delta = dti - (dti.normalize() if only_time else pd.Timestamp("1970-01-01"))
+    # when dti has timezone info also the subtracted timestamp must have it
+    # otherwise subtracting fails
+    initial_ts = pd.Timestamp("1970-01-01", tz=None if dti.tz is None else "UTC")
+    # pandas in versions before 1.4 don't support subtracting different timezones
+    # remove next two lines when read-the-docs start supporting config files
+    # for subprojects, or they change default python version to 3.8
+    if dti.tz is not None:
+        dti = dti.tz_convert("UTC")
+    delta = dti - (dti.normalize() if only_time else initial_ts)
     return (delta / pd.Timedelta("1s")).values
 
 

{"Orange/widgets/data/oweditdomain.py": "\"\"\"\nEdit Domain\n-----------\n\nA widget for manual editing of a domain's attributes.\n\n\"\"\"\nimport warnings\nfrom xml.sax.saxutils import escape\nfrom itertools import zip_longest, repeat, chain\nfrom contextlib import contextmanager\nfrom collections import namedtuple, Counter\nfrom functools import singledispatch, partial\nfrom typing import (\n    Tuple, List, Any, Optional, Union, Dict, Sequence, Iterable, NamedTuple,\n    FrozenSet, Type, Callable, TypeVar, Mapping, Hashable, cast\n)\n\nimport numpy as np\nimport pandas as pd\nfrom AnyQt.QtWidgets import (\n    QWidget, QListView, QTreeView, QVBoxLayout, QHBoxLayout, QFormLayout,\n    QLineEdit, QAction, QActionGroup, QGroupBox,\n    QStyledItemDelegate, QStyleOptionViewItem, QStyle, QSizePolicy,\n    QDialogButtonBox, QPushButton, QCheckBox, QComboBox, QStackedLayout,\n    QDialog, QRadioButton, QGridLayout, QLabel, QSpinBox, QDoubleSpinBox,\n    QAbstractItemView, QMenu, QToolTip\n)\nfrom AnyQt.QtGui import (\n    QStandardItemModel, QStandardItem, QKeySequence, QIcon, QBrush, QPalette,\n    QHelpEvent\n)\nfrom AnyQt.QtCore import (\n    Qt, QSize, QModelIndex, QAbstractItemModel, QPersistentModelIndex, QRect,\n    QPoint,\n)\nfrom AnyQt.QtCore import pyqtSignal as Signal, pyqtSlot as Slot\n\nimport Orange.data\n\nfrom Orange.preprocess.transformation import (\n    Transformation, Identity, Lookup, MappingTransform\n)\nfrom Orange.misc.collections import DictMissingConst\nfrom Orange.util import frompyfunc\nfrom Orange.widgets import widget, gui, settings\nfrom Orange.widgets.utils import itemmodels, ftry\nfrom Orange.widgets.utils.buttons import FixedSizeButton\nfrom Orange.widgets.utils.itemmodels import signal_blocking\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.widget import Input, Output\n\nndarray = np.ndarray  # pylint: disable=invalid-name\nMArray = np.ma.MaskedArray\nDType = Union[np.dtype, type]\n\nV = TypeVar(\"V\", bound=Orange.data.Variable)  # pylint: disable=invalid-name\nH = TypeVar(\"H\", bound=Hashable)  # pylint: disable=invalid-name\n\n\ndef unique(sequence: Iterable[H]) -> Iterable[H]:\n    \"\"\"\n    Return unique elements in `sequence`, preserving their (first seen) order.\n    \"\"\"\n    # depending on Python >= 3.6 'ordered' dict implementation detail.\n    return iter(dict.fromkeys(sequence))\n\n\nclass _DataType:\n    def __eq__(self, other):\n        \"\"\"Equal if `other` has the same type and all elements compare equal.\"\"\"\n        if type(self) is not type(other):\n            return False\n        return super().__eq__(other)\n\n    def __ne__(self, other):\n        return not self == other\n\n    def __hash__(self):\n        return hash((type(self), super().__hash__()))\n\n    def name_type(self):\n        \"\"\"\n        Returns a tuple with name and type of the variable.\n        It is used since it is forbidden to use names of variables in settings.\n        \"\"\"\n        type_number = {\n            \"Categorical\": 0,\n            \"Real\": 2,\n            \"Time\": 3,\n            \"String\": 4\n        }\n        return self.name, type_number[type(self).__name__]\n\n\n#: An ordered sequence of key, value pairs (variable annotations)\nAnnotationsType = Tuple[Tuple[str, str], ...]\n\n\n# Define abstract representation of the variable types edited\n\nclass Categorical(\n    _DataType, NamedTuple(\"Categorical\", [\n        (\"name\", str),\n        (\"categories\", Tuple[str, ...]),\n        (\"annotations\", AnnotationsType),\n        (\"linked\", bool)\n    ])): pass\n\n\nclass Real(\n    _DataType, NamedTuple(\"Real\", [\n        (\"name\", str),\n        # a precision (int, and a format specifier('f', 'g', or '')\n        (\"format\", Tuple[int, str]),\n        (\"annotations\", AnnotationsType),\n        (\"linked\", bool)\n    ])): pass\n\n\nclass String(\n    _DataType, NamedTuple(\"String\", [\n        (\"name\", str),\n        (\"annotations\", AnnotationsType),\n        (\"linked\", bool)\n    ])): pass\n\n\nclass Time(\n    _DataType, NamedTuple(\"Time\", [\n        (\"name\", str),\n        (\"annotations\", AnnotationsType),\n        (\"linked\", bool)\n    ])): pass\n\n\nVariable = Union[Categorical, Real, Time, String]\nVariableTypes = (Categorical, Real, Time, String)\n\n\n# Define variable transformations.\n\nclass Rename(_DataType, namedtuple(\"Rename\", [\"name\"])):\n    \"\"\"\n    Rename a variable.\n\n    Parameters\n    ----------\n    name : str\n        The new name\n    \"\"\"\n    def __call__(self, var):\n        # type: (Variable) -> Variable\n        return var._replace(name=self.name)\n\n\n#: Mapping of categories.\n#: A list of pairs with the first element the original value and the second\n#: element the new value. If the first element is None then a category level\n#: is added. If the second element is None than the corresponding first level\n#: is dropped. If there are duplicated elements on the right the corresponding\n#: categories on the left are merged.\n#: The mapped order is defined by the translated elements after None removal\n#: and merges (the first occurrence of a multiplied elements defines its\n#: position):\nCategoriesMappingType = List[Tuple[Optional[str], Optional[str]]]\n\n\nclass CategoriesMapping(_DataType, namedtuple(\"CategoriesMapping\", [\"mapping\"])):\n    \"\"\"\n    Change categories of a categorical variable.\n\n    Parameters\n    ----------\n    mapping : CategoriesMappingType\n    \"\"\"\n    def __call__(self, var):\n        # type: (Categorical) -> Categorical\n        cat = tuple(unique(cj for _, cj in self.mapping if cj is not None))\n        return var._replace(categories=cat)\n\n\nclass Annotate(_DataType, namedtuple(\"Annotate\", [\"annotations\"])):\n    \"\"\"\n    Replace variable annotations.\n    \"\"\"\n    def __call__(self, var):\n        return var._replace(annotations=self.annotations)\n\n\nclass Unlink(_DataType, namedtuple(\"Unlink\", [])):\n    \"\"\"Unlink variable from its source, that is, remove compute_value\"\"\"\n\n\nclass StrpTime(_DataType, namedtuple(\"StrpTime\", [\"label\", \"formats\", \"have_date\", \"have_time\"])):\n    \"\"\"Use format on variable interpreted as time\"\"\"\n\n\nTransform = Union[Rename, CategoriesMapping, Annotate, Unlink, StrpTime]\nTransformTypes = (Rename, CategoriesMapping, Annotate, Unlink, StrpTime)\n\nCategoricalTransformTypes = (CategoriesMapping, Unlink)\n\n\n# Reinterpret vector transformations.\nclass CategoricalVector(\n    _DataType, NamedTuple(\"CategoricalVector\", [\n        (\"vtype\", Categorical),\n        (\"data\", Callable[[], MArray]),\n    ])): ...\n\n\nclass RealVector(\n    _DataType, NamedTuple(\"RealVector\", [\n        (\"vtype\", Real),\n        (\"data\", Callable[[], MArray]),\n    ])): ...\n\n\nclass StringVector(\n    _DataType, NamedTuple(\"StringVector\", [\n        (\"vtype\", String),\n        (\"data\", Callable[[], MArray]),\n    ])): ...\n\n\nclass TimeVector(\n    _DataType, NamedTuple(\"TimeVector\", [\n        (\"vtype\", Time),\n        (\"data\", Callable[[], MArray]),\n    ])): ...\n\n\nDataVector = Union[CategoricalVector, RealVector, StringVector, TimeVector]\nDataVectorTypes = (CategoricalVector, RealVector, StringVector, TimeVector)\n\n\nclass AsString(_DataType, NamedTuple(\"AsString\", [])):\n    \"\"\"Reinterpret a data vector as a string.\"\"\"\n    def __call__(self, vector: DataVector) -> StringVector:\n        var, _ = vector\n        if isinstance(var, String):\n            return vector\n        return StringVector(\n            String(var.name, var.annotations, False),\n            lambda: as_string(vector.data()),\n        )\n\n\nclass AsContinuous(_DataType, NamedTuple(\"AsContinuous\", [])):\n    \"\"\"\n    Reinterpret as a continuous variable (values that do not parse as\n    float are NaN).\n    \"\"\"\n    def __call__(self, vector: DataVector) -> RealVector:\n        var, _ = vector\n        if isinstance(var, Real):\n            return vector\n        elif isinstance(var, Categorical):\n            def data() -> MArray:\n                d = vector.data()\n                a = categorical_to_string_vector(d, var.values)\n                return MArray(as_float_or_nan(a, where=a.mask), mask=a.mask)\n            return RealVector(\n                Real(var.name, (6, 'g'), var.annotations, var.linked), data\n            )\n        elif isinstance(var, Time):\n            return RealVector(\n                Real(var.name, (6, 'g'), var.annotations, var.linked),\n                lambda: vector.data().astype(float)\n            )\n        elif isinstance(var, String):\n            def data():\n                s = vector.data()\n                return MArray(as_float_or_nan(s, where=s.mask), mask=s.mask)\n            return RealVector(\n                Real(var.name, (6, \"g\"), var.annotations, var.linked), data\n            )\n        raise AssertionError\n\n\nclass AsCategorical(_DataType, namedtuple(\"AsCategorical\", [])):\n    \"\"\"Reinterpret as a categorical variable\"\"\"\n    def __call__(self, vector: DataVector) -> CategoricalVector:\n        # this is the main complication in type transformation since we need\n        # the data and not just the variable description\n        var, _ = vector\n        if isinstance(var, Categorical):\n            return vector\n        if isinstance(var, (Real, Time, String)):\n            data, values = categorical_from_vector(vector.data())\n            return CategoricalVector(\n                Categorical(var.name, values, var.annotations, var.linked),\n                lambda: data\n            )\n        raise AssertionError\n\n\nclass AsTime(_DataType, namedtuple(\"AsTime\", [])):\n    \"\"\"Reinterpret as a datetime vector\"\"\"\n    def __call__(self, vector: DataVector) -> TimeVector:\n        var, _ = vector\n        if isinstance(var, Time):\n            return vector\n        elif isinstance(var, Real):\n            return TimeVector(\n                Time(var.name, var.annotations, var.linked),\n                lambda: vector.data().astype(\"M8[us]\")\n            )\n        elif isinstance(var, Categorical):\n            def data():\n                d = vector.data()\n                s = categorical_to_string_vector(d, var.values)\n                dt = pd.to_datetime(s, errors=\"coerce\").values.astype(\"M8[us]\")\n                return MArray(dt, mask=d.mask)\n            return TimeVector(\n                Time(var.name, var.annotations, var.linked), data\n            )\n        elif isinstance(var, String):\n            def data():\n                s = vector.data()\n                dt = pd.to_datetime(s, errors=\"coerce\").values.astype(\"M8[us]\")\n                return MArray(dt, mask=s.mask)\n            return TimeVector(\n                Time(var.name, var.annotations, var.linked), data\n            )\n        raise AssertionError\n\n\nReinterpretTransform = Union[AsCategorical, AsContinuous, AsTime, AsString]\nReinterpretTransformTypes = (AsCategorical, AsContinuous, AsTime, AsString)\n\n\ndef deconstruct(obj):\n    # type: (tuple) -> Tuple[str, Tuple[Any, ...]]\n    \"\"\"\n    Deconstruct a tuple subclass to its class name and its contents.\n\n    Parameters\n    ----------\n    obj : A tuple\n\n    Returns\n    -------\n    value: Tuple[str, Tuple[Any, ...]]\n    \"\"\"\n    cname = type(obj).__name__\n    args = tuple(obj)\n    return cname, args\n\n\ndef reconstruct(tname, args):\n    # type: (str, Tuple[Any, ...]) -> Tuple[Any, ...]\n    \"\"\"\n    Reconstruct a tuple subclass (inverse of deconstruct).\n\n    Parameters\n    ----------\n    tname : str\n        Type name\n    args : Tuple[Any, ...]\n\n    Returns\n    -------\n    rval: Tuple[Any, ...]\n    \"\"\"\n    try:\n        constructor = globals()[tname]\n    except KeyError:\n        raise NameError(tname)\n    return constructor(*args)\n\n\ndef formatter_for_dtype(dtype: np.dtype) -> Callable[[Any], str]:\n    if dtype.metadata is None:\n        return str\n    else:\n        return dtype.metadata.get(\"__formatter\", str)  # metadata abuse\n\n\ndef masked_unique(data: MArray) -> Tuple[MArray, ndarray]:\n    if not np.any(data.mask):\n        return np.ma.unique(data, return_inverse=True)\n    elif data.dtype.kind == \"O\":\n        # np.ma.unique does not work for object arrays\n        # (no ma.minimum_fill_value for object arrays)\n        # maybe sorted(set(data.data[...]))\n        unq = np.unique(data.data[~data.mask])\n        mapper = make_dict_mapper(\n            DictMissingConst(len(unq), ((v, i) for i, v in enumerate(unq)))\n        )\n        index = mapper(data.data)\n        unq = np.array(unq.tolist() + [data.fill_value], dtype=data.dtype)\n        unq_mask = [False] * unq.size\n        unq_mask[-1] = True\n        unq = MArray(unq, mask=unq_mask)\n        return unq, index\n    else:\n        unq, index = np.ma.unique(data, return_inverse=True)\n        assert not np.any(unq.mask[:-1]), \\\n            \"masked value if present must be in last position\"\n        return unq, index\n\n\ndef categorical_from_vector(data: MArray) -> Tuple[MArray, Tuple[str, ...]]:\n    formatter = formatter_for_dtype(data.dtype)\n    unq, index = categorize_unique(data)\n    if formatter is not str:\n        # str(np.array([0], \"M8[s]\")[0]) is different then\n        # str(np.array([0], \"M8[s]\").astype(object)[0]) which is what\n        # as_string is doing\n        names = tuple(map(formatter, unq.astype(object)))\n    else:\n        names = tuple(as_string(unq))\n    data = MArray(\n        index, mask=data.mask,\n        dtype=np.dtype(int, metadata={\n            \"__formater\": lambda i: names[i] if 0 <= i < unq.size else \"?\"\n        })\n    )\n    return data, names\n\n\ndef categorize_unique(data: MArray) -> Tuple[ndarray, MArray]:\n    unq, index = masked_unique(data)\n    if np.any(unq.mask):\n        unq = unq[:-1]\n        assert not np.any(unq.mask), \"masked value if present must be last\"\n    unq = unq.data\n    index[data.mask] = -1\n    index = MArray(index, mask=data.mask)\n    return unq, index\n\n\ndef categorical_to_string_vector(data: MArray, values: Tuple[str, ...]) -> MArray:\n    lookup = np.asarray(values, object)\n    out = np.full(data.shape, \"\", dtype=object)\n    mask_ = ~data.mask\n    out[mask_] = lookup[data.data[mask_]]\n    return MArray(out, mask=data.mask, fill_value=\"\")\n\n\n# Item models\n\n\nclass DictItemsModel(QStandardItemModel):\n    \"\"\"A Qt Item Model class displaying the contents of a python\n    dictionary.\n\n    \"\"\"\n    # Implement a proper model with in-place editing.\n    # (Maybe it should be a TableModel with 2 columns)\n    def __init__(self, parent=None, a_dict=None):\n        super().__init__(parent)\n        self._dict = {}\n        self.setHorizontalHeaderLabels([\"Key\", \"Value\"])\n        if a_dict is not None:\n            self.set_dict(a_dict)\n\n    def set_dict(self, a_dict):\n        # type: (Dict[str, str]) -> None\n        self._dict = a_dict\n        self.setRowCount(0)\n        for key, value in sorted(a_dict.items()):\n            key_item = QStandardItem(key)\n            value_item = QStandardItem(value)\n            key_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)\n            value_item.setFlags(value_item.flags() | Qt.ItemIsEditable)\n            self.appendRow([key_item, value_item])\n\n    def get_dict(self):\n        # type: () -> Dict[str, str]\n        rval = {}\n        for row in range(self.rowCount()):\n            key_item = self.item(row, 0)\n            value_item = self.item(row, 1)\n            rval[key_item.text()] = value_item.text()\n        return rval\n\n\nclass VariableEditor(QWidget):\n    \"\"\"\n    An editor widget for a variable.\n\n    Can edit the variable name, and its attributes dictionary.\n    \"\"\"\n    variable_changed = Signal()\n\n    def __init__(self, parent=None, **kwargs):\n        super().__init__(parent, **kwargs)\n        self.var = None  # type: Optional[Variable]\n\n        layout = QVBoxLayout()\n        self.setLayout(layout)\n\n        self.form = form = QFormLayout(\n            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,\n            objectName=\"editor-form-layout\"\n        )\n        layout.addLayout(self.form)\n\n        self.name_edit = QLineEdit(objectName=\"name-editor\")\n        self.name_edit.editingFinished.connect(\n            lambda: self.name_edit.isModified() and self.on_name_changed()\n        )\n        form.addRow(\"Name:\", self.name_edit)\n\n        self.unlink_var_cb = QCheckBox(\n            \"Unlink variable from its source variable\", self,\n            toolTip=\"Make Orange forget that the variable is derived from \"\n                    \"another.\\n\"\n                    \"Use this for instance when you want to consider variables \"\n                    \"with the same name but from different sources as the same \"\n                    \"variable.\"\n        )\n        self.unlink_var_cb.toggled.connect(self._set_unlink)\n        form.addRow(\"\", self.unlink_var_cb)\n\n        vlayout = QVBoxLayout(spacing=1)\n        self.labels_edit = view = QTreeView(\n            objectName=\"annotation-pairs-edit\",\n            rootIsDecorated=False,\n            editTriggers=QTreeView.DoubleClicked | QTreeView.EditKeyPressed,\n        )\n        self.labels_model = model = DictItemsModel()\n        view.setModel(model)\n\n        view.selectionModel().selectionChanged.connect(\n            self.on_label_selection_changed)\n\n        agrp = QActionGroup(view, objectName=\"annotate-action-group\")\n        action_add = QAction(\n            \"+\", self, objectName=\"action-add-label\",\n            toolTip=\"Add a new label.\",\n            shortcut=QKeySequence(QKeySequence.New),\n            shortcutContext=Qt.WidgetShortcut\n        )\n        action_delete = QAction(\n            \"\\N{MINUS SIGN}\", self, objectName=\"action-delete-label\",\n            toolTip=\"Remove selected label.\",\n            shortcut=QKeySequence(QKeySequence.Delete),\n            shortcutContext=Qt.WidgetShortcut\n        )\n        agrp.addAction(action_add)\n        agrp.addAction(action_delete)\n        view.addActions([action_add, action_delete])\n\n        def add_label():\n            row = [QStandardItem(), QStandardItem()]\n            model.appendRow(row)\n            idx = model.index(model.rowCount() - 1, 0)\n            view.setCurrentIndex(idx)\n            view.edit(idx)\n\n        def remove_label():\n            rows = view.selectionModel().selectedRows(0)\n            if rows:\n                assert len(rows) == 1\n                idx = rows[0].row()\n                model.removeRow(idx)\n\n        action_add.triggered.connect(add_label)\n        action_delete.triggered.connect(remove_label)\n        agrp.setEnabled(False)\n\n        self.add_label_action = action_add\n        self.remove_label_action = action_delete\n\n        # Necessary signals to know when the labels change\n        model.dataChanged.connect(self.on_labels_changed)\n        model.rowsInserted.connect(self.on_labels_changed)\n        model.rowsRemoved.connect(self.on_labels_changed)\n\n        vlayout.addWidget(self.labels_edit)\n        hlayout = QHBoxLayout()\n        hlayout.setContentsMargins(0, 0, 0, 0)\n        button = FixedSizeButton(\n            self, defaultAction=self.add_label_action,\n            accessibleName=\"Add\",\n        )\n        hlayout.addWidget(button)\n\n        button = FixedSizeButton(\n            self, defaultAction=self.remove_label_action,\n            accessibleName=\"Remove\",\n        )\n\n        hlayout.addWidget(button)\n        hlayout.addStretch(10)\n        vlayout.addLayout(hlayout)\n        form.addRow(\"Labels:\", vlayout)\n\n    def set_data(self, var, transform=()):\n        # type: (Optional[Variable], Sequence[Transform]) -> None\n        \"\"\"\n        Set the variable to edit.\n        \"\"\"\n        self.clear()\n        self.var = var\n        if var is not None:\n            name = var.name\n            annotations = var.annotations\n            unlink = False\n            for tr in transform:\n                if isinstance(tr, Rename):\n                    name = tr.name\n                elif isinstance(tr, Annotate):\n                    annotations = tr.annotations\n                elif isinstance(tr, Unlink):\n                    unlink = True\n            self.name_edit.setText(name)\n            self.labels_model.set_dict(dict(annotations))\n            self.add_label_action.actionGroup().setEnabled(True)\n            self.unlink_var_cb.setChecked(unlink)\n        else:\n            self.add_label_action.actionGroup().setEnabled(False)\n\n        self.unlink_var_cb.setDisabled(var is None or not var.linked)\n\n    def get_data(self):\n        \"\"\"Retrieve the modified variable.\n        \"\"\"\n        if self.var is None:\n            return None, []\n        name = self.name_edit.text().strip()\n        labels = tuple(sorted(self.labels_model.get_dict().items()))\n        tr = []\n        if self.var.name != name:\n            tr.append(Rename(name))\n        if self.var.annotations != labels:\n            tr.append(Annotate(labels))\n        if self.var.linked and self.unlink_var_cb.isChecked():\n            tr.append(Unlink())\n        return self.var, tr\n\n    def clear(self):\n        \"\"\"Clear the editor state.\n        \"\"\"\n        self.var = None\n        self.name_edit.setText(\"\")\n        self.labels_model.setRowCount(0)\n        self.unlink_var_cb.setChecked(False)\n\n    @Slot()\n    def on_name_changed(self):\n        self.variable_changed.emit()\n\n    @Slot()\n    def on_labels_changed(self):\n        self.variable_changed.emit()\n\n    @Slot()\n    def on_label_selection_changed(self):\n        selected = self.labels_edit.selectionModel().selectedRows()\n        self.remove_label_action.setEnabled(bool(len(selected)))\n\n    def _set_unlink(self, unlink):\n        self.unlink_var_cb.setChecked(unlink)\n        self.variable_changed.emit()\n\n\nclass GroupItemsDialog(QDialog):\n    \"\"\"\n    A dialog for group less frequent values.\n    \"\"\"\n    DEFAULT_LABEL = \"other\"\n\n    def __init__(\n            self, variable: Categorical,\n            data: Union[np.ndarray, List, MArray],\n            selected_attributes: List[str], dialog_settings: Dict[str, Any],\n            parent: QWidget = None, flags: Qt.WindowFlags = Qt.Dialog, **kwargs\n    ) -> None:\n        super().__init__(parent, flags, **kwargs)\n        self.variable = variable\n        self.data = data\n        self.selected_attributes = selected_attributes\n\n        # grouping strategy\n        self.selected_radio = radio1 = QRadioButton(\"Group selected values\")\n        self.frequent_abs_radio = radio2 = QRadioButton(\n            \"Group values with less than\"\n        )\n        self.frequent_rel_radio = radio3 = QRadioButton(\n            \"Group values with less than\"\n        )\n        self.n_values_radio = radio4 = QRadioButton(\n            \"Group all except\"\n        )\n\n        # if selected attributes available check the first radio button,\n        # otherwise disable it\n        if selected_attributes:\n            radio1.setChecked(True)\n        else:\n            radio1.setEnabled(False)\n            # they are remembered by number since radio button instance is\n            # new object for each dialog\n            checked = dialog_settings.get(\"selected_radio\", 0)\n            [radio2, radio3, radio4][checked].setChecked(True)\n\n        label2 = QLabel(\"occurrences\")\n        label3 = QLabel(\"occurrences\")\n        label4 = QLabel(\"most frequent values\")\n\n        self.frequent_abs_spin = spin2 = QSpinBox()\n        max_val = len(data)\n        spin2.setMinimum(1)\n        spin2.setMaximum(max_val)\n        spin2.setValue(dialog_settings.get(\"frequent_abs_spin\", 10))\n        spin2.setMinimumWidth(\n            self.fontMetrics().horizontalAdvance(\"X\") * (len(str(max_val)) + 1) + 20\n        )\n        spin2.valueChanged.connect(self._frequent_abs_spin_changed)\n\n        self.frequent_rel_spin = spin3 = QDoubleSpinBox()\n        spin3.setMinimum(0)\n        spin3.setDecimals(1)\n        spin3.setSingleStep(0.1)\n        spin3.setMaximum(100)\n        spin3.setValue(dialog_settings.get(\"frequent_rel_spin\", 10))\n        spin3.setMinimumWidth(self.fontMetrics().horizontalAdvance(\"X\") * (2 + 1) + 20)\n        spin3.setSuffix(\" %\")\n        spin3.valueChanged.connect(self._frequent_rel_spin_changed)\n\n        self.n_values_spin = spin4 = QSpinBox()\n        spin4.setMinimum(0)\n        spin4.setMaximum(len(variable.categories))\n        spin4.setValue(\n            dialog_settings.get(\n                \"n_values_spin\", min(10, len(variable.categories))\n            )\n        )\n        spin4.setMinimumWidth(\n            self.fontMetrics().horizontalAdvance(\"X\") * (len(str(max_val)) + 1) + 20\n        )\n        spin4.valueChanged.connect(self._n_values_spin_spin_changed)\n\n        grid_layout = QGridLayout()\n        # first row\n        grid_layout.addWidget(radio1, 0, 0, 1, 2)\n        # second row\n        grid_layout.addWidget(radio2, 1, 0, 1, 2)\n        grid_layout.addWidget(spin2, 1, 2)\n        grid_layout.addWidget(label2, 1, 3)\n        # third row\n        grid_layout.addWidget(radio3, 2, 0, 1, 2)\n        grid_layout.addWidget(spin3, 2, 2)\n        grid_layout.addWidget(label3, 2, 3)\n        # fourth row\n        grid_layout.addWidget(radio4, 3, 0)\n        grid_layout.addWidget(spin4, 3, 1)\n        grid_layout.addWidget(label4, 3, 2, 1, 2)\n\n        group_box = QGroupBox()\n        group_box.setLayout(grid_layout)\n\n        # grouped variable name\n        new_name_label = QLabel(\"New value name: \")\n        self.new_name_line_edit = n_line_edit = QLineEdit(\n            dialog_settings.get(\"name_line_edit\", self.DEFAULT_LABEL)\n        )\n        # it is shown gray when user removes the text and let user know that\n        # word others is default one\n        n_line_edit.setPlaceholderText(self.DEFAULT_LABEL)\n        name_hlayout = QHBoxLayout()\n        name_hlayout.addWidget(new_name_label)\n        name_hlayout.addWidget(n_line_edit)\n\n        # confirm_button = QPushButton(\"Apply\")\n        # cancel_button = QPushButton(\"Cancel\")\n        buttons = QDialogButtonBox(\n            orientation=Qt.Horizontal,\n            standardButtons=(QDialogButtonBox.Ok | QDialogButtonBox.Cancel),\n            objectName=\"dialog-button-box\",\n        )\n        buttons.accepted.connect(self.accept)\n        buttons.rejected.connect(self.reject)\n\n        # join components\n        self.setLayout(QVBoxLayout())\n        self.layout().addWidget(group_box)\n        self.layout().addLayout(name_hlayout)\n        self.layout().addWidget(buttons)\n        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)\n\n    def _frequent_abs_spin_changed(self) -> None:\n        self.frequent_abs_radio.setChecked(True)\n\n    def _n_values_spin_spin_changed(self) -> None:\n        self.n_values_radio.setChecked(True)\n\n    def _frequent_rel_spin_changed(self) -> None:\n        self.frequent_rel_radio.setChecked(True)\n\n    def get_merge_attributes(self) -> List[str]:\n        \"\"\"\n        Returns attributes that will be merged\n\n        Returns\n        -------\n        List of attributes' to be merged names\n        \"\"\"\n        if self.selected_radio.isChecked():\n            return self.selected_attributes\n\n        if isinstance(self.data, MArray):\n            non_nan = self.data[~self.data.mask]\n        elif isinstance(self.data, np.ndarray):\n            non_nan = self.data[~np.isnan(self.data)]\n        else:  # list\n            non_nan = [x for x in self.data if x is not None]\n\n        counts = Counter(non_nan)\n        if self.n_values_radio.isChecked():\n            keep_values = self.n_values_spin.value()\n            values = counts.most_common()[keep_values:]\n            indices = [i for i, _ in values]\n        elif self.frequent_abs_radio.isChecked():\n            indices = [v for v, c in counts.most_common()\n                       if c < self.frequent_abs_spin.value()]\n        else:  # self.frequent_rel_radio.isChecked():\n            n_all = sum(counts.values())\n            indices = [v for v, c in counts.most_common()\n                       if c / n_all * 100 < self.frequent_rel_spin.value()]\n\n        indices = np.array(indices, dtype=int)  # indices must be ints\n        return np.array(self.variable.categories)[indices].tolist()\n\n    def get_merged_value_name(self) -> str:\n        \"\"\"\n        Returns\n        -------\n        New label of merged values\n        \"\"\"\n        return self.new_name_line_edit.text() or self.DEFAULT_LABEL\n\n    def get_dialog_settings(self) -> Dict[str, Any]:\n        \"\"\"\n        Returns\n        -------\n        Return the dictionary with vlues set by user in each of the line edits\n        and selected radio button.\n        \"\"\"\n        settings_dict = {\n            \"frequent_abs_spin\": self.frequent_abs_spin.value(),\n            \"frequent_rel_spin\": self.frequent_rel_spin.value(),\n            \"n_values_spin\": self.n_values_spin.value(),\n            \"name_line_edit\": self.new_name_line_edit.text()\n        }\n        checked = [\n            i for i, s in enumerate(\n                [self.frequent_abs_radio,\n                 self.frequent_rel_radio,\n                 self.n_values_radio]\n            ) if s.isChecked()]\n        # when checked empty radio button for selected values is selected\n        # it is not stored in setting since its selection depends on users\n        # selection of values in list\n        if checked:\n            settings_dict[\"selected_radio\"] = checked[0]\n        return settings_dict\n\n\n@contextmanager\ndef disconnected(signal, slot, connection_type=Qt.AutoConnection):\n    signal.disconnect(slot)\n    try:\n        yield\n    finally:\n        signal.connect(slot, connection_type)\n\n\n#: In 'reordable' models holds the original position of the item\n#: (if applicable).\nSourcePosRole = Qt.UserRole\n#: The original name\nSourceNameRole = Qt.UserRole + 2\n\n#: The added/dropped state (type is ItemEditState)\nEditStateRole = Qt.UserRole + 1\n\n\nclass ItemEditState:\n    NoState = 0\n    Dropped = 1\n    Added = 2\n\n\n#: Role used to retrieve the count of 'key' values in the model.\nMultiplicityRole = Qt.UserRole + 0x67\n\n\nclass CountedListModel(itemmodels.PyListModel):\n    \"\"\"\n    A list model counting how many times unique `key` values appear in\n    the list.\n\n    The counts are cached and invalidated on any change to the model involving\n    the changes to `keyRoles`.\n    \"\"\"\n    #: cached counts\n    __counts_cache = None  # type: Optional[Counter]\n\n    def data(self, index, role=Qt.DisplayRole):\n        # type: (QModelIndex, int) -> Any\n        if role == MultiplicityRole:\n            key = self.key(index)\n            counts = self.__counts()\n            return counts.get(key, 1)\n        return super().data(index, role)\n\n    def setData(self, index, value, role=Qt.EditRole):\n        # type: (QModelIndex, Any, int)-> bool\n        rval = super().setData(index, value, role)\n        if role in self.keyRoles():\n            self.invalidateCounts()\n        return rval\n\n    def setItemData(self, index, data):\n        # type: (QModelIndex, Dict[int, Any]) -> bool\n        rval = super().setItemData(index, data)\n        if self.keyRoles().intersection(set(data.keys())):\n            self.invalidateCounts()\n        return rval\n\n    def endInsertRows(self):\n        super().endInsertRows()\n        self.invalidateCounts()\n\n    def endRemoveRows(self):\n        super().endRemoveRows()\n        self.invalidateCounts()\n\n    def endResetModel(self) -> None:\n        super().endResetModel()\n        self.invalidateCounts()\n\n    def invalidateCounts(self) -> None:\n        \"\"\"\n        Invalidate the cached counts.\n        \"\"\"\n        self.__counts_cache = None\n        # emit the change for the whole model\n        self.dataChanged.emit(\n            self.index(0), self.index(self.rowCount() - 1), [MultiplicityRole]\n        )\n\n    def __counts(self):\n        # type: () -> Counter\n        if self.__counts_cache is not None:\n            return self.__counts_cache\n        counts = Counter()\n        for index in map(self.index, range(self.rowCount())):\n            key = self.key(index)\n            try:\n                counts[key] += 1\n            except TypeError:  # pragma: no cover\n                warnings.warn(f\"key value '{key}' is not hashable\")\n        self.__counts_cache = counts\n        return self.__counts_cache\n\n    def key(self, index):\n        # type: (QModelIndex) -> Any\n        \"\"\"\n        Return the 'key' value that is to be counted.\n\n        The default implementation returns Qt.EditRole value for the index\n\n        Parameters\n        ----------\n        index : QModelIndex\n            The model index.\n\n        Returns\n        -------\n        key : Any\n        \"\"\"\n        return self.data(index, Qt.EditRole)\n\n    def keyRoles(self):\n        # type: () -> FrozenSet[int]\n        \"\"\"\n        Return a set of item roles on which `key` depends.\n\n        The counts are invalidated and recomputed whenever any of the roles in\n        this set changes.\n\n        By default the only role returned is Qt.EditRole\n        \"\"\"\n        return frozenset({Qt.EditRole})\n\n\nclass CountedStateModel(CountedListModel):\n    \"\"\"\n    Count by EditRole (name) and EditStateRole (ItemEditState)\n    \"\"\"\n    # The purpose is to count the items with target name only for\n    # ItemEditState.NoRole, i.e. excluding added/dropped values.\n    #\n    def key(self, index):  # type: (QModelIndex) -> Tuple[Any, Any]\n        # reimplemented\n        return self.data(index, Qt.EditRole), self.data(index, EditStateRole)\n\n    def keyRoles(self):  # type: () -> FrozenSet[int]\n        # reimplemented\n        return frozenset({Qt.EditRole, EditStateRole})\n\n\ndef mapRectTo(widget: QWidget, parent: QWidget, rect: QRect) -> QRect:  # pylint: disable=redefined-outer-name\n    return QRect(widget.mapTo(parent, rect.topLeft()), rect.size())\n\n\ndef mapRectToGlobal(widget: QWidget, rect: QRect) -> QRect:  # pylint: disable=redefined-outer-name\n    return QRect(widget.mapToGlobal(rect.topLeft()), rect.size())\n\n\nclass CategoriesEditDelegate(QStyledItemDelegate):\n    \"\"\"\n    Display delegate for editing categories.\n\n    Displayed items are styled for add, remove, merge and rename operations.\n    \"\"\"\n    def initStyleOption(self, option, index):\n        # type: (QStyleOptionViewItem, QModelIndex)-> None\n        super().initStyleOption(option, index)\n        text = str(index.data(Qt.EditRole))\n        sourcename = str(index.data(SourceNameRole))\n        editstate = index.data(EditStateRole)\n        counts = index.data(MultiplicityRole)\n        if not isinstance(counts, int):\n            counts = 1\n        suffix = None\n        if editstate == ItemEditState.Dropped:\n            option.state &= ~QStyle.State_Enabled\n            option.font.setStrikeOut(True)\n            text = sourcename\n            suffix = \"(dropped)\"\n        elif editstate == ItemEditState.Added:\n            suffix = \"(added)\"\n        else:\n            text = f\"{sourcename} \\N{RIGHTWARDS ARROW} {text}\"\n            if counts > 1:\n                suffix = \"(merged)\"\n        if suffix is not None:\n            text = text + \" \" + suffix\n        option.text = text\n\n    class CatEditComboBox(QComboBox):\n        prows: List[QPersistentModelIndex]\n\n    def createEditor(\n            self, parent: QWidget, option: 'QStyleOptionViewItem',\n            index: QModelIndex\n    ) -> QWidget:\n        view = option.widget\n        assert isinstance(view, QAbstractItemView)\n        selmodel = view.selectionModel()\n        rows = selmodel.selectedRows(0)\n        if len(rows) < 2:\n            return super().createEditor(parent, option, index)\n        # edit multiple selection\n        cb = CategoriesEditDelegate.CatEditComboBox(\n            editable=True, insertPolicy=QComboBox.InsertAtBottom)\n        cb.setParent(view, Qt.Popup)\n        cb.addItems(\n            list(unique(str(row.data(Qt.EditRole)) for row in rows)))\n        prows = [QPersistentModelIndex(row) for row in rows]\n        cb.prows = prows\n        return cb\n\n    def updateEditorGeometry(\n            self, editor: QWidget, option: 'QStyleOptionViewItem',\n            index: QModelIndex\n    ) -> None:\n        if isinstance(editor, CategoriesEditDelegate.CatEditComboBox):\n            view = cast(QAbstractItemView, option.widget)\n            view.scrollTo(index)\n            vport = view.viewport()\n            vrect = view.visualRect(index)\n            vrect = mapRectTo(vport, view, vrect)\n            vrect = vrect.intersected(vport.geometry())\n            vrect = mapRectToGlobal(vport, vrect)\n            size = editor.sizeHint().expandedTo(vrect.size())\n            editor.resize(size)\n            editor.move(vrect.topLeft())\n        else:\n            super().updateEditorGeometry(editor, option, index)\n\n    def setModelData(\n            self, editor: QWidget, model: QAbstractItemModel, index: QModelIndex\n    ) -> None:\n        if isinstance(editor, CategoriesEditDelegate.CatEditComboBox):\n            text = editor.currentText()\n            with signal_blocking(model):\n                for prow in editor.prows:\n                    if prow.isValid():\n                        model.setData(QModelIndex(prow), text, Qt.EditRole)\n            # this could be better\n            model.dataChanged.emit(\n                model.index(0, 0), model.index(model.rowCount() - 1, 0),\n                (Qt.EditRole,)\n            )\n        else:\n            super().setModelData(editor, model, index)\n\n\nclass DiscreteVariableEditor(VariableEditor):\n    \"\"\"An editor widget for editing a discrete variable.\n\n    Extends the :class:`VariableEditor` to enable editing of\n    variables values.\n    \"\"\"\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.merge_dialog_settings = {}\n        self._values = None\n\n        form = self.layout().itemAt(0)\n        assert isinstance(form, QFormLayout)\n        #: A list model of discrete variable's values.\n        self.values_model = CountedStateModel(\n            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable\n        )\n\n        vlayout = QVBoxLayout(spacing=1)\n        self.values_edit = QListView(\n            editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed,\n            selectionMode=QListView.ExtendedSelection,\n            uniformItemSizes=True,\n        )\n        self.values_edit.setItemDelegate(CategoriesEditDelegate(self))\n        self.values_edit.setModel(self.values_model)\n        self.values_model.dataChanged.connect(self.on_values_changed)\n\n        self.values_edit.selectionModel().selectionChanged.connect(\n            self.on_value_selection_changed)\n        self.values_model.layoutChanged.connect(self.on_value_selection_changed)\n        self.values_model.rowsMoved.connect(self.on_value_selection_changed)\n\n        vlayout.addWidget(self.values_edit)\n        hlayout = QHBoxLayout(spacing=1)\n\n        self.categories_action_group = group = QActionGroup(\n            self, objectName=\"action-group-categories\", enabled=False\n        )\n        self.move_value_up = QAction(\n            \"Move up\", group,\n            iconText=\"\\N{UPWARDS ARROW}\",\n            toolTip=\"Move the selected item up.\",\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier |\n                                  Qt.Key_BracketLeft),\n            shortcutContext=Qt.WidgetShortcut,\n        )\n        self.move_value_up.triggered.connect(self.move_up)\n\n        self.move_value_down = QAction(\n            \"Move down\", group,\n            iconText=\"\\N{DOWNWARDS ARROW}\",\n            toolTip=\"Move the selected item down.\",\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier |\n                                  Qt.Key_BracketRight),\n            shortcutContext=Qt.WidgetShortcut,\n        )\n        self.move_value_down.triggered.connect(self.move_down)\n\n        self.add_new_item = QAction(\n            \"Add\", group,\n            iconText=\"+\",\n            objectName=\"action-add-item\",\n            toolTip=\"Append a new item.\",\n            shortcut=QKeySequence(QKeySequence.New),\n            shortcutContext=Qt.WidgetShortcut,\n        )\n        self.remove_item = QAction(\n            \"Remove item\", group,\n            iconText=\"\\N{MINUS SIGN}\",\n            objectName=\"action-remove-item\",\n            toolTip=\"Delete the selected item.\",\n            shortcut=QKeySequence(QKeySequence.Delete),\n            shortcutContext=Qt.WidgetShortcut,\n        )\n        self.rename_selected_items = QAction(\n            \"Rename selected items\", group,\n            iconText=\"=\",\n            objectName=\"action-rename-selected-items\",\n            toolTip=\"Rename selected items.\",\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_Equal),\n            shortcutContext=Qt.WidgetShortcut,\n        )\n        self.merge_items = QAction(\n            \"Merge\", group,\n            iconText=\"M\",\n            objectName=\"action-activate-merge-dialog\",\n            toolTip=\"Merge infrequent items.\",\n            shortcut=QKeySequence(Qt.ControlModifier | Qt.MetaModifier | Qt.Key_Equal),\n            shortcutContext=Qt.WidgetShortcut\n        )\n\n        self.add_new_item.triggered.connect(self._add_category)\n        self.remove_item.triggered.connect(self._remove_category)\n        self.rename_selected_items.triggered.connect(self._rename_selected_categories)\n        self.merge_items.triggered.connect(self._merge_categories)\n\n        button1 = FixedSizeButton(\n            self, defaultAction=self.move_value_up,\n            accessibleName=\"Move up\"\n        )\n        button2 = FixedSizeButton(\n            self, defaultAction=self.move_value_down,\n            accessibleName=\"Move down\"\n        )\n        button3 = FixedSizeButton(\n            self, defaultAction=self.add_new_item,\n            accessibleName=\"Add\"\n        )\n        button4 = FixedSizeButton(\n            self, defaultAction=self.remove_item,\n            accessibleName=\"Remove\"\n        )\n        button5 = FixedSizeButton(\n            self, defaultAction=self.rename_selected_items,\n            accessibleName=\"Merge selected items\"\n        )\n        button6 = FixedSizeButton(\n            self, defaultAction=self.merge_items,\n            accessibleName=\"Merge infrequent\",\n        )\n\n        self.values_edit.addActions([\n            self.move_value_up, self.move_value_down,\n            self.add_new_item, self.remove_item, self.rename_selected_items\n        ])\n        self.values_edit.setContextMenuPolicy(Qt.CustomContextMenu)\n\n        def context_menu(pos: QPoint):\n            viewport = self.values_edit.viewport()\n            menu = QMenu(self.values_edit)\n            menu.setAttribute(Qt.WA_DeleteOnClose)\n            menu.addActions([self.rename_selected_items, self.remove_item])\n            menu.popup(viewport.mapToGlobal(pos))\n        self.values_edit.customContextMenuRequested.connect(context_menu)\n\n        hlayout.addWidget(button1)\n        hlayout.addWidget(button2)\n        hlayout.addSpacing(3)\n        hlayout.addWidget(button3)\n        hlayout.addWidget(button4)\n        hlayout.addSpacing(3)\n        hlayout.addWidget(button5)\n        hlayout.addWidget(button6)\n\n        hlayout.addStretch(10)\n        vlayout.addLayout(hlayout)\n\n        form.insertRow(2, \"Values:\", vlayout)\n\n        QWidget.setTabOrder(self.name_edit, self.values_edit)\n        QWidget.setTabOrder(self.values_edit, button1)\n        QWidget.setTabOrder(button1, button2)\n        QWidget.setTabOrder(button2, button3)\n        QWidget.setTabOrder(button3, button4)\n        QWidget.setTabOrder(button4, button5)\n        QWidget.setTabOrder(button5, button6)\n\n    def set_data(self, var, transform=()):\n        raise NotImplementedError\n\n    def set_data_categorical(self, var, values, transform=()):\n        # type: (Optional[Categorical], Optional[Sequence[float]], Sequence[Transform]) -> None\n        \"\"\"\n        Set the variable to edit.\n\n        `values` is needed for categorical features to perform grouping.\n        \"\"\"\n        # pylint: disable=too-many-branches\n        super().set_data(var, transform=transform)\n        self._values = values\n        tr = None  # type: Optional[CategoriesMapping]\n        for tr_ in transform:\n            if isinstance(tr_, CategoriesMapping):\n                tr = tr_\n\n        items = []\n        if tr is not None:\n            ci_index = {c: i for i, c in enumerate(var.categories)}\n            for ci, cj in tr.mapping:\n                if ci is None and cj is not None:\n                    # level added\n                    item = {\n                        Qt.EditRole: cj,\n                        EditStateRole: ItemEditState.Added,\n                        SourcePosRole: None\n                    }\n                elif ci is not None and cj is None:\n                    # ci level dropped\n                    item = {\n                        Qt.EditRole: ci,\n                        EditStateRole: ItemEditState.Dropped,\n                        SourcePosRole: ci_index[ci],\n                        SourceNameRole: ci\n                    }\n                elif ci is not None and cj is not None:\n                    # rename or reorder\n                    item = {\n                        Qt.EditRole: cj,\n                        EditStateRole: ItemEditState.NoState,\n                        SourcePosRole: ci_index[ci],\n                        SourceNameRole: ci\n                    }\n                else:\n                    assert False, \"invalid mapping: {!r}\".format(tr.mapping)\n                items.append(item)\n        elif var is not None:\n            items = [\n                {Qt.EditRole: c,\n                 EditStateRole: ItemEditState.NoState,\n                 SourcePosRole: i,\n                 SourceNameRole: c}\n                for i, c in enumerate(var.categories)\n            ]\n        else:\n            items = []\n\n        with disconnected(self.values_model.dataChanged,\n                          self.on_values_changed):\n            self.values_model.clear()\n            self.values_model.insertRows(0, len(items))\n            for i, item in enumerate(items):\n                self.values_model.setItemData(\n                    self.values_model.index(i, 0),\n                    item\n                )\n\n        self.add_new_item.actionGroup().setEnabled(var is not None)\n\n    def __categories_mapping(self):\n        # type: () -> CategoriesMappingType\n        \"\"\"\n        Encode and return the current state as a CategoriesMappingType\n        \"\"\"\n        model = self.values_model\n        source = self.var.categories\n\n        res = []  # type: CategoriesMappingType\n        for i in range(model.rowCount()):\n            midx = model.index(i, 0)\n            category = midx.data(Qt.EditRole)\n            source_pos = midx.data(SourcePosRole)  # type: Optional[int]\n            if source_pos is not None:\n                source_name = source[source_pos]\n            else:\n                source_name = None\n            state = midx.data(EditStateRole)\n            if state == ItemEditState.Dropped:\n                res.append((source_name, None))\n            elif state == ItemEditState.Added:\n                res.append((None, category))\n            else:\n                res.append((source_name, category))\n        return res\n\n    def get_data(self):\n        \"\"\"Retrieve the modified variable\n        \"\"\"\n        var, tr = super().get_data()\n        if var is None:\n            return var, tr\n        mapping = self.__categories_mapping()\n        assert len(mapping) >= len(var.categories), f'{mapping}, {var}'\n        if any(_1 != _2 or _2 != _3\n               for (_1, _2), _3 in zip_longest(mapping, var.categories)):\n            tr.append(CategoriesMapping(mapping))\n        return var, tr\n\n    def clear(self):\n        \"\"\"Clear the model state.\n        \"\"\"\n        super().clear()\n        self.values_model.clear()\n\n    def move_rows(self, rows, offset):\n        if not rows:\n            return\n        assert len(rows) == 1\n        i = rows[0].row()\n        if offset > 0:\n            offset += 1\n        self.values_model.moveRows(QModelIndex(), i, 1, QModelIndex(), i + offset)\n        self.variable_changed.emit()\n\n    def move_up(self):\n        rows = self.values_edit.selectionModel().selectedRows()\n        self.move_rows(rows, -1)\n\n    def move_down(self):\n        rows = self.values_edit.selectionModel().selectedRows()\n        self.move_rows(rows, 1)\n\n    @Slot()\n    def on_values_changed(self):\n        self.variable_changed.emit()\n\n    @Slot()\n    def on_value_selection_changed(self):\n        rows = self.values_edit.selectionModel().selectedRows()\n        if len(rows) == 1:\n            i = rows[0].row()\n            self.move_value_up.setEnabled(i != 0)\n            self.move_value_down.setEnabled(i != self.values_model.rowCount() - 1)\n        else:\n            self.move_value_up.setEnabled(False)\n            self.move_value_down.setEnabled(False)\n\n    def _remove_category(self):\n        \"\"\"\n        Remove the current selected category.\n\n        If the item is an existing category present in the source variable it\n        is marked as removed in the view. But if it was added in the set\n        transformation it is removed entirely from the model and view.\n        \"\"\"\n        view = self.values_edit\n        rows = view.selectionModel().selectedRows(0)\n        if not rows:\n            return  # pragma: no cover\n        for index in rows:\n            model = index.model()\n            state = index.data(EditStateRole)\n            pos = index.data(SourcePosRole)\n            if pos is not None and pos >= 0:\n                # existing level -> only mark/toggle its dropped state,\n                model.setData(\n                    index,\n                    ItemEditState.Dropped if state != ItemEditState.Dropped\n                    else ItemEditState.NoState,\n                    EditStateRole)\n            elif state == ItemEditState.Added:\n                # new level -> remove it\n                model.removeRow(index.row())\n            else:\n                assert False, \"invalid state '{}' for {}\" \\\n                    .format(state, index.row())\n\n    def _add_category(self):\n        \"\"\"\n        Add a new category\n        \"\"\"\n        view = self.values_edit\n        model = view.model()\n\n        with disconnected(model.dataChanged, self.on_values_changed,\n                          Qt.UniqueConnection):\n            row = model.rowCount()\n            if not model.insertRow(model.rowCount()):\n                return\n            index = model.index(row, 0)\n            model.setItemData(\n                index, {\n                    Qt.EditRole: \"\",\n                    SourcePosRole: None,\n                    EditStateRole: ItemEditState.Added\n                }\n            )\n            view.setCurrentIndex(index)\n            view.edit(index)\n        self.on_values_changed()\n\n    def _merge_categories(self) -> None:\n        \"\"\"\n        Merge less common categories into one with the dialog for merge\n        selection.\n        \"\"\"\n        view = self.values_edit\n        model = view.model()  # type: QAbstractItemModel\n\n        selected_attributes = [ind.data(SourceNameRole)\n                               for ind in view.selectedIndexes()]\n        dlg = GroupItemsDialog(\n            self.var, self._values, selected_attributes,\n            self.merge_dialog_settings.get(self.var, {}), self,\n            windowTitle=\"Import Options\",\n            sizeGripEnabled=True,\n        )\n        dlg.setWindowModality(Qt.WindowModal)\n        status = dlg.exec()\n        dlg.deleteLater()\n        self.merge_dialog_settings[self.var] = dlg.get_dialog_settings()\n\n        rows = (model.index(i, 0) for i in range(model.rowCount()))\n\n        def complete_merge(text, merge_attributes):\n            # write the new text for edit role in all rows\n            with disconnected(model.dataChanged, self.on_values_changed):\n                for row in rows:\n                    if row.data(SourceNameRole) in merge_attributes:\n                        model.setData(row, text, Qt.EditRole)\n            self.variable_changed.emit()\n\n        if status == QDialog.Accepted:\n            complete_merge(\n                dlg.get_merged_value_name(), dlg.get_merge_attributes()\n            )\n\n    def _rename_selected_categories(self):\n        \"\"\"\n        Rename selected categories and merging them.\n\n        Popup an editable combo box for selection/edit of a new value.\n        \"\"\"\n        view = self.values_edit\n        selmodel = view.selectionModel()\n        index = view.currentIndex()\n        if not selmodel.isSelected(index):\n            indices = selmodel.selectedRows(0)\n            if indices:\n                index = indices[0]\n        # delegate to the CategoriesEditDelegate\n        view.edit(index)\n\n\nclass ContinuousVariableEditor(VariableEditor):\n    # TODO: enable editing of display format...\n    pass\n\n\nclass TimeVariableEditor(VariableEditor):\n    def __init__(self, parent=None, **kwargs):\n        super().__init__(parent, **kwargs)\n        form = self.layout().itemAt(0)\n\n        self.format_cb = QComboBox()\n        for item, data in [(\"Detect automatically\", (None, 1, 1))] + list(\n            Orange.data.TimeVariable.ADDITIONAL_FORMATS.items()\n        ):\n            self.format_cb.addItem(item, StrpTime(item, *data))\n        self.format_cb.currentIndexChanged.connect(self.variable_changed)\n        form.insertRow(2, \"Format:\", self.format_cb)\n\n    def set_data(self, var, transform=()):\n        super().set_data(var, transform)\n        if self.parent() is not None and isinstance(self.parent().var, Time):\n            # when transforming from time to time disable format selection combo\n            self.format_cb.setEnabled(False)\n        else:\n            # select the format from StrpTime transform\n            for tr in transform:\n                if isinstance(tr, StrpTime):\n                    index = self.format_cb.findText(tr.label)\n                    self.format_cb.setCurrentIndex(index)\n            self.format_cb.setEnabled(True)\n\n    def get_data(self):\n        var, tr = super().get_data()\n        if var is not None and (self.parent() is None or not isinstance(self.parent().var, Time)):\n            # do not add StrpTime when transforming from time to time\n            tr.insert(0, self.format_cb.currentData())\n        return var, tr\n\n\ndef variable_icon(var):\n    # type: (Union[Variable, Type[Variable], ReinterpretTransform]) -> QIcon\n    if not isinstance(var, type):\n        var = type(var)\n\n    if issubclass(var, (Categorical, AsCategorical)):\n        return gui.attributeIconDict[1]\n    elif issubclass(var, (Real, AsContinuous)):\n        return gui.attributeIconDict[2]\n    elif issubclass(var, (String, AsString)):\n        return gui.attributeIconDict[3]\n    elif issubclass(var, (Time, AsTime)):\n        return gui.attributeIconDict[4]\n    else:\n        return gui.attributeIconDict[-1]\n\n\n#: ItemDataRole storing the data vector transform\n#: (`List[Union[ReinterpretTransform, Transform]]`)\nTransformRole = Qt.UserRole + 42\n\n\nclass VariableEditDelegate(QStyledItemDelegate):\n    ReinterpretNames = {\n        AsCategorical: \"categorical\", AsContinuous: \"numeric\",\n        AsString: \"string\", AsTime: \"time\"\n    }\n\n    def initStyleOption(self, option, index):\n        # type: (QStyleOptionViewItem, QModelIndex) -> None\n        super().initStyleOption(option, index)\n        item = index.data(Qt.EditRole)\n        var = tr = None\n        if isinstance(item, DataVectorTypes):\n            var = item.vtype\n            option.icon = variable_icon(var)\n        if isinstance(item, VariableTypes):\n            var = item\n            option.icon = variable_icon(item)\n        elif isinstance(item, Orange.data.Variable):\n            var = item\n            option.icon = gui.attributeIconDict[var]\n\n        transform = index.data(TransformRole)\n        if not isinstance(transform, list):\n            transform = []\n\n        if transform and isinstance(transform[0], ReinterpretTransformTypes):\n            option.icon = variable_icon(transform[0])\n\n        if not option.icon.isNull():\n            option.features |= QStyleOptionViewItem.HasDecoration\n\n        if var is not None:\n            text = var.name\n            for tr in transform:\n                if isinstance(tr, Rename):\n                    text = (\"{} \\N{RIGHTWARDS ARROW} {}\"\n                            .format(var.name, tr.name))\n            for tr in transform:\n                if isinstance(tr, ReinterpretTransformTypes):\n                    text += f\" (reinterpreted as \" \\\n                            f\"{self.ReinterpretNames[type(tr)]})\"\n            option.text = text\n        if transform:\n            # mark as changed (maybe also change color, add text, ...)\n            option.font.setItalic(True)\n\n        multiplicity = index.data(MultiplicityRole)\n        if isinstance(multiplicity, int) and multiplicity > 1:\n            option.palette.setBrush(QPalette.Text, QBrush(Qt.red))\n            option.palette.setBrush(QPalette.HighlightedText, QBrush(Qt.red))\n\n    def helpEvent(self, event: QHelpEvent, view: QAbstractItemView,\n                  option: QStyleOptionViewItem, index: QModelIndex) -> bool:\n        multiplicity = index.data(MultiplicityRole)\n        name = VariableListModel.effective_name(index)\n        if isinstance(multiplicity, int) and multiplicity > 1 \\\n                and name is not None:\n            QToolTip.showText(\n                event.globalPos(), f\"Name `{name}` is duplicated\",\n                view.viewport()\n            )\n            return True\n        else:  # pragma: no cover\n            return super().helpEvent(event, view, option, index)\n\n\n# Item model for edited variables (Variable). Define a display role to be the\n# source variable name. This is used only in keyboard search. The display is\n# otherwise completely handled by a delegate.\nclass VariableListModel(CountedListModel):\n    def data(self, index, role=Qt.DisplayRole):\n        # type: (QModelIndex, Qt.ItemDataRole) -> Any\n        row = index.row()\n        if not index.isValid() or not 0 <= row < self.rowCount():\n            return None\n        if role == Qt.DisplayRole:\n            item = self[row]\n            if isinstance(item, VariableTypes):\n                return item.name\n            if isinstance(item, DataVectorTypes):\n                return item.vtype.name\n        return super().data(index, role)\n\n    def key(self, index):\n        return VariableListModel.effective_name(index)\n\n    def keyRoles(self):  # type: () -> FrozenSet[int]\n        return frozenset((Qt.DisplayRole, Qt.EditRole, TransformRole))\n\n    @staticmethod\n    def effective_name(index) -> Optional[str]:\n        item = index.data(Qt.EditRole)\n        if isinstance(item, DataVectorTypes):\n            var = item.vtype\n        elif isinstance(item, VariableTypes):\n            var = item\n        else:\n            return None\n        tr = index.data(TransformRole)\n        return effective_name(var, tr or [])\n\n\ndef effective_name(var: Variable, tr: Sequence[Transform]) -> str:\n    name = var.name\n    for t in tr:\n        if isinstance(t, Rename):\n            name = t.name\n    return name\n\n\nclass ReinterpretVariableEditor(VariableEditor):\n    \"\"\"\n    A 'compound' variable editor capable of variable type reinterpretations.\n    \"\"\"\n    _editors = {\n        Categorical: 0,\n        Real: 1,\n        String: 2,\n        Time: 3,\n        type(None): -1,\n    }\n\n    def __init__(self, parent=None, **kwargs):\n        # Explicitly skip VariableEditor's __init__, this is ugly but we have\n        # a completely different layout/logic as a compound editor (should\n        # really not subclass VariableEditor).\n        super(VariableEditor, self).__init__(parent, **kwargs)  # pylint: disable=bad-super-call\n        self.var = None  # type: Optional[Variable]\n        self.__transform = None  # type: Optional[ReinterpretTransform]\n        self.__data = None  # type: Optional[DataVector]\n        #: Stored transform state indexed by variable. Used to preserve state\n        #: between type switches.\n        self.__history = {}  # type: Dict[Variable, List[Transform]]\n\n        self.setLayout(QStackedLayout())\n\n        def decorate(editor: VariableEditor) -> VariableEditor:\n            \"\"\"insert an type combo box into a `editor`'s layout.\"\"\"\n            form = editor.layout().itemAt(0)\n            assert isinstance(form, QFormLayout)\n            typecb = QComboBox(objectName=\"type-combo\")\n            typecb.addItem(variable_icon(Categorical), \"Categorical\", Categorical)\n            typecb.addItem(variable_icon(Real), \"Numeric\", Real)\n            typecb.addItem(variable_icon(String), \"Text\", String)\n            typecb.addItem(variable_icon(Time), \"Time\", Time)\n            typecb.activated[int].connect(self.__reinterpret_activated)\n            form.insertRow(1, \"Type:\", typecb)\n            # Insert the typecb after name edit in the focus chain\n            name_edit = editor.findChild(QLineEdit, )\n            if name_edit is not None:\n                QWidget.setTabOrder(name_edit, typecb)\n            return editor\n        # This is ugly. Create an editor for each type and insert a type\n        # selection combo box into its layout. Switch between widgets\n        # on type change.\n        self.disc_edit = dedit = decorate(DiscreteVariableEditor())\n        cedit = decorate(ContinuousVariableEditor())\n        tedit = decorate(TimeVariableEditor())\n        sedit = decorate(VariableEditor())\n\n        for ed in [dedit, cedit, tedit, sedit]:\n            ed.variable_changed.connect(self.variable_changed)\n\n        self.layout().addWidget(dedit)\n        self.layout().addWidget(cedit)\n        self.layout().addWidget(sedit)\n        self.layout().addWidget(tedit)\n\n    def set_data(self, data, transform=()):  # pylint: disable=arguments-differ\n        # type: (Optional[DataVector], Sequence[Transform]) -> None\n        \"\"\"\n        Set the editor data.\n\n        Note\n        ----\n        This must be a `DataVector` as the vector's values are needed for type\n        reinterpretation/casts.\n\n        If the `transform` sequence contains ReinterpretTransform then it\n        must be in the first position.\n        \"\"\"\n        type_transform = None  # type: Optional[ReinterpretTransform]\n        if transform:\n            _tr = transform[0]\n            if isinstance(_tr, ReinterpretTransformTypes):\n                type_transform = _tr\n                transform = transform[1:]\n            assert not any(isinstance(t, ReinterpretTransformTypes)\n                           for t in transform)\n        self.__transform = type_transform\n        self.__data = data\n        self.var = data.vtype if data is not None else None\n\n        if type_transform is not None and data is not None:\n            data = type_transform(data)\n        if data is not None:\n            var = data.vtype\n        else:\n            var = None\n        index = self._editors.get(type(var), -1)\n        self.layout().setCurrentIndex(index)\n        if index != -1:\n            w = self.layout().currentWidget()\n            assert isinstance(w, VariableEditor)\n            if isinstance(var, Categorical):\n                w.set_data_categorical(var, data.data(), transform=transform)\n            else:\n                w.set_data(var, transform=transform)\n            self.__history[var] = tuple(transform)\n            cb = w.findChild(QComboBox, \"type-combo\")\n            cb.setCurrentIndex(index)\n\n    def get_data(self):\n        # type: () -> Tuple[Variable, Sequence[Transform]]\n        editor = self.layout().currentWidget()  # type: VariableEditor\n        var, tr = editor.get_data()\n        if type(var) != type(self.var):  # pylint: disable=unidiomatic-typecheck\n            assert self.__transform is not None\n            var = self.var\n            tr = [self.__transform, *tr]\n        return var, tr\n\n    def __reinterpret_activated(self, index):\n        layout = self.layout()\n        assert isinstance(layout, QStackedLayout)\n        if index == layout.currentIndex():\n            return\n        current = layout.currentWidget()\n        assert isinstance(current, VariableEditor)\n        Specific = {\n            Categorical: CategoricalTransformTypes\n        }\n        _var, _tr = current.get_data()\n        if _var is not None:\n            self.__history[_var] = _tr\n\n        var = self.var\n        transform = self.__transform\n        # take/preserve the general transforms that apply to all types\n        specific = Specific.get(type(var), ())\n        _tr = [t for t in _tr if not isinstance(t, specific)]\n\n        layout.setCurrentIndex(index)\n        w = layout.currentWidget()\n        cb = w.findChild(QComboBox, \"type-combo\")\n        cb.setCurrentIndex(index)\n        cb.setFocus()\n        target = cb.itemData(index, Qt.UserRole)\n        assert issubclass(target, VariableTypes)\n        if not isinstance(var, target):\n            if target == Real:\n                transform = AsContinuous()\n            elif target == Categorical:\n                transform = AsCategorical()\n            elif target == Time:\n                transform = AsTime()\n            elif target == String:\n                transform = AsString()\n        else:\n            transform = None\n            var = self.var\n\n        self.__transform = transform\n        data = None\n        if transform is not None and self.__data is not None:\n            data = transform(self.__data)\n            var = data.vtype\n\n        if var in self.__history:\n            tr = self.__history[var]\n        else:\n            tr = []\n        # type specific transform\n        specific = Specific.get(type(var), ())\n        # merge tr and _tr\n        tr = _tr + [t for t in tr if isinstance(t, specific)]\n        with disconnected(\n                w.variable_changed, self.variable_changed,\n                Qt.UniqueConnection\n        ):\n            if isinstance(w, DiscreteVariableEditor):\n                data = data or self.__data\n                w.set_data_categorical(var, data.data(), transform=tr)\n            else:\n                w.set_data(var, transform=tr)\n        self.variable_changed.emit()\n\n    def set_merge_context(self, merge_context):\n        self.disc_edit.merge_dialog_settings = merge_context\n\n    def get_merge_context(self):\n        return self.disc_edit.merge_dialog_settings\n\n\nclass OWEditDomain(widget.OWWidget):\n    name = \"Edit Domain\"\n    description = \"Rename variables, edit categories and variable annotations.\"\n    icon = \"icons/EditDomain.svg\"\n    priority = 3125\n    keywords = [\"rename\", \"drop\", \"reorder\", \"order\"]\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        data = Output(\"Data\", Orange.data.Table)\n\n    class Error(widget.OWWidget.Error):\n        duplicate_var_name = widget.Msg(\"A variable name is duplicated.\")\n\n    settingsHandler = settings.DomainContextHandler()\n    settings_version = 2\n\n    _domain_change_store = settings.ContextSetting({})\n    _selected_item = settings.ContextSetting(None)  # type: Optional[Tuple[str, int]]\n    _merge_dialog_settings = settings.ContextSetting({})\n    output_table_name = settings.ContextSetting(\"\")\n\n    want_main_area = False\n\n    def __init__(self):\n        super().__init__()\n        self.data = None  # type: Optional[Orange.data.Table]\n        #: The current selected variable index\n        self.selected_index = -1\n        self._invalidated = False\n        self.typeindex = 0\n\n        main = gui.hBox(self.controlArea, spacing=6)\n        box = gui.vBox(main, \"Variables\")\n\n        self.variables_model = VariableListModel(parent=self)\n        self.variables_view = self.domain_view = QListView(\n            selectionMode=QListView.SingleSelection,\n            uniformItemSizes=True,\n        )\n        self.variables_view.setItemDelegate(VariableEditDelegate(self))\n        self.variables_view.setModel(self.variables_model)\n        self.variables_view.selectionModel().selectionChanged.connect(\n            self._on_selection_changed\n        )\n        box.layout().addWidget(self.variables_view)\n\n        box = gui.vBox(main, \"Edit\")\n        self._editor = ReinterpretVariableEditor()\n        box.layout().addWidget(self._editor)\n\n        self.le_output_name = gui.lineEdit(\n            self.buttonsArea, self, \"output_table_name\", \"Output table name: \",\n            orientation=Qt.Horizontal)\n\n        gui.rubber(self.buttonsArea)\n\n        bbox = gui.hBox(self.buttonsArea)\n        breset_all = gui.button(\n            bbox, self, \"Reset All\",\n            objectName=\"button-reset-all\",\n            toolTip=\"Reset all variables to their input state.\",\n            autoDefault=False,\n            callback=self.reset_all\n        )\n        breset = gui.button(\n            bbox, self, \"Reset Selected\",\n            objectName=\"button-reset\",\n            toolTip=\"Rest selected variable to its input state.\",\n            autoDefault=False,\n            callback=self.reset_selected\n        )\n        bapply = gui.button(\n            bbox, self, \"Apply\",\n            objectName=\"button-apply\",\n            toolTip=\"Apply changes and commit data on output.\",\n            default=True,\n            autoDefault=False,\n            callback=self.commit\n        )\n\n        self.variables_view.setFocus(Qt.NoFocusReason)  # initial focus\n\n    @Inputs.data\n    def set_data(self, data):\n        \"\"\"Set input dataset.\"\"\"\n        self.closeContext()\n        self.clear()\n        self.data = data\n\n        if self.data is not None:\n            self.setup_model(data)\n            self.le_output_name.setPlaceholderText(data.name)\n            self.openContext(self.data)\n            self._editor.set_merge_context(self._merge_dialog_settings)\n            self._restore()\n        else:\n            self.le_output_name.setPlaceholderText(\"\")\n\n        self.commit()\n\n    def clear(self):\n        \"\"\"Clear the widget state.\"\"\"\n        self.data = None\n        self.variables_model.clear()\n        self.clear_editor()\n        assert self.selected_index == -1\n        self.selected_index = -1\n\n        self._selected_item = None\n        self._domain_change_store = {}\n        self._merge_dialog_settings = {}\n\n    def reset_selected(self):\n        \"\"\"Reset the currently selected variable to its original state.\"\"\"\n        ind = self.selected_var_index()\n        if ind >= 0:\n            model = self.variables_model\n            midx = model.index(ind)\n            var = midx.data(Qt.EditRole)\n            tr = midx.data(TransformRole)\n            if not tr:\n                return  # nothing to reset\n            editor = self._editor\n            with disconnected(editor.variable_changed,\n                              self._on_variable_changed):\n                model.setData(midx, [], TransformRole)\n                editor.set_data(var, transform=[])\n            self._invalidate()\n\n    def reset_all(self):\n        \"\"\"Reset all variables to their original state.\"\"\"\n        self._domain_change_store = {}\n        if self.data is not None:\n            model = self.variables_model\n            for i in range(model.rowCount()):\n                midx = model.index(i)\n                model.setData(midx, [], TransformRole)\n            index = self.selected_var_index()\n            if index >= 0:\n                self.open_editor(index)\n            self._invalidate()\n\n    def selected_var_index(self):\n        \"\"\"Return the current selected variable index.\"\"\"\n        rows = self.variables_view.selectedIndexes()\n        assert len(rows) <= 1\n        return rows[0].row() if rows else -1\n\n    def setup_model(self, data: Orange.data.Table):\n        model = self.variables_model\n        vars_ = []\n        columns = []\n        for i, _, var, coldata in enumerate_columns(data):\n            var = abstract(var)\n            vars_.append(var)\n            if isinstance(var, Categorical):\n                data = CategoricalVector(var, coldata)\n            elif isinstance(var, Real):\n                data = RealVector(var, coldata)\n            elif isinstance(var, Time):\n                data = TimeVector(var, coldata)\n            elif isinstance(var, String):\n                data = StringVector(var, coldata)\n            columns.append(data)\n\n        model[:] = vars_\n        for i, d in enumerate(columns):\n            model.setData(model.index(i), d, Qt.EditRole)\n\n    def _restore(self, ):\n        \"\"\"\n        Restore the edit transform from saved state.\n        \"\"\"\n        model = self.variables_model\n        for i in range(model.rowCount()):\n            midx = model.index(i, 0)\n            coldesc = model.data(midx, Qt.EditRole)  # type: DataVector\n            tr = self._restore_transform(coldesc.vtype)\n            if tr:\n                model.setData(midx, tr, TransformRole)\n\n        # Restore the current variable selection\n        i = -1\n        if self._selected_item is not None:\n            for i, vec in enumerate(model):\n                if vec.vtype.name_type() == self._selected_item:\n                    break\n        if i == -1 and model.rowCount():\n            i = 0\n\n        if i != -1:\n            itemmodels.select_row(self.variables_view, i)\n\n    def _on_selection_changed(self):\n        self.selected_index = self.selected_var_index()\n        if self.selected_index != -1:\n            self._selected_item = self.variables_model[self.selected_index].vtype.name_type()\n        else:\n            self._selected_item = None\n        self.open_editor(self.selected_index)\n\n    def open_editor(self, index):\n        # type: (int) -> None\n        self.clear_editor()\n        model = self.variables_model\n        if not 0 <= index < model.rowCount():\n            return\n        idx = model.index(index, 0)\n        vector = model.data(idx, Qt.EditRole)\n        tr = model.data(idx, TransformRole)\n        if tr is None:\n            tr = []\n        editor = self._editor\n        editor.set_data(vector, transform=tr)\n        editor.variable_changed.connect(\n            self._on_variable_changed, Qt.UniqueConnection\n        )\n\n    def clear_editor(self):\n        current = self._editor\n        try:\n            current.variable_changed.disconnect(self._on_variable_changed)\n        except TypeError:\n            pass\n        current.set_data(None)\n        current.layout().currentWidget().clear()\n\n    @Slot()\n    def _on_variable_changed(self):\n        \"\"\"User edited the current variable in editor.\"\"\"\n        assert 0 <= self.selected_index <= len(self.variables_model)\n        editor = self._editor\n        var, transform = editor.get_data()\n        model = self.variables_model\n        midx = model.index(self.selected_index, 0)\n        model.setData(midx, transform, TransformRole)\n        self._store_transform(var, transform)\n        self._invalidate()\n\n    def _store_transform(self, var, transform):\n        # type: (Variable, List[Transform]) -> None\n        self._domain_change_store[deconstruct(var)] = [deconstruct(t) for t in transform]\n\n    def _restore_transform(self, var):\n        # type: (Variable) -> List[Transform]\n        tr_ = self._domain_change_store.get(deconstruct(var), [])\n        tr = []\n\n        for t in tr_:\n            try:\n                tr.append(reconstruct(*t))\n            except (NameError, TypeError) as err:\n                warnings.warn(\n                    \"Failed to restore transform: {}, {!r}\".format(t, err),\n                    UserWarning, stacklevel=2\n                )\n        return tr\n\n    def _invalidate(self):\n        self._set_modified(True)\n\n    def _set_modified(self, state):\n        self._invalidated = state\n        b = self.findChild(QPushButton, \"button-apply\")\n        if isinstance(b, QPushButton):\n            f = b.font()\n            f.setItalic(state)\n            b.setFont(f)\n\n    def commit(self):\n        \"\"\"\n        Apply the changes to the input data and send the changed data to output.\n        \"\"\"\n        self._set_modified(False)\n        self.Error.duplicate_var_name.clear()\n\n        data = self.data\n        if data is None:\n            self.Outputs.data.send(None)\n            return\n        model = self.variables_model\n\n        def state(i):\n            # type: (int) -> Tuple[DataVector, List[Transform]]\n            midx = self.variables_model.index(i, 0)\n            return (model.data(midx, Qt.EditRole),\n                    model.data(midx, TransformRole))\n\n        state = [state(i) for i in range(model.rowCount())]\n        input_vars = data.domain.variables + data.domain.metas\n        if self.output_table_name in (\"\", data.name) \\\n                and not any(requires_transform(var, trs)\n                            for var, (_, trs) in zip(input_vars, state)):\n            self.Outputs.data.send(data)\n            return\n\n        assert all(v_.vtype.name == v.name\n                   for v, (v_, _) in zip(input_vars, state))\n        output_vars = []\n        unlinked_vars = []\n        unlink_domain = False\n        for (_, tr), v in zip(state, input_vars):\n            if tr:\n                var = apply_transform(v, data, tr)\n                if requires_unlink(v, tr):\n                    unlinked_var = var.copy(compute_value=None)\n                    unlink_domain = True\n                else:\n                    unlinked_var = var\n            else:\n                unlinked_var = var = v\n            output_vars.append(var)\n            unlinked_vars.append(unlinked_var)\n\n        if len(output_vars) != len({v.name for v in output_vars}):\n            self.Error.duplicate_var_name()\n            self.Outputs.data.send(None)\n            return\n\n        domain = data.domain\n        nx = len(domain.attributes)\n        ny = len(domain.class_vars)\n\n        def construct_domain(vars_list):\n            # Move non primitive Xs, Ys to metas (if they were changed)\n            Xs = [v for v in vars_list[:nx] if v.is_primitive()]\n            Ys = [v for v in vars_list[nx: nx + ny] if v.is_primitive()]\n            Ms = vars_list[nx + ny:] + \\\n                 [v for v in vars_list[:nx + ny] if not  v.is_primitive()]\n            return Orange.data.Domain(Xs, Ys, Ms)\n\n        domain = construct_domain(output_vars)\n        new_data = data.transform(domain)\n        if unlink_domain:\n            unlinked_domain = construct_domain(unlinked_vars)\n            new_data = new_data.from_numpy(\n                unlinked_domain,\n                new_data.X, new_data.Y, new_data.metas, new_data.W,\n                new_data.attributes, new_data.ids\n            )\n        if self.output_table_name:\n            new_data.name = self.output_table_name\n        self.Outputs.data.send(new_data)\n\n    def sizeHint(self):\n        sh = super().sizeHint()\n        return sh.expandedTo(QSize(660, 550))\n\n    def storeSpecificSettings(self):\n        \"\"\"\n        Update setting before context closes - also when widget closes.\n        \"\"\"\n        self._merge_dialog_settings = self._editor.get_merge_context()\n\n    def send_report(self):\n\n        if self.data is not None:\n            model = self.variables_model\n            state = ((model.data(midx, Qt.EditRole),\n                      model.data(midx, TransformRole))\n                     for i in range(model.rowCount())\n                     for midx in [model.index(i)])\n            parts = []\n            for vector, trs in state:\n                if trs:\n                    parts.append(report_transform(vector.vtype, trs))\n            if parts:\n                html = (\"<ul>\" +\n                        \"\".join(map(\"<li>{}</li>\".format, parts)) +\n                        \"</ul>\")\n            else:\n                html = \"No changes\"\n            self.report_raw(\"\", html)\n        else:\n            self.report_data(None)\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        # pylint: disable=bad-continuation\n        if version is None or version <= 1:\n            hints_ = context.values.get(\"domain_change_hints\", ({}, -2))[0]\n            store = []\n            ns = \"Orange.data.variable\"\n            mapping = {\n                \"DiscreteVariable\":\n                    lambda name, args, attrs:\n                        (\"Categorical\", (name, tuple(args[0][1]), ())),\n                \"TimeVariable\":\n                    lambda name, _, attrs:\n                        (\"Time\", (name, ())),\n                \"ContinuousVariable\":\n                    lambda name, _, attrs:\n                        (\"Real\", (name, (3, \"f\"), ())),\n                \"StringVariable\":\n                    lambda name, _, attrs:\n                        (\"String\", (name, ())),\n            }\n            for (module, class_name, *rest), target in hints_.items():\n                if module != ns:\n                    continue\n                f = mapping.get(class_name)\n                if f is None:\n                    continue\n                trs = []\n                key_mapped = f(*rest)\n                item_mapped = f(*target[2:])\n                src = reconstruct(*key_mapped)   # type: Variable\n                dst = reconstruct(*item_mapped)  # type: Variable\n                if src.name != dst.name:\n                    trs.append(Rename(dst.name))\n                if src.annotations != dst.annotations:\n                    trs.append(Annotate(dst.annotations))\n                if isinstance(src, Categorical):\n                    if src.categories != dst.categories:\n                        assert len(src.categories) == len(dst.categories)\n                        trs.append(CategoriesMapping(\n                            list(zip(src.categories, dst.categories))))\n                store.append((deconstruct(src), [deconstruct(tr) for tr in trs]))\n            context.values[\"_domain_change_store\"] = (dict(store), -2)\n\n\ndef enumerate_columns(\n        table: Orange.data.Table\n) -> Iterable[Tuple[int, str, Orange.data.Variable, Callable[[], ndarray]]]:\n    domain = table.domain\n    for i, (var, role) in enumerate(\n            chain(zip(domain.attributes, repeat(\"x\")),\n                  zip(domain.class_vars, repeat(\"y\")),\n                  zip(domain.metas, repeat(\"m\"))),\n    ):\n        if i >= len(domain.variables):\n            i = len(domain.variables) - i - 1\n        data = partial(table_column_data, table, i)\n        yield i, role, var, data\n\n\ndef table_column_data(\n        table: Orange.data.Table,\n        var: Union[Orange.data.Variable, int],\n        dtype=None\n) -> MArray:\n    col, copy = table.get_column_view(var)\n    var = table.domain[var]  # type: Orange.data.Variable\n    if var.is_primitive() and not np.issubdtype(col.dtype, np.inexact):\n        col = col.astype(float)\n        copy = True\n\n    if dtype is None:\n        if isinstance(var, Orange.data.TimeVariable):\n            dtype = np.dtype(\"M8[us]\")\n            col = col * 1e6\n        elif isinstance(var, Orange.data.ContinuousVariable):\n            dtype = np.dtype(float)\n        elif isinstance(var, Orange.data.DiscreteVariable):\n            _values = tuple(var.values)\n            _n_values = len(_values)\n            dtype = np.dtype(int, metadata={\n                \"__formatter\": lambda i: _values[i] if 0 <= i < _n_values else \"?\"\n            })\n        elif isinstance(var, Orange.data.StringVariable):\n            dtype = np.dtype(object)\n        else:\n            assert False\n    mask = orange_isna(var, col)\n\n    if dtype != col.dtype:\n        col = col.astype(dtype)\n        copy = True\n\n    if not copy:\n        col = col.copy()\n    return MArray(col, mask=mask)\n\n\ndef report_transform(var, trs):\n    # type: (Variable, List[Transform]) -> str\n    \"\"\"\n    Return a html fragment summarizing the changes applied by `trs` list.\n\n    Parameters\n    ----------\n    var : Variable\n        A variable descriptor no which trs operates\n    trs : List[Transform]\n        A non empty list of `Transform` instances.\n\n    Returns\n    -------\n    report : str\n    \"\"\"\n    # pylint: disable=too-many-branches\n    ReinterpretTypeCode = {\n        AsCategorical:  \"C\", AsContinuous: \"N\", AsString: \"S\", AsTime: \"T\",\n    }\n\n    def type_char(value: ReinterpretTransform) -> str:\n        return ReinterpretTypeCode.get(type(value), \"?\")\n\n    def strike(text):\n        return \"<s>{}</s>\".format(escape(text))\n\n    def i(text):\n        return \"<i>{}</i>\".format(escape(text))\n\n    def text(text):\n        return \"<span>{}</span>\".format(escape(text))\n    assert trs\n    rename = annotate = catmap = unlink = None\n    reinterpret = None\n\n    for tr in trs:\n        if isinstance(tr, Rename):\n            rename = tr\n        elif isinstance(tr, Annotate):\n            annotate = tr\n        elif isinstance(tr, CategoriesMapping):\n            catmap = tr\n        elif isinstance(tr, Unlink):\n            unlink = tr\n        elif isinstance(tr, ReinterpretTransformTypes):\n            reinterpret = tr\n\n    if reinterpret is not None:\n        header = \"{} \u2192 ({}) {}\".format(\n            var.name, type_char(reinterpret),\n            rename.name if rename is not None else var.name\n        )\n    elif rename is not None:\n        header = \"{} \u2192 {}\".format(var.name, rename.name)\n    else:\n        header = var.name\n    if unlink is not None:\n        header += \"(unlinked from source)\"\n\n    values_section = None\n    if catmap is not None:\n        values_section = (\"Values\", [])\n        lines = values_section[1]\n        for ci, cj in catmap.mapping:\n            if ci is None:\n                item = cj + (\"&nbsp;\" * 3) + \"(added)\"\n            elif cj is None:\n                item = strike(ci)\n            else:\n                item = ci + \" \u2192 \" + cj\n            lines.append(item)\n\n    annotate_section = None\n    if annotate is not None:\n        annotate_section = (\"Labels\", [])\n        lines = annotate_section[1]\n        old = dict(var.annotations)\n        new = dict(annotate.annotations)\n        for name in sorted(set(old) - set(new)):\n            lines.append(\n                \"<s>\" + i(name) + \" : \" + text(old[name]) + \"</s>\"\n            )\n        for name in sorted(set(new) - set(old)):\n            lines.append(\n                i(name) + \" : \" + text(new[name]) + \"&nbsp;\" * 3 + i(\"(new)\")\n            )\n\n        for name in sorted(set(new) & set(old)):\n            if new[name] != old[name]:\n                lines.append(\n                    i(name) + \" : \" + text(old[name]) + \" \u2192 \" + text(new[name])\n                )\n\n    html = [\"<div style='font-weight: bold;'>{}</div>\".format(header)]\n    for title, contents in filter(None, [values_section, annotate_section]):\n        section_header = \"<div>{}:</div>\".format(title)\n        section_contents = \"<br/>\\n\".join(contents)\n        html.append(section_header)\n        html.append(\n            \"<div style='padding-left: 1em;'>\" +\n            section_contents +\n            \"</div>\"\n        )\n    return \"\\n\".join(html)\n\n\ndef abstract(var):\n    # type: (Orange.data.Variable) -> Variable\n    \"\"\"\n    Return `Varaible` descriptor for an `Orange.data.Variable` instance.\n\n    Parameters\n    ----------\n    var : Orange.data.Variable\n\n    Returns\n    -------\n    var : Variable\n    \"\"\"\n    annotations = tuple(sorted(\n        (key, str(value))\n        for key, value in var.attributes.items()\n    ))\n    linked = var.compute_value is not None\n    if isinstance(var, Orange.data.DiscreteVariable):\n        return Categorical(var.name, tuple(var.values), annotations, linked)\n    elif isinstance(var, Orange.data.TimeVariable):\n        return Time(var.name, annotations, linked)\n    elif isinstance(var, Orange.data.ContinuousVariable):\n        return Real(var.name, (var.number_of_decimals, 'f'), annotations, linked)\n    elif isinstance(var, Orange.data.StringVariable):\n        return String(var.name, annotations, linked)\n    else:\n        raise TypeError\n\n\ndef _parse_attributes(mapping):\n    # type: (Iterable[Tuple[str, str]]) -> Dict[str, Any]\n    # Use the same functionality that parses attributes\n    # when reading text files\n    return Orange.data.Flags([\n        \"{}={}\".format(*item) for item in mapping\n    ]).attributes\n\n\ndef apply_transform(var, table, trs):\n    # type: (Orange.data.Variable, Orange.data.Table, List[Transform]) -> Orange.data.Variable\n    \"\"\"\n    Apply a list of `Transform` instances on an `Orange.data.Variable`.\n    \"\"\"\n    if trs and isinstance(trs[0], ReinterpretTransformTypes):\n        reinterpret, trs = trs[0], trs[1:]\n        coldata = table_column_data(table, var)\n        var = apply_reinterpret(var, reinterpret, coldata)\n    if trs:\n        return apply_transform_var(var, trs)\n    else:\n        return var\n\n\ndef requires_unlink(var: Orange.data.Variable, trs: List[Transform]) -> bool:\n    # Variable is only unlinked if it has compute_value  or if it has other\n    # transformations (that might had added compute_value)\n    return trs is not None \\\n           and any(isinstance(tr, Unlink) for tr in trs) \\\n           and (var.compute_value is not None or len(trs) > 1)\n\n\ndef requires_transform(var: Orange.data.Variable, trs: List[Transform]) -> bool:\n    # Unlink is treated separately: Unlink is required only if the variable\n    # has compute_value. Hence tranform is required if it has any\n    # transformations other than Unlink, or if unlink is indeed required.\n    return trs is not None and (\n        not all(isinstance(tr, Unlink) for tr in trs)\n        or requires_unlink(var, trs)\n    )\n\n\n@singledispatch\ndef apply_transform_var(var, trs):\n    # type: (Orange.data.Variable, List[Transform]) -> Orange.data.Variable\n    raise NotImplementedError\n\n\n@apply_transform_var.register(Orange.data.DiscreteVariable)\ndef apply_transform_discete(var, trs):\n    # type: (Orange.data.DiscreteVariable, List[Transform]) -> Orange.data.Variable\n    # pylint: disable=too-many-branches\n    name, annotations = var.name, var.attributes\n    mapping = None\n    for tr in trs:\n        if isinstance(tr, Rename):\n            name = tr.name\n        elif isinstance(tr, CategoriesMapping):\n            mapping = tr.mapping\n        elif isinstance(tr, Annotate):\n            annotations = _parse_attributes(tr.annotations)\n\n    source_values = var.values\n    if mapping is not None:\n        dest_values = list(unique(cj for ci, cj in mapping if cj is not None))\n    else:\n        dest_values = var.values\n\n    def positions(values):\n        rval = {c: i for i, c in enumerate(values)}\n        assert len(rval) == len(values)\n        return rval\n    source_codes = positions(source_values)\n    dest_codes = positions(dest_values)\n    if mapping is not None:\n        # construct a lookup table\n        lookup = np.full(len(source_values), np.nan, dtype=float)\n        for ci, cj in mapping:\n            if ci is not None and cj is not None:\n                i, j = source_codes[ci], dest_codes[cj]\n                lookup[i] = j\n        lookup = Lookup(var, lookup)\n    else:\n        lookup = Identity(var)\n    variable = Orange.data.DiscreteVariable(\n        name, values=dest_values, compute_value=lookup\n    )\n    variable.attributes.update(annotations)\n    return variable\n\n\n@apply_transform_var.register(Orange.data.ContinuousVariable)\ndef apply_transform_continuous(var, trs):\n    # type: (Orange.data.ContinuousVariable, List[Transform]) -> Orange.data.Variable\n    name, annotations = var.name, var.attributes\n    for tr in trs:\n        if isinstance(tr, Rename):\n            name = tr.name\n        elif isinstance(tr, Annotate):\n            annotations = _parse_attributes(tr.annotations)\n    variable = Orange.data.ContinuousVariable(\n        name=name, compute_value=Identity(var)\n    )\n    variable.attributes.update(annotations)\n    return variable\n\n\n@apply_transform_var.register(Orange.data.TimeVariable)\ndef apply_transform_time(var, trs):\n    # type: (Orange.data.TimeVariable, List[Transform]) -> Orange.data.Variable\n    name, annotations = var.name, var.attributes\n    for tr in trs:\n        if isinstance(tr, Rename):\n            name = tr.name\n        elif isinstance(tr, Annotate):\n            annotations = _parse_attributes(tr.annotations)\n    variable = Orange.data.TimeVariable(\n        name=name, have_date=var.have_date, have_time=var.have_time,\n        compute_value=Identity(var)\n    )\n    variable.attributes.update(annotations)\n    return variable\n\n\n@apply_transform_var.register(Orange.data.StringVariable)\ndef apply_transform_string(var, trs):\n    # type: (Orange.data.StringVariable, List[Transform]) -> Orange.data.Variable\n    name, annotations = var.name, var.attributes\n    out_type = Orange.data.StringVariable\n    compute_value = Identity\n    for tr in trs:\n        if isinstance(tr, Rename):\n            name = tr.name\n        elif isinstance(tr, Annotate):\n            annotations = _parse_attributes(tr.annotations)\n        elif isinstance(tr, StrpTime):\n            out_type = partial(\n                Orange.data.TimeVariable, have_date=tr.have_date, have_time=tr.have_time\n            )\n            compute_value = partial(ReparseTimeTransform, tr=tr)\n    variable = out_type(name=name, compute_value=compute_value(var))\n    variable.attributes.update(annotations)\n    return variable\n\n\ndef make_dict_mapper(\n        mapping: Mapping, dtype: Optional[DType] = None\n) -> Callable:\n    \"\"\"\n    Wrap a `mapping` into a callable ufunc-like function with\n    `out`, `dtype`, `where`, ... parameters. If `dtype` is passed to\n    `make_dict_mapper` it is used as a the default return dtype,\n    otherwise the default dtype is `object`.\n    \"\"\"\n    return frompyfunc(mapping.__getitem__, 1, 1, dtype)\n\n\nas_string = np.frompyfunc(str, 1, 1)\nparse_float = ftry(float, ValueError, float(\"nan\"))\n\n_parse_float = np.frompyfunc(parse_float, 1, 1)\n\n\ndef as_float_or_nan(\n        arr: ndarray, out: Optional[ndarray] = None, where: Optional[ndarray] = True,\n        dtype=None, **kwargs\n) -> ndarray:\n    \"\"\"\n    Convert elements of the input array using builtin `float`, fill elements\n    where conversion failed with NaN.\n    \"\"\"\n    if out is None:\n        out = np.full(arr.shape, np.nan, float if dtype is None else dtype)\n    if np.issubdtype(arr.dtype, np.inexact) or \\\n            np.issubdtype(arr.dtype, np.integer):\n        np.copyto(out, arr, casting=\"unsafe\", where=where)\n        return out\n    return _parse_float(arr, out, where=where, casting=\"unsafe\", **kwargs)\n\n\ndef copy_attributes(dst: V, src: Orange.data.Variable) -> V:\n    # copy `attributes` and `sparse` members from src to dst\n    dst.attributes = dict(src.attributes)\n    dst.sparse = src.sparse\n    return dst\n\n\n# Building (and applying) concrete type transformations on Table columns\n\n@singledispatch\ndef apply_reinterpret(var, tr, data):\n    # type: (Orange.data.Variable, ReinterpretTransform, MArray) -> Orange.data.Variable\n    \"\"\"\n    Apply a re-interpret transform to an `Orange.data.Table`'s column\n    \"\"\"\n    raise NotImplementedError\n\n\n@apply_reinterpret.register(Orange.data.DiscreteVariable)\ndef apply_reinterpret_d(var, tr, data):\n    # type: (Orange.data.DiscreteVariable, ReinterpretTransform, ndarray) -> Orange.data.Variable\n    if isinstance(tr, AsCategorical):\n        return var\n    elif isinstance(tr, (AsString, AsTime)):\n        # TimeVar will be interpreted by StrpTime later\n        f = Lookup(var, np.array(var.values, dtype=object), unknown=\"\")\n        rvar = Orange.data.StringVariable(name=var.name, compute_value=f)\n    elif isinstance(tr, AsContinuous):\n        f = Lookup(var, np.array(list(map(parse_float, var.values))),\n                   unknown=np.nan)\n        rvar = Orange.data.ContinuousVariable(\n            name=var.name, compute_value=f, sparse=var.sparse\n        )\n    else:\n        assert False\n    return copy_attributes(rvar, var)\n\n\n@apply_reinterpret.register(Orange.data.ContinuousVariable)\ndef apply_reinterpret_c(var, tr, data: MArray):\n    if isinstance(tr, AsCategorical):\n        # This is ill-defined and should not result in a 'compute_value'\n        # (post-hoc expunge from the domain once translated?)\n        values, index = categorize_unique(data)\n        coldata = index.astype(float)\n        coldata[index.mask] = np.nan\n        tr = LookupMappingTransform(\n            var, {v: i for i, v in enumerate(values)}, dtype=np.float64, unknown=np.nan\n        )\n        values = tuple(as_string(values))\n        rvar = Orange.data.DiscreteVariable(\n            name=var.name, values=values, compute_value=tr\n        )\n    elif isinstance(tr, AsContinuous):\n        return var\n    elif isinstance(tr, AsString):\n        # TimeVar will be interpreted by StrpTime later\n        tstr = ToStringTransform(var)\n        rvar = Orange.data.StringVariable(name=var.name, compute_value=tstr)\n    elif isinstance(tr, AsTime):\n        rvar = Orange.data.TimeVariable(name=var.name, compute_value=Identity(var))\n    else:\n        assert False\n    return copy_attributes(rvar, var)\n\n\n@apply_reinterpret.register(Orange.data.StringVariable)\ndef apply_reinterpret_s(var: Orange.data.StringVariable, tr, data: MArray):\n    if isinstance(tr, AsCategorical):\n        # This is ill-defined and should not result in a 'compute_value'\n        # (post-hoc expunge from the domain once translated?)\n        _, values = categorical_from_vector(data)\n        mapping = {v: float(i) for i, v in enumerate(values)}\n        tr = LookupMappingTransform(var, mapping)\n        rvar = Orange.data.DiscreteVariable(\n            name=var.name, values=values, compute_value=tr\n        )\n    elif isinstance(tr, AsContinuous):\n        rvar = Orange.data.ContinuousVariable(\n            var.name, compute_value=ToContinuousTransform(var)\n        )\n    elif isinstance(tr, (AsString, AsTime)):\n        # TimeVar will be interpreted by StrpTime later\n        return var\n    else:\n        assert False\n    return copy_attributes(rvar, var)\n\n\n@apply_reinterpret.register(Orange.data.TimeVariable)\ndef apply_reinterpret_t(var: Orange.data.TimeVariable, tr, data):\n    if isinstance(tr, AsCategorical):\n        values, _ = categorize_unique(data)\n        or_values = values.astype(float) / 1e6\n        mapping = {v: i for i, v in enumerate(or_values)}\n        tr = LookupMappingTransform(var, mapping)\n        values = tuple(as_string(values))\n        rvar = Orange.data.DiscreteVariable(\n            name=var.name, values=values, compute_value=tr\n        )\n    elif isinstance(tr, AsContinuous):\n        rvar = Orange.data.TimeVariable(\n            name=var.name, compute_value=Identity(var)\n        )\n    elif isinstance(tr, AsString):\n        rvar = Orange.data.StringVariable(\n            name=var.name, compute_value=ToStringTransform(var)\n        )\n    elif isinstance(tr, AsTime):\n        return var\n    else:\n        assert False\n    return copy_attributes(rvar, var)\n\n\ndef orange_isna(variable: Orange.data.Variable, data: ndarray) -> ndarray:\n    \"\"\"\n    Return a bool mask masking N/A elements in `data` for the `variable`.\n    \"\"\"\n    if variable.is_primitive():\n        return np.isnan(data)\n    else:\n        return data == variable.Unknown\n\n\nclass ToStringTransform(Transformation):\n    \"\"\"\n    Transform a variable to string.\n    \"\"\"\n    def transform(self, c):\n        if self.variable.is_string:\n            return c\n        elif self.variable.is_discrete or self.variable.is_time:\n            r = column_str_repr(self.variable, c)\n        elif self.variable.is_continuous:\n            r = as_string(c)\n        mask = orange_isna(self.variable, c)\n        return np.where(mask, \"\", r)\n\n\nclass ToContinuousTransform(Transformation):\n    def transform(self, c):\n        if self.variable.is_time:\n            return c\n        elif self.variable.is_continuous:\n            return c\n        elif self.variable.is_discrete:\n            lookup = Lookup(\n                self.variable, as_float_or_nan(self.variable.values),\n                unknown=np.nan\n            )\n            return lookup.transform(c)\n        elif self.variable.is_string:\n            return as_float_or_nan(c)\n        else:\n            raise TypeError\n\n\ndef datetime_to_epoch(dti: pd.DatetimeIndex, only_time) -> np.ndarray:\n    \"\"\"Convert datetime to epoch\"\"\"\n    delta = dti - (dti.normalize() if only_time else pd.Timestamp(\"1970-01-01\"))\n    return (delta / pd.Timedelta(\"1s\")).values\n\n\nclass ReparseTimeTransform(Transformation):\n    \"\"\"\n    Re-parse the column's string repr as datetime.\n    \"\"\"\n    def __init__(self, variable, tr):\n        super().__init__(variable)\n        self.tr = tr\n\n    def transform(self, c):\n        # if self.formats is none guess format option is selected\n        formats = self.tr.formats if self.tr.formats is not None else [None]\n        for f in formats:\n            d = pd.to_datetime(c, errors=\"coerce\", format=f)\n            if pd.notnull(d).any():\n                return datetime_to_epoch(d, only_time=not self.tr.have_date)\n        return np.nan\n\n\n# Alias for back compatibility (unpickling transforms)\nLookupMappingTransform = MappingTransform\n\n\n@singledispatch\ndef column_str_repr(var: Orange.data.Variable, coldata: ndarray) -> ndarray:\n    \"\"\"Return a array of str representations of coldata for the `variable.\"\"\"\n    _f = np.frompyfunc(var.repr_val, 1, 1)\n    return _f(coldata)\n\n\n@column_str_repr.register(Orange.data.DiscreteVariable)\ndef column_str_repr_discrete(\n        var: Orange.data.DiscreteVariable, coldata: ndarray\n) -> ndarray:\n    values = np.array(var.values, dtype=object)\n    lookup = Lookup(var, values, \"?\")\n    return lookup.transform(coldata)\n\n\n@column_str_repr.register(Orange.data.StringVariable)\ndef column_str_repr_string(\n        var: Orange.data.StringVariable, coldata: ndarray\n) -> ndarray:\n    return np.where(coldata == var.Unknown, \"?\", coldata)\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWEditDomain).run(Orange.data.Table(\"iris\"))\n"}
A user will ask you to solve a task. You should first draft your thinking process (inner monologue). Then, generate the solution.

Your response format must follow the template below:
<think>
Your thoughts or/and draft, like working through an exercise on scratch paper. Be as casual and as long as you want until you are confident to generate a correct solution.
</think>
<solution>
Final solution presented to the user.
</solution>
system
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** Error When Converting Timezone-Aware Datetime to Epoch

**Description:**
When attempting to convert datetime objects that include timezone information to epoch values, a `TypeError` is raised. This issue arises because the subtraction operation fails when the datetime index has a timezone, but the initial timestamp does not.

**Example Buggy Code:**
```python
import pandas as pd
import numpy as np

def datetime_to_epoch(dti: pd.DatetimeIndex, only_time) -> np.ndarray:
    delta = dti - pd.Timestamp("1970-01-01")
    return (delta / pd.Timedelta("1s")).values

dti = pd.DatetimeIndex(["2021-02-08 01:02:03+01:00", "2021-02-07 01:02:03+01:00"])
epoch = datetime_to_epoch(dti, only_time=False)
```

**Expected Behavior:**
The function should correctly compute the epoch times for timezone-aware datetime objects without raising any errors.

**Actual Behavior:**
A `TypeError` is raised with the message:
```
TypeError: Timestamp subtraction must have the same timezones or no timezones
```
This prevents the successful conversion of timezone-aware datetime objects to epoch values.
[/ISSUE]
--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.

--- BEGIN FILE ---
```
### Orange/widgets/data/oweditdomain.py
"""
Edit Domain
-----------

A widget for manual editing of a domain's attributes.

"""
import warnings
from xml.sax.saxutils import escape
from itertools import zip_longest, repeat, chain
from contextlib import contextmanager
from collections import namedtuple, Counter
from functools import singledispatch, partial
from typing import (
    Tuple, List, Any, Optional, Union, Dict, Sequence, Iterable, NamedTuple,
    FrozenSet, Type, Callable, TypeVar, Mapping, Hashable, cast
)

import numpy as np
import pandas as pd
from AnyQt.QtWidgets import (
    QWidget, QListView, QTreeView, QVBoxLayout, QHBoxLayout, QFormLayout,
    QLineEdit, QAction, QActionGroup, QGroupBox,
    QStyledItemDelegate, QStyleOptionViewItem, QStyle, QSizePolicy,
    QDialogButtonBox, QPushButton, QCheckBox, QComboBox, QStackedLayout,
    QDialog, QRadioButton, QGridLayout, QLabel, QSpinBox, QDoubleSpinBox,
    QAbstractItemView, QMenu, QToolTip
)
from AnyQt.QtGui import (
    QStandardItemModel, QStandardItem, QKeySequence, QIcon, QBrush, QPalette,
    QHelpEvent
)
from AnyQt.QtCore import (
    Qt, QSize, QModelIndex, QAbstractItemModel, QPersistentModelIndex, QRect,
    QPoint,
)
from AnyQt.QtCore import pyqtSignal as Signal, pyqtSlot as Slot

import Orange.data

from Orange.preprocess.transformation import (
    Transformation, Identity, Lookup, MappingTransform
)
from Orange.misc.collections import DictMissingConst
from Orange.util import frompyfunc
from Orange.widgets import widget, gui, settings
from Orange.widgets.utils import itemmodels, ftry
from Orange.widgets.utils.buttons import FixedSizeButton
from Orange.widgets.utils.itemmodels import signal_blocking
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.widget import Input, Output

ndarray = np.ndarray  # pylint: disable=invalid-name
MArray = np.ma.MaskedArray
DType = Union[np.dtype, type]

V = TypeVar("V", bound=Orange.data.Variable)  # pylint: disable=invalid-name
H = TypeVar("H", bound=Hashable)  # pylint: disable=invalid-name


def unique(sequence: Iterable[H]) -> Iterable[H]:
    """
    Return unique elements in `sequence`, preserving their (first seen) order.
    """
    # depending on Python >= 3.6 'ordered' dict implementation detail.
    return iter(dict.fromkeys(sequence))


class _DataType:
    def __eq__(self, other):
        """Equal if `other` has the same type and all elements compare equal."""
        if type(self) is not type(other):
            return False
        return super().__eq__(other)

    def __ne__(self, other):
        return not self == other

    def __hash__(self):
        return hash((type(self), super().__hash__()))

    def name_type(self):
        """
        Returns a tuple with name and type of the variable.
        It is used since it is forbidden to use names of variables in settings.
        """
        type_number = {
            "Categorical": 0,
            "Real": 2,
            "Time": 3,
            "String": 4
        }
        return self.name, type_number[type(self).__name__]


#: An ordered sequence of key, value pairs (variable annotations)
AnnotationsType = Tuple[Tuple[str, str], ...]


# Define abstract representation of the variable types edited

class Categorical(
    _DataType, NamedTuple("Categorical", [
        ("name", str),
        ("categories", Tuple[str, ...]),
        ("annotations", AnnotationsType),
        ("linked", bool)
    ])): pass


class Real(
    _DataType, NamedTuple("Real", [
        ("name", str),
        # a precision (int, and a format specifier('f', 'g', or '')
        ("format", Tuple[int, str]),
        ("annotations", AnnotationsType),
        ("linked", bool)
    ])): pass


class String(
    _DataType, NamedTuple("String", [
        ("name", str),
        ("annotations", AnnotationsType),
        ("linked", bool)
    ])): pass


class Time(
    _DataType, NamedTuple("Time", [
        ("name", str),
        ("annotations", AnnotationsType),
        ("linked", bool)
    ])): pass


Variable = Union[Categorical, Real, Time, String]
VariableTypes = (Categorical, Real, Time, String)


# Define variable transformations.

class Rename(_DataType, namedtuple("Rename", ["name"])):
    """
    Rename a variable.

    Parameters
    ----------
    name : str
        The new name
    """
    def __call__(self, var):
        # type: (Variable) -> Variable
        return var._replace(name=self.name)


#: Mapping of categories.
#: A list of pairs with the first element the original value and the second
#: element the new value. If the first element is None then a category level
#: is added. If the second element is None than the corresponding first level
#: is dropped. If there are duplicated elements on the right the corresponding
#: categories on the left are merged.
#: The mapped order is defined by the translated elements after None removal
#: and merges (the first occurrence of a multiplied elements defines its
#: position):
CategoriesMappingType = List[Tuple[Optional[str], Optional[str]]]


class CategoriesMapping(_DataType, namedtuple("CategoriesMapping", ["mapping"])):
    """
    Change categories of a categorical variable.

    Parameters
    ----------
    mapping : CategoriesMappingType
    """
    def __call__(self, var):
        # type: (Categorical) -> Categorical
        cat = tuple(unique(cj for _, cj in self.mapping if cj is not None))
        return var._replace(categories=cat)


class Annotate(_DataType, namedtuple("Annotate", ["annotations"])):
    """
    Replace variable annotations.
    """
    def __call__(self, var):
        return var._replace(annotations=self.annotations)


class Unlink(_DataType, namedtuple("Unlink", [])):
    """Unlink variable from its source, that is, remove compute_value"""


class StrpTime(_DataType, namedtuple("StrpTime", ["label", "formats", "have_date", "have_time"])):
    """Use format on variable interpreted as time"""


Transform = Union[Rename, CategoriesMapping, Annotate, Unlink, StrpTime]
TransformTypes = (Rename, CategoriesMapping, Annotate, Unlink, StrpTime)

CategoricalTransformTypes = (CategoriesMapping, Unlink)


# Reinterpret vector transformations.
class CategoricalVector(
    _DataType, NamedTuple("CategoricalVector", [
        ("vtype", Categorical),
        ("data", Callable[[], MArray]),
    ])): ...


class RealVector(
    _DataType, NamedTuple("RealVector", [
        ("vtype", Real),
        ("data", Callable[[], MArray]),
    ])): ...


class StringVector(
    _DataType, NamedTuple("StringVector", [
        ("vtype", String),
        ("data", Callable[[], MArray]),
    ])): ...


class TimeVector(
    _DataType, NamedTuple("TimeVector", [
        ("vtype", Time),
        ("data", Callable[[], MArray]),
    ])): ...


DataVector = Union[CategoricalVector, RealVector, StringVector, TimeVector]
DataVectorTypes = (CategoricalVector, RealVector, StringVector, TimeVector)


class AsString(_DataType, NamedTuple("AsString", [])):
    """Reinterpret a data vector as a string."""
    def __call__(self, vector: DataVector) -> StringVector:
        var, _ = vector
        if isinstance(var, String):
            return vector
        return StringVector(
            String(var.name, var.annotations, False),
            lambda: as_string(vector.data()),
        )


class AsContinuous(_DataType, NamedTuple("AsContinuous", [])):
    """
    Reinterpret as a continuous variable (values that do not parse as
    float are NaN).
    """
    def __call__(self, vector: DataVector) -> RealVector:
        var, _ = vector
        if isinstance(var, Real):
            return vector
        elif isinstance(var, Categorical):
            def data() -> MArray:
                d = vector.data()
                a = categorical_to_string_vector(d, var.values)
                return MArray(as_float_or_nan(a, where=a.mask), mask=a.mask)
            return RealVector(
                Real(var.name, (6, 'g'), var.annotations, var.linked), data
            )
        elif isinstance(var, Time):
            return RealVector(
                Real(var.name, (6, 'g'), var.annotations, var.linked),
                lambda: vector.data().astype(float)
            )
        elif isinstance(var, String):
            def data():
                s = vector.data()
                return MArray(as_float_or_nan(s, where=s.mask), mask=s.mask)
            return RealVector(
                Real(var.name, (6, "g"), var.annotations, var.linked), data
            )
        raise AssertionError


class AsCategorical(_DataType, namedtuple("AsCategorical", [])):
    """Reinterpret as a categorical variable"""
    def __call__(self, vector: DataVector) -> CategoricalVector:
        # this is the main complication in type transformation since we need
        # the data and not just the variable description
        var, _ = vector
        if isinstance(var, Categorical):
            return vector
        if isinstance(var, (Real, Time, String)):
            data, values = categorical_from_vector(vector.data())
            return CategoricalVector(
                Categorical(var.name, values, var.annotations, var.linked),
                lambda: data
            )
        raise AssertionError


class AsTime(_DataType, namedtuple("AsTime", [])):
    """Reinterpret as a datetime vector"""
    def __call__(self, vector: DataVector) -> TimeVector:
        var, _ = vector
        if isinstance(var, Time):
            return vector
        elif isinstance(var, Real):
            return TimeVector(
                Time(var.name, var.annotations, var.linked),
                lambda: vector.data().astype("M8[us]")
            )
        elif isinstance(var, Categorical):
            def data():
                d = vector.data()
                s = categorical_to_string_vector(d, var.values)
                dt = pd.to_datetime(s, errors="coerce").values.astype("M8[us]")
                return MArray(dt, mask=d.mask)
            return TimeVector(
                Time(var.name, var.annotations, var.linked), data
            )
        elif isinstance(var, String):
            def data():
                s = vector.data()
                dt = pd.to_datetime(s, errors="coerce").values.astype("M8[us]")
                return MArray(dt, mask=s.mask)
            return TimeVector(
                Time(var.name, var.annotations, var.linked), data
            )
        raise AssertionError


ReinterpretTransform = Union[AsCategorical, AsContinuous, AsTime, AsString]
ReinterpretTransformTypes = (AsCategorical, AsContinuous, AsTime, AsString)


def deconstruct(obj):
    # type: (tuple) -> Tuple[str, Tuple[Any, ...]]
    """
    Deconstruct a tuple subclass to its class name and its contents.

    Parameters
    ----------
    obj : A tuple

    Returns
    -------
    value: Tuple[str, Tuple[Any, ...]]
    """
    cname = type(obj).__name__
    args = tuple(obj)
    return cname, args


def reconstruct(tname, args):
    # type: (str, Tuple[Any, ...]) -> Tuple[Any, ...]
    """
    Reconstruct a tuple subclass (inverse of deconstruct).

    Parameters
    ----------
    tname : str
        Type name
    args : Tuple[Any, ...]

    Returns
    -------
    rval: Tuple[Any, ...]
    """
    try:
        constructor = globals()[tname]
    except KeyError:
        raise NameError(tname)
    return constructor(*args)


def formatter_for_dtype(dtype: np.dtype) -> Callable[[Any], str]:
    if dtype.metadata is None:
        return str
    else:
        return dtype.metadata.get("__formatter", str)  # metadata abuse


def masked_unique(data: MArray) -> Tuple[MArray, ndarray]:
    if not np.any(data.mask):
        return np.ma.unique(data, return_inverse=True)
    elif data.dtype.kind == "O":
        # np.ma.unique does not work for object arrays
        # (no ma.minimum_fill_value for object arrays)
        # maybe sorted(set(data.data[...]))
        unq = np.unique(data.data[~data.mask])
        mapper = make_dict_mapper(
            DictMissingConst(len(unq), ((v, i) for i, v in enumerate(unq)))
        )
        index = mapper(data.data)
        unq = np.array(unq.tolist() + [data.fill_value], dtype=data.dtype)
        unq_mask = [False] * unq.size
        unq_mask[-1] = True
        unq = MArray(unq, mask=unq_mask)
        return unq, index
    else:
        unq, index = np.ma.unique(data, return_inverse=True)
        assert not np.any(unq.mask[:-1]), \
            "masked value if present must be in last position"
        return unq, index


def categorical_from_vector(data: MArray) -> Tuple[MArray, Tuple[str, ...]]:
    formatter = formatter_for_dtype(data.dtype)
    unq, index = categorize_unique(data)
    if formatter is not str:
        # str(np.array([0], "M8[s]")[0]) is different then
        # str(np.array([0], "M8[s]").astype(object)[0]) which is what
        # as_string is doing
        names = tuple(map(formatter, unq.astype(object)))
    else:
        names = tuple(as_string(unq))
    data = MArray(
        index, mask=data.mask,
        dtype=np.dtype(int, metadata={
            "__formater": lambda i: names[i] if 0 <= i < unq.size else "?"
        })
    )
    return data, names


def categorize_unique(data: MArray) -> Tuple[ndarray, MArray]:
    unq, index = masked_unique(data)
    if np.any(unq.mask):
        unq = unq[:-1]
        assert not np.any(unq.mask), "masked value if present must be last"
    unq = unq.data
    index[data.mask] = -1
    index = MArray(index, mask=data.mask)
    return unq, index


def categorical_to_string_vector(data: MArray, values: Tuple[str, ...]) -> MArray:
    lookup = np.asarray(values, object)
    out = np.full(data.shape, "", dtype=object)
    mask_ = ~data.mask
    out[mask_] = lookup[data.data[mask_]]
    return MArray(out, mask=data.mask, fill_value="")


# Item models


class DictItemsModel(QStandardItemModel):
    """A Qt Item Model class displaying the contents of a python
    dictionary.

    """
    # Implement a proper model with in-place editing.
    # (Maybe it should be a TableModel with 2 columns)
    def __init__(self, parent=None, a_dict=None):
        super().__init__(parent)
        self._dict = {}
        self.setHorizontalHeaderLabels(["Key", "Value"])
        if a_dict is not None:
            self.set_dict(a_dict)

    def set_dict(self, a_dict):
        # type: (Dict[str, str]) -> None
        self._dict = a_dict
        self.setRowCount(0)
        for key, value in sorted(a_dict.items()):
            key_item = QStandardItem(key)
            value_item = QStandardItem(value)
            key_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
            value_item.setFlags(value_item.flags() | Qt.ItemIsEditable)
            self.appendRow([key_item, value_item])

    def get_dict(self):
        # type: () -> Dict[str, str]
        rval = {}
        for row in range(self.rowCount()):
            key_item = self.item(row, 0)
            value_item = self.item(row, 1)
            rval[key_item.text()] = value_item.text()
        return rval


class VariableEditor(QWidget):
    """
    An editor widget for a variable.

    Can edit the variable name, and its attributes dictionary.
    """
    variable_changed = Signal()

    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)
        self.var = None  # type: Optional[Variable]

        layout = QVBoxLayout()
        self.setLayout(layout)

        self.form = form = QFormLayout(
            fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow,
            objectName="editor-form-layout"
        )
        layout.addLayout(self.form)

        self.name_edit = QLineEdit(objectName="name-editor")
        self.name_edit.editingFinished.connect(
            lambda: self.name_edit.isModified() and self.on_name_changed()
        )
        form.addRow("Name:", self.name_edit)

        self.unlink_var_cb = QCheckBox(
            "Unlink variable from its source variable", self,
            toolTip="Make Orange forget that the variable is derived from "
                    "another.\n"
                    "Use this for instance when you want to consider variables "
                    "with the same name but from different sources as the same "
                    "variable."
        )
        self.unlink_var_cb.toggled.connect(self._set_unlink)
        form.addRow("", self.unlink_var_cb)

        vlayout = QVBoxLayout(spacing=1)
        self.labels_edit = view = QTreeView(
            objectName="annotation-pairs-edit",
            rootIsDecorated=False,
            editTriggers=QTreeView.DoubleClicked | QTreeView.EditKeyPressed,
        )
        self.labels_model = model = DictItemsModel()
        view.setModel(model)

        view.selectionModel().selectionChanged.connect(
            self.on_label_selection_changed)

        agrp = QActionGroup(view, objectName="annotate-action-group")
        action_add = QAction(
            "+", self, objectName="action-add-label",
            toolTip="Add a new label.",
            shortcut=QKeySequence(QKeySequence.New),
            shortcutContext=Qt.WidgetShortcut
        )
        action_delete = QAction(
            "\N{MINUS SIGN}", self, objectName="action-delete-label",
            toolTip="Remove selected label.",
            shortcut=QKeySequence(QKeySequence.Delete),
            shortcutContext=Qt.WidgetShortcut
        )
        agrp.addAction(action_add)
        agrp.addAction(action_delete)
        view.addActions([action_add, action_delete])

        def add_label():
            row = [QStandardItem(), QStandardItem()]
            model.appendRow(row)
            idx = model.index(model.rowCount() - 1, 0)
            view.setCurrentIndex(idx)
            view.edit(idx)

        def remove_label():
            rows = view.selectionModel().selectedRows(0)
            if rows:
                assert len(rows) == 1
                idx = rows[0].row()
                model.removeRow(idx)

        action_add.triggered.connect(add_label)
        action_delete.triggered.connect(remove_label)
        agrp.setEnabled(False)

        self.add_label_action = action_add
        self.remove_label_action = action_delete

        # Necessary signals to know when the labels change
        model.dataChanged.connect(self.on_labels_changed)
        model.rowsInserted.connect(self.on_labels_changed)
        model.rowsRemoved.connect(self.on_labels_changed)

        vlayout.addWidget(self.labels_edit)
        hlayout = QHBoxLayout()
        hlayout.setContentsMargins(0, 0, 0, 0)
        button = FixedSizeButton(
            self, defaultAction=self.add_label_action,
            accessibleName="Add",
        )
        hlayout.addWidget(button)

        button = FixedSizeButton(
            self, defaultAction=self.remove_label_action,
            accessibleName="Remove",
        )

        hlayout.addWidget(button)
        hlayout.addStretch(10)
        vlayout.addLayout(hlayout)
        form.addRow("Labels:", vlayout)

    def set_data(self, var, transform=()):
        # type: (Optional[Variable], Sequence[Transform]) -> None
        """
        Set the variable to edit.
        """
        self.clear()
        self.var = var
        if var is not None:
            name = var.name
            annotations = var.annotations
            unlink = False
            for tr in transform:
                if isinstance(tr, Rename):
                    name = tr.name
                elif isinstance(tr, Annotate):
                    annotations = tr.annotations
                elif isinstance(tr, Unlink):
                    unlink = True
            self.name_edit.setText(name)
            self.labels_model.set_dict(dict(annotations))
            self.add_label_action.actionGroup().setEnabled(True)
            self.unlink_var_cb.setChecked(unlink)
        else:
            self.add_label_action.actionGroup().setEnabled(False)

        self.unlink_var_cb.setDisabled(var is None or not var.linked)

    def get_data(self):
        """Retrieve the modified variable.
        """
        if self.var is None:
            return None, []
        name = self.name_edit.text().strip()
        labels = tuple(sorted(self.labels_model.get_dict().items()))
        tr = []
        if self.var.name != name:
            tr.append(Rename(name))
        if self.var.annotations != labels:
            tr.append(Annotate(labels))
        if self.var.linked and self.unlink_var_cb.isChecked():
            tr.append(Unlink())
        return self.var, tr

    def clear(self):
        """Clear the editor state.
        """
        self.var = None
        self.name_edit.setText("")
        self.labels_model.setRowCount(0)
        self.unlink_var_cb.setChecked(False)

    @Slot()
    def on_name_changed(self):
        self.variable_changed.emit()

    @Slot()
    def on_labels_changed(self):
        self.variable_changed.emit()

    @Slot()
    def on_label_selection_changed(self):
        selected = self.labels_edit.selectionModel().selectedRows()
        self.remove_label_action.setEnabled(bool(len(selected)))

    def _set_unlink(self, unlink):
        self.unlink_var_cb.setChecked(unlink)
        self.variable_changed.emit()


class GroupItemsDialog(QDialog):
    """
    A dialog for group less frequent values.
    """
    DEFAULT_LABEL = "other"

    def __init__(
            self, variable: Categorical,
            data: Union[np.ndarray, List, MArray],
            selected_attributes: List[str], dialog_settings: Dict[str, Any],
            parent: QWidget = None, flags: Qt.WindowFlags = Qt.Dialog, **kwargs
    ) -> None:
        super().__init__(parent, flags, **kwargs)
        self.variable = variable
        self.data = data
        self.selected_attributes = selected_attributes

        # grouping strategy
        self.selected_radio = radio1 = QRadioButton("Group selected values")
        self.frequent_abs_radio = radio2 = QRadioButton(
            "Group values with less than"
        )
        self.frequent_rel_radio = radio3 = QRadioButton(
            "Group values with less than"
        )
        self.n_values_radio = radio4 = QRadioButton(
            "Group all except"
        )

        # if selected attributes available check the first radio button,
        # otherwise disable it
        if selected_attributes:
            radio1.setChecked(True)
        else:
            radio1.setEnabled(False)
            # they are remembered by number since radio button instance is
            # new object for each dialog
            checked = dialog_settings.get("selected_radio", 0)
            [radio2, radio3, radio4][checked].setChecked(True)

        label2 = QLabel("occurrences")
        label3 = QLabel("occurrences")
        label4 = QLabel("most frequent values")

        self.frequent_abs_spin = spin2 = QSpinBox()
        max_val = len(data)
        spin2.setMinimum(1)
        spin2.setMaximum(max_val)
        spin2.setValue(dialog_settings.get("frequent_abs_spin", 10))
        spin2.setMinimumWidth(
            self.fontMetrics().horizontalAdvance("X") * (len(str(max_val)) + 1) + 20
        )
        spin2.valueChanged.connect(self._frequent_abs_spin_changed)

        self.frequent_rel_spin = spin3 = QDoubleSpinBox()
        spin3.setMinimum(0)
        spin3.setDecimals(1)
        spin3.setSingleStep(0.1)
        spin3.setMaximum(100)
        spin3.setValue(dialog_settings.get("frequent_rel_spin", 10))
        spin3.setMinimumWidth(self.fontMetrics().horizontalAdvance("X") * (2 + 1) + 20)
        spin3.setSuffix(" %")
        spin3.valueChanged.connect(self._frequent_rel_spin_changed)

        self.n_values_spin = spin4 = QSpinBox()
        spin4.setMinimum(0)
        spin4.setMaximum(len(variable.categories))
        spin4.setValue(
            dialog_settings.get(
                "n_values_spin", min(10, len(variable.categories))
            )
        )
        spin4.setMinimumWidth(
            self.fontMetrics().horizontalAdvance("X") * (len(str(max_val)) + 1) + 20
        )
        spin4.valueChanged.connect(self._n_values_spin_spin_changed)

        grid_layout = QGridLayout()
        # first row
        grid_layout.addWidget(radio1, 0, 0, 1, 2)
        # second row
        grid_layout.addWidget(radio2, 1, 0, 1, 2)
        grid_layout.addWidget(spin2, 1, 2)
        grid_layout.addWidget(label2, 1, 3)
        # third row
        grid_layout.addWidget(radio3, 2, 0, 1, 2)
        grid_layout.addWidget(spin3, 2, 2)
        grid_layout.addWidget(label3, 2, 3)
        # fourth row
        grid_layout.addWidget(radio4, 3, 0)
        grid_layout.addWidget(spin4, 3, 1)
        grid_layout.addWidget(label4, 3, 2, 1, 2)

        group_box = QGroupBox()
        group_box.setLayout(grid_layout)

        # grouped variable name
        new_name_label = QLabel("New value name: ")
        self.new_name_line_edit = n_line_edit = QLineEdit(
            dialog_settings.get("name_line_edit", self.DEFAULT_LABEL)
        )
        # it is shown gray when user removes the text and let user know that
        # word others is default one
        n_line_edit.setPlaceholderText(self.DEFAULT_LABEL)
        name_hlayout = QHBoxLayout()
        name_hlayout.addWidget(new_name_label)
        name_hlayout.addWidget(n_line_edit)

        # confirm_button = QPushButton("Apply")
        # cancel_button = QPushButton("Cancel")
        buttons = QDialogButtonBox(
            orientation=Qt.Horizontal,
            standardButtons=(QDialogButtonBox.Ok | QDialogButtonBox.Cancel),
            objectName="dialog-button-box",
        )
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)

        # join components
        self.setLayout(QVBoxLayout())
        self.layout().addWidget(group_box)
        self.layout().addLayout(name_hlayout)
        self.layout().addWidget(buttons)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

    def _frequent_abs_spin_changed(self) -> None:
        self.frequent_abs_radio.setChecked(True)

    def _n_values_spin_spin_changed(self) -> None:
        self.n_values_radio.setChecked(True)

    def _frequent_rel_spin_changed(self) -> None:
        self.frequent_rel_radio.setChecked(True)

    def get_merge_attributes(self) -> List[str]:
        """
        Returns attributes that will be merged

        Returns
        -------
        List of attributes' to be merged names
        """
        if self.selected_radio.isChecked():
            return self.selected_attributes

        if isinstance(self.data, MArray):
            non_nan = self.data[~self.data.mask]
        elif isinstance(self.data, np.ndarray):
            non_nan = self.data[~np.isnan(self.data)]
        else:  # list
            non_nan = [x for x in self.data if x is not None]

        counts = Counter(non_nan)
        if self.n_values_radio.isChecked():
            keep_values = self.n_values_spin.value()
            values = counts.most_common()[keep_values:]
            indices = [i for i, _ in values]
        elif self.frequent_abs_radio.isChecked():
            indices = [v for v, c in counts.most_common()
                       if c < self.frequent_abs_spin.value()]
        else:  # self.frequent_rel_radio.isChecked():
            n_all = sum(counts.values())
            indices = [v for v, c in counts.most_common()
                       if c / n_all * 100 < self.frequent_rel_spin.value()]

        indices = np.array(indices, dtype=int)  # indices must be ints
        return np.array(self.variable.categories)[indices].tolist()

    def get_merged_value_name(self) -> str:
        """
        Returns
        -------
        New label of merged values
        """
        return self.new_name_line_edit.text() or self.DEFAULT_LABEL

    def get_dialog_settings(self) -> Dict[str, Any]:
        """
        Returns
        -------
        Return the dictionary with vlues set by user in each of the line edits
        and selected radio button.
        """
        settings_dict = {
            "frequent_abs_spin": self.frequent_abs_spin.value(),
            "frequent_rel_spin": self.frequent_rel_spin.value(),
            "n_values_spin": self.n_values_spin.value(),
            "name_line_edit": self.new_name_line_edit.text()
        }
        checked = [
            i for i, s in enumerate(
                [self.frequent_abs_radio,
                 self.frequent_rel_radio,
                 self.n_values_radio]
            ) if s.isChecked()]
        # when checked empty radio button for selected values is selected
        # it is not stored in setting since its selection depends on users
        # selection of values in list
        if checked:
            settings_dict["selected_radio"] = checked[0]
        return settings_dict


@contextmanager
def disconnected(signal, slot, connection_type=Qt.AutoConnection):
    signal.disconnect(slot)
    try:
        yield
    finally:
        signal.connect(slot, connection_type)


#: In 'reordable' models holds the original position of the item
#: (if applicable).
SourcePosRole = Qt.UserRole
#: The original name
SourceNameRole = Qt.UserRole + 2

#: The added/dropped state (type is ItemEditState)
EditStateRole = Qt.UserRole + 1


class ItemEditState:
    NoState = 0
    Dropped = 1
    Added = 2


#: Role used to retrieve the count of 'key' values in the model.
MultiplicityRole = Qt.UserRole + 0x67


class CountedListModel(itemmodels.PyListModel):
    """
    A list model counting how many times unique `key` values appear in
    the list.

    The counts are cached and invalidated on any change to the model involving
    the changes to `keyRoles`.
    """
    #: cached counts
    __counts_cache = None  # type: Optional[Counter]

    def data(self, index, role=Qt.DisplayRole):
        # type: (QModelIndex, int) -> Any
        if role == MultiplicityRole:
            key = self.key(index)
            counts = self.__counts()
            return counts.get(key, 1)
        return super().data(index, role)

    def setData(self, index, value, role=Qt.EditRole):
        # type: (QModelIndex, Any, int)-> bool
        rval = super().setData(index, value, role)
        if role in self.keyRoles():
            self.invalidateCounts()
        return rval

    def setItemData(self, index, data):
        # type: (QModelIndex, Dict[int, Any]) -> bool
        rval = super().setItemData(index, data)
        if self.keyRoles().intersection(set(data.keys())):
            self.invalidateCounts()
        return rval

    def endInsertRows(self):
        super().endInsertRows()
        self.invalidateCounts()

    def endRemoveRows(self):
        super().endRemoveRows()
        self.invalidateCounts()

    def endResetModel(self) -> None:
        super().endResetModel()
        self.invalidateCounts()

    def invalidateCounts(self) -> None:
        """
        Invalidate the cached counts.
        """
        self.__counts_cache = None
        # emit the change for the whole model
        self.dataChanged.emit(
            self.index(0), self.index(self.rowCount() - 1), [MultiplicityRole]
        )

    def __counts(self):
        # type: () -> Counter
        if self.__counts_cache is not None:
            return self.__counts_cache
        counts = Counter()
        for index in map(self.index, range(self.rowCount())):
            key = self.key(index)
            try:
                counts[key] += 1
            except TypeError:  # pragma: no cover
                warnings.warn(f"key value '{key}' is not hashable")
        self.__counts_cache = counts
        return self.__counts_cache

    def key(self, index):
        # type: (QModelIndex) -> Any
        """
        Return the 'key' value that is to be counted.

        The default implementation returns Qt.EditRole value for the index

        Parameters
        ----------
        index : QModelIndex
            The model index.

        Returns
        -------
        key : Any
        """
        return self.data(index, Qt.EditRole)

    def keyRoles(self):
        # type: () -> FrozenSet[int]
        """
        Return a set of item roles on which `key` depends.

        The counts are invalidated and recomputed whenever any of the roles in
        this set changes.

        By default the only role returned is Qt.EditRole
        """
        return frozenset({Qt.EditRole})


class CountedStateModel(CountedListModel):
    """
    Count by EditRole (name) and EditStateRole (ItemEditState)
    """
    # The purpose is to count the items with target name only for
    # ItemEditState.NoRole, i.e. excluding added/dropped values.
    #
    def key(self, index):  # type: (QModelIndex) -> Tuple[Any, Any]
        # reimplemented
        return self.data(index, Qt.EditRole), self.data(index, EditStateRole)

    def keyRoles(self):  # type: () -> FrozenSet[int]
        # reimplemented
        return frozenset({Qt.EditRole, EditStateRole})


def mapRectTo(widget: QWidget, parent: QWidget, rect: QRect) -> QRect:  # pylint: disable=redefined-outer-name
    return QRect(widget.mapTo(parent, rect.topLeft()), rect.size())


def mapRectToGlobal(widget: QWidget, rect: QRect) -> QRect:  # pylint: disable=redefined-outer-name
    return QRect(widget.mapToGlobal(rect.topLeft()), rect.size())


class CategoriesEditDelegate(QStyledItemDelegate):
    """
    Display delegate for editing categories.

    Displayed items are styled for add, remove, merge and rename operations.
    """
    def initStyleOption(self, option, index):
        # type: (QStyleOptionViewItem, QModelIndex)-> None
        super().initStyleOption(option, index)
        text = str(index.data(Qt.EditRole))
        sourcename = str(index.data(SourceNameRole))
        editstate = index.data(EditStateRole)
        counts = index.data(MultiplicityRole)
        if not isinstance(counts, int):
            counts = 1
        suffix = None
        if editstate == ItemEditState.Dropped:
            option.state &= ~QStyle.State_Enabled
            option.font.setStrikeOut(True)
            text = sourcename
            suffix = "(dropped)"
        elif editstate == ItemEditState.Added:
            suffix = "(added)"
        else:
            text = f"{sourcename} \N{RIGHTWARDS ARROW} {text}"
            if counts > 1:
                suffix = "(merged)"
        if suffix is not None:
            text = text + " " + suffix
        option.text = text

    class CatEditComboBox(QComboBox):
        prows: List[QPersistentModelIndex]

    def createEditor(
            self, parent: QWidget, option: 'QStyleOptionViewItem',
            index: QModelIndex
    ) -> QWidget:
        view = option.widget
        assert isinstance(view, QAbstractItemView)
        selmodel = view.selectionModel()
        rows = selmodel.selectedRows(0)
        if len(rows) < 2:
            return super().createEditor(parent, option, index)
        # edit multiple selection
        cb = CategoriesEditDelegate.CatEditComboBox(
            editable=True, insertPolicy=QComboBox.InsertAtBottom)
        cb.setParent(view, Qt.Popup)
        cb.addItems(
            list(unique(str(row.data(Qt.EditRole)) for row in rows)))
        prows = [QPersistentModelIndex(row) for row in rows]
        cb.prows = prows
        return cb

    def updateEditorGeometry(
            self, editor: QWidget, option: 'QStyleOptionViewItem',
            index: QModelIndex
    ) -> None:
        if isinstance(editor, CategoriesEditDelegate.CatEditComboBox):
            view = cast(QAbstractItemView, option.widget)
            view.scrollTo(index)
            vport = view.viewport()
            vrect = view.visualRect(index)
            vrect = mapRectTo(vport, view, vrect)
            vrect = vrect.intersected(vport.geometry())
            vrect = mapRectToGlobal(vport, vrect)
            size = editor.sizeHint().expandedTo(vrect.size())
            editor.resize(size)
            editor.move(vrect.topLeft())
        else:
            super().updateEditorGeometry(editor, option, index)

    def setModelData(
            self, editor: QWidget, model: QAbstractItemModel, index: QModelIndex
    ) -> None:
        if isinstance(editor, CategoriesEditDelegate.CatEditComboBox):
            text = editor.currentText()
            with signal_blocking(model):
                for prow in editor.prows:
                    if prow.isValid():
                        model.setData(QModelIndex(prow), text, Qt.EditRole)
            # this could be better
            model.dataChanged.emit(
                model.index(0, 0), model.index(model.rowCount() - 1, 0),
                (Qt.EditRole,)
            )
        else:
            super().setModelData(editor, model, index)


class DiscreteVariableEditor(VariableEditor):
    """An editor widget for editing a discrete variable.

    Extends the :class:`VariableEditor` to enable editing of
    variables values.
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.merge_dialog_settings = {}
        self._values = None

        form = self.layout().itemAt(0)
        assert isinstance(form, QFormLayout)
        #: A list model of discrete variable's values.
        self.values_model = CountedStateModel(
            flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
        )

        vlayout = QVBoxLayout(spacing=1)
        self.values_edit = QListView(
            editTriggers=QListView.DoubleClicked | QListView.EditKeyPressed,
            selectionMode=QListView.ExtendedSelection,
            uniformItemSizes=True,
        )
        self.values_edit.setItemDelegate(CategoriesEditDelegate(self))
        self.values_edit.setModel(self.values_model)
        self.values_model.dataChanged.connect(self.on_values_changed)

        self.values_edit.selectionModel().selectionChanged.connect(
            self.on_value_selection_changed)
        self.values_model.layoutChanged.connect(self.on_value_selection_changed)
        self.values_model.rowsMoved.connect(self.on_value_selection_changed)

        vlayout.addWidget(self.values_edit)
        hlayout = QHBoxLayout(spacing=1)

        self.categories_action_group = group = QActionGroup(
            self, objectName="action-group-categories", enabled=False
        )
        self.move_value_up = QAction(
            "Move up", group,
            iconText="\N{UPWARDS ARROW}",
            toolTip="Move the selected item up.",
            shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier |
                                  Qt.Key_BracketLeft),
            shortcutContext=Qt.WidgetShortcut,
        )
        self.move_value_up.triggered.connect(self.move_up)

        self.move_value_down = QAction(
            "Move down", group,
            iconText="\N{DOWNWARDS ARROW}",
            toolTip="Move the selected item down.",
            shortcut=QKeySequence(Qt.ControlModifier | Qt.AltModifier |
                                  Qt.Key_BracketRight),
            shortcutContext=Qt.WidgetShortcut,
        )
        self.move_value_down.triggered.connect(self.move_down)

        self.add_new_item = QAction(
            "Add", group,
            iconText="+",
            objectName="action-add-item",
            toolTip="Append a new item.",
            shortcut=QKeySequence(QKeySequence.New),
            shortcutContext=Qt.WidgetShortcut,
        )
        self.remove_item = QAction(
            "Remove item", group,
            iconText="\N{MINUS SIGN}",
            objectName="action-remove-item",
            toolTip="Delete the selected item.",
            shortcut=QKeySequence(QKeySequence.Delete),
            shortcutContext=Qt.WidgetShortcut,
        )
        self.rename_selected_items = QAction(
            "Rename selected items", group,
            iconText="=",
            objectName="action-rename-selected-items",
            toolTip="Rename selected items.",
            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_Equal),
            shortcutContext=Qt.WidgetShortcut,
        )
        self.merge_items = QAction(
            "Merge", group,
            iconText="M",
            objectName="action-activate-merge-dialog",
            toolTip="Merge infrequent items.",
            shortcut=QKeySequence(Qt.ControlModifier | Qt.MetaModifier | Qt.Key_Equal),
            shortcutContext=Qt.WidgetShortcut
        )

        self.add_new_item.triggered.connect(self._add_category)
        self.remove_item.triggered.connect(self._remove_category)
        self.rename_selected_items.triggered.connect(self._rename_selected_categories)
        self.merge_items.triggered.connect(self._merge_categories)

        button1 = FixedSizeButton(
            self, defaultAction=self.move_value_up,
            accessibleName="Move up"
        )
        button2 = FixedSizeButton(
            self, defaultAction=self.move_value_down,
            accessibleName="Move down"
        )
        button3 = FixedSizeButton(
            self, defaultAction=self.add_new_item,
            accessibleName="Add"
        )
        button4 = FixedSizeButton(
            self, defaultAction=self.remove_item,
            accessibleName="Remove"
        )
        button5 = FixedSizeButton(
            self, defaultAction=self.rename_selected_items,
            accessibleName="Merge selected items"
        )
        button6 = FixedSizeButton(
            self, defaultAction=self.merge_items,
            accessibleName="Merge infrequent",
        )

        self.values_edit.addActions([
            self.move_value_up, self.move_value_down,
            self.add_new_item, self.remove_item, self.rename_selected_items
        ])
        self.values_edit.setContextMenuPolicy(Qt.CustomContextMenu)

        def context_menu(pos: QPoint):
            viewport = self.values_edit.viewport()
            menu = QMenu(self.values_edit)
            menu.setAttribute(Qt.WA_DeleteOnClose)
            menu.addActions([self.rename_selected_items, self.remove_item])
            menu.popup(viewport.mapToGlobal(pos))
        self.values_edit.customContextMenuRequested.connect(context_menu)

        hlayout.addWidget(button1)
        hlayout.addWidget(button2)
        hlayout.addSpacing(3)
        hlayout.addWidget(button3)
        hlayout.addWidget(button4)
        hlayout.addSpacing(3)
        hlayout.addWidget(button5)
        hlayout.addWidget(button6)

        hlayout.addStretch(10)
        vlayout.addLayout(hlayout)

        form.insertRow(2, "Values:", vlayout)

        QWidget.setTabOrder(self.name_edit, self.values_edit)
        QWidget.setTabOrder(self.values_edit, button1)
        QWidget.setTabOrder(button1, button2)
        QWidget.setTabOrder(button2, button3)
        QWidget.setTabOrder(button3, button4)
        QWidget.setTabOrder(button4, button5)
        QWidget.setTabOrder(button5, button6)

    def set_data(self, var, transform=()):
        raise NotImplementedError

    def set_data_categorical(self, var, values, transform=()):
        # type: (Optional[Categorical], Optional[Sequence[float]], Sequence[Transform]) -> None
        """
        Set the variable to edit.

        `values` is needed for categorical features to perform grouping.
        """
        # pylint: disable=too-many-branches
        super().set_data(var, transform=transform)
        self._values = values
        tr = None  # type: Optional[CategoriesMapping]
        for tr_ in transform:
            if isinstance(tr_, CategoriesMapping):
                tr = tr_

        items = []
        if tr is not None:
            ci_index = {c: i for i, c in enumerate(var.categories)}
            for ci, cj in tr.mapping:
                if ci is None and cj is not None:
                    # level added
                    item = {
                        Qt.EditRole: cj,
                        EditStateRole: ItemEditState.Added,
                        SourcePosRole: None
                    }
                elif ci is not None and cj is None:
                    # ci level dropped
                    item = {
                        Qt.EditRole: ci,
                        EditStateRole: ItemEditState.Dropped,
                        SourcePosRole: ci_index[ci],
                        SourceNameRole: ci
                    }
                elif ci is not None and cj is not None:
                    # rename or reorder
                    item = {
                        Qt.EditRole: cj,
                        EditStateRole: ItemEditState.NoState,
                        SourcePosRole: ci_index[ci],
                        SourceNameRole: ci
                    }
                else:
                    assert False, "invalid mapping: {!r}".format(tr.mapping)
                items.append(item)
        elif var is not None:
            items = [
                {Qt.EditRole: c,
                 EditStateRole: ItemEditState.NoState,
                 SourcePosRole: i,
                 SourceNameRole: c}
                for i, c in enumerate(var.categories)
            ]
        else:
            items = []

        with disconnected(self.values_model.dataChanged,
                          self.on_values_changed):
            self.values_model.clear()
            self.values_model.insertRows(0, len(items))
            for i, item in enumerate(items):
                self.values_model.setItemData(
                    self.values_model.index(i, 0),
                    item
                )

        self.add_new_item.actionGroup().setEnabled(var is not None)

    def __categories_mapping(self):
        # type: () -> CategoriesMappingType
        """
        Encode and return the current state as a CategoriesMappingType
        """
        model = self.values_model
        source = self.var.categories

        res = []  # type: CategoriesMappingType
        for i in range(model.rowCount()):
            midx = model.index(i, 0)
            category = midx.data(Qt.EditRole)
            source_pos = midx.data(SourcePosRole)  # type: Optional[int]
            if source_pos is not None:
                source_name = source[source_pos]
            else:
                source_name = None
            state = midx.data(EditStateRole)
            if state == ItemEditState.Dropped:
                res.append((source_name, None))
            elif state == ItemEditState.Added:
                res.append((None, category))
            else:
                res.append((source_name, category))
        return res

    def get_data(self):
        """Retrieve the modified variable
        """
        var, tr = super().get_data()
        if var is None:
            return var, tr
        mapping = self.__categories_mapping()
        assert len(mapping) >= len(var.categories), f'{mapping}, {var}'
        if any(_1 != _2 or _2 != _3
               for (_1, _2), _3 in zip_longest(mapping, var.categories)):
            tr.append(CategoriesMapping(mapping))
        return var, tr

    def clear(self):
        """Clear the model state.
        """
        super().clear()
        self.values_model.clear()

    def move_rows(self, rows, offset):
        if not rows:
            return
        assert len(rows) == 1
        i = rows[0].row()
        if offset > 0:
            offset += 1
        self.values_model.moveRows(QModelIndex(), i, 1, QModelIndex(), i + offset)
        self.variable_changed.emit()

    def move_up(self):
        rows = self.values_edit.selectionModel().selectedRows()
        self.move_rows(rows, -1)

    def move_down(self):
        rows = self.values_edit.selectionModel().selectedRows()
        self.move_rows(rows, 1)

    @Slot()
    def on_values_changed(self):
        self.variable_changed.emit()

    @Slot()
    def on_value_selection_changed(self):
        rows = self.values_edit.selectionModel().selectedRows()
        if len(rows) == 1:
            i = rows[0].row()
            self.move_value_up.setEnabled(i != 0)
            self.move_value_down.setEnabled(i != self.values_model.rowCount() - 1)
        else:
            self.move_value_up.setEnabled(False)
            self.move_value_down.setEnabled(False)

    def _remove_category(self):
        """
        Remove the current selected category.

        If the item is an existing category present in the source variable it
        is marked as removed in the view. But if it was added in the set
        transformation it is removed entirely from the model and view.
        """
        view = self.values_edit
        rows = view.selectionModel().selectedRows(0)
        if not rows:
            return  # pragma: no cover
        for index in rows:
            model = index.model()
            state = index.data(EditStateRole)
            pos = index.data(SourcePosRole)
            if pos is not None and pos >= 0:
                # existing level -> only mark/toggle its dropped state,
                model.setData(
                    index,
                    ItemEditState.Dropped if state != ItemEditState.Dropped
                    else ItemEditState.NoState,
                    EditStateRole)
            elif state == ItemEditState.Added:
                # new level -> remove it
                model.removeRow(index.row())
            else:
                assert False, "invalid state '{}' for {}" \
                    .format(state, index.row())

    def _add_category(self):
        """
        Add a new category
        """
        view = self.values_edit
        model = view.model()

        with disconnected(model.dataChanged, self.on_values_changed,
                          Qt.UniqueConnection):
            row = model.rowCount()
            if not model.insertRow(model.rowCount()):
                return
            index = model.index(row, 0)
            model.setItemData(
                index, {
                    Qt.EditRole: "",
                    SourcePosRole: None,
                    EditStateRole: ItemEditState.Added
                }
            )
            view.setCurrentIndex(index)
            view.edit(index)
        self.on_values_changed()

    def _merge_categories(self) -> None:
        """
        Merge less common categories into one with the dialog for merge
        selection.
        """
        view = self.values_edit
        model = view.model()  # type: QAbstractItemModel

        selected_attributes = [ind.data(SourceNameRole)
                               for ind in view.selectedIndexes()]
        dlg = GroupItemsDialog(
            self.var, self._values, selected_attributes,
            self.merge_dialog_settings.get(self.var, {}), self,
            windowTitle="Import Options",
            sizeGripEnabled=True,
        )
        dlg.setWindowModality(Qt.WindowModal)
        status = dlg.exec()
        dlg.deleteLater()
        self.merge_dialog_settings[self.var] = dlg.get_dialog_settings()

        rows = (model.index(i, 0) for i in range(model.rowCount()))

        def complete_merge(text, merge_attributes):
            # write the new text for edit role in all rows
            with disconnected(model.dataChanged, self.on_values_changed):
                for row in rows:
                    if row.data(SourceNameRole) in merge_attributes:
                        model.setData(row, text, Qt.EditRole)
            self.variable_changed.emit()

        if status == QDialog.Accepted:
            complete_merge(
                dlg.get_merged_value_name(), dlg.get_merge_attributes()
            )

    def _rename_selected_categories(self):
        """
        Rename selected categories and merging them.

        Popup an editable combo box for selection/edit of a new value.
        """
        view = self.values_edit
        selmodel = view.selectionModel()
        index = view.currentIndex()
        if not selmodel.isSelected(index):
            indices = selmodel.selectedRows(0)
            if indices:
                index = indices[0]
        # delegate to the CategoriesEditDelegate
        view.edit(index)


class ContinuousVariableEditor(VariableEditor):
    # TODO: enable editing of display format...
    pass


class TimeVariableEditor(VariableEditor):
    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)
        form = self.layout().itemAt(0)

        self.format_cb = QComboBox()
        for item, data in [("Detect automatically", (None, 1, 1))] + list(
            Orange.data.TimeVariable.ADDITIONAL_FORMATS.items()
        ):
            self.format_cb.addItem(item, StrpTime(item, *data))
        self.format_cb.currentIndexChanged.connect(self.variable_changed)
        form.insertRow(2, "Format:", self.format_cb)

    def set_data(self, var, transform=()):
        super().set_data(var, transform)
        if self.parent() is not None and isinstance(self.parent().var, Time):
            # when transforming from time to time disable format selection combo
            self.format_cb.setEnabled(False)
        else:
            # select the format from StrpTime transform
            for tr in transform:
                if isinstance(tr, StrpTime):
                    index = self.format_cb.findText(tr.label)
                    self.format_cb.setCurrentIndex(index)
            self.format_cb.setEnabled(True)

    def get_data(self):
        var, tr = super().get_data()
        if var is not None and (self.parent() is None or not isinstance(self.parent().var, Time)):
            # do not add StrpTime when transforming from time to time
            tr.insert(0, self.format_cb.currentData())
        return var, tr


def variable_icon(var):
    # type: (Union[Variable, Type[Variable], ReinterpretTransform]) -> QIcon
    if not isinstance(var, type):
        var = type(var)

    if issubclass(var, (Categorical, AsCategorical)):
        return gui.attributeIconDict[1]
    elif issubclass(var, (Real, AsContinuous)):
        return gui.attributeIconDict[2]
    elif issubclass(var, (String, AsString)):
        return gui.attributeIconDict[3]
    elif issubclass(var, (Time, AsTime)):
        return gui.attributeIconDict[4]
    else:
        return gui.attributeIconDict[-1]


#: ItemDataRole storing the data vector transform
#: (`List[Union[ReinterpretTransform, Transform]]`)
TransformRole = Qt.UserRole + 42


class VariableEditDelegate(QStyledItemDelegate):
    ReinterpretNames = {
        AsCategorical: "categorical", AsContinuous: "numeric",
        AsString: "string", AsTime: "time"
    }

    def initStyleOption(self, option, index):
        # type: (QStyleOptionViewItem, QModelIndex) -> None
        super().initStyleOption(option, index)
        item = index.data(Qt.EditRole)
        var = tr = None
        if isinstance(item, DataVectorTypes):
            var = item.vtype
            option.icon = variable_icon(var)
        if isinstance(item, VariableTypes):
            var = item
            option.icon = variable_icon(item)
        elif isinstance(item, Orange.data.Variable):
            var = item
            option.icon = gui.attributeIconDict[var]

        transform = index.data(TransformRole)
        if not isinstance(transform, list):
            transform = []

        if transform and isinstance(transform[0], ReinterpretTransformTypes):
            option.icon = variable_icon(transform[0])

        if not option.icon.isNull():
            option.features |= QStyleOptionViewItem.HasDecoration

        if var is not None:
            text = var.name
            for tr in transform:
                if isinstance(tr, Rename):
                    text = ("{} \N{RIGHTWARDS ARROW} {}"
                            .format(var.name, tr.name))
            for tr in transform:
                if isinstance(tr, ReinterpretTransformTypes):
                    text += f" (reinterpreted as " \
                            f"{self.ReinterpretNames[type(tr)]})"
            option.text = text
        if transform:
            # mark as changed (maybe also change color, add text, ...)
            option.font.setItalic(True)

        multiplicity = index.data(MultiplicityRole)
        if isinstance(multiplicity, int) and multiplicity > 1:
            option.palette.setBrush(QPalette.Text, QBrush(Qt.red))
            option.palette.setBrush(QPalette.HighlightedText, QBrush(Qt.red))

    def helpEvent(self, event: QHelpEvent, view: QAbstractItemView,
                  option: QStyleOptionViewItem, index: QModelIndex) -> bool:
        multiplicity = index.data(MultiplicityRole)
        name = VariableListModel.effective_name(index)
        if isinstance(multiplicity, int) and multiplicity > 1 \
                and name is not None:
            QToolTip.showText(
                event.globalPos(), f"Name `{name}` is duplicated",
                view.viewport()
            )
            return True
        else:  # pragma: no cover
            return super().helpEvent(event, view, option, index)


# Item model for edited variables (Variable). Define a display role to be the
# source variable name. This is used only in keyboard search. The display is
# otherwise completely handled by a delegate.
class VariableListModel(CountedListModel):
    def data(self, index, role=Qt.DisplayRole):
        # type: (QModelIndex, Qt.ItemDataRole) -> Any
        row = index.row()
        if not index.isValid() or not 0 <= row < self.rowCount():
            return None
        if role == Qt.DisplayRole:
            item = self[row]
            if isinstance(item, VariableTypes):
                return item.name
            if isinstance(item, DataVectorTypes):
                return item.vtype.name
        return super().data(index, role)

    def key(self, index):
        return VariableListModel.effective_name(index)

    def keyRoles(self):  # type: () -> FrozenSet[int]
        return frozenset((Qt.DisplayRole, Qt.EditRole, TransformRole))

    @staticmethod
    def effective_name(index) -> Optional[str]:
        item = index.data(Qt.EditRole)
        if isinstance(item, DataVectorTypes):
            var = item.vtype
        elif isinstance(item, VariableTypes):
            var = item
        else:
            return None
        tr = index.data(TransformRole)
        return effective_name(var, tr or [])


def effective_name(var: Variable, tr: Sequence[Transform]) -> str:
    name = var.name
    for t in tr:
        if isinstance(t, Rename):
            name = t.name
    return name


class ReinterpretVariableEditor(VariableEditor):
    """
    A 'compound' variable editor capable of variable type reinterpretations.
    """
    _editors = {
        Categorical: 0,
        Real: 1,
        String: 2,
        Time: 3,
        type(None): -1,
    }

    def __init__(self, parent=None, **kwargs):
        # Explicitly skip VariableEditor's __init__, this is ugly but we have
        # a completely different layout/logic as a compound editor (should
        # really not subclass VariableEditor).
        super(VariableEditor, self).__init__(parent, **kwargs)  # pylint: disable=bad-super-call
        self.var = None  # type: Optional[Variable]
        self.__transform = None  # type: Optional[ReinterpretTransform]
        self.__data = None  # type: Optional[DataVector]
        #: Stored transform state indexed by variable. Used to preserve state
        #: between type switches.
        self.__history = {}  # type: Dict[Variable, List[Transform]]

        self.setLayout(QStackedLayout())

        def decorate(editor: VariableEditor) -> VariableEditor:
            """insert an type combo box into a `editor`'s layout."""
            form = editor.layout().itemAt(0)
            assert isinstance(form, QFormLayout)
            typecb = QComboBox(objectName="type-combo")
            typecb.addItem(variable_icon(Categorical), "Categorical", Categorical)
            typecb.addItem(variable_icon(Real), "Numeric", Real)
            typecb.addItem(variable_icon(String), "Text", String)
            typecb.addItem(variable_icon(Time), "Time", Time)
            typecb.activated[int].connect(self.__reinterpret_activated)
            form.insertRow(1, "Type:", typecb)
            # Insert the typecb after name edit in the focus chain
            name_edit = editor.findChild(QLineEdit, )
            if name_edit is not None:
                QWidget.setTabOrder(name_edit, typecb)
            return editor
        # This is ugly. Create an editor for each type and insert a type
        # selection combo box into its layout. Switch between widgets
        # on type change.
        self.disc_edit = dedit = decorate(DiscreteVariableEditor())
        cedit = decorate(ContinuousVariableEditor())
        tedit = decorate(TimeVariableEditor())
        sedit = decorate(VariableEditor())

        for ed in [dedit, cedit, tedit, sedit]:
            ed.variable_changed.connect(self.variable_changed)

        self.layout().addWidget(dedit)
        self.layout().addWidget(cedit)
        self.layout().addWidget(sedit)
        self.layout().addWidget(tedit)

    def set_data(self, data, transform=()):  # pylint: disable=arguments-differ
        # type: (Optional[DataVector], Sequence[Transform]) -> None
        """
        Set the editor data.

        Note
        ----
        This must be a `DataVector` as the vector's values are needed for type
        reinterpretation/casts.

        If the `transform` sequence contains ReinterpretTransform then it
        must be in the first position.
        """
        type_transform = None  # type: Optional[ReinterpretTransform]
        if transform:
            _tr = transform[0]
            if isinstance(_tr, ReinterpretTransformTypes):
                type_transform = _tr
                transform = transform[1:]
            assert not any(isinstance(t, ReinterpretTransformTypes)
                           for t in transform)
        self.__transform = type_transform
        self.__data = data
        self.var = data.vtype if data is not None else None

        if type_transform is not None and data is not None:
            data = type_transform(data)
        if data is not None:
            var = data.vtype
        else:
            var = None
        index = self._editors.get(type(var), -1)
        self.layout().setCurrentIndex(index)
        if index != -1:
            w = self.layout().currentWidget()
            assert isinstance(w, VariableEditor)
            if isinstance(var, Categorical):
                w.set_data_categorical(var, data.data(), transform=transform)
            else:
                w.set_data(var, transform=transform)
            self.__history[var] = tuple(transform)
            cb = w.findChild(QComboBox, "type-combo")
            cb.setCurrentIndex(index)

    def get_data(self):
        # type: () -> Tuple[Variable, Sequence[Transform]]
        editor = self.layout().currentWidget()  # type: VariableEditor
        var, tr = editor.get_data()
        if type(var) != type(self.var):  # pylint: disable=unidiomatic-typecheck
            assert self.__transform is not None
            var = self.var
            tr = [self.__transform, *tr]
        return var, tr

    def __reinterpret_activated(self, index):
        layout = self.layout()
        assert isinstance(layout, QStackedLayout)
        if index == layout.currentIndex():
            return
        current = layout.currentWidget()
        assert isinstance(current, VariableEditor)
        Specific = {
            Categorical: CategoricalTransformTypes
        }
        _var, _tr = current.get_data()
        if _var is not None:
            self.__history[_var] = _tr

        var = self.var
        transform = self.__transform
        # take/preserve the general transforms that apply to all types
        specific = Specific.get(type(var), ())
        _tr = [t for t in _tr if not isinstance(t, specific)]

        layout.setCurrentIndex(index)
        w = layout.currentWidget()
        cb = w.findChild(QComboBox, "type-combo")
        cb.setCurrentIndex(index)
        cb.setFocus()
        target = cb.itemData(index, Qt.UserRole)
        assert issubclass(target, VariableTypes)
        if not isinstance(var, target):
            if target == Real:
                transform = AsContinuous()
            elif target == Categorical:
                transform = AsCategorical()
            elif target == Time:
                transform = AsTime()
            elif target == String:
                transform = AsString()
        else:
            transform = None
            var = self.var

        self.__transform = transform
        data = None
        if transform is not None and self.__data is not None:
            data = transform(self.__data)
            var = data.vtype

        if var in self.__history:
            tr = self.__history[var]
        else:
            tr = []
        # type specific transform
        specific = Specific.get(type(var), ())
        # merge tr and _tr
        tr = _tr + [t for t in tr if isinstance(t, specific)]
        with disconnected(
                w.variable_changed, self.variable_changed,
                Qt.UniqueConnection
        ):
            if isinstance(w, DiscreteVariableEditor):
                data = data or self.__data
                w.set_data_categorical(var, data.data(), transform=tr)
            else:
                w.set_data(var, transform=tr)
        self.variable_changed.emit()

    def set_merge_context(self, merge_context):
        self.disc_edit.merge_dialog_settings = merge_context

    def get_merge_context(self):
        return self.disc_edit.merge_dialog_settings


class OWEditDomain(widget.OWWidget):
    name = "Edit Domain"
    description = "Rename variables, edit categories and variable annotations."
    icon = "icons/EditDomain.svg"
    priority = 3125
    keywords = ["rename", "drop", "reorder", "order"]

    class Inputs:
        data = Input("Data", Orange.data.Table)

    class Outputs:
        data = Output("Data", Orange.data.Table)

    class Error(widget.OWWidget.Error):
        duplicate_var_name = widget.Msg("A variable name is duplicated.")

    settingsHandler = settings.DomainContextHandler()
    settings_version = 2

    _domain_change_store = settings.ContextSetting({})
    _selected_item = settings.ContextSetting(None)  # type: Optional[Tuple[str, int]]
    _merge_dialog_settings = settings.ContextSetting({})
    output_table_name = settings.ContextSetting("")

    want_main_area = False

    def __init__(self):
        super().__init__()
        self.data = None  # type: Optional[Orange.data.Table]
        #: The current selected variable index
        self.selected_index = -1
        self._invalidated = False
        self.typeindex = 0

        main = gui.hBox(self.controlArea, spacing=6)
        box = gui.vBox(main, "Variables")

        self.variables_model = VariableListModel(parent=self)
        self.variables_view = self.domain_view = QListView(
            selectionMode=QListView.SingleSelection,
            uniformItemSizes=True,
        )
        self.variables_view.setItemDelegate(VariableEditDelegate(self))
        self.variables_view.setModel(self.variables_model)
        self.variables_view.selectionModel().selectionChanged.connect(
            self._on_selection_changed
        )
        box.layout().addWidget(self.variables_view)

        box = gui.vBox(main, "Edit")
        self._editor = ReinterpretVariableEditor()
        box.layout().addWidget(self._editor)

        self.le_output_name = gui.lineEdit(
            self.buttonsArea, self, "output_table_name", "Output table name: ",
            orientation=Qt.Horizontal)

        gui.rubber(self.buttonsArea)

        bbox = gui.hBox(self.buttonsArea)
        breset_all = gui.button(
            bbox, self, "Reset All",
            objectName="button-reset-all",
            toolTip="Reset all variables to their input state.",
            autoDefault=False,
            callback=self.reset_all
        )
        breset = gui.button(
            bbox, self, "Reset Selected",
            objectName="button-reset",
            toolTip="Rest selected variable to its input state.",
            autoDefault=False,
            callback=self.reset_selected
        )
        bapply = gui.button(
            bbox, self, "Apply",
            objectName="button-apply",
            toolTip="Apply changes and commit data on output.",
            default=True,
            autoDefault=False,
            callback=self.commit
        )

        self.variables_view.setFocus(Qt.NoFocusReason)  # initial focus

    @Inputs.data
    def set_data(self, data):
        """Set input dataset."""
        self.closeContext()
        self.clear()
        self.data = data

        if self.data is not None:
            self.setup_model(data)
            self.le_output_name.setPlaceholderText(data.name)
            self.openContext(self.data)
            self._editor.set_merge_context(self._merge_dialog_settings)
            self._restore()
        else:
            self.le_output_name.setPlaceholderText("")

        self.commit()

    def clear(self):
        """Clear the widget state."""
        self.data = None
        self.variables_model.clear()
        self.clear_editor()
        assert self.selected_index == -1
        self.selected_index = -1

        self._selected_item = None
        self._domain_change_store = {}
        self._merge_dialog_settings = {}

    def reset_selected(self):
        """Reset the currently selected variable to its original state."""
        ind = self.selected_var_index()
        if ind >= 0:
            model = self.variables_model
            midx = model.index(ind)
            var = midx.data(Qt.EditRole)
            tr = midx.data(TransformRole)
            if not tr:
                return  # nothing to reset
            editor = self._editor
            with disconnected(editor.variable_changed,
                              self._on_variable_changed):
                model.setData(midx, [], TransformRole)
                editor.set_data(var, transform=[])
            self._invalidate()

    def reset_all(self):
        """Reset all variables to their original state."""
        self._domain_change_store = {}
        if self.data is not None:
            model = self.variables_model
            for i in range(model.rowCount()):
                midx = model.index(i)
                model.setData(midx, [], TransformRole)
            index = self.selected_var_index()
            if index >= 0:
                self.open_editor(index)
            self._invalidate()

    def selected_var_index(self):
        """Return the current selected variable index."""
        rows = self.variables_view.selectedIndexes()
        assert len(rows) <= 1
        return rows[0].row() if rows else -1

    def setup_model(self, data: Orange.data.Table):
        model = self.variables_model
        vars_ = []
        columns = []
        for i, _, var, coldata in enumerate_columns(data):
            var = abstract(var)
            vars_.append(var)
            if isinstance(var, Categorical):
                data = CategoricalVector(var, coldata)
            elif isinstance(var, Real):
                data = RealVector(var, coldata)
            elif isinstance(var, Time):
                data = TimeVector(var, coldata)
            elif isinstance(var, String):
                data = StringVector(var, coldata)
            columns.append(data)

        model[:] = vars_
        for i, d in enumerate(columns):
            model.setData(model.index(i), d, Qt.EditRole)

    def _restore(self, ):
        """
        Restore the edit transform from saved state.
        """
        model = self.variables_model
        for i in range(model.rowCount()):
            midx = model.index(i, 0)
            coldesc = model.data(midx, Qt.EditRole)  # type: DataVector
            tr = self._restore_transform(coldesc.vtype)
            if tr:
                model.setData(midx, tr, TransformRole)

        # Restore the current variable selection
        i = -1
        if self._selected_item is not None:
            for i, vec in enumerate(model):
                if vec.vtype.name_type() == self._selected_item:
                    break
        if i == -1 and model.rowCount():
            i = 0

        if i != -1:
            itemmodels.select_row(self.variables_view, i)

    def _on_selection_changed(self):
        self.selected_index = self.selected_var_index()
        if self.selected_index != -1:
            self._selected_item = self.variables_model[self.selected_index].vtype.name_type()
        else:
            self._selected_item = None
        self.open_editor(self.selected_index)

    def open_editor(self, index):
        # type: (int) -> None
        self.clear_editor()
        model = self.variables_model
        if not 0 <= index < model.rowCount():
            return
        idx = model.index(index, 0)
        vector = model.data(idx, Qt.EditRole)
        tr = model.data(idx, TransformRole)
        if tr is None:
            tr = []
        editor = self._editor
        editor.set_data(vector, transform=tr)
        editor.variable_changed.connect(
            self._on_variable_changed, Qt.UniqueConnection
        )

    def clear_editor(self):
        current = self._editor
        try:
            current.variable_changed.disconnect(self._on_variable_changed)
        except TypeError:
            pass
        current.set_data(None)
        current.layout().currentWidget().clear()

    @Slot()
    def _on_variable_changed(self):
        """User edited the current variable in editor."""
        assert 0 <= self.selected_index <= len(self.variables_model)
        editor = self._editor
        var, transform = editor.get_data()
        model = self.variables_model
        midx = model.index(self.selected_index, 0)
        model.setData(midx, transform, TransformRole)
        self._store_transform(var, transform)
        self._invalidate()

    def _store_transform(self, var, transform):
        # type: (Variable, List[Transform]) -> None
        self._domain_change_store[deconstruct(var)] = [deconstruct(t) for t in transform]

    def _restore_transform(self, var):
        # type: (Variable) -> List[Transform]
        tr_ = self._domain_change_store.get(deconstruct(var), [])
        tr = []

        for t in tr_:
            try:
                tr.append(reconstruct(*t))
            except (NameError, TypeError) as err:
                warnings.warn(
                    "Failed to restore transform: {}, {!r}".format(t, err),
                    UserWarning, stacklevel=2
                )
        return tr

    def _invalidate(self):
        self._set_modified(True)

    def _set_modified(self, state):
        self._invalidated = state
        b = self.findChild(QPushButton, "button-apply")
        if isinstance(b, QPushButton):
            f = b.font()
            f.setItalic(state)
            b.setFont(f)

    def commit(self):
        """
        Apply the changes to the input data and send the changed data to output.
        """
        self._set_modified(False)
        self.Error.duplicate_var_name.clear()

        data = self.data
        if data is None:
            self.Outputs.data.send(None)
            return
        model = self.variables_model

        def state(i):
            # type: (int) -> Tuple[DataVector, List[Transform]]
            midx = self.variables_model.index(i, 0)
            return (model.data(midx, Qt.EditRole),
                    model.data(midx, TransformRole))

        state = [state(i) for i in range(model.rowCount())]
        input_vars = data.domain.variables + data.domain.metas
        if self.output_table_name in ("", data.name) \
                and not any(requires_transform(var, trs)
                            for var, (_, trs) in zip(input_vars, state)):
            self.Outputs.data.send(data)
            return

        assert all(v_.vtype.name == v.name
                   for v, (v_, _) in zip(input_vars, state))
        output_vars = []
        unlinked_vars = []
        unlink_domain = False
        for (_, tr), v in zip(state, input_vars):
            if tr:
                var = apply_transform(v, data, tr)
                if requires_unlink(v, tr):
                    unlinked_var = var.copy(compute_value=None)
                    unlink_domain = True
                else:
                    unlinked_var = var
            else:
                unlinked_var = var = v
            output_vars.append(var)
            unlinked_vars.append(unlinked_var)

        if len(output_vars) != len({v.name for v in output_vars}):
            self.Error.duplicate_var_name()
            self.Outputs.data.send(None)
            return

        domain = data.domain
        nx = len(domain.attributes)
        ny = len(domain.class_vars)

        def construct_domain(vars_list):
            # Move non primitive Xs, Ys to metas (if they were changed)
            Xs = [v for v in vars_list[:nx] if v.is_primitive()]
            Ys = [v for v in vars_list[nx: nx + ny] if v.is_primitive()]
            Ms = vars_list[nx + ny:] + \
                 [v for v in vars_list[:nx + ny] if not  v.is_primitive()]
            return Orange.data.Domain(Xs, Ys, Ms)

        domain = construct_domain(output_vars)
        new_data = data.transform(domain)
        if unlink_domain:
            unlinked_domain = construct_domain(unlinked_vars)
            new_data = new_data.from_numpy(
                unlinked_domain,
                new_data.X, new_data.Y, new_data.metas, new_data.W,
                new_data.attributes, new_data.ids
            )
        if self.output_table_name:
            new_data.name = self.output_table_name
        self.Outputs.data.send(new_data)

    def sizeHint(self):
        sh = super().sizeHint()
        return sh.expandedTo(QSize(660, 550))

    def storeSpecificSettings(self):
        """
        Update setting before context closes - also when widget closes.
        """
        self._merge_dialog_settings = self._editor.get_merge_context()

    def send_report(self):

        if self.data is not None:
            model = self.variables_model
            state = ((model.data(midx, Qt.EditRole),
                      model.data(midx, TransformRole))
                     for i in range(model.rowCount())
                     for midx in [model.index(i)])
            parts = []
            for vector, trs in state:
                if trs:
                    parts.append(report_transform(vector.vtype, trs))
            if parts:
                html = ("<ul>" +
                        "".join(map("<li>{}</li>".format, parts)) +
                        "</ul>")
            else:
                html = "No changes"
            self.report_raw("", html)
        else:
            self.report_data(None)

    @classmethod
    def migrate_context(cls, context, version):
        # pylint: disable=bad-continuation
        if version is None or version <= 1:
            hints_ = context.values.get("domain_change_hints", ({}, -2))[0]
            store = []
            ns = "Orange.data.variable"
            mapping = {
                "DiscreteVariable":
                    lambda name, args, attrs:
                        ("Categorical", (name, tuple(args[0][1]), ())),
                "TimeVariable":
                    lambda name, _, attrs:
                        ("Time", (name, ())),
                "ContinuousVariable":
                    lambda name, _, attrs:
                        ("Real", (name, (3, "f"), ())),
                "StringVariable":
                    lambda name, _, attrs:
                        ("String", (name, ())),
            }
            for (module, class_name, *rest), target in hints_.items():
                if module != ns:
                    continue
                f = mapping.get(class_name)
                if f is None:
                    continue
                trs = []
                key_mapped = f(*rest)
                item_mapped = f(*target[2:])
                src = reconstruct(*key_mapped)   # type: Variable
                dst = reconstruct(*item_mapped)  # type: Variable
                if src.name != dst.name:
                    trs.append(Rename(dst.name))
                if src.annotations != dst.annotations:
                    trs.append(Annotate(dst.annotations))
                if isinstance(src, Categorical):
                    if src.categories != dst.categories:
                        assert len(src.categories) == len(dst.categories)
                        trs.append(CategoriesMapping(
                            list(zip(src.categories, dst.categories))))
                store.append((deconstruct(src), [deconstruct(tr) for tr in trs]))
            context.values["_domain_change_store"] = (dict(store), -2)


def enumerate_columns(
        table: Orange.data.Table
) -> Iterable[Tuple[int, str, Orange.data.Variable, Callable[[], ndarray]]]:
    domain = table.domain
    for i, (var, role) in enumerate(
            chain(zip(domain.attributes, repeat("x")),
                  zip(domain.class_vars, repeat("y")),
                  zip(domain.metas, repeat("m"))),
    ):
        if i >= len(domain.variables):
            i = len(domain.variables) - i - 1
        data = partial(table_column_data, table, i)
        yield i, role, var, data


def table_column_data(
        table: Orange.data.Table,
        var: Union[Orange.data.Variable, int],
        dtype=None
) -> MArray:
    col, copy = table.get_column_view(var)
    var = table.domain[var]  # type: Orange.data.Variable
    if var.is_primitive() and not np.issubdtype(col.dtype, np.inexact):
        col = col.astype(float)
        copy = True

    if dtype is None:
        if isinstance(var, Orange.data.TimeVariable):
            dtype = np.dtype("M8[us]")
            col = col * 1e6
        elif isinstance(var, Orange.data.ContinuousVariable):
            dtype = np.dtype(float)
        elif isinstance(var, Orange.data.DiscreteVariable):
            _values = tuple(var.values)
            _n_values = len(_values)
            dtype = np.dtype(int, metadata={
                "__formatter": lambda i: _values[i] if 0 <= i < _n_values else "?"
            })
        elif isinstance(var, Orange.data.StringVariable):
            dtype = np.dtype(object)
        else:
            assert False
    mask = orange_isna(var, col)

    if dtype != col.dtype:
        col = col.astype(dtype)
        copy = True

    if not copy:
        col = col.copy()
    return MArray(col, mask=mask)


def report_transform(var, trs):
    # type: (Variable, List[Transform]) -> str
    """
    Return a html fragment summarizing the changes applied by `trs` list.

    Parameters
    ----------
    var : Variable
        A variable descriptor no which trs operates
    trs : List[Transform]
        A non empty list of `Transform` instances.

    Returns
    -------
    report : str
    """
    # pylint: disable=too-many-branches
    ReinterpretTypeCode = {
        AsCategorical:  "C", AsContinuous: "N", AsString: "S", AsTime: "T",
    }

    def type_char(value: ReinterpretTransform) -> str:
        return ReinterpretTypeCode.get(type(value), "?")

    def strike(text):
        return "<s>{}</s>".format(escape(text))

    def i(text):
        return "<i>{}</i>".format(escape(text))

    def text(text):
        return "<span>{}</span>".format(escape(text))
    assert trs
    rename = annotate = catmap = unlink = None
    reinterpret = None

    for tr in trs:
        if isinstance(tr, Rename):
            rename = tr
        elif isinstance(tr, Annotate):
            annotate = tr
        elif isinstance(tr, CategoriesMapping):
            catmap = tr
        elif isinstance(tr, Unlink):
            unlink = tr
        elif isinstance(tr, ReinterpretTransformTypes):
            reinterpret = tr

    if reinterpret is not None:
        header = "{} → ({}) {}".format(
            var.name, type_char(reinterpret),
            rename.name if rename is not None else var.name
        )
    elif rename is not None:
        header = "{} → {}".format(var.name, rename.name)
    else:
        header = var.name
    if unlink is not None:
        header += "(unlinked from source)"

    values_section = None
    if catmap is not None:
        values_section = ("Values", [])
        lines = values_section[1]
        for ci, cj in catmap.mapping:
            if ci is None:
                item = cj + ("&nbsp;" * 3) + "(added)"
            elif cj is None:
                item = strike(ci)
            else:
                item = ci + " → " + cj
            lines.append(item)

    annotate_section = None
    if annotate is not None:
        annotate_section = ("Labels", [])
        lines = annotate_section[1]
        old = dict(var.annotations)
        new = dict(annotate.annotations)
        for name in sorted(set(old) - set(new)):
            lines.append(
                "<s>" + i(name) + " : " + text(old[name]) + "</s>"
            )
        for name in sorted(set(new) - set(old)):
            lines.append(
                i(name) + " : " + text(new[name]) + "&nbsp;" * 3 + i("(new)")
            )

        for name in sorted(set(new) & set(old)):
            if new[name] != old[name]:
                lines.append(
                    i(name) + " : " + text(old[name]) + " → " + text(new[name])
                )

    html = ["<div style='font-weight: bold;'>{}</div>".format(header)]
    for title, contents in filter(None, [values_section, annotate_section]):
        section_header = "<div>{}:</div>".format(title)
        section_contents = "<br/>\n".join(contents)
        html.append(section_header)
        html.append(
            "<div style='padding-left: 1em;'>" +
            section_contents +
            "</div>"
        )
    return "\n".join(html)


def abstract(var):
    # type: (Orange.data.Variable) -> Variable
    """
    Return `Varaible` descriptor for an `Orange.data.Variable` instance.

    Parameters
    ----------
    var : Orange.data.Variable

    Returns
    -------
    var : Variable
    """
    annotations = tuple(sorted(
        (key, str(value))
        for key, value in var.attributes.items()
    ))
    linked = var.compute_value is not None
    if isinstance(var, Orange.data.DiscreteVariable):
        return Categorical(var.name, tuple(var.values), annotations, linked)
    elif isinstance(var, Orange.data.TimeVariable):
        return Time(var.name, annotations, linked)
    elif isinstance(var, Orange.data.ContinuousVariable):
        return Real(var.name, (var.number_of_decimals, 'f'), annotations, linked)
    elif isinstance(var, Orange.data.StringVariable):
        return String(var.name, annotations, linked)
    else:
        raise TypeError


def _parse_attributes(mapping):
    # type: (Iterable[Tuple[str, str]]) -> Dict[str, Any]
    # Use the same functionality that parses attributes
    # when reading text files
    return Orange.data.Flags([
        "{}={}".format(*item) for item in mapping
    ]).attributes


def apply_transform(var, table, trs):
    # type: (Orange.data.Variable, Orange.data.Table, List[Transform]) -> Orange.data.Variable
    """
    Apply a list of `Transform` instances on an `Orange.data.Variable`.
    """
    if trs and isinstance(trs[0], ReinterpretTransformTypes):
        reinterpret, trs = trs[0], trs[1:]
        coldata = table_column_data(table, var)
        var = apply_reinterpret(var, reinterpret, coldata)
    if trs:
        return apply_transform_var(var, trs)
    else:
        return var


def requires_unlink(var: Orange.data.Variable, trs: List[Transform]) -> bool:
    # Variable is only unlinked if it has compute_value  or if it has other
    # transformations (that might had added compute_value)
    return trs is not None \
           and any(isinstance(tr, Unlink) for tr in trs) \
           and (var.compute_value is not None or len(trs) > 1)


def requires_transform(var: Orange.data.Variable, trs: List[Transform]) -> bool:
    # Unlink is treated separately: Unlink is required only if the variable
    # has compute_value. Hence tranform is required if it has any
    # transformations other than Unlink, or if unlink is indeed required.
    return trs is not None and (
        not all(isinstance(tr, Unlink) for tr in trs)
        or requires_unlink(var, trs)
    )


@singledispatch
def apply_transform_var(var, trs):
    # type: (Orange.data.Variable, List[Transform]) -> Orange.data.Variable
    raise NotImplementedError


@apply_transform_var.register(Orange.data.DiscreteVariable)
def apply_transform_discete(var, trs):
    # type: (Orange.data.DiscreteVariable, List[Transform]) -> Orange.data.Variable
    # pylint: disable=too-many-branches
    name, annotations = var.name, var.attributes
    mapping = None
    for tr in trs:
        if isinstance(tr, Rename):
            name = tr.name
        elif isinstance(tr, CategoriesMapping):
            mapping = tr.mapping
        elif isinstance(tr, Annotate):
            annotations = _parse_attributes(tr.annotations)

    source_values = var.values
    if mapping is not None:
        dest_values = list(unique(cj for ci, cj in mapping if cj is not None))
    else:
        dest_values = var.values

    def positions(values):
        rval = {c: i for i, c in enumerate(values)}
        assert len(rval) == len(values)
        return rval
    source_codes = positions(source_values)
    dest_codes = positions(dest_values)
    if mapping is not None:
        # construct a lookup table
        lookup = np.full(len(source_values), np.nan, dtype=float)
        for ci, cj in mapping:
            if ci is not None and cj is not None:
                i, j = source_codes[ci], dest_codes[cj]
                lookup[i] = j
        lookup = Lookup(var, lookup)
    else:
        lookup = Identity(var)
    variable = Orange.data.DiscreteVariable(
        name, values=dest_values, compute_value=lookup
    )
    variable.attributes.update(annotations)
    return variable


@apply_transform_var.register(Orange.data.ContinuousVariable)
def apply_transform_continuous(var, trs):
    # type: (Orange.data.ContinuousVariable, List[Transform]) -> Orange.data.Variable
    name, annotations = var.name, var.attributes
    for tr in trs:
        if isinstance(tr, Rename):
            name = tr.name
        elif isinstance(tr, Annotate):
            annotations = _parse_attributes(tr.annotations)
    variable = Orange.data.ContinuousVariable(
        name=name, compute_value=Identity(var)
    )
    variable.attributes.update(annotations)
    return variable


@apply_transform_var.register(Orange.data.TimeVariable)
def apply_transform_time(var, trs):
    # type: (Orange.data.TimeVariable, List[Transform]) -> Orange.data.Variable
    name, annotations = var.name, var.attributes
    for tr in trs:
        if isinstance(tr, Rename):
            name = tr.name
        elif isinstance(tr, Annotate):
            annotations = _parse_attributes(tr.annotations)
    variable = Orange.data.TimeVariable(
        name=name, have_date=var.have_date, have_time=var.have_time,
        compute_value=Identity(var)
    )
    variable.attributes.update(annotations)
    return variable


@apply_transform_var.register(Orange.data.StringVariable)
def apply_transform_string(var, trs):
    # type: (Orange.data.StringVariable, List[Transform]) -> Orange.data.Variable
    name, annotations = var.name, var.attributes
    out_type = Orange.data.StringVariable
    compute_value = Identity
    for tr in trs:
        if isinstance(tr, Rename):
            name = tr.name
        elif isinstance(tr, Annotate):
            annotations = _parse_attributes(tr.annotations)
        elif isinstance(tr, StrpTime):
            out_type = partial(
                Orange.data.TimeVariable, have_date=tr.have_date, have_time=tr.have_time
            )
            compute_value = partial(ReparseTimeTransform, tr=tr)
    variable = out_type(name=name, compute_value=compute_value(var))
    variable.attributes.update(annotations)
    return variable


def make_dict_mapper(
        mapping: Mapping, dtype: Optional[DType] = None
) -> Callable:
    """
    Wrap a `mapping` into a callable ufunc-like function with
    `out`, `dtype`, `where`, ... parameters. If `dtype` is passed to
    `make_dict_mapper` it is used as a the default return dtype,
    otherwise the default dtype is `object`.
    """
    return frompyfunc(mapping.__getitem__, 1, 1, dtype)


as_string = np.frompyfunc(str, 1, 1)
parse_float = ftry(float, ValueError, float("nan"))

_parse_float = np.frompyfunc(parse_float, 1, 1)


def as_float_or_nan(
        arr: ndarray, out: Optional[ndarray] = None, where: Optional[ndarray] = True,
        dtype=None, **kwargs
) -> ndarray:
    """
    Convert elements of the input array using builtin `float`, fill elements
    where conversion failed with NaN.
    """
    if out is None:
        out = np.full(arr.shape, np.nan, float if dtype is None else dtype)
    if np.issubdtype(arr.dtype, np.inexact) or \
            np.issubdtype(arr.dtype, np.integer):
        np.copyto(out, arr, casting="unsafe", where=where)
        return out
    return _parse_float(arr, out, where=where, casting="unsafe", **kwargs)


def copy_attributes(dst: V, src: Orange.data.Variable) -> V:
    # copy `attributes` and `sparse` members from src to dst
    dst.attributes = dict(src.attributes)
    dst.sparse = src.sparse
    return dst


# Building (and applying) concrete type transformations on Table columns

@singledispatch
def apply_reinterpret(var, tr, data):
    # type: (Orange.data.Variable, ReinterpretTransform, MArray) -> Orange.data.Variable
    """
    Apply a re-interpret transform to an `Orange.data.Table`'s column
    """
    raise NotImplementedError


@apply_reinterpret.register(Orange.data.DiscreteVariable)
def apply_reinterpret_d(var, tr, data):
    # type: (Orange.data.DiscreteVariable, ReinterpretTransform, ndarray) -> Orange.data.Variable
    if isinstance(tr, AsCategorical):
        return var
    elif isinstance(tr, (AsString, AsTime)):
        # TimeVar will be interpreted by StrpTime later
        f = Lookup(var, np.array(var.values, dtype=object), unknown="")
        rvar = Orange.data.StringVariable(name=var.name, compute_value=f)
    elif isinstance(tr, AsContinuous):
        f = Lookup(var, np.array(list(map(parse_float, var.values))),
                   unknown=np.nan)
        rvar = Orange.data.ContinuousVariable(
            name=var.name, compute_value=f, sparse=var.sparse
        )
    else:
        assert False
    return copy_attributes(rvar, var)


@apply_reinterpret.register(Orange.data.ContinuousVariable)
def apply_reinterpret_c(var, tr, data: MArray):
    if isinstance(tr, AsCategorical):
        # This is ill-defined and should not result in a 'compute_value'
        # (post-hoc expunge from the domain once translated?)
        values, index = categorize_unique(data)
        coldata = index.astype(float)
        coldata[index.mask] = np.nan
        tr = LookupMappingTransform(
            var, {v: i for i, v in enumerate(values)}, dtype=np.float64, unknown=np.nan
        )
        values = tuple(as_string(values))
        rvar = Orange.data.DiscreteVariable(
            name=var.name, values=values, compute_value=tr
        )
    elif isinstance(tr, AsContinuous):
        return var
    elif isinstance(tr, AsString):
        # TimeVar will be interpreted by StrpTime later
        tstr = ToStringTransform(var)
        rvar = Orange.data.StringVariable(name=var.name, compute_value=tstr)
    elif isinstance(tr, AsTime):
        rvar = Orange.data.TimeVariable(name=var.name, compute_value=Identity(var))
    else:
        assert False
    return copy_attributes(rvar, var)


@apply_reinterpret.register(Orange.data.StringVariable)
def apply_reinterpret_s(var: Orange.data.StringVariable, tr, data: MArray):
    if isinstance(tr, AsCategorical):
        # This is ill-defined and should not result in a 'compute_value'
        # (post-hoc expunge from the domain once translated?)
        _, values = categorical_from_vector(data)
        mapping = {v: float(i) for i, v in enumerate(values)}
        tr = LookupMappingTransform(var, mapping)
        rvar = Orange.data.DiscreteVariable(
            name=var.name, values=values, compute_value=tr
        )
    elif isinstance(tr, AsContinuous):
        rvar = Orange.data.ContinuousVariable(
            var.name, compute_value=ToContinuousTransform(var)
        )
    elif isinstance(tr, (AsString, AsTime)):
        # TimeVar will be interpreted by StrpTime later
        return var
    else:
        assert False
    return copy_attributes(rvar, var)


@apply_reinterpret.register(Orange.data.TimeVariable)
def apply_reinterpret_t(var: Orange.data.TimeVariable, tr, data):
    if isinstance(tr, AsCategorical):
        values, _ = categorize_unique(data)
        or_values = values.astype(float) / 1e6
        mapping = {v: i for i, v in enumerate(or_values)}
        tr = LookupMappingTransform(var, mapping)
        values = tuple(as_string(values))
        rvar = Orange.data.DiscreteVariable(
            name=var.name, values=values, compute_value=tr
        )
    elif isinstance(tr, AsContinuous):
        rvar = Orange.data.TimeVariable(
            name=var.name, compute_value=Identity(var)
        )
    elif isinstance(tr, AsString):
        rvar = Orange.data.StringVariable(
            name=var.name, compute_value=ToStringTransform(var)
        )
    elif isinstance(tr, AsTime):
        return var
    else:
        assert False
    return copy_attributes(rvar, var)


def orange_isna(variable: Orange.data.Variable, data: ndarray) -> ndarray:
    """
    Return a bool mask masking N/A elements in `data` for the `variable`.
    """
    if variable.is_primitive():
        return np.isnan(data)
    else:
        return data == variable.Unknown


class ToStringTransform(Transformation):
    """
    Transform a variable to string.
    """
    def transform(self, c):
        if self.variable.is_string:
            return c
        elif self.variable.is_discrete or self.variable.is_time:
            r = column_str_repr(self.variable, c)
        elif self.variable.is_continuous:
            r = as_string(c)
        mask = orange_isna(self.variable, c)
        return np.where(mask, "", r)


class ToContinuousTransform(Transformation):
    def transform(self, c):
        if self.variable.is_time:
            return c
        elif self.variable.is_continuous:
            return c
        elif self.variable.is_discrete:
            lookup = Lookup(
                self.variable, as_float_or_nan(self.variable.values),
                unknown=np.nan
            )
            return lookup.transform(c)
        elif self.variable.is_string:
            return as_float_or_nan(c)
        else:
            raise TypeError


def datetime_to_epoch(dti: pd.DatetimeIndex, only_time) -> np.ndarray:
    """Convert datetime to epoch"""
    delta = dti - (dti.normalize() if only_time else pd.Timestamp("1970-01-01"))
    return (delta / pd.Timedelta("1s")).values


class ReparseTimeTransform(Transformation):
    """
    Re-parse the column's string repr as datetime.
    """
    def __init__(self, variable, tr):
        super().__init__(variable)
        self.tr = tr

    def transform(self, c):
        # if self.formats is none guess format option is selected
        formats = self.tr.formats if self.tr.formats is not None else [None]
        for f in formats:
            d = pd.to_datetime(c, errors="coerce", format=f)
            if pd.notnull(d).any():
                return datetime_to_epoch(d, only_time=not self.tr.have_date)
        return np.nan


# Alias for back compatibility (unpickling transforms)
LookupMappingTransform = MappingTransform


@singledispatch
def column_str_repr(var: Orange.data.Variable, coldata: ndarray) -> ndarray:
    """Return a array of str representations of coldata for the `variable."""
    _f = np.frompyfunc(var.repr_val, 1, 1)
    return _f(coldata)


@column_str_repr.register(Orange.data.DiscreteVariable)
def column_str_repr_discrete(
        var: Orange.data.DiscreteVariable, coldata: ndarray
) -> ndarray:
    values = np.array(var.values, dtype=object)
    lookup = Lookup(var, values, "?")
    return lookup.transform(coldata)


@column_str_repr.register(Orange.data.StringVariable)
def column_str_repr_string(
        var: Orange.data.StringVariable, coldata: ndarray
) -> ndarray:
    return np.where(coldata == var.Unknown, "?", coldata)


if __name__ == "__main__":  # pragma: no cover
    WidgetPreview(OWEditDomain).run(Orange.data.Table("iris"))

```
--- END FILE ---

Please first localize the bug based on the issue statement, and then generate *SEARCH/REPLACE* edits to fix the issue.

Every *SEARCH/REPLACE* edit must use this format:
1. The file path
2. The start of search block: <<<<<<< SEARCH
3. A contiguous chunk of lines to search for in the existing source code
4. The dividing line: =======
5. The lines to replace into the source code
6. The end of the replace block: >>>>>>> REPLACE

Here is an example:

```python
### mathweb/flask/app.py
<<<<<<< SEARCH
from flask import Flask
=======
import math
from flask import Flask
>>>>>>> REPLACE
```
</solution>

Please note that the *SEARCH/REPLACE* edit REQUIRES PROPER INDENTATION. If you would like to add the line '        print(x)', you must fully write that out, with all those spaces before the code!
Wrap each *SEARCH/REPLACE* edit in a code block as shown in the example above. If you have multiple *SEARCH/REPLACE* edits, use a separate code block for each one.
user