{
    "file_diffs": [
        {
            "old_file_content": "from collections import namedtuple\nfrom itertools import chain, product\n\nimport numpy as np\n\nfrom AnyQt.QtCore import Qt, QModelIndex, pyqtSignal as Signal\nfrom AnyQt.QtWidgets import (\n    QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout\n)\nfrom orangewidget.utils.combobox import ComboBoxSearch\n\nimport Orange\nfrom Orange.data import StringVariable, ContinuousVariable, Variable\nfrom Orange.data.util import hstack, get_unique_names_duplicates\nfrom Orange.widgets import widget, gui\nfrom Orange.widgets.settings import Setting, ContextHandler, ContextSetting\nfrom Orange.widgets.utils import vartype\nfrom Orange.widgets.utils.itemmodels import DomainModel\nfrom Orange.widgets.utils.sql import check_sql_input\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.utils.state_summary import format_multiple_summaries, \\\n    format_summary_details\nfrom Orange.widgets.widget import Input, Output, Msg\n\nINSTANCEID = \"Instance id\"\nINDEX = \"Row index\"\n\n\nclass ConditionBox(QWidget):\n    vars_changed = Signal(list)\n\n    RowItems = namedtuple(\n        \"RowItems\",\n        (\"pre_label\", \"left_combo\", \"in_label\", \"right_combo\",\n         \"remove_button\", \"add_button\"))\n\n    def __init__(self, parent, model_left, model_right, pre_label, in_label):\n        super().__init__(parent)\n        self.model_left = model_left\n        self.model_right = model_right\n        self.pre_label, self.in_label = pre_label, in_label\n        self.rows = []\n        self.setLayout(QVBoxLayout())\n        self.layout().setContentsMargins(0, 0, 0, 0)\n        self.layout().setSpacing(0)\n        self.setMouseTracking(True)\n\n    def add_row(self):\n        def sync_combos():\n            combo = self.sender()\n            index = combo.currentIndex()\n            model = combo.model()\n\n            other = ({row_items.left_combo, row_items.right_combo}\n                     - {combo}).pop()\n            other_index = other.currentIndex()\n            other_model = other.model()\n\n            if 0 <= index < len(combo.model()):\n                var = model[index]\n                if isinstance(var, str):\n                    other.setCurrentText(model[index])\n                elif isinstance(other_model[other_index], str):\n                    for other_var in other_model:\n                        if isinstance(other_var, Variable) \\\n                                and var.name == other_var.name \\\n                                and (type(other_var) is type(var)\n                                     or (not var.is_continuous\n                                         and not other_var.is_continuous)):\n                            other.setCurrentText(var.name)\n                            break\n\n            self.emit_list()\n\n        def get_combo(model):\n            combo = ComboBoxSearch(self)\n            combo.setModel(model)\n            # We use signal activated because it is triggered only on user\n            # interaction, not programmatically.\n            combo.activated.connect(sync_combos)\n            return combo\n\n        def get_button(label, callback):\n            button = QPushButton(label, self)\n            button.setFlat(True)\n            button.setFixedWidth(12)\n            button.clicked.connect(callback)\n            return button\n\n        row = self.layout().count()\n        row_items = self.RowItems(\n            QLabel(\"and\" if row else self.pre_label),\n            get_combo(self.model_left),\n            QLabel(self.in_label),\n            get_combo(self.model_right),\n            get_button(\"×\", self.on_remove_row),\n            get_button(\"+\", self.on_add_row)\n        )\n        layout = QHBoxLayout()\n        layout.setSpacing(10)\n        self.layout().addLayout(layout)\n        layout.addStretch(10)\n        for item in row_items:\n            layout.addWidget(item)\n        self.rows.append(row_items)\n        self._reset_buttons()\n\n    def remove_row(self, row):\n        self.layout().takeAt(self.rows.index(row))\n        self.rows.remove(row)\n        for item in row:\n            item.deleteLater()\n        self._reset_buttons()\n\n    def on_add_row(self, _):\n        self.add_row()\n        self.emit_list()\n\n    def on_remove_row(self):\n        if len(self.rows) == 1:\n            return\n        button = self.sender()\n        for row in self.rows:\n            if button is row.remove_button:\n                self.remove_row(row)\n                break\n        self.emit_list()\n\n    def _reset_buttons(self):\n        def endis(button, enable, text):\n            button.setEnabled(enable)\n            button.setText(text if enable else \"\")\n\n        self.rows[0].pre_label.setText(self.pre_label)\n        single_row = len(self.rows) == 1\n        endis(self.rows[0].remove_button, not single_row, \"×\")\n        endis(self.rows[-1].add_button, True, \"+\")\n        if not single_row:\n            endis(self.rows[-2].add_button, False, \"\")\n\n    def current_state(self):\n        def get_var(model, combo):\n            return model[combo.currentIndex()]\n\n        return [(get_var(self.model_left, row.left_combo),\n                 get_var(self.model_right, row.right_combo))\n                for row in self.rows]\n\n    def set_state(self, values):\n        while len(self.rows) > len(values):\n            self.remove_row(self.rows[0])\n        while len(self.rows) < len(values):\n            self.add_row()\n        for (val_left, val_right), row in zip(values, self.rows):\n            row.left_combo.setCurrentIndex(self.model_left.indexOf(val_left))\n            row.right_combo.setCurrentIndex(self.model_right.indexOf(val_right))\n\n    def emit_list(self):\n        self.vars_changed.emit(self.current_state())\n\n\nclass DomainModelWithTooltips(DomainModel):\n    def data(self, index, role=Qt.DisplayRole):\n        if role == Qt.ToolTipRole \\\n                and isinstance(index, QModelIndex) and index.isValid():\n            if index.row() == 0:\n                return \"Match rows sequentially\"\n            if index.row() == 1:\n                return \"Re-match rows from tables obtained from the same \" \\\n                       \"source,\\n\" \\\n                       \"e.g. data from the same file that was split within \" \\\n                       \"the workflow.\"\n        return super().data(index, role)\n\n\nclass MergeDataContextHandler(ContextHandler):\n    # `widget` is used as an argument in most methods\n    # pylint: disable=redefined-outer-name\n    # context handlers override methods using different signatures\n    # pylint: disable=arguments-differ\n\n    def new_context(self, variables1, variables2):\n        context = super().new_context()\n        context.variables1 = variables1\n        context.variables2 = variables2\n        return context\n\n    def open_context(self, widget, domain1, domain2):\n        if domain1 is not None and domain2 is not None:\n            super().open_context(widget,\n                                 self._encode_domain(domain1),\n                                 self._encode_domain(domain2))\n\n    @staticmethod\n    def encode_variables(variables):\n        return [(v.name, 100 + vartype(v))\n                if isinstance(v, Variable) else (v, 100)\n                for v in variables]\n\n    @staticmethod\n    def decode_pair(widget, pair):\n        left_domain = widget.data and widget.data.domain\n        right_domain = widget.extra_data and widget.extra_data.domain\n        return tuple(var[0] if var[0] in (INDEX, INSTANCEID) else domain[var[0]]\n                     for domain, var in zip((left_domain, right_domain), pair))\n\n    def _encode_domain(self, domain):\n        if domain is None:\n            return {}\n        all_vars = chain(domain.variables, domain.metas)\n        return dict(self.encode_variables(all_vars))\n\n    def settings_from_widget(self, widget, *_args):\n        context = widget.current_context\n        if context is None:\n            return\n        context.values[\"attr_pairs\"] = [self.encode_variables(row)\n                                        for row in widget.attr_pairs]\n\n    def settings_to_widget(self, widget, *_args):\n        context = widget.current_context\n        if context is None:\n            return\n        pairs = context.values.get(\"attr_pairs\")\n        if pairs:\n            # attr_pairs is schema only setting which means it is not always\n            # present. When not present leave widgets default.\n            widget.attr_pairs = [\n                self.decode_pair(widget, pair) for pair in pairs\n            ]\n\n    def match(self, context, variables1, variables2):\n        def matches(part, variables):\n            return all(var[1] == 100 and var[0] in variables\n                       or variables.get(var[0], -1) == var[1] for var in part)\n\n        if (variables1, variables2) == (context.variables1, context.variables2):\n            return self.PERFECT_MATCH\n\n        if \"attr_pairs\" in context.values:\n            left, right = zip(*context.values[\"attr_pairs\"])\n            if matches(left, variables1) and matches(right, variables2):\n                return 0.5\n\n        return self.NO_MATCH\n\n\nclass OWMergeData(widget.OWWidget):\n    name = \"Merge Data\"\n    description = \"Merge datasets based on the values of selected features.\"\n    icon = \"icons/MergeData.svg\"\n    priority = 1110\n    keywords = [\"join\"]\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table, default=True, replaces=[\"Data A\"])\n        extra_data = Input(\"Extra Data\", Orange.data.Table, replaces=[\"Data B\"])\n\n    class Outputs:\n        data = Output(\"Data\",\n                      Orange.data.Table,\n                      replaces=[\"Merged Data A+B\", \"Merged Data B+A\", \"Merged Data\"])\n\n    LeftJoin, InnerJoin, OuterJoin = range(3)\n    OptionNames = (\"Append columns from Extra data\",\n                   \"Find matching pairs of rows\",\n                   \"Concatenate tables\")\n    OptionDescriptions = (\n        \"The first table may contain, for instance, city names,\\n\"\n        \"and the second would be a list of cities and their coordinates.\\n\"\n        \"Columns with coordinates would then be appended to the output.\",\n\n        \"Input tables contain different features describing the same data \"\n        \"instances.\\n\"\n        \"Output contains matched instances. Rows without matches are removed.\",\n\n        \"Input tables contain different features describing the same data \"\n        \"instances.\\n\"\n        \"Output contains all instances. Data from merged instances is \"\n        \"merged into single rows.\"\n    )\n\n    UserAdviceMessages = [\n        widget.Message(\n            \"Confused about merging options?\\nSee the tooltips!\",\n            \"merging_types\")]\n\n    settingsHandler = MergeDataContextHandler()\n    attr_pairs = ContextSetting(None, schema_only=True)\n    merging = Setting(LeftJoin)\n    auto_apply = Setting(True)\n    settings_version = 2\n\n    want_main_area = False\n    resizing_enabled = False\n\n    class Warning(widget.OWWidget.Warning):\n        renamed_vars = Msg(\"Some variables have been renamed \"\n                           \"to avoid duplicates.\\n{}\")\n\n    class Error(widget.OWWidget.Error):\n        matching_numeric_with_nonnum = Msg(\n            \"Numeric and non-numeric columns ({} and {}) cannot be matched.\")\n        matching_index_with_sth = Msg(\"Row index cannot be matched with {}.\")\n        matching_id_with_sth = Msg(\"Instance cannot be matched with {}.\")\n        nonunique_left = Msg(\n            \"Some combinations of values on the left appear in multiple rows.\\n\"\n            \"For this type of merging, every possible combination of values \"\n            \"on the left should appear at most once.\")\n        nonunique_right = Msg(\n            \"Some combinations of values on the right appear in multiple rows.\"\n            \"\\n\"\n            \"Every possible combination of values on the right should appear \"\n            \"at most once.\")\n\n    def __init__(self):\n        super().__init__()\n\n        self.data = None\n        self.extra_data = None\n\n        content = [\n            INDEX, INSTANCEID,\n            DomainModel.ATTRIBUTES, DomainModel.CLASSES, DomainModel.METAS]\n        self.model = DomainModelWithTooltips(content)\n        self.extra_model = DomainModelWithTooltips(content)\n\n        self.info.set_input_summary(self.info.NoInput)\n        self.info.set_output_summary(self.info.NoOutput)\n\n        grp = gui.radioButtons(\n            self.controlArea, self, \"merging\", box=\"Merging\",\n            btnLabels=self.OptionNames, tooltips=self.OptionDescriptions,\n            callback=self.change_merging)\n\n        self.attr_boxes = ConditionBox(\n            self, self.model, self.extra_model, \"\", \"matches\")\n        self.attr_boxes.add_row()\n        box = gui.vBox(self.controlArea, box=\"Row matching\")\n        box.layout().addWidget(self.attr_boxes)\n\n        gui.auto_apply(self.buttonsArea, self)\n        # connect after wrapping self.commit with gui.auto_commit!\n        self.attr_boxes.vars_changed.connect(self.commit)\n        self.attr_boxes.vars_changed.connect(self.store_combo_state)\n        self.settingsAboutToBePacked.connect(self.store_combo_state)\n\n    def change_merging(self):\n        self.commit()\n\n    @Inputs.data\n    @check_sql_input\n    def set_data(self, data):\n        self.data = data\n        self.model.set_domain(data.domain if data else None)\n\n    @Inputs.extra_data\n    @check_sql_input\n    def set_extra_data(self, data):\n        self.extra_data = data\n        self.extra_model.set_domain(data.domain if data else None)\n\n    def store_combo_state(self):\n        self.attr_pairs = self.attr_boxes.current_state()\n\n    def handleNewSignals(self):\n        self.closeContext()\n        self.attr_pairs = [self._find_best_match()]\n        self.openContext(self.data and self.data.domain,\n                         self.extra_data and self.extra_data.domain)\n        self.attr_boxes.set_state(self.attr_pairs)\n\n        summary, details, kwargs = self.info.NoInput, \"\", {}\n        if self.data or self.extra_data:\n            n_data = len(self.data) if self.data else 0\n            n_extra_data = len(self.extra_data) if self.extra_data else 0\n            summary = f\"{self.info.format_number(n_data)}, \" \\\n                      f\"{self.info.format_number(n_extra_data)}\"\n            kwargs = {\"format\": Qt.RichText}\n            details = format_multiple_summaries([\n                (\"Data\", self.data),\n                (\"Extra data\", self.extra_data)\n            ])\n        self.info.set_input_summary(summary, details, **kwargs)\n\n        self.unconditional_commit()\n\n    def _find_best_match(self):\n        def get_unique_str_metas_names(model_):\n            return [m for m in model_ if isinstance(m, StringVariable)]\n\n        attr, extra_attr, n_max_intersect = INDEX, INDEX, 0\n        str_metas = get_unique_str_metas_names(self.model)\n        extra_str_metas = get_unique_str_metas_names(self.extra_model)\n        for m_a, m_b in product(str_metas, extra_str_metas):\n            col = self.data[:, m_a].metas\n            extra_col = self.extra_data[:, m_b].metas\n            if col.size and extra_col.size \\\n                    and isinstance(col[0][0], str) \\\n                    and isinstance(extra_col[0][0], str):\n                n_inter = len(np.intersect1d(col, extra_col))\n                if n_inter > n_max_intersect:\n                    n_max_intersect, attr, extra_attr = n_inter, m_a, m_b\n        return attr, extra_attr\n\n    def commit(self):\n        self.clear_messages()\n        merged = self.merge() if self.data and self.extra_data else None\n        self.Outputs.data.send(merged)\n        details = format_summary_details(merged) if merged else \"\"\n        summary = len(merged) if merged else self.info.NoOutput\n        self.info.set_output_summary(summary, details)\n\n    def send_report(self):\n        # pylint: disable=invalid-sequence-index\n        self.report_items((\n            (\"Merging\", self.OptionNames[self.merging]),\n            (\"Match\",\n             \", \".join(\n                 f\"{self._get_col_name(left)} with {self._get_col_name(right)}\"\n                 for left, right in self.attr_boxes.current_state()))))\n\n    def merge(self):\n        # pylint: disable=invalid-sequence-index\n        pairs = self.attr_boxes.current_state()\n        if not self._check_pair_types(pairs):\n            return None\n        left_vars, right_vars = zip(*pairs)\n        left_mask = np.full(len(self.data), True)\n        left = np.vstack(tuple(self._values(self.data, var, left_mask)\n                               for var in left_vars)).T\n        right_mask = np.full(len(self.extra_data), True)\n        right = np.vstack(tuple(self._values(self.extra_data, var, right_mask)\n                                for var in right_vars)).T\n        if not self._check_uniqueness(left, left_mask, right, right_mask):\n            return None\n        method = self._merge_methods[self.merging]\n        lefti, righti, rightu = method(self, left, left_mask, right, right_mask)\n        reduced_extra_data = \\\n            self._compute_reduced_extra_data(right_vars, lefti, righti, rightu)\n        return self._join_table_by_indices(\n            reduced_extra_data, lefti, righti, rightu)\n\n    def _check_pair_types(self, pairs):\n        for left, right in pairs:\n            if isinstance(left, ContinuousVariable) \\\n                    != isinstance(right, ContinuousVariable):\n                self.Error.matching_numeric_with_nonnum(left, right)\n                return False\n            if INDEX in (left, right) and left != right:\n                self.Error.matching_index_with_sth(\n                    self._get_col_name(({left, right} - {INDEX}).pop()))\n                return False\n            if INSTANCEID in (left, right) and left != right:\n                self.Error.matching_id_with_sth(\n                    self._get_col_name(({left, right} - {INSTANCEID}).pop()))\n                return False\n        return True\n\n    @staticmethod\n    def _get_col_name(obj):\n        return f\"'{obj.name}'\" if isinstance(obj, Variable) else obj.lower()\n\n    def _check_uniqueness(self, left, left_mask, right, right_mask):\n        ok = True\n        masked_right = right[right_mask]\n        if len(set(map(tuple, masked_right))) != len(masked_right):\n            self.Error.nonunique_right()\n            ok = False\n        if self.merging != self.LeftJoin:\n            masked_left = left[left_mask]\n            if len(set(map(tuple, masked_left))) != len(masked_left):\n                self.Error.nonunique_left()\n                ok = False\n        return ok\n\n    def _compute_reduced_extra_data(self,\n                                    right_match_vars, lefti, righti, rightu):\n        \"\"\"Prepare a table with extra columns that will appear in the merged\n        table\"\"\"\n        domain = self.data.domain\n        extra_domain = self.extra_data.domain\n\n        def var_needed(var):\n            if rightu is not None and rightu.size:\n                return True\n            if var in right_match_vars and self.merging != self.OuterJoin:\n                return False\n            if var not in domain:\n                return True\n            both_defined = (lefti != -1) * (righti != -1)\n            left_col = \\\n                self.data.get_column_view(var)[0][lefti[both_defined]]\n            right_col = \\\n                self.extra_data.get_column_view(var)[0][righti[both_defined]]\n            if var.is_primitive():\n                left_col = left_col.astype(float)\n                right_col = right_col.astype(float)\n                mask_left = np.isfinite(left_col)\n                mask_right = np.isfinite(right_col)\n                return not (\n                    np.all(mask_left == mask_right)\n                    and np.all(left_col[mask_left] == right_col[mask_right]))\n            else:\n                return not np.all(left_col == right_col)\n\n        extra_vars = [\n            var for var in chain(extra_domain.variables, extra_domain.metas)\n            if var_needed(var)]\n        return self.extra_data[:, extra_vars]\n\n    @staticmethod\n    def _values(data, var, mask):\n        \"\"\"Return an iterotor over keys for rows of the table.\"\"\"\n        if var == INDEX:\n            return np.arange(len(data))\n        if var == INSTANCEID:\n            return np.fromiter(\n                (inst.id for inst in data), count=len(data), dtype=np.int)\n        col = data.get_column_view(var)[0]\n        if var.is_primitive():\n            col = col.astype(float, copy=False)\n            nans = np.isnan(col)\n            mask *= ~nans\n            if var.is_discrete:\n                col = col.astype(int)\n                col[nans] = len(var.values)\n                col = np.array(var.values + (np.nan, ))[col]\n        else:\n            col = col.copy()\n            defined = col.astype(bool)\n            mask *= defined\n            col[~mask] = np.nan\n        return col\n\n    def _left_join_indices(self, left, left_mask, right, right_mask):\n        \"\"\"Compute a two-row array of indices:\n        - the first row contains indices for the primary table,\n        - the second row contains the matching rows in the extra table or -1\"\"\"\n        data = self.data\n        # Don't match nans. This is needed since numpy may change nan to string\n        # nan, so nan's will match each other\n        indices = np.arange(len(right))\n        indices[~right_mask] = -1\n        if right.shape[1] == 1:\n            # The more common case can be handled faster\n            right_map = dict(zip(right.flatten(), indices))\n            righti = (right_map.get(val, -1) for val in left.flatten())\n        else:\n            right_map = dict(zip(map(tuple, right), indices))\n            righti = (right_map.get(tuple(val), -1) for val in left)\n        righti = np.fromiter(righti, dtype=np.int64, count=len(data))\n        lefti = np.arange(len(data), dtype=np.int64)\n        righti[lefti[~left_mask]] = -1\n        return lefti, righti, None\n\n    def _inner_join_indices(self, left, left_mask, right, right_mask):\n        \"\"\"Use _augment_indices to compute the array of indices,\n        then remove those with no match in the second table\"\"\"\n        lefti, righti, _ = \\\n            self._left_join_indices(left, left_mask, right, right_mask)\n        mask = righti != [-1]\n        return lefti[mask], righti[mask], None\n\n    def _outer_join_indices(self, left, left_mask, right, right_mask):\n        \"\"\"Use _augment_indices to compute the array of indices,\n        then add rows in the second table without a match in the first\"\"\"\n        lefti, righti, _ = \\\n            self._left_join_indices(left, left_mask, right, right_mask)\n        unused = np.full(len(right), True)\n        unused[righti] = False\n        if len(right) - 1 not in righti:\n            # righti can include -1, which sets the last element as used\n            unused[-1] = True\n        return lefti, righti, np.nonzero(unused)[0]\n\n    _merge_methods = [\n        _left_join_indices, _inner_join_indices, _outer_join_indices]\n\n    def _join_table_by_indices(self, reduced_extra, lefti, righti, rightu):\n        \"\"\"Join (horizontally) self.data and reduced_extra, taking the pairs\n        of rows given in indices\"\"\"\n        if not lefti.size:\n            return None\n        lt_dom = self.data.domain\n        xt_dom = reduced_extra.domain\n        domain = self._domain_rename_duplicates(\n            lt_dom.attributes + xt_dom.attributes,\n            lt_dom.class_vars + xt_dom.class_vars,\n            lt_dom.metas + xt_dom.metas)\n        X = self._join_array_by_indices(\n            self.data.X, reduced_extra.X, lefti, righti)\n        Y = self._join_array_by_indices(\n            np.c_[self.data.Y], np.c_[reduced_extra.Y], lefti, righti)\n        string_cols = [i for i, var in enumerate(domain.metas) if var.is_string]\n        metas = self._join_array_by_indices(\n            self.data.metas, reduced_extra.metas, lefti, righti, string_cols)\n        if rightu is not None:\n            # This domain is used for transforming the extra rows for outer join\n            # It must use the original - not renamed - variables from right, so\n            # values are copied,\n            # but new domain for the left, so renamed values are *not* copied\n            right_domain = Orange.data.Domain(\n                domain.attributes[:len(lt_dom.attributes)] + xt_dom.attributes,\n                domain.class_vars[:len(lt_dom.class_vars)] + xt_dom.class_vars,\n                domain.metas[:len(lt_dom.metas)] + xt_dom.metas)\n            extras = self.extra_data[rightu].transform(right_domain)\n            X = np.vstack((X, extras.X))\n            extras_Y = extras.Y\n            if extras_Y.ndim == 1:\n                extras_Y = extras_Y.reshape(-1, 1)\n            Y = np.vstack((Y, extras_Y))\n            metas = np.vstack((metas, extras.metas))\n        table = Orange.data.Table.from_numpy(domain, X, Y, metas)\n        table.name = getattr(self.data, 'name', '')\n        table.attributes = getattr(self.data, 'attributes', {})\n        if rightu is not None:\n            table.ids = np.hstack(\n                (self.data.ids, self.extra_data.ids[rightu]))\n        else:\n            table.ids = self.data.ids[lefti]\n\n        return table\n\n    def _domain_rename_duplicates(self, attributes, class_vars, metas):\n        \"\"\"Check for duplicate variable names in domain. If any, rename\n        the variables, by replacing them with new ones (names are\n        appended a number). \"\"\"\n        attrs, cvars, mets = [], [], []\n        n_attrs, n_cvars, n_metas = len(attributes), len(class_vars), len(metas)\n        lists = [attrs] * n_attrs + [cvars] * n_cvars + [mets] * n_metas\n\n        all_vars = attributes + class_vars + metas\n        proposed_names = [m.name for m in all_vars]\n        unique_names = get_unique_names_duplicates(proposed_names)\n        duplicates = set()\n        for p_name, u_name, var, c in zip(proposed_names, unique_names,\n                                          all_vars, lists):\n            if p_name != u_name:\n                duplicates.add(p_name)\n                var = var.copy(name=u_name)\n            c.append(var)\n        if duplicates:\n            self.Warning.renamed_vars(\", \".join(duplicates))\n        return Orange.data.Domain(attrs, cvars, mets)\n\n    @staticmethod\n    def _join_array_by_indices(left, right, lefti, righti, string_cols=None):\n        \"\"\"Join (horizontally) two arrays, taking pairs of rows given in indices\n        \"\"\"\n        def prepare(arr, inds, str_cols):\n            try:\n                newarr = arr[inds]\n            except IndexError:\n                newarr = np.full_like(arr, np.nan)\n            else:\n                empty = np.full(arr.shape[1], np.nan)\n                if str_cols:\n                    assert arr.dtype == object\n                    empty = empty.astype(object)\n                    empty[str_cols] = ''\n                newarr[inds == -1] = empty\n            return newarr\n\n        left_width = left.shape[1]\n        str_left = [i for i in string_cols or () if i < left_width]\n        str_right = [i - left_width for i in string_cols or () if i >= left_width]\n        res = hstack((prepare(left, lefti, str_left),\n                      prepare(right, righti, str_right)))\n        return res\n\n    @staticmethod\n    def migrate_settings(settings, version=None):\n        def mig_value(x):\n            if x == \"Position (index)\":\n                return INDEX\n            if x == \"Source position (index)\":\n                return INSTANCEID\n            return x\n\n        if not version:\n            operations = (\"augment\", \"merge\", \"combine\")\n            oper = operations[settings[\"merging\"]]\n            settings[\"attr_pairs\"] = (\n                True, True,\n                [(mig_value(settings[f\"attr_{oper}_data\"]),\n                  mig_value(settings[f\"attr_{oper}_extra\"]))])\n            for oper in operations:\n                del settings[f\"attr_{oper}_data\"]\n                del settings[f\"attr_{oper}_extra\"]\n\n        if not version or version < 2 and \"attr_pairs\" in settings:\n            data_exists, extra_exists, attr_pairs = settings.pop(\"attr_pairs\")\n            if not (data_exists and extra_exists):\n                settings[\"context_settings\"] = []\n                return\n\n            mapper = {0: (INDEX, 100), 1: (INSTANCEID, 100)}\n            context = ContextHandler().new_context()\n            context.values[\"attr_pairs\"] = [tuple(mapper.get(var, (var, 100))\n                                                  for var in pair)\n                                            for pair in attr_pairs]\n            context.variables1 = {}\n            context.variables2 = {}\n            settings[\"context_settings\"] = [context]\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWMergeData).run(\n        set_data=Orange.data.Table(\"tests/data-gender-region\"),\n        set_extra_data=Orange.data.Table(\"tests/data-regions\"))\n",
            "new_file_content": "from collections import namedtuple\nfrom itertools import chain, product\n\nimport numpy as np\n\nfrom AnyQt.QtCore import Qt, QModelIndex, pyqtSignal as Signal\nfrom AnyQt.QtWidgets import (\n    QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout\n)\nfrom orangewidget.utils.combobox import ComboBoxSearch\n\nimport Orange\nfrom Orange.data import StringVariable, ContinuousVariable, Variable, Domain\nfrom Orange.data.util import hstack, get_unique_names_duplicates\nfrom Orange.widgets import widget, gui\nfrom Orange.widgets.settings import Setting, ContextHandler, ContextSetting\nfrom Orange.widgets.utils import vartype\nfrom Orange.widgets.utils.itemmodels import DomainModel\nfrom Orange.widgets.utils.sql import check_sql_input\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.utils.state_summary import format_multiple_summaries, \\\n    format_summary_details\nfrom Orange.widgets.widget import Input, Output, Msg\n\nINSTANCEID = \"Instance id\"\nINDEX = \"Row index\"\n\n\nclass ConditionBox(QWidget):\n    vars_changed = Signal(list)\n\n    RowItems = namedtuple(\n        \"RowItems\",\n        (\"pre_label\", \"left_combo\", \"in_label\", \"right_combo\",\n         \"remove_button\", \"add_button\"))\n\n    def __init__(self, parent, model_left, model_right, pre_label, in_label):\n        super().__init__(parent)\n        self.model_left = model_left\n        self.model_right = model_right\n        self.pre_label, self.in_label = pre_label, in_label\n        self.rows = []\n        self.setLayout(QVBoxLayout())\n        self.layout().setContentsMargins(0, 0, 0, 0)\n        self.layout().setSpacing(0)\n        self.setMouseTracking(True)\n\n    def add_row(self):\n        def sync_combos():\n            combo = self.sender()\n            index = combo.currentIndex()\n            model = combo.model()\n\n            other = ({row_items.left_combo, row_items.right_combo}\n                     - {combo}).pop()\n            other_index = other.currentIndex()\n            other_model = other.model()\n\n            if 0 <= index < len(combo.model()):\n                var = model[index]\n                if isinstance(var, str):\n                    other.setCurrentText(model[index])\n                elif isinstance(other_model[other_index], str):\n                    for other_var in other_model:\n                        if isinstance(other_var, Variable) \\\n                                and var.name == other_var.name \\\n                                and (type(other_var) is type(var)\n                                     or (not var.is_continuous\n                                         and not other_var.is_continuous)):\n                            other.setCurrentText(var.name)\n                            break\n\n            self.emit_list()\n\n        def get_combo(model):\n            combo = ComboBoxSearch(self)\n            combo.setModel(model)\n            # We use signal activated because it is triggered only on user\n            # interaction, not programmatically.\n            combo.activated.connect(sync_combos)\n            return combo\n\n        def get_button(label, callback):\n            button = QPushButton(label, self)\n            button.setFlat(True)\n            button.setFixedWidth(12)\n            button.clicked.connect(callback)\n            return button\n\n        row = self.layout().count()\n        row_items = self.RowItems(\n            QLabel(\"and\" if row else self.pre_label),\n            get_combo(self.model_left),\n            QLabel(self.in_label),\n            get_combo(self.model_right),\n            get_button(\"×\", self.on_remove_row),\n            get_button(\"+\", self.on_add_row)\n        )\n        layout = QHBoxLayout()\n        layout.setSpacing(10)\n        self.layout().addLayout(layout)\n        layout.addStretch(10)\n        for item in row_items:\n            layout.addWidget(item)\n        self.rows.append(row_items)\n        self._reset_buttons()\n\n    def remove_row(self, row):\n        self.layout().takeAt(self.rows.index(row))\n        self.rows.remove(row)\n        for item in row:\n            item.deleteLater()\n        self._reset_buttons()\n\n    def on_add_row(self, _):\n        self.add_row()\n        self.emit_list()\n\n    def on_remove_row(self):\n        if len(self.rows) == 1:\n            return\n        button = self.sender()\n        for row in self.rows:\n            if button is row.remove_button:\n                self.remove_row(row)\n                break\n        self.emit_list()\n\n    def _reset_buttons(self):\n        def endis(button, enable, text):\n            button.setEnabled(enable)\n            button.setText(text if enable else \"\")\n\n        self.rows[0].pre_label.setText(self.pre_label)\n        single_row = len(self.rows) == 1\n        endis(self.rows[0].remove_button, not single_row, \"×\")\n        endis(self.rows[-1].add_button, True, \"+\")\n        if not single_row:\n            endis(self.rows[-2].add_button, False, \"\")\n\n    def current_state(self):\n        def get_var(model, combo):\n            return model[combo.currentIndex()]\n\n        return [(get_var(self.model_left, row.left_combo),\n                 get_var(self.model_right, row.right_combo))\n                for row in self.rows]\n\n    def set_state(self, values):\n        while len(self.rows) > len(values):\n            self.remove_row(self.rows[0])\n        while len(self.rows) < len(values):\n            self.add_row()\n        for (val_left, val_right), row in zip(values, self.rows):\n            row.left_combo.setCurrentIndex(self.model_left.indexOf(val_left))\n            row.right_combo.setCurrentIndex(self.model_right.indexOf(val_right))\n\n    def emit_list(self):\n        self.vars_changed.emit(self.current_state())\n\n\nclass DomainModelWithTooltips(DomainModel):\n    def data(self, index, role=Qt.DisplayRole):\n        if role == Qt.ToolTipRole \\\n                and isinstance(index, QModelIndex) and index.isValid():\n            if index.row() == 0:\n                return \"Match rows sequentially\"\n            if index.row() == 1:\n                return \"Re-match rows from tables obtained from the same \" \\\n                       \"source,\\n\" \\\n                       \"e.g. data from the same file that was split within \" \\\n                       \"the workflow.\"\n        return super().data(index, role)\n\n\nclass MergeDataContextHandler(ContextHandler):\n    # `widget` is used as an argument in most methods\n    # pylint: disable=redefined-outer-name\n    # context handlers override methods using different signatures\n    # pylint: disable=arguments-differ\n\n    def new_context(self, variables1, variables2):\n        context = super().new_context()\n        context.variables1 = variables1\n        context.variables2 = variables2\n        return context\n\n    def open_context(self, widget, domain1, domain2):\n        if domain1 is not None and domain2 is not None:\n            super().open_context(widget,\n                                 self._encode_domain(domain1),\n                                 self._encode_domain(domain2))\n\n    @staticmethod\n    def encode_variables(variables):\n        return [(v.name, 100 + vartype(v))\n                if isinstance(v, Variable) else (v, 100)\n                for v in variables]\n\n    @staticmethod\n    def decode_pair(widget, pair):\n        left_domain = widget.data and widget.data.domain\n        right_domain = widget.extra_data and widget.extra_data.domain\n        return tuple(var[0] if var[0] in (INDEX, INSTANCEID) else domain[var[0]]\n                     for domain, var in zip((left_domain, right_domain), pair))\n\n    def _encode_domain(self, domain):\n        if domain is None:\n            return {}\n        if not isinstance(domain, Domain):\n            domain = domain.domain\n        all_vars = chain(domain.variables, domain.metas)\n        return dict(self.encode_variables(all_vars))\n\n    def settings_from_widget(self, widget, *_args):\n        context = widget.current_context\n        if context is None:\n            return\n        context.values[\"attr_pairs\"] = [self.encode_variables(row)\n                                        for row in widget.attr_pairs]\n\n    def settings_to_widget(self, widget, *_args):\n        context = widget.current_context\n        if context is None:\n            return\n        pairs = context.values.get(\"attr_pairs\")\n        if pairs:\n            # attr_pairs is schema only setting which means it is not always\n            # present. When not present leave widgets default.\n            widget.attr_pairs = [\n                self.decode_pair(widget, pair) for pair in pairs\n            ]\n\n    def match(self, context, variables1, variables2):\n        def matches(part, variables):\n            return all(var[1] == 100 and var[0] in variables\n                       or variables.get(var[0], -1) == var[1] for var in part)\n\n        if (variables1, variables2) == (context.variables1, context.variables2):\n            return self.PERFECT_MATCH\n\n        if \"attr_pairs\" in context.values:\n            left, right = zip(*context.values[\"attr_pairs\"])\n            if matches(left, variables1) and matches(right, variables2):\n                return 0.5\n\n        return self.NO_MATCH\n\n\nclass OWMergeData(widget.OWWidget):\n    name = \"Merge Data\"\n    description = \"Merge datasets based on the values of selected features.\"\n    icon = \"icons/MergeData.svg\"\n    priority = 1110\n    keywords = [\"join\"]\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table, default=True, replaces=[\"Data A\"])\n        extra_data = Input(\"Extra Data\", Orange.data.Table, replaces=[\"Data B\"])\n\n    class Outputs:\n        data = Output(\"Data\",\n                      Orange.data.Table,\n                      replaces=[\"Merged Data A+B\", \"Merged Data B+A\", \"Merged Data\"])\n\n    LeftJoin, InnerJoin, OuterJoin = range(3)\n    OptionNames = (\"Append columns from Extra data\",\n                   \"Find matching pairs of rows\",\n                   \"Concatenate tables\")\n    OptionDescriptions = (\n        \"The first table may contain, for instance, city names,\\n\"\n        \"and the second would be a list of cities and their coordinates.\\n\"\n        \"Columns with coordinates would then be appended to the output.\",\n\n        \"Input tables contain different features describing the same data \"\n        \"instances.\\n\"\n        \"Output contains matched instances. Rows without matches are removed.\",\n\n        \"Input tables contain different features describing the same data \"\n        \"instances.\\n\"\n        \"Output contains all instances. Data from merged instances is \"\n        \"merged into single rows.\"\n    )\n\n    UserAdviceMessages = [\n        widget.Message(\n            \"Confused about merging options?\\nSee the tooltips!\",\n            \"merging_types\")]\n\n    settingsHandler = MergeDataContextHandler()\n    attr_pairs = ContextSetting(None, schema_only=True)\n    merging = Setting(LeftJoin)\n    auto_apply = Setting(True)\n    settings_version = 2\n\n    want_main_area = False\n    resizing_enabled = False\n\n    class Warning(widget.OWWidget.Warning):\n        renamed_vars = Msg(\"Some variables have been renamed \"\n                           \"to avoid duplicates.\\n{}\")\n\n    class Error(widget.OWWidget.Error):\n        matching_numeric_with_nonnum = Msg(\n            \"Numeric and non-numeric columns ({} and {}) cannot be matched.\")\n        matching_index_with_sth = Msg(\"Row index cannot be matched with {}.\")\n        matching_id_with_sth = Msg(\"Instance cannot be matched with {}.\")\n        nonunique_left = Msg(\n            \"Some combinations of values on the left appear in multiple rows.\\n\"\n            \"For this type of merging, every possible combination of values \"\n            \"on the left should appear at most once.\")\n        nonunique_right = Msg(\n            \"Some combinations of values on the right appear in multiple rows.\"\n            \"\\n\"\n            \"Every possible combination of values on the right should appear \"\n            \"at most once.\")\n\n    def __init__(self):\n        super().__init__()\n\n        self.data = None\n        self.extra_data = None\n\n        content = [\n            INDEX, INSTANCEID,\n            DomainModel.ATTRIBUTES, DomainModel.CLASSES, DomainModel.METAS]\n        self.model = DomainModelWithTooltips(content)\n        self.extra_model = DomainModelWithTooltips(content)\n\n        self.info.set_input_summary(self.info.NoInput)\n        self.info.set_output_summary(self.info.NoOutput)\n\n        grp = gui.radioButtons(\n            self.controlArea, self, \"merging\", box=\"Merging\",\n            btnLabels=self.OptionNames, tooltips=self.OptionDescriptions,\n            callback=self.change_merging)\n\n        self.attr_boxes = ConditionBox(\n            self, self.model, self.extra_model, \"\", \"matches\")\n        self.attr_boxes.add_row()\n        box = gui.vBox(self.controlArea, box=\"Row matching\")\n        box.layout().addWidget(self.attr_boxes)\n\n        gui.auto_apply(self.buttonsArea, self)\n        # connect after wrapping self.commit with gui.auto_commit!\n        self.attr_boxes.vars_changed.connect(self.commit)\n        self.attr_boxes.vars_changed.connect(self.store_combo_state)\n        self.settingsAboutToBePacked.connect(self.store_combo_state)\n\n    def change_merging(self):\n        self.commit()\n\n    @Inputs.data\n    @check_sql_input\n    def set_data(self, data):\n        self.data = data\n        self.model.set_domain(data.domain if data else None)\n\n    @Inputs.extra_data\n    @check_sql_input\n    def set_extra_data(self, data):\n        self.extra_data = data\n        self.extra_model.set_domain(data.domain if data else None)\n\n    def store_combo_state(self):\n        self.attr_pairs = self.attr_boxes.current_state()\n\n    def handleNewSignals(self):\n        self.closeContext()\n        self.attr_pairs = [self._find_best_match()]\n        self.openContext(self.data and self.data.domain,\n                         self.extra_data and self.extra_data.domain)\n        self.attr_boxes.set_state(self.attr_pairs)\n\n        summary, details, kwargs = self.info.NoInput, \"\", {}\n        if self.data or self.extra_data:\n            n_data = len(self.data) if self.data else 0\n            n_extra_data = len(self.extra_data) if self.extra_data else 0\n            summary = f\"{self.info.format_number(n_data)}, \" \\\n                      f\"{self.info.format_number(n_extra_data)}\"\n            kwargs = {\"format\": Qt.RichText}\n            details = format_multiple_summaries([\n                (\"Data\", self.data),\n                (\"Extra data\", self.extra_data)\n            ])\n        self.info.set_input_summary(summary, details, **kwargs)\n\n        self.unconditional_commit()\n\n    def _find_best_match(self):\n        def get_unique_str_metas_names(model_):\n            return [m for m in model_ if isinstance(m, StringVariable)]\n\n        attr, extra_attr, n_max_intersect = INDEX, INDEX, 0\n        str_metas = get_unique_str_metas_names(self.model)\n        extra_str_metas = get_unique_str_metas_names(self.extra_model)\n        for m_a, m_b in product(str_metas, extra_str_metas):\n            col = self.data[:, m_a].metas\n            extra_col = self.extra_data[:, m_b].metas\n            if col.size and extra_col.size \\\n                    and isinstance(col[0][0], str) \\\n                    and isinstance(extra_col[0][0], str):\n                n_inter = len(np.intersect1d(col, extra_col))\n                if n_inter > n_max_intersect:\n                    n_max_intersect, attr, extra_attr = n_inter, m_a, m_b\n        return attr, extra_attr\n\n    def commit(self):\n        self.clear_messages()\n        merged = self.merge() if self.data and self.extra_data else None\n        self.Outputs.data.send(merged)\n        details = format_summary_details(merged) if merged else \"\"\n        summary = len(merged) if merged else self.info.NoOutput\n        self.info.set_output_summary(summary, details)\n\n    def send_report(self):\n        # pylint: disable=invalid-sequence-index\n        self.report_items((\n            (\"Merging\", self.OptionNames[self.merging]),\n            (\"Match\",\n             \", \".join(\n                 f\"{self._get_col_name(left)} with {self._get_col_name(right)}\"\n                 for left, right in self.attr_boxes.current_state()))))\n\n    def merge(self):\n        # pylint: disable=invalid-sequence-index\n        pairs = self.attr_boxes.current_state()\n        if not self._check_pair_types(pairs):\n            return None\n        left_vars, right_vars = zip(*pairs)\n        left_mask = np.full(len(self.data), True)\n        left = np.vstack(tuple(self._values(self.data, var, left_mask)\n                               for var in left_vars)).T\n        right_mask = np.full(len(self.extra_data), True)\n        right = np.vstack(tuple(self._values(self.extra_data, var, right_mask)\n                                for var in right_vars)).T\n        if not self._check_uniqueness(left, left_mask, right, right_mask):\n            return None\n        method = self._merge_methods[self.merging]\n        lefti, righti, rightu = method(self, left, left_mask, right, right_mask)\n        reduced_extra_data = \\\n            self._compute_reduced_extra_data(right_vars, lefti, righti, rightu)\n        return self._join_table_by_indices(\n            reduced_extra_data, lefti, righti, rightu)\n\n    def _check_pair_types(self, pairs):\n        for left, right in pairs:\n            if isinstance(left, ContinuousVariable) \\\n                    != isinstance(right, ContinuousVariable):\n                self.Error.matching_numeric_with_nonnum(left, right)\n                return False\n            if INDEX in (left, right) and left != right:\n                self.Error.matching_index_with_sth(\n                    self._get_col_name(({left, right} - {INDEX}).pop()))\n                return False\n            if INSTANCEID in (left, right) and left != right:\n                self.Error.matching_id_with_sth(\n                    self._get_col_name(({left, right} - {INSTANCEID}).pop()))\n                return False\n        return True\n\n    @staticmethod\n    def _get_col_name(obj):\n        return f\"'{obj.name}'\" if isinstance(obj, Variable) else obj.lower()\n\n    def _check_uniqueness(self, left, left_mask, right, right_mask):\n        ok = True\n        masked_right = right[right_mask]\n        if len(set(map(tuple, masked_right))) != len(masked_right):\n            self.Error.nonunique_right()\n            ok = False\n        if self.merging != self.LeftJoin:\n            masked_left = left[left_mask]\n            if len(set(map(tuple, masked_left))) != len(masked_left):\n                self.Error.nonunique_left()\n                ok = False\n        return ok\n\n    def _compute_reduced_extra_data(self,\n                                    right_match_vars, lefti, righti, rightu):\n        \"\"\"Prepare a table with extra columns that will appear in the merged\n        table\"\"\"\n        domain = self.data.domain\n        extra_domain = self.extra_data.domain\n\n        def var_needed(var):\n            if rightu is not None and rightu.size:\n                return True\n            if var in right_match_vars and self.merging != self.OuterJoin:\n                return False\n            if var not in domain:\n                return True\n            both_defined = (lefti != -1) * (righti != -1)\n            left_col = \\\n                self.data.get_column_view(var)[0][lefti[both_defined]]\n            right_col = \\\n                self.extra_data.get_column_view(var)[0][righti[both_defined]]\n            if var.is_primitive():\n                left_col = left_col.astype(float)\n                right_col = right_col.astype(float)\n                mask_left = np.isfinite(left_col)\n                mask_right = np.isfinite(right_col)\n                return not (\n                    np.all(mask_left == mask_right)\n                    and np.all(left_col[mask_left] == right_col[mask_right]))\n            else:\n                return not np.all(left_col == right_col)\n\n        extra_vars = [\n            var for var in chain(extra_domain.variables, extra_domain.metas)\n            if var_needed(var)]\n        return self.extra_data[:, extra_vars]\n\n    @staticmethod\n    def _values(data, var, mask):\n        \"\"\"Return an iterotor over keys for rows of the table.\"\"\"\n        if var == INDEX:\n            return np.arange(len(data))\n        if var == INSTANCEID:\n            return np.fromiter(\n                (inst.id for inst in data), count=len(data), dtype=np.int)\n        col = data.get_column_view(var)[0]\n        if var.is_primitive():\n            col = col.astype(float, copy=False)\n            nans = np.isnan(col)\n            mask *= ~nans\n            if var.is_discrete:\n                col = col.astype(int)\n                col[nans] = len(var.values)\n                col = np.array(var.values + (np.nan, ))[col]\n        else:\n            col = col.copy()\n            defined = col.astype(bool)\n            mask *= defined\n            col[~mask] = np.nan\n        return col\n\n    def _left_join_indices(self, left, left_mask, right, right_mask):\n        \"\"\"Compute a two-row array of indices:\n        - the first row contains indices for the primary table,\n        - the second row contains the matching rows in the extra table or -1\"\"\"\n        data = self.data\n        # Don't match nans. This is needed since numpy may change nan to string\n        # nan, so nan's will match each other\n        indices = np.arange(len(right))\n        indices[~right_mask] = -1\n        if right.shape[1] == 1:\n            # The more common case can be handled faster\n            right_map = dict(zip(right.flatten(), indices))\n            righti = (right_map.get(val, -1) for val in left.flatten())\n        else:\n            right_map = dict(zip(map(tuple, right), indices))\n            righti = (right_map.get(tuple(val), -1) for val in left)\n        righti = np.fromiter(righti, dtype=np.int64, count=len(data))\n        lefti = np.arange(len(data), dtype=np.int64)\n        righti[lefti[~left_mask]] = -1\n        return lefti, righti, None\n\n    def _inner_join_indices(self, left, left_mask, right, right_mask):\n        \"\"\"Use _augment_indices to compute the array of indices,\n        then remove those with no match in the second table\"\"\"\n        lefti, righti, _ = \\\n            self._left_join_indices(left, left_mask, right, right_mask)\n        mask = righti != [-1]\n        return lefti[mask], righti[mask], None\n\n    def _outer_join_indices(self, left, left_mask, right, right_mask):\n        \"\"\"Use _augment_indices to compute the array of indices,\n        then add rows in the second table without a match in the first\"\"\"\n        lefti, righti, _ = \\\n            self._left_join_indices(left, left_mask, right, right_mask)\n        unused = np.full(len(right), True)\n        unused[righti] = False\n        if len(right) - 1 not in righti:\n            # righti can include -1, which sets the last element as used\n            unused[-1] = True\n        return lefti, righti, np.nonzero(unused)[0]\n\n    _merge_methods = [\n        _left_join_indices, _inner_join_indices, _outer_join_indices]\n\n    def _join_table_by_indices(self, reduced_extra, lefti, righti, rightu):\n        \"\"\"Join (horizontally) self.data and reduced_extra, taking the pairs\n        of rows given in indices\"\"\"\n        if not lefti.size:\n            return None\n        lt_dom = self.data.domain\n        xt_dom = reduced_extra.domain\n        domain = self._domain_rename_duplicates(\n            lt_dom.attributes + xt_dom.attributes,\n            lt_dom.class_vars + xt_dom.class_vars,\n            lt_dom.metas + xt_dom.metas)\n        X = self._join_array_by_indices(\n            self.data.X, reduced_extra.X, lefti, righti)\n        Y = self._join_array_by_indices(\n            np.c_[self.data.Y], np.c_[reduced_extra.Y], lefti, righti)\n        string_cols = [i for i, var in enumerate(domain.metas) if var.is_string]\n        metas = self._join_array_by_indices(\n            self.data.metas, reduced_extra.metas, lefti, righti, string_cols)\n        if rightu is not None:\n            # This domain is used for transforming the extra rows for outer join\n            # It must use the original - not renamed - variables from right, so\n            # values are copied,\n            # but new domain for the left, so renamed values are *not* copied\n            right_domain = Orange.data.Domain(\n                domain.attributes[:len(lt_dom.attributes)] + xt_dom.attributes,\n                domain.class_vars[:len(lt_dom.class_vars)] + xt_dom.class_vars,\n                domain.metas[:len(lt_dom.metas)] + xt_dom.metas)\n            extras = self.extra_data[rightu].transform(right_domain)\n            X = np.vstack((X, extras.X))\n            extras_Y = extras.Y\n            if extras_Y.ndim == 1:\n                extras_Y = extras_Y.reshape(-1, 1)\n            Y = np.vstack((Y, extras_Y))\n            metas = np.vstack((metas, extras.metas))\n        table = Orange.data.Table.from_numpy(domain, X, Y, metas)\n        table.name = getattr(self.data, 'name', '')\n        table.attributes = getattr(self.data, 'attributes', {})\n        if rightu is not None:\n            table.ids = np.hstack(\n                (self.data.ids, self.extra_data.ids[rightu]))\n        else:\n            table.ids = self.data.ids[lefti]\n\n        return table\n\n    def _domain_rename_duplicates(self, attributes, class_vars, metas):\n        \"\"\"Check for duplicate variable names in domain. If any, rename\n        the variables, by replacing them with new ones (names are\n        appended a number). \"\"\"\n        attrs, cvars, mets = [], [], []\n        n_attrs, n_cvars, n_metas = len(attributes), len(class_vars), len(metas)\n        lists = [attrs] * n_attrs + [cvars] * n_cvars + [mets] * n_metas\n\n        all_vars = attributes + class_vars + metas\n        proposed_names = [m.name for m in all_vars]\n        unique_names = get_unique_names_duplicates(proposed_names)\n        duplicates = set()\n        for p_name, u_name, var, c in zip(proposed_names, unique_names,\n                                          all_vars, lists):\n            if p_name != u_name:\n                duplicates.add(p_name)\n                var = var.copy(name=u_name)\n            c.append(var)\n        if duplicates:\n            self.Warning.renamed_vars(\", \".join(duplicates))\n        return Orange.data.Domain(attrs, cvars, mets)\n\n    @staticmethod\n    def _join_array_by_indices(left, right, lefti, righti, string_cols=None):\n        \"\"\"Join (horizontally) two arrays, taking pairs of rows given in indices\n        \"\"\"\n        def prepare(arr, inds, str_cols):\n            try:\n                newarr = arr[inds]\n            except IndexError:\n                newarr = np.full_like(arr, np.nan)\n            else:\n                empty = np.full(arr.shape[1], np.nan)\n                if str_cols:\n                    assert arr.dtype == object\n                    empty = empty.astype(object)\n                    empty[str_cols] = ''\n                newarr[inds == -1] = empty\n            return newarr\n\n        left_width = left.shape[1]\n        str_left = [i for i in string_cols or () if i < left_width]\n        str_right = [i - left_width for i in string_cols or () if i >= left_width]\n        res = hstack((prepare(left, lefti, str_left),\n                      prepare(right, righti, str_right)))\n        return res\n\n    @staticmethod\n    def migrate_settings(settings, version=None):\n        def mig_value(x):\n            if x == \"Position (index)\":\n                return INDEX\n            if x == \"Source position (index)\":\n                return INSTANCEID\n            return x\n\n        if not version:\n            operations = (\"augment\", \"merge\", \"combine\")\n            oper = operations[settings[\"merging\"]]\n            settings[\"attr_pairs\"] = (\n                True, True,\n                [(mig_value(settings[f\"attr_{oper}_data\"]),\n                  mig_value(settings[f\"attr_{oper}_extra\"]))])\n            for oper in operations:\n                del settings[f\"attr_{oper}_data\"]\n                del settings[f\"attr_{oper}_extra\"]\n\n        if not version or version < 2 and \"attr_pairs\" in settings:\n            data_exists, extra_exists, attr_pairs = settings.pop(\"attr_pairs\")\n            if not (data_exists and extra_exists):\n                settings[\"context_settings\"] = []\n                return\n\n            mapper = {0: (INDEX, 100), 1: (INSTANCEID, 100)}\n            context = ContextHandler().new_context()\n            context.values[\"attr_pairs\"] = [tuple(mapper.get(var, (var, 100))\n                                                  for var in pair)\n                                            for pair in attr_pairs]\n            context.variables1 = {}\n            context.variables2 = {}\n            settings[\"context_settings\"] = [context]\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWMergeData).run(\n        set_data=Orange.data.Table(\"tests/data-gender-region\"),\n        set_extra_data=Orange.data.Table(\"tests/data-regions\"))\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/data/owmergedata.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "17ca69559",
                "new_commit_hash": "e48f37d8c",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/data/owmergedata.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/data/owmergedata.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 10,
                            "length": 7
                        },
                        "new_range": {
                            "start": 10,
                            "length": 7
                        },
                        "section": "from AnyQt.QtWidgets import ("
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "from orangewidget.utils.combobox import ComboBoxSearch",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "import Orange",
                                "type": "context"
                            },
                            {
                                "content": "from Orange.data import StringVariable, ContinuousVariable, Variable",
                                "type": "deleted"
                            },
                            {
                                "content": "from Orange.data import StringVariable, ContinuousVariable, Variable, Domain",
                                "type": "added"
                            },
                            {
                                "content": "from Orange.data.util import hstack, get_unique_names_duplicates",
                                "type": "context"
                            },
                            {
                                "content": "from Orange.widgets import widget, gui",
                                "type": "context"
                            },
                            {
                                "content": "from Orange.widgets.settings import Setting, ContextHandler, ContextSetting",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [],
                    "added_entities": [
                        {
                            "file_name": "Orange/widgets/data/owmergedata.py",
                            "type": "statement",
                            "name": "",
                            "content": "from Orange.data import StringVariable, ContinuousVariable, Variable, Domain",
                            "ast_type_str": "ImportFrom",
                            "start_lineno": 13,
                            "end_lineno": 13,
                            "parent": null
                        }
                    ],
                    "deleted_entities": [
                        {
                            "file_name": "Orange/widgets/data/owmergedata.py",
                            "type": "statement",
                            "name": "",
                            "content": "from Orange.data import StringVariable, ContinuousVariable, Variable",
                            "ast_type_str": "ImportFrom",
                            "start_lineno": 13,
                            "end_lineno": 13,
                            "parent": null
                        }
                    ]
                },
                {
                    "descriptor": {
                        "old_range": {
                            "start": 207,
                            "length": 6
                        },
                        "new_range": {
                            "start": 207,
                            "length": 8
                        },
                        "section": "class MergeDataContextHandler(ContextHandler):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "    def _encode_domain(self, domain):",
                                "type": "context"
                            },
                            {
                                "content": "        if domain is None:",
                                "type": "context"
                            },
                            {
                                "content": "            return {}",
                                "type": "context"
                            },
                            {
                                "content": "        if not isinstance(domain, Domain):",
                                "type": "added"
                            },
                            {
                                "content": "            domain = domain.domain",
                                "type": "added"
                            },
                            {
                                "content": "        all_vars = chain(domain.variables, domain.metas)",
                                "type": "context"
                            },
                            {
                                "content": "        return dict(self.encode_variables(all_vars))",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/data/owmergedata.py",
                            "type": "class",
                            "name": "MergeDataContextHandler",
                            "content": "class MergeDataContextHandler(ContextHandler):\n    # `widget` is used as an argument in most methods\n    # pylint: disable=redefined-outer-name\n    # context handlers override methods using different signatures\n    # pylint: disable=arguments-differ\n\n    def new_context(self, variables1, variables2):\n        context = super().new_context()\n        context.variables1 = variables1\n        context.variables2 = variables2\n        return context\n\n    def open_context(self, widget, domain1, domain2):\n        if domain1 is not None and domain2 is not None:\n            super().open_context(widget,\n                                 self._encode_domain(domain1),\n                                 self._encode_domain(domain2))\n\n    @staticmethod\n    def encode_variables(variables):\n        return [(v.name, 100 + vartype(v))\n                if isinstance(v, Variable) else (v, 100)\n                for v in variables]\n\n    @staticmethod\n    def decode_pair(widget, pair):\n        left_domain = widget.data and widget.data.domain\n        right_domain = widget.extra_data and widget.extra_data.domain\n        return tuple(var[0] if var[0] in (INDEX, INSTANCEID) else domain[var[0]]\n                     for domain, var in zip((left_domain, right_domain), pair))\n\n    def _encode_domain(self, domain):\n        if domain is None:\n            return {}\n        if not isinstance(domain, Domain):\n            domain = domain.domain\n        all_vars = chain(domain.variables, domain.metas)\n        return dict(self.encode_variables(all_vars))\n\n    def settings_from_widget(self, widget, *_args):\n        context = widget.current_context\n        if context is None:\n            return\n        context.values[\"attr_pairs\"] = [self.encode_variables(row)\n                                        for row in widget.attr_pairs]\n\n    def settings_to_widget(self, widget, *_args):\n        context = widget.current_context\n        if context is None:\n            return\n        pairs = context.values.get(\"attr_pairs\")\n        if pairs:\n            # attr_pairs is schema only setting which means it is not always\n            # present. When not present leave widgets default.\n            widget.attr_pairs = [\n                self.decode_pair(widget, pair) for pair in pairs\n            ]\n\n    def match(self, context, variables1, variables2):\n        def matches(part, variables):\n            return all(var[1] == 100 and var[0] in variables\n                       or variables.get(var[0], -1) == var[1] for var in part)\n\n        if (variables1, variables2) == (context.variables1, context.variables2):\n            return self.PERFECT_MATCH\n\n        if \"attr_pairs\" in context.values:\n            left, right = zip(*context.values[\"attr_pairs\"])\n            if matches(left, variables1) and matches(right, variables2):\n                return 0.5\n\n        return self.NO_MATCH",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 176,
                            "end_lineno": 247,
                            "parent": null
                        },
                        {
                            "file_name": "Orange/widgets/data/owmergedata.py",
                            "type": "method",
                            "name": "MergeDataContextHandler._encode_domain",
                            "content": "class MergeDataContextHandler(ContextHandler):\n    # `widget` is used as an argument in most methods\n    # pylint: disable=redefined-outer-name\n    # context handlers override methods using different signatures\n    # pylint: disable=arguments-differ\n\n    def new_context(self, variables1, variables2):\n        context = super().new_context()\n        context.variables1 = variables1\n        context.variables2 = variables2\n        return context\n\n    def open_context(self, widget, domain1, domain2):\n        if domain1 is not None and domain2 is not None:\n            super().open_context(widget,\n                                 self._encode_domain(domain1),\n                                 self._encode_domain(domain2))\n\n    @staticmethod\n    def encode_variables(variables):\n        return [(v.name, 100 + vartype(v))\n                if isinstance(v, Variable) else (v, 100)\n                for v in variables]\n\n    @staticmethod\n    def decode_pair(widget, pair):\n        left_domain = widget.data and widget.data.domain\n        right_domain = widget.extra_data and widget.extra_data.domain\n        return tuple(var[0] if var[0] in (INDEX, INSTANCEID) else domain[var[0]]\n                     for domain, var in zip((left_domain, right_domain), pair))\n\n    def _encode_domain(self, domain):\n        if domain is None:\n            return {}\n        if not isinstance(domain, Domain):\n            domain = domain.domain\n        all_vars = chain(domain.variables, domain.metas)\n        return dict(self.encode_variables(all_vars))\n\n    def settings_from_widget(self, widget, *_args):\n        context = widget.current_context\n        if context is None:\n            return\n        context.values[\"attr_pairs\"] = [self.encode_variables(row)\n                                        for row in widget.attr_pairs]\n\n    def settings_to_widget(self, widget, *_args):\n        context = widget.current_context\n        if context is None:\n            return\n        pairs = context.values.get(\"attr_pairs\")\n        if pairs:\n            # attr_pairs is schema only setting which means it is not always\n            # present. When not present leave widgets default.\n            widget.attr_pairs = [\n                self.decode_pair(widget, pair) for pair in pairs\n            ]\n\n    def match(self, context, variables1, variables2):\n        def matches(part, variables):\n            return all(var[1] == 100 and var[0] in variables\n                       or variables.get(var[0], -1) == var[1] for var in part)\n\n        if (variables1, variables2) == (context.variables1, context.variables2):\n            return self.PERFECT_MATCH\n\n        if \"attr_pairs\" in context.values:\n            left, right = zip(*context.values[\"attr_pairs\"])\n            if matches(left, variables1) and matches(right, variables2):\n                return 0.5\n\n        return self.NO_MATCH",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 207,
                            "end_lineno": 213,
                            "parent": {
                                "file_name": "Orange/widgets/data/owmergedata.py",
                                "type": "class",
                                "name": "MergeDataContextHandler",
                                "content": "class MergeDataContextHandler(ContextHandler):\n    # `widget` is used as an argument in most methods\n    # pylint: disable=redefined-outer-name\n    # context handlers override methods using different signatures\n    # pylint: disable=arguments-differ\n\n    def new_context(self, variables1, variables2):\n        context = super().new_context()\n        context.variables1 = variables1\n        context.variables2 = variables2\n        return context\n\n    def open_context(self, widget, domain1, domain2):\n        if domain1 is not None and domain2 is not None:\n            super().open_context(widget,\n                                 self._encode_domain(domain1),\n                                 self._encode_domain(domain2))\n\n    @staticmethod\n    def encode_variables(variables):\n        return [(v.name, 100 + vartype(v))\n                if isinstance(v, Variable) else (v, 100)\n                for v in variables]\n\n    @staticmethod\n    def decode_pair(widget, pair):\n        left_domain = widget.data and widget.data.domain\n        right_domain = widget.extra_data and widget.extra_data.domain\n        return tuple(var[0] if var[0] in (INDEX, INSTANCEID) else domain[var[0]]\n                     for domain, var in zip((left_domain, right_domain), pair))\n\n    def _encode_domain(self, domain):\n        if domain is None:\n            return {}\n        if not isinstance(domain, Domain):\n            domain = domain.domain\n        all_vars = chain(domain.variables, domain.metas)\n        return dict(self.encode_variables(all_vars))\n\n    def settings_from_widget(self, widget, *_args):\n        context = widget.current_context\n        if context is None:\n            return\n        context.values[\"attr_pairs\"] = [self.encode_variables(row)\n                                        for row in widget.attr_pairs]\n\n    def settings_to_widget(self, widget, *_args):\n        context = widget.current_context\n        if context is None:\n            return\n        pairs = context.values.get(\"attr_pairs\")\n        if pairs:\n            # attr_pairs is schema only setting which means it is not always\n            # present. When not present leave widgets default.\n            widget.attr_pairs = [\n                self.decode_pair(widget, pair) for pair in pairs\n            ]\n\n    def match(self, context, variables1, variables2):\n        def matches(part, variables):\n            return all(var[1] == 100 and var[0] in variables\n                       or variables.get(var[0], -1) == var[1] for var in part)\n\n        if (variables1, variables2) == (context.variables1, context.variables2):\n            return self.PERFECT_MATCH\n\n        if \"attr_pairs\" in context.values:\n            left, right = zip(*context.values[\"attr_pairs\"])\n            if matches(left, variables1) and matches(right, variables2):\n                return 0.5\n\n        return self.NO_MATCH",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 176,
                                "end_lineno": 247,
                                "parent": null
                            }
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        },
        {
            "old_file_content": "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring\n# There are never too many tests, so:\n# pylint: disable=too-many-lines,too-many-public-methods, protected-access\nfrom itertools import chain\nimport unittest\nfrom types import SimpleNamespace\nfrom unittest.mock import Mock\n\nimport numpy as np\nimport scipy.sparse as sp\n\nfrom AnyQt.QtCore import Qt\n\nfrom orangewidget.widget import StateInfo\n\nfrom Orange.data import Table, Domain, DiscreteVariable, StringVariable, \\\n    ContinuousVariable\nfrom Orange.widgets.data.owmergedata import OWMergeData, INSTANCEID, INDEX, \\\n    MergeDataContextHandler\nfrom Orange.widgets.tests.base import WidgetTest\nfrom Orange.widgets.utils.state_summary import format_multiple_summaries, \\\n    format_summary_details\nfrom Orange.tests import test_filename\n\n\nclass TestOWMergeData(WidgetTest):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        domainA = Domain([DiscreteVariable(\"dA1\", (\"a\", \"b\", \"c\", \"d\")),\n                          DiscreteVariable(\"dA2\", (\"aa\", \"bb\"))],\n                         DiscreteVariable(\"clsA\", (\"aaa\", \"bbb\", \"ccc\")),\n                         [DiscreteVariable(\"mA1\", (\"cc\", \"dd\")),\n                          StringVariable(\"mA2\")])\n        XA = np.array([[0, 0], [1, 1], [2, 0], [3, 1]])\n        yA = np.array([0, 1, 2, np.nan])\n        metasA = np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                           [0.0, \"m4\"]]).astype(object)\n\n        domainB = Domain([DiscreteVariable(\"dB1\", values=(\"a\", \"b\", \"c\")),\n                          DiscreteVariable(\"dB2\", values=(\"aa\", \"bb\"))],\n                         DiscreteVariable(\"clsB\", values=(\"bbb\", \"ccc\")),\n                         [DiscreteVariable(\"mB1\", (\"m4\", \"m5\"))])\n        XB = np.array([[0, 0], [1, 1], [2, np.nan]])\n        yB = np.array([np.nan, 1, 0])\n        metasB = np.array([[np.nan], [1], [0]]).astype(object)\n        cls.dataA = Table(domainA, XA, yA, metasA)\n        cls.dataA.name = 'dataA'\n        cls.dataA.attributes = 'dataA attributes'\n        cls.dataB = Table(domainB, XB, yB, metasB)\n        cls.dataB.name = 'dataB'\n        cls.dataB.attributes = 'dataB attributes'\n\n    def setUp(self):\n        self.widget = self.create_widget(OWMergeData)\n\n    def test_input_remove(self):\n        \"\"\"Check widget after inputs have been removed\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.send_signal(self.widget.Inputs.extra_data, None)\n\n    def test_combobox_items(self):\n        \"\"\"Check if combo box content is properly set\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        row = self.widget.attr_boxes.rows[0]\n        data_combo, extra_combo = row.left_combo, row.right_combo\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n        data_items = extra_items = list(\n            chain([INDEX, INSTANCEID], domainA.variables, domainA.metas))\n        self.assertListEqual(data_combo.model()[:], data_items)\n        self.assertListEqual(extra_combo.model()[:], extra_items)\n\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        data_items = list(\n            chain([INDEX, INSTANCEID], domainA.variables, domainA.metas))\n        extra_items = list(\n            chain([INDEX, INSTANCEID], domainB.variables, domainB.metas))\n        self.assertListEqual(data_combo.model()[:], data_items)\n        self.assertListEqual(extra_combo.model()[:], extra_items)\n\n        self.send_signal(self.widget.Inputs.data, self.dataB)\n        data_items = extra_items = list(\n            chain([INDEX, INSTANCEID], domainB.variables, domainB.metas))\n        self.assertListEqual(data_combo.model()[:], data_items)\n        self.assertListEqual(extra_combo.model()[:], extra_items)\n\n    def test_combo_box_sync(self):\n        row = self.widget.attr_boxes.rows[0]\n        data_combo, extra_combo = row.left_combo, row.right_combo\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        extra_combo.setCurrentIndex(3)\n        data_combo.setCurrentIndex(0)\n        data_combo.activated.emit(0)\n        self.assertEqual(data_combo.currentIndex(), 0)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        data_combo.setCurrentIndex(1)\n        data_combo.activated.emit(1)\n        self.assertEqual(data_combo.currentIndex(), 1)\n        self.assertEqual(extra_combo.currentIndex(), 1)\n\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        self.assertEqual(data_combo.currentIndex(), 0)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        extra_combo.setCurrentIndex(1)\n        extra_combo.activated.emit(1)\n        self.assertEqual(data_combo.currentIndex(), 1)\n        self.assertEqual(extra_combo.currentIndex(), 1)\n\n    def test_attr_combo_tooltips(self):\n        row = self.widget.attr_boxes.rows[0]\n        model = row.left_combo.model()\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        tip = model.data(model.index(2, 0), Qt.ToolTipRole)\n        # Test the test; if general tooltips ever change and the following\n        # assert fails, the rest of this test has to be modified accordingly\n        self.assertTrue(tip.startswith(\"<b>\"))\n\n        # Just test that tooltip is a string (implicitly) and that it's not\n        # a generic DomainModel tooltip\n        tip = model.data(model.index(0, 0), Qt.ToolTipRole)\n        self.assertFalse(tip.startswith(\"<b>\"))\n\n        tip = model.data(model.index(1, 0), Qt.ToolTipRole)\n        self.assertFalse(tip.startswith(\"<b>\"))\n\n    def test_match_attr_name(self):\n        widget = self.widget\n        row = widget.attr_boxes.rows[0]\n        data_combo, extra_combo = row.left_combo, row.right_combo\n\n        domainA = Domain([DiscreteVariable(\"dA1\", (\"a\", \"b\", \"c\", \"d\")),\n                          DiscreteVariable(\"dA2\", (\"aa\", \"bb\")),\n                          DiscreteVariable(\"dA3\", (\"aa\", \"bb\"))],\n                         DiscreteVariable(\"cls\", (\"aaa\", \"bbb\", \"ccc\")),\n                         [DiscreteVariable(\"mA1\", (\"cc\", \"dd\")),\n                          StringVariable(\"mA2\")])\n        XA = np.array([[0, 0, 0], [1, 1, 0], [2, 0, 0], [3, 1, 0]])\n        yA = np.array([0, 1, 2, np.nan])\n        metasA = np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                           [0.0, \"m4\"]]).astype(object)\n\n        domainB = Domain([DiscreteVariable(\"dB1\", values=(\"a\", \"b\", \"c\")),\n                          ContinuousVariable(\"dA2\")],\n                         None,\n                         [StringVariable(\"cls\"),\n                          DiscreteVariable(\"dA1\", (\"m4\", \"m5\"))])\n        XB = np.array([[0, 0], [1, 1], [2, np.nan]])\n        yB = np.empty((3, 0))\n        metasB = np.array([[np.nan, np.nan], [1, 1], [0, 0]]).astype(object)\n        dataA = Table(domainA, XA, yA, metasA)\n        dataA.name = 'dataA'\n        dataA.attributes = 'dataA attributes'\n        dataB = Table(domainB, XB, yB, metasB)\n        dataB.name = 'dataB'\n        dataB.attributes = 'dataB attributes'\n\n        self.send_signal(widget.Inputs.data, dataA)\n        self.send_signal(widget.Inputs.extra_data, dataB)\n\n        # match variable if available and the other combo is Row Index\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n        self.assertEqual(extra_combo.currentIndex(), 5)\n\n        # match variable if available and the other combo is ID\n        extra_combo.setCurrentIndex(1)\n        extra_combo.activated.emit(1)\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n        self.assertEqual(extra_combo.currentIndex(), 5)\n\n        # don't match variable if other combo is set\n        extra_combo.setCurrentIndex(4)\n        extra_combo.activated.emit(4)\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n        self.assertEqual(extra_combo.currentIndex(), 4)\n\n        # don't match if nothing to match to\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(4)\n        data_combo.activated.emit(4)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        # don't match numeric with non-numeric\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(3)\n        data_combo.activated.emit(3)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        # allow matching string with discrete\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(5)\n        data_combo.activated.emit(5)\n        self.assertEqual(extra_combo.currentIndex(), 4)\n\n    def test_add_row_button(self):\n        boxes = self.widget.attr_boxes\n        boxes.set_state([(INSTANCEID, INSTANCEID), (INSTANCEID, INSTANCEID)])\n        boxes.rows[-1].add_button.clicked.emit()\n        self.assertEqual(len(boxes.rows), 3)\n        self.assertEqual(boxes.layout().count(), 3)\n\n    def test_remove_row(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        var0, var1 = self.dataA.domain.attributes[:2]\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n        for i, row in enumerate(boxes.rows):\n            self.assertTrue(row.remove_button.isEnabled())\n            self.assertEqual(row.remove_button.text(), \"×\")\n            self.assertEqual(row.add_button.isEnabled(), i == 2)\n            self.assertEqual(row.add_button.text(), [\"\", \"+\"][i == 2])\n\n        boxes.rows[1].remove_button.clicked.emit()\n        self.assertEqual(boxes.current_state(), [(INDEX, INDEX), (var0, var1)])\n        for i, row in enumerate(boxes.rows):\n            self.assertTrue(row.remove_button.isEnabled())\n            self.assertEqual(row.remove_button.text(), \"×\")\n            self.assertEqual(row.add_button.isEnabled(), i == 1)\n            self.assertEqual(row.add_button.text(), [\"\", \"+\"][i])\n\n        boxes.rows[1].remove_button.clicked.emit()\n        self.assertEqual(boxes.current_state(), [(INDEX, INDEX)])\n        row = boxes.rows[0]\n        self.assertFalse(row.remove_button.isEnabled())\n        self.assertEqual(row.remove_button.text(), \"\")\n        self.assertTrue(row.add_button.isEnabled())\n        self.assertEqual(row.add_button.text(), \"+\")\n\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n        boxes.rows[2].remove_button.clicked.emit()\n        self.assertEqual(\n            boxes.current_state(), [(INDEX, INDEX), (INSTANCEID, INSTANCEID)])\n        for i, row in enumerate(boxes.rows):\n            self.assertTrue(row.remove_button.isEnabled())\n            self.assertEqual(row.remove_button.text(), \"×\")\n            self.assertEqual(row.add_button.isEnabled(), i == 1)\n            self.assertEqual(row.add_button.text(), [\"\", \"+\"][i == 1])\n\n    def test_dont_remove_single_row(self):\n        widget = self.widget\n        rows = widget.attr_boxes.rows\n        self.assertEqual(len(rows), 1)\n        rows[0].remove_button.clicked.emit()\n        self.assertEqual(len(rows), 1)\n\n    def test_retrieve_settings(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        var0, var1 = self.dataA.domain.attributes[:2]\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n\n        settings = widget.settingsHandler.pack_data(widget)\n\n        widget2 = self.create_widget(OWMergeData, stored_settings=settings)\n        widget2.attr_boxes.set_state([(INDEX, INDEX)])\n        self.send_signals(\n            [(widget2.Inputs.data, self.dataA),\n             (widget2.Inputs.extra_data, self.dataA)],\n            widget=widget2)\n        self.assertEqual(\n            widget2.attr_boxes.current_state(),\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n\n    def test_match_settings(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        domainA = self.dataA.domain\n        domainB = self.dataB.domain\n\n        self.send_signal(widget.Inputs.data, self.dataA)\n        self.send_signal(widget.Inputs.extra_data, self.dataA)\n        attr_pairs = [(INDEX, INDEX), (INSTANCEID, INSTANCEID),\n                      (domainA[0], domainA[1]), (domainA[1], domainA[0])]\n        boxes.set_state(attr_pairs)\n        boxes.emit_list()\n        self.assertEqual(widget.attr_pairs, attr_pairs)\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertEqual(widget.attr_pairs, [(INDEX, INDEX)])\n\n        self.send_signal(widget.Inputs.data, self.dataA)\n        self.assertEqual(widget.attr_pairs, attr_pairs)\n\n        self.send_signal(widget.Inputs.extra_data, self.dataB)\n        attr_pairs2 = [(domainA[0], domainB[0]), (domainA[1], domainB[1])]\n        boxes.set_state(attr_pairs2)\n        boxes.emit_list()\n        self.assertEqual(widget.attr_pairs, attr_pairs2)\n\n        self.send_signal(widget.Inputs.extra_data, self.dataA)\n        self.assertEqual(widget.attr_pairs, attr_pairs)\n\n    def test_migrate_settings(self):\n        def create_and_send(settings):\n            widget = self.create_widget(OWMergeData, stored_settings=settings)\n            for signal in (widget.Inputs.data, widget.Inputs.extra_data):\n                self.send_signal(signal, self.dataA)\n            return widget\n\n        domainA = self.dataA.domain\n        attr1, attr2, attr3 = domainA.variables\n        attr4, attr5 = domainA.metas\n\n        # Migration from version == None\n        orig_settings = dict(\n            attr_augment_data=attr1.name,\n            attr_augment_extra=attr2.name,\n            attr_merge_data=attr3.name,\n            attr_merge_extra=attr4.name,\n            attr_combine_data=attr5.name,\n            attr_combine_extra='Position (index)')\n\n        widget = create_and_send(dict(merging=0, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr1, attr2)]))\n\n        widget = create_and_send(dict(merging=1, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr3, attr4)]))\n\n        widget = create_and_send(dict(merging=2, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr5, attr5)]))\n\n        orig_settings[\"attr_combine_extra\"] = \"Source position (index)\"\n        widget = create_and_send(dict(merging=2, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr5, attr5)]))\n\n        # Migration from version 1\n        orig_settings = {\"attr_pairs\": (True, True, [[attr1.name, attr2.name],\n                                                     [attr3.name, attr4.name]]),\n                         \"__version__\": 1}\n        widget = create_and_send(orig_settings)\n        self.assertEqual(widget.attr_pairs, ([(attr1, attr2), (attr3, attr4)]))\n\n    def test_migrate_settings_attr_pairs_extra_none(self):\n        settings = {'attr_pairs': (True, False, [['sepal length', 0]])}\n        OWMergeData.migrate_settings(settings, 1)\n        self.assertListEqual(settings[\"context_settings\"], [])\n\n    def test_migrate_settings_attr_pairs_data_none(self):\n        settings = {'attr_pairs': (False, True, [[0, \"sepal length\"]])}\n        OWMergeData.migrate_settings(settings, 1)\n        self.assertListEqual(settings[\"context_settings\"], [])\n\n    def test_migrate_settings_attr_pairs_id_idx(self):\n        settings = {\"attr_pairs\": (True, True, [[0, 1]])}\n        OWMergeData.migrate_settings(settings, 1)\n        context = settings[\"context_settings\"][0]\n        self.assertListEqual(context.values[\"attr_pairs\"],\n                             [((INDEX, 100), (INSTANCEID, 100))])\n        self.assertDictEqual(context.variables1, {})\n        self.assertDictEqual(context.variables2, {})\n\n    def test_migrate_settings_attr_pairs_vars(self):\n        settings = {\"attr_pairs\": (True, True,\n                                   [[\"sepal length\", \"sepal width\"],\n                                    [\"petal length\", \"petal width\"]])}\n        OWMergeData.migrate_settings(settings, 1)\n        context = settings[\"context_settings\"][0]\n        self.assertListEqual(context.values[\"attr_pairs\"],\n                             [((\"sepal length\", 100), (\"sepal width\", 100)),\n                              ((\"petal length\", 100), (\"petal width\", 100))])\n        self.assertDictEqual(context.variables1, {})\n        self.assertDictEqual(context.variables2, {})\n\n    def test_report(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        var0, var1 = self.dataA.domain.attributes[:2]\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n        widget.send_report()\n        # Don't crash, that's it\n\n    def test_no_matches(self):\n        \"\"\"Check output is None when there are no matches in inner join\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        domA = self.dataA.domain\n        domB = self.dataB.domain\n\n        self.widget.attr_boxes.set_state([(domA[\"dA1\"], domB[\"dB2\"])])\n        self.widget.controls.merging.buttons[self.widget.LeftJoin].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.data))\n\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertIsNone(self.get_output(self.widget.Outputs.data))\n\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.data))\n\n    def test_output_merge_by_ids_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by\n        Source position (index)\"\"\"\n        domain = self.dataA.domain\n        result = Table(domain, np.array([[1, 1], [2, 0]]), np.array([1, 2]),\n                       np.array([[1.0, \"m2\"], [np.nan, \"m3\"]]).astype(object))\n        self.send_signal(self.widget.Inputs.data, self.dataA[:3, [0, \"clsA\", -1]])\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA[1:, [1, \"clsA\", -2]])\n        self.widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_ids_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        Source position (index)\"\"\"\n        domainA = self.dataA.domain\n        values = domainA.class_var.values\n        domain = Domain(domainA.attributes,\n                        (DiscreteVariable(\"clsA (1)\", values),\n                         DiscreteVariable(\"clsA (2)\", values)),\n                        domainA.metas)\n        result = Table(domain,\n                       np.array([[1, 1], [2, 0], [3, np.nan], [np.nan, 0]]),\n                       np.array([[1, 1], [2, 2], [np.nan, np.nan], [np.nan, 0]]),\n                       np.array([[1.0, \"m2\"], [np.nan, \"m3\"],\n                                 [0.0, \"\"], [np.nan, \"m1\"]]).astype(object))\n        self.widget.merging = 2\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        self.send_signal(self.widget.Inputs.data, self.dataA[1:, [0, \"clsA\", -1]])\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA[:3, [1, \"clsA\", -2]])\n        self.widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        self.widget.attr_boxes.emit_list()\n        out = self.get_output(self.widget.Outputs.data)\n        self.assertTablesEqual(out, result)\n        np.testing.assert_equal(\n            out.ids, np.hstack((self.dataA.ids[1:], self.dataA.ids[:1])))\n\n    def test_output_merge_by_ids_outer_single_class(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        Source position (index) when all extra rows are matched and there is\n        only a single class variable in the output\"\"\"\n        domainA = self.dataA.domain\n        values = domainA.class_var.values\n        domain = Domain(domainA.attributes,\n                        DiscreteVariable(\"clsA\", values),\n                        domainA.metas)\n        result = Table(domain,\n                       np.array([[0, 0], [1, 1], [2, 0], [3, np.nan]]),\n                       np.array([[0], [1], [2], [np.nan]]),\n                       np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                                 [0.0, \"\"]]).astype(object))\n        self.widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        self.widget.merging = 2\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        # When Y is a single column, Table.Y returns a vector, not a 2d array,\n        # which cause an exception in outer_join's vstack for Y if extra data\n        # has no unmatched rows.\n        # This test also checks this condition.\n        self.send_signal(self.widget.Inputs.data, self.dataA[:, [0, \"clsA\", -1]])\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA[:3, [1, -2]])\n        out = self.get_output(self.widget.Outputs.data)\n        self.assertTablesEqual(out, result)\n        np.testing.assert_equal(out.ids, self.dataA.ids)\n\n    def test_output_merge_by_index_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        Position (index)\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1],\n                             [2, 0, 2, np.nan], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]\n                            ]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_index_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by\n        Position (index)\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1], [2, 0, 2, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.controls.merging.buttons[1].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_index_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        Position (index)\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1],\n                             [2, 0, 2, np.nan], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.controls.merging.buttons[2].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_attribute_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        attribute\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes[1:],\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0], [1, 1, 1],\n                             [2, 0, np.nan], [3, 1, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[0], domainB[0])])\n        self.widget.commit()\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertTablesEqual(output, result)\n        self.assertEqual(output.name, self.dataA.name)\n        np.testing.assert_array_equal(output.ids, self.dataA.ids)\n        self.assertEqual(output.attributes, self.dataA.attributes)\n\n    def test_output_merge_by_attribute_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by attribute\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes[1:],\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0], [1, 1, 1], [2, 0, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[0], domainB[0])])\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_attribute_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        attribute\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1],\n                             [2, 0, 2, np.nan], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[0], domainB[0])])\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_attribute_outer_same_attr(self):\n        \"\"\"Values of columns from extra aata are copied to left part if they\n        match\"\"\"\n        name = StringVariable(\"name\")\n        domainA = Domain([ContinuousVariable(\"x\")], None, [name])\n        domainB = Domain([ContinuousVariable(\"y\")], None, [name])\n        xA = np.array([[0], [1], [2]])\n        mA = np.array([[\"a\"], [\"b\"], [\"c\"]])\n        xB = np.array([[4], [5], [6], [7]])\n        mB = np.array([[\"b\"], [\"d\"], [\"a\"], [\"c\"]])\n        dataA = Table(domainA, xA, None, mA)\n        dataB = Table(domainB, xB, None, mB)\n\n        self.send_signal(self.widget.Inputs.data, dataA)\n        self.send_signal(self.widget.Inputs.extra_data, dataB)\n        self.widget.attr_boxes.set_state([(name, name)])\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        out = self.get_output(self.widget.Outputs.data)\n        np.testing.assert_equal(\n            out.X,\n            np.array([[0, 6], [1, 4], [2, 7], [np.nan, 5]]))\n        self.assertEqual(\" \".join(out.metas.flatten()), \"a a b b c c  d\")\n\n    def test_output_merge_by_class_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        class variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars, domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, 2, np.nan],\n                             [2, 0, 1, 1], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([0, 1, 2, np.nan])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 0.0],\n                             [np.nan, \"m3\", 1.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[2], domainB[2])])\n        self.widget.commit()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_class_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by class\n        variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars, domainA.metas + domainB.metas)\n        result_X = np.array([[1, 1, 2, np.nan], [2, 0, 1, 1]])\n        result_Y = np.array([1, 2])\n        result_M = np.array([[1.0, \"m2\", 0.0], [np.nan, \"m3\", 1.0]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state(\n            [(domainA.class_vars[0], domainB.class_vars[0])])\n        self.widget.controls.merging.buttons[1].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_class_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        class variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, 2, np.nan],\n                             [2, 0, 1, 1], [3, 1, np.nan, np.nan],\n                             [np.nan, np.nan, 0, 0]])\n        result_Y = np.array([[0, np.nan], [1, 0], [2, 1], [np.nan, np.nan],\n                             [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 0.0],\n                             [np.nan, \"m3\", 1.0], [0.0, \"m4\", np.nan],\n                             [np.nan, \"\", np.nan]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state(\n            [(domainA.class_vars[0], domainB.class_vars[0])])\n        self.widget.controls.merging.buttons[2].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_meta_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        meta variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, np.nan, np.nan],\n                             [2, 0, np.nan, np.nan], [3, 1, 2, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, np.nan],\n                             [2, np.nan], [np.nan, 0]])\n        result_M = np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                             [0.0, \"m4\"]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[-2], domainB[-1])])\n        self.widget.commit()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_meta_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by meta\n        variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas)\n        result_X = np.array([[3, 1, 2, np.nan]])\n        result_Y = np.array([[np.nan, 0]])\n        result_M = np.array([[0.0, \"m4\"]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[-2], domainB[-1])])\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_meta_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        meta variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, np.nan, np.nan],\n                             [2, 0, np.nan, np.nan], [3, 1, 2, np.nan],\n                             [np.nan, np.nan, 0, 0], [np.nan, np.nan, 1, 1]])\n        result_Y = np.array([[0, np.nan], [1, np.nan], [2, np.nan],\n                             [np.nan, 0], [np.nan, np.nan], [np.nan, 1]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", np.nan],\n                             [np.nan, \"m3\", np.nan], [0.0, \"m4\", 0.0],\n                             [np.nan, \"\", np.nan], [np.nan, \"\", 1.0]]\n                           ).astype(object)\n\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[-2], domainB[-1])])\n        self.widget.controls.merging.buttons[2].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def assertTablesEqual(self, table1, table2):\n        self.assertEqual(table1.domain, table2.domain)\n        np.testing.assert_array_equal(table1.X, table2.X)\n        np.testing.assert_array_equal(table1.Y, table2.Y)\n        np.testing.assert_array_equal(table1.metas.astype(str),\n                                      table2.metas.astype(str))\n\n    def test_best_match(self):\n        \"\"\"Check default merging attributes setup\"\"\"\n        widget = self.widget\n        indices = list(range(101))\n        indices.pop(26)\n        zoo = Table(\"zoo\")[indices]\n        zoo_images = Table(test_filename(\"datasets/zoo-with-images.tab\"))\n        self.send_signal(widget.Inputs.data, zoo)\n        self.send_signal(widget.Inputs.extra_data, zoo_images)\n        for i in range(3):\n            self.assertEqual(widget.attr_boxes.current_state(),\n                             [(zoo.domain[\"name\"], zoo_images.domain[\"name\"])],\n                             f\"wrong attributes chosen for merge_type={i}\")\n\n    def test_sparse(self):\n        data = Table(\"iris\")[::25]\n        data_ed_dense = Table(\"titanic\")[::300]\n        data_ed_sparse = Table(\"titanic\")[::300].to_sparse()\n        self.send_signal(\"Data\", data)\n\n        self.send_signal(\"Extra Data\", data_ed_dense)\n        output_dense = self.get_output(\"Data\")\n        self.assertFalse(sp.issparse(output_dense.X))\n        self.assertFalse(output_dense.is_sparse())\n\n        self.send_signal(\"Extra Data\", data_ed_sparse)\n        output_sparse = self.get_output(\"Data\")\n        self.assertTrue(sp.issparse(output_sparse.X))\n        self.assertTrue(output_sparse.is_sparse())\n\n        output_sparse.X = output_sparse.X.toarray()\n        self.assertTablesEqual(output_dense, output_sparse)\n\n    def test_commit_on_new_data(self):\n        \"\"\"Check that disabling auto apply doesn't block on new data\"\"\"\n        self.widget.auto_apply = False\n        self.widget.merging = 2\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.data))\n\n    def test_multiple_attributes_left(self):\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n\n        X = np.array([[0, 1], [1, 1], [3, np.nan], [np.nan, 0]])\n        Y = np.array([1, 2, 0, 0])\n        metas = np.array([[0, \"a\"], [1, \"b\"], [0, \"c\"], [0, \"d\"]])\n        dataA = Table(domainA, X, Y, metas)\n        dataA.name = \"dataA\"\n\n        X = np.array(\n            [[0, 0], [1, 0], [0, 1], [1, 1], [2, 0], [3, np.nan]])\n        Y = np.array([0, 0, 1, 1, 0, 1])\n        metas = np.array([[0], [1], [0], [1], [1], [1]])\n        dataB = Table(domainB, X, Y, metas)\n        dataB.name = \"dataB\"\n\n        self.send_signal(self.widget.Inputs.data, dataA)\n        self.send_signal(self.widget.Inputs.extra_data, dataB)\n        self.widget.attr_boxes.set_state(\n            [(domainA[0], domainB[0]), (domainA[1], domainB[1])])\n        self.widget.commit()\n        output = self.get_output(self.widget.Outputs.data)\n\n        self.assertEqual(output.name, dataA.name)\n        np.testing.assert_array_equal(output.ids, dataA.ids)\n        self.assertEqual(output.attributes, dataA.attributes)\n        np.testing.assert_equal(output.X, dataA.X)\n\n    def test_nonunique(self):\n        widget = self.widget\n        x = ContinuousVariable(\"x\")\n        d = DiscreteVariable(\"d\", values=tuple(\"abc\"))\n        domain = Domain([x, d], [])\n        dataA = Table.from_numpy(\n            domain, np.array([[1.0, 0], [1, 1], [2, 1]]))\n        dataB = Table.from_numpy(\n            domain, np.array([[1.0, 0], [2, 1], [3, 1]]))\n        dataB.ids = dataA.ids\n        self.send_signal(widget.Inputs.data, dataA)\n        self.send_signal(widget.Inputs.extra_data, dataB)\n        widget.merging = widget.InnerJoin\n\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n\n        widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.attr_boxes.set_state([(INDEX, INDEX)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.attr_boxes.set_state([(x, x)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.LeftJoin\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.InnerJoin\n        widget.attr_boxes.set_state([(x, x), (d, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.attr_boxes.set_state([(d, d)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.nonunique_left.is_shown())\n        self.assertTrue(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.LeftJoin\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertTrue(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.InnerJoin\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.nonunique_left.is_shown())\n        self.assertTrue(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        self.send_signal(widget.Inputs.data, None)\n        self.send_signal(widget.Inputs.extra_data, None)\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n    def test_invalide_pairs(self):\n        widget = self.widget\n        x = ContinuousVariable(\"x\")\n        d = DiscreteVariable(\"d\", values=tuple(\"abc\"))\n        domain = Domain([x, d], [])\n        dataA = Table.from_numpy(\n            domain, np.array([[1.0, 0], [1, 1], [2, 1]]))\n        dataB = Table.from_numpy(\n            domain, np.array([[1.0, 0], [2, 1], [3, 1]]))\n        dataB.ids = dataA.ids\n        self.send_signal(widget.Inputs.data, dataA)\n        self.send_signal(widget.Inputs.extra_data, dataB)\n\n        widget.attr_boxes.set_state([(x, x), (d, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (INDEX, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertTrue(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (d, INDEX)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertTrue(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (INSTANCEID, d)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (d, INSTANCEID)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (INDEX, INSTANCEID)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.matching_id_with_sth.is_shown()\n                        or widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (x, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertTrue(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n    def test_duplicate_names(self):\n        domain = Domain([ContinuousVariable(\"C1\")],\n                        metas=[DiscreteVariable(\"Feature\", values=(\"A\", \"B\"))])\n        data = Table(domain, np.array([[1.], [0.]]),\n                     metas=np.array([[1.], [0.]]))\n        domain = Domain([ContinuousVariable(\"C1\")],\n                        metas=[StringVariable(\"Feature\")])\n        extra_data = Table(domain, np.array([[1.], [0.]]),\n                           metas=np.array([[\"A\"], [\"B\"]]))\n        self.send_signal(self.widget.Inputs.data, data)\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        self.assertTrue(self.widget.Warning.renamed_vars.is_shown())\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.metas],\n                             [\"Feature (1)\", \"Feature (2)\"])\n\n    def test_keep_non_duplicate_variables(self):\n        domain = Domain([ContinuousVariable(\"A\"), ContinuousVariable(\"B\")])\n        data = Table(domain, np.array([[0., 0], [0, 1]]))\n        extra_data = Table(domain, np.array([[0., 1], [0, 1]]))\n        self.send_signal(self.widget.Inputs.data, data)\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.variables],\n                             [\"A\", \"B (1)\", \"B (2)\"])\n\n    def test_keep_non_duplicate_variables_missing_rows(self):\n        c = DiscreteVariable(\"C\", values=(\"a\", \"b\", \"c\"))\n        domain = Domain([ContinuousVariable(\"A\"), ContinuousVariable(\"B\"), c])\n        data = Table(domain, np.array([[0., 0, 0], [1, 1, 1]]))\n        extra_data = Table(domain, np.array([[0., 1, 1], [0, 1, 2]]))\n        self.send_signal(self.widget.Inputs.data, data)\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        self.widget.attr_boxes.set_state([(c, c)])\n        self.widget.attr_boxes.emit_list()\n\n        # Only one row is matched; A has different values and it's duplicated,\n        # and B has the same values, so we get only one copy\n        self.widget.merging = self.widget.InnerJoin\n        self.widget.unconditional_commit()\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.variables],\n                             [\"A (1)\", \"B\", \"C\", \"A (2)\"])\n\n        # Table has additional rows; keep all columns\n        self.widget.merging = self.widget.OuterJoin\n        self.widget.unconditional_commit()\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual(\n            [m.name for m in merged_data.domain.variables],\n            [\"A (1)\", \"B (1)\", \"C (1)\", \"A (2)\", \"B (2)\", \"C (2)\"])\n\n        # First row is unmatched, data for B(2) is missing, but attribute\n        # shouldn't be added\n        extra_data = Table(domain, np.array([[1., 1, 1], [0, 1, 2]]))\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        self.widget.merging = self.widget.LeftJoin\n        self.widget.unconditional_commit()\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.variables],\n                             [\"A\", \"B\", \"C\"])\n\n    def test_summary(self):\n        \"\"\"Check if the status bar is updated when data is received\"\"\"\n        data = self.dataA\n        info = self.widget.info\n        no_input, no_output = \"No data on input\", \"No data on output\"\n\n        self.send_signal(self.widget.Inputs.data, data)\n        data_list = [(\"Data\", data), (\"Extra data\", None)]\n        summary, details = f\"{len(data)}, 0\", format_multiple_summaries(data_list)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.assertIsInstance(info._StateInfo__output_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n        self.send_signal(self.widget.Inputs.extra_data, data)\n        data_list = [(\"Data\", data), (\"Extra data\", data)]\n        summary = f\"{len(data)}, {len(data)}\"\n        details = format_multiple_summaries(data_list)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        output = self.get_output(self.widget.Outputs.data)\n        summary, details = f\"{len(output)}\", format_summary_details(output)\n        self.assertEqual(info._StateInfo__output_summary.brief, summary)\n        self.assertEqual(info._StateInfo__output_summary.details, details)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        data_list = [(\"Data\", None), (\"Extra data\", data)]\n        summary, details = f\"0, {len(data)}\", format_multiple_summaries(data_list)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.assertIsInstance(info._StateInfo__output_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n        self.send_signal(self.widget.Inputs.extra_data, None)\n        self.assertIsInstance(info._StateInfo__input_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__input_summary.details, no_input)\n        self.assertIsInstance(info._StateInfo__output_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\nclass MergeDataContextHandlerTest(unittest.TestCase):\n    # These units are too small to test individually, so they are tested\n    # within their function in the widget.\n\n    # The following test only covers obscure cases that seem to appear only\n    # within the context of some tests and can't appear in real world.\n    def test_malformed_contexts(self):\n        widget = Mock()\n        handler = MergeDataContextHandler()\n        # pylint: disable=protected-access\n        self.assertEqual(handler._encode_domain(None), {})\n\n        widget.current_context = None\n        handler.settings_from_widget(widget)  # mustn't crash\n        handler.settings_to_widget(widget)  # mustn't crash\n\n    def test_attr_pairs_not_present(self):\n        data = Table(\"iris\")\n\n        context = SimpleNamespace(values={})\n        widget = SimpleNamespace(\n            current_context=context, attr_pairs=(\"a\", \"b\")\n        )\n        handler = MergeDataContextHandler()\n\n        handler.settings_to_widget(widget)  # mustn't crash\n        # no attr_pairs in context -> handler must not change widget.attr_pairs\n        self.assertTupleEqual(widget.attr_pairs, (\"a\", \"b\"))\n\n        context = SimpleNamespace(\n            values={\n                \"attr_pairs\": [((data.domain[0], 100), (data.domain[1], 100))]\n            }\n        )\n        widget = SimpleNamespace(\n            current_context=context,\n            attr_pairs=(\"a\", \"b\"),\n            data=data,\n            extra_data=data,\n        )\n\n        handler.settings_to_widget(widget)  # mustn't crash\n        # values taken from context\n        self.assertListEqual(\n            widget.attr_pairs, [(data.domain[0], data.domain[1])]\n        )\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\n# There are never too many tests, so:\n# pylint: disable=too-many-lines,too-many-public-methods, protected-access\nfrom itertools import chain\nimport unittest\nfrom types import SimpleNamespace\nfrom unittest.mock import Mock\n\nimport numpy as np\nimport scipy.sparse as sp\n\nfrom AnyQt.QtCore import Qt\n\nfrom orangewidget.widget import StateInfo\n\nfrom Orange.data import Table, Domain, DiscreteVariable, StringVariable, \\\n    ContinuousVariable\nfrom Orange.widgets.data.owmergedata import OWMergeData, INSTANCEID, INDEX, \\\n    MergeDataContextHandler\nfrom Orange.widgets.tests.base import WidgetTest\nfrom Orange.widgets.utils.state_summary import format_multiple_summaries, \\\n    format_summary_details\nfrom Orange.tests import test_filename\n\n\nclass TestOWMergeData(WidgetTest):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        domainA = Domain([DiscreteVariable(\"dA1\", (\"a\", \"b\", \"c\", \"d\")),\n                          DiscreteVariable(\"dA2\", (\"aa\", \"bb\"))],\n                         DiscreteVariable(\"clsA\", (\"aaa\", \"bbb\", \"ccc\")),\n                         [DiscreteVariable(\"mA1\", (\"cc\", \"dd\")),\n                          StringVariable(\"mA2\")])\n        XA = np.array([[0, 0], [1, 1], [2, 0], [3, 1]])\n        yA = np.array([0, 1, 2, np.nan])\n        metasA = np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                           [0.0, \"m4\"]]).astype(object)\n\n        domainB = Domain([DiscreteVariable(\"dB1\", values=(\"a\", \"b\", \"c\")),\n                          DiscreteVariable(\"dB2\", values=(\"aa\", \"bb\"))],\n                         DiscreteVariable(\"clsB\", values=(\"bbb\", \"ccc\")),\n                         [DiscreteVariable(\"mB1\", (\"m4\", \"m5\"))])\n        XB = np.array([[0, 0], [1, 1], [2, np.nan]])\n        yB = np.array([np.nan, 1, 0])\n        metasB = np.array([[np.nan], [1], [0]]).astype(object)\n        cls.dataA = Table(domainA, XA, yA, metasA)\n        cls.dataA.name = 'dataA'\n        cls.dataA.attributes = 'dataA attributes'\n        cls.dataB = Table(domainB, XB, yB, metasB)\n        cls.dataB.name = 'dataB'\n        cls.dataB.attributes = 'dataB attributes'\n\n    def setUp(self):\n        self.widget = self.create_widget(OWMergeData)\n\n    def test_input_remove(self):\n        \"\"\"Check widget after inputs have been removed\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.send_signal(self.widget.Inputs.extra_data, None)\n\n    def test_combobox_items(self):\n        \"\"\"Check if combo box content is properly set\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        row = self.widget.attr_boxes.rows[0]\n        data_combo, extra_combo = row.left_combo, row.right_combo\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n        data_items = extra_items = list(\n            chain([INDEX, INSTANCEID], domainA.variables, domainA.metas))\n        self.assertListEqual(data_combo.model()[:], data_items)\n        self.assertListEqual(extra_combo.model()[:], extra_items)\n\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        data_items = list(\n            chain([INDEX, INSTANCEID], domainA.variables, domainA.metas))\n        extra_items = list(\n            chain([INDEX, INSTANCEID], domainB.variables, domainB.metas))\n        self.assertListEqual(data_combo.model()[:], data_items)\n        self.assertListEqual(extra_combo.model()[:], extra_items)\n\n        self.send_signal(self.widget.Inputs.data, self.dataB)\n        data_items = extra_items = list(\n            chain([INDEX, INSTANCEID], domainB.variables, domainB.metas))\n        self.assertListEqual(data_combo.model()[:], data_items)\n        self.assertListEqual(extra_combo.model()[:], extra_items)\n\n    def test_combo_box_sync(self):\n        row = self.widget.attr_boxes.rows[0]\n        data_combo, extra_combo = row.left_combo, row.right_combo\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        extra_combo.setCurrentIndex(3)\n        data_combo.setCurrentIndex(0)\n        data_combo.activated.emit(0)\n        self.assertEqual(data_combo.currentIndex(), 0)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        data_combo.setCurrentIndex(1)\n        data_combo.activated.emit(1)\n        self.assertEqual(data_combo.currentIndex(), 1)\n        self.assertEqual(extra_combo.currentIndex(), 1)\n\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        self.assertEqual(data_combo.currentIndex(), 0)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        extra_combo.setCurrentIndex(1)\n        extra_combo.activated.emit(1)\n        self.assertEqual(data_combo.currentIndex(), 1)\n        self.assertEqual(extra_combo.currentIndex(), 1)\n\n    def test_attr_combo_tooltips(self):\n        row = self.widget.attr_boxes.rows[0]\n        model = row.left_combo.model()\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        tip = model.data(model.index(2, 0), Qt.ToolTipRole)\n        # Test the test; if general tooltips ever change and the following\n        # assert fails, the rest of this test has to be modified accordingly\n        self.assertTrue(tip.startswith(\"<b>\"))\n\n        # Just test that tooltip is a string (implicitly) and that it's not\n        # a generic DomainModel tooltip\n        tip = model.data(model.index(0, 0), Qt.ToolTipRole)\n        self.assertFalse(tip.startswith(\"<b>\"))\n\n        tip = model.data(model.index(1, 0), Qt.ToolTipRole)\n        self.assertFalse(tip.startswith(\"<b>\"))\n\n    def test_match_attr_name(self):\n        widget = self.widget\n        row = widget.attr_boxes.rows[0]\n        data_combo, extra_combo = row.left_combo, row.right_combo\n\n        domainA = Domain([DiscreteVariable(\"dA1\", (\"a\", \"b\", \"c\", \"d\")),\n                          DiscreteVariable(\"dA2\", (\"aa\", \"bb\")),\n                          DiscreteVariable(\"dA3\", (\"aa\", \"bb\"))],\n                         DiscreteVariable(\"cls\", (\"aaa\", \"bbb\", \"ccc\")),\n                         [DiscreteVariable(\"mA1\", (\"cc\", \"dd\")),\n                          StringVariable(\"mA2\")])\n        XA = np.array([[0, 0, 0], [1, 1, 0], [2, 0, 0], [3, 1, 0]])\n        yA = np.array([0, 1, 2, np.nan])\n        metasA = np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                           [0.0, \"m4\"]]).astype(object)\n\n        domainB = Domain([DiscreteVariable(\"dB1\", values=(\"a\", \"b\", \"c\")),\n                          ContinuousVariable(\"dA2\")],\n                         None,\n                         [StringVariable(\"cls\"),\n                          DiscreteVariable(\"dA1\", (\"m4\", \"m5\"))])\n        XB = np.array([[0, 0], [1, 1], [2, np.nan]])\n        yB = np.empty((3, 0))\n        metasB = np.array([[np.nan, np.nan], [1, 1], [0, 0]]).astype(object)\n        dataA = Table(domainA, XA, yA, metasA)\n        dataA.name = 'dataA'\n        dataA.attributes = 'dataA attributes'\n        dataB = Table(domainB, XB, yB, metasB)\n        dataB.name = 'dataB'\n        dataB.attributes = 'dataB attributes'\n\n        self.send_signal(widget.Inputs.data, dataA)\n        self.send_signal(widget.Inputs.extra_data, dataB)\n\n        # match variable if available and the other combo is Row Index\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n        self.assertEqual(extra_combo.currentIndex(), 5)\n\n        # match variable if available and the other combo is ID\n        extra_combo.setCurrentIndex(1)\n        extra_combo.activated.emit(1)\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n        self.assertEqual(extra_combo.currentIndex(), 5)\n\n        # don't match variable if other combo is set\n        extra_combo.setCurrentIndex(4)\n        extra_combo.activated.emit(4)\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n        self.assertEqual(extra_combo.currentIndex(), 4)\n\n        # don't match if nothing to match to\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(4)\n        data_combo.activated.emit(4)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        # don't match numeric with non-numeric\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(3)\n        data_combo.activated.emit(3)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        # allow matching string with discrete\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(5)\n        data_combo.activated.emit(5)\n        self.assertEqual(extra_combo.currentIndex(), 4)\n\n    def test_add_row_button(self):\n        boxes = self.widget.attr_boxes\n        boxes.set_state([(INSTANCEID, INSTANCEID), (INSTANCEID, INSTANCEID)])\n        boxes.rows[-1].add_button.clicked.emit()\n        self.assertEqual(len(boxes.rows), 3)\n        self.assertEqual(boxes.layout().count(), 3)\n\n    def test_remove_row(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        var0, var1 = self.dataA.domain.attributes[:2]\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n        for i, row in enumerate(boxes.rows):\n            self.assertTrue(row.remove_button.isEnabled())\n            self.assertEqual(row.remove_button.text(), \"×\")\n            self.assertEqual(row.add_button.isEnabled(), i == 2)\n            self.assertEqual(row.add_button.text(), [\"\", \"+\"][i == 2])\n\n        boxes.rows[1].remove_button.clicked.emit()\n        self.assertEqual(boxes.current_state(), [(INDEX, INDEX), (var0, var1)])\n        for i, row in enumerate(boxes.rows):\n            self.assertTrue(row.remove_button.isEnabled())\n            self.assertEqual(row.remove_button.text(), \"×\")\n            self.assertEqual(row.add_button.isEnabled(), i == 1)\n            self.assertEqual(row.add_button.text(), [\"\", \"+\"][i])\n\n        boxes.rows[1].remove_button.clicked.emit()\n        self.assertEqual(boxes.current_state(), [(INDEX, INDEX)])\n        row = boxes.rows[0]\n        self.assertFalse(row.remove_button.isEnabled())\n        self.assertEqual(row.remove_button.text(), \"\")\n        self.assertTrue(row.add_button.isEnabled())\n        self.assertEqual(row.add_button.text(), \"+\")\n\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n        boxes.rows[2].remove_button.clicked.emit()\n        self.assertEqual(\n            boxes.current_state(), [(INDEX, INDEX), (INSTANCEID, INSTANCEID)])\n        for i, row in enumerate(boxes.rows):\n            self.assertTrue(row.remove_button.isEnabled())\n            self.assertEqual(row.remove_button.text(), \"×\")\n            self.assertEqual(row.add_button.isEnabled(), i == 1)\n            self.assertEqual(row.add_button.text(), [\"\", \"+\"][i == 1])\n\n    def test_dont_remove_single_row(self):\n        widget = self.widget\n        rows = widget.attr_boxes.rows\n        self.assertEqual(len(rows), 1)\n        rows[0].remove_button.clicked.emit()\n        self.assertEqual(len(rows), 1)\n\n    def test_retrieve_settings(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        var0, var1 = self.dataA.domain.attributes[:2]\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n\n        settings = widget.settingsHandler.pack_data(widget)\n\n        widget2 = self.create_widget(OWMergeData, stored_settings=settings)\n        widget2.attr_boxes.set_state([(INDEX, INDEX)])\n        self.send_signals(\n            [(widget2.Inputs.data, self.dataA),\n             (widget2.Inputs.extra_data, self.dataA)],\n            widget=widget2)\n        self.assertEqual(\n            widget2.attr_boxes.current_state(),\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n\n    def test_match_settings(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        domainA = self.dataA.domain\n        domainB = self.dataB.domain\n\n        self.send_signal(widget.Inputs.data, self.dataA)\n        self.send_signal(widget.Inputs.extra_data, self.dataA)\n        attr_pairs = [(INDEX, INDEX), (INSTANCEID, INSTANCEID),\n                      (domainA[0], domainA[1]), (domainA[1], domainA[0])]\n        boxes.set_state(attr_pairs)\n        boxes.emit_list()\n        self.assertEqual(widget.attr_pairs, attr_pairs)\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertEqual(widget.attr_pairs, [(INDEX, INDEX)])\n\n        self.send_signal(widget.Inputs.data, self.dataA)\n        self.assertEqual(widget.attr_pairs, attr_pairs)\n\n        self.send_signal(widget.Inputs.extra_data, self.dataB)\n        attr_pairs2 = [(domainA[0], domainB[0]), (domainA[1], domainB[1])]\n        boxes.set_state(attr_pairs2)\n        boxes.emit_list()\n        self.assertEqual(widget.attr_pairs, attr_pairs2)\n\n        self.send_signal(widget.Inputs.extra_data, self.dataA)\n        self.assertEqual(widget.attr_pairs, attr_pairs)\n\n    def test_migrate_settings(self):\n        def create_and_send(settings):\n            widget = self.create_widget(OWMergeData, stored_settings=settings)\n            for signal in (widget.Inputs.data, widget.Inputs.extra_data):\n                self.send_signal(signal, self.dataA)\n            return widget\n\n        domainA = self.dataA.domain\n        attr1, attr2, attr3 = domainA.variables\n        attr4, attr5 = domainA.metas\n\n        # Migration from version == None\n        orig_settings = dict(\n            attr_augment_data=attr1.name,\n            attr_augment_extra=attr2.name,\n            attr_merge_data=attr3.name,\n            attr_merge_extra=attr4.name,\n            attr_combine_data=attr5.name,\n            attr_combine_extra='Position (index)')\n\n        widget = create_and_send(dict(merging=0, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr1, attr2)]))\n\n        widget = create_and_send(dict(merging=1, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr3, attr4)]))\n\n        widget = create_and_send(dict(merging=2, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr5, attr5)]))\n\n        orig_settings[\"attr_combine_extra\"] = \"Source position (index)\"\n        widget = create_and_send(dict(merging=2, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr5, attr5)]))\n\n        # Migration from version 1\n        orig_settings = {\"attr_pairs\": (True, True, [[attr1.name, attr2.name],\n                                                     [attr3.name, attr4.name]]),\n                         \"__version__\": 1}\n        widget = create_and_send(orig_settings)\n        self.assertEqual(widget.attr_pairs, ([(attr1, attr2), (attr3, attr4)]))\n\n    def test_migrate_settings_attr_pairs_extra_none(self):\n        settings = {'attr_pairs': (True, False, [['sepal length', 0]])}\n        OWMergeData.migrate_settings(settings, 1)\n        self.assertListEqual(settings[\"context_settings\"], [])\n\n    def test_migrate_settings_attr_pairs_data_none(self):\n        settings = {'attr_pairs': (False, True, [[0, \"sepal length\"]])}\n        OWMergeData.migrate_settings(settings, 1)\n        self.assertListEqual(settings[\"context_settings\"], [])\n\n    def test_migrate_settings_attr_pairs_id_idx(self):\n        settings = {\"attr_pairs\": (True, True, [[0, 1]])}\n        OWMergeData.migrate_settings(settings, 1)\n        context = settings[\"context_settings\"][0]\n        self.assertListEqual(context.values[\"attr_pairs\"],\n                             [((INDEX, 100), (INSTANCEID, 100))])\n        self.assertDictEqual(context.variables1, {})\n        self.assertDictEqual(context.variables2, {})\n\n    def test_migrate_settings_attr_pairs_vars(self):\n        settings = {\"attr_pairs\": (True, True,\n                                   [[\"sepal length\", \"sepal width\"],\n                                    [\"petal length\", \"petal width\"]])}\n        OWMergeData.migrate_settings(settings, 1)\n        context = settings[\"context_settings\"][0]\n        self.assertListEqual(context.values[\"attr_pairs\"],\n                             [((\"sepal length\", 100), (\"sepal width\", 100)),\n                              ((\"petal length\", 100), (\"petal width\", 100))])\n        self.assertDictEqual(context.variables1, {})\n        self.assertDictEqual(context.variables2, {})\n\n    def test_report(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        var0, var1 = self.dataA.domain.attributes[:2]\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n        widget.send_report()\n        # Don't crash, that's it\n\n    def test_no_matches(self):\n        \"\"\"Check output is None when there are no matches in inner join\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        domA = self.dataA.domain\n        domB = self.dataB.domain\n\n        self.widget.attr_boxes.set_state([(domA[\"dA1\"], domB[\"dB2\"])])\n        self.widget.controls.merging.buttons[self.widget.LeftJoin].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.data))\n\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertIsNone(self.get_output(self.widget.Outputs.data))\n\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.data))\n\n    def test_output_merge_by_ids_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by\n        Source position (index)\"\"\"\n        domain = self.dataA.domain\n        result = Table(domain, np.array([[1, 1], [2, 0]]), np.array([1, 2]),\n                       np.array([[1.0, \"m2\"], [np.nan, \"m3\"]]).astype(object))\n        self.send_signal(self.widget.Inputs.data, self.dataA[:3, [0, \"clsA\", -1]])\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA[1:, [1, \"clsA\", -2]])\n        self.widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_ids_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        Source position (index)\"\"\"\n        domainA = self.dataA.domain\n        values = domainA.class_var.values\n        domain = Domain(domainA.attributes,\n                        (DiscreteVariable(\"clsA (1)\", values),\n                         DiscreteVariable(\"clsA (2)\", values)),\n                        domainA.metas)\n        result = Table(domain,\n                       np.array([[1, 1], [2, 0], [3, np.nan], [np.nan, 0]]),\n                       np.array([[1, 1], [2, 2], [np.nan, np.nan], [np.nan, 0]]),\n                       np.array([[1.0, \"m2\"], [np.nan, \"m3\"],\n                                 [0.0, \"\"], [np.nan, \"m1\"]]).astype(object))\n        self.widget.merging = 2\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        self.send_signal(self.widget.Inputs.data, self.dataA[1:, [0, \"clsA\", -1]])\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA[:3, [1, \"clsA\", -2]])\n        self.widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        self.widget.attr_boxes.emit_list()\n        out = self.get_output(self.widget.Outputs.data)\n        self.assertTablesEqual(out, result)\n        np.testing.assert_equal(\n            out.ids, np.hstack((self.dataA.ids[1:], self.dataA.ids[:1])))\n\n    def test_output_merge_by_ids_outer_single_class(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        Source position (index) when all extra rows are matched and there is\n        only a single class variable in the output\"\"\"\n        domainA = self.dataA.domain\n        values = domainA.class_var.values\n        domain = Domain(domainA.attributes,\n                        DiscreteVariable(\"clsA\", values),\n                        domainA.metas)\n        result = Table(domain,\n                       np.array([[0, 0], [1, 1], [2, 0], [3, np.nan]]),\n                       np.array([[0], [1], [2], [np.nan]]),\n                       np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                                 [0.0, \"\"]]).astype(object))\n        self.widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        self.widget.merging = 2\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        # When Y is a single column, Table.Y returns a vector, not a 2d array,\n        # which cause an exception in outer_join's vstack for Y if extra data\n        # has no unmatched rows.\n        # This test also checks this condition.\n        self.send_signal(self.widget.Inputs.data, self.dataA[:, [0, \"clsA\", -1]])\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA[:3, [1, -2]])\n        out = self.get_output(self.widget.Outputs.data)\n        self.assertTablesEqual(out, result)\n        np.testing.assert_equal(out.ids, self.dataA.ids)\n\n    def test_output_merge_by_index_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        Position (index)\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1],\n                             [2, 0, 2, np.nan], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]\n                            ]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_index_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by\n        Position (index)\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1], [2, 0, 2, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.controls.merging.buttons[1].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_index_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        Position (index)\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1],\n                             [2, 0, 2, np.nan], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.controls.merging.buttons[2].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_attribute_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        attribute\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes[1:],\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0], [1, 1, 1],\n                             [2, 0, np.nan], [3, 1, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[0], domainB[0])])\n        self.widget.commit()\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertTablesEqual(output, result)\n        self.assertEqual(output.name, self.dataA.name)\n        np.testing.assert_array_equal(output.ids, self.dataA.ids)\n        self.assertEqual(output.attributes, self.dataA.attributes)\n\n    def test_output_merge_by_attribute_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by attribute\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes[1:],\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0], [1, 1, 1], [2, 0, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[0], domainB[0])])\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_attribute_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        attribute\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1],\n                             [2, 0, 2, np.nan], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[0], domainB[0])])\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_attribute_outer_same_attr(self):\n        \"\"\"Values of columns from extra aata are copied to left part if they\n        match\"\"\"\n        name = StringVariable(\"name\")\n        domainA = Domain([ContinuousVariable(\"x\")], None, [name])\n        domainB = Domain([ContinuousVariable(\"y\")], None, [name])\n        xA = np.array([[0], [1], [2]])\n        mA = np.array([[\"a\"], [\"b\"], [\"c\"]])\n        xB = np.array([[4], [5], [6], [7]])\n        mB = np.array([[\"b\"], [\"d\"], [\"a\"], [\"c\"]])\n        dataA = Table(domainA, xA, None, mA)\n        dataB = Table(domainB, xB, None, mB)\n\n        self.send_signal(self.widget.Inputs.data, dataA)\n        self.send_signal(self.widget.Inputs.extra_data, dataB)\n        self.widget.attr_boxes.set_state([(name, name)])\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        out = self.get_output(self.widget.Outputs.data)\n        np.testing.assert_equal(\n            out.X,\n            np.array([[0, 6], [1, 4], [2, 7], [np.nan, 5]]))\n        self.assertEqual(\" \".join(out.metas.flatten()), \"a a b b c c  d\")\n\n    def test_output_merge_by_class_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        class variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars, domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, 2, np.nan],\n                             [2, 0, 1, 1], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([0, 1, 2, np.nan])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 0.0],\n                             [np.nan, \"m3\", 1.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[2], domainB[2])])\n        self.widget.commit()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_class_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by class\n        variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars, domainA.metas + domainB.metas)\n        result_X = np.array([[1, 1, 2, np.nan], [2, 0, 1, 1]])\n        result_Y = np.array([1, 2])\n        result_M = np.array([[1.0, \"m2\", 0.0], [np.nan, \"m3\", 1.0]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state(\n            [(domainA.class_vars[0], domainB.class_vars[0])])\n        self.widget.controls.merging.buttons[1].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_class_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        class variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, 2, np.nan],\n                             [2, 0, 1, 1], [3, 1, np.nan, np.nan],\n                             [np.nan, np.nan, 0, 0]])\n        result_Y = np.array([[0, np.nan], [1, 0], [2, 1], [np.nan, np.nan],\n                             [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 0.0],\n                             [np.nan, \"m3\", 1.0], [0.0, \"m4\", np.nan],\n                             [np.nan, \"\", np.nan]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state(\n            [(domainA.class_vars[0], domainB.class_vars[0])])\n        self.widget.controls.merging.buttons[2].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_meta_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        meta variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, np.nan, np.nan],\n                             [2, 0, np.nan, np.nan], [3, 1, 2, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, np.nan],\n                             [2, np.nan], [np.nan, 0]])\n        result_M = np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                             [0.0, \"m4\"]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[-2], domainB[-1])])\n        self.widget.commit()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_meta_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by meta\n        variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas)\n        result_X = np.array([[3, 1, 2, np.nan]])\n        result_Y = np.array([[np.nan, 0]])\n        result_M = np.array([[0.0, \"m4\"]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[-2], domainB[-1])])\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_meta_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        meta variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, np.nan, np.nan],\n                             [2, 0, np.nan, np.nan], [3, 1, 2, np.nan],\n                             [np.nan, np.nan, 0, 0], [np.nan, np.nan, 1, 1]])\n        result_Y = np.array([[0, np.nan], [1, np.nan], [2, np.nan],\n                             [np.nan, 0], [np.nan, np.nan], [np.nan, 1]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", np.nan],\n                             [np.nan, \"m3\", np.nan], [0.0, \"m4\", 0.0],\n                             [np.nan, \"\", np.nan], [np.nan, \"\", 1.0]]\n                           ).astype(object)\n\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[-2], domainB[-1])])\n        self.widget.controls.merging.buttons[2].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def assertTablesEqual(self, table1, table2):\n        self.assertEqual(table1.domain, table2.domain)\n        np.testing.assert_array_equal(table1.X, table2.X)\n        np.testing.assert_array_equal(table1.Y, table2.Y)\n        np.testing.assert_array_equal(table1.metas.astype(str),\n                                      table2.metas.astype(str))\n\n    def test_best_match(self):\n        \"\"\"Check default merging attributes setup\"\"\"\n        widget = self.widget\n        indices = list(range(101))\n        indices.pop(26)\n        zoo = Table(\"zoo\")[indices]\n        zoo_images = Table(test_filename(\"datasets/zoo-with-images.tab\"))\n        self.send_signal(widget.Inputs.data, zoo)\n        self.send_signal(widget.Inputs.extra_data, zoo_images)\n        for i in range(3):\n            self.assertEqual(widget.attr_boxes.current_state(),\n                             [(zoo.domain[\"name\"], zoo_images.domain[\"name\"])],\n                             f\"wrong attributes chosen for merge_type={i}\")\n\n    def test_sparse(self):\n        data = Table(\"iris\")[::25]\n        data_ed_dense = Table(\"titanic\")[::300]\n        data_ed_sparse = Table(\"titanic\")[::300].to_sparse()\n        self.send_signal(\"Data\", data)\n\n        self.send_signal(\"Extra Data\", data_ed_dense)\n        output_dense = self.get_output(\"Data\")\n        self.assertFalse(sp.issparse(output_dense.X))\n        self.assertFalse(output_dense.is_sparse())\n\n        self.send_signal(\"Extra Data\", data_ed_sparse)\n        output_sparse = self.get_output(\"Data\")\n        self.assertTrue(sp.issparse(output_sparse.X))\n        self.assertTrue(output_sparse.is_sparse())\n\n        output_sparse.X = output_sparse.X.toarray()\n        self.assertTablesEqual(output_dense, output_sparse)\n\n    def test_commit_on_new_data(self):\n        \"\"\"Check that disabling auto apply doesn't block on new data\"\"\"\n        self.widget.auto_apply = False\n        self.widget.merging = 2\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.data))\n\n    def test_multiple_attributes_left(self):\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n\n        X = np.array([[0, 1], [1, 1], [3, np.nan], [np.nan, 0]])\n        Y = np.array([1, 2, 0, 0])\n        metas = np.array([[0, \"a\"], [1, \"b\"], [0, \"c\"], [0, \"d\"]])\n        dataA = Table(domainA, X, Y, metas)\n        dataA.name = \"dataA\"\n\n        X = np.array(\n            [[0, 0], [1, 0], [0, 1], [1, 1], [2, 0], [3, np.nan]])\n        Y = np.array([0, 0, 1, 1, 0, 1])\n        metas = np.array([[0], [1], [0], [1], [1], [1]])\n        dataB = Table(domainB, X, Y, metas)\n        dataB.name = \"dataB\"\n\n        self.send_signal(self.widget.Inputs.data, dataA)\n        self.send_signal(self.widget.Inputs.extra_data, dataB)\n        self.widget.attr_boxes.set_state(\n            [(domainA[0], domainB[0]), (domainA[1], domainB[1])])\n        self.widget.commit()\n        output = self.get_output(self.widget.Outputs.data)\n\n        self.assertEqual(output.name, dataA.name)\n        np.testing.assert_array_equal(output.ids, dataA.ids)\n        self.assertEqual(output.attributes, dataA.attributes)\n        np.testing.assert_equal(output.X, dataA.X)\n\n    def test_nonunique(self):\n        widget = self.widget\n        x = ContinuousVariable(\"x\")\n        d = DiscreteVariable(\"d\", values=tuple(\"abc\"))\n        domain = Domain([x, d], [])\n        dataA = Table.from_numpy(\n            domain, np.array([[1.0, 0], [1, 1], [2, 1]]))\n        dataB = Table.from_numpy(\n            domain, np.array([[1.0, 0], [2, 1], [3, 1]]))\n        dataB.ids = dataA.ids\n        self.send_signal(widget.Inputs.data, dataA)\n        self.send_signal(widget.Inputs.extra_data, dataB)\n        widget.merging = widget.InnerJoin\n\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n\n        widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.attr_boxes.set_state([(INDEX, INDEX)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.attr_boxes.set_state([(x, x)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.LeftJoin\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.InnerJoin\n        widget.attr_boxes.set_state([(x, x), (d, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.attr_boxes.set_state([(d, d)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.nonunique_left.is_shown())\n        self.assertTrue(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.LeftJoin\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertTrue(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.InnerJoin\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.nonunique_left.is_shown())\n        self.assertTrue(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        self.send_signal(widget.Inputs.data, None)\n        self.send_signal(widget.Inputs.extra_data, None)\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n    def test_invalide_pairs(self):\n        widget = self.widget\n        x = ContinuousVariable(\"x\")\n        d = DiscreteVariable(\"d\", values=tuple(\"abc\"))\n        domain = Domain([x, d], [])\n        dataA = Table.from_numpy(\n            domain, np.array([[1.0, 0], [1, 1], [2, 1]]))\n        dataB = Table.from_numpy(\n            domain, np.array([[1.0, 0], [2, 1], [3, 1]]))\n        dataB.ids = dataA.ids\n        self.send_signal(widget.Inputs.data, dataA)\n        self.send_signal(widget.Inputs.extra_data, dataB)\n\n        widget.attr_boxes.set_state([(x, x), (d, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (INDEX, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertTrue(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (d, INDEX)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertTrue(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (INSTANCEID, d)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (d, INSTANCEID)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (INDEX, INSTANCEID)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.matching_id_with_sth.is_shown()\n                        or widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (x, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertTrue(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n    def test_duplicate_names(self):\n        domain = Domain([ContinuousVariable(\"C1\")],\n                        metas=[DiscreteVariable(\"Feature\", values=(\"A\", \"B\"))])\n        data = Table(domain, np.array([[1.], [0.]]),\n                     metas=np.array([[1.], [0.]]))\n        domain = Domain([ContinuousVariable(\"C1\")],\n                        metas=[StringVariable(\"Feature\")])\n        extra_data = Table(domain, np.array([[1.], [0.]]),\n                           metas=np.array([[\"A\"], [\"B\"]]))\n        self.send_signal(self.widget.Inputs.data, data)\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        self.assertTrue(self.widget.Warning.renamed_vars.is_shown())\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.metas],\n                             [\"Feature (1)\", \"Feature (2)\"])\n\n    def test_keep_non_duplicate_variables(self):\n        domain = Domain([ContinuousVariable(\"A\"), ContinuousVariable(\"B\")])\n        data = Table(domain, np.array([[0., 0], [0, 1]]))\n        extra_data = Table(domain, np.array([[0., 1], [0, 1]]))\n        self.send_signal(self.widget.Inputs.data, data)\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.variables],\n                             [\"A\", \"B (1)\", \"B (2)\"])\n\n    def test_keep_non_duplicate_variables_missing_rows(self):\n        c = DiscreteVariable(\"C\", values=(\"a\", \"b\", \"c\"))\n        domain = Domain([ContinuousVariable(\"A\"), ContinuousVariable(\"B\"), c])\n        data = Table(domain, np.array([[0., 0, 0], [1, 1, 1]]))\n        extra_data = Table(domain, np.array([[0., 1, 1], [0, 1, 2]]))\n        self.send_signal(self.widget.Inputs.data, data)\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        self.widget.attr_boxes.set_state([(c, c)])\n        self.widget.attr_boxes.emit_list()\n\n        # Only one row is matched; A has different values and it's duplicated,\n        # and B has the same values, so we get only one copy\n        self.widget.merging = self.widget.InnerJoin\n        self.widget.unconditional_commit()\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.variables],\n                             [\"A (1)\", \"B\", \"C\", \"A (2)\"])\n\n        # Table has additional rows; keep all columns\n        self.widget.merging = self.widget.OuterJoin\n        self.widget.unconditional_commit()\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual(\n            [m.name for m in merged_data.domain.variables],\n            [\"A (1)\", \"B (1)\", \"C (1)\", \"A (2)\", \"B (2)\", \"C (2)\"])\n\n        # First row is unmatched, data for B(2) is missing, but attribute\n        # shouldn't be added\n        extra_data = Table(domain, np.array([[1., 1, 1], [0, 1, 2]]))\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        self.widget.merging = self.widget.LeftJoin\n        self.widget.unconditional_commit()\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.variables],\n                             [\"A\", \"B\", \"C\"])\n\n    def test_summary(self):\n        \"\"\"Check if the status bar is updated when data is received\"\"\"\n        data = self.dataA\n        info = self.widget.info\n        no_input, no_output = \"No data on input\", \"No data on output\"\n\n        self.send_signal(self.widget.Inputs.data, data)\n        data_list = [(\"Data\", data), (\"Extra data\", None)]\n        summary, details = f\"{len(data)}, 0\", format_multiple_summaries(data_list)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.assertIsInstance(info._StateInfo__output_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n        self.send_signal(self.widget.Inputs.extra_data, data)\n        data_list = [(\"Data\", data), (\"Extra data\", data)]\n        summary = f\"{len(data)}, {len(data)}\"\n        details = format_multiple_summaries(data_list)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        output = self.get_output(self.widget.Outputs.data)\n        summary, details = f\"{len(output)}\", format_summary_details(output)\n        self.assertEqual(info._StateInfo__output_summary.brief, summary)\n        self.assertEqual(info._StateInfo__output_summary.details, details)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        data_list = [(\"Data\", None), (\"Extra data\", data)]\n        summary, details = f\"0, {len(data)}\", format_multiple_summaries(data_list)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.assertIsInstance(info._StateInfo__output_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n        self.send_signal(self.widget.Inputs.extra_data, None)\n        self.assertIsInstance(info._StateInfo__input_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__input_summary.details, no_input)\n        self.assertIsInstance(info._StateInfo__output_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n    def test_empty_tables(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.dataA[:0])\n        self.send_signal(widget.Inputs.extra_data, self.dataB[:0])\n\n\nclass MergeDataContextHandlerTest(unittest.TestCase):\n    # These units are too small to test individually, so they are tested\n    # within their function in the widget.\n\n    # The following test only covers obscure cases that seem to appear only\n    # within the context of some tests and can't appear in real world.\n    def test_malformed_contexts(self):\n        widget = Mock()\n        handler = MergeDataContextHandler()\n        # pylint: disable=protected-access\n        self.assertEqual(handler._encode_domain(None), {})\n\n        widget.current_context = None\n        handler.settings_from_widget(widget)  # mustn't crash\n        handler.settings_to_widget(widget)  # mustn't crash\n\n    def test_attr_pairs_not_present(self):\n        data = Table(\"iris\")\n\n        context = SimpleNamespace(values={})\n        widget = SimpleNamespace(\n            current_context=context, attr_pairs=(\"a\", \"b\")\n        )\n        handler = MergeDataContextHandler()\n\n        handler.settings_to_widget(widget)  # mustn't crash\n        # no attr_pairs in context -> handler must not change widget.attr_pairs\n        self.assertTupleEqual(widget.attr_pairs, (\"a\", \"b\"))\n\n        context = SimpleNamespace(\n            values={\n                \"attr_pairs\": [((data.domain[0], 100), (data.domain[1], 100))]\n            }\n        )\n        widget = SimpleNamespace(\n            current_context=context,\n            attr_pairs=(\"a\", \"b\"),\n            data=data,\n            extra_data=data,\n        )\n\n        handler.settings_to_widget(widget)  # mustn't crash\n        # values taken from context\n        self.assertListEqual(\n            widget.attr_pairs, [(data.domain[0], data.domain[1])]\n        )\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/data/tests/test_owmergedata.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "29fb9852e",
                "new_commit_hash": "2da8f97d3",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/data/tests/test_owmergedata.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/data/tests/test_owmergedata.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 1063,
                            "length": 6
                        },
                        "new_range": {
                            "start": 1063,
                            "length": 12
                        },
                        "section": "class TestOWMergeData(WidgetTest):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        self.assertIsInstance(info._StateInfo__output_summary, StateInfo.Empty)",
                                "type": "context"
                            },
                            {
                                "content": "        self.assertEqual(info._StateInfo__output_summary.details, no_output)",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    def test_empty_tables(self):",
                                "type": "added"
                            },
                            {
                                "content": "        widget = self.widget",
                                "type": "added"
                            },
                            {
                                "content": "        self.send_signal(widget.Inputs.data, self.dataA[:0])",
                                "type": "added"
                            },
                            {
                                "content": "        self.send_signal(widget.Inputs.extra_data, self.dataB[:0])",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "class MergeDataContextHandlerTest(unittest.TestCase):",
                                "type": "context"
                            },
                            {
                                "content": "    # These units are too small to test individually, so they are tested",
                                "type": "context"
                            },
                            {
                                "content": "    # within their function in the widget.",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/data/tests/test_owmergedata.py",
                            "type": "class",
                            "name": "TestOWMergeData",
                            "content": "class TestOWMergeData(WidgetTest):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        domainA = Domain([DiscreteVariable(\"dA1\", (\"a\", \"b\", \"c\", \"d\")),\n                          DiscreteVariable(\"dA2\", (\"aa\", \"bb\"))],\n                         DiscreteVariable(\"clsA\", (\"aaa\", \"bbb\", \"ccc\")),\n                         [DiscreteVariable(\"mA1\", (\"cc\", \"dd\")),\n                          StringVariable(\"mA2\")])\n        XA = np.array([[0, 0], [1, 1], [2, 0], [3, 1]])\n        yA = np.array([0, 1, 2, np.nan])\n        metasA = np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                           [0.0, \"m4\"]]).astype(object)\n\n        domainB = Domain([DiscreteVariable(\"dB1\", values=(\"a\", \"b\", \"c\")),\n                          DiscreteVariable(\"dB2\", values=(\"aa\", \"bb\"))],\n                         DiscreteVariable(\"clsB\", values=(\"bbb\", \"ccc\")),\n                         [DiscreteVariable(\"mB1\", (\"m4\", \"m5\"))])\n        XB = np.array([[0, 0], [1, 1], [2, np.nan]])\n        yB = np.array([np.nan, 1, 0])\n        metasB = np.array([[np.nan], [1], [0]]).astype(object)\n        cls.dataA = Table(domainA, XA, yA, metasA)\n        cls.dataA.name = 'dataA'\n        cls.dataA.attributes = 'dataA attributes'\n        cls.dataB = Table(domainB, XB, yB, metasB)\n        cls.dataB.name = 'dataB'\n        cls.dataB.attributes = 'dataB attributes'\n\n    def setUp(self):\n        self.widget = self.create_widget(OWMergeData)\n\n    def test_input_remove(self):\n        \"\"\"Check widget after inputs have been removed\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.send_signal(self.widget.Inputs.extra_data, None)\n\n    def test_combobox_items(self):\n        \"\"\"Check if combo box content is properly set\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        row = self.widget.attr_boxes.rows[0]\n        data_combo, extra_combo = row.left_combo, row.right_combo\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n        data_items = extra_items = list(\n            chain([INDEX, INSTANCEID], domainA.variables, domainA.metas))\n        self.assertListEqual(data_combo.model()[:], data_items)\n        self.assertListEqual(extra_combo.model()[:], extra_items)\n\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        data_items = list(\n            chain([INDEX, INSTANCEID], domainA.variables, domainA.metas))\n        extra_items = list(\n            chain([INDEX, INSTANCEID], domainB.variables, domainB.metas))\n        self.assertListEqual(data_combo.model()[:], data_items)\n        self.assertListEqual(extra_combo.model()[:], extra_items)\n\n        self.send_signal(self.widget.Inputs.data, self.dataB)\n        data_items = extra_items = list(\n            chain([INDEX, INSTANCEID], domainB.variables, domainB.metas))\n        self.assertListEqual(data_combo.model()[:], data_items)\n        self.assertListEqual(extra_combo.model()[:], extra_items)\n\n    def test_combo_box_sync(self):\n        row = self.widget.attr_boxes.rows[0]\n        data_combo, extra_combo = row.left_combo, row.right_combo\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        extra_combo.setCurrentIndex(3)\n        data_combo.setCurrentIndex(0)\n        data_combo.activated.emit(0)\n        self.assertEqual(data_combo.currentIndex(), 0)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        data_combo.setCurrentIndex(1)\n        data_combo.activated.emit(1)\n        self.assertEqual(data_combo.currentIndex(), 1)\n        self.assertEqual(extra_combo.currentIndex(), 1)\n\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        self.assertEqual(data_combo.currentIndex(), 0)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        extra_combo.setCurrentIndex(1)\n        extra_combo.activated.emit(1)\n        self.assertEqual(data_combo.currentIndex(), 1)\n        self.assertEqual(extra_combo.currentIndex(), 1)\n\n    def test_attr_combo_tooltips(self):\n        row = self.widget.attr_boxes.rows[0]\n        model = row.left_combo.model()\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        tip = model.data(model.index(2, 0), Qt.ToolTipRole)\n        # Test the test; if general tooltips ever change and the following\n        # assert fails, the rest of this test has to be modified accordingly\n        self.assertTrue(tip.startswith(\"<b>\"))\n\n        # Just test that tooltip is a string (implicitly) and that it's not\n        # a generic DomainModel tooltip\n        tip = model.data(model.index(0, 0), Qt.ToolTipRole)\n        self.assertFalse(tip.startswith(\"<b>\"))\n\n        tip = model.data(model.index(1, 0), Qt.ToolTipRole)\n        self.assertFalse(tip.startswith(\"<b>\"))\n\n    def test_match_attr_name(self):\n        widget = self.widget\n        row = widget.attr_boxes.rows[0]\n        data_combo, extra_combo = row.left_combo, row.right_combo\n\n        domainA = Domain([DiscreteVariable(\"dA1\", (\"a\", \"b\", \"c\", \"d\")),\n                          DiscreteVariable(\"dA2\", (\"aa\", \"bb\")),\n                          DiscreteVariable(\"dA3\", (\"aa\", \"bb\"))],\n                         DiscreteVariable(\"cls\", (\"aaa\", \"bbb\", \"ccc\")),\n                         [DiscreteVariable(\"mA1\", (\"cc\", \"dd\")),\n                          StringVariable(\"mA2\")])\n        XA = np.array([[0, 0, 0], [1, 1, 0], [2, 0, 0], [3, 1, 0]])\n        yA = np.array([0, 1, 2, np.nan])\n        metasA = np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                           [0.0, \"m4\"]]).astype(object)\n\n        domainB = Domain([DiscreteVariable(\"dB1\", values=(\"a\", \"b\", \"c\")),\n                          ContinuousVariable(\"dA2\")],\n                         None,\n                         [StringVariable(\"cls\"),\n                          DiscreteVariable(\"dA1\", (\"m4\", \"m5\"))])\n        XB = np.array([[0, 0], [1, 1], [2, np.nan]])\n        yB = np.empty((3, 0))\n        metasB = np.array([[np.nan, np.nan], [1, 1], [0, 0]]).astype(object)\n        dataA = Table(domainA, XA, yA, metasA)\n        dataA.name = 'dataA'\n        dataA.attributes = 'dataA attributes'\n        dataB = Table(domainB, XB, yB, metasB)\n        dataB.name = 'dataB'\n        dataB.attributes = 'dataB attributes'\n\n        self.send_signal(widget.Inputs.data, dataA)\n        self.send_signal(widget.Inputs.extra_data, dataB)\n\n        # match variable if available and the other combo is Row Index\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n        self.assertEqual(extra_combo.currentIndex(), 5)\n\n        # match variable if available and the other combo is ID\n        extra_combo.setCurrentIndex(1)\n        extra_combo.activated.emit(1)\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n        self.assertEqual(extra_combo.currentIndex(), 5)\n\n        # don't match variable if other combo is set\n        extra_combo.setCurrentIndex(4)\n        extra_combo.activated.emit(4)\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n        self.assertEqual(extra_combo.currentIndex(), 4)\n\n        # don't match if nothing to match to\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(4)\n        data_combo.activated.emit(4)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        # don't match numeric with non-numeric\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(3)\n        data_combo.activated.emit(3)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        # allow matching string with discrete\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(5)\n        data_combo.activated.emit(5)\n        self.assertEqual(extra_combo.currentIndex(), 4)\n\n    def test_add_row_button(self):\n        boxes = self.widget.attr_boxes\n        boxes.set_state([(INSTANCEID, INSTANCEID), (INSTANCEID, INSTANCEID)])\n        boxes.rows[-1].add_button.clicked.emit()\n        self.assertEqual(len(boxes.rows), 3)\n        self.assertEqual(boxes.layout().count(), 3)\n\n    def test_remove_row(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        var0, var1 = self.dataA.domain.attributes[:2]\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n        for i, row in enumerate(boxes.rows):\n            self.assertTrue(row.remove_button.isEnabled())\n            self.assertEqual(row.remove_button.text(), \"×\")\n            self.assertEqual(row.add_button.isEnabled(), i == 2)\n            self.assertEqual(row.add_button.text(), [\"\", \"+\"][i == 2])\n\n        boxes.rows[1].remove_button.clicked.emit()\n        self.assertEqual(boxes.current_state(), [(INDEX, INDEX), (var0, var1)])\n        for i, row in enumerate(boxes.rows):\n            self.assertTrue(row.remove_button.isEnabled())\n            self.assertEqual(row.remove_button.text(), \"×\")\n            self.assertEqual(row.add_button.isEnabled(), i == 1)\n            self.assertEqual(row.add_button.text(), [\"\", \"+\"][i])\n\n        boxes.rows[1].remove_button.clicked.emit()\n        self.assertEqual(boxes.current_state(), [(INDEX, INDEX)])\n        row = boxes.rows[0]\n        self.assertFalse(row.remove_button.isEnabled())\n        self.assertEqual(row.remove_button.text(), \"\")\n        self.assertTrue(row.add_button.isEnabled())\n        self.assertEqual(row.add_button.text(), \"+\")\n\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n        boxes.rows[2].remove_button.clicked.emit()\n        self.assertEqual(\n            boxes.current_state(), [(INDEX, INDEX), (INSTANCEID, INSTANCEID)])\n        for i, row in enumerate(boxes.rows):\n            self.assertTrue(row.remove_button.isEnabled())\n            self.assertEqual(row.remove_button.text(), \"×\")\n            self.assertEqual(row.add_button.isEnabled(), i == 1)\n            self.assertEqual(row.add_button.text(), [\"\", \"+\"][i == 1])\n\n    def test_dont_remove_single_row(self):\n        widget = self.widget\n        rows = widget.attr_boxes.rows\n        self.assertEqual(len(rows), 1)\n        rows[0].remove_button.clicked.emit()\n        self.assertEqual(len(rows), 1)\n\n    def test_retrieve_settings(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        var0, var1 = self.dataA.domain.attributes[:2]\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n\n        settings = widget.settingsHandler.pack_data(widget)\n\n        widget2 = self.create_widget(OWMergeData, stored_settings=settings)\n        widget2.attr_boxes.set_state([(INDEX, INDEX)])\n        self.send_signals(\n            [(widget2.Inputs.data, self.dataA),\n             (widget2.Inputs.extra_data, self.dataA)],\n            widget=widget2)\n        self.assertEqual(\n            widget2.attr_boxes.current_state(),\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n\n    def test_match_settings(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        domainA = self.dataA.domain\n        domainB = self.dataB.domain\n\n        self.send_signal(widget.Inputs.data, self.dataA)\n        self.send_signal(widget.Inputs.extra_data, self.dataA)\n        attr_pairs = [(INDEX, INDEX), (INSTANCEID, INSTANCEID),\n                      (domainA[0], domainA[1]), (domainA[1], domainA[0])]\n        boxes.set_state(attr_pairs)\n        boxes.emit_list()\n        self.assertEqual(widget.attr_pairs, attr_pairs)\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertEqual(widget.attr_pairs, [(INDEX, INDEX)])\n\n        self.send_signal(widget.Inputs.data, self.dataA)\n        self.assertEqual(widget.attr_pairs, attr_pairs)\n\n        self.send_signal(widget.Inputs.extra_data, self.dataB)\n        attr_pairs2 = [(domainA[0], domainB[0]), (domainA[1], domainB[1])]\n        boxes.set_state(attr_pairs2)\n        boxes.emit_list()\n        self.assertEqual(widget.attr_pairs, attr_pairs2)\n\n        self.send_signal(widget.Inputs.extra_data, self.dataA)\n        self.assertEqual(widget.attr_pairs, attr_pairs)\n\n    def test_migrate_settings(self):\n        def create_and_send(settings):\n            widget = self.create_widget(OWMergeData, stored_settings=settings)\n            for signal in (widget.Inputs.data, widget.Inputs.extra_data):\n                self.send_signal(signal, self.dataA)\n            return widget\n\n        domainA = self.dataA.domain\n        attr1, attr2, attr3 = domainA.variables\n        attr4, attr5 = domainA.metas\n\n        # Migration from version == None\n        orig_settings = dict(\n            attr_augment_data=attr1.name,\n            attr_augment_extra=attr2.name,\n            attr_merge_data=attr3.name,\n            attr_merge_extra=attr4.name,\n            attr_combine_data=attr5.name,\n            attr_combine_extra='Position (index)')\n\n        widget = create_and_send(dict(merging=0, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr1, attr2)]))\n\n        widget = create_and_send(dict(merging=1, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr3, attr4)]))\n\n        widget = create_and_send(dict(merging=2, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr5, attr5)]))\n\n        orig_settings[\"attr_combine_extra\"] = \"Source position (index)\"\n        widget = create_and_send(dict(merging=2, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr5, attr5)]))\n\n        # Migration from version 1\n        orig_settings = {\"attr_pairs\": (True, True, [[attr1.name, attr2.name],\n                                                     [attr3.name, attr4.name]]),\n                         \"__version__\": 1}\n        widget = create_and_send(orig_settings)\n        self.assertEqual(widget.attr_pairs, ([(attr1, attr2), (attr3, attr4)]))\n\n    def test_migrate_settings_attr_pairs_extra_none(self):\n        settings = {'attr_pairs': (True, False, [['sepal length', 0]])}\n        OWMergeData.migrate_settings(settings, 1)\n        self.assertListEqual(settings[\"context_settings\"], [])\n\n    def test_migrate_settings_attr_pairs_data_none(self):\n        settings = {'attr_pairs': (False, True, [[0, \"sepal length\"]])}\n        OWMergeData.migrate_settings(settings, 1)\n        self.assertListEqual(settings[\"context_settings\"], [])\n\n    def test_migrate_settings_attr_pairs_id_idx(self):\n        settings = {\"attr_pairs\": (True, True, [[0, 1]])}\n        OWMergeData.migrate_settings(settings, 1)\n        context = settings[\"context_settings\"][0]\n        self.assertListEqual(context.values[\"attr_pairs\"],\n                             [((INDEX, 100), (INSTANCEID, 100))])\n        self.assertDictEqual(context.variables1, {})\n        self.assertDictEqual(context.variables2, {})\n\n    def test_migrate_settings_attr_pairs_vars(self):\n        settings = {\"attr_pairs\": (True, True,\n                                   [[\"sepal length\", \"sepal width\"],\n                                    [\"petal length\", \"petal width\"]])}\n        OWMergeData.migrate_settings(settings, 1)\n        context = settings[\"context_settings\"][0]\n        self.assertListEqual(context.values[\"attr_pairs\"],\n                             [((\"sepal length\", 100), (\"sepal width\", 100)),\n                              ((\"petal length\", 100), (\"petal width\", 100))])\n        self.assertDictEqual(context.variables1, {})\n        self.assertDictEqual(context.variables2, {})\n\n    def test_report(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        var0, var1 = self.dataA.domain.attributes[:2]\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n        widget.send_report()\n        # Don't crash, that's it\n\n    def test_no_matches(self):\n        \"\"\"Check output is None when there are no matches in inner join\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        domA = self.dataA.domain\n        domB = self.dataB.domain\n\n        self.widget.attr_boxes.set_state([(domA[\"dA1\"], domB[\"dB2\"])])\n        self.widget.controls.merging.buttons[self.widget.LeftJoin].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.data))\n\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertIsNone(self.get_output(self.widget.Outputs.data))\n\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.data))\n\n    def test_output_merge_by_ids_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by\n        Source position (index)\"\"\"\n        domain = self.dataA.domain\n        result = Table(domain, np.array([[1, 1], [2, 0]]), np.array([1, 2]),\n                       np.array([[1.0, \"m2\"], [np.nan, \"m3\"]]).astype(object))\n        self.send_signal(self.widget.Inputs.data, self.dataA[:3, [0, \"clsA\", -1]])\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA[1:, [1, \"clsA\", -2]])\n        self.widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_ids_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        Source position (index)\"\"\"\n        domainA = self.dataA.domain\n        values = domainA.class_var.values\n        domain = Domain(domainA.attributes,\n                        (DiscreteVariable(\"clsA (1)\", values),\n                         DiscreteVariable(\"clsA (2)\", values)),\n                        domainA.metas)\n        result = Table(domain,\n                       np.array([[1, 1], [2, 0], [3, np.nan], [np.nan, 0]]),\n                       np.array([[1, 1], [2, 2], [np.nan, np.nan], [np.nan, 0]]),\n                       np.array([[1.0, \"m2\"], [np.nan, \"m3\"],\n                                 [0.0, \"\"], [np.nan, \"m1\"]]).astype(object))\n        self.widget.merging = 2\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        self.send_signal(self.widget.Inputs.data, self.dataA[1:, [0, \"clsA\", -1]])\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA[:3, [1, \"clsA\", -2]])\n        self.widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        self.widget.attr_boxes.emit_list()\n        out = self.get_output(self.widget.Outputs.data)\n        self.assertTablesEqual(out, result)\n        np.testing.assert_equal(\n            out.ids, np.hstack((self.dataA.ids[1:], self.dataA.ids[:1])))\n\n    def test_output_merge_by_ids_outer_single_class(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        Source position (index) when all extra rows are matched and there is\n        only a single class variable in the output\"\"\"\n        domainA = self.dataA.domain\n        values = domainA.class_var.values\n        domain = Domain(domainA.attributes,\n                        DiscreteVariable(\"clsA\", values),\n                        domainA.metas)\n        result = Table(domain,\n                       np.array([[0, 0], [1, 1], [2, 0], [3, np.nan]]),\n                       np.array([[0], [1], [2], [np.nan]]),\n                       np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                                 [0.0, \"\"]]).astype(object))\n        self.widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        self.widget.merging = 2\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        # When Y is a single column, Table.Y returns a vector, not a 2d array,\n        # which cause an exception in outer_join's vstack for Y if extra data\n        # has no unmatched rows.\n        # This test also checks this condition.\n        self.send_signal(self.widget.Inputs.data, self.dataA[:, [0, \"clsA\", -1]])\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA[:3, [1, -2]])\n        out = self.get_output(self.widget.Outputs.data)\n        self.assertTablesEqual(out, result)\n        np.testing.assert_equal(out.ids, self.dataA.ids)\n\n    def test_output_merge_by_index_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        Position (index)\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1],\n                             [2, 0, 2, np.nan], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]\n                            ]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_index_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by\n        Position (index)\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1], [2, 0, 2, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.controls.merging.buttons[1].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_index_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        Position (index)\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1],\n                             [2, 0, 2, np.nan], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.controls.merging.buttons[2].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_attribute_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        attribute\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes[1:],\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0], [1, 1, 1],\n                             [2, 0, np.nan], [3, 1, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[0], domainB[0])])\n        self.widget.commit()\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertTablesEqual(output, result)\n        self.assertEqual(output.name, self.dataA.name)\n        np.testing.assert_array_equal(output.ids, self.dataA.ids)\n        self.assertEqual(output.attributes, self.dataA.attributes)\n\n    def test_output_merge_by_attribute_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by attribute\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes[1:],\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0], [1, 1, 1], [2, 0, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[0], domainB[0])])\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_attribute_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        attribute\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1],\n                             [2, 0, 2, np.nan], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[0], domainB[0])])\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_attribute_outer_same_attr(self):\n        \"\"\"Values of columns from extra aata are copied to left part if they\n        match\"\"\"\n        name = StringVariable(\"name\")\n        domainA = Domain([ContinuousVariable(\"x\")], None, [name])\n        domainB = Domain([ContinuousVariable(\"y\")], None, [name])\n        xA = np.array([[0], [1], [2]])\n        mA = np.array([[\"a\"], [\"b\"], [\"c\"]])\n        xB = np.array([[4], [5], [6], [7]])\n        mB = np.array([[\"b\"], [\"d\"], [\"a\"], [\"c\"]])\n        dataA = Table(domainA, xA, None, mA)\n        dataB = Table(domainB, xB, None, mB)\n\n        self.send_signal(self.widget.Inputs.data, dataA)\n        self.send_signal(self.widget.Inputs.extra_data, dataB)\n        self.widget.attr_boxes.set_state([(name, name)])\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        out = self.get_output(self.widget.Outputs.data)\n        np.testing.assert_equal(\n            out.X,\n            np.array([[0, 6], [1, 4], [2, 7], [np.nan, 5]]))\n        self.assertEqual(\" \".join(out.metas.flatten()), \"a a b b c c  d\")\n\n    def test_output_merge_by_class_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        class variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars, domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, 2, np.nan],\n                             [2, 0, 1, 1], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([0, 1, 2, np.nan])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 0.0],\n                             [np.nan, \"m3\", 1.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[2], domainB[2])])\n        self.widget.commit()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_class_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by class\n        variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars, domainA.metas + domainB.metas)\n        result_X = np.array([[1, 1, 2, np.nan], [2, 0, 1, 1]])\n        result_Y = np.array([1, 2])\n        result_M = np.array([[1.0, \"m2\", 0.0], [np.nan, \"m3\", 1.0]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state(\n            [(domainA.class_vars[0], domainB.class_vars[0])])\n        self.widget.controls.merging.buttons[1].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_class_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        class variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, 2, np.nan],\n                             [2, 0, 1, 1], [3, 1, np.nan, np.nan],\n                             [np.nan, np.nan, 0, 0]])\n        result_Y = np.array([[0, np.nan], [1, 0], [2, 1], [np.nan, np.nan],\n                             [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 0.0],\n                             [np.nan, \"m3\", 1.0], [0.0, \"m4\", np.nan],\n                             [np.nan, \"\", np.nan]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state(\n            [(domainA.class_vars[0], domainB.class_vars[0])])\n        self.widget.controls.merging.buttons[2].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_meta_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        meta variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, np.nan, np.nan],\n                             [2, 0, np.nan, np.nan], [3, 1, 2, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, np.nan],\n                             [2, np.nan], [np.nan, 0]])\n        result_M = np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                             [0.0, \"m4\"]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[-2], domainB[-1])])\n        self.widget.commit()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_meta_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by meta\n        variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas)\n        result_X = np.array([[3, 1, 2, np.nan]])\n        result_Y = np.array([[np.nan, 0]])\n        result_M = np.array([[0.0, \"m4\"]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[-2], domainB[-1])])\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_meta_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        meta variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, np.nan, np.nan],\n                             [2, 0, np.nan, np.nan], [3, 1, 2, np.nan],\n                             [np.nan, np.nan, 0, 0], [np.nan, np.nan, 1, 1]])\n        result_Y = np.array([[0, np.nan], [1, np.nan], [2, np.nan],\n                             [np.nan, 0], [np.nan, np.nan], [np.nan, 1]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", np.nan],\n                             [np.nan, \"m3\", np.nan], [0.0, \"m4\", 0.0],\n                             [np.nan, \"\", np.nan], [np.nan, \"\", 1.0]]\n                           ).astype(object)\n\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[-2], domainB[-1])])\n        self.widget.controls.merging.buttons[2].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def assertTablesEqual(self, table1, table2):\n        self.assertEqual(table1.domain, table2.domain)\n        np.testing.assert_array_equal(table1.X, table2.X)\n        np.testing.assert_array_equal(table1.Y, table2.Y)\n        np.testing.assert_array_equal(table1.metas.astype(str),\n                                      table2.metas.astype(str))\n\n    def test_best_match(self):\n        \"\"\"Check default merging attributes setup\"\"\"\n        widget = self.widget\n        indices = list(range(101))\n        indices.pop(26)\n        zoo = Table(\"zoo\")[indices]\n        zoo_images = Table(test_filename(\"datasets/zoo-with-images.tab\"))\n        self.send_signal(widget.Inputs.data, zoo)\n        self.send_signal(widget.Inputs.extra_data, zoo_images)\n        for i in range(3):\n            self.assertEqual(widget.attr_boxes.current_state(),\n                             [(zoo.domain[\"name\"], zoo_images.domain[\"name\"])],\n                             f\"wrong attributes chosen for merge_type={i}\")\n\n    def test_sparse(self):\n        data = Table(\"iris\")[::25]\n        data_ed_dense = Table(\"titanic\")[::300]\n        data_ed_sparse = Table(\"titanic\")[::300].to_sparse()\n        self.send_signal(\"Data\", data)\n\n        self.send_signal(\"Extra Data\", data_ed_dense)\n        output_dense = self.get_output(\"Data\")\n        self.assertFalse(sp.issparse(output_dense.X))\n        self.assertFalse(output_dense.is_sparse())\n\n        self.send_signal(\"Extra Data\", data_ed_sparse)\n        output_sparse = self.get_output(\"Data\")\n        self.assertTrue(sp.issparse(output_sparse.X))\n        self.assertTrue(output_sparse.is_sparse())\n\n        output_sparse.X = output_sparse.X.toarray()\n        self.assertTablesEqual(output_dense, output_sparse)\n\n    def test_commit_on_new_data(self):\n        \"\"\"Check that disabling auto apply doesn't block on new data\"\"\"\n        self.widget.auto_apply = False\n        self.widget.merging = 2\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.data))\n\n    def test_multiple_attributes_left(self):\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n\n        X = np.array([[0, 1], [1, 1], [3, np.nan], [np.nan, 0]])\n        Y = np.array([1, 2, 0, 0])\n        metas = np.array([[0, \"a\"], [1, \"b\"], [0, \"c\"], [0, \"d\"]])\n        dataA = Table(domainA, X, Y, metas)\n        dataA.name = \"dataA\"\n\n        X = np.array(\n            [[0, 0], [1, 0], [0, 1], [1, 1], [2, 0], [3, np.nan]])\n        Y = np.array([0, 0, 1, 1, 0, 1])\n        metas = np.array([[0], [1], [0], [1], [1], [1]])\n        dataB = Table(domainB, X, Y, metas)\n        dataB.name = \"dataB\"\n\n        self.send_signal(self.widget.Inputs.data, dataA)\n        self.send_signal(self.widget.Inputs.extra_data, dataB)\n        self.widget.attr_boxes.set_state(\n            [(domainA[0], domainB[0]), (domainA[1], domainB[1])])\n        self.widget.commit()\n        output = self.get_output(self.widget.Outputs.data)\n\n        self.assertEqual(output.name, dataA.name)\n        np.testing.assert_array_equal(output.ids, dataA.ids)\n        self.assertEqual(output.attributes, dataA.attributes)\n        np.testing.assert_equal(output.X, dataA.X)\n\n    def test_nonunique(self):\n        widget = self.widget\n        x = ContinuousVariable(\"x\")\n        d = DiscreteVariable(\"d\", values=tuple(\"abc\"))\n        domain = Domain([x, d], [])\n        dataA = Table.from_numpy(\n            domain, np.array([[1.0, 0], [1, 1], [2, 1]]))\n        dataB = Table.from_numpy(\n            domain, np.array([[1.0, 0], [2, 1], [3, 1]]))\n        dataB.ids = dataA.ids\n        self.send_signal(widget.Inputs.data, dataA)\n        self.send_signal(widget.Inputs.extra_data, dataB)\n        widget.merging = widget.InnerJoin\n\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n\n        widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.attr_boxes.set_state([(INDEX, INDEX)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.attr_boxes.set_state([(x, x)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.LeftJoin\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.InnerJoin\n        widget.attr_boxes.set_state([(x, x), (d, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.attr_boxes.set_state([(d, d)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.nonunique_left.is_shown())\n        self.assertTrue(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.LeftJoin\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertTrue(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.InnerJoin\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.nonunique_left.is_shown())\n        self.assertTrue(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        self.send_signal(widget.Inputs.data, None)\n        self.send_signal(widget.Inputs.extra_data, None)\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n    def test_invalide_pairs(self):\n        widget = self.widget\n        x = ContinuousVariable(\"x\")\n        d = DiscreteVariable(\"d\", values=tuple(\"abc\"))\n        domain = Domain([x, d], [])\n        dataA = Table.from_numpy(\n            domain, np.array([[1.0, 0], [1, 1], [2, 1]]))\n        dataB = Table.from_numpy(\n            domain, np.array([[1.0, 0], [2, 1], [3, 1]]))\n        dataB.ids = dataA.ids\n        self.send_signal(widget.Inputs.data, dataA)\n        self.send_signal(widget.Inputs.extra_data, dataB)\n\n        widget.attr_boxes.set_state([(x, x), (d, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (INDEX, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertTrue(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (d, INDEX)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertTrue(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (INSTANCEID, d)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (d, INSTANCEID)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (INDEX, INSTANCEID)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.matching_id_with_sth.is_shown()\n                        or widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (x, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertTrue(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n    def test_duplicate_names(self):\n        domain = Domain([ContinuousVariable(\"C1\")],\n                        metas=[DiscreteVariable(\"Feature\", values=(\"A\", \"B\"))])\n        data = Table(domain, np.array([[1.], [0.]]),\n                     metas=np.array([[1.], [0.]]))\n        domain = Domain([ContinuousVariable(\"C1\")],\n                        metas=[StringVariable(\"Feature\")])\n        extra_data = Table(domain, np.array([[1.], [0.]]),\n                           metas=np.array([[\"A\"], [\"B\"]]))\n        self.send_signal(self.widget.Inputs.data, data)\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        self.assertTrue(self.widget.Warning.renamed_vars.is_shown())\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.metas],\n                             [\"Feature (1)\", \"Feature (2)\"])\n\n    def test_keep_non_duplicate_variables(self):\n        domain = Domain([ContinuousVariable(\"A\"), ContinuousVariable(\"B\")])\n        data = Table(domain, np.array([[0., 0], [0, 1]]))\n        extra_data = Table(domain, np.array([[0., 1], [0, 1]]))\n        self.send_signal(self.widget.Inputs.data, data)\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.variables],\n                             [\"A\", \"B (1)\", \"B (2)\"])\n\n    def test_keep_non_duplicate_variables_missing_rows(self):\n        c = DiscreteVariable(\"C\", values=(\"a\", \"b\", \"c\"))\n        domain = Domain([ContinuousVariable(\"A\"), ContinuousVariable(\"B\"), c])\n        data = Table(domain, np.array([[0., 0, 0], [1, 1, 1]]))\n        extra_data = Table(domain, np.array([[0., 1, 1], [0, 1, 2]]))\n        self.send_signal(self.widget.Inputs.data, data)\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        self.widget.attr_boxes.set_state([(c, c)])\n        self.widget.attr_boxes.emit_list()\n\n        # Only one row is matched; A has different values and it's duplicated,\n        # and B has the same values, so we get only one copy\n        self.widget.merging = self.widget.InnerJoin\n        self.widget.unconditional_commit()\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.variables],\n                             [\"A (1)\", \"B\", \"C\", \"A (2)\"])\n\n        # Table has additional rows; keep all columns\n        self.widget.merging = self.widget.OuterJoin\n        self.widget.unconditional_commit()\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual(\n            [m.name for m in merged_data.domain.variables],\n            [\"A (1)\", \"B (1)\", \"C (1)\", \"A (2)\", \"B (2)\", \"C (2)\"])\n\n        # First row is unmatched, data for B(2) is missing, but attribute\n        # shouldn't be added\n        extra_data = Table(domain, np.array([[1., 1, 1], [0, 1, 2]]))\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        self.widget.merging = self.widget.LeftJoin\n        self.widget.unconditional_commit()\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.variables],\n                             [\"A\", \"B\", \"C\"])\n\n    def test_summary(self):\n        \"\"\"Check if the status bar is updated when data is received\"\"\"\n        data = self.dataA\n        info = self.widget.info\n        no_input, no_output = \"No data on input\", \"No data on output\"\n\n        self.send_signal(self.widget.Inputs.data, data)\n        data_list = [(\"Data\", data), (\"Extra data\", None)]\n        summary, details = f\"{len(data)}, 0\", format_multiple_summaries(data_list)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.assertIsInstance(info._StateInfo__output_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n        self.send_signal(self.widget.Inputs.extra_data, data)\n        data_list = [(\"Data\", data), (\"Extra data\", data)]\n        summary = f\"{len(data)}, {len(data)}\"\n        details = format_multiple_summaries(data_list)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        output = self.get_output(self.widget.Outputs.data)\n        summary, details = f\"{len(output)}\", format_summary_details(output)\n        self.assertEqual(info._StateInfo__output_summary.brief, summary)\n        self.assertEqual(info._StateInfo__output_summary.details, details)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        data_list = [(\"Data\", None), (\"Extra data\", data)]\n        summary, details = f\"0, {len(data)}\", format_multiple_summaries(data_list)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.assertIsInstance(info._StateInfo__output_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n        self.send_signal(self.widget.Inputs.extra_data, None)\n        self.assertIsInstance(info._StateInfo__input_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__input_summary.details, no_input)\n        self.assertIsInstance(info._StateInfo__output_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n    def test_empty_tables(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.dataA[:0])\n        self.send_signal(widget.Inputs.extra_data, self.dataB[:0])",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 27,
                            "end_lineno": 1069,
                            "parent": null
                        }
                    ],
                    "added_entities": [
                        {
                            "file_name": "Orange/widgets/data/tests/test_owmergedata.py",
                            "type": "method",
                            "name": "TestOWMergeData.test_empty_tables",
                            "content": "class TestOWMergeData(WidgetTest):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        domainA = Domain([DiscreteVariable(\"dA1\", (\"a\", \"b\", \"c\", \"d\")),\n                          DiscreteVariable(\"dA2\", (\"aa\", \"bb\"))],\n                         DiscreteVariable(\"clsA\", (\"aaa\", \"bbb\", \"ccc\")),\n                         [DiscreteVariable(\"mA1\", (\"cc\", \"dd\")),\n                          StringVariable(\"mA2\")])\n        XA = np.array([[0, 0], [1, 1], [2, 0], [3, 1]])\n        yA = np.array([0, 1, 2, np.nan])\n        metasA = np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                           [0.0, \"m4\"]]).astype(object)\n\n        domainB = Domain([DiscreteVariable(\"dB1\", values=(\"a\", \"b\", \"c\")),\n                          DiscreteVariable(\"dB2\", values=(\"aa\", \"bb\"))],\n                         DiscreteVariable(\"clsB\", values=(\"bbb\", \"ccc\")),\n                         [DiscreteVariable(\"mB1\", (\"m4\", \"m5\"))])\n        XB = np.array([[0, 0], [1, 1], [2, np.nan]])\n        yB = np.array([np.nan, 1, 0])\n        metasB = np.array([[np.nan], [1], [0]]).astype(object)\n        cls.dataA = Table(domainA, XA, yA, metasA)\n        cls.dataA.name = 'dataA'\n        cls.dataA.attributes = 'dataA attributes'\n        cls.dataB = Table(domainB, XB, yB, metasB)\n        cls.dataB.name = 'dataB'\n        cls.dataB.attributes = 'dataB attributes'\n\n    def setUp(self):\n        self.widget = self.create_widget(OWMergeData)\n\n    def test_input_remove(self):\n        \"\"\"Check widget after inputs have been removed\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.send_signal(self.widget.Inputs.extra_data, None)\n\n    def test_combobox_items(self):\n        \"\"\"Check if combo box content is properly set\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        row = self.widget.attr_boxes.rows[0]\n        data_combo, extra_combo = row.left_combo, row.right_combo\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n        data_items = extra_items = list(\n            chain([INDEX, INSTANCEID], domainA.variables, domainA.metas))\n        self.assertListEqual(data_combo.model()[:], data_items)\n        self.assertListEqual(extra_combo.model()[:], extra_items)\n\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        data_items = list(\n            chain([INDEX, INSTANCEID], domainA.variables, domainA.metas))\n        extra_items = list(\n            chain([INDEX, INSTANCEID], domainB.variables, domainB.metas))\n        self.assertListEqual(data_combo.model()[:], data_items)\n        self.assertListEqual(extra_combo.model()[:], extra_items)\n\n        self.send_signal(self.widget.Inputs.data, self.dataB)\n        data_items = extra_items = list(\n            chain([INDEX, INSTANCEID], domainB.variables, domainB.metas))\n        self.assertListEqual(data_combo.model()[:], data_items)\n        self.assertListEqual(extra_combo.model()[:], extra_items)\n\n    def test_combo_box_sync(self):\n        row = self.widget.attr_boxes.rows[0]\n        data_combo, extra_combo = row.left_combo, row.right_combo\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        extra_combo.setCurrentIndex(3)\n        data_combo.setCurrentIndex(0)\n        data_combo.activated.emit(0)\n        self.assertEqual(data_combo.currentIndex(), 0)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        data_combo.setCurrentIndex(1)\n        data_combo.activated.emit(1)\n        self.assertEqual(data_combo.currentIndex(), 1)\n        self.assertEqual(extra_combo.currentIndex(), 1)\n\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        self.assertEqual(data_combo.currentIndex(), 0)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        extra_combo.setCurrentIndex(1)\n        extra_combo.activated.emit(1)\n        self.assertEqual(data_combo.currentIndex(), 1)\n        self.assertEqual(extra_combo.currentIndex(), 1)\n\n    def test_attr_combo_tooltips(self):\n        row = self.widget.attr_boxes.rows[0]\n        model = row.left_combo.model()\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        tip = model.data(model.index(2, 0), Qt.ToolTipRole)\n        # Test the test; if general tooltips ever change and the following\n        # assert fails, the rest of this test has to be modified accordingly\n        self.assertTrue(tip.startswith(\"<b>\"))\n\n        # Just test that tooltip is a string (implicitly) and that it's not\n        # a generic DomainModel tooltip\n        tip = model.data(model.index(0, 0), Qt.ToolTipRole)\n        self.assertFalse(tip.startswith(\"<b>\"))\n\n        tip = model.data(model.index(1, 0), Qt.ToolTipRole)\n        self.assertFalse(tip.startswith(\"<b>\"))\n\n    def test_match_attr_name(self):\n        widget = self.widget\n        row = widget.attr_boxes.rows[0]\n        data_combo, extra_combo = row.left_combo, row.right_combo\n\n        domainA = Domain([DiscreteVariable(\"dA1\", (\"a\", \"b\", \"c\", \"d\")),\n                          DiscreteVariable(\"dA2\", (\"aa\", \"bb\")),\n                          DiscreteVariable(\"dA3\", (\"aa\", \"bb\"))],\n                         DiscreteVariable(\"cls\", (\"aaa\", \"bbb\", \"ccc\")),\n                         [DiscreteVariable(\"mA1\", (\"cc\", \"dd\")),\n                          StringVariable(\"mA2\")])\n        XA = np.array([[0, 0, 0], [1, 1, 0], [2, 0, 0], [3, 1, 0]])\n        yA = np.array([0, 1, 2, np.nan])\n        metasA = np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                           [0.0, \"m4\"]]).astype(object)\n\n        domainB = Domain([DiscreteVariable(\"dB1\", values=(\"a\", \"b\", \"c\")),\n                          ContinuousVariable(\"dA2\")],\n                         None,\n                         [StringVariable(\"cls\"),\n                          DiscreteVariable(\"dA1\", (\"m4\", \"m5\"))])\n        XB = np.array([[0, 0], [1, 1], [2, np.nan]])\n        yB = np.empty((3, 0))\n        metasB = np.array([[np.nan, np.nan], [1, 1], [0, 0]]).astype(object)\n        dataA = Table(domainA, XA, yA, metasA)\n        dataA.name = 'dataA'\n        dataA.attributes = 'dataA attributes'\n        dataB = Table(domainB, XB, yB, metasB)\n        dataB.name = 'dataB'\n        dataB.attributes = 'dataB attributes'\n\n        self.send_signal(widget.Inputs.data, dataA)\n        self.send_signal(widget.Inputs.extra_data, dataB)\n\n        # match variable if available and the other combo is Row Index\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n        self.assertEqual(extra_combo.currentIndex(), 5)\n\n        # match variable if available and the other combo is ID\n        extra_combo.setCurrentIndex(1)\n        extra_combo.activated.emit(1)\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n        self.assertEqual(extra_combo.currentIndex(), 5)\n\n        # don't match variable if other combo is set\n        extra_combo.setCurrentIndex(4)\n        extra_combo.activated.emit(4)\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n        self.assertEqual(extra_combo.currentIndex(), 4)\n\n        # don't match if nothing to match to\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(4)\n        data_combo.activated.emit(4)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        # don't match numeric with non-numeric\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(3)\n        data_combo.activated.emit(3)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        # allow matching string with discrete\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(5)\n        data_combo.activated.emit(5)\n        self.assertEqual(extra_combo.currentIndex(), 4)\n\n    def test_add_row_button(self):\n        boxes = self.widget.attr_boxes\n        boxes.set_state([(INSTANCEID, INSTANCEID), (INSTANCEID, INSTANCEID)])\n        boxes.rows[-1].add_button.clicked.emit()\n        self.assertEqual(len(boxes.rows), 3)\n        self.assertEqual(boxes.layout().count(), 3)\n\n    def test_remove_row(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        var0, var1 = self.dataA.domain.attributes[:2]\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n        for i, row in enumerate(boxes.rows):\n            self.assertTrue(row.remove_button.isEnabled())\n            self.assertEqual(row.remove_button.text(), \"×\")\n            self.assertEqual(row.add_button.isEnabled(), i == 2)\n            self.assertEqual(row.add_button.text(), [\"\", \"+\"][i == 2])\n\n        boxes.rows[1].remove_button.clicked.emit()\n        self.assertEqual(boxes.current_state(), [(INDEX, INDEX), (var0, var1)])\n        for i, row in enumerate(boxes.rows):\n            self.assertTrue(row.remove_button.isEnabled())\n            self.assertEqual(row.remove_button.text(), \"×\")\n            self.assertEqual(row.add_button.isEnabled(), i == 1)\n            self.assertEqual(row.add_button.text(), [\"\", \"+\"][i])\n\n        boxes.rows[1].remove_button.clicked.emit()\n        self.assertEqual(boxes.current_state(), [(INDEX, INDEX)])\n        row = boxes.rows[0]\n        self.assertFalse(row.remove_button.isEnabled())\n        self.assertEqual(row.remove_button.text(), \"\")\n        self.assertTrue(row.add_button.isEnabled())\n        self.assertEqual(row.add_button.text(), \"+\")\n\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n        boxes.rows[2].remove_button.clicked.emit()\n        self.assertEqual(\n            boxes.current_state(), [(INDEX, INDEX), (INSTANCEID, INSTANCEID)])\n        for i, row in enumerate(boxes.rows):\n            self.assertTrue(row.remove_button.isEnabled())\n            self.assertEqual(row.remove_button.text(), \"×\")\n            self.assertEqual(row.add_button.isEnabled(), i == 1)\n            self.assertEqual(row.add_button.text(), [\"\", \"+\"][i == 1])\n\n    def test_dont_remove_single_row(self):\n        widget = self.widget\n        rows = widget.attr_boxes.rows\n        self.assertEqual(len(rows), 1)\n        rows[0].remove_button.clicked.emit()\n        self.assertEqual(len(rows), 1)\n\n    def test_retrieve_settings(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        var0, var1 = self.dataA.domain.attributes[:2]\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n\n        settings = widget.settingsHandler.pack_data(widget)\n\n        widget2 = self.create_widget(OWMergeData, stored_settings=settings)\n        widget2.attr_boxes.set_state([(INDEX, INDEX)])\n        self.send_signals(\n            [(widget2.Inputs.data, self.dataA),\n             (widget2.Inputs.extra_data, self.dataA)],\n            widget=widget2)\n        self.assertEqual(\n            widget2.attr_boxes.current_state(),\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n\n    def test_match_settings(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        domainA = self.dataA.domain\n        domainB = self.dataB.domain\n\n        self.send_signal(widget.Inputs.data, self.dataA)\n        self.send_signal(widget.Inputs.extra_data, self.dataA)\n        attr_pairs = [(INDEX, INDEX), (INSTANCEID, INSTANCEID),\n                      (domainA[0], domainA[1]), (domainA[1], domainA[0])]\n        boxes.set_state(attr_pairs)\n        boxes.emit_list()\n        self.assertEqual(widget.attr_pairs, attr_pairs)\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertEqual(widget.attr_pairs, [(INDEX, INDEX)])\n\n        self.send_signal(widget.Inputs.data, self.dataA)\n        self.assertEqual(widget.attr_pairs, attr_pairs)\n\n        self.send_signal(widget.Inputs.extra_data, self.dataB)\n        attr_pairs2 = [(domainA[0], domainB[0]), (domainA[1], domainB[1])]\n        boxes.set_state(attr_pairs2)\n        boxes.emit_list()\n        self.assertEqual(widget.attr_pairs, attr_pairs2)\n\n        self.send_signal(widget.Inputs.extra_data, self.dataA)\n        self.assertEqual(widget.attr_pairs, attr_pairs)\n\n    def test_migrate_settings(self):\n        def create_and_send(settings):\n            widget = self.create_widget(OWMergeData, stored_settings=settings)\n            for signal in (widget.Inputs.data, widget.Inputs.extra_data):\n                self.send_signal(signal, self.dataA)\n            return widget\n\n        domainA = self.dataA.domain\n        attr1, attr2, attr3 = domainA.variables\n        attr4, attr5 = domainA.metas\n\n        # Migration from version == None\n        orig_settings = dict(\n            attr_augment_data=attr1.name,\n            attr_augment_extra=attr2.name,\n            attr_merge_data=attr3.name,\n            attr_merge_extra=attr4.name,\n            attr_combine_data=attr5.name,\n            attr_combine_extra='Position (index)')\n\n        widget = create_and_send(dict(merging=0, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr1, attr2)]))\n\n        widget = create_and_send(dict(merging=1, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr3, attr4)]))\n\n        widget = create_and_send(dict(merging=2, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr5, attr5)]))\n\n        orig_settings[\"attr_combine_extra\"] = \"Source position (index)\"\n        widget = create_and_send(dict(merging=2, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr5, attr5)]))\n\n        # Migration from version 1\n        orig_settings = {\"attr_pairs\": (True, True, [[attr1.name, attr2.name],\n                                                     [attr3.name, attr4.name]]),\n                         \"__version__\": 1}\n        widget = create_and_send(orig_settings)\n        self.assertEqual(widget.attr_pairs, ([(attr1, attr2), (attr3, attr4)]))\n\n    def test_migrate_settings_attr_pairs_extra_none(self):\n        settings = {'attr_pairs': (True, False, [['sepal length', 0]])}\n        OWMergeData.migrate_settings(settings, 1)\n        self.assertListEqual(settings[\"context_settings\"], [])\n\n    def test_migrate_settings_attr_pairs_data_none(self):\n        settings = {'attr_pairs': (False, True, [[0, \"sepal length\"]])}\n        OWMergeData.migrate_settings(settings, 1)\n        self.assertListEqual(settings[\"context_settings\"], [])\n\n    def test_migrate_settings_attr_pairs_id_idx(self):\n        settings = {\"attr_pairs\": (True, True, [[0, 1]])}\n        OWMergeData.migrate_settings(settings, 1)\n        context = settings[\"context_settings\"][0]\n        self.assertListEqual(context.values[\"attr_pairs\"],\n                             [((INDEX, 100), (INSTANCEID, 100))])\n        self.assertDictEqual(context.variables1, {})\n        self.assertDictEqual(context.variables2, {})\n\n    def test_migrate_settings_attr_pairs_vars(self):\n        settings = {\"attr_pairs\": (True, True,\n                                   [[\"sepal length\", \"sepal width\"],\n                                    [\"petal length\", \"petal width\"]])}\n        OWMergeData.migrate_settings(settings, 1)\n        context = settings[\"context_settings\"][0]\n        self.assertListEqual(context.values[\"attr_pairs\"],\n                             [((\"sepal length\", 100), (\"sepal width\", 100)),\n                              ((\"petal length\", 100), (\"petal width\", 100))])\n        self.assertDictEqual(context.variables1, {})\n        self.assertDictEqual(context.variables2, {})\n\n    def test_report(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        var0, var1 = self.dataA.domain.attributes[:2]\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n        widget.send_report()\n        # Don't crash, that's it\n\n    def test_no_matches(self):\n        \"\"\"Check output is None when there are no matches in inner join\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        domA = self.dataA.domain\n        domB = self.dataB.domain\n\n        self.widget.attr_boxes.set_state([(domA[\"dA1\"], domB[\"dB2\"])])\n        self.widget.controls.merging.buttons[self.widget.LeftJoin].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.data))\n\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertIsNone(self.get_output(self.widget.Outputs.data))\n\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.data))\n\n    def test_output_merge_by_ids_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by\n        Source position (index)\"\"\"\n        domain = self.dataA.domain\n        result = Table(domain, np.array([[1, 1], [2, 0]]), np.array([1, 2]),\n                       np.array([[1.0, \"m2\"], [np.nan, \"m3\"]]).astype(object))\n        self.send_signal(self.widget.Inputs.data, self.dataA[:3, [0, \"clsA\", -1]])\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA[1:, [1, \"clsA\", -2]])\n        self.widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_ids_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        Source position (index)\"\"\"\n        domainA = self.dataA.domain\n        values = domainA.class_var.values\n        domain = Domain(domainA.attributes,\n                        (DiscreteVariable(\"clsA (1)\", values),\n                         DiscreteVariable(\"clsA (2)\", values)),\n                        domainA.metas)\n        result = Table(domain,\n                       np.array([[1, 1], [2, 0], [3, np.nan], [np.nan, 0]]),\n                       np.array([[1, 1], [2, 2], [np.nan, np.nan], [np.nan, 0]]),\n                       np.array([[1.0, \"m2\"], [np.nan, \"m3\"],\n                                 [0.0, \"\"], [np.nan, \"m1\"]]).astype(object))\n        self.widget.merging = 2\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        self.send_signal(self.widget.Inputs.data, self.dataA[1:, [0, \"clsA\", -1]])\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA[:3, [1, \"clsA\", -2]])\n        self.widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        self.widget.attr_boxes.emit_list()\n        out = self.get_output(self.widget.Outputs.data)\n        self.assertTablesEqual(out, result)\n        np.testing.assert_equal(\n            out.ids, np.hstack((self.dataA.ids[1:], self.dataA.ids[:1])))\n\n    def test_output_merge_by_ids_outer_single_class(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        Source position (index) when all extra rows are matched and there is\n        only a single class variable in the output\"\"\"\n        domainA = self.dataA.domain\n        values = domainA.class_var.values\n        domain = Domain(domainA.attributes,\n                        DiscreteVariable(\"clsA\", values),\n                        domainA.metas)\n        result = Table(domain,\n                       np.array([[0, 0], [1, 1], [2, 0], [3, np.nan]]),\n                       np.array([[0], [1], [2], [np.nan]]),\n                       np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                                 [0.0, \"\"]]).astype(object))\n        self.widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        self.widget.merging = 2\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        # When Y is a single column, Table.Y returns a vector, not a 2d array,\n        # which cause an exception in outer_join's vstack for Y if extra data\n        # has no unmatched rows.\n        # This test also checks this condition.\n        self.send_signal(self.widget.Inputs.data, self.dataA[:, [0, \"clsA\", -1]])\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA[:3, [1, -2]])\n        out = self.get_output(self.widget.Outputs.data)\n        self.assertTablesEqual(out, result)\n        np.testing.assert_equal(out.ids, self.dataA.ids)\n\n    def test_output_merge_by_index_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        Position (index)\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1],\n                             [2, 0, 2, np.nan], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]\n                            ]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_index_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by\n        Position (index)\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1], [2, 0, 2, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.controls.merging.buttons[1].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_index_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        Position (index)\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1],\n                             [2, 0, 2, np.nan], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.controls.merging.buttons[2].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_attribute_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        attribute\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes[1:],\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0], [1, 1, 1],\n                             [2, 0, np.nan], [3, 1, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[0], domainB[0])])\n        self.widget.commit()\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertTablesEqual(output, result)\n        self.assertEqual(output.name, self.dataA.name)\n        np.testing.assert_array_equal(output.ids, self.dataA.ids)\n        self.assertEqual(output.attributes, self.dataA.attributes)\n\n    def test_output_merge_by_attribute_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by attribute\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes[1:],\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0], [1, 1, 1], [2, 0, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[0], domainB[0])])\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_attribute_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        attribute\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1],\n                             [2, 0, 2, np.nan], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[0], domainB[0])])\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_attribute_outer_same_attr(self):\n        \"\"\"Values of columns from extra aata are copied to left part if they\n        match\"\"\"\n        name = StringVariable(\"name\")\n        domainA = Domain([ContinuousVariable(\"x\")], None, [name])\n        domainB = Domain([ContinuousVariable(\"y\")], None, [name])\n        xA = np.array([[0], [1], [2]])\n        mA = np.array([[\"a\"], [\"b\"], [\"c\"]])\n        xB = np.array([[4], [5], [6], [7]])\n        mB = np.array([[\"b\"], [\"d\"], [\"a\"], [\"c\"]])\n        dataA = Table(domainA, xA, None, mA)\n        dataB = Table(domainB, xB, None, mB)\n\n        self.send_signal(self.widget.Inputs.data, dataA)\n        self.send_signal(self.widget.Inputs.extra_data, dataB)\n        self.widget.attr_boxes.set_state([(name, name)])\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        out = self.get_output(self.widget.Outputs.data)\n        np.testing.assert_equal(\n            out.X,\n            np.array([[0, 6], [1, 4], [2, 7], [np.nan, 5]]))\n        self.assertEqual(\" \".join(out.metas.flatten()), \"a a b b c c  d\")\n\n    def test_output_merge_by_class_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        class variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars, domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, 2, np.nan],\n                             [2, 0, 1, 1], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([0, 1, 2, np.nan])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 0.0],\n                             [np.nan, \"m3\", 1.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[2], domainB[2])])\n        self.widget.commit()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_class_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by class\n        variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars, domainA.metas + domainB.metas)\n        result_X = np.array([[1, 1, 2, np.nan], [2, 0, 1, 1]])\n        result_Y = np.array([1, 2])\n        result_M = np.array([[1.0, \"m2\", 0.0], [np.nan, \"m3\", 1.0]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state(\n            [(domainA.class_vars[0], domainB.class_vars[0])])\n        self.widget.controls.merging.buttons[1].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_class_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        class variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, 2, np.nan],\n                             [2, 0, 1, 1], [3, 1, np.nan, np.nan],\n                             [np.nan, np.nan, 0, 0]])\n        result_Y = np.array([[0, np.nan], [1, 0], [2, 1], [np.nan, np.nan],\n                             [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 0.0],\n                             [np.nan, \"m3\", 1.0], [0.0, \"m4\", np.nan],\n                             [np.nan, \"\", np.nan]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state(\n            [(domainA.class_vars[0], domainB.class_vars[0])])\n        self.widget.controls.merging.buttons[2].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_meta_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        meta variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, np.nan, np.nan],\n                             [2, 0, np.nan, np.nan], [3, 1, 2, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, np.nan],\n                             [2, np.nan], [np.nan, 0]])\n        result_M = np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                             [0.0, \"m4\"]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[-2], domainB[-1])])\n        self.widget.commit()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_meta_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by meta\n        variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas)\n        result_X = np.array([[3, 1, 2, np.nan]])\n        result_Y = np.array([[np.nan, 0]])\n        result_M = np.array([[0.0, \"m4\"]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[-2], domainB[-1])])\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_meta_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        meta variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, np.nan, np.nan],\n                             [2, 0, np.nan, np.nan], [3, 1, 2, np.nan],\n                             [np.nan, np.nan, 0, 0], [np.nan, np.nan, 1, 1]])\n        result_Y = np.array([[0, np.nan], [1, np.nan], [2, np.nan],\n                             [np.nan, 0], [np.nan, np.nan], [np.nan, 1]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", np.nan],\n                             [np.nan, \"m3\", np.nan], [0.0, \"m4\", 0.0],\n                             [np.nan, \"\", np.nan], [np.nan, \"\", 1.0]]\n                           ).astype(object)\n\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[-2], domainB[-1])])\n        self.widget.controls.merging.buttons[2].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def assertTablesEqual(self, table1, table2):\n        self.assertEqual(table1.domain, table2.domain)\n        np.testing.assert_array_equal(table1.X, table2.X)\n        np.testing.assert_array_equal(table1.Y, table2.Y)\n        np.testing.assert_array_equal(table1.metas.astype(str),\n                                      table2.metas.astype(str))\n\n    def test_best_match(self):\n        \"\"\"Check default merging attributes setup\"\"\"\n        widget = self.widget\n        indices = list(range(101))\n        indices.pop(26)\n        zoo = Table(\"zoo\")[indices]\n        zoo_images = Table(test_filename(\"datasets/zoo-with-images.tab\"))\n        self.send_signal(widget.Inputs.data, zoo)\n        self.send_signal(widget.Inputs.extra_data, zoo_images)\n        for i in range(3):\n            self.assertEqual(widget.attr_boxes.current_state(),\n                             [(zoo.domain[\"name\"], zoo_images.domain[\"name\"])],\n                             f\"wrong attributes chosen for merge_type={i}\")\n\n    def test_sparse(self):\n        data = Table(\"iris\")[::25]\n        data_ed_dense = Table(\"titanic\")[::300]\n        data_ed_sparse = Table(\"titanic\")[::300].to_sparse()\n        self.send_signal(\"Data\", data)\n\n        self.send_signal(\"Extra Data\", data_ed_dense)\n        output_dense = self.get_output(\"Data\")\n        self.assertFalse(sp.issparse(output_dense.X))\n        self.assertFalse(output_dense.is_sparse())\n\n        self.send_signal(\"Extra Data\", data_ed_sparse)\n        output_sparse = self.get_output(\"Data\")\n        self.assertTrue(sp.issparse(output_sparse.X))\n        self.assertTrue(output_sparse.is_sparse())\n\n        output_sparse.X = output_sparse.X.toarray()\n        self.assertTablesEqual(output_dense, output_sparse)\n\n    def test_commit_on_new_data(self):\n        \"\"\"Check that disabling auto apply doesn't block on new data\"\"\"\n        self.widget.auto_apply = False\n        self.widget.merging = 2\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.data))\n\n    def test_multiple_attributes_left(self):\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n\n        X = np.array([[0, 1], [1, 1], [3, np.nan], [np.nan, 0]])\n        Y = np.array([1, 2, 0, 0])\n        metas = np.array([[0, \"a\"], [1, \"b\"], [0, \"c\"], [0, \"d\"]])\n        dataA = Table(domainA, X, Y, metas)\n        dataA.name = \"dataA\"\n\n        X = np.array(\n            [[0, 0], [1, 0], [0, 1], [1, 1], [2, 0], [3, np.nan]])\n        Y = np.array([0, 0, 1, 1, 0, 1])\n        metas = np.array([[0], [1], [0], [1], [1], [1]])\n        dataB = Table(domainB, X, Y, metas)\n        dataB.name = \"dataB\"\n\n        self.send_signal(self.widget.Inputs.data, dataA)\n        self.send_signal(self.widget.Inputs.extra_data, dataB)\n        self.widget.attr_boxes.set_state(\n            [(domainA[0], domainB[0]), (domainA[1], domainB[1])])\n        self.widget.commit()\n        output = self.get_output(self.widget.Outputs.data)\n\n        self.assertEqual(output.name, dataA.name)\n        np.testing.assert_array_equal(output.ids, dataA.ids)\n        self.assertEqual(output.attributes, dataA.attributes)\n        np.testing.assert_equal(output.X, dataA.X)\n\n    def test_nonunique(self):\n        widget = self.widget\n        x = ContinuousVariable(\"x\")\n        d = DiscreteVariable(\"d\", values=tuple(\"abc\"))\n        domain = Domain([x, d], [])\n        dataA = Table.from_numpy(\n            domain, np.array([[1.0, 0], [1, 1], [2, 1]]))\n        dataB = Table.from_numpy(\n            domain, np.array([[1.0, 0], [2, 1], [3, 1]]))\n        dataB.ids = dataA.ids\n        self.send_signal(widget.Inputs.data, dataA)\n        self.send_signal(widget.Inputs.extra_data, dataB)\n        widget.merging = widget.InnerJoin\n\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n\n        widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.attr_boxes.set_state([(INDEX, INDEX)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.attr_boxes.set_state([(x, x)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.LeftJoin\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.InnerJoin\n        widget.attr_boxes.set_state([(x, x), (d, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.attr_boxes.set_state([(d, d)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.nonunique_left.is_shown())\n        self.assertTrue(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.LeftJoin\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertTrue(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.InnerJoin\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.nonunique_left.is_shown())\n        self.assertTrue(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        self.send_signal(widget.Inputs.data, None)\n        self.send_signal(widget.Inputs.extra_data, None)\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n    def test_invalide_pairs(self):\n        widget = self.widget\n        x = ContinuousVariable(\"x\")\n        d = DiscreteVariable(\"d\", values=tuple(\"abc\"))\n        domain = Domain([x, d], [])\n        dataA = Table.from_numpy(\n            domain, np.array([[1.0, 0], [1, 1], [2, 1]]))\n        dataB = Table.from_numpy(\n            domain, np.array([[1.0, 0], [2, 1], [3, 1]]))\n        dataB.ids = dataA.ids\n        self.send_signal(widget.Inputs.data, dataA)\n        self.send_signal(widget.Inputs.extra_data, dataB)\n\n        widget.attr_boxes.set_state([(x, x), (d, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (INDEX, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertTrue(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (d, INDEX)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertTrue(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (INSTANCEID, d)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (d, INSTANCEID)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (INDEX, INSTANCEID)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.matching_id_with_sth.is_shown()\n                        or widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (x, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertTrue(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n    def test_duplicate_names(self):\n        domain = Domain([ContinuousVariable(\"C1\")],\n                        metas=[DiscreteVariable(\"Feature\", values=(\"A\", \"B\"))])\n        data = Table(domain, np.array([[1.], [0.]]),\n                     metas=np.array([[1.], [0.]]))\n        domain = Domain([ContinuousVariable(\"C1\")],\n                        metas=[StringVariable(\"Feature\")])\n        extra_data = Table(domain, np.array([[1.], [0.]]),\n                           metas=np.array([[\"A\"], [\"B\"]]))\n        self.send_signal(self.widget.Inputs.data, data)\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        self.assertTrue(self.widget.Warning.renamed_vars.is_shown())\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.metas],\n                             [\"Feature (1)\", \"Feature (2)\"])\n\n    def test_keep_non_duplicate_variables(self):\n        domain = Domain([ContinuousVariable(\"A\"), ContinuousVariable(\"B\")])\n        data = Table(domain, np.array([[0., 0], [0, 1]]))\n        extra_data = Table(domain, np.array([[0., 1], [0, 1]]))\n        self.send_signal(self.widget.Inputs.data, data)\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.variables],\n                             [\"A\", \"B (1)\", \"B (2)\"])\n\n    def test_keep_non_duplicate_variables_missing_rows(self):\n        c = DiscreteVariable(\"C\", values=(\"a\", \"b\", \"c\"))\n        domain = Domain([ContinuousVariable(\"A\"), ContinuousVariable(\"B\"), c])\n        data = Table(domain, np.array([[0., 0, 0], [1, 1, 1]]))\n        extra_data = Table(domain, np.array([[0., 1, 1], [0, 1, 2]]))\n        self.send_signal(self.widget.Inputs.data, data)\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        self.widget.attr_boxes.set_state([(c, c)])\n        self.widget.attr_boxes.emit_list()\n\n        # Only one row is matched; A has different values and it's duplicated,\n        # and B has the same values, so we get only one copy\n        self.widget.merging = self.widget.InnerJoin\n        self.widget.unconditional_commit()\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.variables],\n                             [\"A (1)\", \"B\", \"C\", \"A (2)\"])\n\n        # Table has additional rows; keep all columns\n        self.widget.merging = self.widget.OuterJoin\n        self.widget.unconditional_commit()\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual(\n            [m.name for m in merged_data.domain.variables],\n            [\"A (1)\", \"B (1)\", \"C (1)\", \"A (2)\", \"B (2)\", \"C (2)\"])\n\n        # First row is unmatched, data for B(2) is missing, but attribute\n        # shouldn't be added\n        extra_data = Table(domain, np.array([[1., 1, 1], [0, 1, 2]]))\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        self.widget.merging = self.widget.LeftJoin\n        self.widget.unconditional_commit()\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.variables],\n                             [\"A\", \"B\", \"C\"])\n\n    def test_summary(self):\n        \"\"\"Check if the status bar is updated when data is received\"\"\"\n        data = self.dataA\n        info = self.widget.info\n        no_input, no_output = \"No data on input\", \"No data on output\"\n\n        self.send_signal(self.widget.Inputs.data, data)\n        data_list = [(\"Data\", data), (\"Extra data\", None)]\n        summary, details = f\"{len(data)}, 0\", format_multiple_summaries(data_list)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.assertIsInstance(info._StateInfo__output_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n        self.send_signal(self.widget.Inputs.extra_data, data)\n        data_list = [(\"Data\", data), (\"Extra data\", data)]\n        summary = f\"{len(data)}, {len(data)}\"\n        details = format_multiple_summaries(data_list)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        output = self.get_output(self.widget.Outputs.data)\n        summary, details = f\"{len(output)}\", format_summary_details(output)\n        self.assertEqual(info._StateInfo__output_summary.brief, summary)\n        self.assertEqual(info._StateInfo__output_summary.details, details)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        data_list = [(\"Data\", None), (\"Extra data\", data)]\n        summary, details = f\"0, {len(data)}\", format_multiple_summaries(data_list)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.assertIsInstance(info._StateInfo__output_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n        self.send_signal(self.widget.Inputs.extra_data, None)\n        self.assertIsInstance(info._StateInfo__input_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__input_summary.details, no_input)\n        self.assertIsInstance(info._StateInfo__output_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n    def test_empty_tables(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.dataA[:0])\n        self.send_signal(widget.Inputs.extra_data, self.dataB[:0])",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 1066,
                            "end_lineno": 1069,
                            "parent": {
                                "file_name": "Orange/widgets/data/tests/test_owmergedata.py",
                                "type": "class",
                                "name": "TestOWMergeData",
                                "content": "class TestOWMergeData(WidgetTest):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        domainA = Domain([DiscreteVariable(\"dA1\", (\"a\", \"b\", \"c\", \"d\")),\n                          DiscreteVariable(\"dA2\", (\"aa\", \"bb\"))],\n                         DiscreteVariable(\"clsA\", (\"aaa\", \"bbb\", \"ccc\")),\n                         [DiscreteVariable(\"mA1\", (\"cc\", \"dd\")),\n                          StringVariable(\"mA2\")])\n        XA = np.array([[0, 0], [1, 1], [2, 0], [3, 1]])\n        yA = np.array([0, 1, 2, np.nan])\n        metasA = np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                           [0.0, \"m4\"]]).astype(object)\n\n        domainB = Domain([DiscreteVariable(\"dB1\", values=(\"a\", \"b\", \"c\")),\n                          DiscreteVariable(\"dB2\", values=(\"aa\", \"bb\"))],\n                         DiscreteVariable(\"clsB\", values=(\"bbb\", \"ccc\")),\n                         [DiscreteVariable(\"mB1\", (\"m4\", \"m5\"))])\n        XB = np.array([[0, 0], [1, 1], [2, np.nan]])\n        yB = np.array([np.nan, 1, 0])\n        metasB = np.array([[np.nan], [1], [0]]).astype(object)\n        cls.dataA = Table(domainA, XA, yA, metasA)\n        cls.dataA.name = 'dataA'\n        cls.dataA.attributes = 'dataA attributes'\n        cls.dataB = Table(domainB, XB, yB, metasB)\n        cls.dataB.name = 'dataB'\n        cls.dataB.attributes = 'dataB attributes'\n\n    def setUp(self):\n        self.widget = self.create_widget(OWMergeData)\n\n    def test_input_remove(self):\n        \"\"\"Check widget after inputs have been removed\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.send_signal(self.widget.Inputs.extra_data, None)\n\n    def test_combobox_items(self):\n        \"\"\"Check if combo box content is properly set\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        row = self.widget.attr_boxes.rows[0]\n        data_combo, extra_combo = row.left_combo, row.right_combo\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n        data_items = extra_items = list(\n            chain([INDEX, INSTANCEID], domainA.variables, domainA.metas))\n        self.assertListEqual(data_combo.model()[:], data_items)\n        self.assertListEqual(extra_combo.model()[:], extra_items)\n\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        data_items = list(\n            chain([INDEX, INSTANCEID], domainA.variables, domainA.metas))\n        extra_items = list(\n            chain([INDEX, INSTANCEID], domainB.variables, domainB.metas))\n        self.assertListEqual(data_combo.model()[:], data_items)\n        self.assertListEqual(extra_combo.model()[:], extra_items)\n\n        self.send_signal(self.widget.Inputs.data, self.dataB)\n        data_items = extra_items = list(\n            chain([INDEX, INSTANCEID], domainB.variables, domainB.metas))\n        self.assertListEqual(data_combo.model()[:], data_items)\n        self.assertListEqual(extra_combo.model()[:], extra_items)\n\n    def test_combo_box_sync(self):\n        row = self.widget.attr_boxes.rows[0]\n        data_combo, extra_combo = row.left_combo, row.right_combo\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        extra_combo.setCurrentIndex(3)\n        data_combo.setCurrentIndex(0)\n        data_combo.activated.emit(0)\n        self.assertEqual(data_combo.currentIndex(), 0)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        data_combo.setCurrentIndex(1)\n        data_combo.activated.emit(1)\n        self.assertEqual(data_combo.currentIndex(), 1)\n        self.assertEqual(extra_combo.currentIndex(), 1)\n\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        self.assertEqual(data_combo.currentIndex(), 0)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        extra_combo.setCurrentIndex(1)\n        extra_combo.activated.emit(1)\n        self.assertEqual(data_combo.currentIndex(), 1)\n        self.assertEqual(extra_combo.currentIndex(), 1)\n\n    def test_attr_combo_tooltips(self):\n        row = self.widget.attr_boxes.rows[0]\n        model = row.left_combo.model()\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        tip = model.data(model.index(2, 0), Qt.ToolTipRole)\n        # Test the test; if general tooltips ever change and the following\n        # assert fails, the rest of this test has to be modified accordingly\n        self.assertTrue(tip.startswith(\"<b>\"))\n\n        # Just test that tooltip is a string (implicitly) and that it's not\n        # a generic DomainModel tooltip\n        tip = model.data(model.index(0, 0), Qt.ToolTipRole)\n        self.assertFalse(tip.startswith(\"<b>\"))\n\n        tip = model.data(model.index(1, 0), Qt.ToolTipRole)\n        self.assertFalse(tip.startswith(\"<b>\"))\n\n    def test_match_attr_name(self):\n        widget = self.widget\n        row = widget.attr_boxes.rows[0]\n        data_combo, extra_combo = row.left_combo, row.right_combo\n\n        domainA = Domain([DiscreteVariable(\"dA1\", (\"a\", \"b\", \"c\", \"d\")),\n                          DiscreteVariable(\"dA2\", (\"aa\", \"bb\")),\n                          DiscreteVariable(\"dA3\", (\"aa\", \"bb\"))],\n                         DiscreteVariable(\"cls\", (\"aaa\", \"bbb\", \"ccc\")),\n                         [DiscreteVariable(\"mA1\", (\"cc\", \"dd\")),\n                          StringVariable(\"mA2\")])\n        XA = np.array([[0, 0, 0], [1, 1, 0], [2, 0, 0], [3, 1, 0]])\n        yA = np.array([0, 1, 2, np.nan])\n        metasA = np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                           [0.0, \"m4\"]]).astype(object)\n\n        domainB = Domain([DiscreteVariable(\"dB1\", values=(\"a\", \"b\", \"c\")),\n                          ContinuousVariable(\"dA2\")],\n                         None,\n                         [StringVariable(\"cls\"),\n                          DiscreteVariable(\"dA1\", (\"m4\", \"m5\"))])\n        XB = np.array([[0, 0], [1, 1], [2, np.nan]])\n        yB = np.empty((3, 0))\n        metasB = np.array([[np.nan, np.nan], [1, 1], [0, 0]]).astype(object)\n        dataA = Table(domainA, XA, yA, metasA)\n        dataA.name = 'dataA'\n        dataA.attributes = 'dataA attributes'\n        dataB = Table(domainB, XB, yB, metasB)\n        dataB.name = 'dataB'\n        dataB.attributes = 'dataB attributes'\n\n        self.send_signal(widget.Inputs.data, dataA)\n        self.send_signal(widget.Inputs.extra_data, dataB)\n\n        # match variable if available and the other combo is Row Index\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n        self.assertEqual(extra_combo.currentIndex(), 5)\n\n        # match variable if available and the other combo is ID\n        extra_combo.setCurrentIndex(1)\n        extra_combo.activated.emit(1)\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n        self.assertEqual(extra_combo.currentIndex(), 5)\n\n        # don't match variable if other combo is set\n        extra_combo.setCurrentIndex(4)\n        extra_combo.activated.emit(4)\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n        self.assertEqual(extra_combo.currentIndex(), 4)\n\n        # don't match if nothing to match to\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(4)\n        data_combo.activated.emit(4)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        # don't match numeric with non-numeric\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(3)\n        data_combo.activated.emit(3)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        # allow matching string with discrete\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(5)\n        data_combo.activated.emit(5)\n        self.assertEqual(extra_combo.currentIndex(), 4)\n\n    def test_add_row_button(self):\n        boxes = self.widget.attr_boxes\n        boxes.set_state([(INSTANCEID, INSTANCEID), (INSTANCEID, INSTANCEID)])\n        boxes.rows[-1].add_button.clicked.emit()\n        self.assertEqual(len(boxes.rows), 3)\n        self.assertEqual(boxes.layout().count(), 3)\n\n    def test_remove_row(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        var0, var1 = self.dataA.domain.attributes[:2]\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n        for i, row in enumerate(boxes.rows):\n            self.assertTrue(row.remove_button.isEnabled())\n            self.assertEqual(row.remove_button.text(), \"×\")\n            self.assertEqual(row.add_button.isEnabled(), i == 2)\n            self.assertEqual(row.add_button.text(), [\"\", \"+\"][i == 2])\n\n        boxes.rows[1].remove_button.clicked.emit()\n        self.assertEqual(boxes.current_state(), [(INDEX, INDEX), (var0, var1)])\n        for i, row in enumerate(boxes.rows):\n            self.assertTrue(row.remove_button.isEnabled())\n            self.assertEqual(row.remove_button.text(), \"×\")\n            self.assertEqual(row.add_button.isEnabled(), i == 1)\n            self.assertEqual(row.add_button.text(), [\"\", \"+\"][i])\n\n        boxes.rows[1].remove_button.clicked.emit()\n        self.assertEqual(boxes.current_state(), [(INDEX, INDEX)])\n        row = boxes.rows[0]\n        self.assertFalse(row.remove_button.isEnabled())\n        self.assertEqual(row.remove_button.text(), \"\")\n        self.assertTrue(row.add_button.isEnabled())\n        self.assertEqual(row.add_button.text(), \"+\")\n\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n        boxes.rows[2].remove_button.clicked.emit()\n        self.assertEqual(\n            boxes.current_state(), [(INDEX, INDEX), (INSTANCEID, INSTANCEID)])\n        for i, row in enumerate(boxes.rows):\n            self.assertTrue(row.remove_button.isEnabled())\n            self.assertEqual(row.remove_button.text(), \"×\")\n            self.assertEqual(row.add_button.isEnabled(), i == 1)\n            self.assertEqual(row.add_button.text(), [\"\", \"+\"][i == 1])\n\n    def test_dont_remove_single_row(self):\n        widget = self.widget\n        rows = widget.attr_boxes.rows\n        self.assertEqual(len(rows), 1)\n        rows[0].remove_button.clicked.emit()\n        self.assertEqual(len(rows), 1)\n\n    def test_retrieve_settings(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        var0, var1 = self.dataA.domain.attributes[:2]\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n\n        settings = widget.settingsHandler.pack_data(widget)\n\n        widget2 = self.create_widget(OWMergeData, stored_settings=settings)\n        widget2.attr_boxes.set_state([(INDEX, INDEX)])\n        self.send_signals(\n            [(widget2.Inputs.data, self.dataA),\n             (widget2.Inputs.extra_data, self.dataA)],\n            widget=widget2)\n        self.assertEqual(\n            widget2.attr_boxes.current_state(),\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n\n    def test_match_settings(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        domainA = self.dataA.domain\n        domainB = self.dataB.domain\n\n        self.send_signal(widget.Inputs.data, self.dataA)\n        self.send_signal(widget.Inputs.extra_data, self.dataA)\n        attr_pairs = [(INDEX, INDEX), (INSTANCEID, INSTANCEID),\n                      (domainA[0], domainA[1]), (domainA[1], domainA[0])]\n        boxes.set_state(attr_pairs)\n        boxes.emit_list()\n        self.assertEqual(widget.attr_pairs, attr_pairs)\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertEqual(widget.attr_pairs, [(INDEX, INDEX)])\n\n        self.send_signal(widget.Inputs.data, self.dataA)\n        self.assertEqual(widget.attr_pairs, attr_pairs)\n\n        self.send_signal(widget.Inputs.extra_data, self.dataB)\n        attr_pairs2 = [(domainA[0], domainB[0]), (domainA[1], domainB[1])]\n        boxes.set_state(attr_pairs2)\n        boxes.emit_list()\n        self.assertEqual(widget.attr_pairs, attr_pairs2)\n\n        self.send_signal(widget.Inputs.extra_data, self.dataA)\n        self.assertEqual(widget.attr_pairs, attr_pairs)\n\n    def test_migrate_settings(self):\n        def create_and_send(settings):\n            widget = self.create_widget(OWMergeData, stored_settings=settings)\n            for signal in (widget.Inputs.data, widget.Inputs.extra_data):\n                self.send_signal(signal, self.dataA)\n            return widget\n\n        domainA = self.dataA.domain\n        attr1, attr2, attr3 = domainA.variables\n        attr4, attr5 = domainA.metas\n\n        # Migration from version == None\n        orig_settings = dict(\n            attr_augment_data=attr1.name,\n            attr_augment_extra=attr2.name,\n            attr_merge_data=attr3.name,\n            attr_merge_extra=attr4.name,\n            attr_combine_data=attr5.name,\n            attr_combine_extra='Position (index)')\n\n        widget = create_and_send(dict(merging=0, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr1, attr2)]))\n\n        widget = create_and_send(dict(merging=1, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr3, attr4)]))\n\n        widget = create_and_send(dict(merging=2, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr5, attr5)]))\n\n        orig_settings[\"attr_combine_extra\"] = \"Source position (index)\"\n        widget = create_and_send(dict(merging=2, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr5, attr5)]))\n\n        # Migration from version 1\n        orig_settings = {\"attr_pairs\": (True, True, [[attr1.name, attr2.name],\n                                                     [attr3.name, attr4.name]]),\n                         \"__version__\": 1}\n        widget = create_and_send(orig_settings)\n        self.assertEqual(widget.attr_pairs, ([(attr1, attr2), (attr3, attr4)]))\n\n    def test_migrate_settings_attr_pairs_extra_none(self):\n        settings = {'attr_pairs': (True, False, [['sepal length', 0]])}\n        OWMergeData.migrate_settings(settings, 1)\n        self.assertListEqual(settings[\"context_settings\"], [])\n\n    def test_migrate_settings_attr_pairs_data_none(self):\n        settings = {'attr_pairs': (False, True, [[0, \"sepal length\"]])}\n        OWMergeData.migrate_settings(settings, 1)\n        self.assertListEqual(settings[\"context_settings\"], [])\n\n    def test_migrate_settings_attr_pairs_id_idx(self):\n        settings = {\"attr_pairs\": (True, True, [[0, 1]])}\n        OWMergeData.migrate_settings(settings, 1)\n        context = settings[\"context_settings\"][0]\n        self.assertListEqual(context.values[\"attr_pairs\"],\n                             [((INDEX, 100), (INSTANCEID, 100))])\n        self.assertDictEqual(context.variables1, {})\n        self.assertDictEqual(context.variables2, {})\n\n    def test_migrate_settings_attr_pairs_vars(self):\n        settings = {\"attr_pairs\": (True, True,\n                                   [[\"sepal length\", \"sepal width\"],\n                                    [\"petal length\", \"petal width\"]])}\n        OWMergeData.migrate_settings(settings, 1)\n        context = settings[\"context_settings\"][0]\n        self.assertListEqual(context.values[\"attr_pairs\"],\n                             [((\"sepal length\", 100), (\"sepal width\", 100)),\n                              ((\"petal length\", 100), (\"petal width\", 100))])\n        self.assertDictEqual(context.variables1, {})\n        self.assertDictEqual(context.variables2, {})\n\n    def test_report(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        var0, var1 = self.dataA.domain.attributes[:2]\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n        widget.send_report()\n        # Don't crash, that's it\n\n    def test_no_matches(self):\n        \"\"\"Check output is None when there are no matches in inner join\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        domA = self.dataA.domain\n        domB = self.dataB.domain\n\n        self.widget.attr_boxes.set_state([(domA[\"dA1\"], domB[\"dB2\"])])\n        self.widget.controls.merging.buttons[self.widget.LeftJoin].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.data))\n\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertIsNone(self.get_output(self.widget.Outputs.data))\n\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.data))\n\n    def test_output_merge_by_ids_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by\n        Source position (index)\"\"\"\n        domain = self.dataA.domain\n        result = Table(domain, np.array([[1, 1], [2, 0]]), np.array([1, 2]),\n                       np.array([[1.0, \"m2\"], [np.nan, \"m3\"]]).astype(object))\n        self.send_signal(self.widget.Inputs.data, self.dataA[:3, [0, \"clsA\", -1]])\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA[1:, [1, \"clsA\", -2]])\n        self.widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_ids_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        Source position (index)\"\"\"\n        domainA = self.dataA.domain\n        values = domainA.class_var.values\n        domain = Domain(domainA.attributes,\n                        (DiscreteVariable(\"clsA (1)\", values),\n                         DiscreteVariable(\"clsA (2)\", values)),\n                        domainA.metas)\n        result = Table(domain,\n                       np.array([[1, 1], [2, 0], [3, np.nan], [np.nan, 0]]),\n                       np.array([[1, 1], [2, 2], [np.nan, np.nan], [np.nan, 0]]),\n                       np.array([[1.0, \"m2\"], [np.nan, \"m3\"],\n                                 [0.0, \"\"], [np.nan, \"m1\"]]).astype(object))\n        self.widget.merging = 2\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        self.send_signal(self.widget.Inputs.data, self.dataA[1:, [0, \"clsA\", -1]])\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA[:3, [1, \"clsA\", -2]])\n        self.widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        self.widget.attr_boxes.emit_list()\n        out = self.get_output(self.widget.Outputs.data)\n        self.assertTablesEqual(out, result)\n        np.testing.assert_equal(\n            out.ids, np.hstack((self.dataA.ids[1:], self.dataA.ids[:1])))\n\n    def test_output_merge_by_ids_outer_single_class(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        Source position (index) when all extra rows are matched and there is\n        only a single class variable in the output\"\"\"\n        domainA = self.dataA.domain\n        values = domainA.class_var.values\n        domain = Domain(domainA.attributes,\n                        DiscreteVariable(\"clsA\", values),\n                        domainA.metas)\n        result = Table(domain,\n                       np.array([[0, 0], [1, 1], [2, 0], [3, np.nan]]),\n                       np.array([[0], [1], [2], [np.nan]]),\n                       np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                                 [0.0, \"\"]]).astype(object))\n        self.widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        self.widget.merging = 2\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        # When Y is a single column, Table.Y returns a vector, not a 2d array,\n        # which cause an exception in outer_join's vstack for Y if extra data\n        # has no unmatched rows.\n        # This test also checks this condition.\n        self.send_signal(self.widget.Inputs.data, self.dataA[:, [0, \"clsA\", -1]])\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA[:3, [1, -2]])\n        out = self.get_output(self.widget.Outputs.data)\n        self.assertTablesEqual(out, result)\n        np.testing.assert_equal(out.ids, self.dataA.ids)\n\n    def test_output_merge_by_index_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        Position (index)\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1],\n                             [2, 0, 2, np.nan], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]\n                            ]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_index_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by\n        Position (index)\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1], [2, 0, 2, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.controls.merging.buttons[1].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_index_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        Position (index)\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1],\n                             [2, 0, 2, np.nan], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.controls.merging.buttons[2].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_attribute_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        attribute\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes[1:],\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0], [1, 1, 1],\n                             [2, 0, np.nan], [3, 1, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[0], domainB[0])])\n        self.widget.commit()\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertTablesEqual(output, result)\n        self.assertEqual(output.name, self.dataA.name)\n        np.testing.assert_array_equal(output.ids, self.dataA.ids)\n        self.assertEqual(output.attributes, self.dataA.attributes)\n\n    def test_output_merge_by_attribute_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by attribute\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes[1:],\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0], [1, 1, 1], [2, 0, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[0], domainB[0])])\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_attribute_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        attribute\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1],\n                             [2, 0, 2, np.nan], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[0], domainB[0])])\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_attribute_outer_same_attr(self):\n        \"\"\"Values of columns from extra aata are copied to left part if they\n        match\"\"\"\n        name = StringVariable(\"name\")\n        domainA = Domain([ContinuousVariable(\"x\")], None, [name])\n        domainB = Domain([ContinuousVariable(\"y\")], None, [name])\n        xA = np.array([[0], [1], [2]])\n        mA = np.array([[\"a\"], [\"b\"], [\"c\"]])\n        xB = np.array([[4], [5], [6], [7]])\n        mB = np.array([[\"b\"], [\"d\"], [\"a\"], [\"c\"]])\n        dataA = Table(domainA, xA, None, mA)\n        dataB = Table(domainB, xB, None, mB)\n\n        self.send_signal(self.widget.Inputs.data, dataA)\n        self.send_signal(self.widget.Inputs.extra_data, dataB)\n        self.widget.attr_boxes.set_state([(name, name)])\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        out = self.get_output(self.widget.Outputs.data)\n        np.testing.assert_equal(\n            out.X,\n            np.array([[0, 6], [1, 4], [2, 7], [np.nan, 5]]))\n        self.assertEqual(\" \".join(out.metas.flatten()), \"a a b b c c  d\")\n\n    def test_output_merge_by_class_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        class variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars, domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, 2, np.nan],\n                             [2, 0, 1, 1], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([0, 1, 2, np.nan])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 0.0],\n                             [np.nan, \"m3\", 1.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[2], domainB[2])])\n        self.widget.commit()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_class_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by class\n        variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars, domainA.metas + domainB.metas)\n        result_X = np.array([[1, 1, 2, np.nan], [2, 0, 1, 1]])\n        result_Y = np.array([1, 2])\n        result_M = np.array([[1.0, \"m2\", 0.0], [np.nan, \"m3\", 1.0]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state(\n            [(domainA.class_vars[0], domainB.class_vars[0])])\n        self.widget.controls.merging.buttons[1].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_class_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        class variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, 2, np.nan],\n                             [2, 0, 1, 1], [3, 1, np.nan, np.nan],\n                             [np.nan, np.nan, 0, 0]])\n        result_Y = np.array([[0, np.nan], [1, 0], [2, 1], [np.nan, np.nan],\n                             [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 0.0],\n                             [np.nan, \"m3\", 1.0], [0.0, \"m4\", np.nan],\n                             [np.nan, \"\", np.nan]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state(\n            [(domainA.class_vars[0], domainB.class_vars[0])])\n        self.widget.controls.merging.buttons[2].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_meta_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        meta variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, np.nan, np.nan],\n                             [2, 0, np.nan, np.nan], [3, 1, 2, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, np.nan],\n                             [2, np.nan], [np.nan, 0]])\n        result_M = np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                             [0.0, \"m4\"]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[-2], domainB[-1])])\n        self.widget.commit()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_meta_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by meta\n        variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas)\n        result_X = np.array([[3, 1, 2, np.nan]])\n        result_Y = np.array([[np.nan, 0]])\n        result_M = np.array([[0.0, \"m4\"]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[-2], domainB[-1])])\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_meta_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        meta variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, np.nan, np.nan],\n                             [2, 0, np.nan, np.nan], [3, 1, 2, np.nan],\n                             [np.nan, np.nan, 0, 0], [np.nan, np.nan, 1, 1]])\n        result_Y = np.array([[0, np.nan], [1, np.nan], [2, np.nan],\n                             [np.nan, 0], [np.nan, np.nan], [np.nan, 1]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", np.nan],\n                             [np.nan, \"m3\", np.nan], [0.0, \"m4\", 0.0],\n                             [np.nan, \"\", np.nan], [np.nan, \"\", 1.0]]\n                           ).astype(object)\n\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[-2], domainB[-1])])\n        self.widget.controls.merging.buttons[2].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def assertTablesEqual(self, table1, table2):\n        self.assertEqual(table1.domain, table2.domain)\n        np.testing.assert_array_equal(table1.X, table2.X)\n        np.testing.assert_array_equal(table1.Y, table2.Y)\n        np.testing.assert_array_equal(table1.metas.astype(str),\n                                      table2.metas.astype(str))\n\n    def test_best_match(self):\n        \"\"\"Check default merging attributes setup\"\"\"\n        widget = self.widget\n        indices = list(range(101))\n        indices.pop(26)\n        zoo = Table(\"zoo\")[indices]\n        zoo_images = Table(test_filename(\"datasets/zoo-with-images.tab\"))\n        self.send_signal(widget.Inputs.data, zoo)\n        self.send_signal(widget.Inputs.extra_data, zoo_images)\n        for i in range(3):\n            self.assertEqual(widget.attr_boxes.current_state(),\n                             [(zoo.domain[\"name\"], zoo_images.domain[\"name\"])],\n                             f\"wrong attributes chosen for merge_type={i}\")\n\n    def test_sparse(self):\n        data = Table(\"iris\")[::25]\n        data_ed_dense = Table(\"titanic\")[::300]\n        data_ed_sparse = Table(\"titanic\")[::300].to_sparse()\n        self.send_signal(\"Data\", data)\n\n        self.send_signal(\"Extra Data\", data_ed_dense)\n        output_dense = self.get_output(\"Data\")\n        self.assertFalse(sp.issparse(output_dense.X))\n        self.assertFalse(output_dense.is_sparse())\n\n        self.send_signal(\"Extra Data\", data_ed_sparse)\n        output_sparse = self.get_output(\"Data\")\n        self.assertTrue(sp.issparse(output_sparse.X))\n        self.assertTrue(output_sparse.is_sparse())\n\n        output_sparse.X = output_sparse.X.toarray()\n        self.assertTablesEqual(output_dense, output_sparse)\n\n    def test_commit_on_new_data(self):\n        \"\"\"Check that disabling auto apply doesn't block on new data\"\"\"\n        self.widget.auto_apply = False\n        self.widget.merging = 2\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.data))\n\n    def test_multiple_attributes_left(self):\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n\n        X = np.array([[0, 1], [1, 1], [3, np.nan], [np.nan, 0]])\n        Y = np.array([1, 2, 0, 0])\n        metas = np.array([[0, \"a\"], [1, \"b\"], [0, \"c\"], [0, \"d\"]])\n        dataA = Table(domainA, X, Y, metas)\n        dataA.name = \"dataA\"\n\n        X = np.array(\n            [[0, 0], [1, 0], [0, 1], [1, 1], [2, 0], [3, np.nan]])\n        Y = np.array([0, 0, 1, 1, 0, 1])\n        metas = np.array([[0], [1], [0], [1], [1], [1]])\n        dataB = Table(domainB, X, Y, metas)\n        dataB.name = \"dataB\"\n\n        self.send_signal(self.widget.Inputs.data, dataA)\n        self.send_signal(self.widget.Inputs.extra_data, dataB)\n        self.widget.attr_boxes.set_state(\n            [(domainA[0], domainB[0]), (domainA[1], domainB[1])])\n        self.widget.commit()\n        output = self.get_output(self.widget.Outputs.data)\n\n        self.assertEqual(output.name, dataA.name)\n        np.testing.assert_array_equal(output.ids, dataA.ids)\n        self.assertEqual(output.attributes, dataA.attributes)\n        np.testing.assert_equal(output.X, dataA.X)\n\n    def test_nonunique(self):\n        widget = self.widget\n        x = ContinuousVariable(\"x\")\n        d = DiscreteVariable(\"d\", values=tuple(\"abc\"))\n        domain = Domain([x, d], [])\n        dataA = Table.from_numpy(\n            domain, np.array([[1.0, 0], [1, 1], [2, 1]]))\n        dataB = Table.from_numpy(\n            domain, np.array([[1.0, 0], [2, 1], [3, 1]]))\n        dataB.ids = dataA.ids\n        self.send_signal(widget.Inputs.data, dataA)\n        self.send_signal(widget.Inputs.extra_data, dataB)\n        widget.merging = widget.InnerJoin\n\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n\n        widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.attr_boxes.set_state([(INDEX, INDEX)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.attr_boxes.set_state([(x, x)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.LeftJoin\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.InnerJoin\n        widget.attr_boxes.set_state([(x, x), (d, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.attr_boxes.set_state([(d, d)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.nonunique_left.is_shown())\n        self.assertTrue(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.LeftJoin\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertTrue(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.InnerJoin\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.nonunique_left.is_shown())\n        self.assertTrue(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        self.send_signal(widget.Inputs.data, None)\n        self.send_signal(widget.Inputs.extra_data, None)\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n    def test_invalide_pairs(self):\n        widget = self.widget\n        x = ContinuousVariable(\"x\")\n        d = DiscreteVariable(\"d\", values=tuple(\"abc\"))\n        domain = Domain([x, d], [])\n        dataA = Table.from_numpy(\n            domain, np.array([[1.0, 0], [1, 1], [2, 1]]))\n        dataB = Table.from_numpy(\n            domain, np.array([[1.0, 0], [2, 1], [3, 1]]))\n        dataB.ids = dataA.ids\n        self.send_signal(widget.Inputs.data, dataA)\n        self.send_signal(widget.Inputs.extra_data, dataB)\n\n        widget.attr_boxes.set_state([(x, x), (d, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (INDEX, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertTrue(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (d, INDEX)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertTrue(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (INSTANCEID, d)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (d, INSTANCEID)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (INDEX, INSTANCEID)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.matching_id_with_sth.is_shown()\n                        or widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (x, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertTrue(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n    def test_duplicate_names(self):\n        domain = Domain([ContinuousVariable(\"C1\")],\n                        metas=[DiscreteVariable(\"Feature\", values=(\"A\", \"B\"))])\n        data = Table(domain, np.array([[1.], [0.]]),\n                     metas=np.array([[1.], [0.]]))\n        domain = Domain([ContinuousVariable(\"C1\")],\n                        metas=[StringVariable(\"Feature\")])\n        extra_data = Table(domain, np.array([[1.], [0.]]),\n                           metas=np.array([[\"A\"], [\"B\"]]))\n        self.send_signal(self.widget.Inputs.data, data)\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        self.assertTrue(self.widget.Warning.renamed_vars.is_shown())\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.metas],\n                             [\"Feature (1)\", \"Feature (2)\"])\n\n    def test_keep_non_duplicate_variables(self):\n        domain = Domain([ContinuousVariable(\"A\"), ContinuousVariable(\"B\")])\n        data = Table(domain, np.array([[0., 0], [0, 1]]))\n        extra_data = Table(domain, np.array([[0., 1], [0, 1]]))\n        self.send_signal(self.widget.Inputs.data, data)\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.variables],\n                             [\"A\", \"B (1)\", \"B (2)\"])\n\n    def test_keep_non_duplicate_variables_missing_rows(self):\n        c = DiscreteVariable(\"C\", values=(\"a\", \"b\", \"c\"))\n        domain = Domain([ContinuousVariable(\"A\"), ContinuousVariable(\"B\"), c])\n        data = Table(domain, np.array([[0., 0, 0], [1, 1, 1]]))\n        extra_data = Table(domain, np.array([[0., 1, 1], [0, 1, 2]]))\n        self.send_signal(self.widget.Inputs.data, data)\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        self.widget.attr_boxes.set_state([(c, c)])\n        self.widget.attr_boxes.emit_list()\n\n        # Only one row is matched; A has different values and it's duplicated,\n        # and B has the same values, so we get only one copy\n        self.widget.merging = self.widget.InnerJoin\n        self.widget.unconditional_commit()\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.variables],\n                             [\"A (1)\", \"B\", \"C\", \"A (2)\"])\n\n        # Table has additional rows; keep all columns\n        self.widget.merging = self.widget.OuterJoin\n        self.widget.unconditional_commit()\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual(\n            [m.name for m in merged_data.domain.variables],\n            [\"A (1)\", \"B (1)\", \"C (1)\", \"A (2)\", \"B (2)\", \"C (2)\"])\n\n        # First row is unmatched, data for B(2) is missing, but attribute\n        # shouldn't be added\n        extra_data = Table(domain, np.array([[1., 1, 1], [0, 1, 2]]))\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        self.widget.merging = self.widget.LeftJoin\n        self.widget.unconditional_commit()\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.variables],\n                             [\"A\", \"B\", \"C\"])\n\n    def test_summary(self):\n        \"\"\"Check if the status bar is updated when data is received\"\"\"\n        data = self.dataA\n        info = self.widget.info\n        no_input, no_output = \"No data on input\", \"No data on output\"\n\n        self.send_signal(self.widget.Inputs.data, data)\n        data_list = [(\"Data\", data), (\"Extra data\", None)]\n        summary, details = f\"{len(data)}, 0\", format_multiple_summaries(data_list)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.assertIsInstance(info._StateInfo__output_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n        self.send_signal(self.widget.Inputs.extra_data, data)\n        data_list = [(\"Data\", data), (\"Extra data\", data)]\n        summary = f\"{len(data)}, {len(data)}\"\n        details = format_multiple_summaries(data_list)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        output = self.get_output(self.widget.Outputs.data)\n        summary, details = f\"{len(output)}\", format_summary_details(output)\n        self.assertEqual(info._StateInfo__output_summary.brief, summary)\n        self.assertEqual(info._StateInfo__output_summary.details, details)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        data_list = [(\"Data\", None), (\"Extra data\", data)]\n        summary, details = f\"0, {len(data)}\", format_multiple_summaries(data_list)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.assertIsInstance(info._StateInfo__output_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n        self.send_signal(self.widget.Inputs.extra_data, None)\n        self.assertIsInstance(info._StateInfo__input_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__input_summary.details, no_input)\n        self.assertIsInstance(info._StateInfo__output_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n    def test_empty_tables(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.dataA[:0])\n        self.send_signal(widget.Inputs.extra_data, self.dataB[:0])",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 27,
                                "end_lineno": 1069,
                                "parent": null
                            }
                        }
                    ],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "528aadcc10e6424095a74508f07432a16ac980cb^",
    "new_commit_hash": "528aadcc10e6424095a74508f07432a16ac980cb",
    "commit_message": "MergeData: Fix context handler to accept Table",
    "commit_date": "2021-03-27T12:55:07+01:00",
    "metadata": {}
}
{
    "repo_name": "orange3",
    "new_commit_hash": "528aadcc10e6424095a74508f07432a16ac980cb",
    "test_file_codes": [
        "# Test methods with long descriptive names can omit docstrings\n# pylint: disable=missing-docstring\n# There are never too many tests, so:\n# pylint: disable=too-many-lines,too-many-public-methods, protected-access\nfrom itertools import chain\nimport unittest\nfrom types import SimpleNamespace\nfrom unittest.mock import Mock\n\nimport numpy as np\nimport scipy.sparse as sp\n\nfrom AnyQt.QtCore import Qt\n\nfrom orangewidget.widget import StateInfo\n\nfrom Orange.data import Table, Domain, DiscreteVariable, StringVariable, \\\n    ContinuousVariable\nfrom Orange.widgets.data.owmergedata import OWMergeData, INSTANCEID, INDEX, \\\n    MergeDataContextHandler\nfrom Orange.widgets.tests.base import WidgetTest\nfrom Orange.widgets.utils.state_summary import format_multiple_summaries, \\\n    format_summary_details\nfrom Orange.tests import test_filename\n\n\nclass TestOWMergeData(WidgetTest):\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n        domainA = Domain([DiscreteVariable(\"dA1\", (\"a\", \"b\", \"c\", \"d\")),\n                          DiscreteVariable(\"dA2\", (\"aa\", \"bb\"))],\n                         DiscreteVariable(\"clsA\", (\"aaa\", \"bbb\", \"ccc\")),\n                         [DiscreteVariable(\"mA1\", (\"cc\", \"dd\")),\n                          StringVariable(\"mA2\")])\n        XA = np.array([[0, 0], [1, 1], [2, 0], [3, 1]])\n        yA = np.array([0, 1, 2, np.nan])\n        metasA = np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                           [0.0, \"m4\"]]).astype(object)\n\n        domainB = Domain([DiscreteVariable(\"dB1\", values=(\"a\", \"b\", \"c\")),\n                          DiscreteVariable(\"dB2\", values=(\"aa\", \"bb\"))],\n                         DiscreteVariable(\"clsB\", values=(\"bbb\", \"ccc\")),\n                         [DiscreteVariable(\"mB1\", (\"m4\", \"m5\"))])\n        XB = np.array([[0, 0], [1, 1], [2, np.nan]])\n        yB = np.array([np.nan, 1, 0])\n        metasB = np.array([[np.nan], [1], [0]]).astype(object)\n        cls.dataA = Table(domainA, XA, yA, metasA)\n        cls.dataA.name = 'dataA'\n        cls.dataA.attributes = 'dataA attributes'\n        cls.dataB = Table(domainB, XB, yB, metasB)\n        cls.dataB.name = 'dataB'\n        cls.dataB.attributes = 'dataB attributes'\n\n    def setUp(self):\n        self.widget = self.create_widget(OWMergeData)\n\n    def test_input_remove(self):\n        \"\"\"Check widget after inputs have been removed\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n        self.send_signal(self.widget.Inputs.data, None)\n        self.send_signal(self.widget.Inputs.extra_data, None)\n\n    def test_combobox_items(self):\n        \"\"\"Check if combo box content is properly set\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        row = self.widget.attr_boxes.rows[0]\n        data_combo, extra_combo = row.left_combo, row.right_combo\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n        data_items = extra_items = list(\n            chain([INDEX, INSTANCEID], domainA.variables, domainA.metas))\n        self.assertListEqual(data_combo.model()[:], data_items)\n        self.assertListEqual(extra_combo.model()[:], extra_items)\n\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        data_items = list(\n            chain([INDEX, INSTANCEID], domainA.variables, domainA.metas))\n        extra_items = list(\n            chain([INDEX, INSTANCEID], domainB.variables, domainB.metas))\n        self.assertListEqual(data_combo.model()[:], data_items)\n        self.assertListEqual(extra_combo.model()[:], extra_items)\n\n        self.send_signal(self.widget.Inputs.data, self.dataB)\n        data_items = extra_items = list(\n            chain([INDEX, INSTANCEID], domainB.variables, domainB.metas))\n        self.assertListEqual(data_combo.model()[:], data_items)\n        self.assertListEqual(extra_combo.model()[:], extra_items)\n\n    def test_combo_box_sync(self):\n        row = self.widget.attr_boxes.rows[0]\n        data_combo, extra_combo = row.left_combo, row.right_combo\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        extra_combo.setCurrentIndex(3)\n        data_combo.setCurrentIndex(0)\n        data_combo.activated.emit(0)\n        self.assertEqual(data_combo.currentIndex(), 0)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        data_combo.setCurrentIndex(1)\n        data_combo.activated.emit(1)\n        self.assertEqual(data_combo.currentIndex(), 1)\n        self.assertEqual(extra_combo.currentIndex(), 1)\n\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        self.assertEqual(data_combo.currentIndex(), 0)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        extra_combo.setCurrentIndex(1)\n        extra_combo.activated.emit(1)\n        self.assertEqual(data_combo.currentIndex(), 1)\n        self.assertEqual(extra_combo.currentIndex(), 1)\n\n    def test_attr_combo_tooltips(self):\n        row = self.widget.attr_boxes.rows[0]\n        model = row.left_combo.model()\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        tip = model.data(model.index(2, 0), Qt.ToolTipRole)\n        # Test the test; if general tooltips ever change and the following\n        # assert fails, the rest of this test has to be modified accordingly\n        self.assertTrue(tip.startswith(\"<b>\"))\n\n        # Just test that tooltip is a string (implicitly) and that it's not\n        # a generic DomainModel tooltip\n        tip = model.data(model.index(0, 0), Qt.ToolTipRole)\n        self.assertFalse(tip.startswith(\"<b>\"))\n\n        tip = model.data(model.index(1, 0), Qt.ToolTipRole)\n        self.assertFalse(tip.startswith(\"<b>\"))\n\n    def test_match_attr_name(self):\n        widget = self.widget\n        row = widget.attr_boxes.rows[0]\n        data_combo, extra_combo = row.left_combo, row.right_combo\n\n        domainA = Domain([DiscreteVariable(\"dA1\", (\"a\", \"b\", \"c\", \"d\")),\n                          DiscreteVariable(\"dA2\", (\"aa\", \"bb\")),\n                          DiscreteVariable(\"dA3\", (\"aa\", \"bb\"))],\n                         DiscreteVariable(\"cls\", (\"aaa\", \"bbb\", \"ccc\")),\n                         [DiscreteVariable(\"mA1\", (\"cc\", \"dd\")),\n                          StringVariable(\"mA2\")])\n        XA = np.array([[0, 0, 0], [1, 1, 0], [2, 0, 0], [3, 1, 0]])\n        yA = np.array([0, 1, 2, np.nan])\n        metasA = np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                           [0.0, \"m4\"]]).astype(object)\n\n        domainB = Domain([DiscreteVariable(\"dB1\", values=(\"a\", \"b\", \"c\")),\n                          ContinuousVariable(\"dA2\")],\n                         None,\n                         [StringVariable(\"cls\"),\n                          DiscreteVariable(\"dA1\", (\"m4\", \"m5\"))])\n        XB = np.array([[0, 0], [1, 1], [2, np.nan]])\n        yB = np.empty((3, 0))\n        metasB = np.array([[np.nan, np.nan], [1, 1], [0, 0]]).astype(object)\n        dataA = Table(domainA, XA, yA, metasA)\n        dataA.name = 'dataA'\n        dataA.attributes = 'dataA attributes'\n        dataB = Table(domainB, XB, yB, metasB)\n        dataB.name = 'dataB'\n        dataB.attributes = 'dataB attributes'\n\n        self.send_signal(widget.Inputs.data, dataA)\n        self.send_signal(widget.Inputs.extra_data, dataB)\n\n        # match variable if available and the other combo is Row Index\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n        self.assertEqual(extra_combo.currentIndex(), 5)\n\n        # match variable if available and the other combo is ID\n        extra_combo.setCurrentIndex(1)\n        extra_combo.activated.emit(1)\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n        self.assertEqual(extra_combo.currentIndex(), 5)\n\n        # don't match variable if other combo is set\n        extra_combo.setCurrentIndex(4)\n        extra_combo.activated.emit(4)\n        data_combo.setCurrentIndex(2)\n        data_combo.activated.emit(2)\n        self.assertEqual(extra_combo.currentIndex(), 4)\n\n        # don't match if nothing to match to\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(4)\n        data_combo.activated.emit(4)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        # don't match numeric with non-numeric\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(3)\n        data_combo.activated.emit(3)\n        self.assertEqual(extra_combo.currentIndex(), 0)\n\n        # allow matching string with discrete\n        extra_combo.setCurrentIndex(0)\n        extra_combo.activated.emit(0)\n        data_combo.setCurrentIndex(5)\n        data_combo.activated.emit(5)\n        self.assertEqual(extra_combo.currentIndex(), 4)\n\n    def test_add_row_button(self):\n        boxes = self.widget.attr_boxes\n        boxes.set_state([(INSTANCEID, INSTANCEID), (INSTANCEID, INSTANCEID)])\n        boxes.rows[-1].add_button.clicked.emit()\n        self.assertEqual(len(boxes.rows), 3)\n        self.assertEqual(boxes.layout().count(), 3)\n\n    def test_remove_row(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        var0, var1 = self.dataA.domain.attributes[:2]\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n        for i, row in enumerate(boxes.rows):\n            self.assertTrue(row.remove_button.isEnabled())\n            self.assertEqual(row.remove_button.text(), \"×\")\n            self.assertEqual(row.add_button.isEnabled(), i == 2)\n            self.assertEqual(row.add_button.text(), [\"\", \"+\"][i == 2])\n\n        boxes.rows[1].remove_button.clicked.emit()\n        self.assertEqual(boxes.current_state(), [(INDEX, INDEX), (var0, var1)])\n        for i, row in enumerate(boxes.rows):\n            self.assertTrue(row.remove_button.isEnabled())\n            self.assertEqual(row.remove_button.text(), \"×\")\n            self.assertEqual(row.add_button.isEnabled(), i == 1)\n            self.assertEqual(row.add_button.text(), [\"\", \"+\"][i])\n\n        boxes.rows[1].remove_button.clicked.emit()\n        self.assertEqual(boxes.current_state(), [(INDEX, INDEX)])\n        row = boxes.rows[0]\n        self.assertFalse(row.remove_button.isEnabled())\n        self.assertEqual(row.remove_button.text(), \"\")\n        self.assertTrue(row.add_button.isEnabled())\n        self.assertEqual(row.add_button.text(), \"+\")\n\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n        boxes.rows[2].remove_button.clicked.emit()\n        self.assertEqual(\n            boxes.current_state(), [(INDEX, INDEX), (INSTANCEID, INSTANCEID)])\n        for i, row in enumerate(boxes.rows):\n            self.assertTrue(row.remove_button.isEnabled())\n            self.assertEqual(row.remove_button.text(), \"×\")\n            self.assertEqual(row.add_button.isEnabled(), i == 1)\n            self.assertEqual(row.add_button.text(), [\"\", \"+\"][i == 1])\n\n    def test_dont_remove_single_row(self):\n        widget = self.widget\n        rows = widget.attr_boxes.rows\n        self.assertEqual(len(rows), 1)\n        rows[0].remove_button.clicked.emit()\n        self.assertEqual(len(rows), 1)\n\n    def test_retrieve_settings(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        var0, var1 = self.dataA.domain.attributes[:2]\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n\n        settings = widget.settingsHandler.pack_data(widget)\n\n        widget2 = self.create_widget(OWMergeData, stored_settings=settings)\n        widget2.attr_boxes.set_state([(INDEX, INDEX)])\n        self.send_signals(\n            [(widget2.Inputs.data, self.dataA),\n             (widget2.Inputs.extra_data, self.dataA)],\n            widget=widget2)\n        self.assertEqual(\n            widget2.attr_boxes.current_state(),\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n\n    def test_match_settings(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        domainA = self.dataA.domain\n        domainB = self.dataB.domain\n\n        self.send_signal(widget.Inputs.data, self.dataA)\n        self.send_signal(widget.Inputs.extra_data, self.dataA)\n        attr_pairs = [(INDEX, INDEX), (INSTANCEID, INSTANCEID),\n                      (domainA[0], domainA[1]), (domainA[1], domainA[0])]\n        boxes.set_state(attr_pairs)\n        boxes.emit_list()\n        self.assertEqual(widget.attr_pairs, attr_pairs)\n\n        self.send_signal(widget.Inputs.data, None)\n        self.assertEqual(widget.attr_pairs, [(INDEX, INDEX)])\n\n        self.send_signal(widget.Inputs.data, self.dataA)\n        self.assertEqual(widget.attr_pairs, attr_pairs)\n\n        self.send_signal(widget.Inputs.extra_data, self.dataB)\n        attr_pairs2 = [(domainA[0], domainB[0]), (domainA[1], domainB[1])]\n        boxes.set_state(attr_pairs2)\n        boxes.emit_list()\n        self.assertEqual(widget.attr_pairs, attr_pairs2)\n\n        self.send_signal(widget.Inputs.extra_data, self.dataA)\n        self.assertEqual(widget.attr_pairs, attr_pairs)\n\n    def test_migrate_settings(self):\n        def create_and_send(settings):\n            widget = self.create_widget(OWMergeData, stored_settings=settings)\n            for signal in (widget.Inputs.data, widget.Inputs.extra_data):\n                self.send_signal(signal, self.dataA)\n            return widget\n\n        domainA = self.dataA.domain\n        attr1, attr2, attr3 = domainA.variables\n        attr4, attr5 = domainA.metas\n\n        # Migration from version == None\n        orig_settings = dict(\n            attr_augment_data=attr1.name,\n            attr_augment_extra=attr2.name,\n            attr_merge_data=attr3.name,\n            attr_merge_extra=attr4.name,\n            attr_combine_data=attr5.name,\n            attr_combine_extra='Position (index)')\n\n        widget = create_and_send(dict(merging=0, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr1, attr2)]))\n\n        widget = create_and_send(dict(merging=1, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr3, attr4)]))\n\n        widget = create_and_send(dict(merging=2, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr5, attr5)]))\n\n        orig_settings[\"attr_combine_extra\"] = \"Source position (index)\"\n        widget = create_and_send(dict(merging=2, **orig_settings))\n        self.assertEqual(widget.attr_pairs, ([(attr5, attr5)]))\n\n        # Migration from version 1\n        orig_settings = {\"attr_pairs\": (True, True, [[attr1.name, attr2.name],\n                                                     [attr3.name, attr4.name]]),\n                         \"__version__\": 1}\n        widget = create_and_send(orig_settings)\n        self.assertEqual(widget.attr_pairs, ([(attr1, attr2), (attr3, attr4)]))\n\n    def test_migrate_settings_attr_pairs_extra_none(self):\n        settings = {'attr_pairs': (True, False, [['sepal length', 0]])}\n        OWMergeData.migrate_settings(settings, 1)\n        self.assertListEqual(settings[\"context_settings\"], [])\n\n    def test_migrate_settings_attr_pairs_data_none(self):\n        settings = {'attr_pairs': (False, True, [[0, \"sepal length\"]])}\n        OWMergeData.migrate_settings(settings, 1)\n        self.assertListEqual(settings[\"context_settings\"], [])\n\n    def test_migrate_settings_attr_pairs_id_idx(self):\n        settings = {\"attr_pairs\": (True, True, [[0, 1]])}\n        OWMergeData.migrate_settings(settings, 1)\n        context = settings[\"context_settings\"][0]\n        self.assertListEqual(context.values[\"attr_pairs\"],\n                             [((INDEX, 100), (INSTANCEID, 100))])\n        self.assertDictEqual(context.variables1, {})\n        self.assertDictEqual(context.variables2, {})\n\n    def test_migrate_settings_attr_pairs_vars(self):\n        settings = {\"attr_pairs\": (True, True,\n                                   [[\"sepal length\", \"sepal width\"],\n                                    [\"petal length\", \"petal width\"]])}\n        OWMergeData.migrate_settings(settings, 1)\n        context = settings[\"context_settings\"][0]\n        self.assertListEqual(context.values[\"attr_pairs\"],\n                             [((\"sepal length\", 100), (\"sepal width\", 100)),\n                              ((\"petal length\", 100), (\"petal width\", 100))])\n        self.assertDictEqual(context.variables1, {})\n        self.assertDictEqual(context.variables2, {})\n\n    def test_report(self):\n        widget = self.widget\n        boxes = widget.attr_boxes\n        var0, var1 = self.dataA.domain.attributes[:2]\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA)\n        boxes.set_state(\n            [(INDEX, INDEX), (INSTANCEID, INSTANCEID), (var0, var1)])\n        widget.send_report()\n        # Don't crash, that's it\n\n    def test_no_matches(self):\n        \"\"\"Check output is None when there are no matches in inner join\"\"\"\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        domA = self.dataA.domain\n        domB = self.dataB.domain\n\n        self.widget.attr_boxes.set_state([(domA[\"dA1\"], domB[\"dB2\"])])\n        self.widget.controls.merging.buttons[self.widget.LeftJoin].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.data))\n\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertIsNone(self.get_output(self.widget.Outputs.data))\n\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.data))\n\n    def test_output_merge_by_ids_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by\n        Source position (index)\"\"\"\n        domain = self.dataA.domain\n        result = Table(domain, np.array([[1, 1], [2, 0]]), np.array([1, 2]),\n                       np.array([[1.0, \"m2\"], [np.nan, \"m3\"]]).astype(object))\n        self.send_signal(self.widget.Inputs.data, self.dataA[:3, [0, \"clsA\", -1]])\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA[1:, [1, \"clsA\", -2]])\n        self.widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_ids_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        Source position (index)\"\"\"\n        domainA = self.dataA.domain\n        values = domainA.class_var.values\n        domain = Domain(domainA.attributes,\n                        (DiscreteVariable(\"clsA (1)\", values),\n                         DiscreteVariable(\"clsA (2)\", values)),\n                        domainA.metas)\n        result = Table(domain,\n                       np.array([[1, 1], [2, 0], [3, np.nan], [np.nan, 0]]),\n                       np.array([[1, 1], [2, 2], [np.nan, np.nan], [np.nan, 0]]),\n                       np.array([[1.0, \"m2\"], [np.nan, \"m3\"],\n                                 [0.0, \"\"], [np.nan, \"m1\"]]).astype(object))\n        self.widget.merging = 2\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        self.send_signal(self.widget.Inputs.data, self.dataA[1:, [0, \"clsA\", -1]])\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA[:3, [1, \"clsA\", -2]])\n        self.widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        self.widget.attr_boxes.emit_list()\n        out = self.get_output(self.widget.Outputs.data)\n        self.assertTablesEqual(out, result)\n        np.testing.assert_equal(\n            out.ids, np.hstack((self.dataA.ids[1:], self.dataA.ids[:1])))\n\n    def test_output_merge_by_ids_outer_single_class(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        Source position (index) when all extra rows are matched and there is\n        only a single class variable in the output\"\"\"\n        domainA = self.dataA.domain\n        values = domainA.class_var.values\n        domain = Domain(domainA.attributes,\n                        DiscreteVariable(\"clsA\", values),\n                        domainA.metas)\n        result = Table(domain,\n                       np.array([[0, 0], [1, 1], [2, 0], [3, np.nan]]),\n                       np.array([[0], [1], [2], [np.nan]]),\n                       np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                                 [0.0, \"\"]]).astype(object))\n        self.widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        self.widget.merging = 2\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        # When Y is a single column, Table.Y returns a vector, not a 2d array,\n        # which cause an exception in outer_join's vstack for Y if extra data\n        # has no unmatched rows.\n        # This test also checks this condition.\n        self.send_signal(self.widget.Inputs.data, self.dataA[:, [0, \"clsA\", -1]])\n        self.send_signal(self.widget.Inputs.extra_data, self.dataA[:3, [1, -2]])\n        out = self.get_output(self.widget.Outputs.data)\n        self.assertTablesEqual(out, result)\n        np.testing.assert_equal(out.ids, self.dataA.ids)\n\n    def test_output_merge_by_index_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        Position (index)\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1],\n                             [2, 0, 2, np.nan], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]\n                            ]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_index_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by\n        Position (index)\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1], [2, 0, 2, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.controls.merging.buttons[1].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_index_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        Position (index)\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1],\n                             [2, 0, 2, np.nan], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.controls.merging.buttons[2].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_attribute_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        attribute\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes[1:],\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0], [1, 1, 1],\n                             [2, 0, np.nan], [3, 1, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[0], domainB[0])])\n        self.widget.commit()\n        output = self.get_output(self.widget.Outputs.data)\n        self.assertTablesEqual(output, result)\n        self.assertEqual(output.name, self.dataA.name)\n        np.testing.assert_array_equal(output.ids, self.dataA.ids)\n        self.assertEqual(output.attributes, self.dataA.attributes)\n\n    def test_output_merge_by_attribute_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by attribute\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes[1:],\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0], [1, 1, 1], [2, 0, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[0], domainB[0])])\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_attribute_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        attribute\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, 0, 0], [1, 1, 1, 1],\n                             [2, 0, 2, np.nan], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, 1], [2, 0], [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 1.0],\n                             [np.nan, \"m3\", 0.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[0], domainB[0])])\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_attribute_outer_same_attr(self):\n        \"\"\"Values of columns from extra aata are copied to left part if they\n        match\"\"\"\n        name = StringVariable(\"name\")\n        domainA = Domain([ContinuousVariable(\"x\")], None, [name])\n        domainB = Domain([ContinuousVariable(\"y\")], None, [name])\n        xA = np.array([[0], [1], [2]])\n        mA = np.array([[\"a\"], [\"b\"], [\"c\"]])\n        xB = np.array([[4], [5], [6], [7]])\n        mB = np.array([[\"b\"], [\"d\"], [\"a\"], [\"c\"]])\n        dataA = Table(domainA, xA, None, mA)\n        dataB = Table(domainB, xB, None, mB)\n\n        self.send_signal(self.widget.Inputs.data, dataA)\n        self.send_signal(self.widget.Inputs.extra_data, dataB)\n        self.widget.attr_boxes.set_state([(name, name)])\n        self.widget.controls.merging.buttons[self.widget.OuterJoin].click()\n        out = self.get_output(self.widget.Outputs.data)\n        np.testing.assert_equal(\n            out.X,\n            np.array([[0, 6], [1, 4], [2, 7], [np.nan, 5]]))\n        self.assertEqual(\" \".join(out.metas.flatten()), \"a a b b c c  d\")\n\n    def test_output_merge_by_class_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        class variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars, domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, 2, np.nan],\n                             [2, 0, 1, 1], [3, 1, np.nan, np.nan]])\n        result_Y = np.array([0, 1, 2, np.nan])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 0.0],\n                             [np.nan, \"m3\", 1.0], [0.0, \"m4\", np.nan]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[2], domainB[2])])\n        self.widget.commit()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_class_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by class\n        variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars, domainA.metas + domainB.metas)\n        result_X = np.array([[1, 1, 2, np.nan], [2, 0, 1, 1]])\n        result_Y = np.array([1, 2])\n        result_M = np.array([[1.0, \"m2\", 0.0], [np.nan, \"m3\", 1.0]]\n                           ).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state(\n            [(domainA.class_vars[0], domainB.class_vars[0])])\n        self.widget.controls.merging.buttons[1].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_class_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        class variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, 2, np.nan],\n                             [2, 0, 1, 1], [3, 1, np.nan, np.nan],\n                             [np.nan, np.nan, 0, 0]])\n        result_Y = np.array([[0, np.nan], [1, 0], [2, 1], [np.nan, np.nan],\n                             [np.nan, np.nan]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", 0.0],\n                             [np.nan, \"m3\", 1.0], [0.0, \"m4\", np.nan],\n                             [np.nan, \"\", np.nan]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state(\n            [(domainA.class_vars[0], domainB.class_vars[0])])\n        self.widget.controls.merging.buttons[2].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_meta_left(self):\n        \"\"\"Check output for merging option 'Append columns from Extra Data' by\n        meta variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, np.nan, np.nan],\n                             [2, 0, np.nan, np.nan], [3, 1, 2, np.nan]])\n        result_Y = np.array([[0, np.nan], [1, np.nan],\n                             [2, np.nan], [np.nan, 0]])\n        result_M = np.array([[0.0, \"m1\"], [1.0, \"m2\"], [np.nan, \"m3\"],\n                             [0.0, \"m4\"]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[-2], domainB[-1])])\n        self.widget.commit()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_meta_inner(self):\n        \"\"\"Check output for merging option 'Find matching rows' by meta\n        variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas)\n        result_X = np.array([[3, 1, 2, np.nan]])\n        result_Y = np.array([[np.nan, 0]])\n        result_M = np.array([[0.0, \"m4\"]]).astype(object)\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[-2], domainB[-1])])\n        self.widget.controls.merging.buttons[self.widget.InnerJoin].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def test_output_merge_by_meta_outer(self):\n        \"\"\"Check output for merging option 'Concatenate tables, merge rows' by\n        meta variable\"\"\"\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n        result_d = Domain(domainA.attributes + domainB.attributes,\n                          domainA.class_vars + domainB.class_vars,\n                          domainA.metas + domainB.metas)\n        result_X = np.array([[0, 0, np.nan, np.nan], [1, 1, np.nan, np.nan],\n                             [2, 0, np.nan, np.nan], [3, 1, 2, np.nan],\n                             [np.nan, np.nan, 0, 0], [np.nan, np.nan, 1, 1]])\n        result_Y = np.array([[0, np.nan], [1, np.nan], [2, np.nan],\n                             [np.nan, 0], [np.nan, np.nan], [np.nan, 1]])\n        result_M = np.array([[0.0, \"m1\", np.nan], [1.0, \"m2\", np.nan],\n                             [np.nan, \"m3\", np.nan], [0.0, \"m4\", 0.0],\n                             [np.nan, \"\", np.nan], [np.nan, \"\", 1.0]]\n                           ).astype(object)\n\n        result = Table(result_d, result_X, result_Y, result_M)\n\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.widget.attr_boxes.set_state([(domainA[-2], domainB[-1])])\n        self.widget.controls.merging.buttons[2].click()\n        self.assertTablesEqual(self.get_output(self.widget.Outputs.data), result)\n\n    def assertTablesEqual(self, table1, table2):\n        self.assertEqual(table1.domain, table2.domain)\n        np.testing.assert_array_equal(table1.X, table2.X)\n        np.testing.assert_array_equal(table1.Y, table2.Y)\n        np.testing.assert_array_equal(table1.metas.astype(str),\n                                      table2.metas.astype(str))\n\n    def test_best_match(self):\n        \"\"\"Check default merging attributes setup\"\"\"\n        widget = self.widget\n        indices = list(range(101))\n        indices.pop(26)\n        zoo = Table(\"zoo\")[indices]\n        zoo_images = Table(test_filename(\"datasets/zoo-with-images.tab\"))\n        self.send_signal(widget.Inputs.data, zoo)\n        self.send_signal(widget.Inputs.extra_data, zoo_images)\n        for i in range(3):\n            self.assertEqual(widget.attr_boxes.current_state(),\n                             [(zoo.domain[\"name\"], zoo_images.domain[\"name\"])],\n                             f\"wrong attributes chosen for merge_type={i}\")\n\n    def test_sparse(self):\n        data = Table(\"iris\")[::25]\n        data_ed_dense = Table(\"titanic\")[::300]\n        data_ed_sparse = Table(\"titanic\")[::300].to_sparse()\n        self.send_signal(\"Data\", data)\n\n        self.send_signal(\"Extra Data\", data_ed_dense)\n        output_dense = self.get_output(\"Data\")\n        self.assertFalse(sp.issparse(output_dense.X))\n        self.assertFalse(output_dense.is_sparse())\n\n        self.send_signal(\"Extra Data\", data_ed_sparse)\n        output_sparse = self.get_output(\"Data\")\n        self.assertTrue(sp.issparse(output_sparse.X))\n        self.assertTrue(output_sparse.is_sparse())\n\n        output_sparse.X = output_sparse.X.toarray()\n        self.assertTablesEqual(output_dense, output_sparse)\n\n    def test_commit_on_new_data(self):\n        \"\"\"Check that disabling auto apply doesn't block on new data\"\"\"\n        self.widget.auto_apply = False\n        self.widget.merging = 2\n        self.send_signal(self.widget.Inputs.data, self.dataA)\n        self.send_signal(self.widget.Inputs.extra_data, self.dataB)\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.data))\n\n    def test_multiple_attributes_left(self):\n        domainA, domainB = self.dataA.domain, self.dataB.domain\n\n        X = np.array([[0, 1], [1, 1], [3, np.nan], [np.nan, 0]])\n        Y = np.array([1, 2, 0, 0])\n        metas = np.array([[0, \"a\"], [1, \"b\"], [0, \"c\"], [0, \"d\"]])\n        dataA = Table(domainA, X, Y, metas)\n        dataA.name = \"dataA\"\n\n        X = np.array(\n            [[0, 0], [1, 0], [0, 1], [1, 1], [2, 0], [3, np.nan]])\n        Y = np.array([0, 0, 1, 1, 0, 1])\n        metas = np.array([[0], [1], [0], [1], [1], [1]])\n        dataB = Table(domainB, X, Y, metas)\n        dataB.name = \"dataB\"\n\n        self.send_signal(self.widget.Inputs.data, dataA)\n        self.send_signal(self.widget.Inputs.extra_data, dataB)\n        self.widget.attr_boxes.set_state(\n            [(domainA[0], domainB[0]), (domainA[1], domainB[1])])\n        self.widget.commit()\n        output = self.get_output(self.widget.Outputs.data)\n\n        self.assertEqual(output.name, dataA.name)\n        np.testing.assert_array_equal(output.ids, dataA.ids)\n        self.assertEqual(output.attributes, dataA.attributes)\n        np.testing.assert_equal(output.X, dataA.X)\n\n    def test_nonunique(self):\n        widget = self.widget\n        x = ContinuousVariable(\"x\")\n        d = DiscreteVariable(\"d\", values=tuple(\"abc\"))\n        domain = Domain([x, d], [])\n        dataA = Table.from_numpy(\n            domain, np.array([[1.0, 0], [1, 1], [2, 1]]))\n        dataB = Table.from_numpy(\n            domain, np.array([[1.0, 0], [2, 1], [3, 1]]))\n        dataB.ids = dataA.ids\n        self.send_signal(widget.Inputs.data, dataA)\n        self.send_signal(widget.Inputs.extra_data, dataB)\n        widget.merging = widget.InnerJoin\n\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n\n        widget.attr_boxes.set_state([(INSTANCEID, INSTANCEID)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.attr_boxes.set_state([(INDEX, INDEX)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.attr_boxes.set_state([(x, x)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.LeftJoin\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.InnerJoin\n        widget.attr_boxes.set_state([(x, x), (d, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNotNone(self.get_output(widget.Outputs.data))\n\n        widget.attr_boxes.set_state([(d, d)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.nonunique_left.is_shown())\n        self.assertTrue(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.LeftJoin\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertTrue(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        widget.merging = widget.InnerJoin\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.nonunique_left.is_shown())\n        self.assertTrue(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n        self.send_signal(widget.Inputs.data, None)\n        self.send_signal(widget.Inputs.extra_data, None)\n        self.assertFalse(widget.Error.nonunique_left.is_shown())\n        self.assertFalse(widget.Error.nonunique_right.is_shown())\n        self.assertIsNone(self.get_output(widget.Outputs.data))\n\n    def test_invalide_pairs(self):\n        widget = self.widget\n        x = ContinuousVariable(\"x\")\n        d = DiscreteVariable(\"d\", values=tuple(\"abc\"))\n        domain = Domain([x, d], [])\n        dataA = Table.from_numpy(\n            domain, np.array([[1.0, 0], [1, 1], [2, 1]]))\n        dataB = Table.from_numpy(\n            domain, np.array([[1.0, 0], [2, 1], [3, 1]]))\n        dataB.ids = dataA.ids\n        self.send_signal(widget.Inputs.data, dataA)\n        self.send_signal(widget.Inputs.extra_data, dataB)\n\n        widget.attr_boxes.set_state([(x, x), (d, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (INDEX, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertTrue(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (d, INDEX)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertTrue(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (INSTANCEID, d)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (d, INSTANCEID)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (INDEX, INSTANCEID)])\n        widget.unconditional_commit()\n        self.assertTrue(widget.Error.matching_id_with_sth.is_shown()\n                        or widget.Error.matching_index_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n        widget.attr_boxes.set_state([(x, x), (x, d)])\n        widget.unconditional_commit()\n        self.assertFalse(widget.Error.matching_id_with_sth.is_shown())\n        self.assertFalse(widget.Error.matching_index_with_sth.is_shown())\n        self.assertTrue(widget.Error.matching_numeric_with_nonnum.is_shown())\n\n    def test_duplicate_names(self):\n        domain = Domain([ContinuousVariable(\"C1\")],\n                        metas=[DiscreteVariable(\"Feature\", values=(\"A\", \"B\"))])\n        data = Table(domain, np.array([[1.], [0.]]),\n                     metas=np.array([[1.], [0.]]))\n        domain = Domain([ContinuousVariable(\"C1\")],\n                        metas=[StringVariable(\"Feature\")])\n        extra_data = Table(domain, np.array([[1.], [0.]]),\n                           metas=np.array([[\"A\"], [\"B\"]]))\n        self.send_signal(self.widget.Inputs.data, data)\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        self.assertTrue(self.widget.Warning.renamed_vars.is_shown())\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.metas],\n                             [\"Feature (1)\", \"Feature (2)\"])\n\n    def test_keep_non_duplicate_variables(self):\n        domain = Domain([ContinuousVariable(\"A\"), ContinuousVariable(\"B\")])\n        data = Table(domain, np.array([[0., 0], [0, 1]]))\n        extra_data = Table(domain, np.array([[0., 1], [0, 1]]))\n        self.send_signal(self.widget.Inputs.data, data)\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.variables],\n                             [\"A\", \"B (1)\", \"B (2)\"])\n\n    def test_keep_non_duplicate_variables_missing_rows(self):\n        c = DiscreteVariable(\"C\", values=(\"a\", \"b\", \"c\"))\n        domain = Domain([ContinuousVariable(\"A\"), ContinuousVariable(\"B\"), c])\n        data = Table(domain, np.array([[0., 0, 0], [1, 1, 1]]))\n        extra_data = Table(domain, np.array([[0., 1, 1], [0, 1, 2]]))\n        self.send_signal(self.widget.Inputs.data, data)\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        self.widget.attr_boxes.set_state([(c, c)])\n        self.widget.attr_boxes.emit_list()\n\n        # Only one row is matched; A has different values and it's duplicated,\n        # and B has the same values, so we get only one copy\n        self.widget.merging = self.widget.InnerJoin\n        self.widget.unconditional_commit()\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.variables],\n                             [\"A (1)\", \"B\", \"C\", \"A (2)\"])\n\n        # Table has additional rows; keep all columns\n        self.widget.merging = self.widget.OuterJoin\n        self.widget.unconditional_commit()\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual(\n            [m.name for m in merged_data.domain.variables],\n            [\"A (1)\", \"B (1)\", \"C (1)\", \"A (2)\", \"B (2)\", \"C (2)\"])\n\n        # First row is unmatched, data for B(2) is missing, but attribute\n        # shouldn't be added\n        extra_data = Table(domain, np.array([[1., 1, 1], [0, 1, 2]]))\n        self.send_signal(self.widget.Inputs.extra_data, extra_data)\n        self.widget.merging = self.widget.LeftJoin\n        self.widget.unconditional_commit()\n        merged_data = self.get_output(self.widget.Outputs.data)\n        self.assertListEqual([m.name for m in merged_data.domain.variables],\n                             [\"A\", \"B\", \"C\"])\n\n    def test_summary(self):\n        \"\"\"Check if the status bar is updated when data is received\"\"\"\n        data = self.dataA\n        info = self.widget.info\n        no_input, no_output = \"No data on input\", \"No data on output\"\n\n        self.send_signal(self.widget.Inputs.data, data)\n        data_list = [(\"Data\", data), (\"Extra data\", None)]\n        summary, details = f\"{len(data)}, 0\", format_multiple_summaries(data_list)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.assertIsInstance(info._StateInfo__output_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n        self.send_signal(self.widget.Inputs.extra_data, data)\n        data_list = [(\"Data\", data), (\"Extra data\", data)]\n        summary = f\"{len(data)}, {len(data)}\"\n        details = format_multiple_summaries(data_list)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        output = self.get_output(self.widget.Outputs.data)\n        summary, details = f\"{len(output)}\", format_summary_details(output)\n        self.assertEqual(info._StateInfo__output_summary.brief, summary)\n        self.assertEqual(info._StateInfo__output_summary.details, details)\n\n        self.send_signal(self.widget.Inputs.data, None)\n        data_list = [(\"Data\", None), (\"Extra data\", data)]\n        summary, details = f\"0, {len(data)}\", format_multiple_summaries(data_list)\n        self.assertEqual(info._StateInfo__input_summary.brief, summary)\n        self.assertEqual(info._StateInfo__input_summary.details, details)\n        self.assertIsInstance(info._StateInfo__output_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n        self.send_signal(self.widget.Inputs.extra_data, None)\n        self.assertIsInstance(info._StateInfo__input_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__input_summary.details, no_input)\n        self.assertIsInstance(info._StateInfo__output_summary, StateInfo.Empty)\n        self.assertEqual(info._StateInfo__output_summary.details, no_output)\n\n    def test_empty_tables(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.dataA[:0])\n        self.send_signal(widget.Inputs.extra_data, self.dataB[:0])\n\n\nclass MergeDataContextHandlerTest(unittest.TestCase):\n    # These units are too small to test individually, so they are tested\n    # within their function in the widget.\n\n    # The following test only covers obscure cases that seem to appear only\n    # within the context of some tests and can't appear in real world.\n    def test_malformed_contexts(self):\n        widget = Mock()\n        handler = MergeDataContextHandler()\n        # pylint: disable=protected-access\n        self.assertEqual(handler._encode_domain(None), {})\n\n        widget.current_context = None\n        handler.settings_from_widget(widget)  # mustn't crash\n        handler.settings_to_widget(widget)  # mustn't crash\n\n    def test_attr_pairs_not_present(self):\n        data = Table(\"iris\")\n\n        context = SimpleNamespace(values={})\n        widget = SimpleNamespace(\n            current_context=context, attr_pairs=(\"a\", \"b\")\n        )\n        handler = MergeDataContextHandler()\n\n        handler.settings_to_widget(widget)  # mustn't crash\n        # no attr_pairs in context -> handler must not change widget.attr_pairs\n        self.assertTupleEqual(widget.attr_pairs, (\"a\", \"b\"))\n\n        context = SimpleNamespace(\n            values={\n                \"attr_pairs\": [((data.domain[0], 100), (data.domain[1], 100))]\n            }\n        )\n        widget = SimpleNamespace(\n            current_context=context,\n            attr_pairs=(\"a\", \"b\"),\n            data=data,\n            extra_data=data,\n        )\n\n        handler.settings_to_widget(widget)  # mustn't crash\n        # values taken from context\n        self.assertListEqual(\n            widget.attr_pairs, [(data.domain[0], data.domain[1])]\n        )\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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/Orange3.egg-link (link to .)\nAdding Orange3 3.29.0.dev0+528aadc to easy-install.pth file\nInstalling orange-canvas script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb\nProcessing dependencies for Orange3==3.29.0.dev0+528aadc\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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/bin\nInstalling pip3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.venv/bin\nInstalling pip3.10 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/bin\nInstalling f2py3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.venv/bin\nInstalling f2py3.7 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages\nSearching for httpx==0.16.1\nBest match: httpx 0.16.1\nAdding httpx 0.16.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/bin\nInstalling pyrcc5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.venv/bin\nInstalling pyuic5 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/bin\nInstalling pyftmerge script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.venv/bin\nInstalling pyftsubset script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.venv/bin\nInstalling ttx script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages\nSearching for httpcore==0.12.3\nBest match: httpcore 0.12.3\nAdding httpcore 0.12.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages\nSearching for h11==0.14.0\nBest match: h11 0.14.0\nAdding h11 0.14.0 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages\nFinished processing dependencies for Orange3==3.29.0.dev0+528aadc\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 150ms\nPrepared 18 packages in 3ms\nInstalled 18 packages in 109ms\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 53 packages in 317ms\nInstalled 47 packages in 614ms\n + baycomp==1.0.3\n + bottleneck==1.4.0\n + certifi==2024.12.14\n + cffi==1.15.1\n + chardet==5.2.0\n + charset-normalizer==3.4.1\n + cryptography==44.0.0\n + cycler==0.11.0\n + et-xmlfile==1.1.0\n + fonttools==4.38.0\n + h11==0.14.0\n + httpcore==0.12.3\n + httpx==0.16.1\n + idna==3.10\n + importlib-resources==5.12.0\n + jaraco-classes==3.2.3\n + jeepney==0.8.0\n + joblib==1.3.2\n + keyring==24.1.1\n + keyrings-alt==4.2.0\n + kiwisolver==1.4.5\n + matplotlib==3.5.3\n + more-itertools==9.1.0\n + networkx==2.6.3\n + openpyxl==3.1.3\n + opentsne==1.0.1\n + pandas==1.3.5\n + pillow==9.5.0\n + pip==24.0\n + pycparser==2.21\n + pyparsing==3.1.4\n + python-dateutil==2.9.0.post0\n + python-louvain==0.16\n + pytz==2024.2\n + pyyaml==6.0.1\n + requests==2.31.0\n + 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 39 packages in 65ms\nInstalled 13 packages in 331ms\n + anyqt==0.2.0\n + attrs==24.2.0\n + cattrs==23.1.2\n + commonmark==0.9.1\n + dictdiffer==0.9.0\n + docutils==0.20.1\n + orange-canvas-core==0.1.35\n + orange-widget-base==4.22.0\n + platformdirs==4.0.0\n + pyqtgraph==0.12.4\n + qasync==0.25.0\n + requests-cache==1.1.1\n + url-normalize==1.4.3\nResolved 2 packages in 3ms\nInstalled 2 packages in 19ms\n + psycopg2==2.9.9\n + pymssql==2.3.2\nResolved 19 packages in 29ms\nInstalled 5 packages in 1.24s\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 33ms\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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:3413:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 3413 |   __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:4869:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 4869 |   __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:5615:43: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5615 |           for (__pyx_t_19 = 0; __pyx_t_19 < __pyx_t_18; __pyx_t_19+=1) {\n      |                                           ^\nOrange/classification/_tree_scorers.c:5736:39: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5736 |           __pyx_t_20 = ((__pyx_v_left >= __pyx_v_min_leaf) != 0);\n      |                                       ^~\nOrange/classification/_tree_scorers.c:5742:53: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5742 |           __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:7202:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7202 |         for (__pyx_t_11 = 0; __pyx_t_11 < __pyx_t_10; __pyx_t_11+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c: In function ‘__pyx_pf_6Orange_14classification_13_tree_scorers_16compute_predictions_csc’:\nOrange/classification/_tree_scorers.c:7857:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7857 |         for (__pyx_t_11 = 0; __pyx_t_11 < __pyx_t_10; __pyx_t_11+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c:7917:143: warning: comparison of integer expressions of different signedness: ‘__pyx_t_5numpy_int32_t’ {aka ‘int’} and ‘unsigned int’ [-Wsign-compare]\n 7917 |               __pyx_t_15 = (((*((__pyx_t_5numpy_int32_t *) ( /* dim=0 */ (__pyx_v_indices.data + __pyx_t_14 * __pyx_v_indices.strides[0]) ))) != __pyx_v_i) != 0);\n      |                                                                                                                                               ^~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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_528aadcc10e6424095a74508f07432a16ac980cb/.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": 1,
    "new_commit_res_stdout": "============================= test session starts ==============================\nplatform linux -- Python 3.7.9, pytest-7.4.4, pluggy-1.2.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb\ncollected 53 items\n\nr2e_tests/test_1.py sssE................................................ [ 98%]\n.                                                                        [100%]\n\n==================================== ERRORS ====================================\n_______________________ ERROR at setup of test_filename ________________________\nfile /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/Orange/tests/__init__.py, line 65\n  def test_filename(path):\nE       fixture 'path' not found\n>       available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory\n>       use 'pytest --fixtures [testpath]' for help on them.\n\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/Orange/tests/__init__.py:65\n==================================== PASSES ====================================\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_add_row_button\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_attr_combo_tooltips\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_best_match\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_combo_box_sync\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_combobox_items\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_commit_on_new_data\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_dont_remove_single_row\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_duplicate_names\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_empty_tables\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_image_export\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_input_remove\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_invalide_pairs\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_keep_non_duplicate_variables\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_keep_non_duplicate_variables_missing_rows\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_match_attr_name\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_match_settings\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_migrate_settings\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_migrate_settings_attr_pairs_data_none\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_migrate_settings_attr_pairs_extra_none\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_migrate_settings_attr_pairs_id_idx\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_migrate_settings_attr_pairs_vars\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_multiple_attributes_left\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_no_matches\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_nonunique\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_attribute_inner\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_attribute_left\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_attribute_outer\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_attribute_outer_same_attr\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_class_inner\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_class_left\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_class_outer\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_ids_inner\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_ids_outer\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_ids_outer_single_class\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_index_inner\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_index_left\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_index_outer\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_meta_inner\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_meta_left\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_meta_outer\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_remove_row\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_report\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_retrieve_settings\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_sparse\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_summary\nPASSED r2e_tests/test_1.py::MergeDataContextHandlerTest::test_attr_pairs_not_present\nPASSED r2e_tests/test_1.py::MergeDataContextHandlerTest::test_malformed_contexts\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:244: image exporting not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:238: minimum size not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:250: msg base class not tested as .widget was not set\nERROR r2e_tests/test_1.py::test_filename\n==================== 49 passed, 3 skipped, 1 error in 5.87s ====================\nWidgets left: 0    Max widgets: 2982 \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_528aadcc10e6424095a74508f07432a16ac980cb\ncollected 53 items\n\nr2e_tests/test_1.py sssE........F....................................... [ 98%]\n.                                                                        [100%]\n\n==================================== ERRORS ====================================\n_______________________ ERROR at setup of test_filename ________________________\nfile /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/Orange/tests/__init__.py, line 65\n  def test_filename(path):\nE       fixture 'path' not found\n>       available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_property, record_testsuite_property, record_xml_attribute, recwarn, tmp_path, tmp_path_factory, tmpdir, tmpdir_factory\n>       use 'pytest --fixtures [testpath]' for help on them.\n\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_528aadcc10e6424095a74508f07432a16ac980cb/Orange/tests/__init__.py:65\n=================================== FAILURES ===================================\n______________________ TestOWMergeData.test_empty_tables _______________________\n\nself = <r2e_tests.test_1.TestOWMergeData testMethod=test_empty_tables>\n\n    def test_empty_tables(self):\n        widget = self.widget\n        self.send_signal(widget.Inputs.data, self.dataA[:0])\n>       self.send_signal(widget.Inputs.extra_data, self.dataB[:0])\n\nr2e_tests/test_1.py:1069: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n.venv/lib/python3.7/site-packages/orangewidget/tests/base.py:446: in send_signal\n    widget=widget, wait=wait)\n.venv/lib/python3.7/site-packages/orangewidget/tests/base.py:472: in send_signals\n    widget.handleNewSignals()\nOrange/widgets/data/owmergedata.py:370: in handleNewSignals\n    self.extra_data and self.extra_data.domain)\n.venv/lib/python3.7/site-packages/orangewidget/widget.py:1350: in openContext\n    self.settingsHandler.open_context(self, *a)\nOrange/widgets/data/owmergedata.py:191: in open_context\n    self._encode_domain(domain1),\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <Orange.widgets.data.owmergedata.MergeDataContextHandler object at 0x7cc01fe0fb10>\ndomain = [\n]\n\n    def _encode_domain(self, domain):\n        if domain is None:\n            return {}\n>       all_vars = chain(domain.variables, domain.metas)\nE       AttributeError: 'Table' object has no attribute 'variables'\n\nOrange/widgets/data/owmergedata.py:210: AttributeError\n==================================== PASSES ====================================\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_add_row_button\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_attr_combo_tooltips\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_best_match\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_combo_box_sync\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_combobox_items\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_commit_on_new_data\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_dont_remove_single_row\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_duplicate_names\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_image_export\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_input_remove\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_invalide_pairs\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_keep_non_duplicate_variables\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_keep_non_duplicate_variables_missing_rows\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_match_attr_name\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_match_settings\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_migrate_settings\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_migrate_settings_attr_pairs_data_none\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_migrate_settings_attr_pairs_extra_none\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_migrate_settings_attr_pairs_id_idx\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_migrate_settings_attr_pairs_vars\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_minimum_size\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_multiple_attributes_left\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_no_matches\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_nonunique\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_attribute_inner\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_attribute_left\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_attribute_outer\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_attribute_outer_same_attr\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_class_inner\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_class_left\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_class_outer\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_ids_inner\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_ids_outer\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_ids_outer_single_class\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_index_inner\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_index_left\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_index_outer\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_meta_inner\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_meta_left\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_output_merge_by_meta_outer\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_remove_row\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_report\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_retrieve_settings\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_sparse\nPASSED r2e_tests/test_1.py::TestOWMergeData::test_summary\nPASSED r2e_tests/test_1.py::MergeDataContextHandlerTest::test_attr_pairs_not_present\nPASSED r2e_tests/test_1.py::MergeDataContextHandlerTest::test_malformed_contexts\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:244: image exporting not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:238: minimum size not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:250: msg base class not tested as .widget was not set\nERROR r2e_tests/test_1.py::test_filename\nFAILED r2e_tests/test_1.py::TestOWMergeData::test_empty_tables - AttributeErr...\n=============== 1 failed, 48 passed, 3 skipped, 1 error in 4.43s ===============\nWidgets left: 0    Max widgets: 2982 \n\n",
    "old_commit_res_stderr": ""
}
Orange/widgets/data/owmergedata.py Orange/widgets/data/tests/test_owmergedata.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** AttributeError When Merging Empty Tables in MergeData Widget

**Description:**
When attempting to merge empty tables using the MergeData widget, an `AttributeError` is raised. This occurs because the widget's context handler does not properly handle `Table` objects without a domain, leading to a failure when accessing the `variables` attribute.

**Example Buggy Code:**
```python
# Attempting to merge empty tables
widget = MergeDataWidget()
empty_table = Table(domain=[])
widget.send_signal('data', empty_table)
widget.send_signal('extra_data', empty_table)
```

**Expected Behavior:**
The MergeData widget should gracefully handle empty tables without raising errors, allowing users to merge them without issues.

**Actual Behavior:**
An `AttributeError` is raised with the message `'Table' object has no attribute 'variables'`, preventing the merge operation from completing successfully.

**Error Message:**
```
AttributeError: 'Table' object has no attribute 'variables'
```
[/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/owmergedata.py
from collections import namedtuple
from itertools import chain, product

import numpy as np

from AnyQt.QtCore import Qt, QModelIndex, pyqtSignal as Signal
from AnyQt.QtWidgets import (
    QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout
)
from orangewidget.utils.combobox import ComboBoxSearch

import Orange
from Orange.data import StringVariable, ContinuousVariable, Variable
from Orange.data.util import hstack, get_unique_names_duplicates
from Orange.widgets import widget, gui
from Orange.widgets.settings import Setting, ContextHandler, ContextSetting
from Orange.widgets.utils import vartype
from Orange.widgets.utils.itemmodels import DomainModel
from Orange.widgets.utils.sql import check_sql_input
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.utils.state_summary import format_multiple_summaries, \
    format_summary_details
from Orange.widgets.widget import Input, Output, Msg

INSTANCEID = "Instance id"
INDEX = "Row index"


class ConditionBox(QWidget):
    vars_changed = Signal(list)

    RowItems = namedtuple(
        "RowItems",
        ("pre_label", "left_combo", "in_label", "right_combo",
         "remove_button", "add_button"))

    def __init__(self, parent, model_left, model_right, pre_label, in_label):
        super().__init__(parent)
        self.model_left = model_left
        self.model_right = model_right
        self.pre_label, self.in_label = pre_label, in_label
        self.rows = []
        self.setLayout(QVBoxLayout())
        self.layout().setContentsMargins(0, 0, 0, 0)
        self.layout().setSpacing(0)
        self.setMouseTracking(True)

    def add_row(self):
        def sync_combos():
            combo = self.sender()
            index = combo.currentIndex()
            model = combo.model()

            other = ({row_items.left_combo, row_items.right_combo}
                     - {combo}).pop()
            other_index = other.currentIndex()
            other_model = other.model()

            if 0 <= index < len(combo.model()):
                var = model[index]
                if isinstance(var, str):
                    other.setCurrentText(model[index])
                elif isinstance(other_model[other_index], str):
                    for other_var in other_model:
                        if isinstance(other_var, Variable) \
                                and var.name == other_var.name \
                                and (type(other_var) is type(var)
                                     or (not var.is_continuous
                                         and not other_var.is_continuous)):
                            other.setCurrentText(var.name)
                            break

            self.emit_list()

        def get_combo(model):
            combo = ComboBoxSearch(self)
            combo.setModel(model)
            # We use signal activated because it is triggered only on user
            # interaction, not programmatically.
            combo.activated.connect(sync_combos)
            return combo

        def get_button(label, callback):
            button = QPushButton(label, self)
            button.setFlat(True)
            button.setFixedWidth(12)
            button.clicked.connect(callback)
            return button

        row = self.layout().count()
        row_items = self.RowItems(
            QLabel("and" if row else self.pre_label),
            get_combo(self.model_left),
            QLabel(self.in_label),
            get_combo(self.model_right),
            get_button("×", self.on_remove_row),
            get_button("+", self.on_add_row)
        )
        layout = QHBoxLayout()
        layout.setSpacing(10)
        self.layout().addLayout(layout)
        layout.addStretch(10)
        for item in row_items:
            layout.addWidget(item)
        self.rows.append(row_items)
        self._reset_buttons()

    def remove_row(self, row):
        self.layout().takeAt(self.rows.index(row))
        self.rows.remove(row)
        for item in row:
            item.deleteLater()
        self._reset_buttons()

    def on_add_row(self, _):
        self.add_row()
        self.emit_list()

    def on_remove_row(self):
        if len(self.rows) == 1:
            return
        button = self.sender()
        for row in self.rows:
            if button is row.remove_button:
                self.remove_row(row)
                break
        self.emit_list()

    def _reset_buttons(self):
        def endis(button, enable, text):
            button.setEnabled(enable)
            button.setText(text if enable else "")

        self.rows[0].pre_label.setText(self.pre_label)
        single_row = len(self.rows) == 1
        endis(self.rows[0].remove_button, not single_row, "×")
        endis(self.rows[-1].add_button, True, "+")
        if not single_row:
            endis(self.rows[-2].add_button, False, "")

    def current_state(self):
        def get_var(model, combo):
            return model[combo.currentIndex()]

        return [(get_var(self.model_left, row.left_combo),
                 get_var(self.model_right, row.right_combo))
                for row in self.rows]

    def set_state(self, values):
        while len(self.rows) > len(values):
            self.remove_row(self.rows[0])
        while len(self.rows) < len(values):
            self.add_row()
        for (val_left, val_right), row in zip(values, self.rows):
            row.left_combo.setCurrentIndex(self.model_left.indexOf(val_left))
            row.right_combo.setCurrentIndex(self.model_right.indexOf(val_right))

    def emit_list(self):
        self.vars_changed.emit(self.current_state())


class DomainModelWithTooltips(DomainModel):
    def data(self, index, role=Qt.DisplayRole):
        if role == Qt.ToolTipRole \
                and isinstance(index, QModelIndex) and index.isValid():
            if index.row() == 0:
                return "Match rows sequentially"
            if index.row() == 1:
                return "Re-match rows from tables obtained from the same " \
                       "source,\n" \
                       "e.g. data from the same file that was split within " \
                       "the workflow."
        return super().data(index, role)


class MergeDataContextHandler(ContextHandler):
    # `widget` is used as an argument in most methods
    # pylint: disable=redefined-outer-name
    # context handlers override methods using different signatures
    # pylint: disable=arguments-differ

    def new_context(self, variables1, variables2):
        context = super().new_context()
        context.variables1 = variables1
        context.variables2 = variables2
        return context

    def open_context(self, widget, domain1, domain2):
        if domain1 is not None and domain2 is not None:
            super().open_context(widget,
                                 self._encode_domain(domain1),
                                 self._encode_domain(domain2))

    @staticmethod
    def encode_variables(variables):
        return [(v.name, 100 + vartype(v))
                if isinstance(v, Variable) else (v, 100)
                for v in variables]

    @staticmethod
    def decode_pair(widget, pair):
        left_domain = widget.data and widget.data.domain
        right_domain = widget.extra_data and widget.extra_data.domain
        return tuple(var[0] if var[0] in (INDEX, INSTANCEID) else domain[var[0]]
                     for domain, var in zip((left_domain, right_domain), pair))

    def _encode_domain(self, domain):
        if domain is None:
            return {}
        all_vars = chain(domain.variables, domain.metas)
        return dict(self.encode_variables(all_vars))

    def settings_from_widget(self, widget, *_args):
        context = widget.current_context
        if context is None:
            return
        context.values["attr_pairs"] = [self.encode_variables(row)
                                        for row in widget.attr_pairs]

    def settings_to_widget(self, widget, *_args):
        context = widget.current_context
        if context is None:
            return
        pairs = context.values.get("attr_pairs")
        if pairs:
            # attr_pairs is schema only setting which means it is not always
            # present. When not present leave widgets default.
            widget.attr_pairs = [
                self.decode_pair(widget, pair) for pair in pairs
            ]

    def match(self, context, variables1, variables2):
        def matches(part, variables):
            return all(var[1] == 100 and var[0] in variables
                       or variables.get(var[0], -1) == var[1] for var in part)

        if (variables1, variables2) == (context.variables1, context.variables2):
            return self.PERFECT_MATCH

        if "attr_pairs" in context.values:
            left, right = zip(*context.values["attr_pairs"])
            if matches(left, variables1) and matches(right, variables2):
                return 0.5

        return self.NO_MATCH


class OWMergeData(widget.OWWidget):
    name = "Merge Data"
    description = "Merge datasets based on the values of selected features."
    icon = "icons/MergeData.svg"
    priority = 1110
    keywords = ["join"]

    class Inputs:
        data = Input("Data", Orange.data.Table, default=True, replaces=["Data A"])
        extra_data = Input("Extra Data", Orange.data.Table, replaces=["Data B"])

    class Outputs:
        data = Output("Data",
                      Orange.data.Table,
                      replaces=["Merged Data A+B", "Merged Data B+A", "Merged Data"])

    LeftJoin, InnerJoin, OuterJoin = range(3)
    OptionNames = ("Append columns from Extra data",
                   "Find matching pairs of rows",
                   "Concatenate tables")
    OptionDescriptions = (
        "The first table may contain, for instance, city names,\n"
        "and the second would be a list of cities and their coordinates.\n"
        "Columns with coordinates would then be appended to the output.",

        "Input tables contain different features describing the same data "
        "instances.\n"
        "Output contains matched instances. Rows without matches are removed.",

        "Input tables contain different features describing the same data "
        "instances.\n"
        "Output contains all instances. Data from merged instances is "
        "merged into single rows."
    )

    UserAdviceMessages = [
        widget.Message(
            "Confused about merging options?\nSee the tooltips!",
            "merging_types")]

    settingsHandler = MergeDataContextHandler()
    attr_pairs = ContextSetting(None, schema_only=True)
    merging = Setting(LeftJoin)
    auto_apply = Setting(True)
    settings_version = 2

    want_main_area = False
    resizing_enabled = False

    class Warning(widget.OWWidget.Warning):
        renamed_vars = Msg("Some variables have been renamed "
                           "to avoid duplicates.\n{}")

    class Error(widget.OWWidget.Error):
        matching_numeric_with_nonnum = Msg(
            "Numeric and non-numeric columns ({} and {}) cannot be matched.")
        matching_index_with_sth = Msg("Row index cannot be matched with {}.")
        matching_id_with_sth = Msg("Instance cannot be matched with {}.")
        nonunique_left = Msg(
            "Some combinations of values on the left appear in multiple rows.\n"
            "For this type of merging, every possible combination of values "
            "on the left should appear at most once.")
        nonunique_right = Msg(
            "Some combinations of values on the right appear in multiple rows."
            "\n"
            "Every possible combination of values on the right should appear "
            "at most once.")

    def __init__(self):
        super().__init__()

        self.data = None
        self.extra_data = None

        content = [
            INDEX, INSTANCEID,
            DomainModel.ATTRIBUTES, DomainModel.CLASSES, DomainModel.METAS]
        self.model = DomainModelWithTooltips(content)
        self.extra_model = DomainModelWithTooltips(content)

        self.info.set_input_summary(self.info.NoInput)
        self.info.set_output_summary(self.info.NoOutput)

        grp = gui.radioButtons(
            self.controlArea, self, "merging", box="Merging",
            btnLabels=self.OptionNames, tooltips=self.OptionDescriptions,
            callback=self.change_merging)

        self.attr_boxes = ConditionBox(
            self, self.model, self.extra_model, "", "matches")
        self.attr_boxes.add_row()
        box = gui.vBox(self.controlArea, box="Row matching")
        box.layout().addWidget(self.attr_boxes)

        gui.auto_apply(self.buttonsArea, self)
        # connect after wrapping self.commit with gui.auto_commit!
        self.attr_boxes.vars_changed.connect(self.commit)
        self.attr_boxes.vars_changed.connect(self.store_combo_state)
        self.settingsAboutToBePacked.connect(self.store_combo_state)

    def change_merging(self):
        self.commit()

    @Inputs.data
    @check_sql_input
    def set_data(self, data):
        self.data = data
        self.model.set_domain(data.domain if data else None)

    @Inputs.extra_data
    @check_sql_input
    def set_extra_data(self, data):
        self.extra_data = data
        self.extra_model.set_domain(data.domain if data else None)

    def store_combo_state(self):
        self.attr_pairs = self.attr_boxes.current_state()

    def handleNewSignals(self):
        self.closeContext()
        self.attr_pairs = [self._find_best_match()]
        self.openContext(self.data and self.data.domain,
                         self.extra_data and self.extra_data.domain)
        self.attr_boxes.set_state(self.attr_pairs)

        summary, details, kwargs = self.info.NoInput, "", {}
        if self.data or self.extra_data:
            n_data = len(self.data) if self.data else 0
            n_extra_data = len(self.extra_data) if self.extra_data else 0
            summary = f"{self.info.format_number(n_data)}, " \
                      f"{self.info.format_number(n_extra_data)}"
            kwargs = {"format": Qt.RichText}
            details = format_multiple_summaries([
                ("Data", self.data),
                ("Extra data", self.extra_data)
            ])
        self.info.set_input_summary(summary, details, **kwargs)

        self.unconditional_commit()

    def _find_best_match(self):
        def get_unique_str_metas_names(model_):
            return [m for m in model_ if isinstance(m, StringVariable)]

        attr, extra_attr, n_max_intersect = INDEX, INDEX, 0
        str_metas = get_unique_str_metas_names(self.model)
        extra_str_metas = get_unique_str_metas_names(self.extra_model)
        for m_a, m_b in product(str_metas, extra_str_metas):
            col = self.data[:, m_a].metas
            extra_col = self.extra_data[:, m_b].metas
            if col.size and extra_col.size \
                    and isinstance(col[0][0], str) \
                    and isinstance(extra_col[0][0], str):
                n_inter = len(np.intersect1d(col, extra_col))
                if n_inter > n_max_intersect:
                    n_max_intersect, attr, extra_attr = n_inter, m_a, m_b
        return attr, extra_attr

    def commit(self):
        self.clear_messages()
        merged = self.merge() if self.data and self.extra_data else None
        self.Outputs.data.send(merged)
        details = format_summary_details(merged) if merged else ""
        summary = len(merged) if merged else self.info.NoOutput
        self.info.set_output_summary(summary, details)

    def send_report(self):
        # pylint: disable=invalid-sequence-index
        self.report_items((
            ("Merging", self.OptionNames[self.merging]),
            ("Match",
             ", ".join(
                 f"{self._get_col_name(left)} with {self._get_col_name(right)}"
                 for left, right in self.attr_boxes.current_state()))))

    def merge(self):
        # pylint: disable=invalid-sequence-index
        pairs = self.attr_boxes.current_state()
        if not self._check_pair_types(pairs):
            return None
        left_vars, right_vars = zip(*pairs)
        left_mask = np.full(len(self.data), True)
        left = np.vstack(tuple(self._values(self.data, var, left_mask)
                               for var in left_vars)).T
        right_mask = np.full(len(self.extra_data), True)
        right = np.vstack(tuple(self._values(self.extra_data, var, right_mask)
                                for var in right_vars)).T
        if not self._check_uniqueness(left, left_mask, right, right_mask):
            return None
        method = self._merge_methods[self.merging]
        lefti, righti, rightu = method(self, left, left_mask, right, right_mask)
        reduced_extra_data = \
            self._compute_reduced_extra_data(right_vars, lefti, righti, rightu)
        return self._join_table_by_indices(
            reduced_extra_data, lefti, righti, rightu)

    def _check_pair_types(self, pairs):
        for left, right in pairs:
            if isinstance(left, ContinuousVariable) \
                    != isinstance(right, ContinuousVariable):
                self.Error.matching_numeric_with_nonnum(left, right)
                return False
            if INDEX in (left, right) and left != right:
                self.Error.matching_index_with_sth(
                    self._get_col_name(({left, right} - {INDEX}).pop()))
                return False
            if INSTANCEID in (left, right) and left != right:
                self.Error.matching_id_with_sth(
                    self._get_col_name(({left, right} - {INSTANCEID}).pop()))
                return False
        return True

    @staticmethod
    def _get_col_name(obj):
        return f"'{obj.name}'" if isinstance(obj, Variable) else obj.lower()

    def _check_uniqueness(self, left, left_mask, right, right_mask):
        ok = True
        masked_right = right[right_mask]
        if len(set(map(tuple, masked_right))) != len(masked_right):
            self.Error.nonunique_right()
            ok = False
        if self.merging != self.LeftJoin:
            masked_left = left[left_mask]
            if len(set(map(tuple, masked_left))) != len(masked_left):
                self.Error.nonunique_left()
                ok = False
        return ok

    def _compute_reduced_extra_data(self,
                                    right_match_vars, lefti, righti, rightu):
        """Prepare a table with extra columns that will appear in the merged
        table"""
        domain = self.data.domain
        extra_domain = self.extra_data.domain

        def var_needed(var):
            if rightu is not None and rightu.size:
                return True
            if var in right_match_vars and self.merging != self.OuterJoin:
                return False
            if var not in domain:
                return True
            both_defined = (lefti != -1) * (righti != -1)
            left_col = \
                self.data.get_column_view(var)[0][lefti[both_defined]]
            right_col = \
                self.extra_data.get_column_view(var)[0][righti[both_defined]]
            if var.is_primitive():
                left_col = left_col.astype(float)
                right_col = right_col.astype(float)
                mask_left = np.isfinite(left_col)
                mask_right = np.isfinite(right_col)
                return not (
                    np.all(mask_left == mask_right)
                    and np.all(left_col[mask_left] == right_col[mask_right]))
            else:
                return not np.all(left_col == right_col)

        extra_vars = [
            var for var in chain(extra_domain.variables, extra_domain.metas)
            if var_needed(var)]
        return self.extra_data[:, extra_vars]

    @staticmethod
    def _values(data, var, mask):
        """Return an iterotor over keys for rows of the table."""
        if var == INDEX:
            return np.arange(len(data))
        if var == INSTANCEID:
            return np.fromiter(
                (inst.id for inst in data), count=len(data), dtype=np.int)
        col = data.get_column_view(var)[0]
        if var.is_primitive():
            col = col.astype(float, copy=False)
            nans = np.isnan(col)
            mask *= ~nans
            if var.is_discrete:
                col = col.astype(int)
                col[nans] = len(var.values)
                col = np.array(var.values + (np.nan, ))[col]
        else:
            col = col.copy()
            defined = col.astype(bool)
            mask *= defined
            col[~mask] = np.nan
        return col

    def _left_join_indices(self, left, left_mask, right, right_mask):
        """Compute a two-row array of indices:
        - the first row contains indices for the primary table,
        - the second row contains the matching rows in the extra table or -1"""
        data = self.data
        # Don't match nans. This is needed since numpy may change nan to string
        # nan, so nan's will match each other
        indices = np.arange(len(right))
        indices[~right_mask] = -1
        if right.shape[1] == 1:
            # The more common case can be handled faster
            right_map = dict(zip(right.flatten(), indices))
            righti = (right_map.get(val, -1) for val in left.flatten())
        else:
            right_map = dict(zip(map(tuple, right), indices))
            righti = (right_map.get(tuple(val), -1) for val in left)
        righti = np.fromiter(righti, dtype=np.int64, count=len(data))
        lefti = np.arange(len(data), dtype=np.int64)
        righti[lefti[~left_mask]] = -1
        return lefti, righti, None

    def _inner_join_indices(self, left, left_mask, right, right_mask):
        """Use _augment_indices to compute the array of indices,
        then remove those with no match in the second table"""
        lefti, righti, _ = \
            self._left_join_indices(left, left_mask, right, right_mask)
        mask = righti != [-1]
        return lefti[mask], righti[mask], None

    def _outer_join_indices(self, left, left_mask, right, right_mask):
        """Use _augment_indices to compute the array of indices,
        then add rows in the second table without a match in the first"""
        lefti, righti, _ = \
            self._left_join_indices(left, left_mask, right, right_mask)
        unused = np.full(len(right), True)
        unused[righti] = False
        if len(right) - 1 not in righti:
            # righti can include -1, which sets the last element as used
            unused[-1] = True
        return lefti, righti, np.nonzero(unused)[0]

    _merge_methods = [
        _left_join_indices, _inner_join_indices, _outer_join_indices]

    def _join_table_by_indices(self, reduced_extra, lefti, righti, rightu):
        """Join (horizontally) self.data and reduced_extra, taking the pairs
        of rows given in indices"""
        if not lefti.size:
            return None
        lt_dom = self.data.domain
        xt_dom = reduced_extra.domain
        domain = self._domain_rename_duplicates(
            lt_dom.attributes + xt_dom.attributes,
            lt_dom.class_vars + xt_dom.class_vars,
            lt_dom.metas + xt_dom.metas)
        X = self._join_array_by_indices(
            self.data.X, reduced_extra.X, lefti, righti)
        Y = self._join_array_by_indices(
            np.c_[self.data.Y], np.c_[reduced_extra.Y], lefti, righti)
        string_cols = [i for i, var in enumerate(domain.metas) if var.is_string]
        metas = self._join_array_by_indices(
            self.data.metas, reduced_extra.metas, lefti, righti, string_cols)
        if rightu is not None:
            # This domain is used for transforming the extra rows for outer join
            # It must use the original - not renamed - variables from right, so
            # values are copied,
            # but new domain for the left, so renamed values are *not* copied
            right_domain = Orange.data.Domain(
                domain.attributes[:len(lt_dom.attributes)] + xt_dom.attributes,
                domain.class_vars[:len(lt_dom.class_vars)] + xt_dom.class_vars,
                domain.metas[:len(lt_dom.metas)] + xt_dom.metas)
            extras = self.extra_data[rightu].transform(right_domain)
            X = np.vstack((X, extras.X))
            extras_Y = extras.Y
            if extras_Y.ndim == 1:
                extras_Y = extras_Y.reshape(-1, 1)
            Y = np.vstack((Y, extras_Y))
            metas = np.vstack((metas, extras.metas))
        table = Orange.data.Table.from_numpy(domain, X, Y, metas)
        table.name = getattr(self.data, 'name', '')
        table.attributes = getattr(self.data, 'attributes', {})
        if rightu is not None:
            table.ids = np.hstack(
                (self.data.ids, self.extra_data.ids[rightu]))
        else:
            table.ids = self.data.ids[lefti]

        return table

    def _domain_rename_duplicates(self, attributes, class_vars, metas):
        """Check for duplicate variable names in domain. If any, rename
        the variables, by replacing them with new ones (names are
        appended a number). """
        attrs, cvars, mets = [], [], []
        n_attrs, n_cvars, n_metas = len(attributes), len(class_vars), len(metas)
        lists = [attrs] * n_attrs + [cvars] * n_cvars + [mets] * n_metas

        all_vars = attributes + class_vars + metas
        proposed_names = [m.name for m in all_vars]
        unique_names = get_unique_names_duplicates(proposed_names)
        duplicates = set()
        for p_name, u_name, var, c in zip(proposed_names, unique_names,
                                          all_vars, lists):
            if p_name != u_name:
                duplicates.add(p_name)
                var = var.copy(name=u_name)
            c.append(var)
        if duplicates:
            self.Warning.renamed_vars(", ".join(duplicates))
        return Orange.data.Domain(attrs, cvars, mets)

    @staticmethod
    def _join_array_by_indices(left, right, lefti, righti, string_cols=None):
        """Join (horizontally) two arrays, taking pairs of rows given in indices
        """
        def prepare(arr, inds, str_cols):
            try:
                newarr = arr[inds]
            except IndexError:
                newarr = np.full_like(arr, np.nan)
            else:
                empty = np.full(arr.shape[1], np.nan)
                if str_cols:
                    assert arr.dtype == object
                    empty = empty.astype(object)
                    empty[str_cols] = ''
                newarr[inds == -1] = empty
            return newarr

        left_width = left.shape[1]
        str_left = [i for i in string_cols or () if i < left_width]
        str_right = [i - left_width for i in string_cols or () if i >= left_width]
        res = hstack((prepare(left, lefti, str_left),
                      prepare(right, righti, str_right)))
        return res

    @staticmethod
    def migrate_settings(settings, version=None):
        def mig_value(x):
            if x == "Position (index)":
                return INDEX
            if x == "Source position (index)":
                return INSTANCEID
            return x

        if not version:
            operations = ("augment", "merge", "combine")
            oper = operations[settings["merging"]]
            settings["attr_pairs"] = (
                True, True,
                [(mig_value(settings[f"attr_{oper}_data"]),
                  mig_value(settings[f"attr_{oper}_extra"]))])
            for oper in operations:
                del settings[f"attr_{oper}_data"]
                del settings[f"attr_{oper}_extra"]

        if not version or version < 2 and "attr_pairs" in settings:
            data_exists, extra_exists, attr_pairs = settings.pop("attr_pairs")
            if not (data_exists and extra_exists):
                settings["context_settings"] = []
                return

            mapper = {0: (INDEX, 100), 1: (INSTANCEID, 100)}
            context = ContextHandler().new_context()
            context.values["attr_pairs"] = [tuple(mapper.get(var, (var, 100))
                                                  for var in pair)
                                            for pair in attr_pairs]
            context.variables1 = {}
            context.variables2 = {}
            settings["context_settings"] = [context]


if __name__ == "__main__":  # pragma: no cover
    WidgetPreview(OWMergeData).run(
        set_data=Orange.data.Table("tests/data-gender-region"),
        set_extra_data=Orange.data.Table("tests/data-regions"))

```
--- 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:** AttributeError When Merging Empty Tables in MergeData Widget

**Description:**
When attempting to merge empty tables using the MergeData widget, an `AttributeError` is raised. This occurs because the widget's context handler does not properly handle `Table` objects without a domain, leading to a failure when accessing the `variables` attribute.

**Example Buggy Code:**
```python
# Attempting to merge empty tables
widget = MergeDataWidget()
empty_table = Table(domain=[])
widget.send_signal('data', empty_table)
widget.send_signal('extra_data', empty_table)
```

**Expected Behavior:**
The MergeData widget should gracefully handle empty tables without raising errors, allowing users to merge them without issues.

**Actual Behavior:**
An `AttributeError` is raised with the message `'Table' object has no attribute 'variables'`, preventing the merge operation from completing successfully.

**Error Message:**
```
AttributeError: 'Table' object has no attribute 'variables'
```
[/ISSUE]
{
    "TestOWMergeData.test_add_row_button": "PASSED",
    "TestOWMergeData.test_attr_combo_tooltips": "PASSED",
    "TestOWMergeData.test_best_match": "PASSED",
    "TestOWMergeData.test_combo_box_sync": "PASSED",
    "TestOWMergeData.test_combobox_items": "PASSED",
    "TestOWMergeData.test_commit_on_new_data": "PASSED",
    "TestOWMergeData.test_dont_remove_single_row": "PASSED",
    "TestOWMergeData.test_duplicate_names": "PASSED",
    "TestOWMergeData.test_empty_tables": "PASSED",
    "TestOWMergeData.test_image_export": "PASSED",
    "TestOWMergeData.test_input_remove": "PASSED",
    "TestOWMergeData.test_invalide_pairs": "PASSED",
    "TestOWMergeData.test_keep_non_duplicate_variables": "PASSED",
    "TestOWMergeData.test_keep_non_duplicate_variables_missing_rows": "PASSED",
    "TestOWMergeData.test_match_attr_name": "PASSED",
    "TestOWMergeData.test_match_settings": "PASSED",
    "TestOWMergeData.test_migrate_settings": "PASSED",
    "TestOWMergeData.test_migrate_settings_attr_pairs_data_none": "PASSED",
    "TestOWMergeData.test_migrate_settings_attr_pairs_extra_none": "PASSED",
    "TestOWMergeData.test_migrate_settings_attr_pairs_id_idx": "PASSED",
    "TestOWMergeData.test_migrate_settings_attr_pairs_vars": "PASSED",
    "TestOWMergeData.test_minimum_size": "PASSED",
    "TestOWMergeData.test_msg_base_class": "PASSED",
    "TestOWMergeData.test_multiple_attributes_left": "PASSED",
    "TestOWMergeData.test_no_matches": "PASSED",
    "TestOWMergeData.test_nonunique": "PASSED",
    "TestOWMergeData.test_output_merge_by_attribute_inner": "PASSED",
    "TestOWMergeData.test_output_merge_by_attribute_left": "PASSED",
    "TestOWMergeData.test_output_merge_by_attribute_outer": "PASSED",
    "TestOWMergeData.test_output_merge_by_attribute_outer_same_attr": "PASSED",
    "TestOWMergeData.test_output_merge_by_class_inner": "PASSED",
    "TestOWMergeData.test_output_merge_by_class_left": "PASSED",
    "TestOWMergeData.test_output_merge_by_class_outer": "PASSED",
    "TestOWMergeData.test_output_merge_by_ids_inner": "PASSED",
    "TestOWMergeData.test_output_merge_by_ids_outer": "PASSED",
    "TestOWMergeData.test_output_merge_by_ids_outer_single_class": "PASSED",
    "TestOWMergeData.test_output_merge_by_index_inner": "PASSED",
    "TestOWMergeData.test_output_merge_by_index_left": "PASSED",
    "TestOWMergeData.test_output_merge_by_index_outer": "PASSED",
    "TestOWMergeData.test_output_merge_by_meta_inner": "PASSED",
    "TestOWMergeData.test_output_merge_by_meta_left": "PASSED",
    "TestOWMergeData.test_output_merge_by_meta_outer": "PASSED",
    "TestOWMergeData.test_remove_row": "PASSED",
    "TestOWMergeData.test_report": "PASSED",
    "TestOWMergeData.test_retrieve_settings": "PASSED",
    "TestOWMergeData.test_sparse": "PASSED",
    "TestOWMergeData.test_summary": "PASSED",
    "MergeDataContextHandlerTest.test_attr_pairs_not_present": "PASSED",
    "MergeDataContextHandlerTest.test_malformed_contexts": "PASSED",
    "test_filename": "ERROR"
}
diff --git a/Orange/widgets/data/owmergedata.py b/Orange/widgets/data/owmergedata.py
index 17ca69559..e48f37d8c 100644
--- a/Orange/widgets/data/owmergedata.py
+++ b/Orange/widgets/data/owmergedata.py
@@ -10,7 +10,7 @@ from AnyQt.QtWidgets import (
 from orangewidget.utils.combobox import ComboBoxSearch
 
 import Orange
-from Orange.data import StringVariable, ContinuousVariable, Variable
+from Orange.data import StringVariable, ContinuousVariable, Variable, Domain
 from Orange.data.util import hstack, get_unique_names_duplicates
 from Orange.widgets import widget, gui
 from Orange.widgets.settings import Setting, ContextHandler, ContextSetting
@@ -207,6 +207,8 @@ class MergeDataContextHandler(ContextHandler):
     def _encode_domain(self, domain):
         if domain is None:
             return {}
+        if not isinstance(domain, Domain):
+            domain = domain.domain
         all_vars = chain(domain.variables, domain.metas)
         return dict(self.encode_variables(all_vars))
 

{"Orange/widgets/data/owmergedata.py": "from collections import namedtuple\nfrom itertools import chain, product\n\nimport numpy as np\n\nfrom AnyQt.QtCore import Qt, QModelIndex, pyqtSignal as Signal\nfrom AnyQt.QtWidgets import (\n    QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout\n)\nfrom orangewidget.utils.combobox import ComboBoxSearch\n\nimport Orange\nfrom Orange.data import StringVariable, ContinuousVariable, Variable\nfrom Orange.data.util import hstack, get_unique_names_duplicates\nfrom Orange.widgets import widget, gui\nfrom Orange.widgets.settings import Setting, ContextHandler, ContextSetting\nfrom Orange.widgets.utils import vartype\nfrom Orange.widgets.utils.itemmodels import DomainModel\nfrom Orange.widgets.utils.sql import check_sql_input\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.utils.state_summary import format_multiple_summaries, \\\n    format_summary_details\nfrom Orange.widgets.widget import Input, Output, Msg\n\nINSTANCEID = \"Instance id\"\nINDEX = \"Row index\"\n\n\nclass ConditionBox(QWidget):\n    vars_changed = Signal(list)\n\n    RowItems = namedtuple(\n        \"RowItems\",\n        (\"pre_label\", \"left_combo\", \"in_label\", \"right_combo\",\n         \"remove_button\", \"add_button\"))\n\n    def __init__(self, parent, model_left, model_right, pre_label, in_label):\n        super().__init__(parent)\n        self.model_left = model_left\n        self.model_right = model_right\n        self.pre_label, self.in_label = pre_label, in_label\n        self.rows = []\n        self.setLayout(QVBoxLayout())\n        self.layout().setContentsMargins(0, 0, 0, 0)\n        self.layout().setSpacing(0)\n        self.setMouseTracking(True)\n\n    def add_row(self):\n        def sync_combos():\n            combo = self.sender()\n            index = combo.currentIndex()\n            model = combo.model()\n\n            other = ({row_items.left_combo, row_items.right_combo}\n                     - {combo}).pop()\n            other_index = other.currentIndex()\n            other_model = other.model()\n\n            if 0 <= index < len(combo.model()):\n                var = model[index]\n                if isinstance(var, str):\n                    other.setCurrentText(model[index])\n                elif isinstance(other_model[other_index], str):\n                    for other_var in other_model:\n                        if isinstance(other_var, Variable) \\\n                                and var.name == other_var.name \\\n                                and (type(other_var) is type(var)\n                                     or (not var.is_continuous\n                                         and not other_var.is_continuous)):\n                            other.setCurrentText(var.name)\n                            break\n\n            self.emit_list()\n\n        def get_combo(model):\n            combo = ComboBoxSearch(self)\n            combo.setModel(model)\n            # We use signal activated because it is triggered only on user\n            # interaction, not programmatically.\n            combo.activated.connect(sync_combos)\n            return combo\n\n        def get_button(label, callback):\n            button = QPushButton(label, self)\n            button.setFlat(True)\n            button.setFixedWidth(12)\n            button.clicked.connect(callback)\n            return button\n\n        row = self.layout().count()\n        row_items = self.RowItems(\n            QLabel(\"and\" if row else self.pre_label),\n            get_combo(self.model_left),\n            QLabel(self.in_label),\n            get_combo(self.model_right),\n            get_button(\"\u00d7\", self.on_remove_row),\n            get_button(\"+\", self.on_add_row)\n        )\n        layout = QHBoxLayout()\n        layout.setSpacing(10)\n        self.layout().addLayout(layout)\n        layout.addStretch(10)\n        for item in row_items:\n            layout.addWidget(item)\n        self.rows.append(row_items)\n        self._reset_buttons()\n\n    def remove_row(self, row):\n        self.layout().takeAt(self.rows.index(row))\n        self.rows.remove(row)\n        for item in row:\n            item.deleteLater()\n        self._reset_buttons()\n\n    def on_add_row(self, _):\n        self.add_row()\n        self.emit_list()\n\n    def on_remove_row(self):\n        if len(self.rows) == 1:\n            return\n        button = self.sender()\n        for row in self.rows:\n            if button is row.remove_button:\n                self.remove_row(row)\n                break\n        self.emit_list()\n\n    def _reset_buttons(self):\n        def endis(button, enable, text):\n            button.setEnabled(enable)\n            button.setText(text if enable else \"\")\n\n        self.rows[0].pre_label.setText(self.pre_label)\n        single_row = len(self.rows) == 1\n        endis(self.rows[0].remove_button, not single_row, \"\u00d7\")\n        endis(self.rows[-1].add_button, True, \"+\")\n        if not single_row:\n            endis(self.rows[-2].add_button, False, \"\")\n\n    def current_state(self):\n        def get_var(model, combo):\n            return model[combo.currentIndex()]\n\n        return [(get_var(self.model_left, row.left_combo),\n                 get_var(self.model_right, row.right_combo))\n                for row in self.rows]\n\n    def set_state(self, values):\n        while len(self.rows) > len(values):\n            self.remove_row(self.rows[0])\n        while len(self.rows) < len(values):\n            self.add_row()\n        for (val_left, val_right), row in zip(values, self.rows):\n            row.left_combo.setCurrentIndex(self.model_left.indexOf(val_left))\n            row.right_combo.setCurrentIndex(self.model_right.indexOf(val_right))\n\n    def emit_list(self):\n        self.vars_changed.emit(self.current_state())\n\n\nclass DomainModelWithTooltips(DomainModel):\n    def data(self, index, role=Qt.DisplayRole):\n        if role == Qt.ToolTipRole \\\n                and isinstance(index, QModelIndex) and index.isValid():\n            if index.row() == 0:\n                return \"Match rows sequentially\"\n            if index.row() == 1:\n                return \"Re-match rows from tables obtained from the same \" \\\n                       \"source,\\n\" \\\n                       \"e.g. data from the same file that was split within \" \\\n                       \"the workflow.\"\n        return super().data(index, role)\n\n\nclass MergeDataContextHandler(ContextHandler):\n    # `widget` is used as an argument in most methods\n    # pylint: disable=redefined-outer-name\n    # context handlers override methods using different signatures\n    # pylint: disable=arguments-differ\n\n    def new_context(self, variables1, variables2):\n        context = super().new_context()\n        context.variables1 = variables1\n        context.variables2 = variables2\n        return context\n\n    def open_context(self, widget, domain1, domain2):\n        if domain1 is not None and domain2 is not None:\n            super().open_context(widget,\n                                 self._encode_domain(domain1),\n                                 self._encode_domain(domain2))\n\n    @staticmethod\n    def encode_variables(variables):\n        return [(v.name, 100 + vartype(v))\n                if isinstance(v, Variable) else (v, 100)\n                for v in variables]\n\n    @staticmethod\n    def decode_pair(widget, pair):\n        left_domain = widget.data and widget.data.domain\n        right_domain = widget.extra_data and widget.extra_data.domain\n        return tuple(var[0] if var[0] in (INDEX, INSTANCEID) else domain[var[0]]\n                     for domain, var in zip((left_domain, right_domain), pair))\n\n    def _encode_domain(self, domain):\n        if domain is None:\n            return {}\n        all_vars = chain(domain.variables, domain.metas)\n        return dict(self.encode_variables(all_vars))\n\n    def settings_from_widget(self, widget, *_args):\n        context = widget.current_context\n        if context is None:\n            return\n        context.values[\"attr_pairs\"] = [self.encode_variables(row)\n                                        for row in widget.attr_pairs]\n\n    def settings_to_widget(self, widget, *_args):\n        context = widget.current_context\n        if context is None:\n            return\n        pairs = context.values.get(\"attr_pairs\")\n        if pairs:\n            # attr_pairs is schema only setting which means it is not always\n            # present. When not present leave widgets default.\n            widget.attr_pairs = [\n                self.decode_pair(widget, pair) for pair in pairs\n            ]\n\n    def match(self, context, variables1, variables2):\n        def matches(part, variables):\n            return all(var[1] == 100 and var[0] in variables\n                       or variables.get(var[0], -1) == var[1] for var in part)\n\n        if (variables1, variables2) == (context.variables1, context.variables2):\n            return self.PERFECT_MATCH\n\n        if \"attr_pairs\" in context.values:\n            left, right = zip(*context.values[\"attr_pairs\"])\n            if matches(left, variables1) and matches(right, variables2):\n                return 0.5\n\n        return self.NO_MATCH\n\n\nclass OWMergeData(widget.OWWidget):\n    name = \"Merge Data\"\n    description = \"Merge datasets based on the values of selected features.\"\n    icon = \"icons/MergeData.svg\"\n    priority = 1110\n    keywords = [\"join\"]\n\n    class Inputs:\n        data = Input(\"Data\", Orange.data.Table, default=True, replaces=[\"Data A\"])\n        extra_data = Input(\"Extra Data\", Orange.data.Table, replaces=[\"Data B\"])\n\n    class Outputs:\n        data = Output(\"Data\",\n                      Orange.data.Table,\n                      replaces=[\"Merged Data A+B\", \"Merged Data B+A\", \"Merged Data\"])\n\n    LeftJoin, InnerJoin, OuterJoin = range(3)\n    OptionNames = (\"Append columns from Extra data\",\n                   \"Find matching pairs of rows\",\n                   \"Concatenate tables\")\n    OptionDescriptions = (\n        \"The first table may contain, for instance, city names,\\n\"\n        \"and the second would be a list of cities and their coordinates.\\n\"\n        \"Columns with coordinates would then be appended to the output.\",\n\n        \"Input tables contain different features describing the same data \"\n        \"instances.\\n\"\n        \"Output contains matched instances. Rows without matches are removed.\",\n\n        \"Input tables contain different features describing the same data \"\n        \"instances.\\n\"\n        \"Output contains all instances. Data from merged instances is \"\n        \"merged into single rows.\"\n    )\n\n    UserAdviceMessages = [\n        widget.Message(\n            \"Confused about merging options?\\nSee the tooltips!\",\n            \"merging_types\")]\n\n    settingsHandler = MergeDataContextHandler()\n    attr_pairs = ContextSetting(None, schema_only=True)\n    merging = Setting(LeftJoin)\n    auto_apply = Setting(True)\n    settings_version = 2\n\n    want_main_area = False\n    resizing_enabled = False\n\n    class Warning(widget.OWWidget.Warning):\n        renamed_vars = Msg(\"Some variables have been renamed \"\n                           \"to avoid duplicates.\\n{}\")\n\n    class Error(widget.OWWidget.Error):\n        matching_numeric_with_nonnum = Msg(\n            \"Numeric and non-numeric columns ({} and {}) cannot be matched.\")\n        matching_index_with_sth = Msg(\"Row index cannot be matched with {}.\")\n        matching_id_with_sth = Msg(\"Instance cannot be matched with {}.\")\n        nonunique_left = Msg(\n            \"Some combinations of values on the left appear in multiple rows.\\n\"\n            \"For this type of merging, every possible combination of values \"\n            \"on the left should appear at most once.\")\n        nonunique_right = Msg(\n            \"Some combinations of values on the right appear in multiple rows.\"\n            \"\\n\"\n            \"Every possible combination of values on the right should appear \"\n            \"at most once.\")\n\n    def __init__(self):\n        super().__init__()\n\n        self.data = None\n        self.extra_data = None\n\n        content = [\n            INDEX, INSTANCEID,\n            DomainModel.ATTRIBUTES, DomainModel.CLASSES, DomainModel.METAS]\n        self.model = DomainModelWithTooltips(content)\n        self.extra_model = DomainModelWithTooltips(content)\n\n        self.info.set_input_summary(self.info.NoInput)\n        self.info.set_output_summary(self.info.NoOutput)\n\n        grp = gui.radioButtons(\n            self.controlArea, self, \"merging\", box=\"Merging\",\n            btnLabels=self.OptionNames, tooltips=self.OptionDescriptions,\n            callback=self.change_merging)\n\n        self.attr_boxes = ConditionBox(\n            self, self.model, self.extra_model, \"\", \"matches\")\n        self.attr_boxes.add_row()\n        box = gui.vBox(self.controlArea, box=\"Row matching\")\n        box.layout().addWidget(self.attr_boxes)\n\n        gui.auto_apply(self.buttonsArea, self)\n        # connect after wrapping self.commit with gui.auto_commit!\n        self.attr_boxes.vars_changed.connect(self.commit)\n        self.attr_boxes.vars_changed.connect(self.store_combo_state)\n        self.settingsAboutToBePacked.connect(self.store_combo_state)\n\n    def change_merging(self):\n        self.commit()\n\n    @Inputs.data\n    @check_sql_input\n    def set_data(self, data):\n        self.data = data\n        self.model.set_domain(data.domain if data else None)\n\n    @Inputs.extra_data\n    @check_sql_input\n    def set_extra_data(self, data):\n        self.extra_data = data\n        self.extra_model.set_domain(data.domain if data else None)\n\n    def store_combo_state(self):\n        self.attr_pairs = self.attr_boxes.current_state()\n\n    def handleNewSignals(self):\n        self.closeContext()\n        self.attr_pairs = [self._find_best_match()]\n        self.openContext(self.data and self.data.domain,\n                         self.extra_data and self.extra_data.domain)\n        self.attr_boxes.set_state(self.attr_pairs)\n\n        summary, details, kwargs = self.info.NoInput, \"\", {}\n        if self.data or self.extra_data:\n            n_data = len(self.data) if self.data else 0\n            n_extra_data = len(self.extra_data) if self.extra_data else 0\n            summary = f\"{self.info.format_number(n_data)}, \" \\\n                      f\"{self.info.format_number(n_extra_data)}\"\n            kwargs = {\"format\": Qt.RichText}\n            details = format_multiple_summaries([\n                (\"Data\", self.data),\n                (\"Extra data\", self.extra_data)\n            ])\n        self.info.set_input_summary(summary, details, **kwargs)\n\n        self.unconditional_commit()\n\n    def _find_best_match(self):\n        def get_unique_str_metas_names(model_):\n            return [m for m in model_ if isinstance(m, StringVariable)]\n\n        attr, extra_attr, n_max_intersect = INDEX, INDEX, 0\n        str_metas = get_unique_str_metas_names(self.model)\n        extra_str_metas = get_unique_str_metas_names(self.extra_model)\n        for m_a, m_b in product(str_metas, extra_str_metas):\n            col = self.data[:, m_a].metas\n            extra_col = self.extra_data[:, m_b].metas\n            if col.size and extra_col.size \\\n                    and isinstance(col[0][0], str) \\\n                    and isinstance(extra_col[0][0], str):\n                n_inter = len(np.intersect1d(col, extra_col))\n                if n_inter > n_max_intersect:\n                    n_max_intersect, attr, extra_attr = n_inter, m_a, m_b\n        return attr, extra_attr\n\n    def commit(self):\n        self.clear_messages()\n        merged = self.merge() if self.data and self.extra_data else None\n        self.Outputs.data.send(merged)\n        details = format_summary_details(merged) if merged else \"\"\n        summary = len(merged) if merged else self.info.NoOutput\n        self.info.set_output_summary(summary, details)\n\n    def send_report(self):\n        # pylint: disable=invalid-sequence-index\n        self.report_items((\n            (\"Merging\", self.OptionNames[self.merging]),\n            (\"Match\",\n             \", \".join(\n                 f\"{self._get_col_name(left)} with {self._get_col_name(right)}\"\n                 for left, right in self.attr_boxes.current_state()))))\n\n    def merge(self):\n        # pylint: disable=invalid-sequence-index\n        pairs = self.attr_boxes.current_state()\n        if not self._check_pair_types(pairs):\n            return None\n        left_vars, right_vars = zip(*pairs)\n        left_mask = np.full(len(self.data), True)\n        left = np.vstack(tuple(self._values(self.data, var, left_mask)\n                               for var in left_vars)).T\n        right_mask = np.full(len(self.extra_data), True)\n        right = np.vstack(tuple(self._values(self.extra_data, var, right_mask)\n                                for var in right_vars)).T\n        if not self._check_uniqueness(left, left_mask, right, right_mask):\n            return None\n        method = self._merge_methods[self.merging]\n        lefti, righti, rightu = method(self, left, left_mask, right, right_mask)\n        reduced_extra_data = \\\n            self._compute_reduced_extra_data(right_vars, lefti, righti, rightu)\n        return self._join_table_by_indices(\n            reduced_extra_data, lefti, righti, rightu)\n\n    def _check_pair_types(self, pairs):\n        for left, right in pairs:\n            if isinstance(left, ContinuousVariable) \\\n                    != isinstance(right, ContinuousVariable):\n                self.Error.matching_numeric_with_nonnum(left, right)\n                return False\n            if INDEX in (left, right) and left != right:\n                self.Error.matching_index_with_sth(\n                    self._get_col_name(({left, right} - {INDEX}).pop()))\n                return False\n            if INSTANCEID in (left, right) and left != right:\n                self.Error.matching_id_with_sth(\n                    self._get_col_name(({left, right} - {INSTANCEID}).pop()))\n                return False\n        return True\n\n    @staticmethod\n    def _get_col_name(obj):\n        return f\"'{obj.name}'\" if isinstance(obj, Variable) else obj.lower()\n\n    def _check_uniqueness(self, left, left_mask, right, right_mask):\n        ok = True\n        masked_right = right[right_mask]\n        if len(set(map(tuple, masked_right))) != len(masked_right):\n            self.Error.nonunique_right()\n            ok = False\n        if self.merging != self.LeftJoin:\n            masked_left = left[left_mask]\n            if len(set(map(tuple, masked_left))) != len(masked_left):\n                self.Error.nonunique_left()\n                ok = False\n        return ok\n\n    def _compute_reduced_extra_data(self,\n                                    right_match_vars, lefti, righti, rightu):\n        \"\"\"Prepare a table with extra columns that will appear in the merged\n        table\"\"\"\n        domain = self.data.domain\n        extra_domain = self.extra_data.domain\n\n        def var_needed(var):\n            if rightu is not None and rightu.size:\n                return True\n            if var in right_match_vars and self.merging != self.OuterJoin:\n                return False\n            if var not in domain:\n                return True\n            both_defined = (lefti != -1) * (righti != -1)\n            left_col = \\\n                self.data.get_column_view(var)[0][lefti[both_defined]]\n            right_col = \\\n                self.extra_data.get_column_view(var)[0][righti[both_defined]]\n            if var.is_primitive():\n                left_col = left_col.astype(float)\n                right_col = right_col.astype(float)\n                mask_left = np.isfinite(left_col)\n                mask_right = np.isfinite(right_col)\n                return not (\n                    np.all(mask_left == mask_right)\n                    and np.all(left_col[mask_left] == right_col[mask_right]))\n            else:\n                return not np.all(left_col == right_col)\n\n        extra_vars = [\n            var for var in chain(extra_domain.variables, extra_domain.metas)\n            if var_needed(var)]\n        return self.extra_data[:, extra_vars]\n\n    @staticmethod\n    def _values(data, var, mask):\n        \"\"\"Return an iterotor over keys for rows of the table.\"\"\"\n        if var == INDEX:\n            return np.arange(len(data))\n        if var == INSTANCEID:\n            return np.fromiter(\n                (inst.id for inst in data), count=len(data), dtype=np.int)\n        col = data.get_column_view(var)[0]\n        if var.is_primitive():\n            col = col.astype(float, copy=False)\n            nans = np.isnan(col)\n            mask *= ~nans\n            if var.is_discrete:\n                col = col.astype(int)\n                col[nans] = len(var.values)\n                col = np.array(var.values + (np.nan, ))[col]\n        else:\n            col = col.copy()\n            defined = col.astype(bool)\n            mask *= defined\n            col[~mask] = np.nan\n        return col\n\n    def _left_join_indices(self, left, left_mask, right, right_mask):\n        \"\"\"Compute a two-row array of indices:\n        - the first row contains indices for the primary table,\n        - the second row contains the matching rows in the extra table or -1\"\"\"\n        data = self.data\n        # Don't match nans. This is needed since numpy may change nan to string\n        # nan, so nan's will match each other\n        indices = np.arange(len(right))\n        indices[~right_mask] = -1\n        if right.shape[1] == 1:\n            # The more common case can be handled faster\n            right_map = dict(zip(right.flatten(), indices))\n            righti = (right_map.get(val, -1) for val in left.flatten())\n        else:\n            right_map = dict(zip(map(tuple, right), indices))\n            righti = (right_map.get(tuple(val), -1) for val in left)\n        righti = np.fromiter(righti, dtype=np.int64, count=len(data))\n        lefti = np.arange(len(data), dtype=np.int64)\n        righti[lefti[~left_mask]] = -1\n        return lefti, righti, None\n\n    def _inner_join_indices(self, left, left_mask, right, right_mask):\n        \"\"\"Use _augment_indices to compute the array of indices,\n        then remove those with no match in the second table\"\"\"\n        lefti, righti, _ = \\\n            self._left_join_indices(left, left_mask, right, right_mask)\n        mask = righti != [-1]\n        return lefti[mask], righti[mask], None\n\n    def _outer_join_indices(self, left, left_mask, right, right_mask):\n        \"\"\"Use _augment_indices to compute the array of indices,\n        then add rows in the second table without a match in the first\"\"\"\n        lefti, righti, _ = \\\n            self._left_join_indices(left, left_mask, right, right_mask)\n        unused = np.full(len(right), True)\n        unused[righti] = False\n        if len(right) - 1 not in righti:\n            # righti can include -1, which sets the last element as used\n            unused[-1] = True\n        return lefti, righti, np.nonzero(unused)[0]\n\n    _merge_methods = [\n        _left_join_indices, _inner_join_indices, _outer_join_indices]\n\n    def _join_table_by_indices(self, reduced_extra, lefti, righti, rightu):\n        \"\"\"Join (horizontally) self.data and reduced_extra, taking the pairs\n        of rows given in indices\"\"\"\n        if not lefti.size:\n            return None\n        lt_dom = self.data.domain\n        xt_dom = reduced_extra.domain\n        domain = self._domain_rename_duplicates(\n            lt_dom.attributes + xt_dom.attributes,\n            lt_dom.class_vars + xt_dom.class_vars,\n            lt_dom.metas + xt_dom.metas)\n        X = self._join_array_by_indices(\n            self.data.X, reduced_extra.X, lefti, righti)\n        Y = self._join_array_by_indices(\n            np.c_[self.data.Y], np.c_[reduced_extra.Y], lefti, righti)\n        string_cols = [i for i, var in enumerate(domain.metas) if var.is_string]\n        metas = self._join_array_by_indices(\n            self.data.metas, reduced_extra.metas, lefti, righti, string_cols)\n        if rightu is not None:\n            # This domain is used for transforming the extra rows for outer join\n            # It must use the original - not renamed - variables from right, so\n            # values are copied,\n            # but new domain for the left, so renamed values are *not* copied\n            right_domain = Orange.data.Domain(\n                domain.attributes[:len(lt_dom.attributes)] + xt_dom.attributes,\n                domain.class_vars[:len(lt_dom.class_vars)] + xt_dom.class_vars,\n                domain.metas[:len(lt_dom.metas)] + xt_dom.metas)\n            extras = self.extra_data[rightu].transform(right_domain)\n            X = np.vstack((X, extras.X))\n            extras_Y = extras.Y\n            if extras_Y.ndim == 1:\n                extras_Y = extras_Y.reshape(-1, 1)\n            Y = np.vstack((Y, extras_Y))\n            metas = np.vstack((metas, extras.metas))\n        table = Orange.data.Table.from_numpy(domain, X, Y, metas)\n        table.name = getattr(self.data, 'name', '')\n        table.attributes = getattr(self.data, 'attributes', {})\n        if rightu is not None:\n            table.ids = np.hstack(\n                (self.data.ids, self.extra_data.ids[rightu]))\n        else:\n            table.ids = self.data.ids[lefti]\n\n        return table\n\n    def _domain_rename_duplicates(self, attributes, class_vars, metas):\n        \"\"\"Check for duplicate variable names in domain. If any, rename\n        the variables, by replacing them with new ones (names are\n        appended a number). \"\"\"\n        attrs, cvars, mets = [], [], []\n        n_attrs, n_cvars, n_metas = len(attributes), len(class_vars), len(metas)\n        lists = [attrs] * n_attrs + [cvars] * n_cvars + [mets] * n_metas\n\n        all_vars = attributes + class_vars + metas\n        proposed_names = [m.name for m in all_vars]\n        unique_names = get_unique_names_duplicates(proposed_names)\n        duplicates = set()\n        for p_name, u_name, var, c in zip(proposed_names, unique_names,\n                                          all_vars, lists):\n            if p_name != u_name:\n                duplicates.add(p_name)\n                var = var.copy(name=u_name)\n            c.append(var)\n        if duplicates:\n            self.Warning.renamed_vars(\", \".join(duplicates))\n        return Orange.data.Domain(attrs, cvars, mets)\n\n    @staticmethod\n    def _join_array_by_indices(left, right, lefti, righti, string_cols=None):\n        \"\"\"Join (horizontally) two arrays, taking pairs of rows given in indices\n        \"\"\"\n        def prepare(arr, inds, str_cols):\n            try:\n                newarr = arr[inds]\n            except IndexError:\n                newarr = np.full_like(arr, np.nan)\n            else:\n                empty = np.full(arr.shape[1], np.nan)\n                if str_cols:\n                    assert arr.dtype == object\n                    empty = empty.astype(object)\n                    empty[str_cols] = ''\n                newarr[inds == -1] = empty\n            return newarr\n\n        left_width = left.shape[1]\n        str_left = [i for i in string_cols or () if i < left_width]\n        str_right = [i - left_width for i in string_cols or () if i >= left_width]\n        res = hstack((prepare(left, lefti, str_left),\n                      prepare(right, righti, str_right)))\n        return res\n\n    @staticmethod\n    def migrate_settings(settings, version=None):\n        def mig_value(x):\n            if x == \"Position (index)\":\n                return INDEX\n            if x == \"Source position (index)\":\n                return INSTANCEID\n            return x\n\n        if not version:\n            operations = (\"augment\", \"merge\", \"combine\")\n            oper = operations[settings[\"merging\"]]\n            settings[\"attr_pairs\"] = (\n                True, True,\n                [(mig_value(settings[f\"attr_{oper}_data\"]),\n                  mig_value(settings[f\"attr_{oper}_extra\"]))])\n            for oper in operations:\n                del settings[f\"attr_{oper}_data\"]\n                del settings[f\"attr_{oper}_extra\"]\n\n        if not version or version < 2 and \"attr_pairs\" in settings:\n            data_exists, extra_exists, attr_pairs = settings.pop(\"attr_pairs\")\n            if not (data_exists and extra_exists):\n                settings[\"context_settings\"] = []\n                return\n\n            mapper = {0: (INDEX, 100), 1: (INSTANCEID, 100)}\n            context = ContextHandler().new_context()\n            context.values[\"attr_pairs\"] = [tuple(mapper.get(var, (var, 100))\n                                                  for var in pair)\n                                            for pair in attr_pairs]\n            context.variables1 = {}\n            context.variables2 = {}\n            settings[\"context_settings\"] = [context]\n\n\nif __name__ == \"__main__\":  # pragma: no cover\n    WidgetPreview(OWMergeData).run(\n        set_data=Orange.data.Table(\"tests/data-gender-region\"),\n        set_extra_data=Orange.data.Table(\"tests/data-regions\"))\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:** AttributeError When Merging Empty Tables in MergeData Widget

**Description:**
When attempting to merge empty tables using the MergeData widget, an `AttributeError` is raised. This occurs because the widget's context handler does not properly handle `Table` objects without a domain, leading to a failure when accessing the `variables` attribute.

**Example Buggy Code:**
```python
# Attempting to merge empty tables
widget = MergeDataWidget()
empty_table = Table(domain=[])
widget.send_signal('data', empty_table)
widget.send_signal('extra_data', empty_table)
```

**Expected Behavior:**
The MergeData widget should gracefully handle empty tables without raising errors, allowing users to merge them without issues.

**Actual Behavior:**
An `AttributeError` is raised with the message `'Table' object has no attribute 'variables'`, preventing the merge operation from completing successfully.

**Error Message:**
```
AttributeError: 'Table' object has no attribute 'variables'
```
[/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/owmergedata.py
from collections import namedtuple
from itertools import chain, product

import numpy as np

from AnyQt.QtCore import Qt, QModelIndex, pyqtSignal as Signal
from AnyQt.QtWidgets import (
    QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout
)
from orangewidget.utils.combobox import ComboBoxSearch

import Orange
from Orange.data import StringVariable, ContinuousVariable, Variable
from Orange.data.util import hstack, get_unique_names_duplicates
from Orange.widgets import widget, gui
from Orange.widgets.settings import Setting, ContextHandler, ContextSetting
from Orange.widgets.utils import vartype
from Orange.widgets.utils.itemmodels import DomainModel
from Orange.widgets.utils.sql import check_sql_input
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.utils.state_summary import format_multiple_summaries, \
    format_summary_details
from Orange.widgets.widget import Input, Output, Msg

INSTANCEID = "Instance id"
INDEX = "Row index"


class ConditionBox(QWidget):
    vars_changed = Signal(list)

    RowItems = namedtuple(
        "RowItems",
        ("pre_label", "left_combo", "in_label", "right_combo",
         "remove_button", "add_button"))

    def __init__(self, parent, model_left, model_right, pre_label, in_label):
        super().__init__(parent)
        self.model_left = model_left
        self.model_right = model_right
        self.pre_label, self.in_label = pre_label, in_label
        self.rows = []
        self.setLayout(QVBoxLayout())
        self.layout().setContentsMargins(0, 0, 0, 0)
        self.layout().setSpacing(0)
        self.setMouseTracking(True)

    def add_row(self):
        def sync_combos():
            combo = self.sender()
            index = combo.currentIndex()
            model = combo.model()

            other = ({row_items.left_combo, row_items.right_combo}
                     - {combo}).pop()
            other_index = other.currentIndex()
            other_model = other.model()

            if 0 <= index < len(combo.model()):
                var = model[index]
                if isinstance(var, str):
                    other.setCurrentText(model[index])
                elif isinstance(other_model[other_index], str):
                    for other_var in other_model:
                        if isinstance(other_var, Variable) \
                                and var.name == other_var.name \
                                and (type(other_var) is type(var)
                                     or (not var.is_continuous
                                         and not other_var.is_continuous)):
                            other.setCurrentText(var.name)
                            break

            self.emit_list()

        def get_combo(model):
            combo = ComboBoxSearch(self)
            combo.setModel(model)
            # We use signal activated because it is triggered only on user
            # interaction, not programmatically.
            combo.activated.connect(sync_combos)
            return combo

        def get_button(label, callback):
            button = QPushButton(label, self)
            button.setFlat(True)
            button.setFixedWidth(12)
            button.clicked.connect(callback)
            return button

        row = self.layout().count()
        row_items = self.RowItems(
            QLabel("and" if row else self.pre_label),
            get_combo(self.model_left),
            QLabel(self.in_label),
            get_combo(self.model_right),
            get_button("×", self.on_remove_row),
            get_button("+", self.on_add_row)
        )
        layout = QHBoxLayout()
        layout.setSpacing(10)
        self.layout().addLayout(layout)
        layout.addStretch(10)
        for item in row_items:
            layout.addWidget(item)
        self.rows.append(row_items)
        self._reset_buttons()

    def remove_row(self, row):
        self.layout().takeAt(self.rows.index(row))
        self.rows.remove(row)
        for item in row:
            item.deleteLater()
        self._reset_buttons()

    def on_add_row(self, _):
        self.add_row()
        self.emit_list()

    def on_remove_row(self):
        if len(self.rows) == 1:
            return
        button = self.sender()
        for row in self.rows:
            if button is row.remove_button:
                self.remove_row(row)
                break
        self.emit_list()

    def _reset_buttons(self):
        def endis(button, enable, text):
            button.setEnabled(enable)
            button.setText(text if enable else "")

        self.rows[0].pre_label.setText(self.pre_label)
        single_row = len(self.rows) == 1
        endis(self.rows[0].remove_button, not single_row, "×")
        endis(self.rows[-1].add_button, True, "+")
        if not single_row:
            endis(self.rows[-2].add_button, False, "")

    def current_state(self):
        def get_var(model, combo):
            return model[combo.currentIndex()]

        return [(get_var(self.model_left, row.left_combo),
                 get_var(self.model_right, row.right_combo))
                for row in self.rows]

    def set_state(self, values):
        while len(self.rows) > len(values):
            self.remove_row(self.rows[0])
        while len(self.rows) < len(values):
            self.add_row()
        for (val_left, val_right), row in zip(values, self.rows):
            row.left_combo.setCurrentIndex(self.model_left.indexOf(val_left))
            row.right_combo.setCurrentIndex(self.model_right.indexOf(val_right))

    def emit_list(self):
        self.vars_changed.emit(self.current_state())


class DomainModelWithTooltips(DomainModel):
    def data(self, index, role=Qt.DisplayRole):
        if role == Qt.ToolTipRole \
                and isinstance(index, QModelIndex) and index.isValid():
            if index.row() == 0:
                return "Match rows sequentially"
            if index.row() == 1:
                return "Re-match rows from tables obtained from the same " \
                       "source,\n" \
                       "e.g. data from the same file that was split within " \
                       "the workflow."
        return super().data(index, role)


class MergeDataContextHandler(ContextHandler):
    # `widget` is used as an argument in most methods
    # pylint: disable=redefined-outer-name
    # context handlers override methods using different signatures
    # pylint: disable=arguments-differ

    def new_context(self, variables1, variables2):
        context = super().new_context()
        context.variables1 = variables1
        context.variables2 = variables2
        return context

    def open_context(self, widget, domain1, domain2):
        if domain1 is not None and domain2 is not None:
            super().open_context(widget,
                                 self._encode_domain(domain1),
                                 self._encode_domain(domain2))

    @staticmethod
    def encode_variables(variables):
        return [(v.name, 100 + vartype(v))
                if isinstance(v, Variable) else (v, 100)
                for v in variables]

    @staticmethod
    def decode_pair(widget, pair):
        left_domain = widget.data and widget.data.domain
        right_domain = widget.extra_data and widget.extra_data.domain
        return tuple(var[0] if var[0] in (INDEX, INSTANCEID) else domain[var[0]]
                     for domain, var in zip((left_domain, right_domain), pair))

    def _encode_domain(self, domain):
        if domain is None:
            return {}
        all_vars = chain(domain.variables, domain.metas)
        return dict(self.encode_variables(all_vars))

    def settings_from_widget(self, widget, *_args):
        context = widget.current_context
        if context is None:
            return
        context.values["attr_pairs"] = [self.encode_variables(row)
                                        for row in widget.attr_pairs]

    def settings_to_widget(self, widget, *_args):
        context = widget.current_context
        if context is None:
            return
        pairs = context.values.get("attr_pairs")
        if pairs:
            # attr_pairs is schema only setting which means it is not always
            # present. When not present leave widgets default.
            widget.attr_pairs = [
                self.decode_pair(widget, pair) for pair in pairs
            ]

    def match(self, context, variables1, variables2):
        def matches(part, variables):
            return all(var[1] == 100 and var[0] in variables
                       or variables.get(var[0], -1) == var[1] for var in part)

        if (variables1, variables2) == (context.variables1, context.variables2):
            return self.PERFECT_MATCH

        if "attr_pairs" in context.values:
            left, right = zip(*context.values["attr_pairs"])
            if matches(left, variables1) and matches(right, variables2):
                return 0.5

        return self.NO_MATCH


class OWMergeData(widget.OWWidget):
    name = "Merge Data"
    description = "Merge datasets based on the values of selected features."
    icon = "icons/MergeData.svg"
    priority = 1110
    keywords = ["join"]

    class Inputs:
        data = Input("Data", Orange.data.Table, default=True, replaces=["Data A"])
        extra_data = Input("Extra Data", Orange.data.Table, replaces=["Data B"])

    class Outputs:
        data = Output("Data",
                      Orange.data.Table,
                      replaces=["Merged Data A+B", "Merged Data B+A", "Merged Data"])

    LeftJoin, InnerJoin, OuterJoin = range(3)
    OptionNames = ("Append columns from Extra data",
                   "Find matching pairs of rows",
                   "Concatenate tables")
    OptionDescriptions = (
        "The first table may contain, for instance, city names,\n"
        "and the second would be a list of cities and their coordinates.\n"
        "Columns with coordinates would then be appended to the output.",

        "Input tables contain different features describing the same data "
        "instances.\n"
        "Output contains matched instances. Rows without matches are removed.",

        "Input tables contain different features describing the same data "
        "instances.\n"
        "Output contains all instances. Data from merged instances is "
        "merged into single rows."
    )

    UserAdviceMessages = [
        widget.Message(
            "Confused about merging options?\nSee the tooltips!",
            "merging_types")]

    settingsHandler = MergeDataContextHandler()
    attr_pairs = ContextSetting(None, schema_only=True)
    merging = Setting(LeftJoin)
    auto_apply = Setting(True)
    settings_version = 2

    want_main_area = False
    resizing_enabled = False

    class Warning(widget.OWWidget.Warning):
        renamed_vars = Msg("Some variables have been renamed "
                           "to avoid duplicates.\n{}")

    class Error(widget.OWWidget.Error):
        matching_numeric_with_nonnum = Msg(
            "Numeric and non-numeric columns ({} and {}) cannot be matched.")
        matching_index_with_sth = Msg("Row index cannot be matched with {}.")
        matching_id_with_sth = Msg("Instance cannot be matched with {}.")
        nonunique_left = Msg(
            "Some combinations of values on the left appear in multiple rows.\n"
            "For this type of merging, every possible combination of values "
            "on the left should appear at most once.")
        nonunique_right = Msg(
            "Some combinations of values on the right appear in multiple rows."
            "\n"
            "Every possible combination of values on the right should appear "
            "at most once.")

    def __init__(self):
        super().__init__()

        self.data = None
        self.extra_data = None

        content = [
            INDEX, INSTANCEID,
            DomainModel.ATTRIBUTES, DomainModel.CLASSES, DomainModel.METAS]
        self.model = DomainModelWithTooltips(content)
        self.extra_model = DomainModelWithTooltips(content)

        self.info.set_input_summary(self.info.NoInput)
        self.info.set_output_summary(self.info.NoOutput)

        grp = gui.radioButtons(
            self.controlArea, self, "merging", box="Merging",
            btnLabels=self.OptionNames, tooltips=self.OptionDescriptions,
            callback=self.change_merging)

        self.attr_boxes = ConditionBox(
            self, self.model, self.extra_model, "", "matches")
        self.attr_boxes.add_row()
        box = gui.vBox(self.controlArea, box="Row matching")
        box.layout().addWidget(self.attr_boxes)

        gui.auto_apply(self.buttonsArea, self)
        # connect after wrapping self.commit with gui.auto_commit!
        self.attr_boxes.vars_changed.connect(self.commit)
        self.attr_boxes.vars_changed.connect(self.store_combo_state)
        self.settingsAboutToBePacked.connect(self.store_combo_state)

    def change_merging(self):
        self.commit()

    @Inputs.data
    @check_sql_input
    def set_data(self, data):
        self.data = data
        self.model.set_domain(data.domain if data else None)

    @Inputs.extra_data
    @check_sql_input
    def set_extra_data(self, data):
        self.extra_data = data
        self.extra_model.set_domain(data.domain if data else None)

    def store_combo_state(self):
        self.attr_pairs = self.attr_boxes.current_state()

    def handleNewSignals(self):
        self.closeContext()
        self.attr_pairs = [self._find_best_match()]
        self.openContext(self.data and self.data.domain,
                         self.extra_data and self.extra_data.domain)
        self.attr_boxes.set_state(self.attr_pairs)

        summary, details, kwargs = self.info.NoInput, "", {}
        if self.data or self.extra_data:
            n_data = len(self.data) if self.data else 0
            n_extra_data = len(self.extra_data) if self.extra_data else 0
            summary = f"{self.info.format_number(n_data)}, " \
                      f"{self.info.format_number(n_extra_data)}"
            kwargs = {"format": Qt.RichText}
            details = format_multiple_summaries([
                ("Data", self.data),
                ("Extra data", self.extra_data)
            ])
        self.info.set_input_summary(summary, details, **kwargs)

        self.unconditional_commit()

    def _find_best_match(self):
        def get_unique_str_metas_names(model_):
            return [m for m in model_ if isinstance(m, StringVariable)]

        attr, extra_attr, n_max_intersect = INDEX, INDEX, 0
        str_metas = get_unique_str_metas_names(self.model)
        extra_str_metas = get_unique_str_metas_names(self.extra_model)
        for m_a, m_b in product(str_metas, extra_str_metas):
            col = self.data[:, m_a].metas
            extra_col = self.extra_data[:, m_b].metas
            if col.size and extra_col.size \
                    and isinstance(col[0][0], str) \
                    and isinstance(extra_col[0][0], str):
                n_inter = len(np.intersect1d(col, extra_col))
                if n_inter > n_max_intersect:
                    n_max_intersect, attr, extra_attr = n_inter, m_a, m_b
        return attr, extra_attr

    def commit(self):
        self.clear_messages()
        merged = self.merge() if self.data and self.extra_data else None
        self.Outputs.data.send(merged)
        details = format_summary_details(merged) if merged else ""
        summary = len(merged) if merged else self.info.NoOutput
        self.info.set_output_summary(summary, details)

    def send_report(self):
        # pylint: disable=invalid-sequence-index
        self.report_items((
            ("Merging", self.OptionNames[self.merging]),
            ("Match",
             ", ".join(
                 f"{self._get_col_name(left)} with {self._get_col_name(right)}"
                 for left, right in self.attr_boxes.current_state()))))

    def merge(self):
        # pylint: disable=invalid-sequence-index
        pairs = self.attr_boxes.current_state()
        if not self._check_pair_types(pairs):
            return None
        left_vars, right_vars = zip(*pairs)
        left_mask = np.full(len(self.data), True)
        left = np.vstack(tuple(self._values(self.data, var, left_mask)
                               for var in left_vars)).T
        right_mask = np.full(len(self.extra_data), True)
        right = np.vstack(tuple(self._values(self.extra_data, var, right_mask)
                                for var in right_vars)).T
        if not self._check_uniqueness(left, left_mask, right, right_mask):
            return None
        method = self._merge_methods[self.merging]
        lefti, righti, rightu = method(self, left, left_mask, right, right_mask)
        reduced_extra_data = \
            self._compute_reduced_extra_data(right_vars, lefti, righti, rightu)
        return self._join_table_by_indices(
            reduced_extra_data, lefti, righti, rightu)

    def _check_pair_types(self, pairs):
        for left, right in pairs:
            if isinstance(left, ContinuousVariable) \
                    != isinstance(right, ContinuousVariable):
                self.Error.matching_numeric_with_nonnum(left, right)
                return False
            if INDEX in (left, right) and left != right:
                self.Error.matching_index_with_sth(
                    self._get_col_name(({left, right} - {INDEX}).pop()))
                return False
            if INSTANCEID in (left, right) and left != right:
                self.Error.matching_id_with_sth(
                    self._get_col_name(({left, right} - {INSTANCEID}).pop()))
                return False
        return True

    @staticmethod
    def _get_col_name(obj):
        return f"'{obj.name}'" if isinstance(obj, Variable) else obj.lower()

    def _check_uniqueness(self, left, left_mask, right, right_mask):
        ok = True
        masked_right = right[right_mask]
        if len(set(map(tuple, masked_right))) != len(masked_right):
            self.Error.nonunique_right()
            ok = False
        if self.merging != self.LeftJoin:
            masked_left = left[left_mask]
            if len(set(map(tuple, masked_left))) != len(masked_left):
                self.Error.nonunique_left()
                ok = False
        return ok

    def _compute_reduced_extra_data(self,
                                    right_match_vars, lefti, righti, rightu):
        """Prepare a table with extra columns that will appear in the merged
        table"""
        domain = self.data.domain
        extra_domain = self.extra_data.domain

        def var_needed(var):
            if rightu is not None and rightu.size:
                return True
            if var in right_match_vars and self.merging != self.OuterJoin:
                return False
            if var not in domain:
                return True
            both_defined = (lefti != -1) * (righti != -1)
            left_col = \
                self.data.get_column_view(var)[0][lefti[both_defined]]
            right_col = \
                self.extra_data.get_column_view(var)[0][righti[both_defined]]
            if var.is_primitive():
                left_col = left_col.astype(float)
                right_col = right_col.astype(float)
                mask_left = np.isfinite(left_col)
                mask_right = np.isfinite(right_col)
                return not (
                    np.all(mask_left == mask_right)
                    and np.all(left_col[mask_left] == right_col[mask_right]))
            else:
                return not np.all(left_col == right_col)

        extra_vars = [
            var for var in chain(extra_domain.variables, extra_domain.metas)
            if var_needed(var)]
        return self.extra_data[:, extra_vars]

    @staticmethod
    def _values(data, var, mask):
        """Return an iterotor over keys for rows of the table."""
        if var == INDEX:
            return np.arange(len(data))
        if var == INSTANCEID:
            return np.fromiter(
                (inst.id for inst in data), count=len(data), dtype=np.int)
        col = data.get_column_view(var)[0]
        if var.is_primitive():
            col = col.astype(float, copy=False)
            nans = np.isnan(col)
            mask *= ~nans
            if var.is_discrete:
                col = col.astype(int)
                col[nans] = len(var.values)
                col = np.array(var.values + (np.nan, ))[col]
        else:
            col = col.copy()
            defined = col.astype(bool)
            mask *= defined
            col[~mask] = np.nan
        return col

    def _left_join_indices(self, left, left_mask, right, right_mask):
        """Compute a two-row array of indices:
        - the first row contains indices for the primary table,
        - the second row contains the matching rows in the extra table or -1"""
        data = self.data
        # Don't match nans. This is needed since numpy may change nan to string
        # nan, so nan's will match each other
        indices = np.arange(len(right))
        indices[~right_mask] = -1
        if right.shape[1] == 1:
            # The more common case can be handled faster
            right_map = dict(zip(right.flatten(), indices))
            righti = (right_map.get(val, -1) for val in left.flatten())
        else:
            right_map = dict(zip(map(tuple, right), indices))
            righti = (right_map.get(tuple(val), -1) for val in left)
        righti = np.fromiter(righti, dtype=np.int64, count=len(data))
        lefti = np.arange(len(data), dtype=np.int64)
        righti[lefti[~left_mask]] = -1
        return lefti, righti, None

    def _inner_join_indices(self, left, left_mask, right, right_mask):
        """Use _augment_indices to compute the array of indices,
        then remove those with no match in the second table"""
        lefti, righti, _ = \
            self._left_join_indices(left, left_mask, right, right_mask)
        mask = righti != [-1]
        return lefti[mask], righti[mask], None

    def _outer_join_indices(self, left, left_mask, right, right_mask):
        """Use _augment_indices to compute the array of indices,
        then add rows in the second table without a match in the first"""
        lefti, righti, _ = \
            self._left_join_indices(left, left_mask, right, right_mask)
        unused = np.full(len(right), True)
        unused[righti] = False
        if len(right) - 1 not in righti:
            # righti can include -1, which sets the last element as used
            unused[-1] = True
        return lefti, righti, np.nonzero(unused)[0]

    _merge_methods = [
        _left_join_indices, _inner_join_indices, _outer_join_indices]

    def _join_table_by_indices(self, reduced_extra, lefti, righti, rightu):
        """Join (horizontally) self.data and reduced_extra, taking the pairs
        of rows given in indices"""
        if not lefti.size:
            return None
        lt_dom = self.data.domain
        xt_dom = reduced_extra.domain
        domain = self._domain_rename_duplicates(
            lt_dom.attributes + xt_dom.attributes,
            lt_dom.class_vars + xt_dom.class_vars,
            lt_dom.metas + xt_dom.metas)
        X = self._join_array_by_indices(
            self.data.X, reduced_extra.X, lefti, righti)
        Y = self._join_array_by_indices(
            np.c_[self.data.Y], np.c_[reduced_extra.Y], lefti, righti)
        string_cols = [i for i, var in enumerate(domain.metas) if var.is_string]
        metas = self._join_array_by_indices(
            self.data.metas, reduced_extra.metas, lefti, righti, string_cols)
        if rightu is not None:
            # This domain is used for transforming the extra rows for outer join
            # It must use the original - not renamed - variables from right, so
            # values are copied,
            # but new domain for the left, so renamed values are *not* copied
            right_domain = Orange.data.Domain(
                domain.attributes[:len(lt_dom.attributes)] + xt_dom.attributes,
                domain.class_vars[:len(lt_dom.class_vars)] + xt_dom.class_vars,
                domain.metas[:len(lt_dom.metas)] + xt_dom.metas)
            extras = self.extra_data[rightu].transform(right_domain)
            X = np.vstack((X, extras.X))
            extras_Y = extras.Y
            if extras_Y.ndim == 1:
                extras_Y = extras_Y.reshape(-1, 1)
            Y = np.vstack((Y, extras_Y))
            metas = np.vstack((metas, extras.metas))
        table = Orange.data.Table.from_numpy(domain, X, Y, metas)
        table.name = getattr(self.data, 'name', '')
        table.attributes = getattr(self.data, 'attributes', {})
        if rightu is not None:
            table.ids = np.hstack(
                (self.data.ids, self.extra_data.ids[rightu]))
        else:
            table.ids = self.data.ids[lefti]

        return table

    def _domain_rename_duplicates(self, attributes, class_vars, metas):
        """Check for duplicate variable names in domain. If any, rename
        the variables, by replacing them with new ones (names are
        appended a number). """
        attrs, cvars, mets = [], [], []
        n_attrs, n_cvars, n_metas = len(attributes), len(class_vars), len(metas)
        lists = [attrs] * n_attrs + [cvars] * n_cvars + [mets] * n_metas

        all_vars = attributes + class_vars + metas
        proposed_names = [m.name for m in all_vars]
        unique_names = get_unique_names_duplicates(proposed_names)
        duplicates = set()
        for p_name, u_name, var, c in zip(proposed_names, unique_names,
                                          all_vars, lists):
            if p_name != u_name:
                duplicates.add(p_name)
                var = var.copy(name=u_name)
            c.append(var)
        if duplicates:
            self.Warning.renamed_vars(", ".join(duplicates))
        return Orange.data.Domain(attrs, cvars, mets)

    @staticmethod
    def _join_array_by_indices(left, right, lefti, righti, string_cols=None):
        """Join (horizontally) two arrays, taking pairs of rows given in indices
        """
        def prepare(arr, inds, str_cols):
            try:
                newarr = arr[inds]
            except IndexError:
                newarr = np.full_like(arr, np.nan)
            else:
                empty = np.full(arr.shape[1], np.nan)
                if str_cols:
                    assert arr.dtype == object
                    empty = empty.astype(object)
                    empty[str_cols] = ''
                newarr[inds == -1] = empty
            return newarr

        left_width = left.shape[1]
        str_left = [i for i in string_cols or () if i < left_width]
        str_right = [i - left_width for i in string_cols or () if i >= left_width]
        res = hstack((prepare(left, lefti, str_left),
                      prepare(right, righti, str_right)))
        return res

    @staticmethod
    def migrate_settings(settings, version=None):
        def mig_value(x):
            if x == "Position (index)":
                return INDEX
            if x == "Source position (index)":
                return INSTANCEID
            return x

        if not version:
            operations = ("augment", "merge", "combine")
            oper = operations[settings["merging"]]
            settings["attr_pairs"] = (
                True, True,
                [(mig_value(settings[f"attr_{oper}_data"]),
                  mig_value(settings[f"attr_{oper}_extra"]))])
            for oper in operations:
                del settings[f"attr_{oper}_data"]
                del settings[f"attr_{oper}_extra"]

        if not version or version < 2 and "attr_pairs" in settings:
            data_exists, extra_exists, attr_pairs = settings.pop("attr_pairs")
            if not (data_exists and extra_exists):
                settings["context_settings"] = []
                return

            mapper = {0: (INDEX, 100), 1: (INSTANCEID, 100)}
            context = ContextHandler().new_context()
            context.values["attr_pairs"] = [tuple(mapper.get(var, (var, 100))
                                                  for var in pair)
                                            for pair in attr_pairs]
            context.variables1 = {}
            context.variables2 = {}
            settings["context_settings"] = [context]


if __name__ == "__main__":  # pragma: no cover
    WidgetPreview(OWMergeData).run(
        set_data=Orange.data.Table("tests/data-gender-region"),
        set_extra_data=Orange.data.Table("tests/data-regions"))

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