{
    "file_diffs": [
        {
            "old_file_content": "import os\nfrom itertools import chain\nimport json\n\nimport numpy as np\n\nfrom AnyQt.QtCore import Qt, QSize, QAbstractTableModel, QModelIndex, QTimer, \\\n    QSettings\nfrom AnyQt.QtGui import QColor, QFont, QBrush\nfrom AnyQt.QtWidgets import QHeaderView, QColorDialog, QTableView, QComboBox, \\\n    QFileDialog, QMessageBox\n\nfrom orangewidget.settings import IncompatibleContext\n\nimport Orange\nfrom Orange.preprocess.transformation import Identity\nfrom Orange.util import color_to_hex, hex_to_color\nfrom Orange.widgets import widget, settings, gui\nfrom Orange.widgets.gui import HorizontalGridDelegate\nfrom Orange.widgets.utils import itemmodels, colorpalettes\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.report import colored_square as square\nfrom Orange.widgets.widget import Input, Output\n\nColorRole = next(gui.OrangeUserRole)\nStripRole = next(gui.OrangeUserRole)\n\n\nclass InvalidFileFormat(Exception):\n    pass\n\n\nclass AttrDesc:\n    \"\"\"\n    Describes modifications that will be applied to variable.\n\n    Provides methods that return either the modified value or the original\n\n    Attributes:\n        var (Variable): an instance of variable\n        new_name (str or `None`): a changed name or `None`\n    \"\"\"\n    def __init__(self, var):\n        self.var = var\n        self.new_name = None\n\n    def reset(self):\n        self.new_name = None\n\n    @property\n    def name(self):\n        return self.new_name or self.var.name\n\n    @name.setter\n    def name(self, name):\n        self.new_name = name\n\n    def to_dict(self):\n        d = {}\n        if self.new_name is not None:\n            d[\"rename\"] = self.new_name\n        return d\n\n    @classmethod\n    def from_dict(cls, var, data):\n        desc = cls(var)\n        if not isinstance(data, dict):\n            raise InvalidFileFormat\n        new_name = data.get(\"rename\")\n        if new_name is not None:\n            if not isinstance(new_name, str):\n                raise InvalidFileFormat\n            desc.name = new_name\n        return desc, []\n\n\nclass DiscAttrDesc(AttrDesc):\n    \"\"\"\n    Describes modifications that will be applied to variable.\n\n    Provides methods that return either the modified value or the original\n\n    Attributes:\n        var (DiscreteVariable): an instance of variable\n        name (str or `None`): a changed name or `None`\n        new_colors (list of tuple or None): new colors as tuples (R, G, B)\n        new_values (list of str or None): new names for values, if changed\n    \"\"\"\n    def __init__(self, var):\n        super().__init__(var)\n        self.new_colors = None\n        self.new_values = None\n\n    def reset(self):\n        super().reset()\n        self.new_colors = None\n        self.new_values = None\n\n    @property\n    def colors(self):\n        if self.new_colors is None:\n            return self.var.colors\n        else:\n            return self.new_colors\n\n    def set_color(self, i, color):\n        if self.new_colors is None:\n            self.new_colors = list(self.var.colors)\n        self.new_colors[i] = color\n\n    @property\n    def values(self):\n        return tuple(self.new_values or self.var.values)\n\n    def set_value(self, i, value):\n        if not self.new_values:\n            self.new_values = list(self.var.values)\n        self.new_values[i] = value\n\n    def create_variable(self):\n        new_var = self.var.copy(name=self.name, values=self.values,\n                                compute_value=Identity(self.var))\n        new_var.colors = np.asarray(self.colors)\n        return new_var\n\n    def to_dict(self):\n        d = super().to_dict()\n        if self.new_values is not None:\n            d[\"renamed_values\"] = \\\n                {k: v\n                 for k, v in zip(self.var.values, self.new_values)\n                 if k != v}\n        if self.new_colors is not None:\n            d[\"colors\"] = {\n                value: color_to_hex(color)\n                for value, color in zip(self.var.values, self.colors)}\n        return d\n\n    @classmethod\n    def from_dict(cls, var, data):\n\n        def _check_dict_str_str(d):\n            if not isinstance(d, dict) or \\\n                    not all(isinstance(val, str)\n                            for val in chain(d, d.values())):\n                raise InvalidFileFormat\n\n        obj, warnings = super().from_dict(var, data)\n\n        val_map = data.get(\"renamed_values\")\n        if val_map is not None:\n            _check_dict_str_str(val_map)\n            mapped_values = [val_map.get(value, value) for value in var.values]\n            if len(set(mapped_values)) != len(mapped_values):\n                warnings.append(\n                    f\"{var.name}: \"\n                    \"renaming of values ignored due to duplicate names\")\n            else:\n                obj.new_values = mapped_values\n\n        new_colors = data.get(\"colors\")\n        if new_colors is not None:\n            _check_dict_str_str(new_colors)\n            colors = []\n            for value, def_color in zip(var.values, var.palette.palette):\n                if value in new_colors:\n                    try:\n                        color = hex_to_color(new_colors[value])\n                    except ValueError as exc:\n                        raise InvalidFileFormat from exc\n                    colors.append(color)\n                else:\n                    colors.append(def_color)\n                obj.new_colors = colors\n        return obj, warnings\n\n\nclass ContAttrDesc(AttrDesc):\n    \"\"\"\n    Describes modifications that will be applied to variable.\n\n    Provides methods that return either the modified value or the original\n\n    Attributes:\n        var (ContinuousVariable): an instance of variable\n        name (str or `None`): a changed name or `None`\n        palette_name (str or None): name of palette or None if unmodified\n    \"\"\"\n    def __init__(self, var):\n        super().__init__(var)\n        self.new_palette_name = self._default_palette_name()\n\n    def reset(self):\n        super().reset()\n        self.new_palette_name = self._default_palette_name()\n\n    def _default_palette_name(self):\n        if self.var.palette.name not in colorpalettes.ContinuousPalettes:\n            return colorpalettes.DefaultContinuousPaletteName\n        else:\n            return None\n\n    @property\n    def palette_name(self):\n        return self.new_palette_name or self.var.palette.name\n\n    @palette_name.setter\n    def palette_name(self, palette_name):\n        self.new_palette_name = palette_name\n\n    def create_variable(self):\n        new_var = self.var.copy(name=self.name,\n                                compute_value=Identity(self.var))\n        new_var.attributes[\"palette\"] = self.palette_name\n        return new_var\n\n    def to_dict(self):\n        d = super().to_dict()\n        if self.new_palette_name is not None:\n            d[\"colors\"] = self.palette_name\n        return d\n\n    @classmethod\n    def from_dict(cls, var, data):\n        obj, warnings = super().from_dict(var, data)\n        colors = data.get(\"colors\")\n        if colors is not None:\n            if colors not in colorpalettes.ContinuousPalettes:\n                raise InvalidFileFormat\n            obj.palette_name = colors\n        return obj, warnings\n\n\nclass ColorTableModel(QAbstractTableModel):\n    \"\"\"\n    Base color model for discrete and continuous variables. The model handles:\n    - the first column - variable name (including setData)\n    - flags\n    - row count, computed as len(attrdescs)\n\n    Attribute:\n        attrdescs (list of AttrDesc): attrdescs with user-defined changes\n    \"\"\"\n    def __init__(self):\n        QAbstractTableModel.__init__(self)\n        self.attrdescs = []\n\n    @staticmethod\n    def flags(_):  # pragma: no cover\n        return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable\n\n    def set_data(self, attrdescs):\n        self.modelAboutToBeReset.emit()\n        self.attrdescs = attrdescs\n        self.modelReset.emit()\n\n    def rowCount(self, parent=QModelIndex()):\n        return 0 if parent.isValid() else len(self.attrdescs)\n\n    def data(self, index, role=Qt.DisplayRole):\n        # Only valid for the first column; derived classes implement the rest\n        row = index.row()\n        if role in (Qt.DisplayRole, Qt.EditRole):\n            return self.attrdescs[row].name\n        if role == Qt.FontRole:\n            font = QFont()\n            font.setBold(True)\n            return font\n        if role == Qt.TextAlignmentRole:\n            return Qt.AlignRight | Qt.AlignVCenter\n        return None\n\n    def setData(self, index, value, role):\n        # Only valid for the first column; derived classes implement the rest\n        if role == Qt.EditRole:\n            self.attrdescs[index.row()].name = value\n        else:\n            return False\n        self.dataChanged.emit(index, index)\n        return True\n\n    def reset(self):\n        self.beginResetModel()\n        for desc in self.attrdescs:\n            desc.reset()\n        self.endResetModel()\n\n\nclass DiscColorTableModel(ColorTableModel):\n    \"\"\"\n    A model that stores the colors corresponding to values of discrete\n    variables. Colors are shown as decorations.\n    \"\"\"\n    def columnCount(self, parent=QModelIndex()):\n        if parent.isValid():\n            return 0\n        return 1 + max((len(row.var.values) for row in self.attrdescs),\n                       default=0)\n\n    def data(self, index, role=Qt.DisplayRole):\n        # pylint: disable=too-many-return-statements\n        row, col = index.row(), index.column()\n        if col == 0:\n            return super().data(index, role)\n\n        desc = self.attrdescs[row]\n        if col > len(desc.var.values):\n            return None\n        if role in (Qt.DisplayRole, Qt.EditRole):\n            return desc.values[col - 1]\n\n        color = desc.colors[col - 1]\n        if role == Qt.DecorationRole:\n            return QColor(*color)\n        if role == Qt.ToolTipRole:\n            return color_to_hex(color)\n        if role == ColorRole:\n            return color\n        return None\n\n    def setData(self, index, value, role):\n        row, col = index.row(), index.column()\n        if col == 0:\n            return super().setData(index, value, role)\n\n        desc = self.attrdescs[row]\n        if role == ColorRole:\n            desc.set_color(col - 1, value[:3])\n        elif role == Qt.EditRole:\n            desc.set_value(col - 1, value)\n        else:\n            return False\n        self.dataChanged.emit(index, index)\n        return True\n\n\nclass ContColorTableModel(ColorTableModel):\n    \"\"\"A model that stores the colors corresponding to values of discrete\n    variables. Colors are shown as decorations.\n\n    Attributes:\n        mouse_row (int): the row over which the mouse is hovering\n    \"\"\"\n    def __init__(self):\n        super().__init__()\n        self.mouse_row = None\n\n    def set_mouse_row(self, row):\n        self.mouse_row = row\n\n    @staticmethod\n    def columnCount(parent=QModelIndex()):\n        return 0 if parent.isValid() else 3\n\n    def data(self, index, role=Qt.DisplayRole):\n        def _column0():\n            return ColorTableModel.data(self, index, role)\n\n        def _column1():\n            palette = colorpalettes.ContinuousPalettes[desc.palette_name]\n            if role == Qt.ToolTipRole:\n                return palette.friendly_name\n            if role == ColorRole:\n                return palette\n            if role == StripRole:\n                return palette.color_strip(128, 16)\n            if role == Qt.SizeHintRole:\n                return QSize(150, 16)\n            return None\n\n        def _column2():\n            if role == Qt.SizeHintRole:\n                return QSize(100, 1)\n            if role == Qt.ForegroundRole:\n                return QBrush(Qt.blue)\n            if row == self.mouse_row and role == Qt.DisplayRole:\n                return \"Copy to all\"\n            return None\n\n        row, col = index.row(), index.column()\n        desc = self.attrdescs[row]\n        if 0 <= col <= 2:\n            return [_column0, _column1, _column2][col]()\n\n    # noinspection PyMethodOverriding\n    def setData(self, index, value, role):\n        row, col = index.row(), index.column()\n        if col == 0:\n            return super().setData(index, value, role)\n        if role == ColorRole:\n            self.attrdescs[row].palette_name = value.name\n        else:\n            return False\n        self.dataChanged.emit(index, index)\n        return True\n\n    def copy_to_all(self, index):\n        palette_name = self.attrdescs[index.row()].palette_name\n        for desc in self.attrdescs:\n            desc.palette_name = palette_name\n        self.dataChanged.emit(self.index(0, 1), self.index(self.rowCount(), 1))\n\n\nclass ColorStripDelegate(HorizontalGridDelegate):\n    def __init__(self, view):\n        super().__init__()\n        self.view = view\n\n    def createEditor(self, parent, _, index):\n        class Combo(QComboBox):\n            def __init__(self, parent, initial_data, view):\n                super().__init__(parent)\n                model = itemmodels.ContinuousPalettesModel(icon_width=128)\n                self.setModel(model)\n                self.setCurrentIndex(model.indexOf(initial_data))\n                self.setIconSize(QSize(128, 16))\n                QTimer.singleShot(0, self.showPopup)\n                self.view = view\n\n            def hidePopup(self):\n                super().hidePopup()\n                self.view.closeEditor(self, ColorStripDelegate.NoHint)\n\n        def select(i):\n            self.view.model().setData(\n                index,\n                combo.model().index(i, 0).data(Qt.UserRole),\n                ColorRole)\n\n        combo = Combo(parent, index.data(ColorRole), self.view)\n        combo.currentIndexChanged[int].connect(select)\n        return combo\n\n    def paint(self, painter, option, index):\n        strip = index.data(StripRole)\n        rect = option.rect\n        painter.drawPixmap(\n            rect.x() + 13, rect.y() + (rect.height() - strip.height()) / 2,\n            strip)\n        super().paint(painter, option, index)\n\n\nclass ColorTable(QTableView):\n    \"\"\"\n    The base table view for discrete and continuous attributes.\n\n    Sets the basic properties of the table and implementes mouseRelease that\n    calls handle_click with appropriate index. It also prepares a grid_deleagte\n    that is used in derived classes.\n    \"\"\"\n    def __init__(self, model):\n        QTableView.__init__(self)\n        self.horizontalHeader().hide()\n        self.verticalHeader().hide()\n        self.setShowGrid(False)\n        self.setSelectionMode(QTableView.NoSelection)\n        self.setModel(model)\n        # View doesn't take ownership of delegates, so we store it here\n        self.grid_delegate = HorizontalGridDelegate()\n\n    def mouseReleaseEvent(self, event):\n        index = self.indexAt(event.pos())\n        if not index.isValid():\n            return\n        rect = self.visualRect(index)\n        self.handle_click(index, event.pos().x() - rect.x())\n\n\nclass DiscreteTable(ColorTable):\n    \"\"\"Table view for discrete variables\"\"\"\n    def __init__(self, model):\n        super().__init__(model)\n        self.horizontalHeader().setSectionResizeMode(\n            QHeaderView.ResizeToContents)\n        self.setItemDelegate(self.grid_delegate)\n        self.setEditTriggers(QTableView.NoEditTriggers)\n\n    def handle_click(self, index, x_offset):\n        \"\"\"\n        Handle click events for the first column (call the edit method)\n        and the second (call method for changing the palette)\n        \"\"\"\n        if self.model().data(index, Qt.EditRole) is None:\n            return\n        if index.column() == 0 or x_offset > 24:\n            self.edit(index)\n        else:\n            self.change_color(index)\n\n    def change_color(self, index):\n        \"\"\"Invoke palette editor and set the color\"\"\"\n        color = self.model().data(index, ColorRole)\n        if color is None:\n            return\n        dlg = QColorDialog(QColor(*color))\n        if dlg.exec():\n            color = dlg.selectedColor()\n            self.model().setData(index, color.getRgb(), ColorRole)\n\n\nclass ContinuousTable(ColorTable):\n    \"\"\"Table view for continuous variables\"\"\"\n\n    def __init__(self, model):\n        super().__init__(model)\n        self.viewport().setMouseTracking(True)\n        # View doesn't take ownership of delegates, so we must store it\n        self.color_delegate = ColorStripDelegate(self)\n        self.setItemDelegateForColumn(0, self.grid_delegate)\n        self.setItemDelegateForColumn(1, self.color_delegate)\n        self.setColumnWidth(1, 256)\n        self.setEditTriggers(\n            QTableView.SelectedClicked | QTableView.DoubleClicked)\n\n    def mouseMoveEvent(self, event):\n        \"\"\"Store the hovered row index in the model, trigger viewport update\"\"\"\n        pos = event.pos()\n        ind = self.indexAt(pos)\n        self.model().set_mouse_row(ind.row())\n        super().mouseMoveEvent(event)\n        self.viewport().update()\n\n    def leaveEvent(self, _):\n        \"\"\"Remove the stored the hovered row index, trigger viewport update\"\"\"\n        self.model().set_mouse_row(None)\n        self.viewport().update()\n\n    def handle_click(self, index, _):\n        \"\"\"Call the specific methods for handling clicks for each column\"\"\"\n        if index.column() < 2:\n            self.edit(index)\n        elif index.column() == 2:\n            self.model().copy_to_all(index)\n\n\nclass OWColor(widget.OWWidget):\n    name = \"Color\"\n    description = \"Set color legend for variables.\"\n    icon = \"icons/Colors.svg\"\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        data = Output(\"Data\", Orange.data.Table)\n\n    settingsHandler = settings.PerfectDomainContextHandler(\n        match_values=settings.PerfectDomainContextHandler.MATCH_VALUES_ALL)\n    disc_descs = settings.ContextSetting([])\n    cont_descs = settings.ContextSetting([])\n    selected_schema_index = settings.Setting(0)\n    auto_apply = settings.Setting(True)\n\n    settings_version = 2\n\n    want_main_area = False\n\n    def __init__(self):\n        super().__init__()\n        self.data = None\n        self.orig_domain = self.domain = None\n\n        box = gui.hBox(self.controlArea, \"Discrete Variables\")\n        self.disc_model = DiscColorTableModel()\n        self.disc_view = DiscreteTable(self.disc_model)\n        self.disc_model.dataChanged.connect(self._on_data_changed)\n        box.layout().addWidget(self.disc_view)\n\n        box = gui.hBox(self.controlArea, \"Numeric Variables\")\n        self.cont_model = ContColorTableModel()\n        self.cont_view = ContinuousTable(self.cont_model)\n        self.cont_model.dataChanged.connect(self._on_data_changed)\n        box.layout().addWidget(self.cont_view)\n\n        box = gui.hBox(self.buttonsArea)\n        gui.button(box, self, \"Save\", callback=self.save)\n        gui.button(box, self, \"Load\", callback=self.load)\n        gui.button(box, self, \"Reset\", callback=self.reset)\n        gui.rubber(self.buttonsArea)\n        gui.auto_apply(self.buttonsArea, self, \"auto_apply\")\n\n    @staticmethod\n    def sizeHint():  # pragma: no cover\n        return QSize(500, 570)\n\n    @Inputs.data\n    def set_data(self, data):\n        self.closeContext()\n        self.disc_descs = []\n        self.cont_descs = []\n        if data is None:\n            self.data = self.domain = None\n        else:\n            self.data = data\n            for var in chain(data.domain.variables, data.domain.metas):\n                if var.is_discrete:\n                    self.disc_descs.append(DiscAttrDesc(var))\n                elif var.is_continuous:\n                    self.cont_descs.append(ContAttrDesc(var))\n\n        self.disc_model.set_data(self.disc_descs)\n        self.cont_model.set_data(self.cont_descs)\n        self.openContext(data)\n        self.disc_view.resizeColumnsToContents()\n        self.cont_view.resizeColumnsToContents()\n        self.commit.now()\n\n    def _on_data_changed(self):\n        self.commit.deferred()\n\n    def reset(self):\n        self.disc_model.reset()\n        self.cont_model.reset()\n        # Reset button is in the same box as Load, which has commit.now,\n        # and Apply, hence let Reset commit now, too.\n        self.commit.now()\n\n    def save(self):\n        fname, _ = QFileDialog.getSaveFileName(\n            self, \"File name\", self._start_dir(),\n            \"Variable definitions (*.colors)\")\n        if not fname:\n            return\n        QSettings().setValue(\"colorwidget/last-location\",\n                             os.path.split(fname)[0])\n        self._save_var_defs(fname)\n\n    def _save_var_defs(self, fname):\n        with open(fname, \"w\") as f:\n            json.dump(\n                {vartype: {\n                    var.name: var_data\n                    for var, var_data in (\n                        (desc.var, desc.to_dict()) for desc in repo)\n                    if var_data}\n                 for vartype, repo in ((\"categorical\", self.disc_descs),\n                                       (\"numeric\", self.cont_descs))\n                },\n                f,\n                indent=4)\n\n    def load(self):\n        fname, _ = QFileDialog.getOpenFileName(\n            self, \"File name\", self._start_dir(),\n            \"Variable definitions (*.colors)\")\n        if not fname:\n            return\n\n        try:\n            f = open(fname)\n        except IOError:\n            QMessageBox.critical(self, \"File error\", \"File cannot be opened.\")\n            return\n\n        try:\n            js = json.load(f)  #: dict\n            self._parse_var_defs(js)\n        except (json.JSONDecodeError, InvalidFileFormat):\n            QMessageBox.critical(self, \"File error\", \"Invalid file format.\")\n\n    def _parse_var_defs(self, js):\n        if not isinstance(js, dict) or set(js) != {\"categorical\", \"numeric\"}:\n            raise InvalidFileFormat\n        try:\n            renames = {\n                var_name: desc[\"rename\"]\n                for repo in js.values() for var_name, desc in repo.items()\n                if \"rename\" in desc\n            }\n        # js is an object coming from json file that can be manipulated by\n        # the user, so there are too many things that can go wrong.\n        # Catch all exceptions, therefore.\n        except Exception as exc:\n            raise InvalidFileFormat from exc\n        if not all(isinstance(val, str)\n                   for val in chain(renames, renames.values())):\n            raise InvalidFileFormat\n        renamed_vars = {\n            renames.get(desc.var.name, desc.var.name)\n            for desc in chain(self.disc_descs, self.cont_descs)\n        }\n        if len(renamed_vars) != len(self.disc_descs) + len(self.cont_descs):\n            QMessageBox.warning(\n                self,\n                \"Duplicated variable names\",\n                \"Variables will not be renamed due to duplicated names.\")\n            for repo in js.values():\n                for desc in repo.values():\n                    desc.pop(\"rename\", None)\n\n        # First, construct all descriptions; assign later, after we know\n        # there won't be exceptions due to invalid file format\n        both_descs = []\n        warnings = []\n        for old_desc, repo, desc_type in (\n                (self.disc_descs, \"categorical\", DiscAttrDesc),\n                (self.cont_descs, \"numeric\", ContAttrDesc)):\n            var_by_name = {desc.var.name: desc.var for desc in old_desc}\n            new_descs = {}\n            for var_name, var_data in js[repo].items():\n                var = var_by_name.get(var_name)\n                if var is None:\n                    continue\n                # This can throw InvalidFileFormat\n                new_descs[var_name], warn = desc_type.from_dict(var, var_data)\n                warnings += warn\n            both_descs.append(new_descs)\n\n        self.disc_descs = [both_descs[0].get(desc.var.name, desc)\n                           for desc in self.disc_descs]\n        self.cont_descs = [both_descs[1].get(desc.var.name, desc)\n                           for desc in self.cont_descs]\n        if warnings:\n            QMessageBox.warning(\n                self, \"Invalid definitions\", \"\\n\".join(warnings))\n\n        self.disc_model.set_data(self.disc_descs)\n        self.cont_model.set_data(self.cont_descs)\n        self.commit.now()\n\n    def _start_dir(self):\n        return self.workflowEnv().get(\"basedir\") \\\n               or QSettings().value(\"colorwidget/last-location\") \\\n               or os.path.expanduser(f\"~{os.sep}\")\n\n    @gui.deferred\n    def commit(self):\n        def make(variables):\n            new_vars = []\n            for var in variables:\n                source = disc_dict if var.is_discrete else cont_dict\n                desc = source.get(var.name)\n                new_vars.append(desc.create_variable() if desc else var)\n            return new_vars\n\n        if self.data is None:\n            self.Outputs.data.send(None)\n            return\n\n        disc_dict = {desc.var.name: desc for desc in self.disc_descs}\n        cont_dict = {desc.var.name: desc for desc in self.cont_descs}\n\n        dom = self.data.domain\n        new_domain = Orange.data.Domain(\n            make(dom.attributes), make(dom.class_vars), make(dom.metas))\n        new_data = self.data.transform(new_domain)\n        self.Outputs.data.send(new_data)\n\n    def send_report(self):\n        \"\"\"Send report\"\"\"\n        def _report_variables(variables):\n            def was(n, o):\n                return n if n == o else f\"{n} (was: {o})\"\n\n            max_values = max(\n                (len(var.values) for var in variables if var.is_discrete),\n                default=1)\n\n            rows = \"\"\n            disc_dict = {k.var.name: k for k in self.disc_descs}\n            cont_dict = {k.var.name: k for k in self.cont_descs}\n            for var in variables:\n                if var.is_discrete:\n                    desc = disc_dict[var.name]\n                    value_cols = \"    \\n\".join(\n                        f\"<td>{square(*color)} {was(value, old_value)}</td>\"\n                        for color, value, old_value in\n                        zip(desc.colors, desc.values, var.values))\n                elif var.is_continuous:\n                    desc = cont_dict[var.name]\n                    pal = colorpalettes.ContinuousPalettes[desc.palette_name]\n                    value_cols = f'<td colspan=\"{max_values}\">' \\\n                                 f'{pal.friendly_name}</td>'\n                else:\n                    continue\n                names = was(desc.name, desc.var.name)\n                rows += '<tr style=\"height: 2em\">\\n' \\\n                        f'  <th style=\"text-align: right\">{names}</th>' \\\n                        f'  {value_cols}\\n' \\\n                        '</tr>\\n'\n            return rows\n\n        if not self.data:\n            return\n        dom = self.data.domain\n        sections = (\n            (name, _report_variables(variables))\n            for name, variables in (\n                (\"Features\", dom.attributes),\n                (\"Outcome\" + \"s\" * (len(dom.class_vars) > 1), dom.class_vars),\n                (\"Meta attributes\", dom.metas)))\n        table = \"\".join(f\"<tr><th>{name}</th></tr>{rows}\"\n                        for name, rows in sections if rows)\n        if table:\n            self.report_raw(f\"<table>{table}</table>\")\n\n    @classmethod\n    def migrate_context(cls, _, version):\n        if not version or version < 2:\n            raise IncompatibleContext\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWColor).run(Orange.data.Table(\"heart_disease.tab\"))\n",
            "new_file_content": "import os\nfrom itertools import chain\nimport json\n\nimport numpy as np\n\nfrom AnyQt.QtCore import Qt, QSize, QAbstractTableModel, QModelIndex, QTimer, \\\n    QSettings\nfrom AnyQt.QtGui import QColor, QFont, QBrush\nfrom AnyQt.QtWidgets import QHeaderView, QColorDialog, QTableView, QComboBox, \\\n    QFileDialog, QMessageBox\n\nfrom orangewidget.settings import IncompatibleContext\n\nimport Orange\nfrom Orange.preprocess.transformation import Identity\nfrom Orange.util import color_to_hex, hex_to_color\nfrom Orange.widgets import widget, settings, gui\nfrom Orange.widgets.gui import HorizontalGridDelegate\nfrom Orange.widgets.utils import itemmodels, colorpalettes\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.report import colored_square as square\nfrom Orange.widgets.widget import Input, Output\n\nColorRole = next(gui.OrangeUserRole)\nStripRole = next(gui.OrangeUserRole)\n\n\nclass InvalidFileFormat(Exception):\n    pass\n\n\nclass AttrDesc:\n    \"\"\"\n    Describes modifications that will be applied to variable.\n\n    Provides methods that return either the modified value or the original\n\n    Attributes:\n        var (Variable): an instance of variable\n        new_name (str or `None`): a changed name or `None`\n    \"\"\"\n    def __init__(self, var):\n        self.var = var\n        self.new_name = None\n\n    def reset(self):\n        self.new_name = None\n\n    @property\n    def name(self):\n        return self.new_name or self.var.name\n\n    @name.setter\n    def name(self, name):\n        self.new_name = name\n\n    def to_dict(self):\n        d = {}\n        if self.new_name is not None:\n            d[\"rename\"] = self.new_name\n        return d\n\n    @classmethod\n    def from_dict(cls, var, data):\n        desc = cls(var)\n        if not isinstance(data, dict):\n            raise InvalidFileFormat\n        new_name = data.get(\"rename\")\n        if new_name is not None:\n            if not isinstance(new_name, str):\n                raise InvalidFileFormat\n            desc.name = new_name\n        return desc, []\n\n\nclass DiscAttrDesc(AttrDesc):\n    \"\"\"\n    Describes modifications that will be applied to variable.\n\n    Provides methods that return either the modified value or the original\n\n    Attributes:\n        var (DiscreteVariable): an instance of variable\n        name (str or `None`): a changed name or `None`\n        new_colors (list of tuple or None): new colors as tuples (R, G, B)\n        new_values (list of str or None): new names for values, if changed\n    \"\"\"\n    def __init__(self, var):\n        super().__init__(var)\n        self.new_colors = None\n        self.new_values = None\n\n    def reset(self):\n        super().reset()\n        self.new_colors = None\n        self.new_values = None\n\n    @property\n    def colors(self):\n        if self.new_colors is None:\n            return self.var.colors\n        else:\n            return self.new_colors\n\n    def set_color(self, i, color):\n        if self.new_colors is None:\n            self.new_colors = list(self.var.colors)\n        self.new_colors[i] = color\n\n    @property\n    def values(self):\n        return tuple(self.new_values or self.var.values)\n\n    def set_value(self, i, value):\n        if not self.new_values:\n            self.new_values = list(self.var.values)\n        self.new_values[i] = value\n\n    def create_variable(self):\n        new_var = self.var.copy(name=self.name, values=self.values,\n                                compute_value=Identity(self.var))\n        new_var.colors = np.asarray(self.colors)\n        return new_var\n\n    def to_dict(self):\n        d = super().to_dict()\n        if self.new_values is not None:\n            d[\"renamed_values\"] = \\\n                {k: v\n                 for k, v in zip(self.var.values, self.new_values)\n                 if k != v}\n        if self.new_colors is not None:\n            d[\"colors\"] = {\n                value: color_to_hex(color)\n                for value, color in zip(self.var.values, self.colors)}\n        return d\n\n    @classmethod\n    def from_dict(cls, var, data):\n\n        def _check_dict_str_str(d):\n            if not isinstance(d, dict) or \\\n                    not all(isinstance(val, str)\n                            for val in chain(d, d.values())):\n                raise InvalidFileFormat\n\n        obj, warnings = super().from_dict(var, data)\n\n        val_map = data.get(\"renamed_values\")\n        if val_map is not None:\n            _check_dict_str_str(val_map)\n            mapped_values = [val_map.get(value, value) for value in var.values]\n            if len(set(mapped_values)) != len(mapped_values):\n                warnings.append(\n                    f\"{var.name}: \"\n                    \"renaming of values ignored due to duplicate names\")\n            else:\n                obj.new_values = mapped_values\n\n        new_colors = data.get(\"colors\")\n        if new_colors is not None:\n            _check_dict_str_str(new_colors)\n            colors = []\n            for value, def_color in zip(var.values, var.palette.palette):\n                if value in new_colors:\n                    try:\n                        color = hex_to_color(new_colors[value])\n                    except ValueError as exc:\n                        raise InvalidFileFormat from exc\n                    colors.append(color)\n                else:\n                    colors.append(def_color)\n                obj.new_colors = colors\n        return obj, warnings\n\n\nclass ContAttrDesc(AttrDesc):\n    \"\"\"\n    Describes modifications that will be applied to variable.\n\n    Provides methods that return either the modified value or the original\n\n    Attributes:\n        var (ContinuousVariable): an instance of variable\n        name (str or `None`): a changed name or `None`\n        palette_name (str or None): name of palette or None if unmodified\n    \"\"\"\n    def __init__(self, var):\n        super().__init__(var)\n        self.new_palette_name = self._default_palette_name()\n\n    def reset(self):\n        super().reset()\n        self.new_palette_name = self._default_palette_name()\n\n    def _default_palette_name(self):\n        if self.var.palette.name not in colorpalettes.ContinuousPalettes:\n            return colorpalettes.DefaultContinuousPaletteName\n        else:\n            return None\n\n    @property\n    def palette_name(self):\n        return self.new_palette_name or self.var.palette.name\n\n    @palette_name.setter\n    def palette_name(self, palette_name):\n        self.new_palette_name = palette_name\n\n    def create_variable(self):\n        new_var = self.var.copy(name=self.name,\n                                compute_value=Identity(self.var))\n        new_var.attributes[\"palette\"] = self.palette_name\n        return new_var\n\n    def to_dict(self):\n        d = super().to_dict()\n        if self.new_palette_name is not None:\n            d[\"colors\"] = self.palette_name\n        return d\n\n    @classmethod\n    def from_dict(cls, var, data):\n        obj, warnings = super().from_dict(var, data)\n        colors = data.get(\"colors\")\n        if colors is not None:\n            if colors not in colorpalettes.ContinuousPalettes:\n                raise InvalidFileFormat\n            obj.palette_name = colors\n        return obj, warnings\n\n\nclass ColorTableModel(QAbstractTableModel):\n    \"\"\"\n    Base color model for discrete and continuous variables. The model handles:\n    - the first column - variable name (including setData)\n    - flags\n    - row count, computed as len(attrdescs)\n\n    Attribute:\n        attrdescs (list of AttrDesc): attrdescs with user-defined changes\n    \"\"\"\n    def __init__(self):\n        QAbstractTableModel.__init__(self)\n        self.attrdescs = []\n\n    @staticmethod\n    def flags(_):  # pragma: no cover\n        return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable\n\n    def set_data(self, attrdescs):\n        self.modelAboutToBeReset.emit()\n        self.attrdescs = attrdescs\n        self.modelReset.emit()\n\n    def rowCount(self, parent=QModelIndex()):\n        return 0 if parent.isValid() else len(self.attrdescs)\n\n    def data(self, index, role=Qt.DisplayRole):\n        # Only valid for the first column; derived classes implement the rest\n        row = index.row()\n        if role in (Qt.DisplayRole, Qt.EditRole):\n            return self.attrdescs[row].name\n        if role == Qt.FontRole:\n            font = QFont()\n            font.setBold(True)\n            return font\n        if role == Qt.TextAlignmentRole:\n            return Qt.AlignRight | Qt.AlignVCenter\n        return None\n\n    def setData(self, index, value, role):\n        # Only valid for the first column; derived classes implement the rest\n        if role == Qt.EditRole:\n            self.attrdescs[index.row()].name = value\n        else:\n            return False\n        self.dataChanged.emit(index, index)\n        return True\n\n    def reset(self):\n        self.beginResetModel()\n        for desc in self.attrdescs:\n            desc.reset()\n        self.endResetModel()\n\n\nclass DiscColorTableModel(ColorTableModel):\n    \"\"\"\n    A model that stores the colors corresponding to values of discrete\n    variables. Colors are shown as decorations.\n    \"\"\"\n    def columnCount(self, parent=QModelIndex()):\n        if parent.isValid():\n            return 0\n        return 1 + max((len(row.var.values) for row in self.attrdescs),\n                       default=0)\n\n    def data(self, index, role=Qt.DisplayRole):\n        # pylint: disable=too-many-return-statements\n        row, col = index.row(), index.column()\n        if col == 0:\n            return super().data(index, role)\n\n        desc = self.attrdescs[row]\n        if col > len(desc.var.values):\n            return None\n        if role in (Qt.DisplayRole, Qt.EditRole):\n            return desc.values[col - 1]\n\n        color = desc.colors[col - 1]\n        if role == Qt.DecorationRole:\n            return QColor(*color)\n        if role == Qt.ToolTipRole:\n            return color_to_hex(color)\n        if role == ColorRole:\n            return color\n        return None\n\n    def setData(self, index, value, role):\n        row, col = index.row(), index.column()\n        if col == 0:\n            return super().setData(index, value, role)\n\n        desc = self.attrdescs[row]\n        if role == ColorRole:\n            desc.set_color(col - 1, value[:3])\n        elif role == Qt.EditRole:\n            desc.set_value(col - 1, value)\n        else:\n            return False\n        self.dataChanged.emit(index, index)\n        return True\n\n\nclass ContColorTableModel(ColorTableModel):\n    \"\"\"A model that stores the colors corresponding to values of discrete\n    variables. Colors are shown as decorations.\n\n    Attributes:\n        mouse_row (int): the row over which the mouse is hovering\n    \"\"\"\n    def __init__(self):\n        super().__init__()\n        self.mouse_row = None\n\n    def set_mouse_row(self, row):\n        self.mouse_row = row\n\n    @staticmethod\n    def columnCount(parent=QModelIndex()):\n        return 0 if parent.isValid() else 3\n\n    def data(self, index, role=Qt.DisplayRole):\n        def _column0():\n            return ColorTableModel.data(self, index, role)\n\n        def _column1():\n            palette = colorpalettes.ContinuousPalettes[desc.palette_name]\n            if role == Qt.ToolTipRole:\n                return palette.friendly_name\n            if role == ColorRole:\n                return palette\n            if role == StripRole:\n                return palette.color_strip(128, 16)\n            if role == Qt.SizeHintRole:\n                return QSize(150, 16)\n            return None\n\n        def _column2():\n            if role == Qt.SizeHintRole:\n                return QSize(100, 1)\n            if role == Qt.ForegroundRole:\n                return QBrush(Qt.blue)\n            if row == self.mouse_row and role == Qt.DisplayRole:\n                return \"Copy to all\"\n            return None\n\n        row, col = index.row(), index.column()\n        desc = self.attrdescs[row]\n        if 0 <= col <= 2:\n            return [_column0, _column1, _column2][col]()\n\n    # noinspection PyMethodOverriding\n    def setData(self, index, value, role):\n        row, col = index.row(), index.column()\n        if col == 0:\n            return super().setData(index, value, role)\n        if role == ColorRole:\n            self.attrdescs[row].palette_name = value.name\n        else:\n            return False\n        self.dataChanged.emit(index, index)\n        return True\n\n    def copy_to_all(self, index):\n        palette_name = self.attrdescs[index.row()].palette_name\n        for desc in self.attrdescs:\n            desc.palette_name = palette_name\n        self.dataChanged.emit(self.index(0, 1), self.index(self.rowCount(), 1))\n\n\nclass ColorStripDelegate(HorizontalGridDelegate):\n    def __init__(self, view):\n        super().__init__()\n        self.view = view\n\n    def createEditor(self, parent, _, index):\n        class Combo(QComboBox):\n            def __init__(self, parent, initial_data, view):\n                super().__init__(parent)\n                model = itemmodels.ContinuousPalettesModel(icon_width=128)\n                self.setModel(model)\n                self.setCurrentIndex(model.indexOf(initial_data))\n                self.setIconSize(QSize(128, 16))\n                QTimer.singleShot(0, self.showPopup)\n                self.view = view\n\n            def hidePopup(self):\n                super().hidePopup()\n                self.view.closeEditor(self, ColorStripDelegate.NoHint)\n\n        def select(i):\n            self.view.model().setData(\n                index,\n                combo.model().index(i, 0).data(Qt.UserRole),\n                ColorRole)\n\n        combo = Combo(parent, index.data(ColorRole), self.view)\n        combo.currentIndexChanged[int].connect(select)\n        return combo\n\n    def paint(self, painter, option, index):\n        strip = index.data(StripRole)\n        rect = option.rect\n        painter.drawPixmap(\n            rect.x() + 13, rect.y() + (rect.height() - strip.height()) / 2,\n            strip)\n        super().paint(painter, option, index)\n\n\nclass ColorTable(QTableView):\n    \"\"\"\n    The base table view for discrete and continuous attributes.\n\n    Sets the basic properties of the table and implementes mouseRelease that\n    calls handle_click with appropriate index. It also prepares a grid_deleagte\n    that is used in derived classes.\n    \"\"\"\n    def __init__(self, model):\n        QTableView.__init__(self)\n        self.horizontalHeader().hide()\n        self.verticalHeader().hide()\n        self.setShowGrid(False)\n        self.setSelectionMode(QTableView.NoSelection)\n        self.setModel(model)\n        # View doesn't take ownership of delegates, so we store it here\n        self.grid_delegate = HorizontalGridDelegate()\n\n    def mouseReleaseEvent(self, event):\n        index = self.indexAt(event.pos())\n        if not index.isValid():\n            return\n        rect = self.visualRect(index)\n        self.handle_click(index, event.pos().x() - rect.x())\n\n\nclass DiscreteTable(ColorTable):\n    \"\"\"Table view for discrete variables\"\"\"\n    def __init__(self, model):\n        super().__init__(model)\n        self.horizontalHeader().setSectionResizeMode(\n            QHeaderView.ResizeToContents)\n        self.setItemDelegate(self.grid_delegate)\n        self.setEditTriggers(QTableView.NoEditTriggers)\n\n    def handle_click(self, index, x_offset):\n        \"\"\"\n        Handle click events for the first column (call the edit method)\n        and the second (call method for changing the palette)\n        \"\"\"\n        if self.model().data(index, Qt.EditRole) is None:\n            return\n        if index.column() == 0 or x_offset > 24:\n            self.edit(index)\n        else:\n            self.change_color(index)\n\n    def change_color(self, index):\n        \"\"\"Invoke palette editor and set the color\"\"\"\n        color = self.model().data(index, ColorRole)\n        if color is None:\n            return\n        dlg = QColorDialog(QColor(*color))\n        if dlg.exec():\n            color = dlg.selectedColor()\n            self.model().setData(index, color.getRgb(), ColorRole)\n\n\nclass ContinuousTable(ColorTable):\n    \"\"\"Table view for continuous variables\"\"\"\n\n    def __init__(self, model):\n        super().__init__(model)\n        self.viewport().setMouseTracking(True)\n        # View doesn't take ownership of delegates, so we must store it\n        self.color_delegate = ColorStripDelegate(self)\n        self.setItemDelegateForColumn(0, self.grid_delegate)\n        self.setItemDelegateForColumn(1, self.color_delegate)\n        self.setColumnWidth(1, 256)\n        self.setEditTriggers(\n            QTableView.SelectedClicked | QTableView.DoubleClicked)\n\n    def mouseMoveEvent(self, event):\n        \"\"\"Store the hovered row index in the model, trigger viewport update\"\"\"\n        pos = event.pos()\n        ind = self.indexAt(pos)\n        self.model().set_mouse_row(ind.row())\n        super().mouseMoveEvent(event)\n        self.viewport().update()\n\n    def leaveEvent(self, _):\n        \"\"\"Remove the stored the hovered row index, trigger viewport update\"\"\"\n        self.model().set_mouse_row(None)\n        self.viewport().update()\n\n    def handle_click(self, index, _):\n        \"\"\"Call the specific methods for handling clicks for each column\"\"\"\n        if index.column() < 2:\n            self.edit(index)\n        elif index.column() == 2:\n            self.model().copy_to_all(index)\n\n\nclass OWColor(widget.OWWidget):\n    name = \"Color\"\n    description = \"Set color legend for variables.\"\n    icon = \"icons/Colors.svg\"\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        data = Output(\"Data\", Orange.data.Table)\n\n    settingsHandler = settings.PerfectDomainContextHandler(\n        match_values=settings.PerfectDomainContextHandler.MATCH_VALUES_ALL)\n    disc_descs = settings.ContextSetting([])\n    cont_descs = settings.ContextSetting([])\n    selected_schema_index = settings.Setting(0)\n    auto_apply = settings.Setting(True)\n\n    settings_version = 2\n\n    want_main_area = False\n\n    def __init__(self):\n        super().__init__()\n        self.data = None\n        self.orig_domain = self.domain = None\n\n        box = gui.hBox(self.controlArea, \"Discrete Variables\")\n        self.disc_model = DiscColorTableModel()\n        self.disc_view = DiscreteTable(self.disc_model)\n        self.disc_model.dataChanged.connect(self._on_data_changed)\n        box.layout().addWidget(self.disc_view)\n\n        box = gui.hBox(self.controlArea, \"Numeric Variables\")\n        self.cont_model = ContColorTableModel()\n        self.cont_view = ContinuousTable(self.cont_model)\n        self.cont_model.dataChanged.connect(self._on_data_changed)\n        box.layout().addWidget(self.cont_view)\n\n        box = gui.hBox(self.buttonsArea)\n        gui.button(box, self, \"Save\", callback=self.save)\n        gui.button(box, self, \"Load\", callback=self.load)\n        gui.button(box, self, \"Reset\", callback=self.reset)\n        gui.rubber(self.buttonsArea)\n        gui.auto_apply(self.buttonsArea, self, \"auto_apply\")\n\n    @staticmethod\n    def sizeHint():  # pragma: no cover\n        return QSize(500, 570)\n\n    @Inputs.data\n    def set_data(self, data):\n        self.closeContext()\n        self.disc_descs = []\n        self.cont_descs = []\n        if data is None:\n            self.data = self.domain = None\n        else:\n            self.data = data\n            for var in chain(data.domain.variables, data.domain.metas):\n                if var.is_discrete:\n                    self.disc_descs.append(DiscAttrDesc(var))\n                elif var.is_continuous:\n                    self.cont_descs.append(ContAttrDesc(var))\n\n        self.disc_model.set_data(self.disc_descs)\n        self.cont_model.set_data(self.cont_descs)\n        self.openContext(data)\n        self.disc_view.resizeColumnsToContents()\n        self.cont_view.resizeColumnsToContents()\n        self.commit.now()\n\n    def _on_data_changed(self):\n        self.commit.deferred()\n\n    def reset(self):\n        self.disc_model.reset()\n        self.cont_model.reset()\n        # Reset button is in the same box as Load, which has commit.now,\n        # and Apply, hence let Reset commit now, too.\n        self.commit.now()\n\n    def save(self):\n        fname, _ = QFileDialog.getSaveFileName(\n            self, \"File name\", self._start_dir(),\n            \"Variable definitions (*.colors)\")\n        if not fname:\n            return\n        QSettings().setValue(\"colorwidget/last-location\",\n                             os.path.split(fname)[0])\n        self._save_var_defs(fname)\n\n    def _save_var_defs(self, fname):\n        with open(fname, \"w\") as f:\n            json.dump(\n                {vartype: {\n                    var.name: var_data\n                    for var, var_data in (\n                        (desc.var, desc.to_dict()) for desc in repo)\n                    if var_data}\n                 for vartype, repo in ((\"categorical\", self.disc_descs),\n                                       (\"numeric\", self.cont_descs))\n                },\n                f,\n                indent=4)\n\n    def load(self):\n        fname, _ = QFileDialog.getOpenFileName(\n            self, \"File name\", self._start_dir(),\n            \"Variable definitions (*.colors)\")\n        if not fname:\n            return\n\n        try:\n            f = open(fname)\n        except IOError:\n            QMessageBox.critical(self, \"File error\", \"File cannot be opened.\")\n            return\n\n        try:\n            js = json.load(f)  #: dict\n            self._parse_var_defs(js)\n        except (json.JSONDecodeError, InvalidFileFormat):\n            QMessageBox.critical(self, \"File error\", \"Invalid file format.\")\n\n    def _parse_var_defs(self, js):\n        if not isinstance(js, dict) or set(js) != {\"categorical\", \"numeric\"}:\n            raise InvalidFileFormat\n        try:\n            renames = {\n                var_name: desc[\"rename\"]\n                for repo in js.values() for var_name, desc in repo.items()\n                if \"rename\" in desc\n            }\n        # js is an object coming from json file that can be manipulated by\n        # the user, so there are too many things that can go wrong.\n        # Catch all exceptions, therefore.\n        except Exception as exc:\n            raise InvalidFileFormat from exc\n        if not all(isinstance(val, str)\n                   for val in chain(renames, renames.values())):\n            raise InvalidFileFormat\n        renamed_vars = {\n            renames.get(desc.var.name, desc.var.name)\n            for desc in chain(self.disc_descs, self.cont_descs)\n        }\n        if len(renamed_vars) != len(self.disc_descs) + len(self.cont_descs):\n            QMessageBox.warning(\n                self,\n                \"Duplicated variable names\",\n                \"Variables will not be renamed due to duplicated names.\")\n            for repo in js.values():\n                for desc in repo.values():\n                    desc.pop(\"rename\", None)\n\n        # First, construct all descriptions; assign later, after we know\n        # there won't be exceptions due to invalid file format\n        unused_vars = []\n        both_descs = []\n        warnings = []\n        for old_desc, repo, desc_type in (\n                (self.disc_descs, \"categorical\", DiscAttrDesc),\n                (self.cont_descs, \"numeric\", ContAttrDesc)):\n            var_by_name = {desc.var.name: desc.var for desc in old_desc}\n            new_descs = {}\n            for var_name, var_data in js[repo].items():\n                var = var_by_name.get(var_name)\n                if var is None:\n                    unused_vars.append(var_name)\n                    continue\n                # This can throw InvalidFileFormat\n                new_descs[var_name], warn = desc_type.from_dict(var, var_data)\n                warnings += warn\n            both_descs.append(new_descs)\n        if unused_vars:\n            names = [f\"'{name}'\" for name in unused_vars]\n            if len(unused_vars) == 1:\n                warn = f'Definition for variable {names[0]}, which does not ' \\\n                       f'appear in the data, was ignored.\\n'\n            else:\n                if len(unused_vars) <= 5:\n                    warn = 'Definitions for variables ' \\\n                           f'{\", \".join(names[:-1])} and {names[-1]}'\n                else:\n                    warn = f'Definitions for {\", \".join(names[:4])} ' \\\n                           f'and {len(names) - 4} other variables'\n                warn += \", which do not appear in the data, were ignored.\\n\"\n            warnings.insert(0, warn)\n\n        self.disc_descs = [both_descs[0].get(desc.var.name, desc)\n                           for desc in self.disc_descs]\n        self.cont_descs = [both_descs[1].get(desc.var.name, desc)\n                           for desc in self.cont_descs]\n        if warnings:\n            QMessageBox.warning(\n                self, \"Invalid definitions\", \"\\n\".join(warnings))\n\n        self.disc_model.set_data(self.disc_descs)\n        self.cont_model.set_data(self.cont_descs)\n        self.commit.now()\n\n    def _start_dir(self):\n        return self.workflowEnv().get(\"basedir\") \\\n               or QSettings().value(\"colorwidget/last-location\") \\\n               or os.path.expanduser(f\"~{os.sep}\")\n\n    @gui.deferred\n    def commit(self):\n        def make(variables):\n            new_vars = []\n            for var in variables:\n                source = disc_dict if var.is_discrete else cont_dict\n                desc = source.get(var.name)\n                new_vars.append(desc.create_variable() if desc else var)\n            return new_vars\n\n        if self.data is None:\n            self.Outputs.data.send(None)\n            return\n\n        disc_dict = {desc.var.name: desc for desc in self.disc_descs}\n        cont_dict = {desc.var.name: desc for desc in self.cont_descs}\n\n        dom = self.data.domain\n        new_domain = Orange.data.Domain(\n            make(dom.attributes), make(dom.class_vars), make(dom.metas))\n        new_data = self.data.transform(new_domain)\n        self.Outputs.data.send(new_data)\n\n    def send_report(self):\n        \"\"\"Send report\"\"\"\n        def _report_variables(variables):\n            def was(n, o):\n                return n if n == o else f\"{n} (was: {o})\"\n\n            max_values = max(\n                (len(var.values) for var in variables if var.is_discrete),\n                default=1)\n\n            rows = \"\"\n            disc_dict = {k.var.name: k for k in self.disc_descs}\n            cont_dict = {k.var.name: k for k in self.cont_descs}\n            for var in variables:\n                if var.is_discrete:\n                    desc = disc_dict[var.name]\n                    value_cols = \"    \\n\".join(\n                        f\"<td>{square(*color)} {was(value, old_value)}</td>\"\n                        for color, value, old_value in\n                        zip(desc.colors, desc.values, var.values))\n                elif var.is_continuous:\n                    desc = cont_dict[var.name]\n                    pal = colorpalettes.ContinuousPalettes[desc.palette_name]\n                    value_cols = f'<td colspan=\"{max_values}\">' \\\n                                 f'{pal.friendly_name}</td>'\n                else:\n                    continue\n                names = was(desc.name, desc.var.name)\n                rows += '<tr style=\"height: 2em\">\\n' \\\n                        f'  <th style=\"text-align: right\">{names}</th>' \\\n                        f'  {value_cols}\\n' \\\n                        '</tr>\\n'\n            return rows\n\n        if not self.data:\n            return\n        dom = self.data.domain\n        sections = (\n            (name, _report_variables(variables))\n            for name, variables in (\n                (\"Features\", dom.attributes),\n                (\"Outcome\" + \"s\" * (len(dom.class_vars) > 1), dom.class_vars),\n                (\"Meta attributes\", dom.metas)))\n        table = \"\".join(f\"<tr><th>{name}</th></tr>{rows}\"\n                        for name, rows in sections if rows)\n        if table:\n            self.report_raw(f\"<table>{table}</table>\")\n\n    @classmethod\n    def migrate_context(cls, _, version):\n        if not version or version < 2:\n            raise IncompatibleContext\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWColor).run(Orange.data.Table(\"heart_disease.tab\"))\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/data/owcolor.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "cfd6d5e1b",
                "new_commit_hash": "42fa9480b",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/data/owcolor.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/data/owcolor.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 690,
                            "length": 6
                        },
                        "new_range": {
                            "start": 690,
                            "length": 7
                        },
                        "section": "class OWColor(widget.OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "        # First, construct all descriptions; assign later, after we know",
                                "type": "context"
                            },
                            {
                                "content": "        # there won't be exceptions due to invalid file format",
                                "type": "context"
                            },
                            {
                                "content": "        unused_vars = []",
                                "type": "added"
                            },
                            {
                                "content": "        both_descs = []",
                                "type": "context"
                            },
                            {
                                "content": "        warnings = []",
                                "type": "context"
                            },
                            {
                                "content": "        for old_desc, repo, desc_type in (",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/data/owcolor.py",
                            "type": "class",
                            "name": "OWColor",
                            "content": "class OWColor(widget.OWWidget):\n    name = \"Color\"\n    description = \"Set color legend for variables.\"\n    icon = \"icons/Colors.svg\"\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        data = Output(\"Data\", Orange.data.Table)\n\n    settingsHandler = settings.PerfectDomainContextHandler(\n        match_values=settings.PerfectDomainContextHandler.MATCH_VALUES_ALL)\n    disc_descs = settings.ContextSetting([])\n    cont_descs = settings.ContextSetting([])\n    selected_schema_index = settings.Setting(0)\n    auto_apply = settings.Setting(True)\n\n    settings_version = 2\n\n    want_main_area = False\n\n    def __init__(self):\n        super().__init__()\n        self.data = None\n        self.orig_domain = self.domain = None\n\n        box = gui.hBox(self.controlArea, \"Discrete Variables\")\n        self.disc_model = DiscColorTableModel()\n        self.disc_view = DiscreteTable(self.disc_model)\n        self.disc_model.dataChanged.connect(self._on_data_changed)\n        box.layout().addWidget(self.disc_view)\n\n        box = gui.hBox(self.controlArea, \"Numeric Variables\")\n        self.cont_model = ContColorTableModel()\n        self.cont_view = ContinuousTable(self.cont_model)\n        self.cont_model.dataChanged.connect(self._on_data_changed)\n        box.layout().addWidget(self.cont_view)\n\n        box = gui.hBox(self.buttonsArea)\n        gui.button(box, self, \"Save\", callback=self.save)\n        gui.button(box, self, \"Load\", callback=self.load)\n        gui.button(box, self, \"Reset\", callback=self.reset)\n        gui.rubber(self.buttonsArea)\n        gui.auto_apply(self.buttonsArea, self, \"auto_apply\")\n\n    @staticmethod\n    def sizeHint():  # pragma: no cover\n        return QSize(500, 570)\n\n    @Inputs.data\n    def set_data(self, data):\n        self.closeContext()\n        self.disc_descs = []\n        self.cont_descs = []\n        if data is None:\n            self.data = self.domain = None\n        else:\n            self.data = data\n            for var in chain(data.domain.variables, data.domain.metas):\n                if var.is_discrete:\n                    self.disc_descs.append(DiscAttrDesc(var))\n                elif var.is_continuous:\n                    self.cont_descs.append(ContAttrDesc(var))\n\n        self.disc_model.set_data(self.disc_descs)\n        self.cont_model.set_data(self.cont_descs)\n        self.openContext(data)\n        self.disc_view.resizeColumnsToContents()\n        self.cont_view.resizeColumnsToContents()\n        self.commit.now()\n\n    def _on_data_changed(self):\n        self.commit.deferred()\n\n    def reset(self):\n        self.disc_model.reset()\n        self.cont_model.reset()\n        # Reset button is in the same box as Load, which has commit.now,\n        # and Apply, hence let Reset commit now, too.\n        self.commit.now()\n\n    def save(self):\n        fname, _ = QFileDialog.getSaveFileName(\n            self, \"File name\", self._start_dir(),\n            \"Variable definitions (*.colors)\")\n        if not fname:\n            return\n        QSettings().setValue(\"colorwidget/last-location\",\n                             os.path.split(fname)[0])\n        self._save_var_defs(fname)\n\n    def _save_var_defs(self, fname):\n        with open(fname, \"w\") as f:\n            json.dump(\n                {vartype: {\n                    var.name: var_data\n                    for var, var_data in (\n                        (desc.var, desc.to_dict()) for desc in repo)\n                    if var_data}\n                 for vartype, repo in ((\"categorical\", self.disc_descs),\n                                       (\"numeric\", self.cont_descs))\n                },\n                f,\n                indent=4)\n\n    def load(self):\n        fname, _ = QFileDialog.getOpenFileName(\n            self, \"File name\", self._start_dir(),\n            \"Variable definitions (*.colors)\")\n        if not fname:\n            return\n\n        try:\n            f = open(fname)\n        except IOError:\n            QMessageBox.critical(self, \"File error\", \"File cannot be opened.\")\n            return\n\n        try:\n            js = json.load(f)  #: dict\n            self._parse_var_defs(js)\n        except (json.JSONDecodeError, InvalidFileFormat):\n            QMessageBox.critical(self, \"File error\", \"Invalid file format.\")\n\n    def _parse_var_defs(self, js):\n        if not isinstance(js, dict) or set(js) != {\"categorical\", \"numeric\"}:\n            raise InvalidFileFormat\n        try:\n            renames = {\n                var_name: desc[\"rename\"]\n                for repo in js.values() for var_name, desc in repo.items()\n                if \"rename\" in desc\n            }\n        # js is an object coming from json file that can be manipulated by\n        # the user, so there are too many things that can go wrong.\n        # Catch all exceptions, therefore.\n        except Exception as exc:\n            raise InvalidFileFormat from exc\n        if not all(isinstance(val, str)\n                   for val in chain(renames, renames.values())):\n            raise InvalidFileFormat\n        renamed_vars = {\n            renames.get(desc.var.name, desc.var.name)\n            for desc in chain(self.disc_descs, self.cont_descs)\n        }\n        if len(renamed_vars) != len(self.disc_descs) + len(self.cont_descs):\n            QMessageBox.warning(\n                self,\n                \"Duplicated variable names\",\n                \"Variables will not be renamed due to duplicated names.\")\n            for repo in js.values():\n                for desc in repo.values():\n                    desc.pop(\"rename\", None)\n\n        # First, construct all descriptions; assign later, after we know\n        # there won't be exceptions due to invalid file format\n        unused_vars = []\n        both_descs = []\n        warnings = []\n        for old_desc, repo, desc_type in (\n                (self.disc_descs, \"categorical\", DiscAttrDesc),\n                (self.cont_descs, \"numeric\", ContAttrDesc)):\n            var_by_name = {desc.var.name: desc.var for desc in old_desc}\n            new_descs = {}\n            for var_name, var_data in js[repo].items():\n                var = var_by_name.get(var_name)\n                if var is None:\n                    unused_vars.append(var_name)\n                    continue\n                # This can throw InvalidFileFormat\n                new_descs[var_name], warn = desc_type.from_dict(var, var_data)\n                warnings += warn\n            both_descs.append(new_descs)\n        if unused_vars:\n            names = [f\"'{name}'\" for name in unused_vars]\n            if len(unused_vars) == 1:\n                warn = f'Definition for variable {names[0]}, which does not ' \\\n                       f'appear in the data, was ignored.\\n'\n            else:\n                if len(unused_vars) <= 5:\n                    warn = 'Definitions for variables ' \\\n                           f'{\", \".join(names[:-1])} and {names[-1]}'\n                else:\n                    warn = f'Definitions for {\", \".join(names[:4])} ' \\\n                           f'and {len(names) - 4} other variables'\n                warn += \", which do not appear in the data, were ignored.\\n\"\n            warnings.insert(0, warn)\n\n        self.disc_descs = [both_descs[0].get(desc.var.name, desc)\n                           for desc in self.disc_descs]\n        self.cont_descs = [both_descs[1].get(desc.var.name, desc)\n                           for desc in self.cont_descs]\n        if warnings:\n            QMessageBox.warning(\n                self, \"Invalid definitions\", \"\\n\".join(warnings))\n\n        self.disc_model.set_data(self.disc_descs)\n        self.cont_model.set_data(self.cont_descs)\n        self.commit.now()\n\n    def _start_dir(self):\n        return self.workflowEnv().get(\"basedir\") \\\n               or QSettings().value(\"colorwidget/last-location\") \\\n               or os.path.expanduser(f\"~{os.sep}\")\n\n    @gui.deferred\n    def commit(self):\n        def make(variables):\n            new_vars = []\n            for var in variables:\n                source = disc_dict if var.is_discrete else cont_dict\n                desc = source.get(var.name)\n                new_vars.append(desc.create_variable() if desc else var)\n            return new_vars\n\n        if self.data is None:\n            self.Outputs.data.send(None)\n            return\n\n        disc_dict = {desc.var.name: desc for desc in self.disc_descs}\n        cont_dict = {desc.var.name: desc for desc in self.cont_descs}\n\n        dom = self.data.domain\n        new_domain = Orange.data.Domain(\n            make(dom.attributes), make(dom.class_vars), make(dom.metas))\n        new_data = self.data.transform(new_domain)\n        self.Outputs.data.send(new_data)\n\n    def send_report(self):\n        \"\"\"Send report\"\"\"\n        def _report_variables(variables):\n            def was(n, o):\n                return n if n == o else f\"{n} (was: {o})\"\n\n            max_values = max(\n                (len(var.values) for var in variables if var.is_discrete),\n                default=1)\n\n            rows = \"\"\n            disc_dict = {k.var.name: k for k in self.disc_descs}\n            cont_dict = {k.var.name: k for k in self.cont_descs}\n            for var in variables:\n                if var.is_discrete:\n                    desc = disc_dict[var.name]\n                    value_cols = \"    \\n\".join(\n                        f\"<td>{square(*color)} {was(value, old_value)}</td>\"\n                        for color, value, old_value in\n                        zip(desc.colors, desc.values, var.values))\n                elif var.is_continuous:\n                    desc = cont_dict[var.name]\n                    pal = colorpalettes.ContinuousPalettes[desc.palette_name]\n                    value_cols = f'<td colspan=\"{max_values}\">' \\\n                                 f'{pal.friendly_name}</td>'\n                else:\n                    continue\n                names = was(desc.name, desc.var.name)\n                rows += '<tr style=\"height: 2em\">\\n' \\\n                        f'  <th style=\"text-align: right\">{names}</th>' \\\n                        f'  {value_cols}\\n' \\\n                        '</tr>\\n'\n            return rows\n\n        if not self.data:\n            return\n        dom = self.data.domain\n        sections = (\n            (name, _report_variables(variables))\n            for name, variables in (\n                (\"Features\", dom.attributes),\n                (\"Outcome\" + \"s\" * (len(dom.class_vars) > 1), dom.class_vars),\n                (\"Meta attributes\", dom.metas)))\n        table = \"\".join(f\"<tr><th>{name}</th></tr>{rows}\"\n                        for name, rows in sections if rows)\n        if table:\n            self.report_raw(f\"<table>{table}</table>\")\n\n    @classmethod\n    def migrate_context(cls, _, version):\n        if not version or version < 2:\n            raise IncompatibleContext",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 536,
                            "end_lineno": 816,
                            "parent": null
                        },
                        {
                            "file_name": "Orange/widgets/data/owcolor.py",
                            "type": "method",
                            "name": "OWColor._parse_var_defs",
                            "content": "class OWColor(widget.OWWidget):\n    name = \"Color\"\n    description = \"Set color legend for variables.\"\n    icon = \"icons/Colors.svg\"\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        data = Output(\"Data\", Orange.data.Table)\n\n    settingsHandler = settings.PerfectDomainContextHandler(\n        match_values=settings.PerfectDomainContextHandler.MATCH_VALUES_ALL)\n    disc_descs = settings.ContextSetting([])\n    cont_descs = settings.ContextSetting([])\n    selected_schema_index = settings.Setting(0)\n    auto_apply = settings.Setting(True)\n\n    settings_version = 2\n\n    want_main_area = False\n\n    def __init__(self):\n        super().__init__()\n        self.data = None\n        self.orig_domain = self.domain = None\n\n        box = gui.hBox(self.controlArea, \"Discrete Variables\")\n        self.disc_model = DiscColorTableModel()\n        self.disc_view = DiscreteTable(self.disc_model)\n        self.disc_model.dataChanged.connect(self._on_data_changed)\n        box.layout().addWidget(self.disc_view)\n\n        box = gui.hBox(self.controlArea, \"Numeric Variables\")\n        self.cont_model = ContColorTableModel()\n        self.cont_view = ContinuousTable(self.cont_model)\n        self.cont_model.dataChanged.connect(self._on_data_changed)\n        box.layout().addWidget(self.cont_view)\n\n        box = gui.hBox(self.buttonsArea)\n        gui.button(box, self, \"Save\", callback=self.save)\n        gui.button(box, self, \"Load\", callback=self.load)\n        gui.button(box, self, \"Reset\", callback=self.reset)\n        gui.rubber(self.buttonsArea)\n        gui.auto_apply(self.buttonsArea, self, \"auto_apply\")\n\n    @staticmethod\n    def sizeHint():  # pragma: no cover\n        return QSize(500, 570)\n\n    @Inputs.data\n    def set_data(self, data):\n        self.closeContext()\n        self.disc_descs = []\n        self.cont_descs = []\n        if data is None:\n            self.data = self.domain = None\n        else:\n            self.data = data\n            for var in chain(data.domain.variables, data.domain.metas):\n                if var.is_discrete:\n                    self.disc_descs.append(DiscAttrDesc(var))\n                elif var.is_continuous:\n                    self.cont_descs.append(ContAttrDesc(var))\n\n        self.disc_model.set_data(self.disc_descs)\n        self.cont_model.set_data(self.cont_descs)\n        self.openContext(data)\n        self.disc_view.resizeColumnsToContents()\n        self.cont_view.resizeColumnsToContents()\n        self.commit.now()\n\n    def _on_data_changed(self):\n        self.commit.deferred()\n\n    def reset(self):\n        self.disc_model.reset()\n        self.cont_model.reset()\n        # Reset button is in the same box as Load, which has commit.now,\n        # and Apply, hence let Reset commit now, too.\n        self.commit.now()\n\n    def save(self):\n        fname, _ = QFileDialog.getSaveFileName(\n            self, \"File name\", self._start_dir(),\n            \"Variable definitions (*.colors)\")\n        if not fname:\n            return\n        QSettings().setValue(\"colorwidget/last-location\",\n                             os.path.split(fname)[0])\n        self._save_var_defs(fname)\n\n    def _save_var_defs(self, fname):\n        with open(fname, \"w\") as f:\n            json.dump(\n                {vartype: {\n                    var.name: var_data\n                    for var, var_data in (\n                        (desc.var, desc.to_dict()) for desc in repo)\n                    if var_data}\n                 for vartype, repo in ((\"categorical\", self.disc_descs),\n                                       (\"numeric\", self.cont_descs))\n                },\n                f,\n                indent=4)\n\n    def load(self):\n        fname, _ = QFileDialog.getOpenFileName(\n            self, \"File name\", self._start_dir(),\n            \"Variable definitions (*.colors)\")\n        if not fname:\n            return\n\n        try:\n            f = open(fname)\n        except IOError:\n            QMessageBox.critical(self, \"File error\", \"File cannot be opened.\")\n            return\n\n        try:\n            js = json.load(f)  #: dict\n            self._parse_var_defs(js)\n        except (json.JSONDecodeError, InvalidFileFormat):\n            QMessageBox.critical(self, \"File error\", \"Invalid file format.\")\n\n    def _parse_var_defs(self, js):\n        if not isinstance(js, dict) or set(js) != {\"categorical\", \"numeric\"}:\n            raise InvalidFileFormat\n        try:\n            renames = {\n                var_name: desc[\"rename\"]\n                for repo in js.values() for var_name, desc in repo.items()\n                if \"rename\" in desc\n            }\n        # js is an object coming from json file that can be manipulated by\n        # the user, so there are too many things that can go wrong.\n        # Catch all exceptions, therefore.\n        except Exception as exc:\n            raise InvalidFileFormat from exc\n        if not all(isinstance(val, str)\n                   for val in chain(renames, renames.values())):\n            raise InvalidFileFormat\n        renamed_vars = {\n            renames.get(desc.var.name, desc.var.name)\n            for desc in chain(self.disc_descs, self.cont_descs)\n        }\n        if len(renamed_vars) != len(self.disc_descs) + len(self.cont_descs):\n            QMessageBox.warning(\n                self,\n                \"Duplicated variable names\",\n                \"Variables will not be renamed due to duplicated names.\")\n            for repo in js.values():\n                for desc in repo.values():\n                    desc.pop(\"rename\", None)\n\n        # First, construct all descriptions; assign later, after we know\n        # there won't be exceptions due to invalid file format\n        unused_vars = []\n        both_descs = []\n        warnings = []\n        for old_desc, repo, desc_type in (\n                (self.disc_descs, \"categorical\", DiscAttrDesc),\n                (self.cont_descs, \"numeric\", ContAttrDesc)):\n            var_by_name = {desc.var.name: desc.var for desc in old_desc}\n            new_descs = {}\n            for var_name, var_data in js[repo].items():\n                var = var_by_name.get(var_name)\n                if var is None:\n                    unused_vars.append(var_name)\n                    continue\n                # This can throw InvalidFileFormat\n                new_descs[var_name], warn = desc_type.from_dict(var, var_data)\n                warnings += warn\n            both_descs.append(new_descs)\n        if unused_vars:\n            names = [f\"'{name}'\" for name in unused_vars]\n            if len(unused_vars) == 1:\n                warn = f'Definition for variable {names[0]}, which does not ' \\\n                       f'appear in the data, was ignored.\\n'\n            else:\n                if len(unused_vars) <= 5:\n                    warn = 'Definitions for variables ' \\\n                           f'{\", \".join(names[:-1])} and {names[-1]}'\n                else:\n                    warn = f'Definitions for {\", \".join(names[:4])} ' \\\n                           f'and {len(names) - 4} other variables'\n                warn += \", which do not appear in the data, were ignored.\\n\"\n            warnings.insert(0, warn)\n\n        self.disc_descs = [both_descs[0].get(desc.var.name, desc)\n                           for desc in self.disc_descs]\n        self.cont_descs = [both_descs[1].get(desc.var.name, desc)\n                           for desc in self.cont_descs]\n        if warnings:\n            QMessageBox.warning(\n                self, \"Invalid definitions\", \"\\n\".join(warnings))\n\n        self.disc_model.set_data(self.disc_descs)\n        self.cont_model.set_data(self.cont_descs)\n        self.commit.now()\n\n    def _start_dir(self):\n        return self.workflowEnv().get(\"basedir\") \\\n               or QSettings().value(\"colorwidget/last-location\") \\\n               or os.path.expanduser(f\"~{os.sep}\")\n\n    @gui.deferred\n    def commit(self):\n        def make(variables):\n            new_vars = []\n            for var in variables:\n                source = disc_dict if var.is_discrete else cont_dict\n                desc = source.get(var.name)\n                new_vars.append(desc.create_variable() if desc else var)\n            return new_vars\n\n        if self.data is None:\n            self.Outputs.data.send(None)\n            return\n\n        disc_dict = {desc.var.name: desc for desc in self.disc_descs}\n        cont_dict = {desc.var.name: desc for desc in self.cont_descs}\n\n        dom = self.data.domain\n        new_domain = Orange.data.Domain(\n            make(dom.attributes), make(dom.class_vars), make(dom.metas))\n        new_data = self.data.transform(new_domain)\n        self.Outputs.data.send(new_data)\n\n    def send_report(self):\n        \"\"\"Send report\"\"\"\n        def _report_variables(variables):\n            def was(n, o):\n                return n if n == o else f\"{n} (was: {o})\"\n\n            max_values = max(\n                (len(var.values) for var in variables if var.is_discrete),\n                default=1)\n\n            rows = \"\"\n            disc_dict = {k.var.name: k for k in self.disc_descs}\n            cont_dict = {k.var.name: k for k in self.cont_descs}\n            for var in variables:\n                if var.is_discrete:\n                    desc = disc_dict[var.name]\n                    value_cols = \"    \\n\".join(\n                        f\"<td>{square(*color)} {was(value, old_value)}</td>\"\n                        for color, value, old_value in\n                        zip(desc.colors, desc.values, var.values))\n                elif var.is_continuous:\n                    desc = cont_dict[var.name]\n                    pal = colorpalettes.ContinuousPalettes[desc.palette_name]\n                    value_cols = f'<td colspan=\"{max_values}\">' \\\n                                 f'{pal.friendly_name}</td>'\n                else:\n                    continue\n                names = was(desc.name, desc.var.name)\n                rows += '<tr style=\"height: 2em\">\\n' \\\n                        f'  <th style=\"text-align: right\">{names}</th>' \\\n                        f'  {value_cols}\\n' \\\n                        '</tr>\\n'\n            return rows\n\n        if not self.data:\n            return\n        dom = self.data.domain\n        sections = (\n            (name, _report_variables(variables))\n            for name, variables in (\n                (\"Features\", dom.attributes),\n                (\"Outcome\" + \"s\" * (len(dom.class_vars) > 1), dom.class_vars),\n                (\"Meta attributes\", dom.metas)))\n        table = \"\".join(f\"<tr><th>{name}</th></tr>{rows}\"\n                        for name, rows in sections if rows)\n        if table:\n            self.report_raw(f\"<table>{table}</table>\")\n\n    @classmethod\n    def migrate_context(cls, _, version):\n        if not version or version < 2:\n            raise IncompatibleContext",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 661,
                            "end_lineno": 735,
                            "parent": {
                                "file_name": "Orange/widgets/data/owcolor.py",
                                "type": "class",
                                "name": "OWColor",
                                "content": "class OWColor(widget.OWWidget):\n    name = \"Color\"\n    description = \"Set color legend for variables.\"\n    icon = \"icons/Colors.svg\"\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        data = Output(\"Data\", Orange.data.Table)\n\n    settingsHandler = settings.PerfectDomainContextHandler(\n        match_values=settings.PerfectDomainContextHandler.MATCH_VALUES_ALL)\n    disc_descs = settings.ContextSetting([])\n    cont_descs = settings.ContextSetting([])\n    selected_schema_index = settings.Setting(0)\n    auto_apply = settings.Setting(True)\n\n    settings_version = 2\n\n    want_main_area = False\n\n    def __init__(self):\n        super().__init__()\n        self.data = None\n        self.orig_domain = self.domain = None\n\n        box = gui.hBox(self.controlArea, \"Discrete Variables\")\n        self.disc_model = DiscColorTableModel()\n        self.disc_view = DiscreteTable(self.disc_model)\n        self.disc_model.dataChanged.connect(self._on_data_changed)\n        box.layout().addWidget(self.disc_view)\n\n        box = gui.hBox(self.controlArea, \"Numeric Variables\")\n        self.cont_model = ContColorTableModel()\n        self.cont_view = ContinuousTable(self.cont_model)\n        self.cont_model.dataChanged.connect(self._on_data_changed)\n        box.layout().addWidget(self.cont_view)\n\n        box = gui.hBox(self.buttonsArea)\n        gui.button(box, self, \"Save\", callback=self.save)\n        gui.button(box, self, \"Load\", callback=self.load)\n        gui.button(box, self, \"Reset\", callback=self.reset)\n        gui.rubber(self.buttonsArea)\n        gui.auto_apply(self.buttonsArea, self, \"auto_apply\")\n\n    @staticmethod\n    def sizeHint():  # pragma: no cover\n        return QSize(500, 570)\n\n    @Inputs.data\n    def set_data(self, data):\n        self.closeContext()\n        self.disc_descs = []\n        self.cont_descs = []\n        if data is None:\n            self.data = self.domain = None\n        else:\n            self.data = data\n            for var in chain(data.domain.variables, data.domain.metas):\n                if var.is_discrete:\n                    self.disc_descs.append(DiscAttrDesc(var))\n                elif var.is_continuous:\n                    self.cont_descs.append(ContAttrDesc(var))\n\n        self.disc_model.set_data(self.disc_descs)\n        self.cont_model.set_data(self.cont_descs)\n        self.openContext(data)\n        self.disc_view.resizeColumnsToContents()\n        self.cont_view.resizeColumnsToContents()\n        self.commit.now()\n\n    def _on_data_changed(self):\n        self.commit.deferred()\n\n    def reset(self):\n        self.disc_model.reset()\n        self.cont_model.reset()\n        # Reset button is in the same box as Load, which has commit.now,\n        # and Apply, hence let Reset commit now, too.\n        self.commit.now()\n\n    def save(self):\n        fname, _ = QFileDialog.getSaveFileName(\n            self, \"File name\", self._start_dir(),\n            \"Variable definitions (*.colors)\")\n        if not fname:\n            return\n        QSettings().setValue(\"colorwidget/last-location\",\n                             os.path.split(fname)[0])\n        self._save_var_defs(fname)\n\n    def _save_var_defs(self, fname):\n        with open(fname, \"w\") as f:\n            json.dump(\n                {vartype: {\n                    var.name: var_data\n                    for var, var_data in (\n                        (desc.var, desc.to_dict()) for desc in repo)\n                    if var_data}\n                 for vartype, repo in ((\"categorical\", self.disc_descs),\n                                       (\"numeric\", self.cont_descs))\n                },\n                f,\n                indent=4)\n\n    def load(self):\n        fname, _ = QFileDialog.getOpenFileName(\n            self, \"File name\", self._start_dir(),\n            \"Variable definitions (*.colors)\")\n        if not fname:\n            return\n\n        try:\n            f = open(fname)\n        except IOError:\n            QMessageBox.critical(self, \"File error\", \"File cannot be opened.\")\n            return\n\n        try:\n            js = json.load(f)  #: dict\n            self._parse_var_defs(js)\n        except (json.JSONDecodeError, InvalidFileFormat):\n            QMessageBox.critical(self, \"File error\", \"Invalid file format.\")\n\n    def _parse_var_defs(self, js):\n        if not isinstance(js, dict) or set(js) != {\"categorical\", \"numeric\"}:\n            raise InvalidFileFormat\n        try:\n            renames = {\n                var_name: desc[\"rename\"]\n                for repo in js.values() for var_name, desc in repo.items()\n                if \"rename\" in desc\n            }\n        # js is an object coming from json file that can be manipulated by\n        # the user, so there are too many things that can go wrong.\n        # Catch all exceptions, therefore.\n        except Exception as exc:\n            raise InvalidFileFormat from exc\n        if not all(isinstance(val, str)\n                   for val in chain(renames, renames.values())):\n            raise InvalidFileFormat\n        renamed_vars = {\n            renames.get(desc.var.name, desc.var.name)\n            for desc in chain(self.disc_descs, self.cont_descs)\n        }\n        if len(renamed_vars) != len(self.disc_descs) + len(self.cont_descs):\n            QMessageBox.warning(\n                self,\n                \"Duplicated variable names\",\n                \"Variables will not be renamed due to duplicated names.\")\n            for repo in js.values():\n                for desc in repo.values():\n                    desc.pop(\"rename\", None)\n\n        # First, construct all descriptions; assign later, after we know\n        # there won't be exceptions due to invalid file format\n        unused_vars = []\n        both_descs = []\n        warnings = []\n        for old_desc, repo, desc_type in (\n                (self.disc_descs, \"categorical\", DiscAttrDesc),\n                (self.cont_descs, \"numeric\", ContAttrDesc)):\n            var_by_name = {desc.var.name: desc.var for desc in old_desc}\n            new_descs = {}\n            for var_name, var_data in js[repo].items():\n                var = var_by_name.get(var_name)\n                if var is None:\n                    unused_vars.append(var_name)\n                    continue\n                # This can throw InvalidFileFormat\n                new_descs[var_name], warn = desc_type.from_dict(var, var_data)\n                warnings += warn\n            both_descs.append(new_descs)\n        if unused_vars:\n            names = [f\"'{name}'\" for name in unused_vars]\n            if len(unused_vars) == 1:\n                warn = f'Definition for variable {names[0]}, which does not ' \\\n                       f'appear in the data, was ignored.\\n'\n            else:\n                if len(unused_vars) <= 5:\n                    warn = 'Definitions for variables ' \\\n                           f'{\", \".join(names[:-1])} and {names[-1]}'\n                else:\n                    warn = f'Definitions for {\", \".join(names[:4])} ' \\\n                           f'and {len(names) - 4} other variables'\n                warn += \", which do not appear in the data, were ignored.\\n\"\n            warnings.insert(0, warn)\n\n        self.disc_descs = [both_descs[0].get(desc.var.name, desc)\n                           for desc in self.disc_descs]\n        self.cont_descs = [both_descs[1].get(desc.var.name, desc)\n                           for desc in self.cont_descs]\n        if warnings:\n            QMessageBox.warning(\n                self, \"Invalid definitions\", \"\\n\".join(warnings))\n\n        self.disc_model.set_data(self.disc_descs)\n        self.cont_model.set_data(self.cont_descs)\n        self.commit.now()\n\n    def _start_dir(self):\n        return self.workflowEnv().get(\"basedir\") \\\n               or QSettings().value(\"colorwidget/last-location\") \\\n               or os.path.expanduser(f\"~{os.sep}\")\n\n    @gui.deferred\n    def commit(self):\n        def make(variables):\n            new_vars = []\n            for var in variables:\n                source = disc_dict if var.is_discrete else cont_dict\n                desc = source.get(var.name)\n                new_vars.append(desc.create_variable() if desc else var)\n            return new_vars\n\n        if self.data is None:\n            self.Outputs.data.send(None)\n            return\n\n        disc_dict = {desc.var.name: desc for desc in self.disc_descs}\n        cont_dict = {desc.var.name: desc for desc in self.cont_descs}\n\n        dom = self.data.domain\n        new_domain = Orange.data.Domain(\n            make(dom.attributes), make(dom.class_vars), make(dom.metas))\n        new_data = self.data.transform(new_domain)\n        self.Outputs.data.send(new_data)\n\n    def send_report(self):\n        \"\"\"Send report\"\"\"\n        def _report_variables(variables):\n            def was(n, o):\n                return n if n == o else f\"{n} (was: {o})\"\n\n            max_values = max(\n                (len(var.values) for var in variables if var.is_discrete),\n                default=1)\n\n            rows = \"\"\n            disc_dict = {k.var.name: k for k in self.disc_descs}\n            cont_dict = {k.var.name: k for k in self.cont_descs}\n            for var in variables:\n                if var.is_discrete:\n                    desc = disc_dict[var.name]\n                    value_cols = \"    \\n\".join(\n                        f\"<td>{square(*color)} {was(value, old_value)}</td>\"\n                        for color, value, old_value in\n                        zip(desc.colors, desc.values, var.values))\n                elif var.is_continuous:\n                    desc = cont_dict[var.name]\n                    pal = colorpalettes.ContinuousPalettes[desc.palette_name]\n                    value_cols = f'<td colspan=\"{max_values}\">' \\\n                                 f'{pal.friendly_name}</td>'\n                else:\n                    continue\n                names = was(desc.name, desc.var.name)\n                rows += '<tr style=\"height: 2em\">\\n' \\\n                        f'  <th style=\"text-align: right\">{names}</th>' \\\n                        f'  {value_cols}\\n' \\\n                        '</tr>\\n'\n            return rows\n\n        if not self.data:\n            return\n        dom = self.data.domain\n        sections = (\n            (name, _report_variables(variables))\n            for name, variables in (\n                (\"Features\", dom.attributes),\n                (\"Outcome\" + \"s\" * (len(dom.class_vars) > 1), dom.class_vars),\n                (\"Meta attributes\", dom.metas)))\n        table = \"\".join(f\"<tr><th>{name}</th></tr>{rows}\"\n                        for name, rows in sections if rows)\n        if table:\n            self.report_raw(f\"<table>{table}</table>\")\n\n    @classmethod\n    def migrate_context(cls, _, version):\n        if not version or version < 2:\n            raise IncompatibleContext",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 536,
                                "end_lineno": 816,
                                "parent": null
                            }
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 700,
                            "length": 11
                        },
                        "new_range": {
                            "start": 701,
                            "length": 26
                        },
                        "section": "class OWColor(widget.OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "            for var_name, var_data in js[repo].items():",
                                "type": "context"
                            },
                            {
                                "content": "                var = var_by_name.get(var_name)",
                                "type": "context"
                            },
                            {
                                "content": "                if var is None:",
                                "type": "context"
                            },
                            {
                                "content": "                    unused_vars.append(var_name)",
                                "type": "added"
                            },
                            {
                                "content": "                    continue",
                                "type": "context"
                            },
                            {
                                "content": "                # This can throw InvalidFileFormat",
                                "type": "context"
                            },
                            {
                                "content": "                new_descs[var_name], warn = desc_type.from_dict(var, var_data)",
                                "type": "context"
                            },
                            {
                                "content": "                warnings += warn",
                                "type": "context"
                            },
                            {
                                "content": "            both_descs.append(new_descs)",
                                "type": "context"
                            },
                            {
                                "content": "        if unused_vars:",
                                "type": "added"
                            },
                            {
                                "content": "            names = [f\"'{name}'\" for name in unused_vars]",
                                "type": "added"
                            },
                            {
                                "content": "            if len(unused_vars) == 1:",
                                "type": "added"
                            },
                            {
                                "content": "                warn = f'Definition for variable {names[0]}, which does not ' \\",
                                "type": "added"
                            },
                            {
                                "content": "                       f'appear in the data, was ignored.\\n'",
                                "type": "added"
                            },
                            {
                                "content": "            else:",
                                "type": "added"
                            },
                            {
                                "content": "                if len(unused_vars) <= 5:",
                                "type": "added"
                            },
                            {
                                "content": "                    warn = 'Definitions for variables ' \\",
                                "type": "added"
                            },
                            {
                                "content": "                           f'{\", \".join(names[:-1])} and {names[-1]}'",
                                "type": "added"
                            },
                            {
                                "content": "                else:",
                                "type": "added"
                            },
                            {
                                "content": "                    warn = f'Definitions for {\", \".join(names[:4])} ' \\",
                                "type": "added"
                            },
                            {
                                "content": "                           f'and {len(names) - 4} other variables'",
                                "type": "added"
                            },
                            {
                                "content": "                warn += \", which do not appear in the data, were ignored.\\n\"",
                                "type": "added"
                            },
                            {
                                "content": "            warnings.insert(0, warn)",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "        self.disc_descs = [both_descs[0].get(desc.var.name, desc)",
                                "type": "context"
                            },
                            {
                                "content": "                           for desc in self.disc_descs]",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/data/owcolor.py",
                            "type": "class",
                            "name": "OWColor",
                            "content": "class OWColor(widget.OWWidget):\n    name = \"Color\"\n    description = \"Set color legend for variables.\"\n    icon = \"icons/Colors.svg\"\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        data = Output(\"Data\", Orange.data.Table)\n\n    settingsHandler = settings.PerfectDomainContextHandler(\n        match_values=settings.PerfectDomainContextHandler.MATCH_VALUES_ALL)\n    disc_descs = settings.ContextSetting([])\n    cont_descs = settings.ContextSetting([])\n    selected_schema_index = settings.Setting(0)\n    auto_apply = settings.Setting(True)\n\n    settings_version = 2\n\n    want_main_area = False\n\n    def __init__(self):\n        super().__init__()\n        self.data = None\n        self.orig_domain = self.domain = None\n\n        box = gui.hBox(self.controlArea, \"Discrete Variables\")\n        self.disc_model = DiscColorTableModel()\n        self.disc_view = DiscreteTable(self.disc_model)\n        self.disc_model.dataChanged.connect(self._on_data_changed)\n        box.layout().addWidget(self.disc_view)\n\n        box = gui.hBox(self.controlArea, \"Numeric Variables\")\n        self.cont_model = ContColorTableModel()\n        self.cont_view = ContinuousTable(self.cont_model)\n        self.cont_model.dataChanged.connect(self._on_data_changed)\n        box.layout().addWidget(self.cont_view)\n\n        box = gui.hBox(self.buttonsArea)\n        gui.button(box, self, \"Save\", callback=self.save)\n        gui.button(box, self, \"Load\", callback=self.load)\n        gui.button(box, self, \"Reset\", callback=self.reset)\n        gui.rubber(self.buttonsArea)\n        gui.auto_apply(self.buttonsArea, self, \"auto_apply\")\n\n    @staticmethod\n    def sizeHint():  # pragma: no cover\n        return QSize(500, 570)\n\n    @Inputs.data\n    def set_data(self, data):\n        self.closeContext()\n        self.disc_descs = []\n        self.cont_descs = []\n        if data is None:\n            self.data = self.domain = None\n        else:\n            self.data = data\n            for var in chain(data.domain.variables, data.domain.metas):\n                if var.is_discrete:\n                    self.disc_descs.append(DiscAttrDesc(var))\n                elif var.is_continuous:\n                    self.cont_descs.append(ContAttrDesc(var))\n\n        self.disc_model.set_data(self.disc_descs)\n        self.cont_model.set_data(self.cont_descs)\n        self.openContext(data)\n        self.disc_view.resizeColumnsToContents()\n        self.cont_view.resizeColumnsToContents()\n        self.commit.now()\n\n    def _on_data_changed(self):\n        self.commit.deferred()\n\n    def reset(self):\n        self.disc_model.reset()\n        self.cont_model.reset()\n        # Reset button is in the same box as Load, which has commit.now,\n        # and Apply, hence let Reset commit now, too.\n        self.commit.now()\n\n    def save(self):\n        fname, _ = QFileDialog.getSaveFileName(\n            self, \"File name\", self._start_dir(),\n            \"Variable definitions (*.colors)\")\n        if not fname:\n            return\n        QSettings().setValue(\"colorwidget/last-location\",\n                             os.path.split(fname)[0])\n        self._save_var_defs(fname)\n\n    def _save_var_defs(self, fname):\n        with open(fname, \"w\") as f:\n            json.dump(\n                {vartype: {\n                    var.name: var_data\n                    for var, var_data in (\n                        (desc.var, desc.to_dict()) for desc in repo)\n                    if var_data}\n                 for vartype, repo in ((\"categorical\", self.disc_descs),\n                                       (\"numeric\", self.cont_descs))\n                },\n                f,\n                indent=4)\n\n    def load(self):\n        fname, _ = QFileDialog.getOpenFileName(\n            self, \"File name\", self._start_dir(),\n            \"Variable definitions (*.colors)\")\n        if not fname:\n            return\n\n        try:\n            f = open(fname)\n        except IOError:\n            QMessageBox.critical(self, \"File error\", \"File cannot be opened.\")\n            return\n\n        try:\n            js = json.load(f)  #: dict\n            self._parse_var_defs(js)\n        except (json.JSONDecodeError, InvalidFileFormat):\n            QMessageBox.critical(self, \"File error\", \"Invalid file format.\")\n\n    def _parse_var_defs(self, js):\n        if not isinstance(js, dict) or set(js) != {\"categorical\", \"numeric\"}:\n            raise InvalidFileFormat\n        try:\n            renames = {\n                var_name: desc[\"rename\"]\n                for repo in js.values() for var_name, desc in repo.items()\n                if \"rename\" in desc\n            }\n        # js is an object coming from json file that can be manipulated by\n        # the user, so there are too many things that can go wrong.\n        # Catch all exceptions, therefore.\n        except Exception as exc:\n            raise InvalidFileFormat from exc\n        if not all(isinstance(val, str)\n                   for val in chain(renames, renames.values())):\n            raise InvalidFileFormat\n        renamed_vars = {\n            renames.get(desc.var.name, desc.var.name)\n            for desc in chain(self.disc_descs, self.cont_descs)\n        }\n        if len(renamed_vars) != len(self.disc_descs) + len(self.cont_descs):\n            QMessageBox.warning(\n                self,\n                \"Duplicated variable names\",\n                \"Variables will not be renamed due to duplicated names.\")\n            for repo in js.values():\n                for desc in repo.values():\n                    desc.pop(\"rename\", None)\n\n        # First, construct all descriptions; assign later, after we know\n        # there won't be exceptions due to invalid file format\n        unused_vars = []\n        both_descs = []\n        warnings = []\n        for old_desc, repo, desc_type in (\n                (self.disc_descs, \"categorical\", DiscAttrDesc),\n                (self.cont_descs, \"numeric\", ContAttrDesc)):\n            var_by_name = {desc.var.name: desc.var for desc in old_desc}\n            new_descs = {}\n            for var_name, var_data in js[repo].items():\n                var = var_by_name.get(var_name)\n                if var is None:\n                    unused_vars.append(var_name)\n                    continue\n                # This can throw InvalidFileFormat\n                new_descs[var_name], warn = desc_type.from_dict(var, var_data)\n                warnings += warn\n            both_descs.append(new_descs)\n        if unused_vars:\n            names = [f\"'{name}'\" for name in unused_vars]\n            if len(unused_vars) == 1:\n                warn = f'Definition for variable {names[0]}, which does not ' \\\n                       f'appear in the data, was ignored.\\n'\n            else:\n                if len(unused_vars) <= 5:\n                    warn = 'Definitions for variables ' \\\n                           f'{\", \".join(names[:-1])} and {names[-1]}'\n                else:\n                    warn = f'Definitions for {\", \".join(names[:4])} ' \\\n                           f'and {len(names) - 4} other variables'\n                warn += \", which do not appear in the data, were ignored.\\n\"\n            warnings.insert(0, warn)\n\n        self.disc_descs = [both_descs[0].get(desc.var.name, desc)\n                           for desc in self.disc_descs]\n        self.cont_descs = [both_descs[1].get(desc.var.name, desc)\n                           for desc in self.cont_descs]\n        if warnings:\n            QMessageBox.warning(\n                self, \"Invalid definitions\", \"\\n\".join(warnings))\n\n        self.disc_model.set_data(self.disc_descs)\n        self.cont_model.set_data(self.cont_descs)\n        self.commit.now()\n\n    def _start_dir(self):\n        return self.workflowEnv().get(\"basedir\") \\\n               or QSettings().value(\"colorwidget/last-location\") \\\n               or os.path.expanduser(f\"~{os.sep}\")\n\n    @gui.deferred\n    def commit(self):\n        def make(variables):\n            new_vars = []\n            for var in variables:\n                source = disc_dict if var.is_discrete else cont_dict\n                desc = source.get(var.name)\n                new_vars.append(desc.create_variable() if desc else var)\n            return new_vars\n\n        if self.data is None:\n            self.Outputs.data.send(None)\n            return\n\n        disc_dict = {desc.var.name: desc for desc in self.disc_descs}\n        cont_dict = {desc.var.name: desc for desc in self.cont_descs}\n\n        dom = self.data.domain\n        new_domain = Orange.data.Domain(\n            make(dom.attributes), make(dom.class_vars), make(dom.metas))\n        new_data = self.data.transform(new_domain)\n        self.Outputs.data.send(new_data)\n\n    def send_report(self):\n        \"\"\"Send report\"\"\"\n        def _report_variables(variables):\n            def was(n, o):\n                return n if n == o else f\"{n} (was: {o})\"\n\n            max_values = max(\n                (len(var.values) for var in variables if var.is_discrete),\n                default=1)\n\n            rows = \"\"\n            disc_dict = {k.var.name: k for k in self.disc_descs}\n            cont_dict = {k.var.name: k for k in self.cont_descs}\n            for var in variables:\n                if var.is_discrete:\n                    desc = disc_dict[var.name]\n                    value_cols = \"    \\n\".join(\n                        f\"<td>{square(*color)} {was(value, old_value)}</td>\"\n                        for color, value, old_value in\n                        zip(desc.colors, desc.values, var.values))\n                elif var.is_continuous:\n                    desc = cont_dict[var.name]\n                    pal = colorpalettes.ContinuousPalettes[desc.palette_name]\n                    value_cols = f'<td colspan=\"{max_values}\">' \\\n                                 f'{pal.friendly_name}</td>'\n                else:\n                    continue\n                names = was(desc.name, desc.var.name)\n                rows += '<tr style=\"height: 2em\">\\n' \\\n                        f'  <th style=\"text-align: right\">{names}</th>' \\\n                        f'  {value_cols}\\n' \\\n                        '</tr>\\n'\n            return rows\n\n        if not self.data:\n            return\n        dom = self.data.domain\n        sections = (\n            (name, _report_variables(variables))\n            for name, variables in (\n                (\"Features\", dom.attributes),\n                (\"Outcome\" + \"s\" * (len(dom.class_vars) > 1), dom.class_vars),\n                (\"Meta attributes\", dom.metas)))\n        table = \"\".join(f\"<tr><th>{name}</th></tr>{rows}\"\n                        for name, rows in sections if rows)\n        if table:\n            self.report_raw(f\"<table>{table}</table>\")\n\n    @classmethod\n    def migrate_context(cls, _, version):\n        if not version or version < 2:\n            raise IncompatibleContext",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 536,
                            "end_lineno": 816,
                            "parent": null
                        },
                        {
                            "file_name": "Orange/widgets/data/owcolor.py",
                            "type": "method",
                            "name": "OWColor._parse_var_defs",
                            "content": "class OWColor(widget.OWWidget):\n    name = \"Color\"\n    description = \"Set color legend for variables.\"\n    icon = \"icons/Colors.svg\"\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        data = Output(\"Data\", Orange.data.Table)\n\n    settingsHandler = settings.PerfectDomainContextHandler(\n        match_values=settings.PerfectDomainContextHandler.MATCH_VALUES_ALL)\n    disc_descs = settings.ContextSetting([])\n    cont_descs = settings.ContextSetting([])\n    selected_schema_index = settings.Setting(0)\n    auto_apply = settings.Setting(True)\n\n    settings_version = 2\n\n    want_main_area = False\n\n    def __init__(self):\n        super().__init__()\n        self.data = None\n        self.orig_domain = self.domain = None\n\n        box = gui.hBox(self.controlArea, \"Discrete Variables\")\n        self.disc_model = DiscColorTableModel()\n        self.disc_view = DiscreteTable(self.disc_model)\n        self.disc_model.dataChanged.connect(self._on_data_changed)\n        box.layout().addWidget(self.disc_view)\n\n        box = gui.hBox(self.controlArea, \"Numeric Variables\")\n        self.cont_model = ContColorTableModel()\n        self.cont_view = ContinuousTable(self.cont_model)\n        self.cont_model.dataChanged.connect(self._on_data_changed)\n        box.layout().addWidget(self.cont_view)\n\n        box = gui.hBox(self.buttonsArea)\n        gui.button(box, self, \"Save\", callback=self.save)\n        gui.button(box, self, \"Load\", callback=self.load)\n        gui.button(box, self, \"Reset\", callback=self.reset)\n        gui.rubber(self.buttonsArea)\n        gui.auto_apply(self.buttonsArea, self, \"auto_apply\")\n\n    @staticmethod\n    def sizeHint():  # pragma: no cover\n        return QSize(500, 570)\n\n    @Inputs.data\n    def set_data(self, data):\n        self.closeContext()\n        self.disc_descs = []\n        self.cont_descs = []\n        if data is None:\n            self.data = self.domain = None\n        else:\n            self.data = data\n            for var in chain(data.domain.variables, data.domain.metas):\n                if var.is_discrete:\n                    self.disc_descs.append(DiscAttrDesc(var))\n                elif var.is_continuous:\n                    self.cont_descs.append(ContAttrDesc(var))\n\n        self.disc_model.set_data(self.disc_descs)\n        self.cont_model.set_data(self.cont_descs)\n        self.openContext(data)\n        self.disc_view.resizeColumnsToContents()\n        self.cont_view.resizeColumnsToContents()\n        self.commit.now()\n\n    def _on_data_changed(self):\n        self.commit.deferred()\n\n    def reset(self):\n        self.disc_model.reset()\n        self.cont_model.reset()\n        # Reset button is in the same box as Load, which has commit.now,\n        # and Apply, hence let Reset commit now, too.\n        self.commit.now()\n\n    def save(self):\n        fname, _ = QFileDialog.getSaveFileName(\n            self, \"File name\", self._start_dir(),\n            \"Variable definitions (*.colors)\")\n        if not fname:\n            return\n        QSettings().setValue(\"colorwidget/last-location\",\n                             os.path.split(fname)[0])\n        self._save_var_defs(fname)\n\n    def _save_var_defs(self, fname):\n        with open(fname, \"w\") as f:\n            json.dump(\n                {vartype: {\n                    var.name: var_data\n                    for var, var_data in (\n                        (desc.var, desc.to_dict()) for desc in repo)\n                    if var_data}\n                 for vartype, repo in ((\"categorical\", self.disc_descs),\n                                       (\"numeric\", self.cont_descs))\n                },\n                f,\n                indent=4)\n\n    def load(self):\n        fname, _ = QFileDialog.getOpenFileName(\n            self, \"File name\", self._start_dir(),\n            \"Variable definitions (*.colors)\")\n        if not fname:\n            return\n\n        try:\n            f = open(fname)\n        except IOError:\n            QMessageBox.critical(self, \"File error\", \"File cannot be opened.\")\n            return\n\n        try:\n            js = json.load(f)  #: dict\n            self._parse_var_defs(js)\n        except (json.JSONDecodeError, InvalidFileFormat):\n            QMessageBox.critical(self, \"File error\", \"Invalid file format.\")\n\n    def _parse_var_defs(self, js):\n        if not isinstance(js, dict) or set(js) != {\"categorical\", \"numeric\"}:\n            raise InvalidFileFormat\n        try:\n            renames = {\n                var_name: desc[\"rename\"]\n                for repo in js.values() for var_name, desc in repo.items()\n                if \"rename\" in desc\n            }\n        # js is an object coming from json file that can be manipulated by\n        # the user, so there are too many things that can go wrong.\n        # Catch all exceptions, therefore.\n        except Exception as exc:\n            raise InvalidFileFormat from exc\n        if not all(isinstance(val, str)\n                   for val in chain(renames, renames.values())):\n            raise InvalidFileFormat\n        renamed_vars = {\n            renames.get(desc.var.name, desc.var.name)\n            for desc in chain(self.disc_descs, self.cont_descs)\n        }\n        if len(renamed_vars) != len(self.disc_descs) + len(self.cont_descs):\n            QMessageBox.warning(\n                self,\n                \"Duplicated variable names\",\n                \"Variables will not be renamed due to duplicated names.\")\n            for repo in js.values():\n                for desc in repo.values():\n                    desc.pop(\"rename\", None)\n\n        # First, construct all descriptions; assign later, after we know\n        # there won't be exceptions due to invalid file format\n        unused_vars = []\n        both_descs = []\n        warnings = []\n        for old_desc, repo, desc_type in (\n                (self.disc_descs, \"categorical\", DiscAttrDesc),\n                (self.cont_descs, \"numeric\", ContAttrDesc)):\n            var_by_name = {desc.var.name: desc.var for desc in old_desc}\n            new_descs = {}\n            for var_name, var_data in js[repo].items():\n                var = var_by_name.get(var_name)\n                if var is None:\n                    unused_vars.append(var_name)\n                    continue\n                # This can throw InvalidFileFormat\n                new_descs[var_name], warn = desc_type.from_dict(var, var_data)\n                warnings += warn\n            both_descs.append(new_descs)\n        if unused_vars:\n            names = [f\"'{name}'\" for name in unused_vars]\n            if len(unused_vars) == 1:\n                warn = f'Definition for variable {names[0]}, which does not ' \\\n                       f'appear in the data, was ignored.\\n'\n            else:\n                if len(unused_vars) <= 5:\n                    warn = 'Definitions for variables ' \\\n                           f'{\", \".join(names[:-1])} and {names[-1]}'\n                else:\n                    warn = f'Definitions for {\", \".join(names[:4])} ' \\\n                           f'and {len(names) - 4} other variables'\n                warn += \", which do not appear in the data, were ignored.\\n\"\n            warnings.insert(0, warn)\n\n        self.disc_descs = [both_descs[0].get(desc.var.name, desc)\n                           for desc in self.disc_descs]\n        self.cont_descs = [both_descs[1].get(desc.var.name, desc)\n                           for desc in self.cont_descs]\n        if warnings:\n            QMessageBox.warning(\n                self, \"Invalid definitions\", \"\\n\".join(warnings))\n\n        self.disc_model.set_data(self.disc_descs)\n        self.cont_model.set_data(self.cont_descs)\n        self.commit.now()\n\n    def _start_dir(self):\n        return self.workflowEnv().get(\"basedir\") \\\n               or QSettings().value(\"colorwidget/last-location\") \\\n               or os.path.expanduser(f\"~{os.sep}\")\n\n    @gui.deferred\n    def commit(self):\n        def make(variables):\n            new_vars = []\n            for var in variables:\n                source = disc_dict if var.is_discrete else cont_dict\n                desc = source.get(var.name)\n                new_vars.append(desc.create_variable() if desc else var)\n            return new_vars\n\n        if self.data is None:\n            self.Outputs.data.send(None)\n            return\n\n        disc_dict = {desc.var.name: desc for desc in self.disc_descs}\n        cont_dict = {desc.var.name: desc for desc in self.cont_descs}\n\n        dom = self.data.domain\n        new_domain = Orange.data.Domain(\n            make(dom.attributes), make(dom.class_vars), make(dom.metas))\n        new_data = self.data.transform(new_domain)\n        self.Outputs.data.send(new_data)\n\n    def send_report(self):\n        \"\"\"Send report\"\"\"\n        def _report_variables(variables):\n            def was(n, o):\n                return n if n == o else f\"{n} (was: {o})\"\n\n            max_values = max(\n                (len(var.values) for var in variables if var.is_discrete),\n                default=1)\n\n            rows = \"\"\n            disc_dict = {k.var.name: k for k in self.disc_descs}\n            cont_dict = {k.var.name: k for k in self.cont_descs}\n            for var in variables:\n                if var.is_discrete:\n                    desc = disc_dict[var.name]\n                    value_cols = \"    \\n\".join(\n                        f\"<td>{square(*color)} {was(value, old_value)}</td>\"\n                        for color, value, old_value in\n                        zip(desc.colors, desc.values, var.values))\n                elif var.is_continuous:\n                    desc = cont_dict[var.name]\n                    pal = colorpalettes.ContinuousPalettes[desc.palette_name]\n                    value_cols = f'<td colspan=\"{max_values}\">' \\\n                                 f'{pal.friendly_name}</td>'\n                else:\n                    continue\n                names = was(desc.name, desc.var.name)\n                rows += '<tr style=\"height: 2em\">\\n' \\\n                        f'  <th style=\"text-align: right\">{names}</th>' \\\n                        f'  {value_cols}\\n' \\\n                        '</tr>\\n'\n            return rows\n\n        if not self.data:\n            return\n        dom = self.data.domain\n        sections = (\n            (name, _report_variables(variables))\n            for name, variables in (\n                (\"Features\", dom.attributes),\n                (\"Outcome\" + \"s\" * (len(dom.class_vars) > 1), dom.class_vars),\n                (\"Meta attributes\", dom.metas)))\n        table = \"\".join(f\"<tr><th>{name}</th></tr>{rows}\"\n                        for name, rows in sections if rows)\n        if table:\n            self.report_raw(f\"<table>{table}</table>\")\n\n    @classmethod\n    def migrate_context(cls, _, version):\n        if not version or version < 2:\n            raise IncompatibleContext",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 661,
                            "end_lineno": 735,
                            "parent": {
                                "file_name": "Orange/widgets/data/owcolor.py",
                                "type": "class",
                                "name": "OWColor",
                                "content": "class OWColor(widget.OWWidget):\n    name = \"Color\"\n    description = \"Set color legend for variables.\"\n    icon = \"icons/Colors.svg\"\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        data = Output(\"Data\", Orange.data.Table)\n\n    settingsHandler = settings.PerfectDomainContextHandler(\n        match_values=settings.PerfectDomainContextHandler.MATCH_VALUES_ALL)\n    disc_descs = settings.ContextSetting([])\n    cont_descs = settings.ContextSetting([])\n    selected_schema_index = settings.Setting(0)\n    auto_apply = settings.Setting(True)\n\n    settings_version = 2\n\n    want_main_area = False\n\n    def __init__(self):\n        super().__init__()\n        self.data = None\n        self.orig_domain = self.domain = None\n\n        box = gui.hBox(self.controlArea, \"Discrete Variables\")\n        self.disc_model = DiscColorTableModel()\n        self.disc_view = DiscreteTable(self.disc_model)\n        self.disc_model.dataChanged.connect(self._on_data_changed)\n        box.layout().addWidget(self.disc_view)\n\n        box = gui.hBox(self.controlArea, \"Numeric Variables\")\n        self.cont_model = ContColorTableModel()\n        self.cont_view = ContinuousTable(self.cont_model)\n        self.cont_model.dataChanged.connect(self._on_data_changed)\n        box.layout().addWidget(self.cont_view)\n\n        box = gui.hBox(self.buttonsArea)\n        gui.button(box, self, \"Save\", callback=self.save)\n        gui.button(box, self, \"Load\", callback=self.load)\n        gui.button(box, self, \"Reset\", callback=self.reset)\n        gui.rubber(self.buttonsArea)\n        gui.auto_apply(self.buttonsArea, self, \"auto_apply\")\n\n    @staticmethod\n    def sizeHint():  # pragma: no cover\n        return QSize(500, 570)\n\n    @Inputs.data\n    def set_data(self, data):\n        self.closeContext()\n        self.disc_descs = []\n        self.cont_descs = []\n        if data is None:\n            self.data = self.domain = None\n        else:\n            self.data = data\n            for var in chain(data.domain.variables, data.domain.metas):\n                if var.is_discrete:\n                    self.disc_descs.append(DiscAttrDesc(var))\n                elif var.is_continuous:\n                    self.cont_descs.append(ContAttrDesc(var))\n\n        self.disc_model.set_data(self.disc_descs)\n        self.cont_model.set_data(self.cont_descs)\n        self.openContext(data)\n        self.disc_view.resizeColumnsToContents()\n        self.cont_view.resizeColumnsToContents()\n        self.commit.now()\n\n    def _on_data_changed(self):\n        self.commit.deferred()\n\n    def reset(self):\n        self.disc_model.reset()\n        self.cont_model.reset()\n        # Reset button is in the same box as Load, which has commit.now,\n        # and Apply, hence let Reset commit now, too.\n        self.commit.now()\n\n    def save(self):\n        fname, _ = QFileDialog.getSaveFileName(\n            self, \"File name\", self._start_dir(),\n            \"Variable definitions (*.colors)\")\n        if not fname:\n            return\n        QSettings().setValue(\"colorwidget/last-location\",\n                             os.path.split(fname)[0])\n        self._save_var_defs(fname)\n\n    def _save_var_defs(self, fname):\n        with open(fname, \"w\") as f:\n            json.dump(\n                {vartype: {\n                    var.name: var_data\n                    for var, var_data in (\n                        (desc.var, desc.to_dict()) for desc in repo)\n                    if var_data}\n                 for vartype, repo in ((\"categorical\", self.disc_descs),\n                                       (\"numeric\", self.cont_descs))\n                },\n                f,\n                indent=4)\n\n    def load(self):\n        fname, _ = QFileDialog.getOpenFileName(\n            self, \"File name\", self._start_dir(),\n            \"Variable definitions (*.colors)\")\n        if not fname:\n            return\n\n        try:\n            f = open(fname)\n        except IOError:\n            QMessageBox.critical(self, \"File error\", \"File cannot be opened.\")\n            return\n\n        try:\n            js = json.load(f)  #: dict\n            self._parse_var_defs(js)\n        except (json.JSONDecodeError, InvalidFileFormat):\n            QMessageBox.critical(self, \"File error\", \"Invalid file format.\")\n\n    def _parse_var_defs(self, js):\n        if not isinstance(js, dict) or set(js) != {\"categorical\", \"numeric\"}:\n            raise InvalidFileFormat\n        try:\n            renames = {\n                var_name: desc[\"rename\"]\n                for repo in js.values() for var_name, desc in repo.items()\n                if \"rename\" in desc\n            }\n        # js is an object coming from json file that can be manipulated by\n        # the user, so there are too many things that can go wrong.\n        # Catch all exceptions, therefore.\n        except Exception as exc:\n            raise InvalidFileFormat from exc\n        if not all(isinstance(val, str)\n                   for val in chain(renames, renames.values())):\n            raise InvalidFileFormat\n        renamed_vars = {\n            renames.get(desc.var.name, desc.var.name)\n            for desc in chain(self.disc_descs, self.cont_descs)\n        }\n        if len(renamed_vars) != len(self.disc_descs) + len(self.cont_descs):\n            QMessageBox.warning(\n                self,\n                \"Duplicated variable names\",\n                \"Variables will not be renamed due to duplicated names.\")\n            for repo in js.values():\n                for desc in repo.values():\n                    desc.pop(\"rename\", None)\n\n        # First, construct all descriptions; assign later, after we know\n        # there won't be exceptions due to invalid file format\n        unused_vars = []\n        both_descs = []\n        warnings = []\n        for old_desc, repo, desc_type in (\n                (self.disc_descs, \"categorical\", DiscAttrDesc),\n                (self.cont_descs, \"numeric\", ContAttrDesc)):\n            var_by_name = {desc.var.name: desc.var for desc in old_desc}\n            new_descs = {}\n            for var_name, var_data in js[repo].items():\n                var = var_by_name.get(var_name)\n                if var is None:\n                    unused_vars.append(var_name)\n                    continue\n                # This can throw InvalidFileFormat\n                new_descs[var_name], warn = desc_type.from_dict(var, var_data)\n                warnings += warn\n            both_descs.append(new_descs)\n        if unused_vars:\n            names = [f\"'{name}'\" for name in unused_vars]\n            if len(unused_vars) == 1:\n                warn = f'Definition for variable {names[0]}, which does not ' \\\n                       f'appear in the data, was ignored.\\n'\n            else:\n                if len(unused_vars) <= 5:\n                    warn = 'Definitions for variables ' \\\n                           f'{\", \".join(names[:-1])} and {names[-1]}'\n                else:\n                    warn = f'Definitions for {\", \".join(names[:4])} ' \\\n                           f'and {len(names) - 4} other variables'\n                warn += \", which do not appear in the data, were ignored.\\n\"\n            warnings.insert(0, warn)\n\n        self.disc_descs = [both_descs[0].get(desc.var.name, desc)\n                           for desc in self.disc_descs]\n        self.cont_descs = [both_descs[1].get(desc.var.name, desc)\n                           for desc in self.cont_descs]\n        if warnings:\n            QMessageBox.warning(\n                self, \"Invalid definitions\", \"\\n\".join(warnings))\n\n        self.disc_model.set_data(self.disc_descs)\n        self.cont_model.set_data(self.cont_descs)\n        self.commit.now()\n\n    def _start_dir(self):\n        return self.workflowEnv().get(\"basedir\") \\\n               or QSettings().value(\"colorwidget/last-location\") \\\n               or os.path.expanduser(f\"~{os.sep}\")\n\n    @gui.deferred\n    def commit(self):\n        def make(variables):\n            new_vars = []\n            for var in variables:\n                source = disc_dict if var.is_discrete else cont_dict\n                desc = source.get(var.name)\n                new_vars.append(desc.create_variable() if desc else var)\n            return new_vars\n\n        if self.data is None:\n            self.Outputs.data.send(None)\n            return\n\n        disc_dict = {desc.var.name: desc for desc in self.disc_descs}\n        cont_dict = {desc.var.name: desc for desc in self.cont_descs}\n\n        dom = self.data.domain\n        new_domain = Orange.data.Domain(\n            make(dom.attributes), make(dom.class_vars), make(dom.metas))\n        new_data = self.data.transform(new_domain)\n        self.Outputs.data.send(new_data)\n\n    def send_report(self):\n        \"\"\"Send report\"\"\"\n        def _report_variables(variables):\n            def was(n, o):\n                return n if n == o else f\"{n} (was: {o})\"\n\n            max_values = max(\n                (len(var.values) for var in variables if var.is_discrete),\n                default=1)\n\n            rows = \"\"\n            disc_dict = {k.var.name: k for k in self.disc_descs}\n            cont_dict = {k.var.name: k for k in self.cont_descs}\n            for var in variables:\n                if var.is_discrete:\n                    desc = disc_dict[var.name]\n                    value_cols = \"    \\n\".join(\n                        f\"<td>{square(*color)} {was(value, old_value)}</td>\"\n                        for color, value, old_value in\n                        zip(desc.colors, desc.values, var.values))\n                elif var.is_continuous:\n                    desc = cont_dict[var.name]\n                    pal = colorpalettes.ContinuousPalettes[desc.palette_name]\n                    value_cols = f'<td colspan=\"{max_values}\">' \\\n                                 f'{pal.friendly_name}</td>'\n                else:\n                    continue\n                names = was(desc.name, desc.var.name)\n                rows += '<tr style=\"height: 2em\">\\n' \\\n                        f'  <th style=\"text-align: right\">{names}</th>' \\\n                        f'  {value_cols}\\n' \\\n                        '</tr>\\n'\n            return rows\n\n        if not self.data:\n            return\n        dom = self.data.domain\n        sections = (\n            (name, _report_variables(variables))\n            for name, variables in (\n                (\"Features\", dom.attributes),\n                (\"Outcome\" + \"s\" * (len(dom.class_vars) > 1), dom.class_vars),\n                (\"Meta attributes\", dom.metas)))\n        table = \"\".join(f\"<tr><th>{name}</th></tr>{rows}\"\n                        for name, rows in sections if rows)\n        if table:\n            self.report_raw(f\"<table>{table}</table>\")\n\n    @classmethod\n    def migrate_context(cls, _, version):\n        if not version or version < 2:\n            raise IncompatibleContext",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 536,
                                "end_lineno": 816,
                                "parent": null
                            }
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        },
        {
            "old_file_content": "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring, protected-access,unsubscriptable-object\nimport json\nimport os\nimport tempfile\nimport unittest\nfrom unittest.mock import patch, Mock\n\nimport numpy as np\nfrom AnyQt.QtCore import Qt, QSize, QRect\nfrom AnyQt.QtGui import QBrush\n\nfrom orangewidget.tests.base import GuiTest\n\nfrom Orange.data import Table, ContinuousVariable, DiscreteVariable, Domain\nfrom Orange.preprocess.transformation import Identity\nfrom Orange.util import color_to_hex\nfrom Orange.widgets.utils import colorpalettes\nfrom Orange.widgets.data import owcolor\nfrom Orange.widgets.data.owcolor import ColorRole\nfrom Orange.widgets.tests.base import WidgetTest\n\n\nclass AttrDescTest(unittest.TestCase):\n    def test_name(self):\n        x = ContinuousVariable(\"x\")\n        desc = owcolor.AttrDesc(x)\n        self.assertEqual(desc.name, \"x\")\n        desc.name = \"y\"\n        self.assertEqual(desc.name, \"y\")\n        desc.name = None\n        self.assertEqual(desc.name, \"x\")\n\n    def test_reset(self):\n        x = ContinuousVariable(\"x\")\n        desc = owcolor.AttrDesc(x)\n        desc.reset()\n        self.assertEqual(desc.name, \"x\")\n        desc.name = \"y\"\n        desc.reset()\n        self.assertEqual(desc.name, \"x\")\n\n    def test_to_dict(self):\n        x = ContinuousVariable(\"x\")\n        desc = owcolor.AttrDesc(x)\n        self.assertEqual(desc.to_dict(), {})\n        desc2, warns = owcolor.AttrDesc.from_dict(x, desc.to_dict())\n        self.assertEqual(warns, [])\n        self.assertIsNone(desc2.new_name)\n\n        desc.name = \"y\"\n        self.assertEqual(desc.to_dict(), {\"rename\": \"y\"})\n\n        desc2, warns = owcolor.AttrDesc.from_dict(x, desc.to_dict())\n        self.assertEqual(warns, [])\n        self.assertEqual(desc2.new_name, \"y\")\n\n        self.assertRaises(owcolor.InvalidFileFormat,\n                          owcolor.AttrDesc.from_dict, x, {\"rename\": 42})\n        self.assertRaises(owcolor.InvalidFileFormat,\n                          owcolor.AttrDesc.from_dict, x, [])\n\n        # Additional keys shouldn't cause exceptions\n        owcolor.AttrDesc.from_dict(x, {\"foo\": 42})\n\n\nclass DiscAttrTest(unittest.TestCase):\n    def setUp(self):\n        self.var = DiscreteVariable(\"x\", [\"a\", \"b\", \"c\"])\n        self.desc = owcolor.DiscAttrDesc(self.var)\n\n    def test_colors(self):\n        desc = self.desc\n        colors = desc.colors.copy()\n        desc.set_color(2, (0, 0, 0))\n        colors[2] = 0\n        np.testing.assert_equal(desc.colors, colors)\n\n    def test_values(self):\n        desc = self.desc\n        self.assertEqual(desc.values, (\"a\", \"b\", \"c\"))\n        desc.set_value(1, \"d\")\n        self.assertEqual(desc.values, (\"a\", \"d\", \"c\"))\n\n    def test_create_variable(self):\n        desc = self.desc\n        desc.set_color(0, [1, 2, 3])\n        desc.set_color(1, [4, 5, 6])\n        desc.set_color(2, [7, 8, 9])\n        desc.name = \"z\"\n        desc.set_value(1, \"d\")\n        var = desc.create_variable()\n        self.assertIsInstance(var, DiscreteVariable)\n        self.assertEqual(var.name, \"z\")\n        self.assertEqual(var.values, (\"a\", \"d\", \"c\"))\n        np.testing.assert_equal(var.colors, [[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n        self.assertIsInstance(var.compute_value, Identity)\n        self.assertIs(var.compute_value.variable, desc.var)\n\n        palette = desc.var.attributes[\"palette\"] = object()\n        var = desc.create_variable()\n        self.assertIs(desc.var.attributes[\"palette\"], palette)\n        self.assertFalse(hasattr(var.attributes, \"palette\"))\n        self.assertIsInstance(var.compute_value, Identity)\n        self.assertIs(var.compute_value.variable, desc.var)\n\n    def test_reset(self):\n        desc = self.desc\n        desc.set_color(0, [1, 2, 3])\n        desc.set_color(1, [4, 5, 6])\n        desc.set_color(2, [7, 8, 9])\n        desc.set_value(1, \"d\")\n        desc.reset()\n        np.testing.assert_equal(desc.colors, self.var.colors)\n        self.assertEqual(desc.values, self.var.values)\n\n    def test_to_dict(self):\n        desc = owcolor.DiscAttrDesc(self.var)\n        self.assertEqual(desc.to_dict(), {})\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(self.var, desc.to_dict())\n        self.assertEqual(warns, [])\n        self.assertIsNone(desc2.new_name)\n        self.assertIsNone(desc2.new_values)\n        self.assertIsNone(desc2.new_colors)\n\n        desc.name = \"y\"\n        self.assertEqual(desc.to_dict(), {\"rename\": \"y\"})\n\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(self.var, desc.to_dict())\n        self.assertEqual(warns, [])\n        self.assertEqual(desc2.new_name, \"y\")\n        self.assertIsNone(desc2.new_values)\n        self.assertIsNone(desc2.new_colors)\n\n        desc.set_value(1, \"b2\")\n        desc.set_color(1, [1, 2, 3])\n        desc.set_color(2, [2, 3, 4])\n        self.assertEqual(desc.to_dict(), {\n            \"rename\": \"y\",\n            \"renamed_values\": {\"b\": \"b2\"},\n            \"colors\": {\"a\": color_to_hex(desc.colors[0]),\n                       \"b\": \"#010203\",\n                       \"c\": \"#020304\"}\n        })\n\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(self.var, desc.to_dict())\n        self.assertEqual(warns, [])\n        cols = list(desc.colors)\n        cols[1:] = [[1, 2, 3], [2, 3, 4]]\n        np.testing.assert_equal(desc2.colors, cols)\n        self.assertEqual(desc2.values, (\"a\", \"b2\", \"c\"))\n\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(\n            self.var,\n            {\"rename\": \"y\",\n             \"renamed_values\": {\"b\": \"b2\", \"d\": \"x\"},  # d is redundant\n             \"colors\": {\"b\": \"#010203\",\n                        \"c\": \"#020304\",\n                        \"d\": \"#123456\"}  # d is redundant and must be ignored\n            })\n        self.assertEqual(warns, [])\n        cols = list(desc.colors)\n        cols[1:] = [[1, 2, 3], [2, 3, 4]]\n        np.testing.assert_equal(desc2.colors, cols)\n        self.assertEqual(desc2.values, (\"a\", \"b2\", \"c\"))\n\n    def test_from_dict_coliding_values(self):\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(\n            self.var, {\"renamed_values\": {\"a\": \"b\"}})\n        self.assertEqual(len(warns), 1)\n        self.assertTrue(\"duplicate names\" in warns[0])\n        self.assertIsNone(desc2.new_values)\n        self.assertEqual(desc2.values, (\"a\", \"b\", \"c\"))\n\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(\n            self.var, {\"renamed_values\": {\"a\": \"e\", \"b\": \"e\"}})\n        self.assertEqual(len(warns), 1)\n        self.assertTrue(\"duplicate names\" in warns[0])\n        self.assertIsNone(desc2.new_values)\n        self.assertEqual(desc2.values, (\"a\", \"b\", \"c\"))\n\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(\n            self.var, {\"renamed_values\": {\"a\": \"b\", \"b\": \"a\"}})\n        self.assertEqual(warns, [])\n        self.assertEqual(desc2.values, (\"b\", \"a\", \"c\"))\n\n    def test_from_dict_exceptions(self):\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.AttrDesc.from_dict, self.var, [])\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"rename\": 42}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"colors\": []}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"colors\": {\"a\": 42}}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"colors\": {4: \"#000000\"}}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"colors\": {\"a\": \"#00\"}}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"colors\": {\"a\": \"#qwerty\"}}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"renamed_values\": []}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"renamed_values\": {\"a\": 42}}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"renamed_values\": {4: \"#000000\"}}\n        )\n\n\nclass ContAttrDescTest(unittest.TestCase):\n    def setUp(self):\n        x = ContinuousVariable(\"x\")\n        self.desc = owcolor.ContAttrDesc(x)\n\n    def test_palette(self):\n        desc = self.desc\n        palette = desc.palette_name\n        self.assertIsInstance(palette, str)\n        desc.palette_name = \"foo\"\n        self.assertEqual(desc.palette_name, \"foo\")\n        desc.palette_name = None\n        self.assertEqual(desc.palette_name, palette)\n\n    def test_create_variable(self):\n        desc = self.desc\n        desc.name = \"z\"\n        palette_name = _find_other_palette(\n            colorpalettes.ContinuousPalettes[desc.palette_name]).name\n        desc.palette_name = palette_name\n        var = desc.create_variable()\n        self.assertIsInstance(var, ContinuousVariable)\n        self.assertEqual(var.name, \"z\")\n        self.assertEqual(var.palette.name, palette_name)\n        self.assertIsInstance(var.compute_value, Identity)\n        self.assertIs(var.compute_value.variable, desc.var)\n\n        colors = desc.var.attributes[\"colors\"] = object()\n        var = desc.create_variable()\n        self.assertIs(desc.var.attributes[\"colors\"], colors)\n        self.assertFalse(hasattr(var.attributes, \"colors\"))\n        self.assertIsInstance(var.compute_value, Identity)\n        self.assertIs(var.compute_value.variable, desc.var)\n\n    def test_reset(self):\n        desc = self.desc\n        palette_name = desc.palette_name\n        desc.palette_name = _find_other_palette(\n            colorpalettes.ContinuousPalettes[palette_name]).name\n        desc.reset()\n        np.testing.assert_equal(desc.palette_name, palette_name)\n\n    def test_to_dict(self):\n        x = ContinuousVariable(\"x\")\n        desc = owcolor.ContAttrDesc(x)\n        self.assertEqual(desc.to_dict(), {})\n        desc2, warns = owcolor.ContAttrDesc.from_dict(x, desc.to_dict())\n        self.assertEqual(warns, [])\n        self.assertIsNone(desc2.new_name)\n        self.assertIsNone(desc2.new_palette_name)\n\n        desc.name = \"y\"\n        self.assertEqual(desc.to_dict(), {\"rename\": \"y\"})\n\n        desc2, warns = owcolor.ContAttrDesc.from_dict(x, desc.to_dict())\n        self.assertEqual(warns, [])\n        self.assertEqual(desc2.new_name, \"y\")\n        self.assertIsNone(desc2.new_palette_name)\n\n        desc = owcolor.ContAttrDesc(x)\n        desc.palette_name = \"linear_viridis\"\n        self.assertEqual(desc.to_dict(), {\"colors\": \"linear_viridis\"})\n\n    def test_from_dict_exceptions(self):\n        x = ContinuousVariable(\"x\")\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.ContAttrDesc.from_dict, x, []\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.ContAttrDesc.from_dict, x, {\"colors\": 42}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.ContAttrDesc.from_dict, x, {\"colors\": \"no such palette\"}\n        )\n\n\nclass BaseTestColorTableModel:\n    def test_row_count(self):\n        model = self.model\n\n        self.assertEqual(model.rowCount(), 0)\n        model.set_data(self.descs)\n        self.assertEqual(model.rowCount(), len(self.descs))\n        self.assertEqual(model.rowCount(self.model.index(0, 0)), 0)\n\n    def test_data(self):\n        self.model.set_data(self.descs)\n        data = self.model.data\n\n        index = self.model.index(1, 0)\n        self.assertEqual(data(index, Qt.DisplayRole), self.descs[1].name)\n        self.assertEqual(data(index, Qt.EditRole), self.descs[1].name)\n        self.assertTrue(data(index, Qt.FontRole).bold())\n        self.assertTrue(data(index, Qt.TextAlignmentRole) & Qt.AlignRight)\n\n        self.descs[1].name = \"bar\"\n        self.assertEqual(data(index), \"bar\")\n\n        index = self.model.index(2, 0)\n        self.assertEqual(data(index, Qt.DisplayRole), self.descs[2].name)\n\n    def test_set_data(self):\n        emit = Mock()\n        try:\n            self.model.dataChanged.connect(emit)\n            self.model.set_data(self.descs)\n            data = self.model.data\n            setData = self.model.setData\n\n            index = self.model.index(1, 0)\n            assert self.descs[1].name != \"foo\"\n            self.assertFalse(setData(index, \"foo\", Qt.DisplayRole))\n            emit.assert_not_called()\n            self.assertEqual(data(index, Qt.DisplayRole), self.descs[1].name)\n            self.assertTrue(setData(index, \"foo\", Qt.EditRole))\n            emit.assert_called()\n            self.assertEqual(data(index, Qt.DisplayRole), \"foo\")\n            self.assertEqual(self.descs[1].name, \"foo\")\n        finally:\n            self.model.dataChanged.disconnect(emit)\n\n    def test_reset(self):\n        for desc in self.descs:\n            desc.reset = Mock()\n        self.model.set_data(self.descs)\n        self.model.reset()\n        for desc in self.descs:\n            desc.reset.assert_called()\n\n\nclass TestDiscColorTableModel(GuiTest, BaseTestColorTableModel):\n    def setUp(self):\n        x = DiscreteVariable(\"x\", list(\"abc\"))\n        y = DiscreteVariable(\"y\", list(\"def\"))\n        z = DiscreteVariable(\"z\", list(\"ghijk\"))\n        self.descs = [owcolor.DiscAttrDesc(v) for v in (x, y, z)]\n        self.model = owcolor.DiscColorTableModel()\n\n    def test_column_count(self):\n        model = self.model\n\n        self.assertEqual(model.columnCount(), 1)\n        model.set_data(self.descs[:2])\n        self.assertEqual(model.columnCount(), 4)\n        model.set_data(self.descs)\n        self.assertEqual(model.columnCount(), 6)\n\n        self.assertEqual(model.columnCount(model.index(0, 0)), 0)\n\n    def test_data(self):\n        super().test_data()\n\n        model = self.model\n\n        self.assertIsNone(model.data(model.index(0, 4)))\n\n        index = model.index(1, 2)\n        self.assertEqual(model.data(index, Qt.DisplayRole), \"e\")\n        self.assertEqual(model.data(index, Qt.EditRole), \"e\")\n        font = model.data(index, Qt.FontRole)\n        self.assertTrue(font is None or not font.bold())\n\n        var_colors = self.descs[1].var.colors[1]\n        color = model.data(index, Qt.DecorationRole)\n        np.testing.assert_equal(color.getRgb()[:3], var_colors)\n\n        color = model.data(index, owcolor.ColorRole)\n        np.testing.assert_equal(color, var_colors)\n\n        self.assertEqual(\n            model.data(index, Qt.ToolTipRole), color_to_hex(var_colors))\n\n        self.assertIsNone(model.data(model.index(0, 4)))\n\n        index = model.index(2, 5)\n        self.assertEqual(model.data(index, Qt.DisplayRole), \"k\")\n        self.assertEqual(model.data(index, Qt.EditRole), \"k\")\n        font = model.data(index, Qt.FontRole)\n        self.assertTrue(font is None or not font.bold())\n\n        var_colors = self.descs[2].var.colors[4]\n        color = model.data(index, Qt.DecorationRole)\n        np.testing.assert_equal(color.getRgb()[:3], var_colors)\n\n        color = model.data(index, owcolor.ColorRole)\n        np.testing.assert_equal(color, var_colors)\n\n        self.assertEqual(\n            model.data(index, Qt.ToolTipRole), color_to_hex(var_colors))\n\n        self.descs[2].set_value(4, \"foo\")\n        self.assertEqual(model.data(index, Qt.DisplayRole), \"foo\")\n\n    def test_set_data(self):\n        super().test_set_data()\n\n        model = self.model\n        emit = Mock()\n        try:\n            model.dataChanged.connect(emit)\n\n            index = model.index(2, 5)\n\n            self.assertEqual(model.data(index, Qt.DisplayRole), \"k\")\n            self.assertEqual(model.data(index, Qt.EditRole), \"k\")\n            self.assertFalse(model.setData(index, \"foo\", Qt.DisplayRole))\n            emit.assert_not_called()\n            self.assertEqual(model.data(index, Qt.DisplayRole), \"k\")\n            self.assertTrue(model.setData(index, \"foo\", Qt.EditRole))\n            emit.assert_called()\n            emit.reset_mock()\n            self.assertEqual(model.data(index, Qt.DisplayRole), \"foo\")\n            self.assertEqual(self.descs[2].values, (\"g\", \"h\", \"i\", \"j\", \"foo\"))\n\n            new_color = [0, 1, 2]\n            self.assertTrue(model.setData(index, new_color + [255], ColorRole))\n            emit.assert_called()\n            emit.reset_mock()\n            color = model.data(index, Qt.DecorationRole)\n            rgb = [color.red(), color.green(), color.blue()]\n            self.assertEqual(rgb, new_color)\n\n            color = model.data(index, owcolor.ColorRole)\n            self.assertEqual(list(color), new_color)\n\n            self.assertEqual(\n                model.data(index, Qt.ToolTipRole), color_to_hex(new_color))\n\n            np.testing.assert_equal(self.descs[2].colors[4], rgb)\n        finally:\n            model.dataChanged.disconnect(emit)\n\n\ndef _find_other_palette(initial):\n    for palette in colorpalettes.ContinuousPalettes.values():\n        if palette.name != initial.name:\n            return palette\n    return None  # pragma: no cover\n\n\nclass TestContColorTableModel(GuiTest, BaseTestColorTableModel):\n    def setUp(self):\n        z = ContinuousVariable(\"z\")\n        w = ContinuousVariable(\"w\")\n        u = ContinuousVariable(\"u\")\n        self.descs = [owcolor.ContAttrDesc(v) for v in (z, w, u)]\n        self.model = owcolor.ContColorTableModel()\n\n    def test_column_count(self):\n        model = self.model\n\n        model.set_data(self.descs)\n        self.assertEqual(model.columnCount(), 3)\n        self.assertEqual(model.columnCount(model.index(0, 0)), 0)\n\n    def test_data(self):\n        super().test_data()\n\n        model = self.model\n        index = model.index(1, 1)\n        palette = colorpalettes.ContinuousPalettes[self.descs[1].palette_name]\n        self.assertEqual(model.data(index, Qt.ToolTipRole),\n                         palette.friendly_name)\n        self.assertEqual(model.data(index, ColorRole), palette)\n        with patch.object(palette, \"color_strip\") as color_strip:\n            strip = model.data(index, owcolor.StripRole)\n            self.assertIs(strip, color_strip.return_value)\n            color_strip.assert_called_with(128, 16)\n        self.assertIsInstance(model.data(index, Qt.SizeHintRole), QSize)\n        self.assertIsNone(model.data(index, Qt.FontRole))\n\n        palette = _find_other_palette(self.descs[1])\n        self.descs[1].palette_name = palette.name\n        self.assertIs(model.data(index, ColorRole), palette)\n\n        index = self.model.index(1, 2)\n        self.assertIsNone(model.data(index, Qt.ToolTipRole))\n        self.assertIsInstance(model.data(index, Qt.SizeHintRole), QSize)\n        self.assertIsInstance(model.data(index, Qt.ForegroundRole), QBrush)\n        self.assertIsNone(model.data(index, Qt.DisplayRole))\n        model.set_mouse_row(0)\n        self.assertIsNone(model.data(index, Qt.DisplayRole))\n        model.set_mouse_row(1)\n        self.assertEqual(model.data(index, Qt.DisplayRole), \"Copy to all\")\n\n    def test_set_data(self):\n        super().test_set_data()\n\n        model = self.model\n        index = model.index(1, 1)\n        index2 = model.index(2, 1)\n        initial = model.data(index, ColorRole)\n        initial2 = model.data(index, ColorRole)\n        assert initial.name == initial2.name\n        palette = _find_other_palette(initial)\n\n        emit = Mock()\n        try:\n            model.dataChanged.connect(emit)\n\n            self.assertFalse(model.setData(index, None, Qt.DisplayRole))\n            emit.assert_not_called()\n\n            self.assertTrue(model.setData(index, palette, ColorRole))\n            emit.assert_called()\n            self.assertIs(model.data(index2, ColorRole), initial2)\n\n            self.assertEqual(model.data(index, Qt.ToolTipRole),\n                             palette.friendly_name)\n            self.assertEqual(model.data(index, ColorRole), palette)\n            self.assertEqual(self.descs[1].palette_name, palette.name)\n            with patch.object(palette, \"color_strip\") as color_strip:\n                strip = model.data(index, owcolor.StripRole)\n                self.assertIs(strip, color_strip.return_value)\n                color_strip.assert_called_with(128, 16)\n        finally:\n            model.dataChanged.disconnect(emit)\n\n    def test_copy_to_all(self):\n        super().test_set_data()\n\n        model = self.model\n        index = model.index(1, 1)\n        initial = model.data(index, ColorRole)\n        palette = _find_other_palette(initial)\n\n        emit = Mock()\n        try:\n            model.dataChanged.connect(emit)\n            model.setData(index, palette, ColorRole)\n            emit.assert_called()\n            emit.reset_mock()\n\n            model.copy_to_all(index)\n            emit.assert_called_once()\n            for row, desc in enumerate(self.descs):\n                self.assertEqual(\n                    model.data(model.index(row, 1), ColorRole).name,\n                    palette.name)\n                self.assertEqual(desc.palette_name, palette.name)\n        finally:\n            model.dataChanged.disconnect(emit)\n\n\nclass TestColorStripDelegate(GuiTest):\n    def setUp(self):\n        z = ContinuousVariable(\"z\")\n        w = ContinuousVariable(\"w\")\n        u = ContinuousVariable(\"u\")\n        self.descs = [owcolor.ContAttrDesc(v) for v in (z, w, u)]\n        self.model = owcolor.ContColorTableModel()\n        self.model.set_data(self.descs)\n        self.table = owcolor.ContinuousTable(self.model)\n\n    def test_color_combo(self):\n        model = self.model\n        index = model.index(1, 1)\n        initial = model.data(index, ColorRole)\n        palette = _find_other_palette(initial)\n        model.setData(index, palette, ColorRole)\n        self.assertEqual(self.descs[1].palette_name, palette.name)\n\n        combo = self.table.color_delegate.createEditor(None, Mock(), index)\n        self.assertEqual(combo.currentText(), palette.friendly_name)\n        palette = _find_other_palette(palette)\n        combo.setCurrentIndex(combo.findText(palette.friendly_name))\n        self.assertEqual(self.descs[1].palette_name, palette.name)\n\n        with patch.object(self.table, \"closeEditor\") as closeEditor:\n            combo.hidePopup()\n            closeEditor.assert_called()\n\n    @patch.object(owcolor.HorizontalGridDelegate, \"paint\")\n    def test_paint(self, _):\n        model = self.model\n        index = model.index(1, 1)\n        painter = Mock()\n        option = Mock()\n        option.rect = QRect(10, 20, 30, 40)\n        index.data = Mock()\n        index.data.return_value = Mock()\n        index.data.return_value.height = Mock(return_value=42)\n        self.table.color_delegate.paint(painter, option, index)\n        self.assertIs(painter.drawPixmap.call_args[0][2],\n                      index.data.return_value)\n\n\nclass TestOWColor(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(owcolor.OWColor)\n        self.iris = Table(\"iris\")\n\n    def test_reuse_old_settings(self):\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        assert isinstance(self.widget, owcolor.OWColor)\n        self.widget.saveSettings()\n\n        w = self.create_widget(owcolor.OWColor, reset_default_settings=False)\n        self.send_signal(self.widget.Inputs.data, self.iris, widget=w)\n\n    def test_invalid_input_colors(self):\n        # Handling of invalid value in deprecated 'colors': warn, but don't fail\n        with self.assertWarns(DeprecationWarning):\n            a = ContinuousVariable(\"a\")\n            a.attributes[\"colors\"] = \"invalid\"\n            t = Table.from_domain(Domain([a]))\n            self.send_signal(self.widget.Inputs.data, t)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget.commit, 'now') as commit:\n            self.widget.auto_apply = False\n            commit.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.iris)\n            commit.assert_called()\n\n    def test_commit_on_data_changed(self):\n        widget = self.widget\n        model = widget.cont_model\n        self.send_signal(widget.Inputs.data, self.iris)\n        with patch.object(widget.commit, 'deferred') as commit:\n            commit.reset_mock()\n            model.setData(model.index(0, 0), \"y\", Qt.EditRole)\n            commit.assert_called()\n\n    def test_lose_data(self):\n        widget = self.widget\n        send = widget.Outputs.data.send = Mock()\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        send.assert_called_once()\n        self.assertEqual(widget.disc_model.rowCount(), 1)\n        self.assertEqual(widget.cont_model.rowCount(), 4)\n\n        send.reset_mock()\n        self.send_signal(widget.Inputs.data, None)\n        send.assert_called_with(None)\n        self.assertEqual(widget.disc_model.rowCount(), 0)\n        self.assertEqual(widget.cont_model.rowCount(), 0)\n\n    def test_model_content(self):\n        widget = self.widget\n        data = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, data)\n\n        dm = widget.disc_model\n        self.assertEqual(\n            [dm.data(dm.index(i, 0)) for i in range(dm.rowCount())],\n            [var.name for var in data.domain.variables if var.is_discrete]\n        )\n\n        cm = widget.disc_model\n        self.assertEqual(\n            [dm.data(cm.index(i, 0)) for i in range(cm.rowCount())],\n            [var.name for var in data.domain.variables if var.is_discrete]\n        )\n\n    def test_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, Table(\"zoo\"))\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.widget.send_report()\n\n    def test_string_variables(self):\n        self.send_signal(self.widget.Inputs.data, Table(\"zoo\"))\n\n    def test_reset(self):\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        cont_model = self.widget.cont_model\n        disc_model = self.widget.disc_model\n        cont_model.setData(cont_model.index(0, 0), \"a\", Qt.EditRole)\n        disc_model.setData(disc_model.index(0, 0), \"b\", Qt.EditRole)\n        outp = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(outp.domain[0].name, \"a\")\n        self.assertEqual(outp.domain.class_var.name, \"b\")\n        self.widget.reset()\n        outp = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(\n            outp.domain[0].name, self.iris.domain[0].name)\n        self.assertEqual(\n            outp.domain.class_var.name, self.iris.domain.class_var.name)\n\n    def test_save(self):\n        self.widget._save_var_defs = Mock()\n        with patch.object(owcolor.QFileDialog, \"getSaveFileName\",\n                          return_value=(\"\", \"\")):\n            self.widget.save()\n            self.widget._save_var_defs.assert_not_called()\n        with patch.object(owcolor.QFileDialog, \"getSaveFileName\",\n                          return_value=(\"foo\", \"bar\")):\n            self.widget.save()\n            self.widget._save_var_defs.assert_called_with(\"foo\")\n\n    def test_save_low(self):\n        descA = owcolor.DiscAttrDesc(\n            DiscreteVariable(\"varA\", values=tuple(\"abc\")))\n        descA.name = \"a2\"\n        descB = owcolor.DiscAttrDesc(\n            DiscreteVariable(\"varB\", values=tuple(\"abc\")))\n        descB.set_value(1, \"X\")\n        descC = owcolor.ContAttrDesc(ContinuousVariable(\"varC\"))\n        descC.name = \"c2\"\n        descD = owcolor.ContAttrDesc(ContinuousVariable(\"varD\"))\n        descD.palette_name = \"linear_viridis\"\n        descE = owcolor.ContAttrDesc(ContinuousVariable(\"varE\"))\n        self.widget.disc_descs = [descA, descB]\n        self.widget.cont_descs = [descC, descD, descE]\n\n        with tempfile.TemporaryDirectory() as path:\n            fname = os.path.join(path, \"foo.colors\")\n            self.widget._save_var_defs(fname)\n            with open(fname) as f:\n                js = json.load(f)\n            self.assertEqual(js[\"categorical\"],\n                             {\"varA\": {\"rename\": \"a2\"},\n                              \"varB\": {\"renamed_values\": {\"b\": \"X\"}}})\n            self.assertEqual(js[\"numeric\"],\n                             {\"varC\": {\"rename\": \"c2\"},\n                              \"varD\": {\"colors\": \"linear_viridis\"}})\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.critical\")\n    def test_load(self, msg_box):\n        self.widget._parse_var_defs = Mock()\n        self.widget._save_var_defs = Mock()\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"\", \"\")):\n            with patch(\"builtins.open\"):\n                self.widget.load()\n                open.assert_not_called()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\") as json_load, \\\n                    patch(\"builtins.open\", side_effect=IOError):\n                self.widget.load()\n                msg_box.assert_called()\n                msg_box.reset_mock()\n                json_load.assert_not_called()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\", side_effect=json.JSONDecodeError(\"err\", \"d\", 42)), \\\n                    patch(\"builtins.open\"):\n                self.widget.load()\n                msg_box.assert_called()\n                msg_box.reset_mock()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\"), patch(\"builtins.open\"):\n                self.widget.load()\n                msg_box.assert_not_called()\n                msg_box.reset_mock()\n                self.widget._parse_var_defs.assert_called_with(json.load.return_value)\n\n    def _create_descs(self):\n        disc_vars = [DiscreteVariable(f\"var{c}\", values=(\"a\", \"b\", \"c\"))\n                     for c in \"AB\"]\n        cont_vars = [ContinuousVariable(f\"var{c}\") for c in \"CDE\"]\n        self.widget.disc_descs = [owcolor.DiscAttrDesc(v) for v in disc_vars]\n        self.widget.cont_descs = [owcolor.ContAttrDesc(v) for v in cont_vars]\n        return disc_vars, cont_vars\n\n    def test_parse_var_defs(self):\n        js = {\"categorical\": {\"varA\": {\"rename\": \"a2\"},\n                              \"varB\": {\"renamed_values\": {\"b\": \"X\"}}},\n              \"numeric\": {\"varC\": {\"rename\": \"c2\"},\n                          \"varD\": {\"colors\": \"linear_viridis\"}}}\n\n        self._create_descs()\n        descE = self.widget.cont_descs[-1]\n\n        self.widget._parse_var_defs(js)\n        self.assertEqual(len(self.widget.disc_descs), 2)\n        descA = self.widget.disc_descs[0]\n        self.assertEqual(descA.name, \"a2\")\n        self.assertIsNone(descA.new_values)\n        self.assertIsNone(descA.new_colors)\n\n        descB = self.widget.disc_descs[1]\n        self.assertIsNone(descB.new_name)\n        self.assertEqual(descB.new_values, [\"a\", \"X\", \"c\"])\n        self.assertIsNone(descB.new_colors)\n\n        self.assertEqual(len(self.widget.cont_descs), 3)\n        descC = self.widget.cont_descs[0]\n        self.assertEqual(descC.name, \"c2\")\n        self.assertIsNone(descC.new_palette_name)\n\n        descD = self.widget.cont_descs[1]\n        self.assertIsNone(descD.new_name)\n        self.assertEqual(descD.new_palette_name, \"linear_viridis\")\n\n        self.assertIs(self.widget.cont_descs[2], descE)\n\n    def test_parse_var_defs_invalid(self):\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs, 42)\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {\"a\": 42}, \"numeric\": {}})\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {\"a\": {\"rename\": 4}}, \"numeric\": {}})\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {42: {\"rename\": \"b\"}}, \"numeric\": {}})\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_parse_var_defs_shows_warnings(self, msg_box):\n        self._create_descs()\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"renamed_values\": {\"a\": \"b\"}}},\n             \"numeric\": {}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicate names\" in msg_box.call_args[0][2])\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_parse_var_defs_no_rename(self, msg_box):\n        self._create_descs()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"varB\"}},\n             \"numeric\": {}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicated names\" in msg_box.call_args[0][2])\n        msg_box.reset_mock()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"X\"}},\n             \"numeric\": {\"varD\": {\"rename\": \"X\"}}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicated names\" in msg_box.call_args[0][2])\n        msg_box.reset_mock()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"varD\"}},\n             \"numeric\": {\"varD\": {\"rename\": \"varA\"}}})\n        msg_box.assert_not_called()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"X\"}},\n             \"numeric\": {\"var not\": {\"rename\": \"X\"}}})\n        msg_box.assert_not_called()\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n",
            "new_file_content": "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring, protected-access,unsubscriptable-object\nimport json\nimport os\nimport tempfile\nimport unittest\nfrom unittest.mock import patch, Mock\n\nimport numpy as np\nfrom AnyQt.QtCore import Qt, QSize, QRect\nfrom AnyQt.QtGui import QBrush\n\nfrom orangewidget.tests.base import GuiTest\n\nfrom Orange.data import Table, ContinuousVariable, DiscreteVariable, Domain\nfrom Orange.preprocess.transformation import Identity\nfrom Orange.util import color_to_hex\nfrom Orange.widgets.utils import colorpalettes\nfrom Orange.widgets.data import owcolor\nfrom Orange.widgets.data.owcolor import ColorRole\nfrom Orange.widgets.tests.base import WidgetTest\n\n\nclass AttrDescTest(unittest.TestCase):\n    def test_name(self):\n        x = ContinuousVariable(\"x\")\n        desc = owcolor.AttrDesc(x)\n        self.assertEqual(desc.name, \"x\")\n        desc.name = \"y\"\n        self.assertEqual(desc.name, \"y\")\n        desc.name = None\n        self.assertEqual(desc.name, \"x\")\n\n    def test_reset(self):\n        x = ContinuousVariable(\"x\")\n        desc = owcolor.AttrDesc(x)\n        desc.reset()\n        self.assertEqual(desc.name, \"x\")\n        desc.name = \"y\"\n        desc.reset()\n        self.assertEqual(desc.name, \"x\")\n\n    def test_to_dict(self):\n        x = ContinuousVariable(\"x\")\n        desc = owcolor.AttrDesc(x)\n        self.assertEqual(desc.to_dict(), {})\n        desc2, warns = owcolor.AttrDesc.from_dict(x, desc.to_dict())\n        self.assertEqual(warns, [])\n        self.assertIsNone(desc2.new_name)\n\n        desc.name = \"y\"\n        self.assertEqual(desc.to_dict(), {\"rename\": \"y\"})\n\n        desc2, warns = owcolor.AttrDesc.from_dict(x, desc.to_dict())\n        self.assertEqual(warns, [])\n        self.assertEqual(desc2.new_name, \"y\")\n\n        self.assertRaises(owcolor.InvalidFileFormat,\n                          owcolor.AttrDesc.from_dict, x, {\"rename\": 42})\n        self.assertRaises(owcolor.InvalidFileFormat,\n                          owcolor.AttrDesc.from_dict, x, [])\n\n        # Additional keys shouldn't cause exceptions\n        owcolor.AttrDesc.from_dict(x, {\"foo\": 42})\n\n\nclass DiscAttrTest(unittest.TestCase):\n    def setUp(self):\n        self.var = DiscreteVariable(\"x\", [\"a\", \"b\", \"c\"])\n        self.desc = owcolor.DiscAttrDesc(self.var)\n\n    def test_colors(self):\n        desc = self.desc\n        colors = desc.colors.copy()\n        desc.set_color(2, (0, 0, 0))\n        colors[2] = 0\n        np.testing.assert_equal(desc.colors, colors)\n\n    def test_values(self):\n        desc = self.desc\n        self.assertEqual(desc.values, (\"a\", \"b\", \"c\"))\n        desc.set_value(1, \"d\")\n        self.assertEqual(desc.values, (\"a\", \"d\", \"c\"))\n\n    def test_create_variable(self):\n        desc = self.desc\n        desc.set_color(0, [1, 2, 3])\n        desc.set_color(1, [4, 5, 6])\n        desc.set_color(2, [7, 8, 9])\n        desc.name = \"z\"\n        desc.set_value(1, \"d\")\n        var = desc.create_variable()\n        self.assertIsInstance(var, DiscreteVariable)\n        self.assertEqual(var.name, \"z\")\n        self.assertEqual(var.values, (\"a\", \"d\", \"c\"))\n        np.testing.assert_equal(var.colors, [[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n        self.assertIsInstance(var.compute_value, Identity)\n        self.assertIs(var.compute_value.variable, desc.var)\n\n        palette = desc.var.attributes[\"palette\"] = object()\n        var = desc.create_variable()\n        self.assertIs(desc.var.attributes[\"palette\"], palette)\n        self.assertFalse(hasattr(var.attributes, \"palette\"))\n        self.assertIsInstance(var.compute_value, Identity)\n        self.assertIs(var.compute_value.variable, desc.var)\n\n    def test_reset(self):\n        desc = self.desc\n        desc.set_color(0, [1, 2, 3])\n        desc.set_color(1, [4, 5, 6])\n        desc.set_color(2, [7, 8, 9])\n        desc.set_value(1, \"d\")\n        desc.reset()\n        np.testing.assert_equal(desc.colors, self.var.colors)\n        self.assertEqual(desc.values, self.var.values)\n\n    def test_to_dict(self):\n        desc = owcolor.DiscAttrDesc(self.var)\n        self.assertEqual(desc.to_dict(), {})\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(self.var, desc.to_dict())\n        self.assertEqual(warns, [])\n        self.assertIsNone(desc2.new_name)\n        self.assertIsNone(desc2.new_values)\n        self.assertIsNone(desc2.new_colors)\n\n        desc.name = \"y\"\n        self.assertEqual(desc.to_dict(), {\"rename\": \"y\"})\n\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(self.var, desc.to_dict())\n        self.assertEqual(warns, [])\n        self.assertEqual(desc2.new_name, \"y\")\n        self.assertIsNone(desc2.new_values)\n        self.assertIsNone(desc2.new_colors)\n\n        desc.set_value(1, \"b2\")\n        desc.set_color(1, [1, 2, 3])\n        desc.set_color(2, [2, 3, 4])\n        self.assertEqual(desc.to_dict(), {\n            \"rename\": \"y\",\n            \"renamed_values\": {\"b\": \"b2\"},\n            \"colors\": {\"a\": color_to_hex(desc.colors[0]),\n                       \"b\": \"#010203\",\n                       \"c\": \"#020304\"}\n        })\n\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(self.var, desc.to_dict())\n        self.assertEqual(warns, [])\n        cols = list(desc.colors)\n        cols[1:] = [[1, 2, 3], [2, 3, 4]]\n        np.testing.assert_equal(desc2.colors, cols)\n        self.assertEqual(desc2.values, (\"a\", \"b2\", \"c\"))\n\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(\n            self.var,\n            {\"rename\": \"y\",\n             \"renamed_values\": {\"b\": \"b2\", \"d\": \"x\"},  # d is redundant\n             \"colors\": {\"b\": \"#010203\",\n                        \"c\": \"#020304\",\n                        \"d\": \"#123456\"}  # d is redundant and must be ignored\n            })\n        self.assertEqual(warns, [])\n        cols = list(desc.colors)\n        cols[1:] = [[1, 2, 3], [2, 3, 4]]\n        np.testing.assert_equal(desc2.colors, cols)\n        self.assertEqual(desc2.values, (\"a\", \"b2\", \"c\"))\n\n    def test_from_dict_coliding_values(self):\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(\n            self.var, {\"renamed_values\": {\"a\": \"b\"}})\n        self.assertEqual(len(warns), 1)\n        self.assertTrue(\"duplicate names\" in warns[0])\n        self.assertIsNone(desc2.new_values)\n        self.assertEqual(desc2.values, (\"a\", \"b\", \"c\"))\n\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(\n            self.var, {\"renamed_values\": {\"a\": \"e\", \"b\": \"e\"}})\n        self.assertEqual(len(warns), 1)\n        self.assertTrue(\"duplicate names\" in warns[0])\n        self.assertIsNone(desc2.new_values)\n        self.assertEqual(desc2.values, (\"a\", \"b\", \"c\"))\n\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(\n            self.var, {\"renamed_values\": {\"a\": \"b\", \"b\": \"a\"}})\n        self.assertEqual(warns, [])\n        self.assertEqual(desc2.values, (\"b\", \"a\", \"c\"))\n\n    def test_from_dict_exceptions(self):\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.AttrDesc.from_dict, self.var, [])\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"rename\": 42}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"colors\": []}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"colors\": {\"a\": 42}}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"colors\": {4: \"#000000\"}}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"colors\": {\"a\": \"#00\"}}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"colors\": {\"a\": \"#qwerty\"}}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"renamed_values\": []}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"renamed_values\": {\"a\": 42}}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"renamed_values\": {4: \"#000000\"}}\n        )\n\n\nclass ContAttrDescTest(unittest.TestCase):\n    def setUp(self):\n        x = ContinuousVariable(\"x\")\n        self.desc = owcolor.ContAttrDesc(x)\n\n    def test_palette(self):\n        desc = self.desc\n        palette = desc.palette_name\n        self.assertIsInstance(palette, str)\n        desc.palette_name = \"foo\"\n        self.assertEqual(desc.palette_name, \"foo\")\n        desc.palette_name = None\n        self.assertEqual(desc.palette_name, palette)\n\n    def test_create_variable(self):\n        desc = self.desc\n        desc.name = \"z\"\n        palette_name = _find_other_palette(\n            colorpalettes.ContinuousPalettes[desc.palette_name]).name\n        desc.palette_name = palette_name\n        var = desc.create_variable()\n        self.assertIsInstance(var, ContinuousVariable)\n        self.assertEqual(var.name, \"z\")\n        self.assertEqual(var.palette.name, palette_name)\n        self.assertIsInstance(var.compute_value, Identity)\n        self.assertIs(var.compute_value.variable, desc.var)\n\n        colors = desc.var.attributes[\"colors\"] = object()\n        var = desc.create_variable()\n        self.assertIs(desc.var.attributes[\"colors\"], colors)\n        self.assertFalse(hasattr(var.attributes, \"colors\"))\n        self.assertIsInstance(var.compute_value, Identity)\n        self.assertIs(var.compute_value.variable, desc.var)\n\n    def test_reset(self):\n        desc = self.desc\n        palette_name = desc.palette_name\n        desc.palette_name = _find_other_palette(\n            colorpalettes.ContinuousPalettes[palette_name]).name\n        desc.reset()\n        np.testing.assert_equal(desc.palette_name, palette_name)\n\n    def test_to_dict(self):\n        x = ContinuousVariable(\"x\")\n        desc = owcolor.ContAttrDesc(x)\n        self.assertEqual(desc.to_dict(), {})\n        desc2, warns = owcolor.ContAttrDesc.from_dict(x, desc.to_dict())\n        self.assertEqual(warns, [])\n        self.assertIsNone(desc2.new_name)\n        self.assertIsNone(desc2.new_palette_name)\n\n        desc.name = \"y\"\n        self.assertEqual(desc.to_dict(), {\"rename\": \"y\"})\n\n        desc2, warns = owcolor.ContAttrDesc.from_dict(x, desc.to_dict())\n        self.assertEqual(warns, [])\n        self.assertEqual(desc2.new_name, \"y\")\n        self.assertIsNone(desc2.new_palette_name)\n\n        desc = owcolor.ContAttrDesc(x)\n        desc.palette_name = \"linear_viridis\"\n        self.assertEqual(desc.to_dict(), {\"colors\": \"linear_viridis\"})\n\n    def test_from_dict_exceptions(self):\n        x = ContinuousVariable(\"x\")\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.ContAttrDesc.from_dict, x, []\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.ContAttrDesc.from_dict, x, {\"colors\": 42}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.ContAttrDesc.from_dict, x, {\"colors\": \"no such palette\"}\n        )\n\n\nclass BaseTestColorTableModel:\n    def test_row_count(self):\n        model = self.model\n\n        self.assertEqual(model.rowCount(), 0)\n        model.set_data(self.descs)\n        self.assertEqual(model.rowCount(), len(self.descs))\n        self.assertEqual(model.rowCount(self.model.index(0, 0)), 0)\n\n    def test_data(self):\n        self.model.set_data(self.descs)\n        data = self.model.data\n\n        index = self.model.index(1, 0)\n        self.assertEqual(data(index, Qt.DisplayRole), self.descs[1].name)\n        self.assertEqual(data(index, Qt.EditRole), self.descs[1].name)\n        self.assertTrue(data(index, Qt.FontRole).bold())\n        self.assertTrue(data(index, Qt.TextAlignmentRole) & Qt.AlignRight)\n\n        self.descs[1].name = \"bar\"\n        self.assertEqual(data(index), \"bar\")\n\n        index = self.model.index(2, 0)\n        self.assertEqual(data(index, Qt.DisplayRole), self.descs[2].name)\n\n    def test_set_data(self):\n        emit = Mock()\n        try:\n            self.model.dataChanged.connect(emit)\n            self.model.set_data(self.descs)\n            data = self.model.data\n            setData = self.model.setData\n\n            index = self.model.index(1, 0)\n            assert self.descs[1].name != \"foo\"\n            self.assertFalse(setData(index, \"foo\", Qt.DisplayRole))\n            emit.assert_not_called()\n            self.assertEqual(data(index, Qt.DisplayRole), self.descs[1].name)\n            self.assertTrue(setData(index, \"foo\", Qt.EditRole))\n            emit.assert_called()\n            self.assertEqual(data(index, Qt.DisplayRole), \"foo\")\n            self.assertEqual(self.descs[1].name, \"foo\")\n        finally:\n            self.model.dataChanged.disconnect(emit)\n\n    def test_reset(self):\n        for desc in self.descs:\n            desc.reset = Mock()\n        self.model.set_data(self.descs)\n        self.model.reset()\n        for desc in self.descs:\n            desc.reset.assert_called()\n\n\nclass TestDiscColorTableModel(GuiTest, BaseTestColorTableModel):\n    def setUp(self):\n        x = DiscreteVariable(\"x\", list(\"abc\"))\n        y = DiscreteVariable(\"y\", list(\"def\"))\n        z = DiscreteVariable(\"z\", list(\"ghijk\"))\n        self.descs = [owcolor.DiscAttrDesc(v) for v in (x, y, z)]\n        self.model = owcolor.DiscColorTableModel()\n\n    def test_column_count(self):\n        model = self.model\n\n        self.assertEqual(model.columnCount(), 1)\n        model.set_data(self.descs[:2])\n        self.assertEqual(model.columnCount(), 4)\n        model.set_data(self.descs)\n        self.assertEqual(model.columnCount(), 6)\n\n        self.assertEqual(model.columnCount(model.index(0, 0)), 0)\n\n    def test_data(self):\n        super().test_data()\n\n        model = self.model\n\n        self.assertIsNone(model.data(model.index(0, 4)))\n\n        index = model.index(1, 2)\n        self.assertEqual(model.data(index, Qt.DisplayRole), \"e\")\n        self.assertEqual(model.data(index, Qt.EditRole), \"e\")\n        font = model.data(index, Qt.FontRole)\n        self.assertTrue(font is None or not font.bold())\n\n        var_colors = self.descs[1].var.colors[1]\n        color = model.data(index, Qt.DecorationRole)\n        np.testing.assert_equal(color.getRgb()[:3], var_colors)\n\n        color = model.data(index, owcolor.ColorRole)\n        np.testing.assert_equal(color, var_colors)\n\n        self.assertEqual(\n            model.data(index, Qt.ToolTipRole), color_to_hex(var_colors))\n\n        self.assertIsNone(model.data(model.index(0, 4)))\n\n        index = model.index(2, 5)\n        self.assertEqual(model.data(index, Qt.DisplayRole), \"k\")\n        self.assertEqual(model.data(index, Qt.EditRole), \"k\")\n        font = model.data(index, Qt.FontRole)\n        self.assertTrue(font is None or not font.bold())\n\n        var_colors = self.descs[2].var.colors[4]\n        color = model.data(index, Qt.DecorationRole)\n        np.testing.assert_equal(color.getRgb()[:3], var_colors)\n\n        color = model.data(index, owcolor.ColorRole)\n        np.testing.assert_equal(color, var_colors)\n\n        self.assertEqual(\n            model.data(index, Qt.ToolTipRole), color_to_hex(var_colors))\n\n        self.descs[2].set_value(4, \"foo\")\n        self.assertEqual(model.data(index, Qt.DisplayRole), \"foo\")\n\n    def test_set_data(self):\n        super().test_set_data()\n\n        model = self.model\n        emit = Mock()\n        try:\n            model.dataChanged.connect(emit)\n\n            index = model.index(2, 5)\n\n            self.assertEqual(model.data(index, Qt.DisplayRole), \"k\")\n            self.assertEqual(model.data(index, Qt.EditRole), \"k\")\n            self.assertFalse(model.setData(index, \"foo\", Qt.DisplayRole))\n            emit.assert_not_called()\n            self.assertEqual(model.data(index, Qt.DisplayRole), \"k\")\n            self.assertTrue(model.setData(index, \"foo\", Qt.EditRole))\n            emit.assert_called()\n            emit.reset_mock()\n            self.assertEqual(model.data(index, Qt.DisplayRole), \"foo\")\n            self.assertEqual(self.descs[2].values, (\"g\", \"h\", \"i\", \"j\", \"foo\"))\n\n            new_color = [0, 1, 2]\n            self.assertTrue(model.setData(index, new_color + [255], ColorRole))\n            emit.assert_called()\n            emit.reset_mock()\n            color = model.data(index, Qt.DecorationRole)\n            rgb = [color.red(), color.green(), color.blue()]\n            self.assertEqual(rgb, new_color)\n\n            color = model.data(index, owcolor.ColorRole)\n            self.assertEqual(list(color), new_color)\n\n            self.assertEqual(\n                model.data(index, Qt.ToolTipRole), color_to_hex(new_color))\n\n            np.testing.assert_equal(self.descs[2].colors[4], rgb)\n        finally:\n            model.dataChanged.disconnect(emit)\n\n\ndef _find_other_palette(initial):\n    for palette in colorpalettes.ContinuousPalettes.values():\n        if palette.name != initial.name:\n            return palette\n    return None  # pragma: no cover\n\n\nclass TestContColorTableModel(GuiTest, BaseTestColorTableModel):\n    def setUp(self):\n        z = ContinuousVariable(\"z\")\n        w = ContinuousVariable(\"w\")\n        u = ContinuousVariable(\"u\")\n        self.descs = [owcolor.ContAttrDesc(v) for v in (z, w, u)]\n        self.model = owcolor.ContColorTableModel()\n\n    def test_column_count(self):\n        model = self.model\n\n        model.set_data(self.descs)\n        self.assertEqual(model.columnCount(), 3)\n        self.assertEqual(model.columnCount(model.index(0, 0)), 0)\n\n    def test_data(self):\n        super().test_data()\n\n        model = self.model\n        index = model.index(1, 1)\n        palette = colorpalettes.ContinuousPalettes[self.descs[1].palette_name]\n        self.assertEqual(model.data(index, Qt.ToolTipRole),\n                         palette.friendly_name)\n        self.assertEqual(model.data(index, ColorRole), palette)\n        with patch.object(palette, \"color_strip\") as color_strip:\n            strip = model.data(index, owcolor.StripRole)\n            self.assertIs(strip, color_strip.return_value)\n            color_strip.assert_called_with(128, 16)\n        self.assertIsInstance(model.data(index, Qt.SizeHintRole), QSize)\n        self.assertIsNone(model.data(index, Qt.FontRole))\n\n        palette = _find_other_palette(self.descs[1])\n        self.descs[1].palette_name = palette.name\n        self.assertIs(model.data(index, ColorRole), palette)\n\n        index = self.model.index(1, 2)\n        self.assertIsNone(model.data(index, Qt.ToolTipRole))\n        self.assertIsInstance(model.data(index, Qt.SizeHintRole), QSize)\n        self.assertIsInstance(model.data(index, Qt.ForegroundRole), QBrush)\n        self.assertIsNone(model.data(index, Qt.DisplayRole))\n        model.set_mouse_row(0)\n        self.assertIsNone(model.data(index, Qt.DisplayRole))\n        model.set_mouse_row(1)\n        self.assertEqual(model.data(index, Qt.DisplayRole), \"Copy to all\")\n\n    def test_set_data(self):\n        super().test_set_data()\n\n        model = self.model\n        index = model.index(1, 1)\n        index2 = model.index(2, 1)\n        initial = model.data(index, ColorRole)\n        initial2 = model.data(index, ColorRole)\n        assert initial.name == initial2.name\n        palette = _find_other_palette(initial)\n\n        emit = Mock()\n        try:\n            model.dataChanged.connect(emit)\n\n            self.assertFalse(model.setData(index, None, Qt.DisplayRole))\n            emit.assert_not_called()\n\n            self.assertTrue(model.setData(index, palette, ColorRole))\n            emit.assert_called()\n            self.assertIs(model.data(index2, ColorRole), initial2)\n\n            self.assertEqual(model.data(index, Qt.ToolTipRole),\n                             palette.friendly_name)\n            self.assertEqual(model.data(index, ColorRole), palette)\n            self.assertEqual(self.descs[1].palette_name, palette.name)\n            with patch.object(palette, \"color_strip\") as color_strip:\n                strip = model.data(index, owcolor.StripRole)\n                self.assertIs(strip, color_strip.return_value)\n                color_strip.assert_called_with(128, 16)\n        finally:\n            model.dataChanged.disconnect(emit)\n\n    def test_copy_to_all(self):\n        super().test_set_data()\n\n        model = self.model\n        index = model.index(1, 1)\n        initial = model.data(index, ColorRole)\n        palette = _find_other_palette(initial)\n\n        emit = Mock()\n        try:\n            model.dataChanged.connect(emit)\n            model.setData(index, palette, ColorRole)\n            emit.assert_called()\n            emit.reset_mock()\n\n            model.copy_to_all(index)\n            emit.assert_called_once()\n            for row, desc in enumerate(self.descs):\n                self.assertEqual(\n                    model.data(model.index(row, 1), ColorRole).name,\n                    palette.name)\n                self.assertEqual(desc.palette_name, palette.name)\n        finally:\n            model.dataChanged.disconnect(emit)\n\n\nclass TestColorStripDelegate(GuiTest):\n    def setUp(self):\n        z = ContinuousVariable(\"z\")\n        w = ContinuousVariable(\"w\")\n        u = ContinuousVariable(\"u\")\n        self.descs = [owcolor.ContAttrDesc(v) for v in (z, w, u)]\n        self.model = owcolor.ContColorTableModel()\n        self.model.set_data(self.descs)\n        self.table = owcolor.ContinuousTable(self.model)\n\n    def test_color_combo(self):\n        model = self.model\n        index = model.index(1, 1)\n        initial = model.data(index, ColorRole)\n        palette = _find_other_palette(initial)\n        model.setData(index, palette, ColorRole)\n        self.assertEqual(self.descs[1].palette_name, palette.name)\n\n        combo = self.table.color_delegate.createEditor(None, Mock(), index)\n        self.assertEqual(combo.currentText(), palette.friendly_name)\n        palette = _find_other_palette(palette)\n        combo.setCurrentIndex(combo.findText(palette.friendly_name))\n        self.assertEqual(self.descs[1].palette_name, palette.name)\n\n        with patch.object(self.table, \"closeEditor\") as closeEditor:\n            combo.hidePopup()\n            closeEditor.assert_called()\n\n    @patch.object(owcolor.HorizontalGridDelegate, \"paint\")\n    def test_paint(self, _):\n        model = self.model\n        index = model.index(1, 1)\n        painter = Mock()\n        option = Mock()\n        option.rect = QRect(10, 20, 30, 40)\n        index.data = Mock()\n        index.data.return_value = Mock()\n        index.data.return_value.height = Mock(return_value=42)\n        self.table.color_delegate.paint(painter, option, index)\n        self.assertIs(painter.drawPixmap.call_args[0][2],\n                      index.data.return_value)\n\n\nclass TestOWColor(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(owcolor.OWColor)\n        self.iris = Table(\"iris\")\n\n    def test_reuse_old_settings(self):\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        assert isinstance(self.widget, owcolor.OWColor)\n        self.widget.saveSettings()\n\n        w = self.create_widget(owcolor.OWColor, reset_default_settings=False)\n        self.send_signal(self.widget.Inputs.data, self.iris, widget=w)\n\n    def test_invalid_input_colors(self):\n        # Handling of invalid value in deprecated 'colors': warn, but don't fail\n        with self.assertWarns(DeprecationWarning):\n            a = ContinuousVariable(\"a\")\n            a.attributes[\"colors\"] = \"invalid\"\n            t = Table.from_domain(Domain([a]))\n            self.send_signal(self.widget.Inputs.data, t)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget.commit, 'now') as commit:\n            self.widget.auto_apply = False\n            commit.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.iris)\n            commit.assert_called()\n\n    def test_commit_on_data_changed(self):\n        widget = self.widget\n        model = widget.cont_model\n        self.send_signal(widget.Inputs.data, self.iris)\n        with patch.object(widget.commit, 'deferred') as commit:\n            commit.reset_mock()\n            model.setData(model.index(0, 0), \"y\", Qt.EditRole)\n            commit.assert_called()\n\n    def test_lose_data(self):\n        widget = self.widget\n        send = widget.Outputs.data.send = Mock()\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        send.assert_called_once()\n        self.assertEqual(widget.disc_model.rowCount(), 1)\n        self.assertEqual(widget.cont_model.rowCount(), 4)\n\n        send.reset_mock()\n        self.send_signal(widget.Inputs.data, None)\n        send.assert_called_with(None)\n        self.assertEqual(widget.disc_model.rowCount(), 0)\n        self.assertEqual(widget.cont_model.rowCount(), 0)\n\n    def test_model_content(self):\n        widget = self.widget\n        data = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, data)\n\n        dm = widget.disc_model\n        self.assertEqual(\n            [dm.data(dm.index(i, 0)) for i in range(dm.rowCount())],\n            [var.name for var in data.domain.variables if var.is_discrete]\n        )\n\n        cm = widget.disc_model\n        self.assertEqual(\n            [dm.data(cm.index(i, 0)) for i in range(cm.rowCount())],\n            [var.name for var in data.domain.variables if var.is_discrete]\n        )\n\n    def test_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, Table(\"zoo\"))\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.widget.send_report()\n\n    def test_string_variables(self):\n        self.send_signal(self.widget.Inputs.data, Table(\"zoo\"))\n\n    def test_reset(self):\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        cont_model = self.widget.cont_model\n        disc_model = self.widget.disc_model\n        cont_model.setData(cont_model.index(0, 0), \"a\", Qt.EditRole)\n        disc_model.setData(disc_model.index(0, 0), \"b\", Qt.EditRole)\n        outp = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(outp.domain[0].name, \"a\")\n        self.assertEqual(outp.domain.class_var.name, \"b\")\n        self.widget.reset()\n        outp = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(\n            outp.domain[0].name, self.iris.domain[0].name)\n        self.assertEqual(\n            outp.domain.class_var.name, self.iris.domain.class_var.name)\n\n    def test_save(self):\n        self.widget._save_var_defs = Mock()\n        with patch.object(owcolor.QFileDialog, \"getSaveFileName\",\n                          return_value=(\"\", \"\")):\n            self.widget.save()\n            self.widget._save_var_defs.assert_not_called()\n        with patch.object(owcolor.QFileDialog, \"getSaveFileName\",\n                          return_value=(\"foo\", \"bar\")):\n            self.widget.save()\n            self.widget._save_var_defs.assert_called_with(\"foo\")\n\n    def test_save_low(self):\n        descA = owcolor.DiscAttrDesc(\n            DiscreteVariable(\"varA\", values=tuple(\"abc\")))\n        descA.name = \"a2\"\n        descB = owcolor.DiscAttrDesc(\n            DiscreteVariable(\"varB\", values=tuple(\"abc\")))\n        descB.set_value(1, \"X\")\n        descC = owcolor.ContAttrDesc(ContinuousVariable(\"varC\"))\n        descC.name = \"c2\"\n        descD = owcolor.ContAttrDesc(ContinuousVariable(\"varD\"))\n        descD.palette_name = \"linear_viridis\"\n        descE = owcolor.ContAttrDesc(ContinuousVariable(\"varE\"))\n        self.widget.disc_descs = [descA, descB]\n        self.widget.cont_descs = [descC, descD, descE]\n\n        with tempfile.TemporaryDirectory() as path:\n            fname = os.path.join(path, \"foo.colors\")\n            self.widget._save_var_defs(fname)\n            with open(fname) as f:\n                js = json.load(f)\n            self.assertEqual(js[\"categorical\"],\n                             {\"varA\": {\"rename\": \"a2\"},\n                              \"varB\": {\"renamed_values\": {\"b\": \"X\"}}})\n            self.assertEqual(js[\"numeric\"],\n                             {\"varC\": {\"rename\": \"c2\"},\n                              \"varD\": {\"colors\": \"linear_viridis\"}})\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.critical\")\n    def test_load(self, msg_box):\n        self.widget._parse_var_defs = Mock()\n        self.widget._save_var_defs = Mock()\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"\", \"\")):\n            with patch(\"builtins.open\"):\n                self.widget.load()\n                open.assert_not_called()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\") as json_load, \\\n                    patch(\"builtins.open\", side_effect=IOError):\n                self.widget.load()\n                msg_box.assert_called()\n                msg_box.reset_mock()\n                json_load.assert_not_called()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\", side_effect=json.JSONDecodeError(\"err\", \"d\", 42)), \\\n                    patch(\"builtins.open\"):\n                self.widget.load()\n                msg_box.assert_called()\n                msg_box.reset_mock()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\"), patch(\"builtins.open\"):\n                self.widget.load()\n                msg_box.assert_not_called()\n                msg_box.reset_mock()\n                self.widget._parse_var_defs.assert_called_with(json.load.return_value)\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_load_ignore_warning(self, msg_box):\n        self.widget._parse_var_defs(dict(categorical={}, numeric={}))\n        msg_box.assert_not_called()\n\n        no_change = dict(renamed_values={}, colors={})\n        for names, message in (\n                ((\"foo\",),\n                 \"'foo'\"),\n                ((\"foo\", \"bar\"),\n                 \"'foo' and 'bar'\"),\n                ((\"foo\", \"bar\", \"baz\"),\n                 \"'foo', 'bar' and 'baz'\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\"),\n                 \"'foo', 'bar', 'baz' and 'qux'\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\"),\n                 \"'foo', 'bar', 'baz', 'qux' and 'quux'\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\", \"corge\"),\n                 \"'foo', 'bar', 'baz', 'qux' and 2 other\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\", \"corge\", \"grault\"),\n                 \"'foo', 'bar', 'baz', 'qux' and 3 other\")):\n            self.widget._parse_var_defs(dict(\n                categorical=dict.fromkeys(names, no_change),\n                numeric={}))\n            self.assertIn(message, msg_box.call_args[0][2])\n\n    def _create_descs(self):\n        disc_vars = [DiscreteVariable(f\"var{c}\", values=(\"a\", \"b\", \"c\"))\n                     for c in \"AB\"]\n        cont_vars = [ContinuousVariable(f\"var{c}\") for c in \"CDE\"]\n        self.widget.disc_descs = [owcolor.DiscAttrDesc(v) for v in disc_vars]\n        self.widget.cont_descs = [owcolor.ContAttrDesc(v) for v in cont_vars]\n        return disc_vars, cont_vars\n\n    def test_parse_var_defs(self):\n        js = {\"categorical\": {\"varA\": {\"rename\": \"a2\"},\n                              \"varB\": {\"renamed_values\": {\"b\": \"X\"}}},\n              \"numeric\": {\"varC\": {\"rename\": \"c2\"},\n                          \"varD\": {\"colors\": \"linear_viridis\"}}}\n\n        self._create_descs()\n        descE = self.widget.cont_descs[-1]\n\n        self.widget._parse_var_defs(js)\n        self.assertEqual(len(self.widget.disc_descs), 2)\n        descA = self.widget.disc_descs[0]\n        self.assertEqual(descA.name, \"a2\")\n        self.assertIsNone(descA.new_values)\n        self.assertIsNone(descA.new_colors)\n\n        descB = self.widget.disc_descs[1]\n        self.assertIsNone(descB.new_name)\n        self.assertEqual(descB.new_values, [\"a\", \"X\", \"c\"])\n        self.assertIsNone(descB.new_colors)\n\n        self.assertEqual(len(self.widget.cont_descs), 3)\n        descC = self.widget.cont_descs[0]\n        self.assertEqual(descC.name, \"c2\")\n        self.assertIsNone(descC.new_palette_name)\n\n        descD = self.widget.cont_descs[1]\n        self.assertIsNone(descD.new_name)\n        self.assertEqual(descD.new_palette_name, \"linear_viridis\")\n\n        self.assertIs(self.widget.cont_descs[2], descE)\n\n    def test_parse_var_defs_invalid(self):\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs, 42)\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {\"a\": 42}, \"numeric\": {}})\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {\"a\": {\"rename\": 4}}, \"numeric\": {}})\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {42: {\"rename\": \"b\"}}, \"numeric\": {}})\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_parse_var_defs_shows_warnings(self, msg_box):\n        self._create_descs()\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"renamed_values\": {\"a\": \"b\"}}},\n             \"numeric\": {}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicate names\" in msg_box.call_args[0][2])\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_parse_var_defs_no_rename(self, msg_box):\n        self._create_descs()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"varB\"}},\n             \"numeric\": {}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicated names\" in msg_box.call_args[0][2])\n        msg_box.reset_mock()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"X\"}},\n             \"numeric\": {\"varD\": {\"rename\": \"X\"}}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicated names\" in msg_box.call_args[0][2])\n        msg_box.reset_mock()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"varD\"}},\n             \"numeric\": {\"varD\": {\"rename\": \"varA\"}}})\n        msg_box.assert_not_called()\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/data/tests/test_owcolor.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "fa8ba624c",
                "new_commit_hash": "634bc6d68",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/data/tests/test_owcolor.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/data/tests/test_owcolor.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 793,
                            "length": 6
                        },
                        "new_range": {
                            "start": 793,
                            "length": 32
                        },
                        "section": "class TestOWColor(WidgetTest):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "                msg_box.reset_mock()",
                                "type": "context"
                            },
                            {
                                "content": "                self.widget._parse_var_defs.assert_called_with(json.load.return_value)",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")",
                                "type": "added"
                            },
                            {
                                "content": "    def test_load_ignore_warning(self, msg_box):",
                                "type": "added"
                            },
                            {
                                "content": "        self.widget._parse_var_defs(dict(categorical={}, numeric={}))",
                                "type": "added"
                            },
                            {
                                "content": "        msg_box.assert_not_called()",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        no_change = dict(renamed_values={}, colors={})",
                                "type": "added"
                            },
                            {
                                "content": "        for names, message in (",
                                "type": "added"
                            },
                            {
                                "content": "                ((\"foo\",),",
                                "type": "added"
                            },
                            {
                                "content": "                 \"'foo'\"),",
                                "type": "added"
                            },
                            {
                                "content": "                ((\"foo\", \"bar\"),",
                                "type": "added"
                            },
                            {
                                "content": "                 \"'foo' and 'bar'\"),",
                                "type": "added"
                            },
                            {
                                "content": "                ((\"foo\", \"bar\", \"baz\"),",
                                "type": "added"
                            },
                            {
                                "content": "                 \"'foo', 'bar' and 'baz'\"),",
                                "type": "added"
                            },
                            {
                                "content": "                ((\"foo\", \"bar\", \"baz\", \"qux\"),",
                                "type": "added"
                            },
                            {
                                "content": "                 \"'foo', 'bar', 'baz' and 'qux'\"),",
                                "type": "added"
                            },
                            {
                                "content": "                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\"),",
                                "type": "added"
                            },
                            {
                                "content": "                 \"'foo', 'bar', 'baz', 'qux' and 'quux'\"),",
                                "type": "added"
                            },
                            {
                                "content": "                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\", \"corge\"),",
                                "type": "added"
                            },
                            {
                                "content": "                 \"'foo', 'bar', 'baz', 'qux' and 2 other\"),",
                                "type": "added"
                            },
                            {
                                "content": "                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\", \"corge\", \"grault\"),",
                                "type": "added"
                            },
                            {
                                "content": "                 \"'foo', 'bar', 'baz', 'qux' and 3 other\")):",
                                "type": "added"
                            },
                            {
                                "content": "            self.widget._parse_var_defs(dict(",
                                "type": "added"
                            },
                            {
                                "content": "                categorical=dict.fromkeys(names, no_change),",
                                "type": "added"
                            },
                            {
                                "content": "                numeric={}))",
                                "type": "added"
                            },
                            {
                                "content": "            self.assertIn(message, msg_box.call_args[0][2])",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    def _create_descs(self):",
                                "type": "context"
                            },
                            {
                                "content": "        disc_vars = [DiscreteVariable(f\"var{c}\", values=(\"a\", \"b\", \"c\"))",
                                "type": "context"
                            },
                            {
                                "content": "                     for c in \"AB\"]",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/data/tests/test_owcolor.py",
                            "type": "class",
                            "name": "TestOWColor",
                            "content": "class TestOWColor(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(owcolor.OWColor)\n        self.iris = Table(\"iris\")\n\n    def test_reuse_old_settings(self):\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        assert isinstance(self.widget, owcolor.OWColor)\n        self.widget.saveSettings()\n\n        w = self.create_widget(owcolor.OWColor, reset_default_settings=False)\n        self.send_signal(self.widget.Inputs.data, self.iris, widget=w)\n\n    def test_invalid_input_colors(self):\n        # Handling of invalid value in deprecated 'colors': warn, but don't fail\n        with self.assertWarns(DeprecationWarning):\n            a = ContinuousVariable(\"a\")\n            a.attributes[\"colors\"] = \"invalid\"\n            t = Table.from_domain(Domain([a]))\n            self.send_signal(self.widget.Inputs.data, t)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget.commit, 'now') as commit:\n            self.widget.auto_apply = False\n            commit.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.iris)\n            commit.assert_called()\n\n    def test_commit_on_data_changed(self):\n        widget = self.widget\n        model = widget.cont_model\n        self.send_signal(widget.Inputs.data, self.iris)\n        with patch.object(widget.commit, 'deferred') as commit:\n            commit.reset_mock()\n            model.setData(model.index(0, 0), \"y\", Qt.EditRole)\n            commit.assert_called()\n\n    def test_lose_data(self):\n        widget = self.widget\n        send = widget.Outputs.data.send = Mock()\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        send.assert_called_once()\n        self.assertEqual(widget.disc_model.rowCount(), 1)\n        self.assertEqual(widget.cont_model.rowCount(), 4)\n\n        send.reset_mock()\n        self.send_signal(widget.Inputs.data, None)\n        send.assert_called_with(None)\n        self.assertEqual(widget.disc_model.rowCount(), 0)\n        self.assertEqual(widget.cont_model.rowCount(), 0)\n\n    def test_model_content(self):\n        widget = self.widget\n        data = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, data)\n\n        dm = widget.disc_model\n        self.assertEqual(\n            [dm.data(dm.index(i, 0)) for i in range(dm.rowCount())],\n            [var.name for var in data.domain.variables if var.is_discrete]\n        )\n\n        cm = widget.disc_model\n        self.assertEqual(\n            [dm.data(cm.index(i, 0)) for i in range(cm.rowCount())],\n            [var.name for var in data.domain.variables if var.is_discrete]\n        )\n\n    def test_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, Table(\"zoo\"))\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.widget.send_report()\n\n    def test_string_variables(self):\n        self.send_signal(self.widget.Inputs.data, Table(\"zoo\"))\n\n    def test_reset(self):\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        cont_model = self.widget.cont_model\n        disc_model = self.widget.disc_model\n        cont_model.setData(cont_model.index(0, 0), \"a\", Qt.EditRole)\n        disc_model.setData(disc_model.index(0, 0), \"b\", Qt.EditRole)\n        outp = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(outp.domain[0].name, \"a\")\n        self.assertEqual(outp.domain.class_var.name, \"b\")\n        self.widget.reset()\n        outp = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(\n            outp.domain[0].name, self.iris.domain[0].name)\n        self.assertEqual(\n            outp.domain.class_var.name, self.iris.domain.class_var.name)\n\n    def test_save(self):\n        self.widget._save_var_defs = Mock()\n        with patch.object(owcolor.QFileDialog, \"getSaveFileName\",\n                          return_value=(\"\", \"\")):\n            self.widget.save()\n            self.widget._save_var_defs.assert_not_called()\n        with patch.object(owcolor.QFileDialog, \"getSaveFileName\",\n                          return_value=(\"foo\", \"bar\")):\n            self.widget.save()\n            self.widget._save_var_defs.assert_called_with(\"foo\")\n\n    def test_save_low(self):\n        descA = owcolor.DiscAttrDesc(\n            DiscreteVariable(\"varA\", values=tuple(\"abc\")))\n        descA.name = \"a2\"\n        descB = owcolor.DiscAttrDesc(\n            DiscreteVariable(\"varB\", values=tuple(\"abc\")))\n        descB.set_value(1, \"X\")\n        descC = owcolor.ContAttrDesc(ContinuousVariable(\"varC\"))\n        descC.name = \"c2\"\n        descD = owcolor.ContAttrDesc(ContinuousVariable(\"varD\"))\n        descD.palette_name = \"linear_viridis\"\n        descE = owcolor.ContAttrDesc(ContinuousVariable(\"varE\"))\n        self.widget.disc_descs = [descA, descB]\n        self.widget.cont_descs = [descC, descD, descE]\n\n        with tempfile.TemporaryDirectory() as path:\n            fname = os.path.join(path, \"foo.colors\")\n            self.widget._save_var_defs(fname)\n            with open(fname) as f:\n                js = json.load(f)\n            self.assertEqual(js[\"categorical\"],\n                             {\"varA\": {\"rename\": \"a2\"},\n                              \"varB\": {\"renamed_values\": {\"b\": \"X\"}}})\n            self.assertEqual(js[\"numeric\"],\n                             {\"varC\": {\"rename\": \"c2\"},\n                              \"varD\": {\"colors\": \"linear_viridis\"}})\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.critical\")\n    def test_load(self, msg_box):\n        self.widget._parse_var_defs = Mock()\n        self.widget._save_var_defs = Mock()\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"\", \"\")):\n            with patch(\"builtins.open\"):\n                self.widget.load()\n                open.assert_not_called()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\") as json_load, \\\n                    patch(\"builtins.open\", side_effect=IOError):\n                self.widget.load()\n                msg_box.assert_called()\n                msg_box.reset_mock()\n                json_load.assert_not_called()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\", side_effect=json.JSONDecodeError(\"err\", \"d\", 42)), \\\n                    patch(\"builtins.open\"):\n                self.widget.load()\n                msg_box.assert_called()\n                msg_box.reset_mock()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\"), patch(\"builtins.open\"):\n                self.widget.load()\n                msg_box.assert_not_called()\n                msg_box.reset_mock()\n                self.widget._parse_var_defs.assert_called_with(json.load.return_value)\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_load_ignore_warning(self, msg_box):\n        self.widget._parse_var_defs(dict(categorical={}, numeric={}))\n        msg_box.assert_not_called()\n\n        no_change = dict(renamed_values={}, colors={})\n        for names, message in (\n                ((\"foo\",),\n                 \"'foo'\"),\n                ((\"foo\", \"bar\"),\n                 \"'foo' and 'bar'\"),\n                ((\"foo\", \"bar\", \"baz\"),\n                 \"'foo', 'bar' and 'baz'\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\"),\n                 \"'foo', 'bar', 'baz' and 'qux'\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\"),\n                 \"'foo', 'bar', 'baz', 'qux' and 'quux'\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\", \"corge\"),\n                 \"'foo', 'bar', 'baz', 'qux' and 2 other\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\", \"corge\", \"grault\"),\n                 \"'foo', 'bar', 'baz', 'qux' and 3 other\")):\n            self.widget._parse_var_defs(dict(\n                categorical=dict.fromkeys(names, no_change),\n                numeric={}))\n            self.assertIn(message, msg_box.call_args[0][2])\n\n    def _create_descs(self):\n        disc_vars = [DiscreteVariable(f\"var{c}\", values=(\"a\", \"b\", \"c\"))\n                     for c in \"AB\"]\n        cont_vars = [ContinuousVariable(f\"var{c}\") for c in \"CDE\"]\n        self.widget.disc_descs = [owcolor.DiscAttrDesc(v) for v in disc_vars]\n        self.widget.cont_descs = [owcolor.ContAttrDesc(v) for v in cont_vars]\n        return disc_vars, cont_vars\n\n    def test_parse_var_defs(self):\n        js = {\"categorical\": {\"varA\": {\"rename\": \"a2\"},\n                              \"varB\": {\"renamed_values\": {\"b\": \"X\"}}},\n              \"numeric\": {\"varC\": {\"rename\": \"c2\"},\n                          \"varD\": {\"colors\": \"linear_viridis\"}}}\n\n        self._create_descs()\n        descE = self.widget.cont_descs[-1]\n\n        self.widget._parse_var_defs(js)\n        self.assertEqual(len(self.widget.disc_descs), 2)\n        descA = self.widget.disc_descs[0]\n        self.assertEqual(descA.name, \"a2\")\n        self.assertIsNone(descA.new_values)\n        self.assertIsNone(descA.new_colors)\n\n        descB = self.widget.disc_descs[1]\n        self.assertIsNone(descB.new_name)\n        self.assertEqual(descB.new_values, [\"a\", \"X\", \"c\"])\n        self.assertIsNone(descB.new_colors)\n\n        self.assertEqual(len(self.widget.cont_descs), 3)\n        descC = self.widget.cont_descs[0]\n        self.assertEqual(descC.name, \"c2\")\n        self.assertIsNone(descC.new_palette_name)\n\n        descD = self.widget.cont_descs[1]\n        self.assertIsNone(descD.new_name)\n        self.assertEqual(descD.new_palette_name, \"linear_viridis\")\n\n        self.assertIs(self.widget.cont_descs[2], descE)\n\n    def test_parse_var_defs_invalid(self):\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs, 42)\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {\"a\": 42}, \"numeric\": {}})\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {\"a\": {\"rename\": 4}}, \"numeric\": {}})\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {42: {\"rename\": \"b\"}}, \"numeric\": {}})\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_parse_var_defs_shows_warnings(self, msg_box):\n        self._create_descs()\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"renamed_values\": {\"a\": \"b\"}}},\n             \"numeric\": {}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicate names\" in msg_box.call_args[0][2])\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_parse_var_defs_no_rename(self, msg_box):\n        self._create_descs()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"varB\"}},\n             \"numeric\": {}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicated names\" in msg_box.call_args[0][2])\n        msg_box.reset_mock()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"X\"}},\n             \"numeric\": {\"varD\": {\"rename\": \"X\"}}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicated names\" in msg_box.call_args[0][2])\n        msg_box.reset_mock()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"varD\"}},\n             \"numeric\": {\"varD\": {\"rename\": \"varA\"}}})\n        msg_box.assert_not_called()",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 619,
                            "end_lineno": 909,
                            "parent": null
                        }
                    ],
                    "added_entities": [
                        {
                            "file_name": "Orange/widgets/data/tests/test_owcolor.py",
                            "type": "method",
                            "name": "TestOWColor.test_load_ignore_warning",
                            "content": "class TestOWColor(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(owcolor.OWColor)\n        self.iris = Table(\"iris\")\n\n    def test_reuse_old_settings(self):\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        assert isinstance(self.widget, owcolor.OWColor)\n        self.widget.saveSettings()\n\n        w = self.create_widget(owcolor.OWColor, reset_default_settings=False)\n        self.send_signal(self.widget.Inputs.data, self.iris, widget=w)\n\n    def test_invalid_input_colors(self):\n        # Handling of invalid value in deprecated 'colors': warn, but don't fail\n        with self.assertWarns(DeprecationWarning):\n            a = ContinuousVariable(\"a\")\n            a.attributes[\"colors\"] = \"invalid\"\n            t = Table.from_domain(Domain([a]))\n            self.send_signal(self.widget.Inputs.data, t)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget.commit, 'now') as commit:\n            self.widget.auto_apply = False\n            commit.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.iris)\n            commit.assert_called()\n\n    def test_commit_on_data_changed(self):\n        widget = self.widget\n        model = widget.cont_model\n        self.send_signal(widget.Inputs.data, self.iris)\n        with patch.object(widget.commit, 'deferred') as commit:\n            commit.reset_mock()\n            model.setData(model.index(0, 0), \"y\", Qt.EditRole)\n            commit.assert_called()\n\n    def test_lose_data(self):\n        widget = self.widget\n        send = widget.Outputs.data.send = Mock()\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        send.assert_called_once()\n        self.assertEqual(widget.disc_model.rowCount(), 1)\n        self.assertEqual(widget.cont_model.rowCount(), 4)\n\n        send.reset_mock()\n        self.send_signal(widget.Inputs.data, None)\n        send.assert_called_with(None)\n        self.assertEqual(widget.disc_model.rowCount(), 0)\n        self.assertEqual(widget.cont_model.rowCount(), 0)\n\n    def test_model_content(self):\n        widget = self.widget\n        data = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, data)\n\n        dm = widget.disc_model\n        self.assertEqual(\n            [dm.data(dm.index(i, 0)) for i in range(dm.rowCount())],\n            [var.name for var in data.domain.variables if var.is_discrete]\n        )\n\n        cm = widget.disc_model\n        self.assertEqual(\n            [dm.data(cm.index(i, 0)) for i in range(cm.rowCount())],\n            [var.name for var in data.domain.variables if var.is_discrete]\n        )\n\n    def test_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, Table(\"zoo\"))\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.widget.send_report()\n\n    def test_string_variables(self):\n        self.send_signal(self.widget.Inputs.data, Table(\"zoo\"))\n\n    def test_reset(self):\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        cont_model = self.widget.cont_model\n        disc_model = self.widget.disc_model\n        cont_model.setData(cont_model.index(0, 0), \"a\", Qt.EditRole)\n        disc_model.setData(disc_model.index(0, 0), \"b\", Qt.EditRole)\n        outp = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(outp.domain[0].name, \"a\")\n        self.assertEqual(outp.domain.class_var.name, \"b\")\n        self.widget.reset()\n        outp = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(\n            outp.domain[0].name, self.iris.domain[0].name)\n        self.assertEqual(\n            outp.domain.class_var.name, self.iris.domain.class_var.name)\n\n    def test_save(self):\n        self.widget._save_var_defs = Mock()\n        with patch.object(owcolor.QFileDialog, \"getSaveFileName\",\n                          return_value=(\"\", \"\")):\n            self.widget.save()\n            self.widget._save_var_defs.assert_not_called()\n        with patch.object(owcolor.QFileDialog, \"getSaveFileName\",\n                          return_value=(\"foo\", \"bar\")):\n            self.widget.save()\n            self.widget._save_var_defs.assert_called_with(\"foo\")\n\n    def test_save_low(self):\n        descA = owcolor.DiscAttrDesc(\n            DiscreteVariable(\"varA\", values=tuple(\"abc\")))\n        descA.name = \"a2\"\n        descB = owcolor.DiscAttrDesc(\n            DiscreteVariable(\"varB\", values=tuple(\"abc\")))\n        descB.set_value(1, \"X\")\n        descC = owcolor.ContAttrDesc(ContinuousVariable(\"varC\"))\n        descC.name = \"c2\"\n        descD = owcolor.ContAttrDesc(ContinuousVariable(\"varD\"))\n        descD.palette_name = \"linear_viridis\"\n        descE = owcolor.ContAttrDesc(ContinuousVariable(\"varE\"))\n        self.widget.disc_descs = [descA, descB]\n        self.widget.cont_descs = [descC, descD, descE]\n\n        with tempfile.TemporaryDirectory() as path:\n            fname = os.path.join(path, \"foo.colors\")\n            self.widget._save_var_defs(fname)\n            with open(fname) as f:\n                js = json.load(f)\n            self.assertEqual(js[\"categorical\"],\n                             {\"varA\": {\"rename\": \"a2\"},\n                              \"varB\": {\"renamed_values\": {\"b\": \"X\"}}})\n            self.assertEqual(js[\"numeric\"],\n                             {\"varC\": {\"rename\": \"c2\"},\n                              \"varD\": {\"colors\": \"linear_viridis\"}})\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.critical\")\n    def test_load(self, msg_box):\n        self.widget._parse_var_defs = Mock()\n        self.widget._save_var_defs = Mock()\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"\", \"\")):\n            with patch(\"builtins.open\"):\n                self.widget.load()\n                open.assert_not_called()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\") as json_load, \\\n                    patch(\"builtins.open\", side_effect=IOError):\n                self.widget.load()\n                msg_box.assert_called()\n                msg_box.reset_mock()\n                json_load.assert_not_called()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\", side_effect=json.JSONDecodeError(\"err\", \"d\", 42)), \\\n                    patch(\"builtins.open\"):\n                self.widget.load()\n                msg_box.assert_called()\n                msg_box.reset_mock()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\"), patch(\"builtins.open\"):\n                self.widget.load()\n                msg_box.assert_not_called()\n                msg_box.reset_mock()\n                self.widget._parse_var_defs.assert_called_with(json.load.return_value)\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_load_ignore_warning(self, msg_box):\n        self.widget._parse_var_defs(dict(categorical={}, numeric={}))\n        msg_box.assert_not_called()\n\n        no_change = dict(renamed_values={}, colors={})\n        for names, message in (\n                ((\"foo\",),\n                 \"'foo'\"),\n                ((\"foo\", \"bar\"),\n                 \"'foo' and 'bar'\"),\n                ((\"foo\", \"bar\", \"baz\"),\n                 \"'foo', 'bar' and 'baz'\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\"),\n                 \"'foo', 'bar', 'baz' and 'qux'\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\"),\n                 \"'foo', 'bar', 'baz', 'qux' and 'quux'\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\", \"corge\"),\n                 \"'foo', 'bar', 'baz', 'qux' and 2 other\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\", \"corge\", \"grault\"),\n                 \"'foo', 'bar', 'baz', 'qux' and 3 other\")):\n            self.widget._parse_var_defs(dict(\n                categorical=dict.fromkeys(names, no_change),\n                numeric={}))\n            self.assertIn(message, msg_box.call_args[0][2])\n\n    def _create_descs(self):\n        disc_vars = [DiscreteVariable(f\"var{c}\", values=(\"a\", \"b\", \"c\"))\n                     for c in \"AB\"]\n        cont_vars = [ContinuousVariable(f\"var{c}\") for c in \"CDE\"]\n        self.widget.disc_descs = [owcolor.DiscAttrDesc(v) for v in disc_vars]\n        self.widget.cont_descs = [owcolor.ContAttrDesc(v) for v in cont_vars]\n        return disc_vars, cont_vars\n\n    def test_parse_var_defs(self):\n        js = {\"categorical\": {\"varA\": {\"rename\": \"a2\"},\n                              \"varB\": {\"renamed_values\": {\"b\": \"X\"}}},\n              \"numeric\": {\"varC\": {\"rename\": \"c2\"},\n                          \"varD\": {\"colors\": \"linear_viridis\"}}}\n\n        self._create_descs()\n        descE = self.widget.cont_descs[-1]\n\n        self.widget._parse_var_defs(js)\n        self.assertEqual(len(self.widget.disc_descs), 2)\n        descA = self.widget.disc_descs[0]\n        self.assertEqual(descA.name, \"a2\")\n        self.assertIsNone(descA.new_values)\n        self.assertIsNone(descA.new_colors)\n\n        descB = self.widget.disc_descs[1]\n        self.assertIsNone(descB.new_name)\n        self.assertEqual(descB.new_values, [\"a\", \"X\", \"c\"])\n        self.assertIsNone(descB.new_colors)\n\n        self.assertEqual(len(self.widget.cont_descs), 3)\n        descC = self.widget.cont_descs[0]\n        self.assertEqual(descC.name, \"c2\")\n        self.assertIsNone(descC.new_palette_name)\n\n        descD = self.widget.cont_descs[1]\n        self.assertIsNone(descD.new_name)\n        self.assertEqual(descD.new_palette_name, \"linear_viridis\")\n\n        self.assertIs(self.widget.cont_descs[2], descE)\n\n    def test_parse_var_defs_invalid(self):\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs, 42)\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {\"a\": 42}, \"numeric\": {}})\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {\"a\": {\"rename\": 4}}, \"numeric\": {}})\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {42: {\"rename\": \"b\"}}, \"numeric\": {}})\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_parse_var_defs_shows_warnings(self, msg_box):\n        self._create_descs()\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"renamed_values\": {\"a\": \"b\"}}},\n             \"numeric\": {}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicate names\" in msg_box.call_args[0][2])\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_parse_var_defs_no_rename(self, msg_box):\n        self._create_descs()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"varB\"}},\n             \"numeric\": {}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicated names\" in msg_box.call_args[0][2])\n        msg_box.reset_mock()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"X\"}},\n             \"numeric\": {\"varD\": {\"rename\": \"X\"}}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicated names\" in msg_box.call_args[0][2])\n        msg_box.reset_mock()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"varD\"}},\n             \"numeric\": {\"varD\": {\"rename\": \"varA\"}}})\n        msg_box.assert_not_called()",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 797,
                            "end_lineno": 820,
                            "parent": {
                                "file_name": "Orange/widgets/data/tests/test_owcolor.py",
                                "type": "class",
                                "name": "TestOWColor",
                                "content": "class TestOWColor(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(owcolor.OWColor)\n        self.iris = Table(\"iris\")\n\n    def test_reuse_old_settings(self):\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        assert isinstance(self.widget, owcolor.OWColor)\n        self.widget.saveSettings()\n\n        w = self.create_widget(owcolor.OWColor, reset_default_settings=False)\n        self.send_signal(self.widget.Inputs.data, self.iris, widget=w)\n\n    def test_invalid_input_colors(self):\n        # Handling of invalid value in deprecated 'colors': warn, but don't fail\n        with self.assertWarns(DeprecationWarning):\n            a = ContinuousVariable(\"a\")\n            a.attributes[\"colors\"] = \"invalid\"\n            t = Table.from_domain(Domain([a]))\n            self.send_signal(self.widget.Inputs.data, t)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget.commit, 'now') as commit:\n            self.widget.auto_apply = False\n            commit.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.iris)\n            commit.assert_called()\n\n    def test_commit_on_data_changed(self):\n        widget = self.widget\n        model = widget.cont_model\n        self.send_signal(widget.Inputs.data, self.iris)\n        with patch.object(widget.commit, 'deferred') as commit:\n            commit.reset_mock()\n            model.setData(model.index(0, 0), \"y\", Qt.EditRole)\n            commit.assert_called()\n\n    def test_lose_data(self):\n        widget = self.widget\n        send = widget.Outputs.data.send = Mock()\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        send.assert_called_once()\n        self.assertEqual(widget.disc_model.rowCount(), 1)\n        self.assertEqual(widget.cont_model.rowCount(), 4)\n\n        send.reset_mock()\n        self.send_signal(widget.Inputs.data, None)\n        send.assert_called_with(None)\n        self.assertEqual(widget.disc_model.rowCount(), 0)\n        self.assertEqual(widget.cont_model.rowCount(), 0)\n\n    def test_model_content(self):\n        widget = self.widget\n        data = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, data)\n\n        dm = widget.disc_model\n        self.assertEqual(\n            [dm.data(dm.index(i, 0)) for i in range(dm.rowCount())],\n            [var.name for var in data.domain.variables if var.is_discrete]\n        )\n\n        cm = widget.disc_model\n        self.assertEqual(\n            [dm.data(cm.index(i, 0)) for i in range(cm.rowCount())],\n            [var.name for var in data.domain.variables if var.is_discrete]\n        )\n\n    def test_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, Table(\"zoo\"))\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.widget.send_report()\n\n    def test_string_variables(self):\n        self.send_signal(self.widget.Inputs.data, Table(\"zoo\"))\n\n    def test_reset(self):\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        cont_model = self.widget.cont_model\n        disc_model = self.widget.disc_model\n        cont_model.setData(cont_model.index(0, 0), \"a\", Qt.EditRole)\n        disc_model.setData(disc_model.index(0, 0), \"b\", Qt.EditRole)\n        outp = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(outp.domain[0].name, \"a\")\n        self.assertEqual(outp.domain.class_var.name, \"b\")\n        self.widget.reset()\n        outp = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(\n            outp.domain[0].name, self.iris.domain[0].name)\n        self.assertEqual(\n            outp.domain.class_var.name, self.iris.domain.class_var.name)\n\n    def test_save(self):\n        self.widget._save_var_defs = Mock()\n        with patch.object(owcolor.QFileDialog, \"getSaveFileName\",\n                          return_value=(\"\", \"\")):\n            self.widget.save()\n            self.widget._save_var_defs.assert_not_called()\n        with patch.object(owcolor.QFileDialog, \"getSaveFileName\",\n                          return_value=(\"foo\", \"bar\")):\n            self.widget.save()\n            self.widget._save_var_defs.assert_called_with(\"foo\")\n\n    def test_save_low(self):\n        descA = owcolor.DiscAttrDesc(\n            DiscreteVariable(\"varA\", values=tuple(\"abc\")))\n        descA.name = \"a2\"\n        descB = owcolor.DiscAttrDesc(\n            DiscreteVariable(\"varB\", values=tuple(\"abc\")))\n        descB.set_value(1, \"X\")\n        descC = owcolor.ContAttrDesc(ContinuousVariable(\"varC\"))\n        descC.name = \"c2\"\n        descD = owcolor.ContAttrDesc(ContinuousVariable(\"varD\"))\n        descD.palette_name = \"linear_viridis\"\n        descE = owcolor.ContAttrDesc(ContinuousVariable(\"varE\"))\n        self.widget.disc_descs = [descA, descB]\n        self.widget.cont_descs = [descC, descD, descE]\n\n        with tempfile.TemporaryDirectory() as path:\n            fname = os.path.join(path, \"foo.colors\")\n            self.widget._save_var_defs(fname)\n            with open(fname) as f:\n                js = json.load(f)\n            self.assertEqual(js[\"categorical\"],\n                             {\"varA\": {\"rename\": \"a2\"},\n                              \"varB\": {\"renamed_values\": {\"b\": \"X\"}}})\n            self.assertEqual(js[\"numeric\"],\n                             {\"varC\": {\"rename\": \"c2\"},\n                              \"varD\": {\"colors\": \"linear_viridis\"}})\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.critical\")\n    def test_load(self, msg_box):\n        self.widget._parse_var_defs = Mock()\n        self.widget._save_var_defs = Mock()\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"\", \"\")):\n            with patch(\"builtins.open\"):\n                self.widget.load()\n                open.assert_not_called()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\") as json_load, \\\n                    patch(\"builtins.open\", side_effect=IOError):\n                self.widget.load()\n                msg_box.assert_called()\n                msg_box.reset_mock()\n                json_load.assert_not_called()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\", side_effect=json.JSONDecodeError(\"err\", \"d\", 42)), \\\n                    patch(\"builtins.open\"):\n                self.widget.load()\n                msg_box.assert_called()\n                msg_box.reset_mock()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\"), patch(\"builtins.open\"):\n                self.widget.load()\n                msg_box.assert_not_called()\n                msg_box.reset_mock()\n                self.widget._parse_var_defs.assert_called_with(json.load.return_value)\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_load_ignore_warning(self, msg_box):\n        self.widget._parse_var_defs(dict(categorical={}, numeric={}))\n        msg_box.assert_not_called()\n\n        no_change = dict(renamed_values={}, colors={})\n        for names, message in (\n                ((\"foo\",),\n                 \"'foo'\"),\n                ((\"foo\", \"bar\"),\n                 \"'foo' and 'bar'\"),\n                ((\"foo\", \"bar\", \"baz\"),\n                 \"'foo', 'bar' and 'baz'\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\"),\n                 \"'foo', 'bar', 'baz' and 'qux'\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\"),\n                 \"'foo', 'bar', 'baz', 'qux' and 'quux'\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\", \"corge\"),\n                 \"'foo', 'bar', 'baz', 'qux' and 2 other\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\", \"corge\", \"grault\"),\n                 \"'foo', 'bar', 'baz', 'qux' and 3 other\")):\n            self.widget._parse_var_defs(dict(\n                categorical=dict.fromkeys(names, no_change),\n                numeric={}))\n            self.assertIn(message, msg_box.call_args[0][2])\n\n    def _create_descs(self):\n        disc_vars = [DiscreteVariable(f\"var{c}\", values=(\"a\", \"b\", \"c\"))\n                     for c in \"AB\"]\n        cont_vars = [ContinuousVariable(f\"var{c}\") for c in \"CDE\"]\n        self.widget.disc_descs = [owcolor.DiscAttrDesc(v) for v in disc_vars]\n        self.widget.cont_descs = [owcolor.ContAttrDesc(v) for v in cont_vars]\n        return disc_vars, cont_vars\n\n    def test_parse_var_defs(self):\n        js = {\"categorical\": {\"varA\": {\"rename\": \"a2\"},\n                              \"varB\": {\"renamed_values\": {\"b\": \"X\"}}},\n              \"numeric\": {\"varC\": {\"rename\": \"c2\"},\n                          \"varD\": {\"colors\": \"linear_viridis\"}}}\n\n        self._create_descs()\n        descE = self.widget.cont_descs[-1]\n\n        self.widget._parse_var_defs(js)\n        self.assertEqual(len(self.widget.disc_descs), 2)\n        descA = self.widget.disc_descs[0]\n        self.assertEqual(descA.name, \"a2\")\n        self.assertIsNone(descA.new_values)\n        self.assertIsNone(descA.new_colors)\n\n        descB = self.widget.disc_descs[1]\n        self.assertIsNone(descB.new_name)\n        self.assertEqual(descB.new_values, [\"a\", \"X\", \"c\"])\n        self.assertIsNone(descB.new_colors)\n\n        self.assertEqual(len(self.widget.cont_descs), 3)\n        descC = self.widget.cont_descs[0]\n        self.assertEqual(descC.name, \"c2\")\n        self.assertIsNone(descC.new_palette_name)\n\n        descD = self.widget.cont_descs[1]\n        self.assertIsNone(descD.new_name)\n        self.assertEqual(descD.new_palette_name, \"linear_viridis\")\n\n        self.assertIs(self.widget.cont_descs[2], descE)\n\n    def test_parse_var_defs_invalid(self):\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs, 42)\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {\"a\": 42}, \"numeric\": {}})\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {\"a\": {\"rename\": 4}}, \"numeric\": {}})\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {42: {\"rename\": \"b\"}}, \"numeric\": {}})\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_parse_var_defs_shows_warnings(self, msg_box):\n        self._create_descs()\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"renamed_values\": {\"a\": \"b\"}}},\n             \"numeric\": {}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicate names\" in msg_box.call_args[0][2])\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_parse_var_defs_no_rename(self, msg_box):\n        self._create_descs()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"varB\"}},\n             \"numeric\": {}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicated names\" in msg_box.call_args[0][2])\n        msg_box.reset_mock()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"X\"}},\n             \"numeric\": {\"varD\": {\"rename\": \"X\"}}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicated names\" in msg_box.call_args[0][2])\n        msg_box.reset_mock()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"varD\"}},\n             \"numeric\": {\"varD\": {\"rename\": \"varA\"}}})\n        msg_box.assert_not_called()",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 619,
                                "end_lineno": 909,
                                "parent": null
                            }
                        }
                    ],
                    "deleted_entities": []
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 882,
                            "length": 11
                        },
                        "new_range": {
                            "start": 908,
                            "length": 6
                        },
                        "section": "class TestOWColor(WidgetTest):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "             \"numeric\": {\"varD\": {\"rename\": \"varA\"}}})",
                                "type": "context"
                            },
                            {
                                "content": "        msg_box.assert_not_called()",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "        self.widget._parse_var_defs(",
                                "type": "deleted"
                            },
                            {
                                "content": "            {\"categorical\": {\"varA\": {\"rename\": \"X\"}},",
                                "type": "deleted"
                            },
                            {
                                "content": "             \"numeric\": {\"var not\": {\"rename\": \"X\"}}})",
                                "type": "deleted"
                            },
                            {
                                "content": "        msg_box.assert_not_called()",
                                "type": "deleted"
                            },
                            {
                                "content": "",
                                "type": "deleted"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "if __name__ == \"__main__\":",
                                "type": "context"
                            },
                            {
                                "content": "    unittest.main()",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/data/tests/test_owcolor.py",
                            "type": "class",
                            "name": "TestOWColor",
                            "content": "class TestOWColor(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(owcolor.OWColor)\n        self.iris = Table(\"iris\")\n\n    def test_reuse_old_settings(self):\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        assert isinstance(self.widget, owcolor.OWColor)\n        self.widget.saveSettings()\n\n        w = self.create_widget(owcolor.OWColor, reset_default_settings=False)\n        self.send_signal(self.widget.Inputs.data, self.iris, widget=w)\n\n    def test_invalid_input_colors(self):\n        # Handling of invalid value in deprecated 'colors': warn, but don't fail\n        with self.assertWarns(DeprecationWarning):\n            a = ContinuousVariable(\"a\")\n            a.attributes[\"colors\"] = \"invalid\"\n            t = Table.from_domain(Domain([a]))\n            self.send_signal(self.widget.Inputs.data, t)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget.commit, 'now') as commit:\n            self.widget.auto_apply = False\n            commit.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.iris)\n            commit.assert_called()\n\n    def test_commit_on_data_changed(self):\n        widget = self.widget\n        model = widget.cont_model\n        self.send_signal(widget.Inputs.data, self.iris)\n        with patch.object(widget.commit, 'deferred') as commit:\n            commit.reset_mock()\n            model.setData(model.index(0, 0), \"y\", Qt.EditRole)\n            commit.assert_called()\n\n    def test_lose_data(self):\n        widget = self.widget\n        send = widget.Outputs.data.send = Mock()\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        send.assert_called_once()\n        self.assertEqual(widget.disc_model.rowCount(), 1)\n        self.assertEqual(widget.cont_model.rowCount(), 4)\n\n        send.reset_mock()\n        self.send_signal(widget.Inputs.data, None)\n        send.assert_called_with(None)\n        self.assertEqual(widget.disc_model.rowCount(), 0)\n        self.assertEqual(widget.cont_model.rowCount(), 0)\n\n    def test_model_content(self):\n        widget = self.widget\n        data = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, data)\n\n        dm = widget.disc_model\n        self.assertEqual(\n            [dm.data(dm.index(i, 0)) for i in range(dm.rowCount())],\n            [var.name for var in data.domain.variables if var.is_discrete]\n        )\n\n        cm = widget.disc_model\n        self.assertEqual(\n            [dm.data(cm.index(i, 0)) for i in range(cm.rowCount())],\n            [var.name for var in data.domain.variables if var.is_discrete]\n        )\n\n    def test_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, Table(\"zoo\"))\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.widget.send_report()\n\n    def test_string_variables(self):\n        self.send_signal(self.widget.Inputs.data, Table(\"zoo\"))\n\n    def test_reset(self):\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        cont_model = self.widget.cont_model\n        disc_model = self.widget.disc_model\n        cont_model.setData(cont_model.index(0, 0), \"a\", Qt.EditRole)\n        disc_model.setData(disc_model.index(0, 0), \"b\", Qt.EditRole)\n        outp = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(outp.domain[0].name, \"a\")\n        self.assertEqual(outp.domain.class_var.name, \"b\")\n        self.widget.reset()\n        outp = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(\n            outp.domain[0].name, self.iris.domain[0].name)\n        self.assertEqual(\n            outp.domain.class_var.name, self.iris.domain.class_var.name)\n\n    def test_save(self):\n        self.widget._save_var_defs = Mock()\n        with patch.object(owcolor.QFileDialog, \"getSaveFileName\",\n                          return_value=(\"\", \"\")):\n            self.widget.save()\n            self.widget._save_var_defs.assert_not_called()\n        with patch.object(owcolor.QFileDialog, \"getSaveFileName\",\n                          return_value=(\"foo\", \"bar\")):\n            self.widget.save()\n            self.widget._save_var_defs.assert_called_with(\"foo\")\n\n    def test_save_low(self):\n        descA = owcolor.DiscAttrDesc(\n            DiscreteVariable(\"varA\", values=tuple(\"abc\")))\n        descA.name = \"a2\"\n        descB = owcolor.DiscAttrDesc(\n            DiscreteVariable(\"varB\", values=tuple(\"abc\")))\n        descB.set_value(1, \"X\")\n        descC = owcolor.ContAttrDesc(ContinuousVariable(\"varC\"))\n        descC.name = \"c2\"\n        descD = owcolor.ContAttrDesc(ContinuousVariable(\"varD\"))\n        descD.palette_name = \"linear_viridis\"\n        descE = owcolor.ContAttrDesc(ContinuousVariable(\"varE\"))\n        self.widget.disc_descs = [descA, descB]\n        self.widget.cont_descs = [descC, descD, descE]\n\n        with tempfile.TemporaryDirectory() as path:\n            fname = os.path.join(path, \"foo.colors\")\n            self.widget._save_var_defs(fname)\n            with open(fname) as f:\n                js = json.load(f)\n            self.assertEqual(js[\"categorical\"],\n                             {\"varA\": {\"rename\": \"a2\"},\n                              \"varB\": {\"renamed_values\": {\"b\": \"X\"}}})\n            self.assertEqual(js[\"numeric\"],\n                             {\"varC\": {\"rename\": \"c2\"},\n                              \"varD\": {\"colors\": \"linear_viridis\"}})\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.critical\")\n    def test_load(self, msg_box):\n        self.widget._parse_var_defs = Mock()\n        self.widget._save_var_defs = Mock()\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"\", \"\")):\n            with patch(\"builtins.open\"):\n                self.widget.load()\n                open.assert_not_called()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\") as json_load, \\\n                    patch(\"builtins.open\", side_effect=IOError):\n                self.widget.load()\n                msg_box.assert_called()\n                msg_box.reset_mock()\n                json_load.assert_not_called()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\", side_effect=json.JSONDecodeError(\"err\", \"d\", 42)), \\\n                    patch(\"builtins.open\"):\n                self.widget.load()\n                msg_box.assert_called()\n                msg_box.reset_mock()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\"), patch(\"builtins.open\"):\n                self.widget.load()\n                msg_box.assert_not_called()\n                msg_box.reset_mock()\n                self.widget._parse_var_defs.assert_called_with(json.load.return_value)\n\n    def _create_descs(self):\n        disc_vars = [DiscreteVariable(f\"var{c}\", values=(\"a\", \"b\", \"c\"))\n                     for c in \"AB\"]\n        cont_vars = [ContinuousVariable(f\"var{c}\") for c in \"CDE\"]\n        self.widget.disc_descs = [owcolor.DiscAttrDesc(v) for v in disc_vars]\n        self.widget.cont_descs = [owcolor.ContAttrDesc(v) for v in cont_vars]\n        return disc_vars, cont_vars\n\n    def test_parse_var_defs(self):\n        js = {\"categorical\": {\"varA\": {\"rename\": \"a2\"},\n                              \"varB\": {\"renamed_values\": {\"b\": \"X\"}}},\n              \"numeric\": {\"varC\": {\"rename\": \"c2\"},\n                          \"varD\": {\"colors\": \"linear_viridis\"}}}\n\n        self._create_descs()\n        descE = self.widget.cont_descs[-1]\n\n        self.widget._parse_var_defs(js)\n        self.assertEqual(len(self.widget.disc_descs), 2)\n        descA = self.widget.disc_descs[0]\n        self.assertEqual(descA.name, \"a2\")\n        self.assertIsNone(descA.new_values)\n        self.assertIsNone(descA.new_colors)\n\n        descB = self.widget.disc_descs[1]\n        self.assertIsNone(descB.new_name)\n        self.assertEqual(descB.new_values, [\"a\", \"X\", \"c\"])\n        self.assertIsNone(descB.new_colors)\n\n        self.assertEqual(len(self.widget.cont_descs), 3)\n        descC = self.widget.cont_descs[0]\n        self.assertEqual(descC.name, \"c2\")\n        self.assertIsNone(descC.new_palette_name)\n\n        descD = self.widget.cont_descs[1]\n        self.assertIsNone(descD.new_name)\n        self.assertEqual(descD.new_palette_name, \"linear_viridis\")\n\n        self.assertIs(self.widget.cont_descs[2], descE)\n\n    def test_parse_var_defs_invalid(self):\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs, 42)\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {\"a\": 42}, \"numeric\": {}})\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {\"a\": {\"rename\": 4}}, \"numeric\": {}})\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {42: {\"rename\": \"b\"}}, \"numeric\": {}})\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_parse_var_defs_shows_warnings(self, msg_box):\n        self._create_descs()\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"renamed_values\": {\"a\": \"b\"}}},\n             \"numeric\": {}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicate names\" in msg_box.call_args[0][2])\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_parse_var_defs_no_rename(self, msg_box):\n        self._create_descs()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"varB\"}},\n             \"numeric\": {}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicated names\" in msg_box.call_args[0][2])\n        msg_box.reset_mock()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"X\"}},\n             \"numeric\": {\"varD\": {\"rename\": \"X\"}}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicated names\" in msg_box.call_args[0][2])\n        msg_box.reset_mock()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"varD\"}},\n             \"numeric\": {\"varD\": {\"rename\": \"varA\"}}})\n        msg_box.assert_not_called()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"X\"}},\n             \"numeric\": {\"var not\": {\"rename\": \"X\"}}})\n        msg_box.assert_not_called()",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 619,
                            "end_lineno": 888,
                            "parent": null
                        },
                        {
                            "file_name": "Orange/widgets/data/tests/test_owcolor.py",
                            "type": "method",
                            "name": "TestOWColor.test_parse_var_defs_no_rename",
                            "content": "class TestOWColor(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(owcolor.OWColor)\n        self.iris = Table(\"iris\")\n\n    def test_reuse_old_settings(self):\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        assert isinstance(self.widget, owcolor.OWColor)\n        self.widget.saveSettings()\n\n        w = self.create_widget(owcolor.OWColor, reset_default_settings=False)\n        self.send_signal(self.widget.Inputs.data, self.iris, widget=w)\n\n    def test_invalid_input_colors(self):\n        # Handling of invalid value in deprecated 'colors': warn, but don't fail\n        with self.assertWarns(DeprecationWarning):\n            a = ContinuousVariable(\"a\")\n            a.attributes[\"colors\"] = \"invalid\"\n            t = Table.from_domain(Domain([a]))\n            self.send_signal(self.widget.Inputs.data, t)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget.commit, 'now') as commit:\n            self.widget.auto_apply = False\n            commit.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.iris)\n            commit.assert_called()\n\n    def test_commit_on_data_changed(self):\n        widget = self.widget\n        model = widget.cont_model\n        self.send_signal(widget.Inputs.data, self.iris)\n        with patch.object(widget.commit, 'deferred') as commit:\n            commit.reset_mock()\n            model.setData(model.index(0, 0), \"y\", Qt.EditRole)\n            commit.assert_called()\n\n    def test_lose_data(self):\n        widget = self.widget\n        send = widget.Outputs.data.send = Mock()\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        send.assert_called_once()\n        self.assertEqual(widget.disc_model.rowCount(), 1)\n        self.assertEqual(widget.cont_model.rowCount(), 4)\n\n        send.reset_mock()\n        self.send_signal(widget.Inputs.data, None)\n        send.assert_called_with(None)\n        self.assertEqual(widget.disc_model.rowCount(), 0)\n        self.assertEqual(widget.cont_model.rowCount(), 0)\n\n    def test_model_content(self):\n        widget = self.widget\n        data = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, data)\n\n        dm = widget.disc_model\n        self.assertEqual(\n            [dm.data(dm.index(i, 0)) for i in range(dm.rowCount())],\n            [var.name for var in data.domain.variables if var.is_discrete]\n        )\n\n        cm = widget.disc_model\n        self.assertEqual(\n            [dm.data(cm.index(i, 0)) for i in range(cm.rowCount())],\n            [var.name for var in data.domain.variables if var.is_discrete]\n        )\n\n    def test_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, Table(\"zoo\"))\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.widget.send_report()\n\n    def test_string_variables(self):\n        self.send_signal(self.widget.Inputs.data, Table(\"zoo\"))\n\n    def test_reset(self):\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        cont_model = self.widget.cont_model\n        disc_model = self.widget.disc_model\n        cont_model.setData(cont_model.index(0, 0), \"a\", Qt.EditRole)\n        disc_model.setData(disc_model.index(0, 0), \"b\", Qt.EditRole)\n        outp = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(outp.domain[0].name, \"a\")\n        self.assertEqual(outp.domain.class_var.name, \"b\")\n        self.widget.reset()\n        outp = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(\n            outp.domain[0].name, self.iris.domain[0].name)\n        self.assertEqual(\n            outp.domain.class_var.name, self.iris.domain.class_var.name)\n\n    def test_save(self):\n        self.widget._save_var_defs = Mock()\n        with patch.object(owcolor.QFileDialog, \"getSaveFileName\",\n                          return_value=(\"\", \"\")):\n            self.widget.save()\n            self.widget._save_var_defs.assert_not_called()\n        with patch.object(owcolor.QFileDialog, \"getSaveFileName\",\n                          return_value=(\"foo\", \"bar\")):\n            self.widget.save()\n            self.widget._save_var_defs.assert_called_with(\"foo\")\n\n    def test_save_low(self):\n        descA = owcolor.DiscAttrDesc(\n            DiscreteVariable(\"varA\", values=tuple(\"abc\")))\n        descA.name = \"a2\"\n        descB = owcolor.DiscAttrDesc(\n            DiscreteVariable(\"varB\", values=tuple(\"abc\")))\n        descB.set_value(1, \"X\")\n        descC = owcolor.ContAttrDesc(ContinuousVariable(\"varC\"))\n        descC.name = \"c2\"\n        descD = owcolor.ContAttrDesc(ContinuousVariable(\"varD\"))\n        descD.palette_name = \"linear_viridis\"\n        descE = owcolor.ContAttrDesc(ContinuousVariable(\"varE\"))\n        self.widget.disc_descs = [descA, descB]\n        self.widget.cont_descs = [descC, descD, descE]\n\n        with tempfile.TemporaryDirectory() as path:\n            fname = os.path.join(path, \"foo.colors\")\n            self.widget._save_var_defs(fname)\n            with open(fname) as f:\n                js = json.load(f)\n            self.assertEqual(js[\"categorical\"],\n                             {\"varA\": {\"rename\": \"a2\"},\n                              \"varB\": {\"renamed_values\": {\"b\": \"X\"}}})\n            self.assertEqual(js[\"numeric\"],\n                             {\"varC\": {\"rename\": \"c2\"},\n                              \"varD\": {\"colors\": \"linear_viridis\"}})\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.critical\")\n    def test_load(self, msg_box):\n        self.widget._parse_var_defs = Mock()\n        self.widget._save_var_defs = Mock()\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"\", \"\")):\n            with patch(\"builtins.open\"):\n                self.widget.load()\n                open.assert_not_called()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\") as json_load, \\\n                    patch(\"builtins.open\", side_effect=IOError):\n                self.widget.load()\n                msg_box.assert_called()\n                msg_box.reset_mock()\n                json_load.assert_not_called()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\", side_effect=json.JSONDecodeError(\"err\", \"d\", 42)), \\\n                    patch(\"builtins.open\"):\n                self.widget.load()\n                msg_box.assert_called()\n                msg_box.reset_mock()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\"), patch(\"builtins.open\"):\n                self.widget.load()\n                msg_box.assert_not_called()\n                msg_box.reset_mock()\n                self.widget._parse_var_defs.assert_called_with(json.load.return_value)\n\n    def _create_descs(self):\n        disc_vars = [DiscreteVariable(f\"var{c}\", values=(\"a\", \"b\", \"c\"))\n                     for c in \"AB\"]\n        cont_vars = [ContinuousVariable(f\"var{c}\") for c in \"CDE\"]\n        self.widget.disc_descs = [owcolor.DiscAttrDesc(v) for v in disc_vars]\n        self.widget.cont_descs = [owcolor.ContAttrDesc(v) for v in cont_vars]\n        return disc_vars, cont_vars\n\n    def test_parse_var_defs(self):\n        js = {\"categorical\": {\"varA\": {\"rename\": \"a2\"},\n                              \"varB\": {\"renamed_values\": {\"b\": \"X\"}}},\n              \"numeric\": {\"varC\": {\"rename\": \"c2\"},\n                          \"varD\": {\"colors\": \"linear_viridis\"}}}\n\n        self._create_descs()\n        descE = self.widget.cont_descs[-1]\n\n        self.widget._parse_var_defs(js)\n        self.assertEqual(len(self.widget.disc_descs), 2)\n        descA = self.widget.disc_descs[0]\n        self.assertEqual(descA.name, \"a2\")\n        self.assertIsNone(descA.new_values)\n        self.assertIsNone(descA.new_colors)\n\n        descB = self.widget.disc_descs[1]\n        self.assertIsNone(descB.new_name)\n        self.assertEqual(descB.new_values, [\"a\", \"X\", \"c\"])\n        self.assertIsNone(descB.new_colors)\n\n        self.assertEqual(len(self.widget.cont_descs), 3)\n        descC = self.widget.cont_descs[0]\n        self.assertEqual(descC.name, \"c2\")\n        self.assertIsNone(descC.new_palette_name)\n\n        descD = self.widget.cont_descs[1]\n        self.assertIsNone(descD.new_name)\n        self.assertEqual(descD.new_palette_name, \"linear_viridis\")\n\n        self.assertIs(self.widget.cont_descs[2], descE)\n\n    def test_parse_var_defs_invalid(self):\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs, 42)\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {\"a\": 42}, \"numeric\": {}})\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {\"a\": {\"rename\": 4}}, \"numeric\": {}})\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {42: {\"rename\": \"b\"}}, \"numeric\": {}})\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_parse_var_defs_shows_warnings(self, msg_box):\n        self._create_descs()\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"renamed_values\": {\"a\": \"b\"}}},\n             \"numeric\": {}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicate names\" in msg_box.call_args[0][2])\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_parse_var_defs_no_rename(self, msg_box):\n        self._create_descs()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"varB\"}},\n             \"numeric\": {}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicated names\" in msg_box.call_args[0][2])\n        msg_box.reset_mock()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"X\"}},\n             \"numeric\": {\"varD\": {\"rename\": \"X\"}}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicated names\" in msg_box.call_args[0][2])\n        msg_box.reset_mock()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"varD\"}},\n             \"numeric\": {\"varD\": {\"rename\": \"varA\"}}})\n        msg_box.assert_not_called()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"X\"}},\n             \"numeric\": {\"var not\": {\"rename\": \"X\"}}})\n        msg_box.assert_not_called()",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 863,
                            "end_lineno": 888,
                            "parent": {
                                "file_name": "Orange/widgets/data/tests/test_owcolor.py",
                                "type": "class",
                                "name": "TestOWColor",
                                "content": "class TestOWColor(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(owcolor.OWColor)\n        self.iris = Table(\"iris\")\n\n    def test_reuse_old_settings(self):\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        assert isinstance(self.widget, owcolor.OWColor)\n        self.widget.saveSettings()\n\n        w = self.create_widget(owcolor.OWColor, reset_default_settings=False)\n        self.send_signal(self.widget.Inputs.data, self.iris, widget=w)\n\n    def test_invalid_input_colors(self):\n        # Handling of invalid value in deprecated 'colors': warn, but don't fail\n        with self.assertWarns(DeprecationWarning):\n            a = ContinuousVariable(\"a\")\n            a.attributes[\"colors\"] = \"invalid\"\n            t = Table.from_domain(Domain([a]))\n            self.send_signal(self.widget.Inputs.data, t)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget.commit, 'now') as commit:\n            self.widget.auto_apply = False\n            commit.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.iris)\n            commit.assert_called()\n\n    def test_commit_on_data_changed(self):\n        widget = self.widget\n        model = widget.cont_model\n        self.send_signal(widget.Inputs.data, self.iris)\n        with patch.object(widget.commit, 'deferred') as commit:\n            commit.reset_mock()\n            model.setData(model.index(0, 0), \"y\", Qt.EditRole)\n            commit.assert_called()\n\n    def test_lose_data(self):\n        widget = self.widget\n        send = widget.Outputs.data.send = Mock()\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        send.assert_called_once()\n        self.assertEqual(widget.disc_model.rowCount(), 1)\n        self.assertEqual(widget.cont_model.rowCount(), 4)\n\n        send.reset_mock()\n        self.send_signal(widget.Inputs.data, None)\n        send.assert_called_with(None)\n        self.assertEqual(widget.disc_model.rowCount(), 0)\n        self.assertEqual(widget.cont_model.rowCount(), 0)\n\n    def test_model_content(self):\n        widget = self.widget\n        data = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, data)\n\n        dm = widget.disc_model\n        self.assertEqual(\n            [dm.data(dm.index(i, 0)) for i in range(dm.rowCount())],\n            [var.name for var in data.domain.variables if var.is_discrete]\n        )\n\n        cm = widget.disc_model\n        self.assertEqual(\n            [dm.data(cm.index(i, 0)) for i in range(cm.rowCount())],\n            [var.name for var in data.domain.variables if var.is_discrete]\n        )\n\n    def test_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, Table(\"zoo\"))\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.widget.send_report()\n\n    def test_string_variables(self):\n        self.send_signal(self.widget.Inputs.data, Table(\"zoo\"))\n\n    def test_reset(self):\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        cont_model = self.widget.cont_model\n        disc_model = self.widget.disc_model\n        cont_model.setData(cont_model.index(0, 0), \"a\", Qt.EditRole)\n        disc_model.setData(disc_model.index(0, 0), \"b\", Qt.EditRole)\n        outp = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(outp.domain[0].name, \"a\")\n        self.assertEqual(outp.domain.class_var.name, \"b\")\n        self.widget.reset()\n        outp = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(\n            outp.domain[0].name, self.iris.domain[0].name)\n        self.assertEqual(\n            outp.domain.class_var.name, self.iris.domain.class_var.name)\n\n    def test_save(self):\n        self.widget._save_var_defs = Mock()\n        with patch.object(owcolor.QFileDialog, \"getSaveFileName\",\n                          return_value=(\"\", \"\")):\n            self.widget.save()\n            self.widget._save_var_defs.assert_not_called()\n        with patch.object(owcolor.QFileDialog, \"getSaveFileName\",\n                          return_value=(\"foo\", \"bar\")):\n            self.widget.save()\n            self.widget._save_var_defs.assert_called_with(\"foo\")\n\n    def test_save_low(self):\n        descA = owcolor.DiscAttrDesc(\n            DiscreteVariable(\"varA\", values=tuple(\"abc\")))\n        descA.name = \"a2\"\n        descB = owcolor.DiscAttrDesc(\n            DiscreteVariable(\"varB\", values=tuple(\"abc\")))\n        descB.set_value(1, \"X\")\n        descC = owcolor.ContAttrDesc(ContinuousVariable(\"varC\"))\n        descC.name = \"c2\"\n        descD = owcolor.ContAttrDesc(ContinuousVariable(\"varD\"))\n        descD.palette_name = \"linear_viridis\"\n        descE = owcolor.ContAttrDesc(ContinuousVariable(\"varE\"))\n        self.widget.disc_descs = [descA, descB]\n        self.widget.cont_descs = [descC, descD, descE]\n\n        with tempfile.TemporaryDirectory() as path:\n            fname = os.path.join(path, \"foo.colors\")\n            self.widget._save_var_defs(fname)\n            with open(fname) as f:\n                js = json.load(f)\n            self.assertEqual(js[\"categorical\"],\n                             {\"varA\": {\"rename\": \"a2\"},\n                              \"varB\": {\"renamed_values\": {\"b\": \"X\"}}})\n            self.assertEqual(js[\"numeric\"],\n                             {\"varC\": {\"rename\": \"c2\"},\n                              \"varD\": {\"colors\": \"linear_viridis\"}})\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.critical\")\n    def test_load(self, msg_box):\n        self.widget._parse_var_defs = Mock()\n        self.widget._save_var_defs = Mock()\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"\", \"\")):\n            with patch(\"builtins.open\"):\n                self.widget.load()\n                open.assert_not_called()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\") as json_load, \\\n                    patch(\"builtins.open\", side_effect=IOError):\n                self.widget.load()\n                msg_box.assert_called()\n                msg_box.reset_mock()\n                json_load.assert_not_called()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\", side_effect=json.JSONDecodeError(\"err\", \"d\", 42)), \\\n                    patch(\"builtins.open\"):\n                self.widget.load()\n                msg_box.assert_called()\n                msg_box.reset_mock()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\"), patch(\"builtins.open\"):\n                self.widget.load()\n                msg_box.assert_not_called()\n                msg_box.reset_mock()\n                self.widget._parse_var_defs.assert_called_with(json.load.return_value)\n\n    def _create_descs(self):\n        disc_vars = [DiscreteVariable(f\"var{c}\", values=(\"a\", \"b\", \"c\"))\n                     for c in \"AB\"]\n        cont_vars = [ContinuousVariable(f\"var{c}\") for c in \"CDE\"]\n        self.widget.disc_descs = [owcolor.DiscAttrDesc(v) for v in disc_vars]\n        self.widget.cont_descs = [owcolor.ContAttrDesc(v) for v in cont_vars]\n        return disc_vars, cont_vars\n\n    def test_parse_var_defs(self):\n        js = {\"categorical\": {\"varA\": {\"rename\": \"a2\"},\n                              \"varB\": {\"renamed_values\": {\"b\": \"X\"}}},\n              \"numeric\": {\"varC\": {\"rename\": \"c2\"},\n                          \"varD\": {\"colors\": \"linear_viridis\"}}}\n\n        self._create_descs()\n        descE = self.widget.cont_descs[-1]\n\n        self.widget._parse_var_defs(js)\n        self.assertEqual(len(self.widget.disc_descs), 2)\n        descA = self.widget.disc_descs[0]\n        self.assertEqual(descA.name, \"a2\")\n        self.assertIsNone(descA.new_values)\n        self.assertIsNone(descA.new_colors)\n\n        descB = self.widget.disc_descs[1]\n        self.assertIsNone(descB.new_name)\n        self.assertEqual(descB.new_values, [\"a\", \"X\", \"c\"])\n        self.assertIsNone(descB.new_colors)\n\n        self.assertEqual(len(self.widget.cont_descs), 3)\n        descC = self.widget.cont_descs[0]\n        self.assertEqual(descC.name, \"c2\")\n        self.assertIsNone(descC.new_palette_name)\n\n        descD = self.widget.cont_descs[1]\n        self.assertIsNone(descD.new_name)\n        self.assertEqual(descD.new_palette_name, \"linear_viridis\")\n\n        self.assertIs(self.widget.cont_descs[2], descE)\n\n    def test_parse_var_defs_invalid(self):\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs, 42)\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {\"a\": 42}, \"numeric\": {}})\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {\"a\": {\"rename\": 4}}, \"numeric\": {}})\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {42: {\"rename\": \"b\"}}, \"numeric\": {}})\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_parse_var_defs_shows_warnings(self, msg_box):\n        self._create_descs()\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"renamed_values\": {\"a\": \"b\"}}},\n             \"numeric\": {}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicate names\" in msg_box.call_args[0][2])\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_parse_var_defs_no_rename(self, msg_box):\n        self._create_descs()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"varB\"}},\n             \"numeric\": {}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicated names\" in msg_box.call_args[0][2])\n        msg_box.reset_mock()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"X\"}},\n             \"numeric\": {\"varD\": {\"rename\": \"X\"}}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicated names\" in msg_box.call_args[0][2])\n        msg_box.reset_mock()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"varD\"}},\n             \"numeric\": {\"varD\": {\"rename\": \"varA\"}}})\n        msg_box.assert_not_called()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"X\"}},\n             \"numeric\": {\"var not\": {\"rename\": \"X\"}}})\n        msg_box.assert_not_called()",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 619,
                                "end_lineno": 888,
                                "parent": null
                            }
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "50f6a758f1c66b8f4a18806714e3c2f4cefcab3e^",
    "new_commit_hash": "50f6a758f1c66b8f4a18806714e3c2f4cefcab3e",
    "commit_message": "Color: Warn about unused variable definitions",
    "commit_date": "2022-01-07T19:46:01+01:00",
    "metadata": {}
}
{
    "repo_name": "orange3",
    "new_commit_hash": "50f6a758f1c66b8f4a18806714e3c2f4cefcab3e",
    "test_file_codes": [
        "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring, protected-access,unsubscriptable-object\nimport json\nimport os\nimport tempfile\nimport unittest\nfrom unittest.mock import patch, Mock\n\nimport numpy as np\nfrom AnyQt.QtCore import Qt, QSize, QRect\nfrom AnyQt.QtGui import QBrush\n\nfrom orangewidget.tests.base import GuiTest\n\nfrom Orange.data import Table, ContinuousVariable, DiscreteVariable, Domain\nfrom Orange.preprocess.transformation import Identity\nfrom Orange.util import color_to_hex\nfrom Orange.widgets.utils import colorpalettes\nfrom Orange.widgets.data import owcolor\nfrom Orange.widgets.data.owcolor import ColorRole\nfrom Orange.widgets.tests.base import WidgetTest\n\n\nclass AttrDescTest(unittest.TestCase):\n    def test_name(self):\n        x = ContinuousVariable(\"x\")\n        desc = owcolor.AttrDesc(x)\n        self.assertEqual(desc.name, \"x\")\n        desc.name = \"y\"\n        self.assertEqual(desc.name, \"y\")\n        desc.name = None\n        self.assertEqual(desc.name, \"x\")\n\n    def test_reset(self):\n        x = ContinuousVariable(\"x\")\n        desc = owcolor.AttrDesc(x)\n        desc.reset()\n        self.assertEqual(desc.name, \"x\")\n        desc.name = \"y\"\n        desc.reset()\n        self.assertEqual(desc.name, \"x\")\n\n    def test_to_dict(self):\n        x = ContinuousVariable(\"x\")\n        desc = owcolor.AttrDesc(x)\n        self.assertEqual(desc.to_dict(), {})\n        desc2, warns = owcolor.AttrDesc.from_dict(x, desc.to_dict())\n        self.assertEqual(warns, [])\n        self.assertIsNone(desc2.new_name)\n\n        desc.name = \"y\"\n        self.assertEqual(desc.to_dict(), {\"rename\": \"y\"})\n\n        desc2, warns = owcolor.AttrDesc.from_dict(x, desc.to_dict())\n        self.assertEqual(warns, [])\n        self.assertEqual(desc2.new_name, \"y\")\n\n        self.assertRaises(owcolor.InvalidFileFormat,\n                          owcolor.AttrDesc.from_dict, x, {\"rename\": 42})\n        self.assertRaises(owcolor.InvalidFileFormat,\n                          owcolor.AttrDesc.from_dict, x, [])\n\n        # Additional keys shouldn't cause exceptions\n        owcolor.AttrDesc.from_dict(x, {\"foo\": 42})\n\n\nclass DiscAttrTest(unittest.TestCase):\n    def setUp(self):\n        self.var = DiscreteVariable(\"x\", [\"a\", \"b\", \"c\"])\n        self.desc = owcolor.DiscAttrDesc(self.var)\n\n    def test_colors(self):\n        desc = self.desc\n        colors = desc.colors.copy()\n        desc.set_color(2, (0, 0, 0))\n        colors[2] = 0\n        np.testing.assert_equal(desc.colors, colors)\n\n    def test_values(self):\n        desc = self.desc\n        self.assertEqual(desc.values, (\"a\", \"b\", \"c\"))\n        desc.set_value(1, \"d\")\n        self.assertEqual(desc.values, (\"a\", \"d\", \"c\"))\n\n    def test_create_variable(self):\n        desc = self.desc\n        desc.set_color(0, [1, 2, 3])\n        desc.set_color(1, [4, 5, 6])\n        desc.set_color(2, [7, 8, 9])\n        desc.name = \"z\"\n        desc.set_value(1, \"d\")\n        var = desc.create_variable()\n        self.assertIsInstance(var, DiscreteVariable)\n        self.assertEqual(var.name, \"z\")\n        self.assertEqual(var.values, (\"a\", \"d\", \"c\"))\n        np.testing.assert_equal(var.colors, [[1, 2, 3], [4, 5, 6], [7, 8, 9]])\n        self.assertIsInstance(var.compute_value, Identity)\n        self.assertIs(var.compute_value.variable, desc.var)\n\n        palette = desc.var.attributes[\"palette\"] = object()\n        var = desc.create_variable()\n        self.assertIs(desc.var.attributes[\"palette\"], palette)\n        self.assertFalse(hasattr(var.attributes, \"palette\"))\n        self.assertIsInstance(var.compute_value, Identity)\n        self.assertIs(var.compute_value.variable, desc.var)\n\n    def test_reset(self):\n        desc = self.desc\n        desc.set_color(0, [1, 2, 3])\n        desc.set_color(1, [4, 5, 6])\n        desc.set_color(2, [7, 8, 9])\n        desc.set_value(1, \"d\")\n        desc.reset()\n        np.testing.assert_equal(desc.colors, self.var.colors)\n        self.assertEqual(desc.values, self.var.values)\n\n    def test_to_dict(self):\n        desc = owcolor.DiscAttrDesc(self.var)\n        self.assertEqual(desc.to_dict(), {})\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(self.var, desc.to_dict())\n        self.assertEqual(warns, [])\n        self.assertIsNone(desc2.new_name)\n        self.assertIsNone(desc2.new_values)\n        self.assertIsNone(desc2.new_colors)\n\n        desc.name = \"y\"\n        self.assertEqual(desc.to_dict(), {\"rename\": \"y\"})\n\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(self.var, desc.to_dict())\n        self.assertEqual(warns, [])\n        self.assertEqual(desc2.new_name, \"y\")\n        self.assertIsNone(desc2.new_values)\n        self.assertIsNone(desc2.new_colors)\n\n        desc.set_value(1, \"b2\")\n        desc.set_color(1, [1, 2, 3])\n        desc.set_color(2, [2, 3, 4])\n        self.assertEqual(desc.to_dict(), {\n            \"rename\": \"y\",\n            \"renamed_values\": {\"b\": \"b2\"},\n            \"colors\": {\"a\": color_to_hex(desc.colors[0]),\n                       \"b\": \"#010203\",\n                       \"c\": \"#020304\"}\n        })\n\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(self.var, desc.to_dict())\n        self.assertEqual(warns, [])\n        cols = list(desc.colors)\n        cols[1:] = [[1, 2, 3], [2, 3, 4]]\n        np.testing.assert_equal(desc2.colors, cols)\n        self.assertEqual(desc2.values, (\"a\", \"b2\", \"c\"))\n\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(\n            self.var,\n            {\"rename\": \"y\",\n             \"renamed_values\": {\"b\": \"b2\", \"d\": \"x\"},  # d is redundant\n             \"colors\": {\"b\": \"#010203\",\n                        \"c\": \"#020304\",\n                        \"d\": \"#123456\"}  # d is redundant and must be ignored\n            })\n        self.assertEqual(warns, [])\n        cols = list(desc.colors)\n        cols[1:] = [[1, 2, 3], [2, 3, 4]]\n        np.testing.assert_equal(desc2.colors, cols)\n        self.assertEqual(desc2.values, (\"a\", \"b2\", \"c\"))\n\n    def test_from_dict_coliding_values(self):\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(\n            self.var, {\"renamed_values\": {\"a\": \"b\"}})\n        self.assertEqual(len(warns), 1)\n        self.assertTrue(\"duplicate names\" in warns[0])\n        self.assertIsNone(desc2.new_values)\n        self.assertEqual(desc2.values, (\"a\", \"b\", \"c\"))\n\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(\n            self.var, {\"renamed_values\": {\"a\": \"e\", \"b\": \"e\"}})\n        self.assertEqual(len(warns), 1)\n        self.assertTrue(\"duplicate names\" in warns[0])\n        self.assertIsNone(desc2.new_values)\n        self.assertEqual(desc2.values, (\"a\", \"b\", \"c\"))\n\n        desc2, warns = owcolor.DiscAttrDesc.from_dict(\n            self.var, {\"renamed_values\": {\"a\": \"b\", \"b\": \"a\"}})\n        self.assertEqual(warns, [])\n        self.assertEqual(desc2.values, (\"b\", \"a\", \"c\"))\n\n    def test_from_dict_exceptions(self):\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.AttrDesc.from_dict, self.var, [])\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"rename\": 42}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"colors\": []}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"colors\": {\"a\": 42}}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"colors\": {4: \"#000000\"}}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"colors\": {\"a\": \"#00\"}}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"colors\": {\"a\": \"#qwerty\"}}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"renamed_values\": []}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"renamed_values\": {\"a\": 42}}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.DiscAttrDesc.from_dict, self.var, {\"renamed_values\": {4: \"#000000\"}}\n        )\n\n\nclass ContAttrDescTest(unittest.TestCase):\n    def setUp(self):\n        x = ContinuousVariable(\"x\")\n        self.desc = owcolor.ContAttrDesc(x)\n\n    def test_palette(self):\n        desc = self.desc\n        palette = desc.palette_name\n        self.assertIsInstance(palette, str)\n        desc.palette_name = \"foo\"\n        self.assertEqual(desc.palette_name, \"foo\")\n        desc.palette_name = None\n        self.assertEqual(desc.palette_name, palette)\n\n    def test_create_variable(self):\n        desc = self.desc\n        desc.name = \"z\"\n        palette_name = _find_other_palette(\n            colorpalettes.ContinuousPalettes[desc.palette_name]).name\n        desc.palette_name = palette_name\n        var = desc.create_variable()\n        self.assertIsInstance(var, ContinuousVariable)\n        self.assertEqual(var.name, \"z\")\n        self.assertEqual(var.palette.name, palette_name)\n        self.assertIsInstance(var.compute_value, Identity)\n        self.assertIs(var.compute_value.variable, desc.var)\n\n        colors = desc.var.attributes[\"colors\"] = object()\n        var = desc.create_variable()\n        self.assertIs(desc.var.attributes[\"colors\"], colors)\n        self.assertFalse(hasattr(var.attributes, \"colors\"))\n        self.assertIsInstance(var.compute_value, Identity)\n        self.assertIs(var.compute_value.variable, desc.var)\n\n    def test_reset(self):\n        desc = self.desc\n        palette_name = desc.palette_name\n        desc.palette_name = _find_other_palette(\n            colorpalettes.ContinuousPalettes[palette_name]).name\n        desc.reset()\n        np.testing.assert_equal(desc.palette_name, palette_name)\n\n    def test_to_dict(self):\n        x = ContinuousVariable(\"x\")\n        desc = owcolor.ContAttrDesc(x)\n        self.assertEqual(desc.to_dict(), {})\n        desc2, warns = owcolor.ContAttrDesc.from_dict(x, desc.to_dict())\n        self.assertEqual(warns, [])\n        self.assertIsNone(desc2.new_name)\n        self.assertIsNone(desc2.new_palette_name)\n\n        desc.name = \"y\"\n        self.assertEqual(desc.to_dict(), {\"rename\": \"y\"})\n\n        desc2, warns = owcolor.ContAttrDesc.from_dict(x, desc.to_dict())\n        self.assertEqual(warns, [])\n        self.assertEqual(desc2.new_name, \"y\")\n        self.assertIsNone(desc2.new_palette_name)\n\n        desc = owcolor.ContAttrDesc(x)\n        desc.palette_name = \"linear_viridis\"\n        self.assertEqual(desc.to_dict(), {\"colors\": \"linear_viridis\"})\n\n    def test_from_dict_exceptions(self):\n        x = ContinuousVariable(\"x\")\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.ContAttrDesc.from_dict, x, []\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.ContAttrDesc.from_dict, x, {\"colors\": 42}\n        )\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            owcolor.ContAttrDesc.from_dict, x, {\"colors\": \"no such palette\"}\n        )\n\n\nclass BaseTestColorTableModel:\n    def test_row_count(self):\n        model = self.model\n\n        self.assertEqual(model.rowCount(), 0)\n        model.set_data(self.descs)\n        self.assertEqual(model.rowCount(), len(self.descs))\n        self.assertEqual(model.rowCount(self.model.index(0, 0)), 0)\n\n    def test_data(self):\n        self.model.set_data(self.descs)\n        data = self.model.data\n\n        index = self.model.index(1, 0)\n        self.assertEqual(data(index, Qt.DisplayRole), self.descs[1].name)\n        self.assertEqual(data(index, Qt.EditRole), self.descs[1].name)\n        self.assertTrue(data(index, Qt.FontRole).bold())\n        self.assertTrue(data(index, Qt.TextAlignmentRole) & Qt.AlignRight)\n\n        self.descs[1].name = \"bar\"\n        self.assertEqual(data(index), \"bar\")\n\n        index = self.model.index(2, 0)\n        self.assertEqual(data(index, Qt.DisplayRole), self.descs[2].name)\n\n    def test_set_data(self):\n        emit = Mock()\n        try:\n            self.model.dataChanged.connect(emit)\n            self.model.set_data(self.descs)\n            data = self.model.data\n            setData = self.model.setData\n\n            index = self.model.index(1, 0)\n            assert self.descs[1].name != \"foo\"\n            self.assertFalse(setData(index, \"foo\", Qt.DisplayRole))\n            emit.assert_not_called()\n            self.assertEqual(data(index, Qt.DisplayRole), self.descs[1].name)\n            self.assertTrue(setData(index, \"foo\", Qt.EditRole))\n            emit.assert_called()\n            self.assertEqual(data(index, Qt.DisplayRole), \"foo\")\n            self.assertEqual(self.descs[1].name, \"foo\")\n        finally:\n            self.model.dataChanged.disconnect(emit)\n\n    def test_reset(self):\n        for desc in self.descs:\n            desc.reset = Mock()\n        self.model.set_data(self.descs)\n        self.model.reset()\n        for desc in self.descs:\n            desc.reset.assert_called()\n\n\nclass TestDiscColorTableModel(GuiTest, BaseTestColorTableModel):\n    def setUp(self):\n        x = DiscreteVariable(\"x\", list(\"abc\"))\n        y = DiscreteVariable(\"y\", list(\"def\"))\n        z = DiscreteVariable(\"z\", list(\"ghijk\"))\n        self.descs = [owcolor.DiscAttrDesc(v) for v in (x, y, z)]\n        self.model = owcolor.DiscColorTableModel()\n\n    def test_column_count(self):\n        model = self.model\n\n        self.assertEqual(model.columnCount(), 1)\n        model.set_data(self.descs[:2])\n        self.assertEqual(model.columnCount(), 4)\n        model.set_data(self.descs)\n        self.assertEqual(model.columnCount(), 6)\n\n        self.assertEqual(model.columnCount(model.index(0, 0)), 0)\n\n    def test_data(self):\n        super().test_data()\n\n        model = self.model\n\n        self.assertIsNone(model.data(model.index(0, 4)))\n\n        index = model.index(1, 2)\n        self.assertEqual(model.data(index, Qt.DisplayRole), \"e\")\n        self.assertEqual(model.data(index, Qt.EditRole), \"e\")\n        font = model.data(index, Qt.FontRole)\n        self.assertTrue(font is None or not font.bold())\n\n        var_colors = self.descs[1].var.colors[1]\n        color = model.data(index, Qt.DecorationRole)\n        np.testing.assert_equal(color.getRgb()[:3], var_colors)\n\n        color = model.data(index, owcolor.ColorRole)\n        np.testing.assert_equal(color, var_colors)\n\n        self.assertEqual(\n            model.data(index, Qt.ToolTipRole), color_to_hex(var_colors))\n\n        self.assertIsNone(model.data(model.index(0, 4)))\n\n        index = model.index(2, 5)\n        self.assertEqual(model.data(index, Qt.DisplayRole), \"k\")\n        self.assertEqual(model.data(index, Qt.EditRole), \"k\")\n        font = model.data(index, Qt.FontRole)\n        self.assertTrue(font is None or not font.bold())\n\n        var_colors = self.descs[2].var.colors[4]\n        color = model.data(index, Qt.DecorationRole)\n        np.testing.assert_equal(color.getRgb()[:3], var_colors)\n\n        color = model.data(index, owcolor.ColorRole)\n        np.testing.assert_equal(color, var_colors)\n\n        self.assertEqual(\n            model.data(index, Qt.ToolTipRole), color_to_hex(var_colors))\n\n        self.descs[2].set_value(4, \"foo\")\n        self.assertEqual(model.data(index, Qt.DisplayRole), \"foo\")\n\n    def test_set_data(self):\n        super().test_set_data()\n\n        model = self.model\n        emit = Mock()\n        try:\n            model.dataChanged.connect(emit)\n\n            index = model.index(2, 5)\n\n            self.assertEqual(model.data(index, Qt.DisplayRole), \"k\")\n            self.assertEqual(model.data(index, Qt.EditRole), \"k\")\n            self.assertFalse(model.setData(index, \"foo\", Qt.DisplayRole))\n            emit.assert_not_called()\n            self.assertEqual(model.data(index, Qt.DisplayRole), \"k\")\n            self.assertTrue(model.setData(index, \"foo\", Qt.EditRole))\n            emit.assert_called()\n            emit.reset_mock()\n            self.assertEqual(model.data(index, Qt.DisplayRole), \"foo\")\n            self.assertEqual(self.descs[2].values, (\"g\", \"h\", \"i\", \"j\", \"foo\"))\n\n            new_color = [0, 1, 2]\n            self.assertTrue(model.setData(index, new_color + [255], ColorRole))\n            emit.assert_called()\n            emit.reset_mock()\n            color = model.data(index, Qt.DecorationRole)\n            rgb = [color.red(), color.green(), color.blue()]\n            self.assertEqual(rgb, new_color)\n\n            color = model.data(index, owcolor.ColorRole)\n            self.assertEqual(list(color), new_color)\n\n            self.assertEqual(\n                model.data(index, Qt.ToolTipRole), color_to_hex(new_color))\n\n            np.testing.assert_equal(self.descs[2].colors[4], rgb)\n        finally:\n            model.dataChanged.disconnect(emit)\n\n\ndef _find_other_palette(initial):\n    for palette in colorpalettes.ContinuousPalettes.values():\n        if palette.name != initial.name:\n            return palette\n    return None  # pragma: no cover\n\n\nclass TestContColorTableModel(GuiTest, BaseTestColorTableModel):\n    def setUp(self):\n        z = ContinuousVariable(\"z\")\n        w = ContinuousVariable(\"w\")\n        u = ContinuousVariable(\"u\")\n        self.descs = [owcolor.ContAttrDesc(v) for v in (z, w, u)]\n        self.model = owcolor.ContColorTableModel()\n\n    def test_column_count(self):\n        model = self.model\n\n        model.set_data(self.descs)\n        self.assertEqual(model.columnCount(), 3)\n        self.assertEqual(model.columnCount(model.index(0, 0)), 0)\n\n    def test_data(self):\n        super().test_data()\n\n        model = self.model\n        index = model.index(1, 1)\n        palette = colorpalettes.ContinuousPalettes[self.descs[1].palette_name]\n        self.assertEqual(model.data(index, Qt.ToolTipRole),\n                         palette.friendly_name)\n        self.assertEqual(model.data(index, ColorRole), palette)\n        with patch.object(palette, \"color_strip\") as color_strip:\n            strip = model.data(index, owcolor.StripRole)\n            self.assertIs(strip, color_strip.return_value)\n            color_strip.assert_called_with(128, 16)\n        self.assertIsInstance(model.data(index, Qt.SizeHintRole), QSize)\n        self.assertIsNone(model.data(index, Qt.FontRole))\n\n        palette = _find_other_palette(self.descs[1])\n        self.descs[1].palette_name = palette.name\n        self.assertIs(model.data(index, ColorRole), palette)\n\n        index = self.model.index(1, 2)\n        self.assertIsNone(model.data(index, Qt.ToolTipRole))\n        self.assertIsInstance(model.data(index, Qt.SizeHintRole), QSize)\n        self.assertIsInstance(model.data(index, Qt.ForegroundRole), QBrush)\n        self.assertIsNone(model.data(index, Qt.DisplayRole))\n        model.set_mouse_row(0)\n        self.assertIsNone(model.data(index, Qt.DisplayRole))\n        model.set_mouse_row(1)\n        self.assertEqual(model.data(index, Qt.DisplayRole), \"Copy to all\")\n\n    def test_set_data(self):\n        super().test_set_data()\n\n        model = self.model\n        index = model.index(1, 1)\n        index2 = model.index(2, 1)\n        initial = model.data(index, ColorRole)\n        initial2 = model.data(index, ColorRole)\n        assert initial.name == initial2.name\n        palette = _find_other_palette(initial)\n\n        emit = Mock()\n        try:\n            model.dataChanged.connect(emit)\n\n            self.assertFalse(model.setData(index, None, Qt.DisplayRole))\n            emit.assert_not_called()\n\n            self.assertTrue(model.setData(index, palette, ColorRole))\n            emit.assert_called()\n            self.assertIs(model.data(index2, ColorRole), initial2)\n\n            self.assertEqual(model.data(index, Qt.ToolTipRole),\n                             palette.friendly_name)\n            self.assertEqual(model.data(index, ColorRole), palette)\n            self.assertEqual(self.descs[1].palette_name, palette.name)\n            with patch.object(palette, \"color_strip\") as color_strip:\n                strip = model.data(index, owcolor.StripRole)\n                self.assertIs(strip, color_strip.return_value)\n                color_strip.assert_called_with(128, 16)\n        finally:\n            model.dataChanged.disconnect(emit)\n\n    def test_copy_to_all(self):\n        super().test_set_data()\n\n        model = self.model\n        index = model.index(1, 1)\n        initial = model.data(index, ColorRole)\n        palette = _find_other_palette(initial)\n\n        emit = Mock()\n        try:\n            model.dataChanged.connect(emit)\n            model.setData(index, palette, ColorRole)\n            emit.assert_called()\n            emit.reset_mock()\n\n            model.copy_to_all(index)\n            emit.assert_called_once()\n            for row, desc in enumerate(self.descs):\n                self.assertEqual(\n                    model.data(model.index(row, 1), ColorRole).name,\n                    palette.name)\n                self.assertEqual(desc.palette_name, palette.name)\n        finally:\n            model.dataChanged.disconnect(emit)\n\n\nclass TestColorStripDelegate(GuiTest):\n    def setUp(self):\n        z = ContinuousVariable(\"z\")\n        w = ContinuousVariable(\"w\")\n        u = ContinuousVariable(\"u\")\n        self.descs = [owcolor.ContAttrDesc(v) for v in (z, w, u)]\n        self.model = owcolor.ContColorTableModel()\n        self.model.set_data(self.descs)\n        self.table = owcolor.ContinuousTable(self.model)\n\n    def test_color_combo(self):\n        model = self.model\n        index = model.index(1, 1)\n        initial = model.data(index, ColorRole)\n        palette = _find_other_palette(initial)\n        model.setData(index, palette, ColorRole)\n        self.assertEqual(self.descs[1].palette_name, palette.name)\n\n        combo = self.table.color_delegate.createEditor(None, Mock(), index)\n        self.assertEqual(combo.currentText(), palette.friendly_name)\n        palette = _find_other_palette(palette)\n        combo.setCurrentIndex(combo.findText(palette.friendly_name))\n        self.assertEqual(self.descs[1].palette_name, palette.name)\n\n        with patch.object(self.table, \"closeEditor\") as closeEditor:\n            combo.hidePopup()\n            closeEditor.assert_called()\n\n    @patch.object(owcolor.HorizontalGridDelegate, \"paint\")\n    def test_paint(self, _):\n        model = self.model\n        index = model.index(1, 1)\n        painter = Mock()\n        option = Mock()\n        option.rect = QRect(10, 20, 30, 40)\n        index.data = Mock()\n        index.data.return_value = Mock()\n        index.data.return_value.height = Mock(return_value=42)\n        self.table.color_delegate.paint(painter, option, index)\n        self.assertIs(painter.drawPixmap.call_args[0][2],\n                      index.data.return_value)\n\n\nclass TestOWColor(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(owcolor.OWColor)\n        self.iris = Table(\"iris\")\n\n    def test_reuse_old_settings(self):\n        self.send_signal(self.widget.Inputs.data, self.iris)\n\n        assert isinstance(self.widget, owcolor.OWColor)\n        self.widget.saveSettings()\n\n        w = self.create_widget(owcolor.OWColor, reset_default_settings=False)\n        self.send_signal(self.widget.Inputs.data, self.iris, widget=w)\n\n    def test_invalid_input_colors(self):\n        # Handling of invalid value in deprecated 'colors': warn, but don't fail\n        with self.assertWarns(DeprecationWarning):\n            a = ContinuousVariable(\"a\")\n            a.attributes[\"colors\"] = \"invalid\"\n            t = Table.from_domain(Domain([a]))\n            self.send_signal(self.widget.Inputs.data, t)\n\n    def test_unconditional_commit_on_new_signal(self):\n        with patch.object(self.widget.commit, 'now') as commit:\n            self.widget.auto_apply = False\n            commit.reset_mock()\n            self.send_signal(self.widget.Inputs.data, self.iris)\n            commit.assert_called()\n\n    def test_commit_on_data_changed(self):\n        widget = self.widget\n        model = widget.cont_model\n        self.send_signal(widget.Inputs.data, self.iris)\n        with patch.object(widget.commit, 'deferred') as commit:\n            commit.reset_mock()\n            model.setData(model.index(0, 0), \"y\", Qt.EditRole)\n            commit.assert_called()\n\n    def test_lose_data(self):\n        widget = self.widget\n        send = widget.Outputs.data.send = Mock()\n\n        self.send_signal(widget.Inputs.data, self.iris)\n        send.assert_called_once()\n        self.assertEqual(widget.disc_model.rowCount(), 1)\n        self.assertEqual(widget.cont_model.rowCount(), 4)\n\n        send.reset_mock()\n        self.send_signal(widget.Inputs.data, None)\n        send.assert_called_with(None)\n        self.assertEqual(widget.disc_model.rowCount(), 0)\n        self.assertEqual(widget.cont_model.rowCount(), 0)\n\n    def test_model_content(self):\n        widget = self.widget\n        data = Table(\"heart_disease\")\n        self.send_signal(widget.Inputs.data, data)\n\n        dm = widget.disc_model\n        self.assertEqual(\n            [dm.data(dm.index(i, 0)) for i in range(dm.rowCount())],\n            [var.name for var in data.domain.variables if var.is_discrete]\n        )\n\n        cm = widget.disc_model\n        self.assertEqual(\n            [dm.data(cm.index(i, 0)) for i in range(cm.rowCount())],\n            [var.name for var in data.domain.variables if var.is_discrete]\n        )\n\n    def test_report(self):\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, Table(\"zoo\"))\n        self.widget.send_report()\n\n        self.send_signal(self.widget.Inputs.data, None)\n        self.widget.send_report()\n\n    def test_string_variables(self):\n        self.send_signal(self.widget.Inputs.data, Table(\"zoo\"))\n\n    def test_reset(self):\n        self.send_signal(self.widget.Inputs.data, self.iris)\n        cont_model = self.widget.cont_model\n        disc_model = self.widget.disc_model\n        cont_model.setData(cont_model.index(0, 0), \"a\", Qt.EditRole)\n        disc_model.setData(disc_model.index(0, 0), \"b\", Qt.EditRole)\n        outp = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(outp.domain[0].name, \"a\")\n        self.assertEqual(outp.domain.class_var.name, \"b\")\n        self.widget.reset()\n        outp = self.get_output(self.widget.Outputs.data)\n        self.assertEqual(\n            outp.domain[0].name, self.iris.domain[0].name)\n        self.assertEqual(\n            outp.domain.class_var.name, self.iris.domain.class_var.name)\n\n    def test_save(self):\n        self.widget._save_var_defs = Mock()\n        with patch.object(owcolor.QFileDialog, \"getSaveFileName\",\n                          return_value=(\"\", \"\")):\n            self.widget.save()\n            self.widget._save_var_defs.assert_not_called()\n        with patch.object(owcolor.QFileDialog, \"getSaveFileName\",\n                          return_value=(\"foo\", \"bar\")):\n            self.widget.save()\n            self.widget._save_var_defs.assert_called_with(\"foo\")\n\n    def test_save_low(self):\n        descA = owcolor.DiscAttrDesc(\n            DiscreteVariable(\"varA\", values=tuple(\"abc\")))\n        descA.name = \"a2\"\n        descB = owcolor.DiscAttrDesc(\n            DiscreteVariable(\"varB\", values=tuple(\"abc\")))\n        descB.set_value(1, \"X\")\n        descC = owcolor.ContAttrDesc(ContinuousVariable(\"varC\"))\n        descC.name = \"c2\"\n        descD = owcolor.ContAttrDesc(ContinuousVariable(\"varD\"))\n        descD.palette_name = \"linear_viridis\"\n        descE = owcolor.ContAttrDesc(ContinuousVariable(\"varE\"))\n        self.widget.disc_descs = [descA, descB]\n        self.widget.cont_descs = [descC, descD, descE]\n\n        with tempfile.TemporaryDirectory() as path:\n            fname = os.path.join(path, \"foo.colors\")\n            self.widget._save_var_defs(fname)\n            with open(fname) as f:\n                js = json.load(f)\n            self.assertEqual(js[\"categorical\"],\n                             {\"varA\": {\"rename\": \"a2\"},\n                              \"varB\": {\"renamed_values\": {\"b\": \"X\"}}})\n            self.assertEqual(js[\"numeric\"],\n                             {\"varC\": {\"rename\": \"c2\"},\n                              \"varD\": {\"colors\": \"linear_viridis\"}})\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.critical\")\n    def test_load(self, msg_box):\n        self.widget._parse_var_defs = Mock()\n        self.widget._save_var_defs = Mock()\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"\", \"\")):\n            with patch(\"builtins.open\"):\n                self.widget.load()\n                open.assert_not_called()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\") as json_load, \\\n                    patch(\"builtins.open\", side_effect=IOError):\n                self.widget.load()\n                msg_box.assert_called()\n                msg_box.reset_mock()\n                json_load.assert_not_called()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\", side_effect=json.JSONDecodeError(\"err\", \"d\", 42)), \\\n                    patch(\"builtins.open\"):\n                self.widget.load()\n                msg_box.assert_called()\n                msg_box.reset_mock()\n                self.widget._parse_var_defs.assert_not_called()\n\n        with patch.object(owcolor.QFileDialog, \"getOpenFileName\",\n                          return_value=(\"foo.colors\", \"*.colors\")):\n            with patch(\"json.load\"), patch(\"builtins.open\"):\n                self.widget.load()\n                msg_box.assert_not_called()\n                msg_box.reset_mock()\n                self.widget._parse_var_defs.assert_called_with(json.load.return_value)\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_load_ignore_warning(self, msg_box):\n        self.widget._parse_var_defs(dict(categorical={}, numeric={}))\n        msg_box.assert_not_called()\n\n        no_change = dict(renamed_values={}, colors={})\n        for names, message in (\n                ((\"foo\",),\n                 \"'foo'\"),\n                ((\"foo\", \"bar\"),\n                 \"'foo' and 'bar'\"),\n                ((\"foo\", \"bar\", \"baz\"),\n                 \"'foo', 'bar' and 'baz'\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\"),\n                 \"'foo', 'bar', 'baz' and 'qux'\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\"),\n                 \"'foo', 'bar', 'baz', 'qux' and 'quux'\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\", \"corge\"),\n                 \"'foo', 'bar', 'baz', 'qux' and 2 other\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\", \"corge\", \"grault\"),\n                 \"'foo', 'bar', 'baz', 'qux' and 3 other\")):\n            self.widget._parse_var_defs(dict(\n                categorical=dict.fromkeys(names, no_change),\n                numeric={}))\n            self.assertIn(message, msg_box.call_args[0][2])\n\n    def _create_descs(self):\n        disc_vars = [DiscreteVariable(f\"var{c}\", values=(\"a\", \"b\", \"c\"))\n                     for c in \"AB\"]\n        cont_vars = [ContinuousVariable(f\"var{c}\") for c in \"CDE\"]\n        self.widget.disc_descs = [owcolor.DiscAttrDesc(v) for v in disc_vars]\n        self.widget.cont_descs = [owcolor.ContAttrDesc(v) for v in cont_vars]\n        return disc_vars, cont_vars\n\n    def test_parse_var_defs(self):\n        js = {\"categorical\": {\"varA\": {\"rename\": \"a2\"},\n                              \"varB\": {\"renamed_values\": {\"b\": \"X\"}}},\n              \"numeric\": {\"varC\": {\"rename\": \"c2\"},\n                          \"varD\": {\"colors\": \"linear_viridis\"}}}\n\n        self._create_descs()\n        descE = self.widget.cont_descs[-1]\n\n        self.widget._parse_var_defs(js)\n        self.assertEqual(len(self.widget.disc_descs), 2)\n        descA = self.widget.disc_descs[0]\n        self.assertEqual(descA.name, \"a2\")\n        self.assertIsNone(descA.new_values)\n        self.assertIsNone(descA.new_colors)\n\n        descB = self.widget.disc_descs[1]\n        self.assertIsNone(descB.new_name)\n        self.assertEqual(descB.new_values, [\"a\", \"X\", \"c\"])\n        self.assertIsNone(descB.new_colors)\n\n        self.assertEqual(len(self.widget.cont_descs), 3)\n        descC = self.widget.cont_descs[0]\n        self.assertEqual(descC.name, \"c2\")\n        self.assertIsNone(descC.new_palette_name)\n\n        descD = self.widget.cont_descs[1]\n        self.assertIsNone(descD.new_name)\n        self.assertEqual(descD.new_palette_name, \"linear_viridis\")\n\n        self.assertIs(self.widget.cont_descs[2], descE)\n\n    def test_parse_var_defs_invalid(self):\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs, 42)\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {\"a\": 42}, \"numeric\": {}})\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {\"a\": {\"rename\": 4}}, \"numeric\": {}})\n        self.assertRaises(\n            owcolor.InvalidFileFormat,\n            self.widget._parse_var_defs,\n            {\"categorical\": {42: {\"rename\": \"b\"}}, \"numeric\": {}})\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_parse_var_defs_shows_warnings(self, msg_box):\n        self._create_descs()\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"renamed_values\": {\"a\": \"b\"}}},\n             \"numeric\": {}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicate names\" in msg_box.call_args[0][2])\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_parse_var_defs_no_rename(self, msg_box):\n        self._create_descs()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"varB\"}},\n             \"numeric\": {}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicated names\" in msg_box.call_args[0][2])\n        msg_box.reset_mock()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"X\"}},\n             \"numeric\": {\"varD\": {\"rename\": \"X\"}}})\n        msg_box.assert_called()\n        self.assertTrue(\"duplicated names\" in msg_box.call_args[0][2])\n        msg_box.reset_mock()\n\n        self.widget._parse_var_defs(\n            {\"categorical\": {\"varA\": {\"rename\": \"varD\"}},\n             \"numeric\": {\"varD\": {\"rename\": \"varA\"}}})\n        msg_box.assert_not_called()\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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/Orange3.egg-link (link to .)\nAdding Orange3 3.32.0.dev0+50f6a75 to easy-install.pth file\nInstalling orange-canvas script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e\nProcessing dependencies for Orange3==3.32.0.dev0+50f6a75\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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages\nSearching for pyqtgraph==0.12.4\nBest match: pyqtgraph 0.12.4\nAdding pyqtgraph 0.12.4 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\nInstalling pip3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\nInstalling pip3.10 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\nInstalling f2py3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\nInstalling f2py3.7 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages\nSearching for httpx==0.19.0\nBest match: httpx 0.19.0\nAdding httpx 0.19.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\nInstalling pyrcc5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\nInstalling pyuic5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\nInstalling jupyter-kernelspec script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\nInstalling jupyter-run script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\nInstalling jupyter-migrate script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\nInstalling jupyter-troubleshoot script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\nInstalling pyftmerge script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\nInstalling pyftsubset script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\nInstalling ttx script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages\nSearching for rfc3986==1.5.0\nBest match: rfc3986 1.5.0\nAdding rfc3986 1.5.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages\nSearching for httpcore==0.13.7\nBest match: httpcore 0.13.7\nAdding httpcore 0.13.7 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\nInstalling iptest3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\nInstalling ipython script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\nInstalling ipython3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages\nSearching for h11==0.12.0\nBest match: h11 0.12.0\nAdding h11 0.12.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages\nFinished processing dependencies for Orange3==3.32.0.dev0+50f6a75\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 124ms\nPrepared 18 packages in 1ms\nInstalled 18 packages in 100ms\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 55 packages in 416ms\nInstalled 48 packages in 577ms\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.12.0\n + httpcore==0.13.7\n + httpx==0.19.0\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 + rfc3986==1.5.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 297ms\nInstalled 38 packages in 601ms\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.4\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 7ms\nInstalled 2 packages in 48ms\n + psycopg2==2.9.9\n + pymssql==2.3.2\nResolved 19 packages in 49ms\nInstalled 5 packages in 922ms\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 2ms\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e/.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 '*.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 'COPYING'\n",
    "new_commit_res_code": 0,
    "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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e\nplugins: anyio-3.7.1\ncollected 51 items\n\nr2e_tests/test_1.py sss................................................  [100%]\n\n==================================== PASSES ====================================\n___________________ TestColorStripDelegate.test_color_combo ____________________\n----------------------------- Captured stderr call -----------------------------\nThis plugin does not support raise()\nThis plugin does not support grabbing the keyboard\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::AttrDescTest::test_name\nPASSED r2e_tests/test_1.py::AttrDescTest::test_reset\nPASSED r2e_tests/test_1.py::AttrDescTest::test_to_dict\nPASSED r2e_tests/test_1.py::DiscAttrTest::test_colors\nPASSED r2e_tests/test_1.py::DiscAttrTest::test_create_variable\nPASSED r2e_tests/test_1.py::DiscAttrTest::test_from_dict_coliding_values\nPASSED r2e_tests/test_1.py::DiscAttrTest::test_from_dict_exceptions\nPASSED r2e_tests/test_1.py::DiscAttrTest::test_reset\nPASSED r2e_tests/test_1.py::DiscAttrTest::test_to_dict\nPASSED r2e_tests/test_1.py::DiscAttrTest::test_values\nPASSED r2e_tests/test_1.py::ContAttrDescTest::test_create_variable\nPASSED r2e_tests/test_1.py::ContAttrDescTest::test_from_dict_exceptions\nPASSED r2e_tests/test_1.py::ContAttrDescTest::test_palette\nPASSED r2e_tests/test_1.py::ContAttrDescTest::test_reset\nPASSED r2e_tests/test_1.py::ContAttrDescTest::test_to_dict\nPASSED r2e_tests/test_1.py::TestDiscColorTableModel::test_column_count\nPASSED r2e_tests/test_1.py::TestDiscColorTableModel::test_data\nPASSED r2e_tests/test_1.py::TestDiscColorTableModel::test_reset\nPASSED r2e_tests/test_1.py::TestDiscColorTableModel::test_row_count\nPASSED r2e_tests/test_1.py::TestDiscColorTableModel::test_set_data\nPASSED r2e_tests/test_1.py::TestContColorTableModel::test_column_count\nPASSED r2e_tests/test_1.py::TestContColorTableModel::test_copy_to_all\nPASSED r2e_tests/test_1.py::TestContColorTableModel::test_data\nPASSED r2e_tests/test_1.py::TestContColorTableModel::test_reset\nPASSED r2e_tests/test_1.py::TestContColorTableModel::test_row_count\nPASSED r2e_tests/test_1.py::TestContColorTableModel::test_set_data\nPASSED r2e_tests/test_1.py::TestColorStripDelegate::test_color_combo\nPASSED r2e_tests/test_1.py::TestColorStripDelegate::test_paint\nPASSED r2e_tests/test_1.py::TestOWColor::test_commit_on_data_changed\nPASSED r2e_tests/test_1.py::TestOWColor::test_image_export\nPASSED r2e_tests/test_1.py::TestOWColor::test_invalid_input_colors\nPASSED r2e_tests/test_1.py::TestOWColor::test_load\nPASSED r2e_tests/test_1.py::TestOWColor::test_load_ignore_warning\nPASSED r2e_tests/test_1.py::TestOWColor::test_lose_data\nPASSED r2e_tests/test_1.py::TestOWColor::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWColor::test_model_content\nPASSED r2e_tests/test_1.py::TestOWColor::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWColor::test_parse_var_defs\nPASSED r2e_tests/test_1.py::TestOWColor::test_parse_var_defs_invalid\nPASSED r2e_tests/test_1.py::TestOWColor::test_parse_var_defs_no_rename\nPASSED r2e_tests/test_1.py::TestOWColor::test_parse_var_defs_shows_warnings\nPASSED r2e_tests/test_1.py::TestOWColor::test_report\nPASSED r2e_tests/test_1.py::TestOWColor::test_reset\nPASSED r2e_tests/test_1.py::TestOWColor::test_reuse_old_settings\nPASSED r2e_tests/test_1.py::TestOWColor::test_save\nPASSED r2e_tests/test_1.py::TestOWColor::test_save_low\nPASSED r2e_tests/test_1.py::TestOWColor::test_string_variables\nPASSED r2e_tests/test_1.py::TestOWColor::test_unconditional_commit_on_new_signal\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\n======================== 48 passed, 3 skipped in 5.56s =========================\nWidgets left: 0    Max widgets: 1806 \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_50f6a758f1c66b8f4a18806714e3c2f4cefcab3e\nplugins: anyio-3.7.1\ncollected 51 items\n\nr2e_tests/test_1.py sss................................F...............  [100%]\n\n=================================== FAILURES ===================================\n_____________________ TestOWColor.test_load_ignore_warning _____________________\n\nself = <r2e_tests.test_1.TestOWColor testMethod=test_load_ignore_warning>\nmsg_box = <MagicMock name='warning' id='137650995264848'>\n\n    @patch(\"Orange.widgets.data.owcolor.QMessageBox.warning\")\n    def test_load_ignore_warning(self, msg_box):\n        self.widget._parse_var_defs(dict(categorical={}, numeric={}))\n        msg_box.assert_not_called()\n    \n        no_change = dict(renamed_values={}, colors={})\n        for names, message in (\n                ((\"foo\",),\n                 \"'foo'\"),\n                ((\"foo\", \"bar\"),\n                 \"'foo' and 'bar'\"),\n                ((\"foo\", \"bar\", \"baz\"),\n                 \"'foo', 'bar' and 'baz'\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\"),\n                 \"'foo', 'bar', 'baz' and 'qux'\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\"),\n                 \"'foo', 'bar', 'baz', 'qux' and 'quux'\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\", \"corge\"),\n                 \"'foo', 'bar', 'baz', 'qux' and 2 other\"),\n                ((\"foo\", \"bar\", \"baz\", \"qux\", \"quux\", \"corge\", \"grault\"),\n                 \"'foo', 'bar', 'baz', 'qux' and 3 other\")):\n            self.widget._parse_var_defs(dict(\n                categorical=dict.fromkeys(names, no_change),\n                numeric={}))\n>           self.assertIn(message, msg_box.call_args[0][2])\nE           TypeError: 'NoneType' object is not subscriptable\n\nr2e_tests/test_1.py:820: TypeError\n==================================== PASSES ====================================\n___________________ TestColorStripDelegate.test_color_combo ____________________\n----------------------------- Captured stderr call -----------------------------\nThis plugin does not support raise()\nThis plugin does not support grabbing the keyboard\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::AttrDescTest::test_name\nPASSED r2e_tests/test_1.py::AttrDescTest::test_reset\nPASSED r2e_tests/test_1.py::AttrDescTest::test_to_dict\nPASSED r2e_tests/test_1.py::DiscAttrTest::test_colors\nPASSED r2e_tests/test_1.py::DiscAttrTest::test_create_variable\nPASSED r2e_tests/test_1.py::DiscAttrTest::test_from_dict_coliding_values\nPASSED r2e_tests/test_1.py::DiscAttrTest::test_from_dict_exceptions\nPASSED r2e_tests/test_1.py::DiscAttrTest::test_reset\nPASSED r2e_tests/test_1.py::DiscAttrTest::test_to_dict\nPASSED r2e_tests/test_1.py::DiscAttrTest::test_values\nPASSED r2e_tests/test_1.py::ContAttrDescTest::test_create_variable\nPASSED r2e_tests/test_1.py::ContAttrDescTest::test_from_dict_exceptions\nPASSED r2e_tests/test_1.py::ContAttrDescTest::test_palette\nPASSED r2e_tests/test_1.py::ContAttrDescTest::test_reset\nPASSED r2e_tests/test_1.py::ContAttrDescTest::test_to_dict\nPASSED r2e_tests/test_1.py::TestDiscColorTableModel::test_column_count\nPASSED r2e_tests/test_1.py::TestDiscColorTableModel::test_data\nPASSED r2e_tests/test_1.py::TestDiscColorTableModel::test_reset\nPASSED r2e_tests/test_1.py::TestDiscColorTableModel::test_row_count\nPASSED r2e_tests/test_1.py::TestDiscColorTableModel::test_set_data\nPASSED r2e_tests/test_1.py::TestContColorTableModel::test_column_count\nPASSED r2e_tests/test_1.py::TestContColorTableModel::test_copy_to_all\nPASSED r2e_tests/test_1.py::TestContColorTableModel::test_data\nPASSED r2e_tests/test_1.py::TestContColorTableModel::test_reset\nPASSED r2e_tests/test_1.py::TestContColorTableModel::test_row_count\nPASSED r2e_tests/test_1.py::TestContColorTableModel::test_set_data\nPASSED r2e_tests/test_1.py::TestColorStripDelegate::test_color_combo\nPASSED r2e_tests/test_1.py::TestColorStripDelegate::test_paint\nPASSED r2e_tests/test_1.py::TestOWColor::test_commit_on_data_changed\nPASSED r2e_tests/test_1.py::TestOWColor::test_image_export\nPASSED r2e_tests/test_1.py::TestOWColor::test_invalid_input_colors\nPASSED r2e_tests/test_1.py::TestOWColor::test_load\nPASSED r2e_tests/test_1.py::TestOWColor::test_lose_data\nPASSED r2e_tests/test_1.py::TestOWColor::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWColor::test_model_content\nPASSED r2e_tests/test_1.py::TestOWColor::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWColor::test_parse_var_defs\nPASSED r2e_tests/test_1.py::TestOWColor::test_parse_var_defs_invalid\nPASSED r2e_tests/test_1.py::TestOWColor::test_parse_var_defs_no_rename\nPASSED r2e_tests/test_1.py::TestOWColor::test_parse_var_defs_shows_warnings\nPASSED r2e_tests/test_1.py::TestOWColor::test_report\nPASSED r2e_tests/test_1.py::TestOWColor::test_reset\nPASSED r2e_tests/test_1.py::TestOWColor::test_reuse_old_settings\nPASSED r2e_tests/test_1.py::TestOWColor::test_save\nPASSED r2e_tests/test_1.py::TestOWColor::test_save_low\nPASSED r2e_tests/test_1.py::TestOWColor::test_string_variables\nPASSED r2e_tests/test_1.py::TestOWColor::test_unconditional_commit_on_new_signal\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\nFAILED r2e_tests/test_1.py::TestOWColor::test_load_ignore_warning - TypeError...\n=================== 1 failed, 47 passed, 3 skipped in 3.58s ====================\nWidgets left: 0    Max widgets: 1806 \n\n",
    "old_commit_res_stderr": ""
}
Orange/widgets/data/owcolor.py Orange/widgets/data/tests/test_owcolor.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** Error Occurs When Loading Variable Definitions with Unused Variables

**Description:**
When loading variable definitions that include unused variables, the application fails to handle the warnings properly. This results in a `TypeError` because the warning message is not correctly generated or handled.

**Example:**
```python
# Attempt to parse variable definitions with unused variables
self.widget._parse_var_defs({
    "categorical": {
        "foo": {"renamed_values": {}},
        "bar": {"renamed_values": {}}
    },
    "numeric": {}
})
```

**Expected Behavior:**
The application should display a warning message indicating that the variables `'foo'` and `'bar'` are defined but not used in the data. No errors should occur during this process.

**Actual Behavior:**
A `TypeError` is raised with the message: `'NoneType' object is not subscriptable`, preventing the warning from being displayed and disrupting the loading process.

[/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/owcolor.py
import os
from itertools import chain
import json

import numpy as np

from AnyQt.QtCore import Qt, QSize, QAbstractTableModel, QModelIndex, QTimer, \
    QSettings
from AnyQt.QtGui import QColor, QFont, QBrush
from AnyQt.QtWidgets import QHeaderView, QColorDialog, QTableView, QComboBox, \
    QFileDialog, QMessageBox

from orangewidget.settings import IncompatibleContext

import Orange
from Orange.preprocess.transformation import Identity
from Orange.util import color_to_hex, hex_to_color
from Orange.widgets import widget, settings, gui
from Orange.widgets.gui import HorizontalGridDelegate
from Orange.widgets.utils import itemmodels, colorpalettes
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.report import colored_square as square
from Orange.widgets.widget import Input, Output

ColorRole = next(gui.OrangeUserRole)
StripRole = next(gui.OrangeUserRole)


class InvalidFileFormat(Exception):
    pass


class AttrDesc:
    """
    Describes modifications that will be applied to variable.

    Provides methods that return either the modified value or the original

    Attributes:
        var (Variable): an instance of variable
        new_name (str or `None`): a changed name or `None`
    """
    def __init__(self, var):
        self.var = var
        self.new_name = None

    def reset(self):
        self.new_name = None

    @property
    def name(self):
        return self.new_name or self.var.name

    @name.setter
    def name(self, name):
        self.new_name = name

    def to_dict(self):
        d = {}
        if self.new_name is not None:
            d["rename"] = self.new_name
        return d

    @classmethod
    def from_dict(cls, var, data):
        desc = cls(var)
        if not isinstance(data, dict):
            raise InvalidFileFormat
        new_name = data.get("rename")
        if new_name is not None:
            if not isinstance(new_name, str):
                raise InvalidFileFormat
            desc.name = new_name
        return desc, []


class DiscAttrDesc(AttrDesc):
    """
    Describes modifications that will be applied to variable.

    Provides methods that return either the modified value or the original

    Attributes:
        var (DiscreteVariable): an instance of variable
        name (str or `None`): a changed name or `None`
        new_colors (list of tuple or None): new colors as tuples (R, G, B)
        new_values (list of str or None): new names for values, if changed
    """
    def __init__(self, var):
        super().__init__(var)
        self.new_colors = None
        self.new_values = None

    def reset(self):
        super().reset()
        self.new_colors = None
        self.new_values = None

    @property
    def colors(self):
        if self.new_colors is None:
            return self.var.colors
        else:
            return self.new_colors

    def set_color(self, i, color):
        if self.new_colors is None:
            self.new_colors = list(self.var.colors)
        self.new_colors[i] = color

    @property
    def values(self):
        return tuple(self.new_values or self.var.values)

    def set_value(self, i, value):
        if not self.new_values:
            self.new_values = list(self.var.values)
        self.new_values[i] = value

    def create_variable(self):
        new_var = self.var.copy(name=self.name, values=self.values,
                                compute_value=Identity(self.var))
        new_var.colors = np.asarray(self.colors)
        return new_var

    def to_dict(self):
        d = super().to_dict()
        if self.new_values is not None:
            d["renamed_values"] = \
                {k: v
                 for k, v in zip(self.var.values, self.new_values)
                 if k != v}
        if self.new_colors is not None:
            d["colors"] = {
                value: color_to_hex(color)
                for value, color in zip(self.var.values, self.colors)}
        return d

    @classmethod
    def from_dict(cls, var, data):

        def _check_dict_str_str(d):
            if not isinstance(d, dict) or \
                    not all(isinstance(val, str)
                            for val in chain(d, d.values())):
                raise InvalidFileFormat

        obj, warnings = super().from_dict(var, data)

        val_map = data.get("renamed_values")
        if val_map is not None:
            _check_dict_str_str(val_map)
            mapped_values = [val_map.get(value, value) for value in var.values]
            if len(set(mapped_values)) != len(mapped_values):
                warnings.append(
                    f"{var.name}: "
                    "renaming of values ignored due to duplicate names")
            else:
                obj.new_values = mapped_values

        new_colors = data.get("colors")
        if new_colors is not None:
            _check_dict_str_str(new_colors)
            colors = []
            for value, def_color in zip(var.values, var.palette.palette):
                if value in new_colors:
                    try:
                        color = hex_to_color(new_colors[value])
                    except ValueError as exc:
                        raise InvalidFileFormat from exc
                    colors.append(color)
                else:
                    colors.append(def_color)
                obj.new_colors = colors
        return obj, warnings


class ContAttrDesc(AttrDesc):
    """
    Describes modifications that will be applied to variable.

    Provides methods that return either the modified value or the original

    Attributes:
        var (ContinuousVariable): an instance of variable
        name (str or `None`): a changed name or `None`
        palette_name (str or None): name of palette or None if unmodified
    """
    def __init__(self, var):
        super().__init__(var)
        self.new_palette_name = self._default_palette_name()

    def reset(self):
        super().reset()
        self.new_palette_name = self._default_palette_name()

    def _default_palette_name(self):
        if self.var.palette.name not in colorpalettes.ContinuousPalettes:
            return colorpalettes.DefaultContinuousPaletteName
        else:
            return None

    @property
    def palette_name(self):
        return self.new_palette_name or self.var.palette.name

    @palette_name.setter
    def palette_name(self, palette_name):
        self.new_palette_name = palette_name

    def create_variable(self):
        new_var = self.var.copy(name=self.name,
                                compute_value=Identity(self.var))
        new_var.attributes["palette"] = self.palette_name
        return new_var

    def to_dict(self):
        d = super().to_dict()
        if self.new_palette_name is not None:
            d["colors"] = self.palette_name
        return d

    @classmethod
    def from_dict(cls, var, data):
        obj, warnings = super().from_dict(var, data)
        colors = data.get("colors")
        if colors is not None:
            if colors not in colorpalettes.ContinuousPalettes:
                raise InvalidFileFormat
            obj.palette_name = colors
        return obj, warnings


class ColorTableModel(QAbstractTableModel):
    """
    Base color model for discrete and continuous variables. The model handles:
    - the first column - variable name (including setData)
    - flags
    - row count, computed as len(attrdescs)

    Attribute:
        attrdescs (list of AttrDesc): attrdescs with user-defined changes
    """
    def __init__(self):
        QAbstractTableModel.__init__(self)
        self.attrdescs = []

    @staticmethod
    def flags(_):  # pragma: no cover
        return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable

    def set_data(self, attrdescs):
        self.modelAboutToBeReset.emit()
        self.attrdescs = attrdescs
        self.modelReset.emit()

    def rowCount(self, parent=QModelIndex()):
        return 0 if parent.isValid() else len(self.attrdescs)

    def data(self, index, role=Qt.DisplayRole):
        # Only valid for the first column; derived classes implement the rest
        row = index.row()
        if role in (Qt.DisplayRole, Qt.EditRole):
            return self.attrdescs[row].name
        if role == Qt.FontRole:
            font = QFont()
            font.setBold(True)
            return font
        if role == Qt.TextAlignmentRole:
            return Qt.AlignRight | Qt.AlignVCenter
        return None

    def setData(self, index, value, role):
        # Only valid for the first column; derived classes implement the rest
        if role == Qt.EditRole:
            self.attrdescs[index.row()].name = value
        else:
            return False
        self.dataChanged.emit(index, index)
        return True

    def reset(self):
        self.beginResetModel()
        for desc in self.attrdescs:
            desc.reset()
        self.endResetModel()


class DiscColorTableModel(ColorTableModel):
    """
    A model that stores the colors corresponding to values of discrete
    variables. Colors are shown as decorations.
    """
    def columnCount(self, parent=QModelIndex()):
        if parent.isValid():
            return 0
        return 1 + max((len(row.var.values) for row in self.attrdescs),
                       default=0)

    def data(self, index, role=Qt.DisplayRole):
        # pylint: disable=too-many-return-statements
        row, col = index.row(), index.column()
        if col == 0:
            return super().data(index, role)

        desc = self.attrdescs[row]
        if col > len(desc.var.values):
            return None
        if role in (Qt.DisplayRole, Qt.EditRole):
            return desc.values[col - 1]

        color = desc.colors[col - 1]
        if role == Qt.DecorationRole:
            return QColor(*color)
        if role == Qt.ToolTipRole:
            return color_to_hex(color)
        if role == ColorRole:
            return color
        return None

    def setData(self, index, value, role):
        row, col = index.row(), index.column()
        if col == 0:
            return super().setData(index, value, role)

        desc = self.attrdescs[row]
        if role == ColorRole:
            desc.set_color(col - 1, value[:3])
        elif role == Qt.EditRole:
            desc.set_value(col - 1, value)
        else:
            return False
        self.dataChanged.emit(index, index)
        return True


class ContColorTableModel(ColorTableModel):
    """A model that stores the colors corresponding to values of discrete
    variables. Colors are shown as decorations.

    Attributes:
        mouse_row (int): the row over which the mouse is hovering
    """
    def __init__(self):
        super().__init__()
        self.mouse_row = None

    def set_mouse_row(self, row):
        self.mouse_row = row

    @staticmethod
    def columnCount(parent=QModelIndex()):
        return 0 if parent.isValid() else 3

    def data(self, index, role=Qt.DisplayRole):
        def _column0():
            return ColorTableModel.data(self, index, role)

        def _column1():
            palette = colorpalettes.ContinuousPalettes[desc.palette_name]
            if role == Qt.ToolTipRole:
                return palette.friendly_name
            if role == ColorRole:
                return palette
            if role == StripRole:
                return palette.color_strip(128, 16)
            if role == Qt.SizeHintRole:
                return QSize(150, 16)
            return None

        def _column2():
            if role == Qt.SizeHintRole:
                return QSize(100, 1)
            if role == Qt.ForegroundRole:
                return QBrush(Qt.blue)
            if row == self.mouse_row and role == Qt.DisplayRole:
                return "Copy to all"
            return None

        row, col = index.row(), index.column()
        desc = self.attrdescs[row]
        if 0 <= col <= 2:
            return [_column0, _column1, _column2][col]()

    # noinspection PyMethodOverriding
    def setData(self, index, value, role):
        row, col = index.row(), index.column()
        if col == 0:
            return super().setData(index, value, role)
        if role == ColorRole:
            self.attrdescs[row].palette_name = value.name
        else:
            return False
        self.dataChanged.emit(index, index)
        return True

    def copy_to_all(self, index):
        palette_name = self.attrdescs[index.row()].palette_name
        for desc in self.attrdescs:
            desc.palette_name = palette_name
        self.dataChanged.emit(self.index(0, 1), self.index(self.rowCount(), 1))


class ColorStripDelegate(HorizontalGridDelegate):
    def __init__(self, view):
        super().__init__()
        self.view = view

    def createEditor(self, parent, _, index):
        class Combo(QComboBox):
            def __init__(self, parent, initial_data, view):
                super().__init__(parent)
                model = itemmodels.ContinuousPalettesModel(icon_width=128)
                self.setModel(model)
                self.setCurrentIndex(model.indexOf(initial_data))
                self.setIconSize(QSize(128, 16))
                QTimer.singleShot(0, self.showPopup)
                self.view = view

            def hidePopup(self):
                super().hidePopup()
                self.view.closeEditor(self, ColorStripDelegate.NoHint)

        def select(i):
            self.view.model().setData(
                index,
                combo.model().index(i, 0).data(Qt.UserRole),
                ColorRole)

        combo = Combo(parent, index.data(ColorRole), self.view)
        combo.currentIndexChanged[int].connect(select)
        return combo

    def paint(self, painter, option, index):
        strip = index.data(StripRole)
        rect = option.rect
        painter.drawPixmap(
            rect.x() + 13, rect.y() + (rect.height() - strip.height()) / 2,
            strip)
        super().paint(painter, option, index)


class ColorTable(QTableView):
    """
    The base table view for discrete and continuous attributes.

    Sets the basic properties of the table and implementes mouseRelease that
    calls handle_click with appropriate index. It also prepares a grid_deleagte
    that is used in derived classes.
    """
    def __init__(self, model):
        QTableView.__init__(self)
        self.horizontalHeader().hide()
        self.verticalHeader().hide()
        self.setShowGrid(False)
        self.setSelectionMode(QTableView.NoSelection)
        self.setModel(model)
        # View doesn't take ownership of delegates, so we store it here
        self.grid_delegate = HorizontalGridDelegate()

    def mouseReleaseEvent(self, event):
        index = self.indexAt(event.pos())
        if not index.isValid():
            return
        rect = self.visualRect(index)
        self.handle_click(index, event.pos().x() - rect.x())


class DiscreteTable(ColorTable):
    """Table view for discrete variables"""
    def __init__(self, model):
        super().__init__(model)
        self.horizontalHeader().setSectionResizeMode(
            QHeaderView.ResizeToContents)
        self.setItemDelegate(self.grid_delegate)
        self.setEditTriggers(QTableView.NoEditTriggers)

    def handle_click(self, index, x_offset):
        """
        Handle click events for the first column (call the edit method)
        and the second (call method for changing the palette)
        """
        if self.model().data(index, Qt.EditRole) is None:
            return
        if index.column() == 0 or x_offset > 24:
            self.edit(index)
        else:
            self.change_color(index)

    def change_color(self, index):
        """Invoke palette editor and set the color"""
        color = self.model().data(index, ColorRole)
        if color is None:
            return
        dlg = QColorDialog(QColor(*color))
        if dlg.exec():
            color = dlg.selectedColor()
            self.model().setData(index, color.getRgb(), ColorRole)


class ContinuousTable(ColorTable):
    """Table view for continuous variables"""

    def __init__(self, model):
        super().__init__(model)
        self.viewport().setMouseTracking(True)
        # View doesn't take ownership of delegates, so we must store it
        self.color_delegate = ColorStripDelegate(self)
        self.setItemDelegateForColumn(0, self.grid_delegate)
        self.setItemDelegateForColumn(1, self.color_delegate)
        self.setColumnWidth(1, 256)
        self.setEditTriggers(
            QTableView.SelectedClicked | QTableView.DoubleClicked)

    def mouseMoveEvent(self, event):
        """Store the hovered row index in the model, trigger viewport update"""
        pos = event.pos()
        ind = self.indexAt(pos)
        self.model().set_mouse_row(ind.row())
        super().mouseMoveEvent(event)
        self.viewport().update()

    def leaveEvent(self, _):
        """Remove the stored the hovered row index, trigger viewport update"""
        self.model().set_mouse_row(None)
        self.viewport().update()

    def handle_click(self, index, _):
        """Call the specific methods for handling clicks for each column"""
        if index.column() < 2:
            self.edit(index)
        elif index.column() == 2:
            self.model().copy_to_all(index)


class OWColor(widget.OWWidget):
    name = "Color"
    description = "Set color legend for variables."
    icon = "icons/Colors.svg"

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

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

    settingsHandler = settings.PerfectDomainContextHandler(
        match_values=settings.PerfectDomainContextHandler.MATCH_VALUES_ALL)
    disc_descs = settings.ContextSetting([])
    cont_descs = settings.ContextSetting([])
    selected_schema_index = settings.Setting(0)
    auto_apply = settings.Setting(True)

    settings_version = 2

    want_main_area = False

    def __init__(self):
        super().__init__()
        self.data = None
        self.orig_domain = self.domain = None

        box = gui.hBox(self.controlArea, "Discrete Variables")
        self.disc_model = DiscColorTableModel()
        self.disc_view = DiscreteTable(self.disc_model)
        self.disc_model.dataChanged.connect(self._on_data_changed)
        box.layout().addWidget(self.disc_view)

        box = gui.hBox(self.controlArea, "Numeric Variables")
        self.cont_model = ContColorTableModel()
        self.cont_view = ContinuousTable(self.cont_model)
        self.cont_model.dataChanged.connect(self._on_data_changed)
        box.layout().addWidget(self.cont_view)

        box = gui.hBox(self.buttonsArea)
        gui.button(box, self, "Save", callback=self.save)
        gui.button(box, self, "Load", callback=self.load)
        gui.button(box, self, "Reset", callback=self.reset)
        gui.rubber(self.buttonsArea)
        gui.auto_apply(self.buttonsArea, self, "auto_apply")

    @staticmethod
    def sizeHint():  # pragma: no cover
        return QSize(500, 570)

    @Inputs.data
    def set_data(self, data):
        self.closeContext()
        self.disc_descs = []
        self.cont_descs = []
        if data is None:
            self.data = self.domain = None
        else:
            self.data = data
            for var in chain(data.domain.variables, data.domain.metas):
                if var.is_discrete:
                    self.disc_descs.append(DiscAttrDesc(var))
                elif var.is_continuous:
                    self.cont_descs.append(ContAttrDesc(var))

        self.disc_model.set_data(self.disc_descs)
        self.cont_model.set_data(self.cont_descs)
        self.openContext(data)
        self.disc_view.resizeColumnsToContents()
        self.cont_view.resizeColumnsToContents()
        self.commit.now()

    def _on_data_changed(self):
        self.commit.deferred()

    def reset(self):
        self.disc_model.reset()
        self.cont_model.reset()
        # Reset button is in the same box as Load, which has commit.now,
        # and Apply, hence let Reset commit now, too.
        self.commit.now()

    def save(self):
        fname, _ = QFileDialog.getSaveFileName(
            self, "File name", self._start_dir(),
            "Variable definitions (*.colors)")
        if not fname:
            return
        QSettings().setValue("colorwidget/last-location",
                             os.path.split(fname)[0])
        self._save_var_defs(fname)

    def _save_var_defs(self, fname):
        with open(fname, "w") as f:
            json.dump(
                {vartype: {
                    var.name: var_data
                    for var, var_data in (
                        (desc.var, desc.to_dict()) for desc in repo)
                    if var_data}
                 for vartype, repo in (("categorical", self.disc_descs),
                                       ("numeric", self.cont_descs))
                },
                f,
                indent=4)

    def load(self):
        fname, _ = QFileDialog.getOpenFileName(
            self, "File name", self._start_dir(),
            "Variable definitions (*.colors)")
        if not fname:
            return

        try:
            f = open(fname)
        except IOError:
            QMessageBox.critical(self, "File error", "File cannot be opened.")
            return

        try:
            js = json.load(f)  #: dict
            self._parse_var_defs(js)
        except (json.JSONDecodeError, InvalidFileFormat):
            QMessageBox.critical(self, "File error", "Invalid file format.")

    def _parse_var_defs(self, js):
        if not isinstance(js, dict) or set(js) != {"categorical", "numeric"}:
            raise InvalidFileFormat
        try:
            renames = {
                var_name: desc["rename"]
                for repo in js.values() for var_name, desc in repo.items()
                if "rename" in desc
            }
        # js is an object coming from json file that can be manipulated by
        # the user, so there are too many things that can go wrong.
        # Catch all exceptions, therefore.
        except Exception as exc:
            raise InvalidFileFormat from exc
        if not all(isinstance(val, str)
                   for val in chain(renames, renames.values())):
            raise InvalidFileFormat
        renamed_vars = {
            renames.get(desc.var.name, desc.var.name)
            for desc in chain(self.disc_descs, self.cont_descs)
        }
        if len(renamed_vars) != len(self.disc_descs) + len(self.cont_descs):
            QMessageBox.warning(
                self,
                "Duplicated variable names",
                "Variables will not be renamed due to duplicated names.")
            for repo in js.values():
                for desc in repo.values():
                    desc.pop("rename", None)

        # First, construct all descriptions; assign later, after we know
        # there won't be exceptions due to invalid file format
        both_descs = []
        warnings = []
        for old_desc, repo, desc_type in (
                (self.disc_descs, "categorical", DiscAttrDesc),
                (self.cont_descs, "numeric", ContAttrDesc)):
            var_by_name = {desc.var.name: desc.var for desc in old_desc}
            new_descs = {}
            for var_name, var_data in js[repo].items():
                var = var_by_name.get(var_name)
                if var is None:
                    continue
                # This can throw InvalidFileFormat
                new_descs[var_name], warn = desc_type.from_dict(var, var_data)
                warnings += warn
            both_descs.append(new_descs)

        self.disc_descs = [both_descs[0].get(desc.var.name, desc)
                           for desc in self.disc_descs]
        self.cont_descs = [both_descs[1].get(desc.var.name, desc)
                           for desc in self.cont_descs]
        if warnings:
            QMessageBox.warning(
                self, "Invalid definitions", "\n".join(warnings))

        self.disc_model.set_data(self.disc_descs)
        self.cont_model.set_data(self.cont_descs)
        self.commit.now()

    def _start_dir(self):
        return self.workflowEnv().get("basedir") \
               or QSettings().value("colorwidget/last-location") \
               or os.path.expanduser(f"~{os.sep}")

    @gui.deferred
    def commit(self):
        def make(variables):
            new_vars = []
            for var in variables:
                source = disc_dict if var.is_discrete else cont_dict
                desc = source.get(var.name)
                new_vars.append(desc.create_variable() if desc else var)
            return new_vars

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

        disc_dict = {desc.var.name: desc for desc in self.disc_descs}
        cont_dict = {desc.var.name: desc for desc in self.cont_descs}

        dom = self.data.domain
        new_domain = Orange.data.Domain(
            make(dom.attributes), make(dom.class_vars), make(dom.metas))
        new_data = self.data.transform(new_domain)
        self.Outputs.data.send(new_data)

    def send_report(self):
        """Send report"""
        def _report_variables(variables):
            def was(n, o):
                return n if n == o else f"{n} (was: {o})"

            max_values = max(
                (len(var.values) for var in variables if var.is_discrete),
                default=1)

            rows = ""
            disc_dict = {k.var.name: k for k in self.disc_descs}
            cont_dict = {k.var.name: k for k in self.cont_descs}
            for var in variables:
                if var.is_discrete:
                    desc = disc_dict[var.name]
                    value_cols = "    \n".join(
                        f"<td>{square(*color)} {was(value, old_value)}</td>"
                        for color, value, old_value in
                        zip(desc.colors, desc.values, var.values))
                elif var.is_continuous:
                    desc = cont_dict[var.name]
                    pal = colorpalettes.ContinuousPalettes[desc.palette_name]
                    value_cols = f'<td colspan="{max_values}">' \
                                 f'{pal.friendly_name}</td>'
                else:
                    continue
                names = was(desc.name, desc.var.name)
                rows += '<tr style="height: 2em">\n' \
                        f'  <th style="text-align: right">{names}</th>' \
                        f'  {value_cols}\n' \
                        '</tr>\n'
            return rows

        if not self.data:
            return
        dom = self.data.domain
        sections = (
            (name, _report_variables(variables))
            for name, variables in (
                ("Features", dom.attributes),
                ("Outcome" + "s" * (len(dom.class_vars) > 1), dom.class_vars),
                ("Meta attributes", dom.metas)))
        table = "".join(f"<tr><th>{name}</th></tr>{rows}"
                        for name, rows in sections if rows)
        if table:
            self.report_raw(f"<table>{table}</table>")

    @classmethod
    def migrate_context(cls, _, version):
        if not version or version < 2:
            raise IncompatibleContext


if __name__ == "__main__":  # pragma: no cover
    WidgetPreview(OWColor).run(Orange.data.Table("heart_disease.tab"))

```
--- 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 Occurs When Loading Variable Definitions with Unused Variables

**Description:**
When loading variable definitions that include unused variables, the application fails to handle the warnings properly. This results in a `TypeError` because the warning message is not correctly generated or handled.

**Example:**
```python
# Attempt to parse variable definitions with unused variables
self.widget._parse_var_defs({
    "categorical": {
        "foo": {"renamed_values": {}},
        "bar": {"renamed_values": {}}
    },
    "numeric": {}
})
```

**Expected Behavior:**
The application should display a warning message indicating that the variables `'foo'` and `'bar'` are defined but not used in the data. No errors should occur during this process.

**Actual Behavior:**
A `TypeError` is raised with the message: `'NoneType' object is not subscriptable`, preventing the warning from being displayed and disrupting the loading process.

[/ISSUE]
{
    "AttrDescTest.test_name": "PASSED",
    "AttrDescTest.test_reset": "PASSED",
    "AttrDescTest.test_to_dict": "PASSED",
    "DiscAttrTest.test_colors": "PASSED",
    "DiscAttrTest.test_create_variable": "PASSED",
    "DiscAttrTest.test_from_dict_coliding_values": "PASSED",
    "DiscAttrTest.test_from_dict_exceptions": "PASSED",
    "DiscAttrTest.test_reset": "PASSED",
    "DiscAttrTest.test_to_dict": "PASSED",
    "DiscAttrTest.test_values": "PASSED",
    "ContAttrDescTest.test_create_variable": "PASSED",
    "ContAttrDescTest.test_from_dict_exceptions": "PASSED",
    "ContAttrDescTest.test_palette": "PASSED",
    "ContAttrDescTest.test_reset": "PASSED",
    "ContAttrDescTest.test_to_dict": "PASSED",
    "TestDiscColorTableModel.test_column_count": "PASSED",
    "TestDiscColorTableModel.test_data": "PASSED",
    "TestDiscColorTableModel.test_reset": "PASSED",
    "TestDiscColorTableModel.test_row_count": "PASSED",
    "TestDiscColorTableModel.test_set_data": "PASSED",
    "TestContColorTableModel.test_column_count": "PASSED",
    "TestContColorTableModel.test_copy_to_all": "PASSED",
    "TestContColorTableModel.test_data": "PASSED",
    "TestContColorTableModel.test_reset": "PASSED",
    "TestContColorTableModel.test_row_count": "PASSED",
    "TestContColorTableModel.test_set_data": "PASSED",
    "TestColorStripDelegate.test_color_combo": "PASSED",
    "TestColorStripDelegate.test_paint": "PASSED",
    "TestOWColor.test_commit_on_data_changed": "PASSED",
    "TestOWColor.test_image_export": "PASSED",
    "TestOWColor.test_invalid_input_colors": "PASSED",
    "TestOWColor.test_load": "PASSED",
    "TestOWColor.test_load_ignore_warning": "PASSED",
    "TestOWColor.test_lose_data": "PASSED",
    "TestOWColor.test_minimum_size": "PASSED",
    "TestOWColor.test_model_content": "PASSED",
    "TestOWColor.test_msg_base_class": "PASSED",
    "TestOWColor.test_parse_var_defs": "PASSED",
    "TestOWColor.test_parse_var_defs_invalid": "PASSED",
    "TestOWColor.test_parse_var_defs_no_rename": "PASSED",
    "TestOWColor.test_parse_var_defs_shows_warnings": "PASSED",
    "TestOWColor.test_report": "PASSED",
    "TestOWColor.test_reset": "PASSED",
    "TestOWColor.test_reuse_old_settings": "PASSED",
    "TestOWColor.test_save": "PASSED",
    "TestOWColor.test_save_low": "PASSED",
    "TestOWColor.test_string_variables": "PASSED",
    "TestOWColor.test_unconditional_commit_on_new_signal": "PASSED"
}
diff --git a/Orange/widgets/data/owcolor.py b/Orange/widgets/data/owcolor.py
index cfd6d5e1b..42fa9480b 100644
--- a/Orange/widgets/data/owcolor.py
+++ b/Orange/widgets/data/owcolor.py
@@ -690,6 +690,7 @@ class OWColor(widget.OWWidget):
 
         # First, construct all descriptions; assign later, after we know
         # there won't be exceptions due to invalid file format
+        unused_vars = []
         both_descs = []
         warnings = []
         for old_desc, repo, desc_type in (
@@ -700,11 +701,26 @@ class OWColor(widget.OWWidget):
             for var_name, var_data in js[repo].items():
                 var = var_by_name.get(var_name)
                 if var is None:
+                    unused_vars.append(var_name)
                     continue
                 # This can throw InvalidFileFormat
                 new_descs[var_name], warn = desc_type.from_dict(var, var_data)
                 warnings += warn
             both_descs.append(new_descs)
+        if unused_vars:
+            names = [f"'{name}'" for name in unused_vars]
+            if len(unused_vars) == 1:
+                warn = f'Definition for variable {names[0]}, which does not ' \
+                       f'appear in the data, was ignored.\n'
+            else:
+                if len(unused_vars) <= 5:
+                    warn = 'Definitions for variables ' \
+                           f'{", ".join(names[:-1])} and {names[-1]}'
+                else:
+                    warn = f'Definitions for {", ".join(names[:4])} ' \
+                           f'and {len(names) - 4} other variables'
+                warn += ", which do not appear in the data, were ignored.\n"
+            warnings.insert(0, warn)
 
         self.disc_descs = [both_descs[0].get(desc.var.name, desc)
                            for desc in self.disc_descs]

{"Orange/widgets/data/owcolor.py": "import os\nfrom itertools import chain\nimport json\n\nimport numpy as np\n\nfrom AnyQt.QtCore import Qt, QSize, QAbstractTableModel, QModelIndex, QTimer, \\\n    QSettings\nfrom AnyQt.QtGui import QColor, QFont, QBrush\nfrom AnyQt.QtWidgets import QHeaderView, QColorDialog, QTableView, QComboBox, \\\n    QFileDialog, QMessageBox\n\nfrom orangewidget.settings import IncompatibleContext\n\nimport Orange\nfrom Orange.preprocess.transformation import Identity\nfrom Orange.util import color_to_hex, hex_to_color\nfrom Orange.widgets import widget, settings, gui\nfrom Orange.widgets.gui import HorizontalGridDelegate\nfrom Orange.widgets.utils import itemmodels, colorpalettes\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.report import colored_square as square\nfrom Orange.widgets.widget import Input, Output\n\nColorRole = next(gui.OrangeUserRole)\nStripRole = next(gui.OrangeUserRole)\n\n\nclass InvalidFileFormat(Exception):\n    pass\n\n\nclass AttrDesc:\n    \"\"\"\n    Describes modifications that will be applied to variable.\n\n    Provides methods that return either the modified value or the original\n\n    Attributes:\n        var (Variable): an instance of variable\n        new_name (str or `None`): a changed name or `None`\n    \"\"\"\n    def __init__(self, var):\n        self.var = var\n        self.new_name = None\n\n    def reset(self):\n        self.new_name = None\n\n    @property\n    def name(self):\n        return self.new_name or self.var.name\n\n    @name.setter\n    def name(self, name):\n        self.new_name = name\n\n    def to_dict(self):\n        d = {}\n        if self.new_name is not None:\n            d[\"rename\"] = self.new_name\n        return d\n\n    @classmethod\n    def from_dict(cls, var, data):\n        desc = cls(var)\n        if not isinstance(data, dict):\n            raise InvalidFileFormat\n        new_name = data.get(\"rename\")\n        if new_name is not None:\n            if not isinstance(new_name, str):\n                raise InvalidFileFormat\n            desc.name = new_name\n        return desc, []\n\n\nclass DiscAttrDesc(AttrDesc):\n    \"\"\"\n    Describes modifications that will be applied to variable.\n\n    Provides methods that return either the modified value or the original\n\n    Attributes:\n        var (DiscreteVariable): an instance of variable\n        name (str or `None`): a changed name or `None`\n        new_colors (list of tuple or None): new colors as tuples (R, G, B)\n        new_values (list of str or None): new names for values, if changed\n    \"\"\"\n    def __init__(self, var):\n        super().__init__(var)\n        self.new_colors = None\n        self.new_values = None\n\n    def reset(self):\n        super().reset()\n        self.new_colors = None\n        self.new_values = None\n\n    @property\n    def colors(self):\n        if self.new_colors is None:\n            return self.var.colors\n        else:\n            return self.new_colors\n\n    def set_color(self, i, color):\n        if self.new_colors is None:\n            self.new_colors = list(self.var.colors)\n        self.new_colors[i] = color\n\n    @property\n    def values(self):\n        return tuple(self.new_values or self.var.values)\n\n    def set_value(self, i, value):\n        if not self.new_values:\n            self.new_values = list(self.var.values)\n        self.new_values[i] = value\n\n    def create_variable(self):\n        new_var = self.var.copy(name=self.name, values=self.values,\n                                compute_value=Identity(self.var))\n        new_var.colors = np.asarray(self.colors)\n        return new_var\n\n    def to_dict(self):\n        d = super().to_dict()\n        if self.new_values is not None:\n            d[\"renamed_values\"] = \\\n                {k: v\n                 for k, v in zip(self.var.values, self.new_values)\n                 if k != v}\n        if self.new_colors is not None:\n            d[\"colors\"] = {\n                value: color_to_hex(color)\n                for value, color in zip(self.var.values, self.colors)}\n        return d\n\n    @classmethod\n    def from_dict(cls, var, data):\n\n        def _check_dict_str_str(d):\n            if not isinstance(d, dict) or \\\n                    not all(isinstance(val, str)\n                            for val in chain(d, d.values())):\n                raise InvalidFileFormat\n\n        obj, warnings = super().from_dict(var, data)\n\n        val_map = data.get(\"renamed_values\")\n        if val_map is not None:\n            _check_dict_str_str(val_map)\n            mapped_values = [val_map.get(value, value) for value in var.values]\n            if len(set(mapped_values)) != len(mapped_values):\n                warnings.append(\n                    f\"{var.name}: \"\n                    \"renaming of values ignored due to duplicate names\")\n            else:\n                obj.new_values = mapped_values\n\n        new_colors = data.get(\"colors\")\n        if new_colors is not None:\n            _check_dict_str_str(new_colors)\n            colors = []\n            for value, def_color in zip(var.values, var.palette.palette):\n                if value in new_colors:\n                    try:\n                        color = hex_to_color(new_colors[value])\n                    except ValueError as exc:\n                        raise InvalidFileFormat from exc\n                    colors.append(color)\n                else:\n                    colors.append(def_color)\n                obj.new_colors = colors\n        return obj, warnings\n\n\nclass ContAttrDesc(AttrDesc):\n    \"\"\"\n    Describes modifications that will be applied to variable.\n\n    Provides methods that return either the modified value or the original\n\n    Attributes:\n        var (ContinuousVariable): an instance of variable\n        name (str or `None`): a changed name or `None`\n        palette_name (str or None): name of palette or None if unmodified\n    \"\"\"\n    def __init__(self, var):\n        super().__init__(var)\n        self.new_palette_name = self._default_palette_name()\n\n    def reset(self):\n        super().reset()\n        self.new_palette_name = self._default_palette_name()\n\n    def _default_palette_name(self):\n        if self.var.palette.name not in colorpalettes.ContinuousPalettes:\n            return colorpalettes.DefaultContinuousPaletteName\n        else:\n            return None\n\n    @property\n    def palette_name(self):\n        return self.new_palette_name or self.var.palette.name\n\n    @palette_name.setter\n    def palette_name(self, palette_name):\n        self.new_palette_name = palette_name\n\n    def create_variable(self):\n        new_var = self.var.copy(name=self.name,\n                                compute_value=Identity(self.var))\n        new_var.attributes[\"palette\"] = self.palette_name\n        return new_var\n\n    def to_dict(self):\n        d = super().to_dict()\n        if self.new_palette_name is not None:\n            d[\"colors\"] = self.palette_name\n        return d\n\n    @classmethod\n    def from_dict(cls, var, data):\n        obj, warnings = super().from_dict(var, data)\n        colors = data.get(\"colors\")\n        if colors is not None:\n            if colors not in colorpalettes.ContinuousPalettes:\n                raise InvalidFileFormat\n            obj.palette_name = colors\n        return obj, warnings\n\n\nclass ColorTableModel(QAbstractTableModel):\n    \"\"\"\n    Base color model for discrete and continuous variables. The model handles:\n    - the first column - variable name (including setData)\n    - flags\n    - row count, computed as len(attrdescs)\n\n    Attribute:\n        attrdescs (list of AttrDesc): attrdescs with user-defined changes\n    \"\"\"\n    def __init__(self):\n        QAbstractTableModel.__init__(self)\n        self.attrdescs = []\n\n    @staticmethod\n    def flags(_):  # pragma: no cover\n        return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable\n\n    def set_data(self, attrdescs):\n        self.modelAboutToBeReset.emit()\n        self.attrdescs = attrdescs\n        self.modelReset.emit()\n\n    def rowCount(self, parent=QModelIndex()):\n        return 0 if parent.isValid() else len(self.attrdescs)\n\n    def data(self, index, role=Qt.DisplayRole):\n        # Only valid for the first column; derived classes implement the rest\n        row = index.row()\n        if role in (Qt.DisplayRole, Qt.EditRole):\n            return self.attrdescs[row].name\n        if role == Qt.FontRole:\n            font = QFont()\n            font.setBold(True)\n            return font\n        if role == Qt.TextAlignmentRole:\n            return Qt.AlignRight | Qt.AlignVCenter\n        return None\n\n    def setData(self, index, value, role):\n        # Only valid for the first column; derived classes implement the rest\n        if role == Qt.EditRole:\n            self.attrdescs[index.row()].name = value\n        else:\n            return False\n        self.dataChanged.emit(index, index)\n        return True\n\n    def reset(self):\n        self.beginResetModel()\n        for desc in self.attrdescs:\n            desc.reset()\n        self.endResetModel()\n\n\nclass DiscColorTableModel(ColorTableModel):\n    \"\"\"\n    A model that stores the colors corresponding to values of discrete\n    variables. Colors are shown as decorations.\n    \"\"\"\n    def columnCount(self, parent=QModelIndex()):\n        if parent.isValid():\n            return 0\n        return 1 + max((len(row.var.values) for row in self.attrdescs),\n                       default=0)\n\n    def data(self, index, role=Qt.DisplayRole):\n        # pylint: disable=too-many-return-statements\n        row, col = index.row(), index.column()\n        if col == 0:\n            return super().data(index, role)\n\n        desc = self.attrdescs[row]\n        if col > len(desc.var.values):\n            return None\n        if role in (Qt.DisplayRole, Qt.EditRole):\n            return desc.values[col - 1]\n\n        color = desc.colors[col - 1]\n        if role == Qt.DecorationRole:\n            return QColor(*color)\n        if role == Qt.ToolTipRole:\n            return color_to_hex(color)\n        if role == ColorRole:\n            return color\n        return None\n\n    def setData(self, index, value, role):\n        row, col = index.row(), index.column()\n        if col == 0:\n            return super().setData(index, value, role)\n\n        desc = self.attrdescs[row]\n        if role == ColorRole:\n            desc.set_color(col - 1, value[:3])\n        elif role == Qt.EditRole:\n            desc.set_value(col - 1, value)\n        else:\n            return False\n        self.dataChanged.emit(index, index)\n        return True\n\n\nclass ContColorTableModel(ColorTableModel):\n    \"\"\"A model that stores the colors corresponding to values of discrete\n    variables. Colors are shown as decorations.\n\n    Attributes:\n        mouse_row (int): the row over which the mouse is hovering\n    \"\"\"\n    def __init__(self):\n        super().__init__()\n        self.mouse_row = None\n\n    def set_mouse_row(self, row):\n        self.mouse_row = row\n\n    @staticmethod\n    def columnCount(parent=QModelIndex()):\n        return 0 if parent.isValid() else 3\n\n    def data(self, index, role=Qt.DisplayRole):\n        def _column0():\n            return ColorTableModel.data(self, index, role)\n\n        def _column1():\n            palette = colorpalettes.ContinuousPalettes[desc.palette_name]\n            if role == Qt.ToolTipRole:\n                return palette.friendly_name\n            if role == ColorRole:\n                return palette\n            if role == StripRole:\n                return palette.color_strip(128, 16)\n            if role == Qt.SizeHintRole:\n                return QSize(150, 16)\n            return None\n\n        def _column2():\n            if role == Qt.SizeHintRole:\n                return QSize(100, 1)\n            if role == Qt.ForegroundRole:\n                return QBrush(Qt.blue)\n            if row == self.mouse_row and role == Qt.DisplayRole:\n                return \"Copy to all\"\n            return None\n\n        row, col = index.row(), index.column()\n        desc = self.attrdescs[row]\n        if 0 <= col <= 2:\n            return [_column0, _column1, _column2][col]()\n\n    # noinspection PyMethodOverriding\n    def setData(self, index, value, role):\n        row, col = index.row(), index.column()\n        if col == 0:\n            return super().setData(index, value, role)\n        if role == ColorRole:\n            self.attrdescs[row].palette_name = value.name\n        else:\n            return False\n        self.dataChanged.emit(index, index)\n        return True\n\n    def copy_to_all(self, index):\n        palette_name = self.attrdescs[index.row()].palette_name\n        for desc in self.attrdescs:\n            desc.palette_name = palette_name\n        self.dataChanged.emit(self.index(0, 1), self.index(self.rowCount(), 1))\n\n\nclass ColorStripDelegate(HorizontalGridDelegate):\n    def __init__(self, view):\n        super().__init__()\n        self.view = view\n\n    def createEditor(self, parent, _, index):\n        class Combo(QComboBox):\n            def __init__(self, parent, initial_data, view):\n                super().__init__(parent)\n                model = itemmodels.ContinuousPalettesModel(icon_width=128)\n                self.setModel(model)\n                self.setCurrentIndex(model.indexOf(initial_data))\n                self.setIconSize(QSize(128, 16))\n                QTimer.singleShot(0, self.showPopup)\n                self.view = view\n\n            def hidePopup(self):\n                super().hidePopup()\n                self.view.closeEditor(self, ColorStripDelegate.NoHint)\n\n        def select(i):\n            self.view.model().setData(\n                index,\n                combo.model().index(i, 0).data(Qt.UserRole),\n                ColorRole)\n\n        combo = Combo(parent, index.data(ColorRole), self.view)\n        combo.currentIndexChanged[int].connect(select)\n        return combo\n\n    def paint(self, painter, option, index):\n        strip = index.data(StripRole)\n        rect = option.rect\n        painter.drawPixmap(\n            rect.x() + 13, rect.y() + (rect.height() - strip.height()) / 2,\n            strip)\n        super().paint(painter, option, index)\n\n\nclass ColorTable(QTableView):\n    \"\"\"\n    The base table view for discrete and continuous attributes.\n\n    Sets the basic properties of the table and implementes mouseRelease that\n    calls handle_click with appropriate index. It also prepares a grid_deleagte\n    that is used in derived classes.\n    \"\"\"\n    def __init__(self, model):\n        QTableView.__init__(self)\n        self.horizontalHeader().hide()\n        self.verticalHeader().hide()\n        self.setShowGrid(False)\n        self.setSelectionMode(QTableView.NoSelection)\n        self.setModel(model)\n        # View doesn't take ownership of delegates, so we store it here\n        self.grid_delegate = HorizontalGridDelegate()\n\n    def mouseReleaseEvent(self, event):\n        index = self.indexAt(event.pos())\n        if not index.isValid():\n            return\n        rect = self.visualRect(index)\n        self.handle_click(index, event.pos().x() - rect.x())\n\n\nclass DiscreteTable(ColorTable):\n    \"\"\"Table view for discrete variables\"\"\"\n    def __init__(self, model):\n        super().__init__(model)\n        self.horizontalHeader().setSectionResizeMode(\n            QHeaderView.ResizeToContents)\n        self.setItemDelegate(self.grid_delegate)\n        self.setEditTriggers(QTableView.NoEditTriggers)\n\n    def handle_click(self, index, x_offset):\n        \"\"\"\n        Handle click events for the first column (call the edit method)\n        and the second (call method for changing the palette)\n        \"\"\"\n        if self.model().data(index, Qt.EditRole) is None:\n            return\n        if index.column() == 0 or x_offset > 24:\n            self.edit(index)\n        else:\n            self.change_color(index)\n\n    def change_color(self, index):\n        \"\"\"Invoke palette editor and set the color\"\"\"\n        color = self.model().data(index, ColorRole)\n        if color is None:\n            return\n        dlg = QColorDialog(QColor(*color))\n        if dlg.exec():\n            color = dlg.selectedColor()\n            self.model().setData(index, color.getRgb(), ColorRole)\n\n\nclass ContinuousTable(ColorTable):\n    \"\"\"Table view for continuous variables\"\"\"\n\n    def __init__(self, model):\n        super().__init__(model)\n        self.viewport().setMouseTracking(True)\n        # View doesn't take ownership of delegates, so we must store it\n        self.color_delegate = ColorStripDelegate(self)\n        self.setItemDelegateForColumn(0, self.grid_delegate)\n        self.setItemDelegateForColumn(1, self.color_delegate)\n        self.setColumnWidth(1, 256)\n        self.setEditTriggers(\n            QTableView.SelectedClicked | QTableView.DoubleClicked)\n\n    def mouseMoveEvent(self, event):\n        \"\"\"Store the hovered row index in the model, trigger viewport update\"\"\"\n        pos = event.pos()\n        ind = self.indexAt(pos)\n        self.model().set_mouse_row(ind.row())\n        super().mouseMoveEvent(event)\n        self.viewport().update()\n\n    def leaveEvent(self, _):\n        \"\"\"Remove the stored the hovered row index, trigger viewport update\"\"\"\n        self.model().set_mouse_row(None)\n        self.viewport().update()\n\n    def handle_click(self, index, _):\n        \"\"\"Call the specific methods for handling clicks for each column\"\"\"\n        if index.column() < 2:\n            self.edit(index)\n        elif index.column() == 2:\n            self.model().copy_to_all(index)\n\n\nclass OWColor(widget.OWWidget):\n    name = \"Color\"\n    description = \"Set color legend for variables.\"\n    icon = \"icons/Colors.svg\"\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table)\n\n    class Outputs:\n        data = Output(\"Data\", Orange.data.Table)\n\n    settingsHandler = settings.PerfectDomainContextHandler(\n        match_values=settings.PerfectDomainContextHandler.MATCH_VALUES_ALL)\n    disc_descs = settings.ContextSetting([])\n    cont_descs = settings.ContextSetting([])\n    selected_schema_index = settings.Setting(0)\n    auto_apply = settings.Setting(True)\n\n    settings_version = 2\n\n    want_main_area = False\n\n    def __init__(self):\n        super().__init__()\n        self.data = None\n        self.orig_domain = self.domain = None\n\n        box = gui.hBox(self.controlArea, \"Discrete Variables\")\n        self.disc_model = DiscColorTableModel()\n        self.disc_view = DiscreteTable(self.disc_model)\n        self.disc_model.dataChanged.connect(self._on_data_changed)\n        box.layout().addWidget(self.disc_view)\n\n        box = gui.hBox(self.controlArea, \"Numeric Variables\")\n        self.cont_model = ContColorTableModel()\n        self.cont_view = ContinuousTable(self.cont_model)\n        self.cont_model.dataChanged.connect(self._on_data_changed)\n        box.layout().addWidget(self.cont_view)\n\n        box = gui.hBox(self.buttonsArea)\n        gui.button(box, self, \"Save\", callback=self.save)\n        gui.button(box, self, \"Load\", callback=self.load)\n        gui.button(box, self, \"Reset\", callback=self.reset)\n        gui.rubber(self.buttonsArea)\n        gui.auto_apply(self.buttonsArea, self, \"auto_apply\")\n\n    @staticmethod\n    def sizeHint():  # pragma: no cover\n        return QSize(500, 570)\n\n    @Inputs.data\n    def set_data(self, data):\n        self.closeContext()\n        self.disc_descs = []\n        self.cont_descs = []\n        if data is None:\n            self.data = self.domain = None\n        else:\n            self.data = data\n            for var in chain(data.domain.variables, data.domain.metas):\n                if var.is_discrete:\n                    self.disc_descs.append(DiscAttrDesc(var))\n                elif var.is_continuous:\n                    self.cont_descs.append(ContAttrDesc(var))\n\n        self.disc_model.set_data(self.disc_descs)\n        self.cont_model.set_data(self.cont_descs)\n        self.openContext(data)\n        self.disc_view.resizeColumnsToContents()\n        self.cont_view.resizeColumnsToContents()\n        self.commit.now()\n\n    def _on_data_changed(self):\n        self.commit.deferred()\n\n    def reset(self):\n        self.disc_model.reset()\n        self.cont_model.reset()\n        # Reset button is in the same box as Load, which has commit.now,\n        # and Apply, hence let Reset commit now, too.\n        self.commit.now()\n\n    def save(self):\n        fname, _ = QFileDialog.getSaveFileName(\n            self, \"File name\", self._start_dir(),\n            \"Variable definitions (*.colors)\")\n        if not fname:\n            return\n        QSettings().setValue(\"colorwidget/last-location\",\n                             os.path.split(fname)[0])\n        self._save_var_defs(fname)\n\n    def _save_var_defs(self, fname):\n        with open(fname, \"w\") as f:\n            json.dump(\n                {vartype: {\n                    var.name: var_data\n                    for var, var_data in (\n                        (desc.var, desc.to_dict()) for desc in repo)\n                    if var_data}\n                 for vartype, repo in ((\"categorical\", self.disc_descs),\n                                       (\"numeric\", self.cont_descs))\n                },\n                f,\n                indent=4)\n\n    def load(self):\n        fname, _ = QFileDialog.getOpenFileName(\n            self, \"File name\", self._start_dir(),\n            \"Variable definitions (*.colors)\")\n        if not fname:\n            return\n\n        try:\n            f = open(fname)\n        except IOError:\n            QMessageBox.critical(self, \"File error\", \"File cannot be opened.\")\n            return\n\n        try:\n            js = json.load(f)  #: dict\n            self._parse_var_defs(js)\n        except (json.JSONDecodeError, InvalidFileFormat):\n            QMessageBox.critical(self, \"File error\", \"Invalid file format.\")\n\n    def _parse_var_defs(self, js):\n        if not isinstance(js, dict) or set(js) != {\"categorical\", \"numeric\"}:\n            raise InvalidFileFormat\n        try:\n            renames = {\n                var_name: desc[\"rename\"]\n                for repo in js.values() for var_name, desc in repo.items()\n                if \"rename\" in desc\n            }\n        # js is an object coming from json file that can be manipulated by\n        # the user, so there are too many things that can go wrong.\n        # Catch all exceptions, therefore.\n        except Exception as exc:\n            raise InvalidFileFormat from exc\n        if not all(isinstance(val, str)\n                   for val in chain(renames, renames.values())):\n            raise InvalidFileFormat\n        renamed_vars = {\n            renames.get(desc.var.name, desc.var.name)\n            for desc in chain(self.disc_descs, self.cont_descs)\n        }\n        if len(renamed_vars) != len(self.disc_descs) + len(self.cont_descs):\n            QMessageBox.warning(\n                self,\n                \"Duplicated variable names\",\n                \"Variables will not be renamed due to duplicated names.\")\n            for repo in js.values():\n                for desc in repo.values():\n                    desc.pop(\"rename\", None)\n\n        # First, construct all descriptions; assign later, after we know\n        # there won't be exceptions due to invalid file format\n        both_descs = []\n        warnings = []\n        for old_desc, repo, desc_type in (\n                (self.disc_descs, \"categorical\", DiscAttrDesc),\n                (self.cont_descs, \"numeric\", ContAttrDesc)):\n            var_by_name = {desc.var.name: desc.var for desc in old_desc}\n            new_descs = {}\n            for var_name, var_data in js[repo].items():\n                var = var_by_name.get(var_name)\n                if var is None:\n                    continue\n                # This can throw InvalidFileFormat\n                new_descs[var_name], warn = desc_type.from_dict(var, var_data)\n                warnings += warn\n            both_descs.append(new_descs)\n\n        self.disc_descs = [both_descs[0].get(desc.var.name, desc)\n                           for desc in self.disc_descs]\n        self.cont_descs = [both_descs[1].get(desc.var.name, desc)\n                           for desc in self.cont_descs]\n        if warnings:\n            QMessageBox.warning(\n                self, \"Invalid definitions\", \"\\n\".join(warnings))\n\n        self.disc_model.set_data(self.disc_descs)\n        self.cont_model.set_data(self.cont_descs)\n        self.commit.now()\n\n    def _start_dir(self):\n        return self.workflowEnv().get(\"basedir\") \\\n               or QSettings().value(\"colorwidget/last-location\") \\\n               or os.path.expanduser(f\"~{os.sep}\")\n\n    @gui.deferred\n    def commit(self):\n        def make(variables):\n            new_vars = []\n            for var in variables:\n                source = disc_dict if var.is_discrete else cont_dict\n                desc = source.get(var.name)\n                new_vars.append(desc.create_variable() if desc else var)\n            return new_vars\n\n        if self.data is None:\n            self.Outputs.data.send(None)\n            return\n\n        disc_dict = {desc.var.name: desc for desc in self.disc_descs}\n        cont_dict = {desc.var.name: desc for desc in self.cont_descs}\n\n        dom = self.data.domain\n        new_domain = Orange.data.Domain(\n            make(dom.attributes), make(dom.class_vars), make(dom.metas))\n        new_data = self.data.transform(new_domain)\n        self.Outputs.data.send(new_data)\n\n    def send_report(self):\n        \"\"\"Send report\"\"\"\n        def _report_variables(variables):\n            def was(n, o):\n                return n if n == o else f\"{n} (was: {o})\"\n\n            max_values = max(\n                (len(var.values) for var in variables if var.is_discrete),\n                default=1)\n\n            rows = \"\"\n            disc_dict = {k.var.name: k for k in self.disc_descs}\n            cont_dict = {k.var.name: k for k in self.cont_descs}\n            for var in variables:\n                if var.is_discrete:\n                    desc = disc_dict[var.name]\n                    value_cols = \"    \\n\".join(\n                        f\"<td>{square(*color)} {was(value, old_value)}</td>\"\n                        for color, value, old_value in\n                        zip(desc.colors, desc.values, var.values))\n                elif var.is_continuous:\n                    desc = cont_dict[var.name]\n                    pal = colorpalettes.ContinuousPalettes[desc.palette_name]\n                    value_cols = f'<td colspan=\"{max_values}\">' \\\n                                 f'{pal.friendly_name}</td>'\n                else:\n                    continue\n                names = was(desc.name, desc.var.name)\n                rows += '<tr style=\"height: 2em\">\\n' \\\n                        f'  <th style=\"text-align: right\">{names}</th>' \\\n                        f'  {value_cols}\\n' \\\n                        '</tr>\\n'\n            return rows\n\n        if not self.data:\n            return\n        dom = self.data.domain\n        sections = (\n            (name, _report_variables(variables))\n            for name, variables in (\n                (\"Features\", dom.attributes),\n                (\"Outcome\" + \"s\" * (len(dom.class_vars) > 1), dom.class_vars),\n                (\"Meta attributes\", dom.metas)))\n        table = \"\".join(f\"<tr><th>{name}</th></tr>{rows}\"\n                        for name, rows in sections if rows)\n        if table:\n            self.report_raw(f\"<table>{table}</table>\")\n\n    @classmethod\n    def migrate_context(cls, _, version):\n        if not version or version < 2:\n            raise IncompatibleContext\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWColor).run(Orange.data.Table(\"heart_disease.tab\"))\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 Occurs When Loading Variable Definitions with Unused Variables

**Description:**
When loading variable definitions that include unused variables, the application fails to handle the warnings properly. This results in a `TypeError` because the warning message is not correctly generated or handled.

**Example:**
```python
# Attempt to parse variable definitions with unused variables
self.widget._parse_var_defs({
    "categorical": {
        "foo": {"renamed_values": {}},
        "bar": {"renamed_values": {}}
    },
    "numeric": {}
})
```

**Expected Behavior:**
The application should display a warning message indicating that the variables `'foo'` and `'bar'` are defined but not used in the data. No errors should occur during this process.

**Actual Behavior:**
A `TypeError` is raised with the message: `'NoneType' object is not subscriptable`, preventing the warning from being displayed and disrupting the loading process.

[/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/owcolor.py
import os
from itertools import chain
import json

import numpy as np

from AnyQt.QtCore import Qt, QSize, QAbstractTableModel, QModelIndex, QTimer, \
    QSettings
from AnyQt.QtGui import QColor, QFont, QBrush
from AnyQt.QtWidgets import QHeaderView, QColorDialog, QTableView, QComboBox, \
    QFileDialog, QMessageBox

from orangewidget.settings import IncompatibleContext

import Orange
from Orange.preprocess.transformation import Identity
from Orange.util import color_to_hex, hex_to_color
from Orange.widgets import widget, settings, gui
from Orange.widgets.gui import HorizontalGridDelegate
from Orange.widgets.utils import itemmodels, colorpalettes
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.report import colored_square as square
from Orange.widgets.widget import Input, Output

ColorRole = next(gui.OrangeUserRole)
StripRole = next(gui.OrangeUserRole)


class InvalidFileFormat(Exception):
    pass


class AttrDesc:
    """
    Describes modifications that will be applied to variable.

    Provides methods that return either the modified value or the original

    Attributes:
        var (Variable): an instance of variable
        new_name (str or `None`): a changed name or `None`
    """
    def __init__(self, var):
        self.var = var
        self.new_name = None

    def reset(self):
        self.new_name = None

    @property
    def name(self):
        return self.new_name or self.var.name

    @name.setter
    def name(self, name):
        self.new_name = name

    def to_dict(self):
        d = {}
        if self.new_name is not None:
            d["rename"] = self.new_name
        return d

    @classmethod
    def from_dict(cls, var, data):
        desc = cls(var)
        if not isinstance(data, dict):
            raise InvalidFileFormat
        new_name = data.get("rename")
        if new_name is not None:
            if not isinstance(new_name, str):
                raise InvalidFileFormat
            desc.name = new_name
        return desc, []


class DiscAttrDesc(AttrDesc):
    """
    Describes modifications that will be applied to variable.

    Provides methods that return either the modified value or the original

    Attributes:
        var (DiscreteVariable): an instance of variable
        name (str or `None`): a changed name or `None`
        new_colors (list of tuple or None): new colors as tuples (R, G, B)
        new_values (list of str or None): new names for values, if changed
    """
    def __init__(self, var):
        super().__init__(var)
        self.new_colors = None
        self.new_values = None

    def reset(self):
        super().reset()
        self.new_colors = None
        self.new_values = None

    @property
    def colors(self):
        if self.new_colors is None:
            return self.var.colors
        else:
            return self.new_colors

    def set_color(self, i, color):
        if self.new_colors is None:
            self.new_colors = list(self.var.colors)
        self.new_colors[i] = color

    @property
    def values(self):
        return tuple(self.new_values or self.var.values)

    def set_value(self, i, value):
        if not self.new_values:
            self.new_values = list(self.var.values)
        self.new_values[i] = value

    def create_variable(self):
        new_var = self.var.copy(name=self.name, values=self.values,
                                compute_value=Identity(self.var))
        new_var.colors = np.asarray(self.colors)
        return new_var

    def to_dict(self):
        d = super().to_dict()
        if self.new_values is not None:
            d["renamed_values"] = \
                {k: v
                 for k, v in zip(self.var.values, self.new_values)
                 if k != v}
        if self.new_colors is not None:
            d["colors"] = {
                value: color_to_hex(color)
                for value, color in zip(self.var.values, self.colors)}
        return d

    @classmethod
    def from_dict(cls, var, data):

        def _check_dict_str_str(d):
            if not isinstance(d, dict) or \
                    not all(isinstance(val, str)
                            for val in chain(d, d.values())):
                raise InvalidFileFormat

        obj, warnings = super().from_dict(var, data)

        val_map = data.get("renamed_values")
        if val_map is not None:
            _check_dict_str_str(val_map)
            mapped_values = [val_map.get(value, value) for value in var.values]
            if len(set(mapped_values)) != len(mapped_values):
                warnings.append(
                    f"{var.name}: "
                    "renaming of values ignored due to duplicate names")
            else:
                obj.new_values = mapped_values

        new_colors = data.get("colors")
        if new_colors is not None:
            _check_dict_str_str(new_colors)
            colors = []
            for value, def_color in zip(var.values, var.palette.palette):
                if value in new_colors:
                    try:
                        color = hex_to_color(new_colors[value])
                    except ValueError as exc:
                        raise InvalidFileFormat from exc
                    colors.append(color)
                else:
                    colors.append(def_color)
                obj.new_colors = colors
        return obj, warnings


class ContAttrDesc(AttrDesc):
    """
    Describes modifications that will be applied to variable.

    Provides methods that return either the modified value or the original

    Attributes:
        var (ContinuousVariable): an instance of variable
        name (str or `None`): a changed name or `None`
        palette_name (str or None): name of palette or None if unmodified
    """
    def __init__(self, var):
        super().__init__(var)
        self.new_palette_name = self._default_palette_name()

    def reset(self):
        super().reset()
        self.new_palette_name = self._default_palette_name()

    def _default_palette_name(self):
        if self.var.palette.name not in colorpalettes.ContinuousPalettes:
            return colorpalettes.DefaultContinuousPaletteName
        else:
            return None

    @property
    def palette_name(self):
        return self.new_palette_name or self.var.palette.name

    @palette_name.setter
    def palette_name(self, palette_name):
        self.new_palette_name = palette_name

    def create_variable(self):
        new_var = self.var.copy(name=self.name,
                                compute_value=Identity(self.var))
        new_var.attributes["palette"] = self.palette_name
        return new_var

    def to_dict(self):
        d = super().to_dict()
        if self.new_palette_name is not None:
            d["colors"] = self.palette_name
        return d

    @classmethod
    def from_dict(cls, var, data):
        obj, warnings = super().from_dict(var, data)
        colors = data.get("colors")
        if colors is not None:
            if colors not in colorpalettes.ContinuousPalettes:
                raise InvalidFileFormat
            obj.palette_name = colors
        return obj, warnings


class ColorTableModel(QAbstractTableModel):
    """
    Base color model for discrete and continuous variables. The model handles:
    - the first column - variable name (including setData)
    - flags
    - row count, computed as len(attrdescs)

    Attribute:
        attrdescs (list of AttrDesc): attrdescs with user-defined changes
    """
    def __init__(self):
        QAbstractTableModel.__init__(self)
        self.attrdescs = []

    @staticmethod
    def flags(_):  # pragma: no cover
        return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable

    def set_data(self, attrdescs):
        self.modelAboutToBeReset.emit()
        self.attrdescs = attrdescs
        self.modelReset.emit()

    def rowCount(self, parent=QModelIndex()):
        return 0 if parent.isValid() else len(self.attrdescs)

    def data(self, index, role=Qt.DisplayRole):
        # Only valid for the first column; derived classes implement the rest
        row = index.row()
        if role in (Qt.DisplayRole, Qt.EditRole):
            return self.attrdescs[row].name
        if role == Qt.FontRole:
            font = QFont()
            font.setBold(True)
            return font
        if role == Qt.TextAlignmentRole:
            return Qt.AlignRight | Qt.AlignVCenter
        return None

    def setData(self, index, value, role):
        # Only valid for the first column; derived classes implement the rest
        if role == Qt.EditRole:
            self.attrdescs[index.row()].name = value
        else:
            return False
        self.dataChanged.emit(index, index)
        return True

    def reset(self):
        self.beginResetModel()
        for desc in self.attrdescs:
            desc.reset()
        self.endResetModel()


class DiscColorTableModel(ColorTableModel):
    """
    A model that stores the colors corresponding to values of discrete
    variables. Colors are shown as decorations.
    """
    def columnCount(self, parent=QModelIndex()):
        if parent.isValid():
            return 0
        return 1 + max((len(row.var.values) for row in self.attrdescs),
                       default=0)

    def data(self, index, role=Qt.DisplayRole):
        # pylint: disable=too-many-return-statements
        row, col = index.row(), index.column()
        if col == 0:
            return super().data(index, role)

        desc = self.attrdescs[row]
        if col > len(desc.var.values):
            return None
        if role in (Qt.DisplayRole, Qt.EditRole):
            return desc.values[col - 1]

        color = desc.colors[col - 1]
        if role == Qt.DecorationRole:
            return QColor(*color)
        if role == Qt.ToolTipRole:
            return color_to_hex(color)
        if role == ColorRole:
            return color
        return None

    def setData(self, index, value, role):
        row, col = index.row(), index.column()
        if col == 0:
            return super().setData(index, value, role)

        desc = self.attrdescs[row]
        if role == ColorRole:
            desc.set_color(col - 1, value[:3])
        elif role == Qt.EditRole:
            desc.set_value(col - 1, value)
        else:
            return False
        self.dataChanged.emit(index, index)
        return True


class ContColorTableModel(ColorTableModel):
    """A model that stores the colors corresponding to values of discrete
    variables. Colors are shown as decorations.

    Attributes:
        mouse_row (int): the row over which the mouse is hovering
    """
    def __init__(self):
        super().__init__()
        self.mouse_row = None

    def set_mouse_row(self, row):
        self.mouse_row = row

    @staticmethod
    def columnCount(parent=QModelIndex()):
        return 0 if parent.isValid() else 3

    def data(self, index, role=Qt.DisplayRole):
        def _column0():
            return ColorTableModel.data(self, index, role)

        def _column1():
            palette = colorpalettes.ContinuousPalettes[desc.palette_name]
            if role == Qt.ToolTipRole:
                return palette.friendly_name
            if role == ColorRole:
                return palette
            if role == StripRole:
                return palette.color_strip(128, 16)
            if role == Qt.SizeHintRole:
                return QSize(150, 16)
            return None

        def _column2():
            if role == Qt.SizeHintRole:
                return QSize(100, 1)
            if role == Qt.ForegroundRole:
                return QBrush(Qt.blue)
            if row == self.mouse_row and role == Qt.DisplayRole:
                return "Copy to all"
            return None

        row, col = index.row(), index.column()
        desc = self.attrdescs[row]
        if 0 <= col <= 2:
            return [_column0, _column1, _column2][col]()

    # noinspection PyMethodOverriding
    def setData(self, index, value, role):
        row, col = index.row(), index.column()
        if col == 0:
            return super().setData(index, value, role)
        if role == ColorRole:
            self.attrdescs[row].palette_name = value.name
        else:
            return False
        self.dataChanged.emit(index, index)
        return True

    def copy_to_all(self, index):
        palette_name = self.attrdescs[index.row()].palette_name
        for desc in self.attrdescs:
            desc.palette_name = palette_name
        self.dataChanged.emit(self.index(0, 1), self.index(self.rowCount(), 1))


class ColorStripDelegate(HorizontalGridDelegate):
    def __init__(self, view):
        super().__init__()
        self.view = view

    def createEditor(self, parent, _, index):
        class Combo(QComboBox):
            def __init__(self, parent, initial_data, view):
                super().__init__(parent)
                model = itemmodels.ContinuousPalettesModel(icon_width=128)
                self.setModel(model)
                self.setCurrentIndex(model.indexOf(initial_data))
                self.setIconSize(QSize(128, 16))
                QTimer.singleShot(0, self.showPopup)
                self.view = view

            def hidePopup(self):
                super().hidePopup()
                self.view.closeEditor(self, ColorStripDelegate.NoHint)

        def select(i):
            self.view.model().setData(
                index,
                combo.model().index(i, 0).data(Qt.UserRole),
                ColorRole)

        combo = Combo(parent, index.data(ColorRole), self.view)
        combo.currentIndexChanged[int].connect(select)
        return combo

    def paint(self, painter, option, index):
        strip = index.data(StripRole)
        rect = option.rect
        painter.drawPixmap(
            rect.x() + 13, rect.y() + (rect.height() - strip.height()) / 2,
            strip)
        super().paint(painter, option, index)


class ColorTable(QTableView):
    """
    The base table view for discrete and continuous attributes.

    Sets the basic properties of the table and implementes mouseRelease that
    calls handle_click with appropriate index. It also prepares a grid_deleagte
    that is used in derived classes.
    """
    def __init__(self, model):
        QTableView.__init__(self)
        self.horizontalHeader().hide()
        self.verticalHeader().hide()
        self.setShowGrid(False)
        self.setSelectionMode(QTableView.NoSelection)
        self.setModel(model)
        # View doesn't take ownership of delegates, so we store it here
        self.grid_delegate = HorizontalGridDelegate()

    def mouseReleaseEvent(self, event):
        index = self.indexAt(event.pos())
        if not index.isValid():
            return
        rect = self.visualRect(index)
        self.handle_click(index, event.pos().x() - rect.x())


class DiscreteTable(ColorTable):
    """Table view for discrete variables"""
    def __init__(self, model):
        super().__init__(model)
        self.horizontalHeader().setSectionResizeMode(
            QHeaderView.ResizeToContents)
        self.setItemDelegate(self.grid_delegate)
        self.setEditTriggers(QTableView.NoEditTriggers)

    def handle_click(self, index, x_offset):
        """
        Handle click events for the first column (call the edit method)
        and the second (call method for changing the palette)
        """
        if self.model().data(index, Qt.EditRole) is None:
            return
        if index.column() == 0 or x_offset > 24:
            self.edit(index)
        else:
            self.change_color(index)

    def change_color(self, index):
        """Invoke palette editor and set the color"""
        color = self.model().data(index, ColorRole)
        if color is None:
            return
        dlg = QColorDialog(QColor(*color))
        if dlg.exec():
            color = dlg.selectedColor()
            self.model().setData(index, color.getRgb(), ColorRole)


class ContinuousTable(ColorTable):
    """Table view for continuous variables"""

    def __init__(self, model):
        super().__init__(model)
        self.viewport().setMouseTracking(True)
        # View doesn't take ownership of delegates, so we must store it
        self.color_delegate = ColorStripDelegate(self)
        self.setItemDelegateForColumn(0, self.grid_delegate)
        self.setItemDelegateForColumn(1, self.color_delegate)
        self.setColumnWidth(1, 256)
        self.setEditTriggers(
            QTableView.SelectedClicked | QTableView.DoubleClicked)

    def mouseMoveEvent(self, event):
        """Store the hovered row index in the model, trigger viewport update"""
        pos = event.pos()
        ind = self.indexAt(pos)
        self.model().set_mouse_row(ind.row())
        super().mouseMoveEvent(event)
        self.viewport().update()

    def leaveEvent(self, _):
        """Remove the stored the hovered row index, trigger viewport update"""
        self.model().set_mouse_row(None)
        self.viewport().update()

    def handle_click(self, index, _):
        """Call the specific methods for handling clicks for each column"""
        if index.column() < 2:
            self.edit(index)
        elif index.column() == 2:
            self.model().copy_to_all(index)


class OWColor(widget.OWWidget):
    name = "Color"
    description = "Set color legend for variables."
    icon = "icons/Colors.svg"

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

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

    settingsHandler = settings.PerfectDomainContextHandler(
        match_values=settings.PerfectDomainContextHandler.MATCH_VALUES_ALL)
    disc_descs = settings.ContextSetting([])
    cont_descs = settings.ContextSetting([])
    selected_schema_index = settings.Setting(0)
    auto_apply = settings.Setting(True)

    settings_version = 2

    want_main_area = False

    def __init__(self):
        super().__init__()
        self.data = None
        self.orig_domain = self.domain = None

        box = gui.hBox(self.controlArea, "Discrete Variables")
        self.disc_model = DiscColorTableModel()
        self.disc_view = DiscreteTable(self.disc_model)
        self.disc_model.dataChanged.connect(self._on_data_changed)
        box.layout().addWidget(self.disc_view)

        box = gui.hBox(self.controlArea, "Numeric Variables")
        self.cont_model = ContColorTableModel()
        self.cont_view = ContinuousTable(self.cont_model)
        self.cont_model.dataChanged.connect(self._on_data_changed)
        box.layout().addWidget(self.cont_view)

        box = gui.hBox(self.buttonsArea)
        gui.button(box, self, "Save", callback=self.save)
        gui.button(box, self, "Load", callback=self.load)
        gui.button(box, self, "Reset", callback=self.reset)
        gui.rubber(self.buttonsArea)
        gui.auto_apply(self.buttonsArea, self, "auto_apply")

    @staticmethod
    def sizeHint():  # pragma: no cover
        return QSize(500, 570)

    @Inputs.data
    def set_data(self, data):
        self.closeContext()
        self.disc_descs = []
        self.cont_descs = []
        if data is None:
            self.data = self.domain = None
        else:
            self.data = data
            for var in chain(data.domain.variables, data.domain.metas):
                if var.is_discrete:
                    self.disc_descs.append(DiscAttrDesc(var))
                elif var.is_continuous:
                    self.cont_descs.append(ContAttrDesc(var))

        self.disc_model.set_data(self.disc_descs)
        self.cont_model.set_data(self.cont_descs)
        self.openContext(data)
        self.disc_view.resizeColumnsToContents()
        self.cont_view.resizeColumnsToContents()
        self.commit.now()

    def _on_data_changed(self):
        self.commit.deferred()

    def reset(self):
        self.disc_model.reset()
        self.cont_model.reset()
        # Reset button is in the same box as Load, which has commit.now,
        # and Apply, hence let Reset commit now, too.
        self.commit.now()

    def save(self):
        fname, _ = QFileDialog.getSaveFileName(
            self, "File name", self._start_dir(),
            "Variable definitions (*.colors)")
        if not fname:
            return
        QSettings().setValue("colorwidget/last-location",
                             os.path.split(fname)[0])
        self._save_var_defs(fname)

    def _save_var_defs(self, fname):
        with open(fname, "w") as f:
            json.dump(
                {vartype: {
                    var.name: var_data
                    for var, var_data in (
                        (desc.var, desc.to_dict()) for desc in repo)
                    if var_data}
                 for vartype, repo in (("categorical", self.disc_descs),
                                       ("numeric", self.cont_descs))
                },
                f,
                indent=4)

    def load(self):
        fname, _ = QFileDialog.getOpenFileName(
            self, "File name", self._start_dir(),
            "Variable definitions (*.colors)")
        if not fname:
            return

        try:
            f = open(fname)
        except IOError:
            QMessageBox.critical(self, "File error", "File cannot be opened.")
            return

        try:
            js = json.load(f)  #: dict
            self._parse_var_defs(js)
        except (json.JSONDecodeError, InvalidFileFormat):
            QMessageBox.critical(self, "File error", "Invalid file format.")

    def _parse_var_defs(self, js):
        if not isinstance(js, dict) or set(js) != {"categorical", "numeric"}:
            raise InvalidFileFormat
        try:
            renames = {
                var_name: desc["rename"]
                for repo in js.values() for var_name, desc in repo.items()
                if "rename" in desc
            }
        # js is an object coming from json file that can be manipulated by
        # the user, so there are too many things that can go wrong.
        # Catch all exceptions, therefore.
        except Exception as exc:
            raise InvalidFileFormat from exc
        if not all(isinstance(val, str)
                   for val in chain(renames, renames.values())):
            raise InvalidFileFormat
        renamed_vars = {
            renames.get(desc.var.name, desc.var.name)
            for desc in chain(self.disc_descs, self.cont_descs)
        }
        if len(renamed_vars) != len(self.disc_descs) + len(self.cont_descs):
            QMessageBox.warning(
                self,
                "Duplicated variable names",
                "Variables will not be renamed due to duplicated names.")
            for repo in js.values():
                for desc in repo.values():
                    desc.pop("rename", None)

        # First, construct all descriptions; assign later, after we know
        # there won't be exceptions due to invalid file format
        both_descs = []
        warnings = []
        for old_desc, repo, desc_type in (
                (self.disc_descs, "categorical", DiscAttrDesc),
                (self.cont_descs, "numeric", ContAttrDesc)):
            var_by_name = {desc.var.name: desc.var for desc in old_desc}
            new_descs = {}
            for var_name, var_data in js[repo].items():
                var = var_by_name.get(var_name)
                if var is None:
                    continue
                # This can throw InvalidFileFormat
                new_descs[var_name], warn = desc_type.from_dict(var, var_data)
                warnings += warn
            both_descs.append(new_descs)

        self.disc_descs = [both_descs[0].get(desc.var.name, desc)
                           for desc in self.disc_descs]
        self.cont_descs = [both_descs[1].get(desc.var.name, desc)
                           for desc in self.cont_descs]
        if warnings:
            QMessageBox.warning(
                self, "Invalid definitions", "\n".join(warnings))

        self.disc_model.set_data(self.disc_descs)
        self.cont_model.set_data(self.cont_descs)
        self.commit.now()

    def _start_dir(self):
        return self.workflowEnv().get("basedir") \
               or QSettings().value("colorwidget/last-location") \
               or os.path.expanduser(f"~{os.sep}")

    @gui.deferred
    def commit(self):
        def make(variables):
            new_vars = []
            for var in variables:
                source = disc_dict if var.is_discrete else cont_dict
                desc = source.get(var.name)
                new_vars.append(desc.create_variable() if desc else var)
            return new_vars

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

        disc_dict = {desc.var.name: desc for desc in self.disc_descs}
        cont_dict = {desc.var.name: desc for desc in self.cont_descs}

        dom = self.data.domain
        new_domain = Orange.data.Domain(
            make(dom.attributes), make(dom.class_vars), make(dom.metas))
        new_data = self.data.transform(new_domain)
        self.Outputs.data.send(new_data)

    def send_report(self):
        """Send report"""
        def _report_variables(variables):
            def was(n, o):
                return n if n == o else f"{n} (was: {o})"

            max_values = max(
                (len(var.values) for var in variables if var.is_discrete),
                default=1)

            rows = ""
            disc_dict = {k.var.name: k for k in self.disc_descs}
            cont_dict = {k.var.name: k for k in self.cont_descs}
            for var in variables:
                if var.is_discrete:
                    desc = disc_dict[var.name]
                    value_cols = "    \n".join(
                        f"<td>{square(*color)} {was(value, old_value)}</td>"
                        for color, value, old_value in
                        zip(desc.colors, desc.values, var.values))
                elif var.is_continuous:
                    desc = cont_dict[var.name]
                    pal = colorpalettes.ContinuousPalettes[desc.palette_name]
                    value_cols = f'<td colspan="{max_values}">' \
                                 f'{pal.friendly_name}</td>'
                else:
                    continue
                names = was(desc.name, desc.var.name)
                rows += '<tr style="height: 2em">\n' \
                        f'  <th style="text-align: right">{names}</th>' \
                        f'  {value_cols}\n' \
                        '</tr>\n'
            return rows

        if not self.data:
            return
        dom = self.data.domain
        sections = (
            (name, _report_variables(variables))
            for name, variables in (
                ("Features", dom.attributes),
                ("Outcome" + "s" * (len(dom.class_vars) > 1), dom.class_vars),
                ("Meta attributes", dom.metas)))
        table = "".join(f"<tr><th>{name}</th></tr>{rows}"
                        for name, rows in sections if rows)
        if table:
            self.report_raw(f"<table>{table}</table>")

    @classmethod
    def migrate_context(cls, _, version):
        if not version or version < 2:
            raise IncompatibleContext


if __name__ == "__main__":  # pragma: no cover
    WidgetPreview(OWColor).run(Orange.data.Table("heart_disease.tab"))

```
--- 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