{
    "file_diffs": [
        {
            "old_file_content": "\"\"\"\n\nTODO:\n  - Sorting by standard deviation: Use coefficient of variation (std/mean)\n    or quartile coefficient of dispersion (Q3 - Q1) / (Q3 + Q1)\n  - Standard deviation for nominal: try out Variation ratio (1 - n_mode/N)\n\"\"\"\n\nimport datetime\nfrom enum import IntEnum\nfrom itertools import chain\nfrom typing import Any, Optional, Tuple, List\n\nimport numpy as np\nimport scipy.stats as ss\nimport scipy.sparse as sp\nfrom AnyQt.QtCore import Qt, QSize, QRectF, QModelIndex, pyqtSlot, \\\n    QRegExp, QItemSelection, QItemSelectionRange, QItemSelectionModel\nfrom AnyQt.QtGui import QPainter, QColor\nfrom AnyQt.QtWidgets import QStyledItemDelegate, QGraphicsScene, QTableView, \\\n    QHeaderView, QStyle, QStyleOptionViewItem\n\nimport Orange.statistics.util as ut\nfrom Orange.data import Table, StringVariable, DiscreteVariable, \\\n    ContinuousVariable, TimeVariable, Domain, Variable\nfrom Orange.widgets import widget, gui\nfrom Orange.widgets.data.utils.histogram import Histogram\nfrom Orange.widgets.settings import Setting, ContextSetting, \\\n    DomainContextHandler\nfrom Orange.widgets.utils.itemmodels import DomainModel, AbstractSortTableModel\nfrom Orange.widgets.utils.signals import Input, Output\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.utils.state_summary import format_summary_details\n\n\ndef _categorical_entropy(x):\n    \"\"\"Compute the entropy of a dense/sparse matrix, column-wise. Assuming\n    categorical values.\"\"\"\n    p = [ut.bincount(row)[0] for row in x.T]\n    p = [pk / np.sum(pk) for pk in p]\n    return np.fromiter((ss.entropy(pk) for pk in p), dtype=np.float64)\n\n\ndef format_time_diff(start, end, round_up_after=2):\n    \"\"\"Return an approximate human readable time difference between two dates.\n\n    Parameters\n    ----------\n    start : int\n        Unix timestamp\n    end : int\n        Unix timestamp\n    round_up_after : int\n        The number of time units before we round up to the next, larger time\n        unit e.g. setting to 2 will allow up to 2 days worth of hours to be\n        shown, after that the difference is shown in days. Or put another way\n        we will show from 1-48 hours before switching to days.\n\n    Returns\n    -------\n    str\n\n    \"\"\"\n    start = datetime.datetime.fromtimestamp(start)\n    end = datetime.datetime.fromtimestamp(end)\n    diff = abs(end - start)  # type: datetime.timedelta\n\n    # Get the different resolutions\n    seconds = diff.total_seconds()\n    minutes = seconds // 60\n    hours = minutes // 60\n    days = diff.days\n    weeks = days // 7\n    months = (end.year - start.year) * 12 + end.month - start.month\n    years = months // 12\n\n    # Check which resolution is most appropriate\n    if years >= round_up_after:\n        return '~%d years' % years\n    elif months >= round_up_after:\n        return '~%d months' % months\n    elif weeks >= round_up_after:\n        return '~%d weeks' % weeks\n    elif days >= round_up_after:\n        return '~%d days' % days\n    elif hours >= round_up_after:\n        return '~%d hours' % hours\n    elif minutes >= round_up_after:\n        return '~%d minutes' % minutes\n    else:\n        return '%d seconds' % seconds\n\n\nclass FeatureStatisticsTableModel(AbstractSortTableModel):\n    CLASS_VAR, META, ATTRIBUTE = range(3)\n    COLOR_FOR_ROLE = {\n        CLASS_VAR: QColor(160, 160, 160),\n        META: QColor(220, 220, 200),\n        ATTRIBUTE: QColor(255, 255, 255),\n    }\n\n    HIDDEN_VAR_TYPES = (StringVariable,)\n\n    class Columns(IntEnum):\n        ICON, NAME, DISTRIBUTION, CENTER, DISPERSION, MIN, MAX, MISSING = range(8)\n\n        @property\n        def name(self):\n            return {self.ICON: '',\n                    self.NAME: 'Name',\n                    self.DISTRIBUTION: 'Distribution',\n                    self.CENTER: 'Center',\n                    self.DISPERSION: 'Dispersion',\n                    self.MIN: 'Min.',\n                    self.MAX: 'Max.',\n                    self.MISSING: 'Missing',\n                    }[self.value]\n\n        @property\n        def index(self):\n            return self.value\n\n        @classmethod\n        def from_index(cls, index):\n            return cls(index)\n\n    def __init__(self, data=None, parent=None):\n        \"\"\"\n\n        Parameters\n        ----------\n        data : Optional[Table]\n        parent : Optional[QWidget]\n\n        \"\"\"\n        super().__init__(parent)\n\n        self.table = None  # type: Optional[Table]\n        self.domain = None  # type: Optional[Domain]\n        self.target_var = None  # type: Optional[Variable]\n        self.n_attributes = self.n_instances = 0\n\n        self.__attributes = self.__class_vars = self.__metas = None\n        self.__distributions_cache = {}\n\n        no_data = np.array([])\n        self._variable_types = self._variable_names = no_data\n        self._min = self._max = self._center = no_data\n        self._dispersion = no_data\n        self._missing = no_data\n        # Clear model initially to set default values\n        self.clear()\n\n        self.set_data(data)\n\n    def set_data(self, data):\n        if data is None:\n            self.clear()\n            return\n\n        self.beginResetModel()\n        self.table = data\n        self.domain = domain = data.domain\n        self.target_var = None\n\n        self.__attributes = self.__filter_attributes(domain.attributes, self.table.X)\n        # We disable pylint warning because the `Y` property squeezes vectors,\n        # while we need a 2d array, which `_Y` provides\n        self.__class_vars = self.__filter_attributes(domain.class_vars, self.table._Y)  # pylint: disable=protected-access\n        self.__metas = self.__filter_attributes(domain.metas, self.table.metas)\n\n        self.n_attributes = len(self.variables)\n        self.n_instances = len(data)\n\n        self.__distributions_cache = {}\n        self.__compute_statistics()\n        self.endResetModel()\n\n    def clear(self):\n        self.beginResetModel()\n        self.table = self.domain = self.target_var = None\n        self.n_attributes = self.n_instances = 0\n        self.__attributes = (np.array([]), np.array([]))\n        self.__class_vars = (np.array([]), np.array([]))\n        self.__metas = (np.array([]), np.array([]))\n        self.__distributions_cache.clear()\n        self.endResetModel()\n\n    @property\n    def variables(self):\n        matrices = [self.__attributes[0], self.__class_vars[0], self.__metas[0]]\n        if not any(m.size for m in matrices):\n            return []\n        return np.hstack(matrices)\n\n    @staticmethod\n    def _attr_indices(attrs):\n        # type: (List) -> Tuple[List[int], List[int], List[int], List[int]]\n        \"\"\"Get the indices of different attribute types eg. discrete.\"\"\"\n        disc_var_idx = [i for i, attr in enumerate(attrs) if isinstance(attr, DiscreteVariable)]\n        cont_var_idx = [i for i, attr in enumerate(attrs)\n                        if isinstance(attr, ContinuousVariable)\n                        and not isinstance(attr, TimeVariable)]\n        time_var_idx = [i for i, attr in enumerate(attrs) if isinstance(attr, TimeVariable)]\n        string_var_idx = [i for i, attr in enumerate(attrs) if isinstance(attr, StringVariable)]\n        return disc_var_idx, cont_var_idx, time_var_idx, string_var_idx\n\n    def __filter_attributes(self, attributes, matrix):\n        \"\"\"Filter out variables which shouldn't be visualized.\"\"\"\n        attributes, matrix = np.asarray(attributes), matrix\n        mask = [idx for idx, attr in enumerate(attributes)\n                if not isinstance(attr, self.HIDDEN_VAR_TYPES)]\n        return attributes[mask], matrix[:, mask]\n\n    def __compute_statistics(self):\n        # Since data matrices can of mixed sparsity, we need to compute\n        # attributes separately for each of them.\n        matrices = [self.__attributes, self.__class_vars, self.__metas]\n        # Filter out any matrices with size 0\n        matrices = list(filter(lambda tup: tup[1].size, matrices))\n\n        self._variable_types = np.array([type(var) for var in self.variables])\n        self._variable_names = np.array([var.name.lower() for var in self.variables])\n        self._min = self.__compute_stat(\n            matrices,\n            discrete_f=lambda x: ut.nanmin(x, axis=0),\n            continuous_f=lambda x: ut.nanmin(x, axis=0),\n            time_f=lambda x: ut.nanmin(x, axis=0),\n        )\n        self._dispersion = self.__compute_stat(\n            matrices,\n            discrete_f=_categorical_entropy,\n            continuous_f=lambda x: np.sqrt(ut.nanvar(x, axis=0)) / ut.nanmean(x, axis=0),\n        )\n        self._missing = self.__compute_stat(\n            matrices,\n            discrete_f=lambda x: ut.countnans(x, axis=0),\n            continuous_f=lambda x: ut.countnans(x, axis=0),\n            string_f=lambda x: (x == StringVariable.Unknown).sum(axis=0),\n            time_f=lambda x: ut.countnans(x, axis=0),\n        )\n        self._max = self.__compute_stat(\n            matrices,\n            discrete_f=lambda x: ut.nanmax(x, axis=0),\n            continuous_f=lambda x: ut.nanmax(x, axis=0),\n            time_f=lambda x: ut.nanmax(x, axis=0),\n        )\n\n        # Since scipy apparently can't do mode on sparse matrices, cast it to\n        # dense. This can be very inefficient for large matrices, and should\n        # be changed\n        def __mode(x, *args, **kwargs):\n            if sp.issparse(x):\n                x = x.todense(order=\"C\")\n            # return ss.mode(x, *args, **kwargs)[0]\n            return ut.nanmode(x, *args, **kwargs)[0]  # Temporary replacement for scipy\n\n        self._center = self.__compute_stat(\n            matrices,\n            discrete_f=lambda x: __mode(x, axis=0),\n            continuous_f=lambda x: ut.nanmean(x, axis=0),\n            time_f=lambda x: ut.nanmean(x, axis=0),\n        )\n\n    def get_statistics_matrix(self, variables=None, return_labels=False):\n        \"\"\"Get the numeric computed statistics in a single matrix. Optionally,\n        we can specify for which variables we want the stats. Also, we can get\n        the string column names as labels if desired.\n\n        Parameters\n        ----------\n        variables : Iterable[Union[Variable, int, str]]\n            Return statistics for only the variables specified. Accepts all\n            formats supported by `domain.index`\n        return_labels : bool\n            In addition to the statistics matrix, also return string labels for\n            the columns of the matrix e.g. 'Mean' or 'Dispersion', as specified\n            in `Columns`.\n\n        Returns\n        -------\n        Union[Tuple[List[str], np.ndarray], np.ndarray]\n\n        \"\"\"\n        if self.table is None:\n            return np.atleast_2d([])\n\n        # If a list of variables is given, select only corresponding stats\n        # variables can be a list or array, pylint: disable=len-as-condition\n        if variables is not None and len(variables) != 0:\n            indices = [self.domain.index(var) for var in variables]\n        else:\n            indices = ...\n\n        matrix = np.vstack((\n            self._center[indices], self._dispersion[indices],\n            self._min[indices], self._max[indices], self._missing[indices],\n        )).T\n\n        # Return string labels for the returned matrix columns e.g. 'Mean',\n        # 'Dispersion' if requested\n        if return_labels:\n            labels = [self.Columns.CENTER.name, self.Columns.DISPERSION.name,\n                      self.Columns.MIN.name, self.Columns.MAX.name,\n                      self.Columns.MISSING.name]\n            return labels, matrix\n\n        return matrix\n\n    def __compute_stat(self, matrices, discrete_f=None, continuous_f=None,\n                       time_f=None, string_f=None, default_val=np.nan):\n        \"\"\"Apply functions to appropriate variable types. The default value is\n        returned if there is no function defined for specific variable types.\n        \"\"\"\n        if not matrices:\n            return np.array([])\n\n        def _to_float(data):\n            if not np.issubdtype(data.dtype, np.number):\n                data = data.astype(np.float64)\n            return data\n\n        def _to_object(data):\n            if data.dtype is not np.object:\n                data = data.astype(np.object)\n            return data\n\n        results = []\n        for variables, x in matrices:\n            result = np.full(len(variables), default_val)\n\n            # While the following caching and checks are messy, the indexing\n            # turns out to be a bottleneck for large datasets, so a single\n            # indexing operation improves performance\n            disc_idx, cont_idx, time_idx, str_idx = self._attr_indices(variables)\n            if discrete_f:\n                x_ = x[:, disc_idx]\n                if x_.size:\n                    result[disc_idx] = discrete_f(_to_float(x_))\n            if continuous_f:\n                x_ = x[:, cont_idx]\n                if x_.size:\n                    result[cont_idx] = continuous_f(_to_float(x_))\n            if time_f:\n                x_ = x[:, time_idx]\n                if x_.size:\n                    result[time_idx] = time_f(_to_float(x_))\n            if string_f:\n                x_ = x[:, str_idx]\n                if x_.size:\n                    result[str_idx] = string_f(_to_object(x_))\n\n            results.append(result)\n\n        return np.hstack(results)\n\n    def sortColumnData(self, column):\n        \"\"\"Prepare the arrays with which we will sort the rows. If we want to\n        sort based on a single value e.g. the name, return a 1d array.\n        Sometimes we may want to sort by multiple criteria, comparing\n        continuous variances with discrete entropies makes no sense, so we want\n        to group those variable types together.\n        \"\"\"\n        # Prepare indices for variable types so we can group them together\n        order = [DiscreteVariable, ContinuousVariable, TimeVariable, StringVariable]\n        mapping = {var: idx for idx, var in enumerate(order)}\n        vmapping = np.vectorize(mapping.__getitem__)\n        var_types_indices = vmapping(self._variable_types)\n\n        # Store the variable name sorted indices so we can pass a default\n        # order when sorting by multiple keys\n        var_name_indices = np.argsort(self._variable_names)\n\n        # Prepare vartype indices so ready when needed\n        disc_idx, _, time_idx, str_idx = self._attr_indices(self.variables)\n\n        # Sort by: (type)\n        if column == self.Columns.ICON:\n            return var_types_indices\n        # Sort by: (name)\n        elif column == self.Columns.NAME:\n            # We use `_variable_names` here and not the indices because the\n            # last (or single) row is actually sorted and we don't want to sort\n            # the indices\n            return self._variable_names\n        # Sort by: (None)\n        elif column == self.Columns.DISTRIBUTION:\n            return np.ones_like(var_types_indices)\n        # Sort by: (type, center)\n        elif column == self.Columns.CENTER:\n            # Sorting discrete or string values by mean makes no sense\n            vals = np.array(self._center)\n            vals[disc_idx] = var_name_indices[disc_idx]\n            vals[str_idx] = var_name_indices[str_idx]\n            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T\n        # Sort by: (type, dispersion)\n        elif column == self.Columns.DISPERSION:\n            # Sort time variables by their dispersion, which is not stored in\n            # the dispersion array\n            vals = np.array(self._dispersion)\n            vals[time_idx] = self._max[time_idx] - self._min[time_idx]\n            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T\n        # Sort by: (type, min)\n        elif column == self.Columns.MIN:\n            # Sorting discrete or string values by min makes no sense\n            vals = np.array(self._min)\n            vals[disc_idx] = var_name_indices[disc_idx]\n            vals[str_idx] = var_name_indices[str_idx]\n            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T\n        # Sort by: (type, max)\n        elif column == self.Columns.MAX:\n            # Sorting discrete or string values by min makes no sense\n            vals = np.array(self._max)\n            vals[disc_idx] = var_name_indices[disc_idx]\n            vals[str_idx] = var_name_indices[str_idx]\n            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T\n        # Sort by: (missing)\n        elif column == self.Columns.MISSING:\n            return self._missing\n\n        return None\n\n    def _sortColumnData(self, column):\n        \"\"\"Allow sorting with 2d arrays.\"\"\"\n        data = np.asarray(self.sortColumnData(column))\n        data = data[self.mapToSourceRows(Ellipsis)]\n\n        assert data.ndim <= 2, 'Data should be at most 2-dimensional'\n        return data\n\n    def _argsortData(self, data, order):\n        if data.ndim == 1:\n            indices = np.argsort(data, kind='mergesort')\n            if order == Qt.DescendingOrder:\n                indices = indices[::-1]\n            # Always sort NaNs last\n            if np.issubdtype(data.dtype, np.number):\n                indices = np.roll(indices, -np.isnan(data).sum())\n        else:\n            assert np.issubdtype(data.dtype, np.number), \\\n                'We do not deal with non numeric values in sorting by ' \\\n                'multiple values'\n            if order == Qt.DescendingOrder:\n                data[:, -1] = -data[:, -1]\n\n            # In order to make sure NaNs always appear at the end, insert a\n            # indicator whether NaN or not. Note that the data array must\n            # contain an empty column of zeros at index -2 since inserting an\n            # extra column after the fact can result in a MemoryError for data\n            # with a large amount of variables\n            assert np.all(data[:, -2] == 0), \\\n                'Add an empty column of zeros at index -2 to accomodate NaNs'\n            np.isnan(data[:, -1], out=data[:, -2])\n\n            indices = np.lexsort(np.flip(data.T, axis=0))\n\n        return indices\n\n    def headerData(self, section, orientation, role):\n        # type: (int, Qt.Orientation, Qt.ItemDataRole) -> Any\n        if orientation == Qt.Horizontal:\n            if role == Qt.DisplayRole:\n                return self.Columns.from_index(section).name\n\n        return None\n\n    def data(self, index, role):\n        # type: (QModelIndex, Qt.ItemDataRole) -> Any\n        def background():\n            if attribute in self.domain.attributes:\n                return self.COLOR_FOR_ROLE[self.ATTRIBUTE]\n            if attribute in self.domain.metas:\n                return self.COLOR_FOR_ROLE[self.META]\n            if attribute in self.domain.class_vars:\n                return self.COLOR_FOR_ROLE[self.CLASS_VAR]\n            return None\n\n        def text_alignment():\n            if column == self.Columns.NAME:\n                return Qt.AlignLeft | Qt.AlignVCenter\n            return Qt.AlignRight | Qt.AlignVCenter\n\n        def decoration():\n            if column == self.Columns.ICON:\n                return gui.attributeIconDict[attribute]\n            return None\n\n        def display():\n            # pylint: disable=too-many-branches\n            def render_value(value):\n                return \"\" if np.isnan(value) else attribute.str_val(value)\n\n            if column == self.Columns.NAME:\n                return attribute.name\n            elif column == self.Columns.DISTRIBUTION:\n                if isinstance(attribute,\n                              (DiscreteVariable, ContinuousVariable)):\n                    if row not in self.__distributions_cache:\n                        scene = QGraphicsScene(parent=self)\n                        histogram = Histogram(\n                            data=self.table,\n                            variable=attribute,\n                            color_attribute=self.target_var,\n                            border=(0, 0, 2, 0),\n                            border_color='#ccc',\n                        )\n                        scene.addItem(histogram)\n                        self.__distributions_cache[row] = scene\n                    return self.__distributions_cache[row]\n            elif column == self.Columns.CENTER:\n                return render_value(self._center[row])\n            elif column == self.Columns.DISPERSION:\n                if isinstance(attribute, TimeVariable):\n                    return format_time_diff(self._min[row], self._max[row])\n                elif isinstance(attribute, DiscreteVariable):\n                    return \"%.3g\" % self._dispersion[row]\n                else:\n                    return render_value(self._dispersion[row])\n            elif column == self.Columns.MIN:\n                if not isinstance(attribute, DiscreteVariable) \\\n                        or attribute.ordered:\n                    return render_value(self._min[row])\n            elif column == self.Columns.MAX:\n                if not isinstance(attribute, DiscreteVariable) \\\n                        or attribute.ordered:\n                    return render_value(self._max[row])\n            elif column == self.Columns.MISSING:\n                return '%d (%d%%)' % (\n                    self._missing[row],\n                    100 * self._missing[row] / self.n_instances\n                )\n            return None\n\n        roles = {Qt.BackgroundRole: background,\n                 Qt.TextAlignmentRole: text_alignment,\n                 Qt.DecorationRole: decoration,\n                 Qt.DisplayRole: display}\n\n        if not index.isValid() or role not in roles:\n            return None\n\n        row, column = self.mapToSourceRows(index.row()), index.column()\n        # Make sure we're not out of range\n        if not 0 <= row <= self.n_attributes:\n            return None\n\n        attribute = self.variables[row]\n        return roles[role]()\n\n    def rowCount(self, parent=QModelIndex()):\n        return 0 if parent.isValid() else self.n_attributes\n\n    def columnCount(self, parent=QModelIndex()):\n        return 0 if parent.isValid() else len(self.Columns)\n\n    def set_target_var(self, variable):\n        self.target_var = variable\n        self.__distributions_cache.clear()\n        start_idx = self.index(0, self.Columns.DISTRIBUTION)\n        end_idx = self.index(self.rowCount(), self.Columns.DISTRIBUTION)\n        self.dataChanged.emit(start_idx, end_idx)\n\n\nclass FeatureStatisticsTableView(QTableView):\n    HISTOGRAM_ASPECT_RATIO = (7, 3)\n    MINIMUM_HISTOGRAM_HEIGHT = 50\n    MAXIMUM_HISTOGRAM_HEIGHT = 80\n\n    def __init__(self, model, parent=None, **kwargs):\n        super().__init__(\n            parent=parent,\n            showGrid=False,\n            cornerButtonEnabled=False,\n            sortingEnabled=True,\n            selectionBehavior=QTableView.SelectRows,\n            selectionMode=QTableView.ExtendedSelection,\n            horizontalScrollMode=QTableView.ScrollPerPixel,\n            verticalScrollMode=QTableView.ScrollPerPixel,\n            **kwargs\n        )\n        self.setModel(model)\n\n        hheader = self.horizontalHeader()\n        hheader.setStretchLastSection(False)\n        # Contents precision specifies how many rows should be taken into\n        # account when computing the sizes, 0 being the visible rows. This is\n        # crucial, since otherwise the `ResizeToContents` section resize mode\n        # would call `sizeHint` on every single row in the data before first\n        # render. However this, this cannot be used here, since this only\n        # appears to work properly when the widget is actually shown. When the\n        # widget is not shown, size `sizeHint` is called on every row.\n        hheader.setResizeContentsPrecision(5)\n        # Set a nice default size so that headers have some space around titles\n        hheader.setDefaultSectionSize(100)\n        # Set individual column behaviour in `set_data` since the logical\n        # indices must be valid in the model, which requires data.\n        hheader.setSectionResizeMode(QHeaderView.Interactive)\n\n        columns = model.Columns\n        hheader.setSectionResizeMode(columns.ICON.index, QHeaderView.ResizeToContents)\n        hheader.setSectionResizeMode(columns.DISTRIBUTION.index, QHeaderView.Stretch)\n\n        vheader = self.verticalHeader()\n        vheader.setVisible(False)\n        vheader.setSectionResizeMode(QHeaderView.Fixed)\n        hheader.sectionResized.connect(self.bind_histogram_aspect_ratio)\n        # TODO: This shifts the scrollarea a bit down when opening widget\n        # hheader.sectionResized.connect(self.keep_row_centered)\n\n        self.setItemDelegate(NoFocusRectDelegate(parent=self))\n        self.setItemDelegateForColumn(\n            FeatureStatisticsTableModel.Columns.DISTRIBUTION,\n            DistributionDelegate(parent=self),\n        )\n\n    def bind_histogram_aspect_ratio(self, logical_index, _, new_size):\n        \"\"\"Force the horizontal and vertical header to maintain the defined\n        aspect ratio specified for the histogram.\"\"\"\n        # Prevent function being exectued more than once per resize\n        if logical_index is not self.model().Columns.DISTRIBUTION.index:\n            return\n        ratio_width, ratio_height = self.HISTOGRAM_ASPECT_RATIO\n        unit_width = new_size / ratio_width\n        new_height = unit_width * ratio_height\n        effective_height = max(new_height, self.MINIMUM_HISTOGRAM_HEIGHT)\n        effective_height = min(effective_height, self.MAXIMUM_HISTOGRAM_HEIGHT)\n        self.verticalHeader().setDefaultSectionSize(effective_height)\n\n    def keep_row_centered(self, logical_index, _1, _2):\n        \"\"\"When resizing the widget when scrolled further down, the\n        positions of rows changes. Obviously, the user resized in order to\n        better see the row of interest. This keeps that row centered.\"\"\"\n        # TODO: This does not work properly\n        # Prevent function being exectued more than once per resize\n        if logical_index is not self.model().Columns.DISTRIBUTION.index:\n            return\n        top_row = self.indexAt(self.rect().topLeft()).row()\n        bottom_row = self.indexAt(self.rect().bottomLeft()).row()\n        middle_row = top_row + (bottom_row - top_row) // 2\n        self.scrollTo(self.model().index(middle_row, 0), QTableView.PositionAtCenter)\n\n\nclass NoFocusRectDelegate(QStyledItemDelegate):\n    \"\"\"Removes the light blue background and border on a focused item.\"\"\"\n\n    def paint(self, painter, option, index):\n        # type: (QPainter, QStyleOptionViewItem, QModelIndex) -> None\n        option.state &= ~QStyle.State_HasFocus\n        super().paint(painter, option, index)\n\n\nclass DistributionDelegate(QStyledItemDelegate):\n    def paint(self, painter, option, index):\n        # type: (QPainter, QStyleOptionViewItem, QModelIndex) -> None\n        scene = index.data(Qt.DisplayRole)  # type: Optional[QGraphicsScene]\n        if scene is None:\n            return super().paint(painter, option, index)\n\n        painter.setRenderHint(QPainter.Antialiasing)\n\n        if option.state & QStyle.State_Selected:\n            background_color = option.palette.highlight()\n        else:\n            background_color = index.data(Qt.BackgroundRole)\n        if background_color is not None:\n            scene.setBackgroundBrush(background_color)\n\n        scene.render(painter, target=QRectF(option.rect), mode=Qt.IgnoreAspectRatio)\n\n        # pylint complains about inconsistent return statements\n        return None\n\n\nclass OWFeatureStatistics(widget.OWWidget):\n    name = 'Feature Statistics'\n    description = 'Show basic statistics for data features.'\n    icon = 'icons/FeatureStatistics.svg'\n\n    class Inputs:\n        data = Input('Data', Table, default=True)\n\n    class Outputs:\n        reduced_data = Output('Reduced Data', Table, default=True)\n        statistics = Output('Statistics', Table)\n\n    want_main_area = True\n    buttons_area_orientation = Qt.Vertical\n\n    settingsHandler = DomainContextHandler()\n    settings_version = 2\n\n    auto_commit = Setting(True)\n    color_var = ContextSetting(None)  # type: Optional[Variable]\n    # filter_string = ContextSetting('')\n\n    sorting = Setting((0, Qt.DescendingOrder))\n    selected_vars = ContextSetting([], schema_only=True)\n\n    def __init__(self):\n        super().__init__()\n\n        self.data = None  # type: Optional[Table]\n\n        # TODO: Implement filtering on the model\n        # filter_box = gui.vBox(self.controlArea, 'Filter')\n        # self.filter_text = gui.lineEdit(\n        #     filter_box, self, value='filter_string',\n        #     placeholderText='Filter variables by name',\n        #     callback=self._filter_table_variables, callbackOnType=True,\n        # )\n        # shortcut = QShortcut(QKeySequence('Ctrl+f'), self, self.filter_text.setFocus)\n        # shortcut.setWhatsThis('Filter variables by name')\n\n        self.color_var_model = DomainModel(\n            valid_types=(ContinuousVariable, DiscreteVariable),\n            placeholder='None',\n        )\n        box = gui.vBox(self.controlArea, 'Histogram')\n        self.cb_color_var = gui.comboBox(\n            box, master=self, value='color_var', model=self.color_var_model,\n            label='Color:', orientation=Qt.Horizontal, contentsLength=13,\n            searchable=True\n        )\n        self.cb_color_var.activated.connect(self.__color_var_changed)\n\n        gui.rubber(self.controlArea)\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        self.info.set_input_summary(self.info.NoInput)\n        self.info.set_output_summary(self.info.NoOutput)\n\n        # Main area\n        self.model = FeatureStatisticsTableModel(parent=self)\n        self.table_view = FeatureStatisticsTableView(self.model, parent=self)\n        self.table_view.selectionModel().selectionChanged.connect(self.on_select)\n        self.table_view.horizontalHeader().sectionClicked.connect(self.on_header_click)\n\n        self.mainArea.layout().addWidget(self.table_view)\n\n    @staticmethod\n    def sizeHint():\n        return QSize(1050, 500)\n\n    def _filter_table_variables(self):\n        regex = QRegExp(self.filter_string)\n        # If the user explicitly types different cases, we assume they know\n        # what they are searching for and account for letter case in filter\n        different_case = (\n            any(c.islower() for c in self.filter_string) and\n            any(c.isupper() for c in self.filter_string)\n        )\n        if not different_case:\n            regex.setCaseSensitivity(Qt.CaseInsensitive)\n\n    @Inputs.data\n    def set_data(self, data):\n        # Clear outputs and reset widget state\n        self.closeContext()\n        self.selected_vars = []\n        self.model.resetSorting()\n        self.Outputs.reduced_data.send(None)\n        self.Outputs.statistics.send(None)\n\n        # Setup widget state for new data and restore settings\n        self.data = data\n\n        if data is not None:\n            self.info.set_input_summary(len(data),\n                                        format_summary_details(data))\n            self.color_var_model.set_domain(data.domain)\n            self.color_var = None\n            if self.data.domain.class_vars:\n                self.color_var = self.data.domain.class_vars[0]\n        else:\n            self.info.set_input_summary(self.info.NoInput)\n            self.color_var_model.set_domain(None)\n            self.color_var = None\n        self.model.set_data(data)\n\n        self.openContext(self.data)\n        self.__restore_selection()\n        self.__restore_sorting()\n        # self._filter_table_variables()\n        self.__color_var_changed()\n\n        self.commit()\n\n    def __restore_selection(self):\n        \"\"\"Restore the selection on the table view from saved settings.\"\"\"\n        selection_model = self.table_view.selectionModel()\n        selection = QItemSelection()\n        if self.selected_vars:\n            var_indices = {var: i for i, var in enumerate(self.model.variables)}\n            selected_indices = [var_indices[var] for var in self.selected_vars]\n            for row in self.model.mapFromSourceRows(selected_indices):\n                selection.append(QItemSelectionRange(\n                    self.model.index(row, 0),\n                    self.model.index(row, self.model.columnCount() - 1)\n                ))\n        selection_model.select(selection, QItemSelectionModel.ClearAndSelect)\n\n    def __restore_sorting(self):\n        \"\"\"Restore the sort column and order from saved settings.\"\"\"\n        sort_column, sort_order = self.sorting\n        if self.model.n_attributes and sort_column < self.model.columnCount():\n            self.model.sort(sort_column, sort_order)\n            self.table_view.horizontalHeader().setSortIndicator(sort_column, sort_order)\n\n    @pyqtSlot(int)\n    def on_header_click(self, *_):\n        # Store the header states\n        sort_order = self.model.sortOrder()\n        sort_column = self.model.sortColumn()\n        self.sorting = sort_column, sort_order\n\n    @pyqtSlot(int)\n    def __color_var_changed(self, *_):\n        if self.model is not None:\n            self.model.set_target_var(self.color_var)\n\n    def on_select(self):\n        selection_indices = list(self.model.mapToSourceRows([\n            i.row() for i in self.table_view.selectionModel().selectedRows()\n        ]))\n        self.selected_vars = list(self.model.variables[selection_indices])\n        self.commit()\n\n    def commit(self):\n        if not self.selected_vars:\n            self.info.set_output_summary(self.info.NoOutput)\n            self.Outputs.reduced_data.send(None)\n            self.Outputs.statistics.send(None)\n            return\n\n        # Send a table with only selected columns to output\n        variables = self.selected_vars\n        self.info.set_output_summary(len(self.data[:, variables]),\n                                     format_summary_details(self.data[:, variables]))\n        self.Outputs.reduced_data.send(self.data[:, variables])\n\n        # Send the statistics of the selected variables to ouput\n        labels, data = self.model.get_statistics_matrix(variables, return_labels=True)\n        var_names = np.atleast_2d([var.name for var in variables]).T\n        domain = Domain(\n            attributes=[ContinuousVariable(name) for name in labels],\n            metas=[StringVariable('Feature')]\n        )\n        statistics = Table(domain, data, metas=var_names)\n        statistics.name = '%s (Feature Statistics)' % self.data.name\n        self.Outputs.statistics.send(statistics)\n\n    def send_report(self):\n        pass\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        if not version or version < 2:\n            selected_rows = context.values.pop(\"selected_rows\", None)\n            if not selected_rows:\n                selected_vars = []\n            else:\n                # This assumes that dict was saved by Python >= 3.6 so dict is\n                # ordered; if not, context hasn't had worked anyway.\n                all_vars = [\n                    (var, tpe)\n                    for (var, tpe) in chain(context.attributes.items(),\n                                            context.metas.items())\n                    # it would be nicer to use cls.HIDDEN_VAR_TYPES, but there\n                    # is no suitable conversion function, and StringVariable (3)\n                    # was the only hidden var when settings_version < 2, so:\n                    if tpe != 3]\n                selected_vars = [all_vars[i] for i in selected_rows]\n            context.values[\"selected_vars\"] = selected_vars, -3\n\n\nif __name__ == '__main__':  # pragma: no cover\n    WidgetPreview(OWFeatureStatistics).run(Table(\"iris\"))\n",
            "new_file_content": "\"\"\"\n\nTODO:\n  - Sorting by standard deviation: Use coefficient of variation (std/mean)\n    or quartile coefficient of dispersion (Q3 - Q1) / (Q3 + Q1)\n  - Standard deviation for nominal: try out Variation ratio (1 - n_mode/N)\n\"\"\"\n\nimport datetime\nfrom enum import IntEnum\nfrom itertools import chain\nfrom typing import Any, Optional, Tuple, List\n\nimport numpy as np\nimport scipy.stats as ss\nimport scipy.sparse as sp\nfrom AnyQt.QtCore import Qt, QSize, QRectF, QModelIndex, pyqtSlot, \\\n    QRegExp, QItemSelection, QItemSelectionRange, QItemSelectionModel\nfrom AnyQt.QtGui import QPainter, QColor\nfrom AnyQt.QtWidgets import QStyledItemDelegate, QGraphicsScene, QTableView, \\\n    QHeaderView, QStyle, QStyleOptionViewItem\n\nimport Orange.statistics.util as ut\nfrom Orange.data import Table, StringVariable, DiscreteVariable, \\\n    ContinuousVariable, TimeVariable, Domain, Variable\nfrom Orange.widgets import widget, gui\nfrom Orange.widgets.data.utils.histogram import Histogram\nfrom Orange.widgets.settings import Setting, ContextSetting, \\\n    DomainContextHandler\nfrom Orange.widgets.utils.itemmodels import DomainModel, AbstractSortTableModel\nfrom Orange.widgets.utils.signals import Input, Output\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.utils.state_summary import format_summary_details\n\n\ndef _categorical_entropy(x):\n    \"\"\"Compute the entropy of a dense/sparse matrix, column-wise. Assuming\n    categorical values.\"\"\"\n    p = [ut.bincount(row)[0] for row in x.T]\n    p = [pk / np.sum(pk) for pk in p]\n    return np.fromiter((ss.entropy(pk) for pk in p), dtype=np.float64)\n\n\ndef format_time_diff(start, end, round_up_after=2):\n    \"\"\"Return an approximate human readable time difference between two dates.\n\n    Parameters\n    ----------\n    start : int\n        Unix timestamp\n    end : int\n        Unix timestamp\n    round_up_after : int\n        The number of time units before we round up to the next, larger time\n        unit e.g. setting to 2 will allow up to 2 days worth of hours to be\n        shown, after that the difference is shown in days. Or put another way\n        we will show from 1-48 hours before switching to days.\n\n    Returns\n    -------\n    str\n\n    \"\"\"\n    start = datetime.datetime.fromtimestamp(start)\n    end = datetime.datetime.fromtimestamp(end)\n    diff = abs(end - start)  # type: datetime.timedelta\n\n    # Get the different resolutions\n    seconds = diff.total_seconds()\n    minutes = seconds // 60\n    hours = minutes // 60\n    days = diff.days\n    weeks = days // 7\n    months = (end.year - start.year) * 12 + end.month - start.month\n    years = months // 12\n\n    # Check which resolution is most appropriate\n    if years >= round_up_after:\n        return '~%d years' % years\n    elif months >= round_up_after:\n        return '~%d months' % months\n    elif weeks >= round_up_after:\n        return '~%d weeks' % weeks\n    elif days >= round_up_after:\n        return '~%d days' % days\n    elif hours >= round_up_after:\n        return '~%d hours' % hours\n    elif minutes >= round_up_after:\n        return '~%d minutes' % minutes\n    else:\n        return '%d seconds' % seconds\n\n\nclass FeatureStatisticsTableModel(AbstractSortTableModel):\n    CLASS_VAR, META, ATTRIBUTE = range(3)\n    COLOR_FOR_ROLE = {\n        CLASS_VAR: QColor(160, 160, 160),\n        META: QColor(220, 220, 200),\n        ATTRIBUTE: QColor(255, 255, 255),\n    }\n\n    HIDDEN_VAR_TYPES = (StringVariable,)\n\n    class Columns(IntEnum):\n        ICON, NAME, DISTRIBUTION, CENTER, DISPERSION, MIN, MAX, MISSING = range(8)\n\n        @property\n        def name(self):\n            return {self.ICON: '',\n                    self.NAME: 'Name',\n                    self.DISTRIBUTION: 'Distribution',\n                    self.CENTER: 'Center',\n                    self.DISPERSION: 'Dispersion',\n                    self.MIN: 'Min.',\n                    self.MAX: 'Max.',\n                    self.MISSING: 'Missing',\n                    }[self.value]\n\n        @property\n        def index(self):\n            return self.value\n\n        @classmethod\n        def from_index(cls, index):\n            return cls(index)\n\n    def __init__(self, data=None, parent=None):\n        \"\"\"\n\n        Parameters\n        ----------\n        data : Optional[Table]\n        parent : Optional[QWidget]\n\n        \"\"\"\n        super().__init__(parent)\n\n        self.table = None  # type: Optional[Table]\n        self.domain = None  # type: Optional[Domain]\n        self.target_var = None  # type: Optional[Variable]\n        self.n_attributes = self.n_instances = 0\n\n        self.__attributes = self.__class_vars = self.__metas = None\n        self.__distributions_cache = {}\n\n        no_data = np.array([])\n        self._variable_types = self._variable_names = no_data\n        self._min = self._max = self._center = no_data\n        self._dispersion = no_data\n        self._missing = no_data\n        # Clear model initially to set default values\n        self.clear()\n\n        self.set_data(data)\n\n    def set_data(self, data):\n        if data is None:\n            self.clear()\n            return\n\n        self.beginResetModel()\n        self.table = data\n        self.domain = domain = data.domain\n        self.target_var = None\n\n        self.__attributes = self.__filter_attributes(domain.attributes, self.table.X)\n        # We disable pylint warning because the `Y` property squeezes vectors,\n        # while we need a 2d array, which `_Y` provides\n        self.__class_vars = self.__filter_attributes(domain.class_vars, self.table._Y)  # pylint: disable=protected-access\n        self.__metas = self.__filter_attributes(domain.metas, self.table.metas)\n\n        self.n_attributes = len(self.variables)\n        self.n_instances = len(data)\n\n        self.__distributions_cache = {}\n        self.__compute_statistics()\n        self.endResetModel()\n\n    def clear(self):\n        self.beginResetModel()\n        self.table = self.domain = self.target_var = None\n        self.n_attributes = self.n_instances = 0\n        self.__attributes = (np.array([]), np.array([]))\n        self.__class_vars = (np.array([]), np.array([]))\n        self.__metas = (np.array([]), np.array([]))\n        self.__distributions_cache.clear()\n        self.endResetModel()\n\n    @property\n    def variables(self):\n        matrices = [self.__attributes[0], self.__class_vars[0], self.__metas[0]]\n        if not any(m.size for m in matrices):\n            return []\n        return np.hstack(matrices)\n\n    @staticmethod\n    def _attr_indices(attrs):\n        # type: (List) -> Tuple[List[int], List[int], List[int], List[int]]\n        \"\"\"Get the indices of different attribute types eg. discrete.\"\"\"\n        disc_var_idx = [i for i, attr in enumerate(attrs) if isinstance(attr, DiscreteVariable)]\n        cont_var_idx = [i for i, attr in enumerate(attrs)\n                        if isinstance(attr, ContinuousVariable)\n                        and not isinstance(attr, TimeVariable)]\n        time_var_idx = [i for i, attr in enumerate(attrs) if isinstance(attr, TimeVariable)]\n        string_var_idx = [i for i, attr in enumerate(attrs) if isinstance(attr, StringVariable)]\n        return disc_var_idx, cont_var_idx, time_var_idx, string_var_idx\n\n    def __filter_attributes(self, attributes, matrix):\n        \"\"\"Filter out variables which shouldn't be visualized.\"\"\"\n        attributes, matrix = np.asarray(attributes), matrix\n        mask = [idx for idx, attr in enumerate(attributes)\n                if not isinstance(attr, self.HIDDEN_VAR_TYPES)]\n        return attributes[mask], matrix[:, mask]\n\n    def __compute_statistics(self):\n        # Since data matrices can of mixed sparsity, we need to compute\n        # attributes separately for each of them.\n        matrices = [self.__attributes, self.__class_vars, self.__metas]\n        # Filter out any matrices with size 0\n        matrices = list(filter(lambda tup: tup[1].size, matrices))\n\n        self._variable_types = np.array([type(var) for var in self.variables])\n        self._variable_names = np.array([var.name.lower() for var in self.variables])\n        self._min = self.__compute_stat(\n            matrices,\n            discrete_f=lambda x: ut.nanmin(x, axis=0),\n            continuous_f=lambda x: ut.nanmin(x, axis=0),\n            time_f=lambda x: ut.nanmin(x, axis=0),\n        )\n        self._dispersion = self.__compute_stat(\n            matrices,\n            discrete_f=_categorical_entropy,\n            continuous_f=lambda x: np.sqrt(ut.nanvar(x, axis=0)) / ut.nanmean(x, axis=0),\n        )\n        self._missing = self.__compute_stat(\n            matrices,\n            discrete_f=lambda x: ut.countnans(x, axis=0),\n            continuous_f=lambda x: ut.countnans(x, axis=0),\n            string_f=lambda x: (x == StringVariable.Unknown).sum(axis=0),\n            time_f=lambda x: ut.countnans(x, axis=0),\n        )\n        self._max = self.__compute_stat(\n            matrices,\n            discrete_f=lambda x: ut.nanmax(x, axis=0),\n            continuous_f=lambda x: ut.nanmax(x, axis=0),\n            time_f=lambda x: ut.nanmax(x, axis=0),\n        )\n\n        # Since scipy apparently can't do mode on sparse matrices, cast it to\n        # dense. This can be very inefficient for large matrices, and should\n        # be changed\n        def __mode(x, *args, **kwargs):\n            if sp.issparse(x):\n                x = x.todense(order=\"C\")\n            # return ss.mode(x, *args, **kwargs)[0]\n            return ut.nanmode(x, *args, **kwargs)[0]  # Temporary replacement for scipy\n\n        self._center = self.__compute_stat(\n            matrices,\n            discrete_f=lambda x: __mode(x, axis=0),\n            continuous_f=lambda x: ut.nanmean(x, axis=0),\n            time_f=lambda x: ut.nanmean(x, axis=0),\n        )\n\n    def get_statistics_matrix(self, variables=None, return_labels=False):\n        \"\"\"Get the numeric computed statistics in a single matrix. Optionally,\n        we can specify for which variables we want the stats. Also, we can get\n        the string column names as labels if desired.\n\n        Parameters\n        ----------\n        variables : Iterable[Union[Variable, int, str]]\n            Return statistics for only the variables specified. Accepts all\n            formats supported by `domain.index`\n        return_labels : bool\n            In addition to the statistics matrix, also return string labels for\n            the columns of the matrix e.g. 'Mean' or 'Dispersion', as specified\n            in `Columns`.\n\n        Returns\n        -------\n        Union[Tuple[List[str], np.ndarray], np.ndarray]\n\n        \"\"\"\n        if self.table is None:\n            return np.atleast_2d([])\n\n        # If a list of variables is given, select only corresponding stats\n        # variables can be a list or array, pylint: disable=len-as-condition\n        if variables is not None and len(variables) != 0:\n            indices = [self.domain.index(var) for var in variables]\n        else:\n            indices = ...\n\n        matrix = np.vstack((\n            self._center[indices], self._dispersion[indices],\n            self._min[indices], self._max[indices], self._missing[indices],\n        )).T\n\n        # Return string labels for the returned matrix columns e.g. 'Mean',\n        # 'Dispersion' if requested\n        if return_labels:\n            labels = [self.Columns.CENTER.name, self.Columns.DISPERSION.name,\n                      self.Columns.MIN.name, self.Columns.MAX.name,\n                      self.Columns.MISSING.name]\n            return labels, matrix\n\n        return matrix\n\n    def __compute_stat(self, matrices, discrete_f=None, continuous_f=None,\n                       time_f=None, string_f=None, default_val=np.nan):\n        \"\"\"Apply functions to appropriate variable types. The default value is\n        returned if there is no function defined for specific variable types.\n        \"\"\"\n        if not matrices:\n            return np.array([])\n\n        def _to_float(data):\n            if not np.issubdtype(data.dtype, np.number):\n                data = data.astype(np.float64)\n            return data\n\n        def _to_object(data):\n            if data.dtype is not np.object:\n                data = data.astype(np.object)\n            return data\n\n        results = []\n        for variables, x in matrices:\n            result = np.full(len(variables), default_val)\n\n            # While the following caching and checks are messy, the indexing\n            # turns out to be a bottleneck for large datasets, so a single\n            # indexing operation improves performance\n            disc_idx, cont_idx, time_idx, str_idx = self._attr_indices(variables)\n            if discrete_f:\n                x_ = x[:, disc_idx]\n                if x_.size:\n                    result[disc_idx] = discrete_f(_to_float(x_))\n            if continuous_f:\n                x_ = x[:, cont_idx]\n                if x_.size:\n                    result[cont_idx] = continuous_f(_to_float(x_))\n            if time_f:\n                x_ = x[:, time_idx]\n                if x_.size:\n                    result[time_idx] = time_f(_to_float(x_))\n            if string_f:\n                x_ = x[:, str_idx]\n                if x_.size:\n                    result[str_idx] = string_f(_to_object(x_))\n\n            results.append(result)\n\n        return np.hstack(results)\n\n    def sortColumnData(self, column):\n        \"\"\"Prepare the arrays with which we will sort the rows. If we want to\n        sort based on a single value e.g. the name, return a 1d array.\n        Sometimes we may want to sort by multiple criteria, comparing\n        continuous variances with discrete entropies makes no sense, so we want\n        to group those variable types together.\n        \"\"\"\n        # Prepare indices for variable types so we can group them together\n        order = [DiscreteVariable, ContinuousVariable, TimeVariable, StringVariable]\n        mapping = {var: idx for idx, var in enumerate(order)}\n        vmapping = np.vectorize(mapping.__getitem__)\n        var_types_indices = vmapping(self._variable_types)\n\n        # Store the variable name sorted indices so we can pass a default\n        # order when sorting by multiple keys\n        var_name_indices = np.argsort(self._variable_names)\n\n        # Prepare vartype indices so ready when needed\n        disc_idx, _, time_idx, str_idx = self._attr_indices(self.variables)\n\n        # Sort by: (type)\n        if column == self.Columns.ICON:\n            return var_types_indices\n        # Sort by: (name)\n        elif column == self.Columns.NAME:\n            # We use `_variable_names` here and not the indices because the\n            # last (or single) row is actually sorted and we don't want to sort\n            # the indices\n            return self._variable_names\n        # Sort by: (None)\n        elif column == self.Columns.DISTRIBUTION:\n            return np.ones_like(var_types_indices)\n        # Sort by: (type, center)\n        elif column == self.Columns.CENTER:\n            # Sorting discrete or string values by mean makes no sense\n            vals = np.array(self._center)\n            vals[disc_idx] = var_name_indices[disc_idx]\n            vals[str_idx] = var_name_indices[str_idx]\n            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T\n        # Sort by: (type, dispersion)\n        elif column == self.Columns.DISPERSION:\n            # Sort time variables by their dispersion, which is not stored in\n            # the dispersion array\n            vals = np.array(self._dispersion)\n            vals[time_idx] = self._max[time_idx] - self._min[time_idx]\n            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T\n        # Sort by: (type, min)\n        elif column == self.Columns.MIN:\n            # Sorting discrete or string values by min makes no sense\n            vals = np.array(self._min)\n            vals[disc_idx] = var_name_indices[disc_idx]\n            vals[str_idx] = var_name_indices[str_idx]\n            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T\n        # Sort by: (type, max)\n        elif column == self.Columns.MAX:\n            # Sorting discrete or string values by min makes no sense\n            vals = np.array(self._max)\n            vals[disc_idx] = var_name_indices[disc_idx]\n            vals[str_idx] = var_name_indices[str_idx]\n            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T\n        # Sort by: (missing)\n        elif column == self.Columns.MISSING:\n            return self._missing\n\n        return None\n\n    def _sortColumnData(self, column):\n        \"\"\"Allow sorting with 2d arrays.\"\"\"\n        data = np.asarray(self.sortColumnData(column))\n        data = data[self.mapToSourceRows(Ellipsis)]\n\n        assert data.ndim <= 2, 'Data should be at most 2-dimensional'\n        return data\n\n    def _argsortData(self, data, order):\n        if data.ndim == 1:\n            indices = np.argsort(data, kind='mergesort')\n            if order == Qt.DescendingOrder:\n                indices = indices[::-1]\n            # Always sort NaNs last\n            if np.issubdtype(data.dtype, np.number):\n                indices = np.roll(indices, -np.isnan(data).sum())\n        else:\n            assert np.issubdtype(data.dtype, np.number), \\\n                'We do not deal with non numeric values in sorting by ' \\\n                'multiple values'\n            if order == Qt.DescendingOrder:\n                data[:, -1] = -data[:, -1]\n\n            # In order to make sure NaNs always appear at the end, insert a\n            # indicator whether NaN or not. Note that the data array must\n            # contain an empty column of zeros at index -2 since inserting an\n            # extra column after the fact can result in a MemoryError for data\n            # with a large amount of variables\n            assert np.all(data[:, -2] == 0), \\\n                'Add an empty column of zeros at index -2 to accomodate NaNs'\n            np.isnan(data[:, -1], out=data[:, -2])\n\n            indices = np.lexsort(np.flip(data.T, axis=0))\n\n        return indices\n\n    def headerData(self, section, orientation, role):\n        # type: (int, Qt.Orientation, Qt.ItemDataRole) -> Any\n        if orientation == Qt.Horizontal:\n            if role == Qt.DisplayRole:\n                return self.Columns.from_index(section).name\n\n        return None\n\n    def data(self, index, role):\n        # type: (QModelIndex, Qt.ItemDataRole) -> Any\n        def background():\n            if attribute in self.domain.attributes:\n                return self.COLOR_FOR_ROLE[self.ATTRIBUTE]\n            if attribute in self.domain.metas:\n                return self.COLOR_FOR_ROLE[self.META]\n            if attribute in self.domain.class_vars:\n                return self.COLOR_FOR_ROLE[self.CLASS_VAR]\n            return None\n\n        def text_alignment():\n            if column == self.Columns.NAME:\n                return Qt.AlignLeft | Qt.AlignVCenter\n            return Qt.AlignRight | Qt.AlignVCenter\n\n        def decoration():\n            if column == self.Columns.ICON:\n                return gui.attributeIconDict[attribute]\n            return None\n\n        def display():\n            # pylint: disable=too-many-branches\n            def render_value(value):\n                return \"\" if np.isnan(value) else attribute.str_val(value)\n\n            if column == self.Columns.NAME:\n                return attribute.name\n            elif column == self.Columns.DISTRIBUTION:\n                if isinstance(attribute,\n                              (DiscreteVariable, ContinuousVariable)):\n                    if row not in self.__distributions_cache:\n                        scene = QGraphicsScene(parent=self)\n                        histogram = Histogram(\n                            data=self.table,\n                            variable=attribute,\n                            color_attribute=self.target_var,\n                            border=(0, 0, 2, 0),\n                            border_color='#ccc',\n                        )\n                        scene.addItem(histogram)\n                        self.__distributions_cache[row] = scene\n                    return self.__distributions_cache[row]\n            elif column == self.Columns.CENTER:\n                return render_value(self._center[row])\n            elif column == self.Columns.DISPERSION:\n                if isinstance(attribute, TimeVariable):\n                    return format_time_diff(self._min[row], self._max[row])\n                elif isinstance(attribute, DiscreteVariable):\n                    return \"%.3g\" % self._dispersion[row]\n                else:\n                    return render_value(self._dispersion[row])\n            elif column == self.Columns.MIN:\n                if not isinstance(attribute, DiscreteVariable) \\\n                        or attribute.ordered:\n                    return render_value(self._min[row])\n            elif column == self.Columns.MAX:\n                if not isinstance(attribute, DiscreteVariable) \\\n                        or attribute.ordered:\n                    return render_value(self._max[row])\n            elif column == self.Columns.MISSING:\n                return '%d (%d%%)' % (\n                    self._missing[row],\n                    100 * self._missing[row] / self.n_instances\n                )\n            return None\n\n        roles = {Qt.BackgroundRole: background,\n                 Qt.TextAlignmentRole: text_alignment,\n                 Qt.DecorationRole: decoration,\n                 Qt.DisplayRole: display}\n\n        if not index.isValid() or role not in roles:\n            return None\n\n        row, column = self.mapToSourceRows(index.row()), index.column()\n        # Make sure we're not out of range\n        if not 0 <= row <= self.n_attributes:\n            return None\n\n        attribute = self.variables[row]\n        return roles[role]()\n\n    def rowCount(self, parent=QModelIndex()):\n        return 0 if parent.isValid() else self.n_attributes\n\n    def columnCount(self, parent=QModelIndex()):\n        return 0 if parent.isValid() else len(self.Columns)\n\n    def set_target_var(self, variable):\n        self.target_var = variable\n        self.__distributions_cache.clear()\n        start_idx = self.index(0, self.Columns.DISTRIBUTION)\n        end_idx = self.index(self.rowCount(), self.Columns.DISTRIBUTION)\n        self.dataChanged.emit(start_idx, end_idx)\n\n\nclass FeatureStatisticsTableView(QTableView):\n    HISTOGRAM_ASPECT_RATIO = (7, 3)\n    MINIMUM_HISTOGRAM_HEIGHT = 50\n    MAXIMUM_HISTOGRAM_HEIGHT = 80\n\n    def __init__(self, model, parent=None, **kwargs):\n        super().__init__(\n            parent=parent,\n            showGrid=False,\n            cornerButtonEnabled=False,\n            sortingEnabled=True,\n            selectionBehavior=QTableView.SelectRows,\n            selectionMode=QTableView.ExtendedSelection,\n            horizontalScrollMode=QTableView.ScrollPerPixel,\n            verticalScrollMode=QTableView.ScrollPerPixel,\n            **kwargs\n        )\n        self.setModel(model)\n\n        hheader = self.horizontalHeader()\n        hheader.setStretchLastSection(False)\n        # Contents precision specifies how many rows should be taken into\n        # account when computing the sizes, 0 being the visible rows. This is\n        # crucial, since otherwise the `ResizeToContents` section resize mode\n        # would call `sizeHint` on every single row in the data before first\n        # render. However this, this cannot be used here, since this only\n        # appears to work properly when the widget is actually shown. When the\n        # widget is not shown, size `sizeHint` is called on every row.\n        hheader.setResizeContentsPrecision(5)\n        # Set a nice default size so that headers have some space around titles\n        hheader.setDefaultSectionSize(100)\n        # Set individual column behaviour in `set_data` since the logical\n        # indices must be valid in the model, which requires data.\n        hheader.setSectionResizeMode(QHeaderView.Interactive)\n\n        columns = model.Columns\n        hheader.setSectionResizeMode(columns.ICON.index, QHeaderView.ResizeToContents)\n        hheader.setSectionResizeMode(columns.DISTRIBUTION.index, QHeaderView.Stretch)\n\n        vheader = self.verticalHeader()\n        vheader.setVisible(False)\n        vheader.setSectionResizeMode(QHeaderView.Fixed)\n        hheader.sectionResized.connect(self.bind_histogram_aspect_ratio)\n        # TODO: This shifts the scrollarea a bit down when opening widget\n        # hheader.sectionResized.connect(self.keep_row_centered)\n\n        self.setItemDelegate(NoFocusRectDelegate(parent=self))\n        self.setItemDelegateForColumn(\n            FeatureStatisticsTableModel.Columns.DISTRIBUTION,\n            DistributionDelegate(parent=self),\n        )\n\n    def bind_histogram_aspect_ratio(self, logical_index, _, new_size):\n        \"\"\"Force the horizontal and vertical header to maintain the defined\n        aspect ratio specified for the histogram.\"\"\"\n        # Prevent function being exectued more than once per resize\n        if logical_index is not self.model().Columns.DISTRIBUTION.index:\n            return\n        ratio_width, ratio_height = self.HISTOGRAM_ASPECT_RATIO\n        unit_width = new_size / ratio_width\n        new_height = unit_width * ratio_height\n        effective_height = max(new_height, self.MINIMUM_HISTOGRAM_HEIGHT)\n        effective_height = min(effective_height, self.MAXIMUM_HISTOGRAM_HEIGHT)\n        self.verticalHeader().setDefaultSectionSize(effective_height)\n\n    def keep_row_centered(self, logical_index, _1, _2):\n        \"\"\"When resizing the widget when scrolled further down, the\n        positions of rows changes. Obviously, the user resized in order to\n        better see the row of interest. This keeps that row centered.\"\"\"\n        # TODO: This does not work properly\n        # Prevent function being exectued more than once per resize\n        if logical_index is not self.model().Columns.DISTRIBUTION.index:\n            return\n        top_row = self.indexAt(self.rect().topLeft()).row()\n        bottom_row = self.indexAt(self.rect().bottomLeft()).row()\n        middle_row = top_row + (bottom_row - top_row) // 2\n        self.scrollTo(self.model().index(middle_row, 0), QTableView.PositionAtCenter)\n\n\nclass NoFocusRectDelegate(QStyledItemDelegate):\n    \"\"\"Removes the light blue background and border on a focused item.\"\"\"\n\n    def paint(self, painter, option, index):\n        # type: (QPainter, QStyleOptionViewItem, QModelIndex) -> None\n        option.state &= ~QStyle.State_HasFocus\n        super().paint(painter, option, index)\n\n\nclass DistributionDelegate(QStyledItemDelegate):\n    def paint(self, painter, option, index):\n        # type: (QPainter, QStyleOptionViewItem, QModelIndex) -> None\n        scene = index.data(Qt.DisplayRole)  # type: Optional[QGraphicsScene]\n        if scene is None:\n            return super().paint(painter, option, index)\n\n        painter.setRenderHint(QPainter.Antialiasing)\n\n        if option.state & QStyle.State_Selected:\n            background_color = option.palette.highlight()\n        else:\n            background_color = index.data(Qt.BackgroundRole)\n        if background_color is not None:\n            scene.setBackgroundBrush(background_color)\n\n        scene.render(painter, target=QRectF(option.rect), mode=Qt.IgnoreAspectRatio)\n\n        # pylint complains about inconsistent return statements\n        return None\n\n\nclass OWFeatureStatistics(widget.OWWidget):\n    name = 'Feature Statistics'\n    description = 'Show basic statistics for data features.'\n    icon = 'icons/FeatureStatistics.svg'\n\n    class Inputs:\n        data = Input('Data', Table, default=True)\n\n    class Outputs:\n        reduced_data = Output('Reduced Data', Table, default=True)\n        statistics = Output('Statistics', Table)\n\n    want_main_area = True\n    buttons_area_orientation = Qt.Vertical\n\n    settingsHandler = DomainContextHandler()\n    settings_version = 2\n\n    auto_commit = Setting(True)\n    color_var = ContextSetting(None)  # type: Optional[Variable]\n    # filter_string = ContextSetting('')\n\n    sorting = Setting((0, Qt.DescendingOrder))\n    selected_vars = ContextSetting([], schema_only=True)\n\n    def __init__(self):\n        super().__init__()\n\n        self.data = None  # type: Optional[Table]\n\n        # TODO: Implement filtering on the model\n        # filter_box = gui.vBox(self.controlArea, 'Filter')\n        # self.filter_text = gui.lineEdit(\n        #     filter_box, self, value='filter_string',\n        #     placeholderText='Filter variables by name',\n        #     callback=self._filter_table_variables, callbackOnType=True,\n        # )\n        # shortcut = QShortcut(QKeySequence('Ctrl+f'), self, self.filter_text.setFocus)\n        # shortcut.setWhatsThis('Filter variables by name')\n\n        self.color_var_model = DomainModel(\n            valid_types=(ContinuousVariable, DiscreteVariable),\n            placeholder='None',\n        )\n        box = gui.vBox(self.controlArea, 'Histogram')\n        self.cb_color_var = gui.comboBox(\n            box, master=self, value='color_var', model=self.color_var_model,\n            label='Color:', orientation=Qt.Horizontal, contentsLength=13,\n            searchable=True\n        )\n        self.cb_color_var.activated.connect(self.__color_var_changed)\n\n        gui.rubber(self.controlArea)\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        self.info.set_input_summary(self.info.NoInput)\n        self.info.set_output_summary(self.info.NoOutput)\n\n        # Main area\n        self.model = FeatureStatisticsTableModel(parent=self)\n        self.table_view = FeatureStatisticsTableView(self.model, parent=self)\n        self.table_view.selectionModel().selectionChanged.connect(self.on_select)\n        self.table_view.horizontalHeader().sectionClicked.connect(self.on_header_click)\n\n        self.mainArea.layout().addWidget(self.table_view)\n\n    @staticmethod\n    def sizeHint():\n        return QSize(1050, 500)\n\n    def _filter_table_variables(self):\n        regex = QRegExp(self.filter_string)\n        # If the user explicitly types different cases, we assume they know\n        # what they are searching for and account for letter case in filter\n        different_case = (\n            any(c.islower() for c in self.filter_string) and\n            any(c.isupper() for c in self.filter_string)\n        )\n        if not different_case:\n            regex.setCaseSensitivity(Qt.CaseInsensitive)\n\n    @Inputs.data\n    def set_data(self, data):\n        # Clear outputs and reset widget state\n        self.closeContext()\n        self.selected_vars = []\n        self.model.resetSorting()\n        self.Outputs.reduced_data.send(None)\n        self.Outputs.statistics.send(None)\n\n        # Setup widget state for new data and restore settings\n        self.data = data\n\n        if data is not None:\n            self.info.set_input_summary(len(data),\n                                        format_summary_details(data))\n            self.color_var_model.set_domain(data.domain)\n            self.color_var = None\n            if self.data.domain.class_vars:\n                self.color_var = self.data.domain.class_vars[0]\n        else:\n            self.info.set_input_summary(self.info.NoInput)\n            self.color_var_model.set_domain(None)\n            self.color_var = None\n        self.model.set_data(data)\n\n        self.openContext(self.data)\n        self.__restore_selection()\n        self.__restore_sorting()\n        # self._filter_table_variables()\n        self.__color_var_changed()\n\n        self.commit()\n\n    def __restore_selection(self):\n        \"\"\"Restore the selection on the table view from saved settings.\"\"\"\n        selection_model = self.table_view.selectionModel()\n        selection = QItemSelection()\n        if self.selected_vars:\n            var_indices = {var: i for i, var in enumerate(self.model.variables)}\n            selected_indices = [var_indices[var] for var in self.selected_vars]\n            for row in self.model.mapFromSourceRows(selected_indices):\n                selection.append(QItemSelectionRange(\n                    self.model.index(row, 0),\n                    self.model.index(row, self.model.columnCount() - 1)\n                ))\n        selection_model.select(selection, QItemSelectionModel.ClearAndSelect)\n\n    def __restore_sorting(self):\n        \"\"\"Restore the sort column and order from saved settings.\"\"\"\n        sort_column, sort_order = self.sorting\n        if self.model.n_attributes and sort_column < self.model.columnCount():\n            self.model.sort(sort_column, sort_order)\n            self.table_view.horizontalHeader().setSortIndicator(sort_column, sort_order)\n\n    @pyqtSlot(int)\n    def on_header_click(self, *_):\n        # Store the header states\n        sort_order = self.model.sortOrder()\n        sort_column = self.model.sortColumn()\n        self.sorting = sort_column, sort_order\n\n    @pyqtSlot(int)\n    def __color_var_changed(self, *_):\n        if self.model is not None:\n            self.model.set_target_var(self.color_var)\n\n    def on_select(self):\n        selection_indices = list(self.model.mapToSourceRows([\n            i.row() for i in self.table_view.selectionModel().selectedRows()\n        ]))\n        self.selected_vars = list(self.model.variables[selection_indices])\n        self.commit()\n\n    def commit(self):\n        if not self.selected_vars:\n            self.info.set_output_summary(self.info.NoOutput)\n            self.Outputs.reduced_data.send(None)\n            self.Outputs.statistics.send(None)\n            return\n\n        # Send a table with only selected columns to output\n        variables = self.selected_vars\n        self.info.set_output_summary(len(self.data[:, variables]),\n                                     format_summary_details(self.data[:, variables]))\n        self.Outputs.reduced_data.send(self.data[:, variables])\n\n        # Send the statistics of the selected variables to ouput\n        labels, data = self.model.get_statistics_matrix(variables, return_labels=True)\n        var_names = np.atleast_2d([var.name for var in variables]).T\n        domain = Domain(\n            attributes=[ContinuousVariable(name) for name in labels],\n            metas=[StringVariable('Feature')]\n        )\n        statistics = Table(domain, data, metas=var_names)\n        statistics.name = '%s (Feature Statistics)' % self.data.name\n        self.Outputs.statistics.send(statistics)\n\n    def send_report(self):\n        view = self.table_view\n        self.report_table(view)\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        if not version or version < 2:\n            selected_rows = context.values.pop(\"selected_rows\", None)\n            if not selected_rows:\n                selected_vars = []\n            else:\n                # This assumes that dict was saved by Python >= 3.6 so dict is\n                # ordered; if not, context hasn't had worked anyway.\n                all_vars = [\n                    (var, tpe)\n                    for (var, tpe) in chain(context.attributes.items(),\n                                            context.metas.items())\n                    # it would be nicer to use cls.HIDDEN_VAR_TYPES, but there\n                    # is no suitable conversion function, and StringVariable (3)\n                    # was the only hidden var when settings_version < 2, so:\n                    if tpe != 3]\n                selected_vars = [all_vars[i] for i in selected_rows]\n            context.values[\"selected_vars\"] = selected_vars, -3\n\n\nif __name__ == '__main__':  # pragma: no cover\n    WidgetPreview(OWFeatureStatistics).run(Table(\"iris\"))\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/data/owfeaturestatistics.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "7d1a55bf2",
                "new_commit_hash": "72692dbd8",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/data/owfeaturestatistics.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/data/owfeaturestatistics.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 850,
                            "length": 7
                        },
                        "new_range": {
                            "start": 850,
                            "length": 8
                        },
                        "section": "class OWFeatureStatistics(widget.OWWidget):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        self.Outputs.statistics.send(statistics)",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    def send_report(self):",
                                "type": "context"
                            },
                            {
                                "content": "        pass",
                                "type": "deleted"
                            },
                            {
                                "content": "        view = self.table_view",
                                "type": "added"
                            },
                            {
                                "content": "        self.report_table(view)",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    @classmethod",
                                "type": "context"
                            },
                            {
                                "content": "    def migrate_context(cls, context, version):",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/data/owfeaturestatistics.py",
                            "type": "class",
                            "name": "OWFeatureStatistics",
                            "content": "class OWFeatureStatistics(widget.OWWidget):\n    name = 'Feature Statistics'\n    description = 'Show basic statistics for data features.'\n    icon = 'icons/FeatureStatistics.svg'\n\n    class Inputs:\n        data = Input('Data', Table, default=True)\n\n    class Outputs:\n        reduced_data = Output('Reduced Data', Table, default=True)\n        statistics = Output('Statistics', Table)\n\n    want_main_area = True\n    buttons_area_orientation = Qt.Vertical\n\n    settingsHandler = DomainContextHandler()\n    settings_version = 2\n\n    auto_commit = Setting(True)\n    color_var = ContextSetting(None)  # type: Optional[Variable]\n    # filter_string = ContextSetting('')\n\n    sorting = Setting((0, Qt.DescendingOrder))\n    selected_vars = ContextSetting([], schema_only=True)\n\n    def __init__(self):\n        super().__init__()\n\n        self.data = None  # type: Optional[Table]\n\n        # TODO: Implement filtering on the model\n        # filter_box = gui.vBox(self.controlArea, 'Filter')\n        # self.filter_text = gui.lineEdit(\n        #     filter_box, self, value='filter_string',\n        #     placeholderText='Filter variables by name',\n        #     callback=self._filter_table_variables, callbackOnType=True,\n        # )\n        # shortcut = QShortcut(QKeySequence('Ctrl+f'), self, self.filter_text.setFocus)\n        # shortcut.setWhatsThis('Filter variables by name')\n\n        self.color_var_model = DomainModel(\n            valid_types=(ContinuousVariable, DiscreteVariable),\n            placeholder='None',\n        )\n        box = gui.vBox(self.controlArea, 'Histogram')\n        self.cb_color_var = gui.comboBox(\n            box, master=self, value='color_var', model=self.color_var_model,\n            label='Color:', orientation=Qt.Horizontal, contentsLength=13,\n            searchable=True\n        )\n        self.cb_color_var.activated.connect(self.__color_var_changed)\n\n        gui.rubber(self.controlArea)\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        self.info.set_input_summary(self.info.NoInput)\n        self.info.set_output_summary(self.info.NoOutput)\n\n        # Main area\n        self.model = FeatureStatisticsTableModel(parent=self)\n        self.table_view = FeatureStatisticsTableView(self.model, parent=self)\n        self.table_view.selectionModel().selectionChanged.connect(self.on_select)\n        self.table_view.horizontalHeader().sectionClicked.connect(self.on_header_click)\n\n        self.mainArea.layout().addWidget(self.table_view)\n\n    @staticmethod\n    def sizeHint():\n        return QSize(1050, 500)\n\n    def _filter_table_variables(self):\n        regex = QRegExp(self.filter_string)\n        # If the user explicitly types different cases, we assume they know\n        # what they are searching for and account for letter case in filter\n        different_case = (\n            any(c.islower() for c in self.filter_string) and\n            any(c.isupper() for c in self.filter_string)\n        )\n        if not different_case:\n            regex.setCaseSensitivity(Qt.CaseInsensitive)\n\n    @Inputs.data\n    def set_data(self, data):\n        # Clear outputs and reset widget state\n        self.closeContext()\n        self.selected_vars = []\n        self.model.resetSorting()\n        self.Outputs.reduced_data.send(None)\n        self.Outputs.statistics.send(None)\n\n        # Setup widget state for new data and restore settings\n        self.data = data\n\n        if data is not None:\n            self.info.set_input_summary(len(data),\n                                        format_summary_details(data))\n            self.color_var_model.set_domain(data.domain)\n            self.color_var = None\n            if self.data.domain.class_vars:\n                self.color_var = self.data.domain.class_vars[0]\n        else:\n            self.info.set_input_summary(self.info.NoInput)\n            self.color_var_model.set_domain(None)\n            self.color_var = None\n        self.model.set_data(data)\n\n        self.openContext(self.data)\n        self.__restore_selection()\n        self.__restore_sorting()\n        # self._filter_table_variables()\n        self.__color_var_changed()\n\n        self.commit()\n\n    def __restore_selection(self):\n        \"\"\"Restore the selection on the table view from saved settings.\"\"\"\n        selection_model = self.table_view.selectionModel()\n        selection = QItemSelection()\n        if self.selected_vars:\n            var_indices = {var: i for i, var in enumerate(self.model.variables)}\n            selected_indices = [var_indices[var] for var in self.selected_vars]\n            for row in self.model.mapFromSourceRows(selected_indices):\n                selection.append(QItemSelectionRange(\n                    self.model.index(row, 0),\n                    self.model.index(row, self.model.columnCount() - 1)\n                ))\n        selection_model.select(selection, QItemSelectionModel.ClearAndSelect)\n\n    def __restore_sorting(self):\n        \"\"\"Restore the sort column and order from saved settings.\"\"\"\n        sort_column, sort_order = self.sorting\n        if self.model.n_attributes and sort_column < self.model.columnCount():\n            self.model.sort(sort_column, sort_order)\n            self.table_view.horizontalHeader().setSortIndicator(sort_column, sort_order)\n\n    @pyqtSlot(int)\n    def on_header_click(self, *_):\n        # Store the header states\n        sort_order = self.model.sortOrder()\n        sort_column = self.model.sortColumn()\n        self.sorting = sort_column, sort_order\n\n    @pyqtSlot(int)\n    def __color_var_changed(self, *_):\n        if self.model is not None:\n            self.model.set_target_var(self.color_var)\n\n    def on_select(self):\n        selection_indices = list(self.model.mapToSourceRows([\n            i.row() for i in self.table_view.selectionModel().selectedRows()\n        ]))\n        self.selected_vars = list(self.model.variables[selection_indices])\n        self.commit()\n\n    def commit(self):\n        if not self.selected_vars:\n            self.info.set_output_summary(self.info.NoOutput)\n            self.Outputs.reduced_data.send(None)\n            self.Outputs.statistics.send(None)\n            return\n\n        # Send a table with only selected columns to output\n        variables = self.selected_vars\n        self.info.set_output_summary(len(self.data[:, variables]),\n                                     format_summary_details(self.data[:, variables]))\n        self.Outputs.reduced_data.send(self.data[:, variables])\n\n        # Send the statistics of the selected variables to ouput\n        labels, data = self.model.get_statistics_matrix(variables, return_labels=True)\n        var_names = np.atleast_2d([var.name for var in variables]).T\n        domain = Domain(\n            attributes=[ContinuousVariable(name) for name in labels],\n            metas=[StringVariable('Feature')]\n        )\n        statistics = Table(domain, data, metas=var_names)\n        statistics.name = '%s (Feature Statistics)' % self.data.name\n        self.Outputs.statistics.send(statistics)\n\n    def send_report(self):\n        pass\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        if not version or version < 2:\n            selected_rows = context.values.pop(\"selected_rows\", None)\n            if not selected_rows:\n                selected_vars = []\n            else:\n                # This assumes that dict was saved by Python >= 3.6 so dict is\n                # ordered; if not, context hasn't had worked anyway.\n                all_vars = [\n                    (var, tpe)\n                    for (var, tpe) in chain(context.attributes.items(),\n                                            context.metas.items())\n                    # it would be nicer to use cls.HIDDEN_VAR_TYPES, but there\n                    # is no suitable conversion function, and StringVariable (3)\n                    # was the only hidden var when settings_version < 2, so:\n                    if tpe != 3]\n                selected_vars = [all_vars[i] for i in selected_rows]\n            context.values[\"selected_vars\"] = selected_vars, -3",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 674,
                            "end_lineno": 873,
                            "parent": null
                        },
                        {
                            "file_name": "Orange/widgets/data/owfeaturestatistics.py",
                            "type": "method",
                            "name": "OWFeatureStatistics.send_report",
                            "content": "class OWFeatureStatistics(widget.OWWidget):\n    name = 'Feature Statistics'\n    description = 'Show basic statistics for data features.'\n    icon = 'icons/FeatureStatistics.svg'\n\n    class Inputs:\n        data = Input('Data', Table, default=True)\n\n    class Outputs:\n        reduced_data = Output('Reduced Data', Table, default=True)\n        statistics = Output('Statistics', Table)\n\n    want_main_area = True\n    buttons_area_orientation = Qt.Vertical\n\n    settingsHandler = DomainContextHandler()\n    settings_version = 2\n\n    auto_commit = Setting(True)\n    color_var = ContextSetting(None)  # type: Optional[Variable]\n    # filter_string = ContextSetting('')\n\n    sorting = Setting((0, Qt.DescendingOrder))\n    selected_vars = ContextSetting([], schema_only=True)\n\n    def __init__(self):\n        super().__init__()\n\n        self.data = None  # type: Optional[Table]\n\n        # TODO: Implement filtering on the model\n        # filter_box = gui.vBox(self.controlArea, 'Filter')\n        # self.filter_text = gui.lineEdit(\n        #     filter_box, self, value='filter_string',\n        #     placeholderText='Filter variables by name',\n        #     callback=self._filter_table_variables, callbackOnType=True,\n        # )\n        # shortcut = QShortcut(QKeySequence('Ctrl+f'), self, self.filter_text.setFocus)\n        # shortcut.setWhatsThis('Filter variables by name')\n\n        self.color_var_model = DomainModel(\n            valid_types=(ContinuousVariable, DiscreteVariable),\n            placeholder='None',\n        )\n        box = gui.vBox(self.controlArea, 'Histogram')\n        self.cb_color_var = gui.comboBox(\n            box, master=self, value='color_var', model=self.color_var_model,\n            label='Color:', orientation=Qt.Horizontal, contentsLength=13,\n            searchable=True\n        )\n        self.cb_color_var.activated.connect(self.__color_var_changed)\n\n        gui.rubber(self.controlArea)\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        self.info.set_input_summary(self.info.NoInput)\n        self.info.set_output_summary(self.info.NoOutput)\n\n        # Main area\n        self.model = FeatureStatisticsTableModel(parent=self)\n        self.table_view = FeatureStatisticsTableView(self.model, parent=self)\n        self.table_view.selectionModel().selectionChanged.connect(self.on_select)\n        self.table_view.horizontalHeader().sectionClicked.connect(self.on_header_click)\n\n        self.mainArea.layout().addWidget(self.table_view)\n\n    @staticmethod\n    def sizeHint():\n        return QSize(1050, 500)\n\n    def _filter_table_variables(self):\n        regex = QRegExp(self.filter_string)\n        # If the user explicitly types different cases, we assume they know\n        # what they are searching for and account for letter case in filter\n        different_case = (\n            any(c.islower() for c in self.filter_string) and\n            any(c.isupper() for c in self.filter_string)\n        )\n        if not different_case:\n            regex.setCaseSensitivity(Qt.CaseInsensitive)\n\n    @Inputs.data\n    def set_data(self, data):\n        # Clear outputs and reset widget state\n        self.closeContext()\n        self.selected_vars = []\n        self.model.resetSorting()\n        self.Outputs.reduced_data.send(None)\n        self.Outputs.statistics.send(None)\n\n        # Setup widget state for new data and restore settings\n        self.data = data\n\n        if data is not None:\n            self.info.set_input_summary(len(data),\n                                        format_summary_details(data))\n            self.color_var_model.set_domain(data.domain)\n            self.color_var = None\n            if self.data.domain.class_vars:\n                self.color_var = self.data.domain.class_vars[0]\n        else:\n            self.info.set_input_summary(self.info.NoInput)\n            self.color_var_model.set_domain(None)\n            self.color_var = None\n        self.model.set_data(data)\n\n        self.openContext(self.data)\n        self.__restore_selection()\n        self.__restore_sorting()\n        # self._filter_table_variables()\n        self.__color_var_changed()\n\n        self.commit()\n\n    def __restore_selection(self):\n        \"\"\"Restore the selection on the table view from saved settings.\"\"\"\n        selection_model = self.table_view.selectionModel()\n        selection = QItemSelection()\n        if self.selected_vars:\n            var_indices = {var: i for i, var in enumerate(self.model.variables)}\n            selected_indices = [var_indices[var] for var in self.selected_vars]\n            for row in self.model.mapFromSourceRows(selected_indices):\n                selection.append(QItemSelectionRange(\n                    self.model.index(row, 0),\n                    self.model.index(row, self.model.columnCount() - 1)\n                ))\n        selection_model.select(selection, QItemSelectionModel.ClearAndSelect)\n\n    def __restore_sorting(self):\n        \"\"\"Restore the sort column and order from saved settings.\"\"\"\n        sort_column, sort_order = self.sorting\n        if self.model.n_attributes and sort_column < self.model.columnCount():\n            self.model.sort(sort_column, sort_order)\n            self.table_view.horizontalHeader().setSortIndicator(sort_column, sort_order)\n\n    @pyqtSlot(int)\n    def on_header_click(self, *_):\n        # Store the header states\n        sort_order = self.model.sortOrder()\n        sort_column = self.model.sortColumn()\n        self.sorting = sort_column, sort_order\n\n    @pyqtSlot(int)\n    def __color_var_changed(self, *_):\n        if self.model is not None:\n            self.model.set_target_var(self.color_var)\n\n    def on_select(self):\n        selection_indices = list(self.model.mapToSourceRows([\n            i.row() for i in self.table_view.selectionModel().selectedRows()\n        ]))\n        self.selected_vars = list(self.model.variables[selection_indices])\n        self.commit()\n\n    def commit(self):\n        if not self.selected_vars:\n            self.info.set_output_summary(self.info.NoOutput)\n            self.Outputs.reduced_data.send(None)\n            self.Outputs.statistics.send(None)\n            return\n\n        # Send a table with only selected columns to output\n        variables = self.selected_vars\n        self.info.set_output_summary(len(self.data[:, variables]),\n                                     format_summary_details(self.data[:, variables]))\n        self.Outputs.reduced_data.send(self.data[:, variables])\n\n        # Send the statistics of the selected variables to ouput\n        labels, data = self.model.get_statistics_matrix(variables, return_labels=True)\n        var_names = np.atleast_2d([var.name for var in variables]).T\n        domain = Domain(\n            attributes=[ContinuousVariable(name) for name in labels],\n            metas=[StringVariable('Feature')]\n        )\n        statistics = Table(domain, data, metas=var_names)\n        statistics.name = '%s (Feature Statistics)' % self.data.name\n        self.Outputs.statistics.send(statistics)\n\n    def send_report(self):\n        pass\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        if not version or version < 2:\n            selected_rows = context.values.pop(\"selected_rows\", None)\n            if not selected_rows:\n                selected_vars = []\n            else:\n                # This assumes that dict was saved by Python >= 3.6 so dict is\n                # ordered; if not, context hasn't had worked anyway.\n                all_vars = [\n                    (var, tpe)\n                    for (var, tpe) in chain(context.attributes.items(),\n                                            context.metas.items())\n                    # it would be nicer to use cls.HIDDEN_VAR_TYPES, but there\n                    # is no suitable conversion function, and StringVariable (3)\n                    # was the only hidden var when settings_version < 2, so:\n                    if tpe != 3]\n                selected_vars = [all_vars[i] for i in selected_rows]\n            context.values[\"selected_vars\"] = selected_vars, -3",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 852,
                            "end_lineno": 853,
                            "parent": {
                                "file_name": "Orange/widgets/data/owfeaturestatistics.py",
                                "type": "class",
                                "name": "OWFeatureStatistics",
                                "content": "class OWFeatureStatistics(widget.OWWidget):\n    name = 'Feature Statistics'\n    description = 'Show basic statistics for data features.'\n    icon = 'icons/FeatureStatistics.svg'\n\n    class Inputs:\n        data = Input('Data', Table, default=True)\n\n    class Outputs:\n        reduced_data = Output('Reduced Data', Table, default=True)\n        statistics = Output('Statistics', Table)\n\n    want_main_area = True\n    buttons_area_orientation = Qt.Vertical\n\n    settingsHandler = DomainContextHandler()\n    settings_version = 2\n\n    auto_commit = Setting(True)\n    color_var = ContextSetting(None)  # type: Optional[Variable]\n    # filter_string = ContextSetting('')\n\n    sorting = Setting((0, Qt.DescendingOrder))\n    selected_vars = ContextSetting([], schema_only=True)\n\n    def __init__(self):\n        super().__init__()\n\n        self.data = None  # type: Optional[Table]\n\n        # TODO: Implement filtering on the model\n        # filter_box = gui.vBox(self.controlArea, 'Filter')\n        # self.filter_text = gui.lineEdit(\n        #     filter_box, self, value='filter_string',\n        #     placeholderText='Filter variables by name',\n        #     callback=self._filter_table_variables, callbackOnType=True,\n        # )\n        # shortcut = QShortcut(QKeySequence('Ctrl+f'), self, self.filter_text.setFocus)\n        # shortcut.setWhatsThis('Filter variables by name')\n\n        self.color_var_model = DomainModel(\n            valid_types=(ContinuousVariable, DiscreteVariable),\n            placeholder='None',\n        )\n        box = gui.vBox(self.controlArea, 'Histogram')\n        self.cb_color_var = gui.comboBox(\n            box, master=self, value='color_var', model=self.color_var_model,\n            label='Color:', orientation=Qt.Horizontal, contentsLength=13,\n            searchable=True\n        )\n        self.cb_color_var.activated.connect(self.__color_var_changed)\n\n        gui.rubber(self.controlArea)\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        self.info.set_input_summary(self.info.NoInput)\n        self.info.set_output_summary(self.info.NoOutput)\n\n        # Main area\n        self.model = FeatureStatisticsTableModel(parent=self)\n        self.table_view = FeatureStatisticsTableView(self.model, parent=self)\n        self.table_view.selectionModel().selectionChanged.connect(self.on_select)\n        self.table_view.horizontalHeader().sectionClicked.connect(self.on_header_click)\n\n        self.mainArea.layout().addWidget(self.table_view)\n\n    @staticmethod\n    def sizeHint():\n        return QSize(1050, 500)\n\n    def _filter_table_variables(self):\n        regex = QRegExp(self.filter_string)\n        # If the user explicitly types different cases, we assume they know\n        # what they are searching for and account for letter case in filter\n        different_case = (\n            any(c.islower() for c in self.filter_string) and\n            any(c.isupper() for c in self.filter_string)\n        )\n        if not different_case:\n            regex.setCaseSensitivity(Qt.CaseInsensitive)\n\n    @Inputs.data\n    def set_data(self, data):\n        # Clear outputs and reset widget state\n        self.closeContext()\n        self.selected_vars = []\n        self.model.resetSorting()\n        self.Outputs.reduced_data.send(None)\n        self.Outputs.statistics.send(None)\n\n        # Setup widget state for new data and restore settings\n        self.data = data\n\n        if data is not None:\n            self.info.set_input_summary(len(data),\n                                        format_summary_details(data))\n            self.color_var_model.set_domain(data.domain)\n            self.color_var = None\n            if self.data.domain.class_vars:\n                self.color_var = self.data.domain.class_vars[0]\n        else:\n            self.info.set_input_summary(self.info.NoInput)\n            self.color_var_model.set_domain(None)\n            self.color_var = None\n        self.model.set_data(data)\n\n        self.openContext(self.data)\n        self.__restore_selection()\n        self.__restore_sorting()\n        # self._filter_table_variables()\n        self.__color_var_changed()\n\n        self.commit()\n\n    def __restore_selection(self):\n        \"\"\"Restore the selection on the table view from saved settings.\"\"\"\n        selection_model = self.table_view.selectionModel()\n        selection = QItemSelection()\n        if self.selected_vars:\n            var_indices = {var: i for i, var in enumerate(self.model.variables)}\n            selected_indices = [var_indices[var] for var in self.selected_vars]\n            for row in self.model.mapFromSourceRows(selected_indices):\n                selection.append(QItemSelectionRange(\n                    self.model.index(row, 0),\n                    self.model.index(row, self.model.columnCount() - 1)\n                ))\n        selection_model.select(selection, QItemSelectionModel.ClearAndSelect)\n\n    def __restore_sorting(self):\n        \"\"\"Restore the sort column and order from saved settings.\"\"\"\n        sort_column, sort_order = self.sorting\n        if self.model.n_attributes and sort_column < self.model.columnCount():\n            self.model.sort(sort_column, sort_order)\n            self.table_view.horizontalHeader().setSortIndicator(sort_column, sort_order)\n\n    @pyqtSlot(int)\n    def on_header_click(self, *_):\n        # Store the header states\n        sort_order = self.model.sortOrder()\n        sort_column = self.model.sortColumn()\n        self.sorting = sort_column, sort_order\n\n    @pyqtSlot(int)\n    def __color_var_changed(self, *_):\n        if self.model is not None:\n            self.model.set_target_var(self.color_var)\n\n    def on_select(self):\n        selection_indices = list(self.model.mapToSourceRows([\n            i.row() for i in self.table_view.selectionModel().selectedRows()\n        ]))\n        self.selected_vars = list(self.model.variables[selection_indices])\n        self.commit()\n\n    def commit(self):\n        if not self.selected_vars:\n            self.info.set_output_summary(self.info.NoOutput)\n            self.Outputs.reduced_data.send(None)\n            self.Outputs.statistics.send(None)\n            return\n\n        # Send a table with only selected columns to output\n        variables = self.selected_vars\n        self.info.set_output_summary(len(self.data[:, variables]),\n                                     format_summary_details(self.data[:, variables]))\n        self.Outputs.reduced_data.send(self.data[:, variables])\n\n        # Send the statistics of the selected variables to ouput\n        labels, data = self.model.get_statistics_matrix(variables, return_labels=True)\n        var_names = np.atleast_2d([var.name for var in variables]).T\n        domain = Domain(\n            attributes=[ContinuousVariable(name) for name in labels],\n            metas=[StringVariable('Feature')]\n        )\n        statistics = Table(domain, data, metas=var_names)\n        statistics.name = '%s (Feature Statistics)' % self.data.name\n        self.Outputs.statistics.send(statistics)\n\n    def send_report(self):\n        pass\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        if not version or version < 2:\n            selected_rows = context.values.pop(\"selected_rows\", None)\n            if not selected_rows:\n                selected_vars = []\n            else:\n                # This assumes that dict was saved by Python >= 3.6 so dict is\n                # ordered; if not, context hasn't had worked anyway.\n                all_vars = [\n                    (var, tpe)\n                    for (var, tpe) in chain(context.attributes.items(),\n                                            context.metas.items())\n                    # it would be nicer to use cls.HIDDEN_VAR_TYPES, but there\n                    # is no suitable conversion function, and StringVariable (3)\n                    # was the only hidden var when settings_version < 2, so:\n                    if tpe != 3]\n                selected_vars = [all_vars[i] for i in selected_rows]\n            context.values[\"selected_vars\"] = selected_vars, -3",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 674,
                                "end_lineno": 873,
                                "parent": null
                            }
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        },
        {
            "old_file_content": "#pylint: disable=unsubscriptable-object\nimport datetime\nimport unittest\nfrom collections import namedtuple\nfrom functools import partial\nfrom itertools import chain\nfrom typing import List\nfrom unittest.mock import Mock\n\nimport numpy as np\nfrom AnyQt.QtCore import QItemSelection, QItemSelectionRange, \\\n    QItemSelectionModel, Qt\n\nfrom Orange.data import Table, Domain, StringVariable, ContinuousVariable, \\\n    DiscreteVariable, TimeVariable\nfrom Orange.widgets.tests.base import WidgetTest, datasets\nfrom Orange.widgets.tests.utils import simulate, table_dense_sparse\nfrom Orange.widgets.data.owfeaturestatistics import \\\n    OWFeatureStatistics\nfrom Orange.widgets.utils.state_summary import format_summary_details\nfrom orangewidget.settings import Context\n\nVarDataPair = namedtuple('VarDataPair', ['variable', 'data'])\n\n# Continuous variable variations\ncontinuous_full = VarDataPair(\n    ContinuousVariable('continuous_full'),\n    np.array([0, 1, 2, 3, 4], dtype=float),\n)\ncontinuous_missing = VarDataPair(\n    ContinuousVariable('continuous_missing'),\n    np.array([0, 1, 2, np.nan, 4], dtype=float),\n)\ncontinuous_all_missing = VarDataPair(\n    ContinuousVariable('continuous_all_missing'),\n    np.array([np.nan] * 5, dtype=float),\n)\ncontinuous_same = VarDataPair(\n    ContinuousVariable('continuous_same'),\n    np.array([3] * 5, dtype=float),\n)\ncontinuous = [\n    continuous_full, continuous_missing, continuous_all_missing,\n    continuous_same\n]\n\n# Unordered discrete variable variations\nrgb_full = VarDataPair(\n    DiscreteVariable('rgb_full', values=('r', 'g', 'b')),\n    np.array([0, 1, 1, 1, 2], dtype=float),\n)\nrgb_missing = VarDataPair(\n    DiscreteVariable('rgb_missing', values=('r', 'g', 'b')),\n    np.array([0, 1, 1, np.nan, 2], dtype=float),\n)\nrgb_all_missing = VarDataPair(\n    DiscreteVariable('rgb_all_missing', values=('r', 'g', 'b')),\n    np.array([np.nan] * 5, dtype=float),\n)\nrgb_bins_missing = VarDataPair(\n    DiscreteVariable('rgb_bins_missing', values=('r', 'g', 'b')),\n    np.array([np.nan, 1, 1, 1, np.nan], dtype=float),\n)\nrgb_same = VarDataPair(\n    DiscreteVariable('rgb_same', values=('r', 'g', 'b')),\n    np.array([2] * 5, dtype=float),\n)\nrgb = [\n    rgb_full, rgb_missing, rgb_all_missing, rgb_bins_missing, rgb_same\n]\n\n# Ordered discrete variable variations\nints_full = VarDataPair(\n    DiscreteVariable('ints_full', values=('2', '3', '4'), ordered=True),\n    np.array([0, 1, 1, 1, 2], dtype=float),\n)\nints_missing = VarDataPair(\n    DiscreteVariable('ints_missing', values=('2', '3', '4'), ordered=True),\n    np.array([0, 1, 1, np.nan, 2], dtype=float),\n)\nints_all_missing = VarDataPair(\n    DiscreteVariable('ints_all_missing', values=('2', '3', '4'), ordered=True),\n    np.array([np.nan] * 5, dtype=float),\n)\nints_bins_missing = VarDataPair(\n    DiscreteVariable('ints_bins_missing', values=('2', '3', '4'), ordered=True),\n    np.array([np.nan, 1, 1, 1, np.nan], dtype=float),\n)\nints_same = VarDataPair(\n    DiscreteVariable('ints_same', values=('2', '3', '4'), ordered=True),\n    np.array([0] * 5, dtype=float),\n)\nints = [\n    ints_full, ints_missing, ints_all_missing, ints_bins_missing, ints_same\n]\n\ndiscrete = list(chain(rgb, ints))\n\n\ndef _to_timestamps(years):\n    return [datetime.datetime(year, 1, 1).timestamp() if not np.isnan(year)\n            else np.nan for year in years]\n\n\n# Time variable variations, windows timestamps need to be valid timestamps so\n# we'll just fill it in with arbitrary years\ntime_full = VarDataPair(\n    TimeVariable('time_full'),\n    np.array(_to_timestamps([2000, 2001, 2002, 2003, 2004]), dtype=float),\n)\ntime_missing = VarDataPair(\n    TimeVariable('time_missing'),\n    np.array(_to_timestamps([2000, np.nan, 2001, 2003, 2004]), dtype=float),\n)\ntime_all_missing = VarDataPair(\n    TimeVariable('time_all_missing'),\n    np.array(_to_timestamps([np.nan] * 5), dtype=float),\n)\ntime_same = VarDataPair(\n    TimeVariable('time_same'),\n    np.array(_to_timestamps([2004] * 5), dtype=float),\n)\ntime = [\n    time_full, time_missing, time_all_missing, time_same\n]\n\n# String variable variations\nstring_full = VarDataPair(\n    StringVariable('string_full'),\n    np.array(['a', 'b', 'c', 'd', 'e'], dtype=object),\n)\nstring_missing = VarDataPair(\n    StringVariable('string_missing'),\n    np.array(['a', 'b', 'c', StringVariable.Unknown, 'e'], dtype=object),\n)\nstring_all_missing = VarDataPair(\n    StringVariable('string_all_missing'),\n    np.array([StringVariable.Unknown] * 5, dtype=object),\n)\nstring_same = VarDataPair(\n    StringVariable('string_same'),\n    np.array(['a'] * 5, dtype=object),\n)\nstring = [\n    string_full, string_missing, string_all_missing, string_same\n]\n\n\ndef make_table(attributes, target=None, metas=None):\n    \"\"\"Build an instance of a table given various variables.\n\n    Parameters\n    ----------\n    attributes : Iterable[Tuple[Variable, np.array]\n    target : Optional[Iterable[Tuple[Variable, np.array]]\n    metas : Optional[Iterable[Tuple[Variable, np.array]]\n\n    Returns\n    -------\n    Table\n\n    \"\"\"\n    attribute_vars, attribute_vals = list(zip(*attributes))\n    attribute_vals = np.array(attribute_vals).T\n\n    target_vars, target_vals = None, None\n    if target is not None:\n        target_vars, target_vals = list(zip(*target))\n        target_vals = np.array(target_vals).T\n\n    meta_vars, meta_vals = None, None\n    if metas is not None:\n        meta_vars, meta_vals = list(zip(*metas))\n        meta_vals = np.array(meta_vals).T\n\n    return Table.from_numpy(\n        Domain(attribute_vars, class_vars=target_vars, metas=meta_vars),\n        X=attribute_vals, Y=target_vals, metas=meta_vals,\n    )\n\n\nclass TestVariousDataSets(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(\n            OWFeatureStatistics, stored_settings={'auto_commit': False}\n        )\n\n    def force_render_table(self):\n        \"\"\"Some fields e.g. histograms are only initialized when they actually\n        need to be rendered\"\"\"\n        model = self.widget.model\n        for i in range(model.rowCount()):\n            for j in range(model.columnCount()):\n                model.data(model.index(i, j), Qt.DisplayRole)\n\n    def run_through_variables(self):\n        simulate.combobox_run_through_all(\n            self.widget.cb_color_var, callback=self.force_render_table)\n\n    @table_dense_sparse\n    def test_runs_on_iris(self, prepare_table):\n        self.send_signal(self.widget.Inputs.data, prepare_table(Table('iris')))\n\n    def test_does_not_crash_on_data_removal(self):\n        self.send_signal(self.widget.Inputs.data, make_table(discrete))\n        self.send_signal(self.widget.Inputs.data, None)\n\n    def test_does_not_crash_on_empty_domain(self):\n        empty_data = Table('iris').transform(Domain([]))\n        self.send_signal(self.widget.Inputs.data, empty_data)\n\n    # No missing values\n    @table_dense_sparse\n    def test_on_data_with_no_missing_values(self, prepare_table):\n        data = make_table([continuous_full, rgb_full, ints_full, time_full])\n        self.send_signal(self.widget.Inputs.data, prepare_table(data))\n        self.run_through_variables()\n\n    @table_dense_sparse\n    def test_on_data_with_no_missing_values_full_domain(self, prepare_table):\n        data = make_table([continuous_full, time_full], [ints_full], [rgb_full])\n        self.send_signal(self.widget.Inputs.data, prepare_table(data))\n        self.run_through_variables()\n\n    # With missing values\n    @table_dense_sparse\n    def test_on_data_with_missing_continuous_values(self, prepare_table):\n        data = make_table([continuous_full, continuous_missing, rgb_full, ints_full, time_full])\n        self.send_signal(self.widget.Inputs.data, prepare_table(data))\n        self.run_through_variables()\n\n    @table_dense_sparse\n    def test_on_data_with_missing_discrete_values(self, prepare_table):\n        data = make_table([continuous_full, rgb_full, rgb_missing, ints_full, time_full])\n        self.send_signal(self.widget.Inputs.data, prepare_table(data))\n        self.run_through_variables()\n\n    @table_dense_sparse\n    def test_on_data_with_discrete_values_all_the_same(self, prepare_table):\n        data = make_table([continuous_full], [ints_same, rgb_same])\n        self.send_signal(self.widget.Inputs.data, prepare_table(data))\n        self.run_through_variables()\n\n    @table_dense_sparse\n    def test_on_data_with_continuous_values_all_the_same(self, prepare_table):\n        data = make_table([ints_full, ints_same], [continuous_same, continuous_full])\n        self.send_signal(self.widget.Inputs.data, prepare_table(data))\n        self.run_through_variables()\n\n    def test_switching_to_dataset_with_no_target_var(self):\n        \"\"\"Switching from data set with target variable to a data set with\n        no target variable should not result in crash.\"\"\"\n        data1 = make_table([continuous_full, ints_full], [ints_same, rgb_same])\n        data2 = make_table([rgb_full, ints_full])\n\n        self.send_signal(self.widget.Inputs.data, data1)\n        self.force_render_table()\n\n        self.send_signal(self.widget.Inputs.data, data2)\n        self.force_render_table()\n\n    def test_switching_to_dataset_with_target_var(self):\n        \"\"\"Switching from data set with no target variable to a data set with\n        a target variable should not result in crash.\"\"\"\n        data1 = make_table([rgb_full, ints_full])\n        data2 = make_table([continuous_full, ints_full], [ints_same, rgb_same])\n\n        self.send_signal(self.widget.Inputs.data, data1)\n        self.force_render_table()\n\n        self.send_signal(self.widget.Inputs.data, data2)\n        self.force_render_table()\n\n    def test_on_edge_case_datasets(self):\n        for data in datasets.datasets():\n            try:\n                self.send_signal(self.widget.Inputs.data, data)\n                self.force_render_table()\n            except Exception as e:\n                raise AssertionError(f\"Failed on `{data.name}`\") from e\n\n\ndef select_rows(rows: List[int], widget: OWFeatureStatistics):\n    \"\"\"Since the widget sorts the rows, selecting rows isn't trivial.\"\"\"\n    indices = widget.model.mapToSourceRows(rows)\n\n    selection = QItemSelection()\n    for idx in indices:\n        selection.append(QItemSelectionRange(\n            widget.model.index(idx, 0),\n            widget.model.index(idx, widget.model.columnCount() - 1)\n        ))\n\n    widget.table_view.selectionModel().select(\n        selection, QItemSelectionModel.ClearAndSelect)\n\n\nclass TestFeatureStatisticsOutputs(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(\n            OWFeatureStatistics, stored_settings={'auto_commit': False}\n        )\n        self.data = make_table(\n            [continuous_full, continuous_missing],\n            target=[rgb_full, rgb_missing], metas=[ints_full, ints_missing]\n        )\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.select_rows = partial(select_rows, widget=self.widget)\n\n    def test_changing_data_updates_ouput(self):\n        # Test behaviour of widget when auto commit is OFF\n        self.widget.auto_commit = False\n\n        # We start of with some data and select some rows\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.select_rows([0])\n        # By default, nothing should be sent since auto commit is off\n        self.assertIsNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNone(self.get_output(self.widget.Outputs.statistics))\n        # When we commit, the data should be on the output\n        self.widget.unconditional_commit()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.statistics))\n\n        # Send some new data\n        iris = Table('iris')\n        self.send_signal(self.widget.Inputs.data, iris)\n        # By default, there should be nothing on the output\n        self.assertIsNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNone(self.get_output(self.widget.Outputs.statistics))\n        # Nothing should change after commit, since we haven't selected any rows\n        self.widget.unconditional_commit()\n        self.assertIsNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNone(self.get_output(self.widget.Outputs.statistics))\n\n        # Now let's switch back to the original data, where we selected row 0\n        self.send_signal(self.widget.Inputs.data, self.data)\n        # Again, since auto commit is off, nothing should be on the output\n        self.assertIsNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNone(self.get_output(self.widget.Outputs.statistics))\n        # Since the row selection is saved into context settings, the appropriate\n        # thing should be sent to output\n        self.widget.unconditional_commit()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.statistics))\n\n    def test_changing_data_updates_output_with_autocommit(self):\n        # Test behaviour of widget when auto commit is ON\n        self.widget.auto_commit = True\n\n        # We start of with some data and select some rows\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.select_rows([0])\n        # Selecting rows should send data to output\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.statistics))\n\n        # Send some new data\n        iris = Table('iris')\n        self.send_signal(self.widget.Inputs.data, iris)\n        # Don't select anything, so the outputs should be empty\n        self.assertIsNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNone(self.get_output(self.widget.Outputs.statistics))\n\n        # Now let's switch back to the original data, where we had selected row 0,\n        # we expect that to be sent to output\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.statistics))\n\n    def test_sends_single_attribute_table_to_output(self):\n        # Check if selecting a single attribute row\n        self.select_rows([0])\n        self.widget.unconditional_commit()\n\n        desired_domain = Domain(attributes=[continuous_full.variable])\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        self.assertEqual(output.domain, desired_domain)\n\n    def test_sends_multiple_attribute_table_to_output(self):\n        # Check if selecting a single attribute row\n        self.select_rows([0, 1])\n        self.widget.unconditional_commit()\n\n        desired_domain = Domain(attributes=[\n            continuous_full.variable, continuous_missing.variable,\n        ])\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        self.assertEqual(output.domain, desired_domain)\n\n    def test_sends_single_class_var_table_to_output(self):\n        self.select_rows([2])\n        self.widget.unconditional_commit()\n\n        desired_domain = Domain(attributes=[], class_vars=[rgb_full.variable])\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        self.assertEqual(output.domain, desired_domain)\n\n    def test_sends_single_meta_table_to_output(self):\n        self.select_rows([4])\n        self.widget.unconditional_commit()\n\n        desired_domain = Domain(attributes=[], metas=[ints_full.variable])\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        self.assertEqual(output.domain, desired_domain)\n\n    def test_sends_multiple_var_types_table_to_output(self):\n        self.select_rows([0, 2, 4])\n        self.widget.unconditional_commit()\n\n        desired_domain = Domain(\n            attributes=[continuous_full.variable],\n            class_vars=[rgb_full.variable],\n            metas=[ints_full.variable],\n        )\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        self.assertEqual(output.domain, desired_domain)\n\n    def test_sends_all_samples_to_output(self):\n        \"\"\"All rows should be sent to output for selected column.\"\"\"\n        self.select_rows([0, 2])\n        self.widget.unconditional_commit()\n\n        selected_vars = Domain(\n            attributes=[continuous_full.variable],\n            class_vars=[rgb_full.variable],\n        )\n\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        np.testing.assert_equal(output.X, self.data[:, selected_vars.variables].X)\n        np.testing.assert_equal(output.Y, self.data[:, selected_vars.variables].Y)\n\n    def test_clearing_selection_sends_none_to_output(self):\n        \"\"\"Clearing all the selected rows should send `None` to output.\"\"\"\n        self.select_rows([0])\n        self.widget.unconditional_commit()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.statistics))\n\n        self.widget.table_view.clearSelection()\n        self.widget.unconditional_commit()\n        self.assertIsNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNone(self.get_output(self.widget.Outputs.statistics))\n\n\nclass TestFeatureStatisticsUI(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(\n            OWFeatureStatistics, stored_settings={'auto_commit': False}\n        )\n        self.data1 = Table('iris')\n        self.data2 = Table('zoo')\n        self.select_rows = partial(select_rows, widget=self.widget)\n\n    def test_restores_previous_selection(self):\n        \"\"\"Widget should remember selection with domain context handler.\"\"\"\n        # Send data and select rows\n        domain1 = self.data1.domain\n        self.send_signal(self.widget.Inputs.data, self.data1)\n        self.select_rows([0, 2])\n        self.assertEqual(set(self.widget.selected_vars),\n                         {domain1[0], domain1[2]})\n\n        # Sending new data clears selection\n        self.send_signal(self.widget.Inputs.data, self.data2)\n        self.assertEqual(len(self.widget.selected_vars), 0)\n\n        # Sending back the old data restores the selection\n        iris3 = self.data1.transform(\n            Domain([domain1[2], domain1[0], domain1[1]], domain1.class_var))\n        self.send_signal(self.widget.Inputs.data, iris3)\n        self.assertEqual(set(self.widget.selected_vars),\n                         {domain1[0], domain1[2]})\n\n    def test_settings_migration_to_ver21(self):\n        settings = {\n            'controlAreaVisible': True, 'savedWidgetGeometry': '',\n            '__version__': 1,\n            'context_settings': [\n                Context(\n                    values={'auto_commit': (True, -2),\n                            'color_var': ('iris', 101),\n                            'selected_rows': [1, 4],\n                            'sorting': ((1, 0), -2), '__version__': 1},\n                    attributes={'petal length': 2, 'petal width': 2,\n                                'sepal length': 2, 'sepal width': 2},\n                    metas={'iris': 1})]\n        }\n        widget = self.create_widget(OWFeatureStatistics,\n                                    stored_settings=settings)\n        self.send_signal(widget.Inputs.data, self.data1)\n        domain = self.data1.domain\n        self.assertEqual(widget.selected_vars, [domain[\"petal width\"],\n                                                domain[\"iris\"]])\n\n\nclass TestSummary(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(OWFeatureStatistics)\n        self.data = make_table(\n            [continuous_full, continuous_missing],\n            target=[rgb_full, rgb_missing], metas=[ints_full, ints_missing]\n        )\n        self.select_rows = partial(select_rows, widget=self.widget)\n\n    def test_summary(self):\n        \"\"\"Check if the status bar is updated when data is received\"\"\"\n        data = self.data\n        input_sum = self.widget.info.set_input_summary = Mock()\n        output_sum = self.widget.info.set_output_summary = Mock()\n\n        self.send_signal(self.widget.Inputs.data, data)\n        input_sum.assert_called_with(len(data), format_summary_details(data))\n\n        self.select_rows([0, 2])\n        self.widget.unconditional_commit()\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        output_sum.assert_called_with(len(output), format_summary_details(output))\n\n        input_sum.reset_mock()\n        output_sum.reset_mock()\n        self.send_signal(self.widget.Inputs.data, None)\n        input_sum.assert_called_once()\n        self.assertEqual(input_sum.call_args[0][0].brief, \"\")\n        output_sum.assert_called_once()\n        self.assertEqual(output_sum.call_args[0][0].brief, \"\")\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n",
            "new_file_content": "#pylint: disable=unsubscriptable-object\nimport datetime\nimport unittest\nfrom collections import namedtuple\nfrom functools import partial\nfrom itertools import chain\nfrom typing import List\nfrom unittest.mock import Mock\n\nimport numpy as np\nfrom AnyQt.QtCore import QItemSelection, QItemSelectionRange, \\\n    QItemSelectionModel, Qt\n\nfrom Orange.data import Table, Domain, StringVariable, ContinuousVariable, \\\n    DiscreteVariable, TimeVariable\nfrom Orange.widgets.tests.base import WidgetTest, datasets\nfrom Orange.widgets.tests.utils import simulate, table_dense_sparse\nfrom Orange.widgets.data.owfeaturestatistics import \\\n    OWFeatureStatistics\nfrom Orange.widgets.utils.state_summary import format_summary_details\nfrom orangewidget.settings import Context\n\nVarDataPair = namedtuple('VarDataPair', ['variable', 'data'])\n\n# Continuous variable variations\ncontinuous_full = VarDataPair(\n    ContinuousVariable('continuous_full'),\n    np.array([0, 1, 2, 3, 4], dtype=float),\n)\ncontinuous_missing = VarDataPair(\n    ContinuousVariable('continuous_missing'),\n    np.array([0, 1, 2, np.nan, 4], dtype=float),\n)\ncontinuous_all_missing = VarDataPair(\n    ContinuousVariable('continuous_all_missing'),\n    np.array([np.nan] * 5, dtype=float),\n)\ncontinuous_same = VarDataPair(\n    ContinuousVariable('continuous_same'),\n    np.array([3] * 5, dtype=float),\n)\ncontinuous = [\n    continuous_full, continuous_missing, continuous_all_missing,\n    continuous_same\n]\n\n# Unordered discrete variable variations\nrgb_full = VarDataPair(\n    DiscreteVariable('rgb_full', values=('r', 'g', 'b')),\n    np.array([0, 1, 1, 1, 2], dtype=float),\n)\nrgb_missing = VarDataPair(\n    DiscreteVariable('rgb_missing', values=('r', 'g', 'b')),\n    np.array([0, 1, 1, np.nan, 2], dtype=float),\n)\nrgb_all_missing = VarDataPair(\n    DiscreteVariable('rgb_all_missing', values=('r', 'g', 'b')),\n    np.array([np.nan] * 5, dtype=float),\n)\nrgb_bins_missing = VarDataPair(\n    DiscreteVariable('rgb_bins_missing', values=('r', 'g', 'b')),\n    np.array([np.nan, 1, 1, 1, np.nan], dtype=float),\n)\nrgb_same = VarDataPair(\n    DiscreteVariable('rgb_same', values=('r', 'g', 'b')),\n    np.array([2] * 5, dtype=float),\n)\nrgb = [\n    rgb_full, rgb_missing, rgb_all_missing, rgb_bins_missing, rgb_same\n]\n\n# Ordered discrete variable variations\nints_full = VarDataPair(\n    DiscreteVariable('ints_full', values=('2', '3', '4'), ordered=True),\n    np.array([0, 1, 1, 1, 2], dtype=float),\n)\nints_missing = VarDataPair(\n    DiscreteVariable('ints_missing', values=('2', '3', '4'), ordered=True),\n    np.array([0, 1, 1, np.nan, 2], dtype=float),\n)\nints_all_missing = VarDataPair(\n    DiscreteVariable('ints_all_missing', values=('2', '3', '4'), ordered=True),\n    np.array([np.nan] * 5, dtype=float),\n)\nints_bins_missing = VarDataPair(\n    DiscreteVariable('ints_bins_missing', values=('2', '3', '4'), ordered=True),\n    np.array([np.nan, 1, 1, 1, np.nan], dtype=float),\n)\nints_same = VarDataPair(\n    DiscreteVariable('ints_same', values=('2', '3', '4'), ordered=True),\n    np.array([0] * 5, dtype=float),\n)\nints = [\n    ints_full, ints_missing, ints_all_missing, ints_bins_missing, ints_same\n]\n\ndiscrete = list(chain(rgb, ints))\n\n\ndef _to_timestamps(years):\n    return [datetime.datetime(year, 1, 1).timestamp() if not np.isnan(year)\n            else np.nan for year in years]\n\n\n# Time variable variations, windows timestamps need to be valid timestamps so\n# we'll just fill it in with arbitrary years\ntime_full = VarDataPair(\n    TimeVariable('time_full'),\n    np.array(_to_timestamps([2000, 2001, 2002, 2003, 2004]), dtype=float),\n)\ntime_missing = VarDataPair(\n    TimeVariable('time_missing'),\n    np.array(_to_timestamps([2000, np.nan, 2001, 2003, 2004]), dtype=float),\n)\ntime_all_missing = VarDataPair(\n    TimeVariable('time_all_missing'),\n    np.array(_to_timestamps([np.nan] * 5), dtype=float),\n)\ntime_same = VarDataPair(\n    TimeVariable('time_same'),\n    np.array(_to_timestamps([2004] * 5), dtype=float),\n)\ntime = [\n    time_full, time_missing, time_all_missing, time_same\n]\n\n# String variable variations\nstring_full = VarDataPair(\n    StringVariable('string_full'),\n    np.array(['a', 'b', 'c', 'd', 'e'], dtype=object),\n)\nstring_missing = VarDataPair(\n    StringVariable('string_missing'),\n    np.array(['a', 'b', 'c', StringVariable.Unknown, 'e'], dtype=object),\n)\nstring_all_missing = VarDataPair(\n    StringVariable('string_all_missing'),\n    np.array([StringVariable.Unknown] * 5, dtype=object),\n)\nstring_same = VarDataPair(\n    StringVariable('string_same'),\n    np.array(['a'] * 5, dtype=object),\n)\nstring = [\n    string_full, string_missing, string_all_missing, string_same\n]\n\n\ndef make_table(attributes, target=None, metas=None):\n    \"\"\"Build an instance of a table given various variables.\n\n    Parameters\n    ----------\n    attributes : Iterable[Tuple[Variable, np.array]\n    target : Optional[Iterable[Tuple[Variable, np.array]]\n    metas : Optional[Iterable[Tuple[Variable, np.array]]\n\n    Returns\n    -------\n    Table\n\n    \"\"\"\n    attribute_vars, attribute_vals = list(zip(*attributes))\n    attribute_vals = np.array(attribute_vals).T\n\n    target_vars, target_vals = None, None\n    if target is not None:\n        target_vars, target_vals = list(zip(*target))\n        target_vals = np.array(target_vals).T\n\n    meta_vars, meta_vals = None, None\n    if metas is not None:\n        meta_vars, meta_vals = list(zip(*metas))\n        meta_vals = np.array(meta_vals).T\n\n    return Table.from_numpy(\n        Domain(attribute_vars, class_vars=target_vars, metas=meta_vars),\n        X=attribute_vals, Y=target_vals, metas=meta_vals,\n    )\n\n\nclass TestVariousDataSets(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(\n            OWFeatureStatistics, stored_settings={'auto_commit': False}\n        )\n\n    def force_render_table(self):\n        \"\"\"Some fields e.g. histograms are only initialized when they actually\n        need to be rendered\"\"\"\n        model = self.widget.model\n        for i in range(model.rowCount()):\n            for j in range(model.columnCount()):\n                model.data(model.index(i, j), Qt.DisplayRole)\n\n    def run_through_variables(self):\n        simulate.combobox_run_through_all(\n            self.widget.cb_color_var, callback=self.force_render_table)\n\n    @table_dense_sparse\n    def test_runs_on_iris(self, prepare_table):\n        self.send_signal(self.widget.Inputs.data, prepare_table(Table('iris')))\n\n    def test_does_not_crash_on_data_removal(self):\n        self.send_signal(self.widget.Inputs.data, make_table(discrete))\n        self.send_signal(self.widget.Inputs.data, None)\n\n    def test_does_not_crash_on_empty_domain(self):\n        empty_data = Table('iris').transform(Domain([]))\n        self.send_signal(self.widget.Inputs.data, empty_data)\n\n    # No missing values\n    @table_dense_sparse\n    def test_on_data_with_no_missing_values(self, prepare_table):\n        data = make_table([continuous_full, rgb_full, ints_full, time_full])\n        self.send_signal(self.widget.Inputs.data, prepare_table(data))\n        self.run_through_variables()\n\n    @table_dense_sparse\n    def test_on_data_with_no_missing_values_full_domain(self, prepare_table):\n        data = make_table([continuous_full, time_full], [ints_full], [rgb_full])\n        self.send_signal(self.widget.Inputs.data, prepare_table(data))\n        self.run_through_variables()\n\n    # With missing values\n    @table_dense_sparse\n    def test_on_data_with_missing_continuous_values(self, prepare_table):\n        data = make_table([continuous_full, continuous_missing, rgb_full, ints_full, time_full])\n        self.send_signal(self.widget.Inputs.data, prepare_table(data))\n        self.run_through_variables()\n\n    @table_dense_sparse\n    def test_on_data_with_missing_discrete_values(self, prepare_table):\n        data = make_table([continuous_full, rgb_full, rgb_missing, ints_full, time_full])\n        self.send_signal(self.widget.Inputs.data, prepare_table(data))\n        self.run_through_variables()\n\n    @table_dense_sparse\n    def test_on_data_with_discrete_values_all_the_same(self, prepare_table):\n        data = make_table([continuous_full], [ints_same, rgb_same])\n        self.send_signal(self.widget.Inputs.data, prepare_table(data))\n        self.run_through_variables()\n\n    @table_dense_sparse\n    def test_on_data_with_continuous_values_all_the_same(self, prepare_table):\n        data = make_table([ints_full, ints_same], [continuous_same, continuous_full])\n        self.send_signal(self.widget.Inputs.data, prepare_table(data))\n        self.run_through_variables()\n\n    def test_switching_to_dataset_with_no_target_var(self):\n        \"\"\"Switching from data set with target variable to a data set with\n        no target variable should not result in crash.\"\"\"\n        data1 = make_table([continuous_full, ints_full], [ints_same, rgb_same])\n        data2 = make_table([rgb_full, ints_full])\n\n        self.send_signal(self.widget.Inputs.data, data1)\n        self.force_render_table()\n\n        self.send_signal(self.widget.Inputs.data, data2)\n        self.force_render_table()\n\n    def test_switching_to_dataset_with_target_var(self):\n        \"\"\"Switching from data set with no target variable to a data set with\n        a target variable should not result in crash.\"\"\"\n        data1 = make_table([rgb_full, ints_full])\n        data2 = make_table([continuous_full, ints_full], [ints_same, rgb_same])\n\n        self.send_signal(self.widget.Inputs.data, data1)\n        self.force_render_table()\n\n        self.send_signal(self.widget.Inputs.data, data2)\n        self.force_render_table()\n\n    def test_on_edge_case_datasets(self):\n        for data in datasets.datasets():\n            try:\n                self.send_signal(self.widget.Inputs.data, data)\n                self.force_render_table()\n            except Exception as e:\n                raise AssertionError(f\"Failed on `{data.name}`\") from e\n\n\ndef select_rows(rows: List[int], widget: OWFeatureStatistics):\n    \"\"\"Since the widget sorts the rows, selecting rows isn't trivial.\"\"\"\n    indices = widget.model.mapToSourceRows(rows)\n\n    selection = QItemSelection()\n    for idx in indices:\n        selection.append(QItemSelectionRange(\n            widget.model.index(idx, 0),\n            widget.model.index(idx, widget.model.columnCount() - 1)\n        ))\n\n    widget.table_view.selectionModel().select(\n        selection, QItemSelectionModel.ClearAndSelect)\n\n\nclass TestFeatureStatisticsOutputs(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(\n            OWFeatureStatistics, stored_settings={'auto_commit': False}\n        )\n        self.data = make_table(\n            [continuous_full, continuous_missing],\n            target=[rgb_full, rgb_missing], metas=[ints_full, ints_missing]\n        )\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.select_rows = partial(select_rows, widget=self.widget)\n\n    def test_changing_data_updates_ouput(self):\n        # Test behaviour of widget when auto commit is OFF\n        self.widget.auto_commit = False\n\n        # We start of with some data and select some rows\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.select_rows([0])\n        # By default, nothing should be sent since auto commit is off\n        self.assertIsNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNone(self.get_output(self.widget.Outputs.statistics))\n        # When we commit, the data should be on the output\n        self.widget.unconditional_commit()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.statistics))\n\n        # Send some new data\n        iris = Table('iris')\n        self.send_signal(self.widget.Inputs.data, iris)\n        # By default, there should be nothing on the output\n        self.assertIsNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNone(self.get_output(self.widget.Outputs.statistics))\n        # Nothing should change after commit, since we haven't selected any rows\n        self.widget.unconditional_commit()\n        self.assertIsNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNone(self.get_output(self.widget.Outputs.statistics))\n\n        # Now let's switch back to the original data, where we selected row 0\n        self.send_signal(self.widget.Inputs.data, self.data)\n        # Again, since auto commit is off, nothing should be on the output\n        self.assertIsNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNone(self.get_output(self.widget.Outputs.statistics))\n        # Since the row selection is saved into context settings, the appropriate\n        # thing should be sent to output\n        self.widget.unconditional_commit()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.statistics))\n\n    def test_changing_data_updates_output_with_autocommit(self):\n        # Test behaviour of widget when auto commit is ON\n        self.widget.auto_commit = True\n\n        # We start of with some data and select some rows\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.select_rows([0])\n        # Selecting rows should send data to output\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.statistics))\n\n        # Send some new data\n        iris = Table('iris')\n        self.send_signal(self.widget.Inputs.data, iris)\n        # Don't select anything, so the outputs should be empty\n        self.assertIsNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNone(self.get_output(self.widget.Outputs.statistics))\n\n        # Now let's switch back to the original data, where we had selected row 0,\n        # we expect that to be sent to output\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.statistics))\n\n    def test_sends_single_attribute_table_to_output(self):\n        # Check if selecting a single attribute row\n        self.select_rows([0])\n        self.widget.unconditional_commit()\n\n        desired_domain = Domain(attributes=[continuous_full.variable])\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        self.assertEqual(output.domain, desired_domain)\n\n    def test_sends_multiple_attribute_table_to_output(self):\n        # Check if selecting a single attribute row\n        self.select_rows([0, 1])\n        self.widget.unconditional_commit()\n\n        desired_domain = Domain(attributes=[\n            continuous_full.variable, continuous_missing.variable,\n        ])\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        self.assertEqual(output.domain, desired_domain)\n\n    def test_sends_single_class_var_table_to_output(self):\n        self.select_rows([2])\n        self.widget.unconditional_commit()\n\n        desired_domain = Domain(attributes=[], class_vars=[rgb_full.variable])\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        self.assertEqual(output.domain, desired_domain)\n\n    def test_sends_single_meta_table_to_output(self):\n        self.select_rows([4])\n        self.widget.unconditional_commit()\n\n        desired_domain = Domain(attributes=[], metas=[ints_full.variable])\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        self.assertEqual(output.domain, desired_domain)\n\n    def test_sends_multiple_var_types_table_to_output(self):\n        self.select_rows([0, 2, 4])\n        self.widget.unconditional_commit()\n\n        desired_domain = Domain(\n            attributes=[continuous_full.variable],\n            class_vars=[rgb_full.variable],\n            metas=[ints_full.variable],\n        )\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        self.assertEqual(output.domain, desired_domain)\n\n    def test_sends_all_samples_to_output(self):\n        \"\"\"All rows should be sent to output for selected column.\"\"\"\n        self.select_rows([0, 2])\n        self.widget.unconditional_commit()\n\n        selected_vars = Domain(\n            attributes=[continuous_full.variable],\n            class_vars=[rgb_full.variable],\n        )\n\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        np.testing.assert_equal(output.X, self.data[:, selected_vars.variables].X)\n        np.testing.assert_equal(output.Y, self.data[:, selected_vars.variables].Y)\n\n    def test_clearing_selection_sends_none_to_output(self):\n        \"\"\"Clearing all the selected rows should send `None` to output.\"\"\"\n        self.select_rows([0])\n        self.widget.unconditional_commit()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.statistics))\n\n        self.widget.table_view.clearSelection()\n        self.widget.unconditional_commit()\n        self.assertIsNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNone(self.get_output(self.widget.Outputs.statistics))\n\n\nclass TestFeatureStatisticsUI(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(\n            OWFeatureStatistics, stored_settings={'auto_commit': False}\n        )\n        self.data1 = Table('iris')\n        self.data2 = Table('zoo')\n        self.select_rows = partial(select_rows, widget=self.widget)\n\n    def test_restores_previous_selection(self):\n        \"\"\"Widget should remember selection with domain context handler.\"\"\"\n        # Send data and select rows\n        domain1 = self.data1.domain\n        self.send_signal(self.widget.Inputs.data, self.data1)\n        self.select_rows([0, 2])\n        self.assertEqual(set(self.widget.selected_vars),\n                         {domain1[0], domain1[2]})\n\n        # Sending new data clears selection\n        self.send_signal(self.widget.Inputs.data, self.data2)\n        self.assertEqual(len(self.widget.selected_vars), 0)\n\n        # Sending back the old data restores the selection\n        iris3 = self.data1.transform(\n            Domain([domain1[2], domain1[0], domain1[1]], domain1.class_var))\n        self.send_signal(self.widget.Inputs.data, iris3)\n        self.assertEqual(set(self.widget.selected_vars),\n                         {domain1[0], domain1[2]})\n\n    def test_settings_migration_to_ver21(self):\n        settings = {\n            'controlAreaVisible': True, 'savedWidgetGeometry': '',\n            '__version__': 1,\n            'context_settings': [\n                Context(\n                    values={'auto_commit': (True, -2),\n                            'color_var': ('iris', 101),\n                            'selected_rows': [1, 4],\n                            'sorting': ((1, 0), -2), '__version__': 1},\n                    attributes={'petal length': 2, 'petal width': 2,\n                                'sepal length': 2, 'sepal width': 2},\n                    metas={'iris': 1})]\n        }\n        widget = self.create_widget(OWFeatureStatistics,\n                                    stored_settings=settings)\n        self.send_signal(widget.Inputs.data, self.data1)\n        domain = self.data1.domain\n        self.assertEqual(widget.selected_vars, [domain[\"petal width\"],\n                                                domain[\"iris\"]])\n\n    def test_report(self):\n        self.send_signal(self.widget.Inputs.data, self.data1)\n\n        self.widget.report_button.click()\n        report_text = self.widget.report_html\n\n        self.assertIn(\"<table>\", report_text)\n        self.assertEqual(6, report_text.count(\"<tr>\"))  # header + 5 rows\n\n\nclass TestSummary(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(OWFeatureStatistics)\n        self.data = make_table(\n            [continuous_full, continuous_missing],\n            target=[rgb_full, rgb_missing], metas=[ints_full, ints_missing]\n        )\n        self.select_rows = partial(select_rows, widget=self.widget)\n\n    def test_summary(self):\n        \"\"\"Check if the status bar is updated when data is received\"\"\"\n        data = self.data\n        input_sum = self.widget.info.set_input_summary = Mock()\n        output_sum = self.widget.info.set_output_summary = Mock()\n\n        self.send_signal(self.widget.Inputs.data, data)\n        input_sum.assert_called_with(len(data), format_summary_details(data))\n\n        self.select_rows([0, 2])\n        self.widget.unconditional_commit()\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        output_sum.assert_called_with(len(output), format_summary_details(output))\n\n        input_sum.reset_mock()\n        output_sum.reset_mock()\n        self.send_signal(self.widget.Inputs.data, None)\n        input_sum.assert_called_once()\n        self.assertEqual(input_sum.call_args[0][0].brief, \"\")\n        output_sum.assert_called_once()\n        self.assertEqual(output_sum.call_args[0][0].brief, \"\")\n\n\nif __name__ == \"__main__\":\n    unittest.main()\n",
            "header": {
                "file": {
                    "path": "Orange/widgets/data/tests/test_owfeaturestatistics.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "c16ef62fd",
                "new_commit_hash": "8709562d6",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Orange/widgets/data/tests/test_owfeaturestatistics.py"
            },
            "plus_file": {
                "path": "b/Orange/widgets/data/tests/test_owfeaturestatistics.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 493,
                            "length": 6
                        },
                        "new_range": {
                            "start": 493,
                            "length": 15
                        },
                        "section": "class TestFeatureStatisticsUI(WidgetTest):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        self.assertEqual(widget.selected_vars, [domain[\"petal width\"],",
                                "type": "context"
                            },
                            {
                                "content": "                                                domain[\"iris\"]])",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    def test_report(self):",
                                "type": "added"
                            },
                            {
                                "content": "        self.send_signal(self.widget.Inputs.data, self.data1)",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        self.widget.report_button.click()",
                                "type": "added"
                            },
                            {
                                "content": "        report_text = self.widget.report_html",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertIn(\"<table>\", report_text)",
                                "type": "added"
                            },
                            {
                                "content": "        self.assertEqual(6, report_text.count(\"<tr>\"))  # header + 5 rows",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "class TestSummary(WidgetTest):",
                                "type": "context"
                            },
                            {
                                "content": "    def setUp(self):",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Orange/widgets/data/tests/test_owfeaturestatistics.py",
                            "type": "class",
                            "name": "TestFeatureStatisticsUI",
                            "content": "class TestFeatureStatisticsUI(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(\n            OWFeatureStatistics, stored_settings={'auto_commit': False}\n        )\n        self.data1 = Table('iris')\n        self.data2 = Table('zoo')\n        self.select_rows = partial(select_rows, widget=self.widget)\n\n    def test_restores_previous_selection(self):\n        \"\"\"Widget should remember selection with domain context handler.\"\"\"\n        # Send data and select rows\n        domain1 = self.data1.domain\n        self.send_signal(self.widget.Inputs.data, self.data1)\n        self.select_rows([0, 2])\n        self.assertEqual(set(self.widget.selected_vars),\n                         {domain1[0], domain1[2]})\n\n        # Sending new data clears selection\n        self.send_signal(self.widget.Inputs.data, self.data2)\n        self.assertEqual(len(self.widget.selected_vars), 0)\n\n        # Sending back the old data restores the selection\n        iris3 = self.data1.transform(\n            Domain([domain1[2], domain1[0], domain1[1]], domain1.class_var))\n        self.send_signal(self.widget.Inputs.data, iris3)\n        self.assertEqual(set(self.widget.selected_vars),\n                         {domain1[0], domain1[2]})\n\n    def test_settings_migration_to_ver21(self):\n        settings = {\n            'controlAreaVisible': True, 'savedWidgetGeometry': '',\n            '__version__': 1,\n            'context_settings': [\n                Context(\n                    values={'auto_commit': (True, -2),\n                            'color_var': ('iris', 101),\n                            'selected_rows': [1, 4],\n                            'sorting': ((1, 0), -2), '__version__': 1},\n                    attributes={'petal length': 2, 'petal width': 2,\n                                'sepal length': 2, 'sepal width': 2},\n                    metas={'iris': 1})]\n        }\n        widget = self.create_widget(OWFeatureStatistics,\n                                    stored_settings=settings)\n        self.send_signal(widget.Inputs.data, self.data1)\n        domain = self.data1.domain\n        self.assertEqual(widget.selected_vars, [domain[\"petal width\"],\n                                                domain[\"iris\"]])\n\n    def test_report(self):\n        self.send_signal(self.widget.Inputs.data, self.data1)\n\n        self.widget.report_button.click()\n        report_text = self.widget.report_html\n\n        self.assertIn(\"<table>\", report_text)\n        self.assertEqual(6, report_text.count(\"<tr>\"))",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 446,
                            "end_lineno": 503,
                            "parent": null
                        }
                    ],
                    "added_entities": [
                        {
                            "file_name": "Orange/widgets/data/tests/test_owfeaturestatistics.py",
                            "type": "method",
                            "name": "TestFeatureStatisticsUI.test_report",
                            "content": "class TestFeatureStatisticsUI(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(\n            OWFeatureStatistics, stored_settings={'auto_commit': False}\n        )\n        self.data1 = Table('iris')\n        self.data2 = Table('zoo')\n        self.select_rows = partial(select_rows, widget=self.widget)\n\n    def test_restores_previous_selection(self):\n        \"\"\"Widget should remember selection with domain context handler.\"\"\"\n        # Send data and select rows\n        domain1 = self.data1.domain\n        self.send_signal(self.widget.Inputs.data, self.data1)\n        self.select_rows([0, 2])\n        self.assertEqual(set(self.widget.selected_vars),\n                         {domain1[0], domain1[2]})\n\n        # Sending new data clears selection\n        self.send_signal(self.widget.Inputs.data, self.data2)\n        self.assertEqual(len(self.widget.selected_vars), 0)\n\n        # Sending back the old data restores the selection\n        iris3 = self.data1.transform(\n            Domain([domain1[2], domain1[0], domain1[1]], domain1.class_var))\n        self.send_signal(self.widget.Inputs.data, iris3)\n        self.assertEqual(set(self.widget.selected_vars),\n                         {domain1[0], domain1[2]})\n\n    def test_settings_migration_to_ver21(self):\n        settings = {\n            'controlAreaVisible': True, 'savedWidgetGeometry': '',\n            '__version__': 1,\n            'context_settings': [\n                Context(\n                    values={'auto_commit': (True, -2),\n                            'color_var': ('iris', 101),\n                            'selected_rows': [1, 4],\n                            'sorting': ((1, 0), -2), '__version__': 1},\n                    attributes={'petal length': 2, 'petal width': 2,\n                                'sepal length': 2, 'sepal width': 2},\n                    metas={'iris': 1})]\n        }\n        widget = self.create_widget(OWFeatureStatistics,\n                                    stored_settings=settings)\n        self.send_signal(widget.Inputs.data, self.data1)\n        domain = self.data1.domain\n        self.assertEqual(widget.selected_vars, [domain[\"petal width\"],\n                                                domain[\"iris\"]])\n\n    def test_report(self):\n        self.send_signal(self.widget.Inputs.data, self.data1)\n\n        self.widget.report_button.click()\n        report_text = self.widget.report_html\n\n        self.assertIn(\"<table>\", report_text)\n        self.assertEqual(6, report_text.count(\"<tr>\"))",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 496,
                            "end_lineno": 503,
                            "parent": {
                                "file_name": "Orange/widgets/data/tests/test_owfeaturestatistics.py",
                                "type": "class",
                                "name": "TestFeatureStatisticsUI",
                                "content": "class TestFeatureStatisticsUI(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(\n            OWFeatureStatistics, stored_settings={'auto_commit': False}\n        )\n        self.data1 = Table('iris')\n        self.data2 = Table('zoo')\n        self.select_rows = partial(select_rows, widget=self.widget)\n\n    def test_restores_previous_selection(self):\n        \"\"\"Widget should remember selection with domain context handler.\"\"\"\n        # Send data and select rows\n        domain1 = self.data1.domain\n        self.send_signal(self.widget.Inputs.data, self.data1)\n        self.select_rows([0, 2])\n        self.assertEqual(set(self.widget.selected_vars),\n                         {domain1[0], domain1[2]})\n\n        # Sending new data clears selection\n        self.send_signal(self.widget.Inputs.data, self.data2)\n        self.assertEqual(len(self.widget.selected_vars), 0)\n\n        # Sending back the old data restores the selection\n        iris3 = self.data1.transform(\n            Domain([domain1[2], domain1[0], domain1[1]], domain1.class_var))\n        self.send_signal(self.widget.Inputs.data, iris3)\n        self.assertEqual(set(self.widget.selected_vars),\n                         {domain1[0], domain1[2]})\n\n    def test_settings_migration_to_ver21(self):\n        settings = {\n            'controlAreaVisible': True, 'savedWidgetGeometry': '',\n            '__version__': 1,\n            'context_settings': [\n                Context(\n                    values={'auto_commit': (True, -2),\n                            'color_var': ('iris', 101),\n                            'selected_rows': [1, 4],\n                            'sorting': ((1, 0), -2), '__version__': 1},\n                    attributes={'petal length': 2, 'petal width': 2,\n                                'sepal length': 2, 'sepal width': 2},\n                    metas={'iris': 1})]\n        }\n        widget = self.create_widget(OWFeatureStatistics,\n                                    stored_settings=settings)\n        self.send_signal(widget.Inputs.data, self.data1)\n        domain = self.data1.domain\n        self.assertEqual(widget.selected_vars, [domain[\"petal width\"],\n                                                domain[\"iris\"]])\n\n    def test_report(self):\n        self.send_signal(self.widget.Inputs.data, self.data1)\n\n        self.widget.report_button.click()\n        report_text = self.widget.report_html\n\n        self.assertIn(\"<table>\", report_text)\n        self.assertEqual(6, report_text.count(\"<tr>\"))",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 446,
                                "end_lineno": 503,
                                "parent": null
                            }
                        }
                    ],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "adad6e21aeff5b099246e243aa6cb9adc5bdf903^",
    "new_commit_hash": "adad6e21aeff5b099246e243aa6cb9adc5bdf903",
    "commit_message": "Merge pull request #4812 from PrimozGodec/report-feat-stat\n\n[ENH] Feature statistics report",
    "commit_date": "2020-06-10T09:40:49+02:00",
    "metadata": {}
}
{
    "repo_name": "orange3",
    "new_commit_hash": "adad6e21aeff5b099246e243aa6cb9adc5bdf903",
    "test_file_codes": [
        "#pylint: disable=unsubscriptable-object\nimport datetime\nimport unittest\nfrom collections import namedtuple\nfrom functools import partial\nfrom itertools import chain\nfrom typing import List\nfrom unittest.mock import Mock\n\nimport numpy as np\nfrom AnyQt.QtCore import QItemSelection, QItemSelectionRange, \\\n    QItemSelectionModel, Qt\n\nfrom Orange.data import Table, Domain, StringVariable, ContinuousVariable, \\\n    DiscreteVariable, TimeVariable\nfrom Orange.widgets.tests.base import WidgetTest, datasets\nfrom Orange.widgets.tests.utils import simulate, table_dense_sparse\nfrom Orange.widgets.data.owfeaturestatistics import \\\n    OWFeatureStatistics\nfrom Orange.widgets.utils.state_summary import format_summary_details\nfrom orangewidget.settings import Context\n\nVarDataPair = namedtuple('VarDataPair', ['variable', 'data'])\n\n# Continuous variable variations\ncontinuous_full = VarDataPair(\n    ContinuousVariable('continuous_full'),\n    np.array([0, 1, 2, 3, 4], dtype=float),\n)\ncontinuous_missing = VarDataPair(\n    ContinuousVariable('continuous_missing'),\n    np.array([0, 1, 2, np.nan, 4], dtype=float),\n)\ncontinuous_all_missing = VarDataPair(\n    ContinuousVariable('continuous_all_missing'),\n    np.array([np.nan] * 5, dtype=float),\n)\ncontinuous_same = VarDataPair(\n    ContinuousVariable('continuous_same'),\n    np.array([3] * 5, dtype=float),\n)\ncontinuous = [\n    continuous_full, continuous_missing, continuous_all_missing,\n    continuous_same\n]\n\n# Unordered discrete variable variations\nrgb_full = VarDataPair(\n    DiscreteVariable('rgb_full', values=('r', 'g', 'b')),\n    np.array([0, 1, 1, 1, 2], dtype=float),\n)\nrgb_missing = VarDataPair(\n    DiscreteVariable('rgb_missing', values=('r', 'g', 'b')),\n    np.array([0, 1, 1, np.nan, 2], dtype=float),\n)\nrgb_all_missing = VarDataPair(\n    DiscreteVariable('rgb_all_missing', values=('r', 'g', 'b')),\n    np.array([np.nan] * 5, dtype=float),\n)\nrgb_bins_missing = VarDataPair(\n    DiscreteVariable('rgb_bins_missing', values=('r', 'g', 'b')),\n    np.array([np.nan, 1, 1, 1, np.nan], dtype=float),\n)\nrgb_same = VarDataPair(\n    DiscreteVariable('rgb_same', values=('r', 'g', 'b')),\n    np.array([2] * 5, dtype=float),\n)\nrgb = [\n    rgb_full, rgb_missing, rgb_all_missing, rgb_bins_missing, rgb_same\n]\n\n# Ordered discrete variable variations\nints_full = VarDataPair(\n    DiscreteVariable('ints_full', values=('2', '3', '4'), ordered=True),\n    np.array([0, 1, 1, 1, 2], dtype=float),\n)\nints_missing = VarDataPair(\n    DiscreteVariable('ints_missing', values=('2', '3', '4'), ordered=True),\n    np.array([0, 1, 1, np.nan, 2], dtype=float),\n)\nints_all_missing = VarDataPair(\n    DiscreteVariable('ints_all_missing', values=('2', '3', '4'), ordered=True),\n    np.array([np.nan] * 5, dtype=float),\n)\nints_bins_missing = VarDataPair(\n    DiscreteVariable('ints_bins_missing', values=('2', '3', '4'), ordered=True),\n    np.array([np.nan, 1, 1, 1, np.nan], dtype=float),\n)\nints_same = VarDataPair(\n    DiscreteVariable('ints_same', values=('2', '3', '4'), ordered=True),\n    np.array([0] * 5, dtype=float),\n)\nints = [\n    ints_full, ints_missing, ints_all_missing, ints_bins_missing, ints_same\n]\n\ndiscrete = list(chain(rgb, ints))\n\n\ndef _to_timestamps(years):\n    return [datetime.datetime(year, 1, 1).timestamp() if not np.isnan(year)\n            else np.nan for year in years]\n\n\n# Time variable variations, windows timestamps need to be valid timestamps so\n# we'll just fill it in with arbitrary years\ntime_full = VarDataPair(\n    TimeVariable('time_full'),\n    np.array(_to_timestamps([2000, 2001, 2002, 2003, 2004]), dtype=float),\n)\ntime_missing = VarDataPair(\n    TimeVariable('time_missing'),\n    np.array(_to_timestamps([2000, np.nan, 2001, 2003, 2004]), dtype=float),\n)\ntime_all_missing = VarDataPair(\n    TimeVariable('time_all_missing'),\n    np.array(_to_timestamps([np.nan] * 5), dtype=float),\n)\ntime_same = VarDataPair(\n    TimeVariable('time_same'),\n    np.array(_to_timestamps([2004] * 5), dtype=float),\n)\ntime = [\n    time_full, time_missing, time_all_missing, time_same\n]\n\n# String variable variations\nstring_full = VarDataPair(\n    StringVariable('string_full'),\n    np.array(['a', 'b', 'c', 'd', 'e'], dtype=object),\n)\nstring_missing = VarDataPair(\n    StringVariable('string_missing'),\n    np.array(['a', 'b', 'c', StringVariable.Unknown, 'e'], dtype=object),\n)\nstring_all_missing = VarDataPair(\n    StringVariable('string_all_missing'),\n    np.array([StringVariable.Unknown] * 5, dtype=object),\n)\nstring_same = VarDataPair(\n    StringVariable('string_same'),\n    np.array(['a'] * 5, dtype=object),\n)\nstring = [\n    string_full, string_missing, string_all_missing, string_same\n]\n\n\ndef make_table(attributes, target=None, metas=None):\n    \"\"\"Build an instance of a table given various variables.\n\n    Parameters\n    ----------\n    attributes : Iterable[Tuple[Variable, np.array]\n    target : Optional[Iterable[Tuple[Variable, np.array]]\n    metas : Optional[Iterable[Tuple[Variable, np.array]]\n\n    Returns\n    -------\n    Table\n\n    \"\"\"\n    attribute_vars, attribute_vals = list(zip(*attributes))\n    attribute_vals = np.array(attribute_vals).T\n\n    target_vars, target_vals = None, None\n    if target is not None:\n        target_vars, target_vals = list(zip(*target))\n        target_vals = np.array(target_vals).T\n\n    meta_vars, meta_vals = None, None\n    if metas is not None:\n        meta_vars, meta_vals = list(zip(*metas))\n        meta_vals = np.array(meta_vals).T\n\n    return Table.from_numpy(\n        Domain(attribute_vars, class_vars=target_vars, metas=meta_vars),\n        X=attribute_vals, Y=target_vals, metas=meta_vals,\n    )\n\n\nclass TestVariousDataSets(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(\n            OWFeatureStatistics, stored_settings={'auto_commit': False}\n        )\n\n    def force_render_table(self):\n        \"\"\"Some fields e.g. histograms are only initialized when they actually\n        need to be rendered\"\"\"\n        model = self.widget.model\n        for i in range(model.rowCount()):\n            for j in range(model.columnCount()):\n                model.data(model.index(i, j), Qt.DisplayRole)\n\n    def run_through_variables(self):\n        simulate.combobox_run_through_all(\n            self.widget.cb_color_var, callback=self.force_render_table)\n\n    @table_dense_sparse\n    def test_runs_on_iris(self, prepare_table):\n        self.send_signal(self.widget.Inputs.data, prepare_table(Table('iris')))\n\n    def test_does_not_crash_on_data_removal(self):\n        self.send_signal(self.widget.Inputs.data, make_table(discrete))\n        self.send_signal(self.widget.Inputs.data, None)\n\n    def test_does_not_crash_on_empty_domain(self):\n        empty_data = Table('iris').transform(Domain([]))\n        self.send_signal(self.widget.Inputs.data, empty_data)\n\n    # No missing values\n    @table_dense_sparse\n    def test_on_data_with_no_missing_values(self, prepare_table):\n        data = make_table([continuous_full, rgb_full, ints_full, time_full])\n        self.send_signal(self.widget.Inputs.data, prepare_table(data))\n        self.run_through_variables()\n\n    @table_dense_sparse\n    def test_on_data_with_no_missing_values_full_domain(self, prepare_table):\n        data = make_table([continuous_full, time_full], [ints_full], [rgb_full])\n        self.send_signal(self.widget.Inputs.data, prepare_table(data))\n        self.run_through_variables()\n\n    # With missing values\n    @table_dense_sparse\n    def test_on_data_with_missing_continuous_values(self, prepare_table):\n        data = make_table([continuous_full, continuous_missing, rgb_full, ints_full, time_full])\n        self.send_signal(self.widget.Inputs.data, prepare_table(data))\n        self.run_through_variables()\n\n    @table_dense_sparse\n    def test_on_data_with_missing_discrete_values(self, prepare_table):\n        data = make_table([continuous_full, rgb_full, rgb_missing, ints_full, time_full])\n        self.send_signal(self.widget.Inputs.data, prepare_table(data))\n        self.run_through_variables()\n\n    @table_dense_sparse\n    def test_on_data_with_discrete_values_all_the_same(self, prepare_table):\n        data = make_table([continuous_full], [ints_same, rgb_same])\n        self.send_signal(self.widget.Inputs.data, prepare_table(data))\n        self.run_through_variables()\n\n    @table_dense_sparse\n    def test_on_data_with_continuous_values_all_the_same(self, prepare_table):\n        data = make_table([ints_full, ints_same], [continuous_same, continuous_full])\n        self.send_signal(self.widget.Inputs.data, prepare_table(data))\n        self.run_through_variables()\n\n    def test_switching_to_dataset_with_no_target_var(self):\n        \"\"\"Switching from data set with target variable to a data set with\n        no target variable should not result in crash.\"\"\"\n        data1 = make_table([continuous_full, ints_full], [ints_same, rgb_same])\n        data2 = make_table([rgb_full, ints_full])\n\n        self.send_signal(self.widget.Inputs.data, data1)\n        self.force_render_table()\n\n        self.send_signal(self.widget.Inputs.data, data2)\n        self.force_render_table()\n\n    def test_switching_to_dataset_with_target_var(self):\n        \"\"\"Switching from data set with no target variable to a data set with\n        a target variable should not result in crash.\"\"\"\n        data1 = make_table([rgb_full, ints_full])\n        data2 = make_table([continuous_full, ints_full], [ints_same, rgb_same])\n\n        self.send_signal(self.widget.Inputs.data, data1)\n        self.force_render_table()\n\n        self.send_signal(self.widget.Inputs.data, data2)\n        self.force_render_table()\n\n    def test_on_edge_case_datasets(self):\n        for data in datasets.datasets():\n            try:\n                self.send_signal(self.widget.Inputs.data, data)\n                self.force_render_table()\n            except Exception as e:\n                raise AssertionError(f\"Failed on `{data.name}`\") from e\n\n\ndef select_rows(rows: List[int], widget: OWFeatureStatistics):\n    \"\"\"Since the widget sorts the rows, selecting rows isn't trivial.\"\"\"\n    indices = widget.model.mapToSourceRows(rows)\n\n    selection = QItemSelection()\n    for idx in indices:\n        selection.append(QItemSelectionRange(\n            widget.model.index(idx, 0),\n            widget.model.index(idx, widget.model.columnCount() - 1)\n        ))\n\n    widget.table_view.selectionModel().select(\n        selection, QItemSelectionModel.ClearAndSelect)\n\n\nclass TestFeatureStatisticsOutputs(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(\n            OWFeatureStatistics, stored_settings={'auto_commit': False}\n        )\n        self.data = make_table(\n            [continuous_full, continuous_missing],\n            target=[rgb_full, rgb_missing], metas=[ints_full, ints_missing]\n        )\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.select_rows = partial(select_rows, widget=self.widget)\n\n    def test_changing_data_updates_ouput(self):\n        # Test behaviour of widget when auto commit is OFF\n        self.widget.auto_commit = False\n\n        # We start of with some data and select some rows\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.select_rows([0])\n        # By default, nothing should be sent since auto commit is off\n        self.assertIsNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNone(self.get_output(self.widget.Outputs.statistics))\n        # When we commit, the data should be on the output\n        self.widget.unconditional_commit()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.statistics))\n\n        # Send some new data\n        iris = Table('iris')\n        self.send_signal(self.widget.Inputs.data, iris)\n        # By default, there should be nothing on the output\n        self.assertIsNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNone(self.get_output(self.widget.Outputs.statistics))\n        # Nothing should change after commit, since we haven't selected any rows\n        self.widget.unconditional_commit()\n        self.assertIsNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNone(self.get_output(self.widget.Outputs.statistics))\n\n        # Now let's switch back to the original data, where we selected row 0\n        self.send_signal(self.widget.Inputs.data, self.data)\n        # Again, since auto commit is off, nothing should be on the output\n        self.assertIsNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNone(self.get_output(self.widget.Outputs.statistics))\n        # Since the row selection is saved into context settings, the appropriate\n        # thing should be sent to output\n        self.widget.unconditional_commit()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.statistics))\n\n    def test_changing_data_updates_output_with_autocommit(self):\n        # Test behaviour of widget when auto commit is ON\n        self.widget.auto_commit = True\n\n        # We start of with some data and select some rows\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.select_rows([0])\n        # Selecting rows should send data to output\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.statistics))\n\n        # Send some new data\n        iris = Table('iris')\n        self.send_signal(self.widget.Inputs.data, iris)\n        # Don't select anything, so the outputs should be empty\n        self.assertIsNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNone(self.get_output(self.widget.Outputs.statistics))\n\n        # Now let's switch back to the original data, where we had selected row 0,\n        # we expect that to be sent to output\n        self.send_signal(self.widget.Inputs.data, self.data)\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.statistics))\n\n    def test_sends_single_attribute_table_to_output(self):\n        # Check if selecting a single attribute row\n        self.select_rows([0])\n        self.widget.unconditional_commit()\n\n        desired_domain = Domain(attributes=[continuous_full.variable])\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        self.assertEqual(output.domain, desired_domain)\n\n    def test_sends_multiple_attribute_table_to_output(self):\n        # Check if selecting a single attribute row\n        self.select_rows([0, 1])\n        self.widget.unconditional_commit()\n\n        desired_domain = Domain(attributes=[\n            continuous_full.variable, continuous_missing.variable,\n        ])\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        self.assertEqual(output.domain, desired_domain)\n\n    def test_sends_single_class_var_table_to_output(self):\n        self.select_rows([2])\n        self.widget.unconditional_commit()\n\n        desired_domain = Domain(attributes=[], class_vars=[rgb_full.variable])\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        self.assertEqual(output.domain, desired_domain)\n\n    def test_sends_single_meta_table_to_output(self):\n        self.select_rows([4])\n        self.widget.unconditional_commit()\n\n        desired_domain = Domain(attributes=[], metas=[ints_full.variable])\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        self.assertEqual(output.domain, desired_domain)\n\n    def test_sends_multiple_var_types_table_to_output(self):\n        self.select_rows([0, 2, 4])\n        self.widget.unconditional_commit()\n\n        desired_domain = Domain(\n            attributes=[continuous_full.variable],\n            class_vars=[rgb_full.variable],\n            metas=[ints_full.variable],\n        )\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        self.assertEqual(output.domain, desired_domain)\n\n    def test_sends_all_samples_to_output(self):\n        \"\"\"All rows should be sent to output for selected column.\"\"\"\n        self.select_rows([0, 2])\n        self.widget.unconditional_commit()\n\n        selected_vars = Domain(\n            attributes=[continuous_full.variable],\n            class_vars=[rgb_full.variable],\n        )\n\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        np.testing.assert_equal(output.X, self.data[:, selected_vars.variables].X)\n        np.testing.assert_equal(output.Y, self.data[:, selected_vars.variables].Y)\n\n    def test_clearing_selection_sends_none_to_output(self):\n        \"\"\"Clearing all the selected rows should send `None` to output.\"\"\"\n        self.select_rows([0])\n        self.widget.unconditional_commit()\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNotNone(self.get_output(self.widget.Outputs.statistics))\n\n        self.widget.table_view.clearSelection()\n        self.widget.unconditional_commit()\n        self.assertIsNone(self.get_output(self.widget.Outputs.reduced_data))\n        self.assertIsNone(self.get_output(self.widget.Outputs.statistics))\n\n\nclass TestFeatureStatisticsUI(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(\n            OWFeatureStatistics, stored_settings={'auto_commit': False}\n        )\n        self.data1 = Table('iris')\n        self.data2 = Table('zoo')\n        self.select_rows = partial(select_rows, widget=self.widget)\n\n    def test_restores_previous_selection(self):\n        \"\"\"Widget should remember selection with domain context handler.\"\"\"\n        # Send data and select rows\n        domain1 = self.data1.domain\n        self.send_signal(self.widget.Inputs.data, self.data1)\n        self.select_rows([0, 2])\n        self.assertEqual(set(self.widget.selected_vars),\n                         {domain1[0], domain1[2]})\n\n        # Sending new data clears selection\n        self.send_signal(self.widget.Inputs.data, self.data2)\n        self.assertEqual(len(self.widget.selected_vars), 0)\n\n        # Sending back the old data restores the selection\n        iris3 = self.data1.transform(\n            Domain([domain1[2], domain1[0], domain1[1]], domain1.class_var))\n        self.send_signal(self.widget.Inputs.data, iris3)\n        self.assertEqual(set(self.widget.selected_vars),\n                         {domain1[0], domain1[2]})\n\n    def test_settings_migration_to_ver21(self):\n        settings = {\n            'controlAreaVisible': True, 'savedWidgetGeometry': '',\n            '__version__': 1,\n            'context_settings': [\n                Context(\n                    values={'auto_commit': (True, -2),\n                            'color_var': ('iris', 101),\n                            'selected_rows': [1, 4],\n                            'sorting': ((1, 0), -2), '__version__': 1},\n                    attributes={'petal length': 2, 'petal width': 2,\n                                'sepal length': 2, 'sepal width': 2},\n                    metas={'iris': 1})]\n        }\n        widget = self.create_widget(OWFeatureStatistics,\n                                    stored_settings=settings)\n        self.send_signal(widget.Inputs.data, self.data1)\n        domain = self.data1.domain\n        self.assertEqual(widget.selected_vars, [domain[\"petal width\"],\n                                                domain[\"iris\"]])\n\n    def test_report(self):\n        self.send_signal(self.widget.Inputs.data, self.data1)\n\n        self.widget.report_button.click()\n        report_text = self.widget.report_html\n\n        self.assertIn(\"<table>\", report_text)\n        self.assertEqual(6, report_text.count(\"<tr>\"))  # header + 5 rows\n\n\nclass TestSummary(WidgetTest):\n    def setUp(self):\n        self.widget = self.create_widget(OWFeatureStatistics)\n        self.data = make_table(\n            [continuous_full, continuous_missing],\n            target=[rgb_full, rgb_missing], metas=[ints_full, ints_missing]\n        )\n        self.select_rows = partial(select_rows, widget=self.widget)\n\n    def test_summary(self):\n        \"\"\"Check if the status bar is updated when data is received\"\"\"\n        data = self.data\n        input_sum = self.widget.info.set_input_summary = Mock()\n        output_sum = self.widget.info.set_output_summary = Mock()\n\n        self.send_signal(self.widget.Inputs.data, data)\n        input_sum.assert_called_with(len(data), format_summary_details(data))\n\n        self.select_rows([0, 2])\n        self.widget.unconditional_commit()\n        output = self.get_output(self.widget.Outputs.reduced_data)\n        output_sum.assert_called_with(len(output), format_summary_details(output))\n\n        input_sum.reset_mock()\n        output_sum.reset_mock()\n        self.send_signal(self.widget.Inputs.data, None)\n        input_sum.assert_called_once()\n        self.assertEqual(input_sum.call_args[0][0].brief, \"\")\n        output_sum.assert_called_once()\n        self.assertEqual(output_sum.call_args[0][0].brief, \"\")\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\nrunning build_src\nbuild_src\nbuilding extension \"Orange.classification._simple_tree\" sources\nbuilding extension \"Orange.classification._tree_scorers\" sources\nbuilding extension \"Orange.data._valuecount\" sources\nbuilding extension \"Orange.data._contingency\" sources\nbuilding extension \"Orange.data._io\" sources\nbuilding extension \"Orange.data._variable\" sources\nbuilding extension \"Orange.distance._distance\" sources\nbuilding extension \"Orange.preprocess._discretize\" sources\nbuilding extension \"Orange.preprocess._relieff\" sources\nbuilding extension \"Orange.projection._som\" sources\nbuilding extension \"Orange.widgets.utils._grid_density\" sources\nbuild_src: building npy-pkg config files\ncustomize UnixCCompiler\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating /tmp/tmpeqrv_p2i/tmp\ncreating /tmp/tmpeqrv_p2i/tmp/tmpeqrv_p2i\ncompile options: '-MMD -MF /tmp/tmpeqrv_p2i/file.c.d -c'\ncc: /tmp/tmpeqrv_p2i/file.c\ncustomize UnixCCompiler using build_ext\ncustomize UnixCCompiler\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating /tmp/tmpqi6lfwzq/tmp\ncreating /tmp/tmpqi6lfwzq/tmp/tmpqi6lfwzq\ncompile options: '-MMD -MF /tmp/tmpqi6lfwzq/file.c.d -c'\ncc: /tmp/tmpqi6lfwzq/file.c\ncustomize UnixCCompiler using build_ext\nbuilding 'Orange.classification._simple_tree' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating build/temp.linux-x86_64-3.7/Orange\ncreating build/temp.linux-x86_64-3.7/Orange/classification\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/classification/_simple_tree.c\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 Orange/classification/_simple_tree.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.classification._tree_scorers' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/classification/_tree_scorers.c\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 Orange/classification/_tree_scorers.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._valuecount' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating build/temp.linux-x86_64-3.7/Orange/data\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/data/_valuecount.c\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 Orange/data/_valuecount.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._contingency' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/data/_contingency.c\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 Orange/data/_contingency.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._io' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/data/_io.c\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 Orange/data/_io.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.data._variable' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/data/_variable.c\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 Orange/data/_variable.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.distance._distance' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating build/temp.linux-x86_64-3.7/Orange/distance\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/distance/_distance.c\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 Orange/distance/_distance.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._discretize' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating build/temp.linux-x86_64-3.7/Orange/preprocess\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/preprocess/_discretize.c\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 Orange/preprocess/_discretize.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.preprocess._relieff' extension\ncompiling C++ sources\nC compiler: c++ -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\nc++: Orange/preprocess/_relieff.cpp\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 Orange/preprocess/_relieff.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.projection._som' extension\ncompiling C sources\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating build/temp.linux-x86_64-3.7/Orange/projection\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\ncc: Orange/projection/_som.c\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 Orange/projection/_som.cpython-37m-x86_64-linux-gnu.so\nbuilding 'Orange.widgets.utils._grid_density' extension\ncompiling C++ sources\nC compiler: c++ -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating build/temp.linux-x86_64-3.7/Orange/widgets\ncreating build/temp.linux-x86_64-3.7/Orange/widgets/utils\ncompile options: '-I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include -I/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/include -I/home/gcpuser/.local/share/uv/python/cpython-3.7.9-linux-x86_64-gnu/include/python3.7m -c'\nc++: Orange/widgets/utils/_grid_density.cpp\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 Orange/widgets/utils/_grid_density.cpython-37m-x86_64-linux-gnu.so\nrunning develop\nrunning build_scripts\nrunning egg_info\nrunning build_src\nbuild_src\nbuilding extension \"Orange.classification._simple_tree\" sources\nbuilding extension \"Orange.classification._tree_scorers\" sources\nbuilding extension \"Orange.data._valuecount\" sources\nbuilding extension \"Orange.data._contingency\" sources\nbuilding extension \"Orange.data._io\" sources\nbuilding extension \"Orange.data._variable\" sources\nbuilding extension \"Orange.distance._distance\" sources\nbuilding extension \"Orange.preprocess._discretize\" sources\nbuilding extension \"Orange.preprocess._relieff\" sources\nbuilding extension \"Orange.projection._som\" sources\nbuilding extension \"Orange.widgets.utils._grid_density\" sources\nbuild_src: building npy-pkg config files\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'\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'\nadding license file 'LICENSE'\nwriting manifest file 'Orange3.egg-info/SOURCES.txt'\nrunning build_ext\ncustomize UnixCCompiler\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating /tmp/tmpcwp39oe3/tmp\ncreating /tmp/tmpcwp39oe3/tmp/tmpcwp39oe3\ncompile options: '-MMD -MF /tmp/tmpcwp39oe3/file.c.d -c'\ncc: /tmp/tmpcwp39oe3/file.c\ncustomize UnixCCompiler using build_ext\ncustomize UnixCCompiler\nC compiler: cc -pthread -Wno-unused-result -Wsign-compare -Wunreachable-code -DNDEBUG -g -fwrapv -O3 -Wall -fPIC -I/tools/deps/include -I/tools/deps/include/ncursesw -fPIC\n\ncreating /tmp/tmp2sjg4apr/tmp\ncreating /tmp/tmp2sjg4apr/tmp/tmp2sjg4apr\ncompile options: '-MMD -MF /tmp/tmp2sjg4apr/file.c.d -c'\ncc: /tmp/tmp2sjg4apr/file.c\ncustomize UnixCCompiler using build_ext\nCreating /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/Orange3.egg-link (link to .)\nAdding Orange3 3.26.0.dev0+adad6e2 to easy-install.pth file\nInstalling orange-canvas script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/bin\n\nInstalled /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903\nProcessing dependencies for Orange3==3.26.0.dev0+adad6e2\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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/bin\nInstalling pip3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/bin\nInstalling pip3.10 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/bin\nInstalling f2py3 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/bin\nInstalling f2py3.7 script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages\nSearching for httpx==0.24.1\nBest match: httpx 0.24.1\nAdding httpx 0.24.1 to easy-install.pth file\nInstalling httpx script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/bin\nInstalling pyftmerge script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/bin\nInstalling pyftsubset script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/bin\nInstalling ttx script to /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/bin\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages\nSearching for httpcore==0.17.3\nBest match: httpcore 0.17.3\nAdding httpcore 0.17.3 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages\nSearching for anyio==3.7.1\nBest match: anyio 3.7.1\nAdding anyio 3.7.1 to easy-install.pth file\n\nUsing /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages\nFinished processing dependencies for Orange3==3.26.0.dev0+adad6e2\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 87ms\nPrepared 18 packages in 48ms\nInstalled 18 packages in 91ms\n + cython==0.29.37\n + exceptiongroup==1.2.2\n + importlib-metadata==6.7.0\n + iniconfig==2.0.0\n + numpy==1.17.5\n + packaging==24.0\n + pluggy==1.2.0\n + pyqt5==5.15.10\n + pyqt5-qt5==5.15.16\n + pyqt5-sip==12.13.0\n + pyqtwebengine==5.15.6\n + pyqtwebengine-qt5==5.15.16\n + pytest==7.4.4\n + setuptools==59.8.0\n + tomli==2.0.1\n + typing-extensions==4.7.1\n + wheel==0.42.0\n + zipp==3.15.0\nResolved 54 packages in 342ms\nInstalled 47 packages in 697ms\n + anyio==3.7.1\n + baycomp==1.0.3\n + bottleneck==1.4.0\n + certifi==2024.12.14\n + cffi==1.15.1\n + chardet==5.2.0\n + charset-normalizer==3.4.1\n + cryptography==44.0.0\n + cycler==0.11.0\n + et-xmlfile==1.1.0\n + fonttools==4.38.0\n + h11==0.14.0\n + httpcore==0.17.3\n + httpx==0.24.1\n + idna==3.10\n + importlib-resources==5.12.0\n + jaraco-classes==3.2.3\n + jeepney==0.8.0\n + joblib==1.3.2\n + keyring==24.1.1\n + keyrings-alt==4.2.0\n + kiwisolver==1.4.5\n + matplotlib==3.5.3\n + more-itertools==9.1.0\n + networkx==2.6.3\n + openpyxl==3.1.3\n + opentsne==1.0.1\n + pandas==1.3.5\n + pillow==9.5.0\n + pip==24.0\n + pycparser==2.21\n + pyparsing==3.1.4\n + python-dateutil==2.9.0.post0\n + python-louvain==0.16\n + pytz==2024.2\n + pyyaml==6.0.1\n + requests==2.31.0\n + scikit-learn==1.0.2\n + scipy==1.7.3\n + secretstorage==3.3.3\n + serverfiles==0.3.1\n + six==1.17.0\n + sniffio==1.3.1\n + threadpoolctl==3.1.0\n + urllib3==2.0.7\n + xlrd==2.0.1\n + xlsxwriter==3.2.0\nResolved 34 packages in 77ms\nInstalled 13 packages in 258ms\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 20ms\n + psycopg2==2.9.9\n + pymssql==2.3.2\nAudited 1 package in 28ms\nAudited 2 packages in 24ms\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      |         ^~~~~~~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/classification/_tree_scorers.c:567:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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:3115:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 3115 |   __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:4619:27: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 4619 |   __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:5396:43: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5396 |           for (__pyx_t_22 = 0; __pyx_t_22 < __pyx_t_21; __pyx_t_22+=1) {\n      |                                           ^\nOrange/classification/_tree_scorers.c:5517:39: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5517 |           __pyx_t_27 = ((__pyx_v_left >= __pyx_v_min_leaf) != 0);\n      |                                       ^~\nOrange/classification/_tree_scorers.c:5523:53: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 5523 |           __pyx_t_27 = (((__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:7051:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7051 |         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:7741:41: warning: comparison of integer expressions of different signedness: ‘unsigned int’ and ‘int’ [-Wsign-compare]\n 7741 |         for (__pyx_t_11 = 0; __pyx_t_11 < __pyx_t_10; __pyx_t_11+=1) {\n      |                                         ^\nOrange/classification/_tree_scorers.c:7801:143: warning: comparison of integer expressions of different signedness: ‘__pyx_t_5numpy_int32_t’ {aka ‘int’} and ‘unsigned int’ [-Wsign-compare]\n 7801 |               __pyx_t_17 = (((*((__pyx_t_5numpy_int32_t *) ( /* dim=0 */ (__pyx_v_indices.data + __pyx_t_18 * __pyx_v_indices.strides[0]) ))) != __pyx_v_i) != 0);\n      |                                                                                                                                               ^~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/data/_valuecount.c:567:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/data/_contingency.c:598:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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:3881:24: warning: ‘__pyx_v_decs’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3881 |           __pyx_v_decs = (__pyx_v_decs / 10.0);\n      |           ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:4070:24: warning: ‘__pyx_v_value’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4070 |           __pyx_t_10 = PyFloat_FromDouble(__pyx_v_value); if (unlikely(!__pyx_t_10)) __PYX_ERR(0, 221, __pyx_L5_error)\n      |                        ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:480:40: warning: ‘__pyx_v_attr_index’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n  480 |   #define PyInt_FromLong               PyLong_FromLong\n      |                                        ^~~~~~~~~~~~~~~\nOrange/data/_io.c:1301:7: note: ‘__pyx_v_attr_index’ was declared here\n 1301 |   int __pyx_v_attr_index;\n      |       ^~~~~~~~~~~~~~~~~~\nOrange/data/_io.c:2935:25: warning: ‘__pyx_v_atomp’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 2935 |           __pyx_v_atomp = (__pyx_v_atomp + 1);\n      |           ~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~\nIn file included from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/distance/_distance.c:598:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/preprocess/_discretize.c:567:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/preprocess/_relieff.cpp:627:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarraytypes.h:1830,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,\n                 from /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/numpy/core/include/numpy/arrayobject.h:4,\n                 from Orange/projection/_som.c:598:\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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:3655:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3655 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:3654:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3654 |   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:4547:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4547 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_38, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_39, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4114:7: note: ‘__pyx_v_win_y’ was declared here\n 4114 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:4536:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4536 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_36, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_37, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:4113:7: note: ‘__pyx_v_win_x’ was declared here\n 4113 |   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:4746:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4746 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:4745:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 4745 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_3update’:\nOrange/projection/_som.c:3141:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 3141 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c: In function ‘__pyx_pw_6Orange_10projection_4_som_1get_winners’:\nOrange/projection/_som.c:2964:223: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 2964 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_27, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_28, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_y;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:2665:7: note: ‘__pyx_v_win_y’ was declared here\n 2665 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:2953:223: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 2953 |           *__Pyx_BufPtrStrided2d(__pyx_t_5numpy_int16_t *, __pyx_pybuffernd_winners.rcbuffer->pybuffer.buf, __pyx_t_25, __pyx_pybuffernd_winners.diminfo[0].strides, __pyx_t_26, __pyx_pybuffernd_winners.diminfo[1].strides) = __pyx_v_win_x;\n      |           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~\nOrange/projection/_som.c:2664:7: note: ‘__pyx_v_win_x’ was declared here\n 2664 |   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:5515:7: warning: ‘__pyx_v_win_y’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5515 |   int __pyx_v_win_y;\n      |       ^~~~~~~~~~~~~\nOrange/projection/_som.c:5514:7: warning: ‘__pyx_v_win_x’ may be used uninitialized in this function [-Wmaybe-uninitialized]\n 5514 |   int __pyx_v_win_x;\n      |       ^~~~~~~~~~~~~\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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,\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.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,\n/home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903/.venv/lib/python3.7/site-packages/setuptools/command/egg_info.py:628: SetuptoolsDeprecationWarning: Custom 'build_py' does not implement 'get_data_files_without_manifest'.\nPlease extend command classes from setuptools instead of distutils.\n  SetuptoolsDeprecationWarning\n",
    "new_commit_res_code": 0,
    "new_commit_res_stdout": "============================= test session starts ==============================\nplatform linux -- Python 3.7.9, pytest-7.4.4, pluggy-1.2.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/orange3_adad6e21aeff5b099246e243aa6cb9adc5bdf903\nplugins: anyio-3.7.1\ncollected 40 items\n\nr2e_tests/test_1.py sss.....................................             [100%]\n\n==================================== PASSES ====================================\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_does_not_crash_on_data_removal\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_does_not_crash_on_empty_domain\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_image_export\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_minimum_size\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_on_data_with_continuous_values_all_the_same\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_on_data_with_discrete_values_all_the_same\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_on_data_with_missing_continuous_values\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_on_data_with_missing_discrete_values\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_on_data_with_no_missing_values\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_on_data_with_no_missing_values_full_domain\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_on_edge_case_datasets\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_runs_on_iris\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_switching_to_dataset_with_no_target_var\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_switching_to_dataset_with_target_var\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_changing_data_updates_ouput\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_changing_data_updates_output_with_autocommit\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_clearing_selection_sends_none_to_output\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_image_export\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_minimum_size\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_sends_all_samples_to_output\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_sends_multiple_attribute_table_to_output\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_sends_multiple_var_types_table_to_output\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_sends_single_attribute_table_to_output\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_sends_single_class_var_table_to_output\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_sends_single_meta_table_to_output\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsUI::test_image_export\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsUI::test_minimum_size\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsUI::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsUI::test_report\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsUI::test_restores_previous_selection\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsUI::test_settings_migration_to_ver21\nPASSED r2e_tests/test_1.py::TestSummary::test_image_export\nPASSED r2e_tests/test_1.py::TestSummary::test_minimum_size\nPASSED r2e_tests/test_1.py::TestSummary::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestSummary::test_summary\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:244: image exporting not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:238: minimum size not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:250: msg base class not tested as .widget was not set\n======================== 37 passed, 3 skipped in 7.51s =========================\nWidgets left: 0    Max widgets: 4537 \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_adad6e21aeff5b099246e243aa6cb9adc5bdf903\nplugins: anyio-3.7.1\ncollected 40 items\n\nr2e_tests/test_1.py sss..............................F......             [100%]\n\n=================================== FAILURES ===================================\n_____________________ TestFeatureStatisticsUI.test_report ______________________\n\nself = <r2e_tests.test_1.TestFeatureStatisticsUI testMethod=test_report>\n\n    def test_report(self):\n        self.send_signal(self.widget.Inputs.data, self.data1)\n    \n        self.widget.report_button.click()\n        report_text = self.widget.report_html\n    \n>       self.assertIn(\"<table>\", report_text)\nE       AssertionError: '<table>' not found in '<section class=\"section\"><h1>Feature Statistics <span class=\\'timestamp\\'>Thu Jan 09 25, 11:36:02</h1><div class=\"content\">\\n</div></section>\\n\\n'\n\nr2e_tests/test_1.py:502: AssertionError\n==================================== PASSES ====================================\n=========================== short test summary info ============================\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_does_not_crash_on_data_removal\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_does_not_crash_on_empty_domain\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_image_export\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_minimum_size\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_on_data_with_continuous_values_all_the_same\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_on_data_with_discrete_values_all_the_same\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_on_data_with_missing_continuous_values\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_on_data_with_missing_discrete_values\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_on_data_with_no_missing_values\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_on_data_with_no_missing_values_full_domain\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_on_edge_case_datasets\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_runs_on_iris\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_switching_to_dataset_with_no_target_var\nPASSED r2e_tests/test_1.py::TestVariousDataSets::test_switching_to_dataset_with_target_var\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_changing_data_updates_ouput\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_changing_data_updates_output_with_autocommit\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_clearing_selection_sends_none_to_output\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_image_export\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_minimum_size\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_sends_all_samples_to_output\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_sends_multiple_attribute_table_to_output\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_sends_multiple_var_types_table_to_output\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_sends_single_attribute_table_to_output\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_sends_single_class_var_table_to_output\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsOutputs::test_sends_single_meta_table_to_output\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsUI::test_image_export\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsUI::test_minimum_size\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsUI::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsUI::test_restores_previous_selection\nPASSED r2e_tests/test_1.py::TestFeatureStatisticsUI::test_settings_migration_to_ver21\nPASSED r2e_tests/test_1.py::TestSummary::test_image_export\nPASSED r2e_tests/test_1.py::TestSummary::test_minimum_size\nPASSED r2e_tests/test_1.py::TestSummary::test_msg_base_class\nPASSED r2e_tests/test_1.py::TestSummary::test_summary\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:244: image exporting not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:238: minimum size not tested as .widget was not set\nSKIPPED [1] .venv/lib/python3.7/site-packages/orangewidget/tests/base.py:250: msg base class not tested as .widget was not set\nFAILED r2e_tests/test_1.py::TestFeatureStatisticsUI::test_report - AssertionE...\n=================== 1 failed, 36 passed, 3 skipped in 5.36s ====================\nWidgets left: 0    Max widgets: 4537 \n\n",
    "old_commit_res_stderr": ""
}
Orange/widgets/data/owfeaturestatistics.py Orange/widgets/data/tests/test_owfeaturestatistics.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
**Analysis:**

- **Key Commit Details:**
  - **Commit Message:** "[ENH] Feature statistics report."
  - **Changed Function:** `send_report` now calls `self.report_table(view)` instead of just `pass`.
  
- **Test Results:**
  - **Old Commit:** The `test_report` failed with an `AssertionError` stating that `'<table>'` was not found in the generated report.
  - **New Commit:** The `test_report` passes, indicating that the issue has been resolved.

- **Main Problem Highlighted:**
  - The feature statistics report was not generating the expected HTML table, resulting in an empty report section.

- **Expected Behavior:**
  - When generating a feature statistics report, the HTML output should include a `<table>` element displaying the relevant statistics.

- **Actual Behavior:**
  - The generated report lacked the `<table>` element, leading to an empty content section without any displayed statistics.

- **Issue Description Strategy:**
  - Clearly state that the feature statistics report does not render the expected HTML table.
  - Provide a concise example illustrating how triggering the report results in missing table elements.
  - Describe the discrepancy between expected and actual behaviors without revealing the solution.

**Drafted GitHub Issue:**

[ISSUE]
**Title:** Feature Statistics Report Does Not Display HTML Table

**Description:**
When generating a feature statistics report, the output does not include an HTML `<table>` element. As a result, the report appears empty without displaying any of the statistical data.

**Example Scenario:**
1. Load the dataset "iris" into the Feature Statistics widget.
2. Click the "Generate Report" button.
3. Inspect the generated report.

**Buggy Code Illustration:**
```python
def test_report_generation():
    widget.load_data("iris")
    widget.click_generate_report()
    report_html = widget.get_report_html()
    # Expected to find '<table>' in report_html
```

**Expected Behavior:**
The generated report should contain an HTML `<table>` element that displays the feature statistics, providing a clear and structured view of the data.

**Actual Behavior:**
The report lacks the `<table>` element, resulting in an empty report section without any visible statistics.

**Impact:**
This issue prevents users from viewing the feature statistics in a structured table format, hindering data analysis and review.

[/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/owfeaturestatistics.py
"""

TODO:
  - Sorting by standard deviation: Use coefficient of variation (std/mean)
    or quartile coefficient of dispersion (Q3 - Q1) / (Q3 + Q1)
  - Standard deviation for nominal: try out Variation ratio (1 - n_mode/N)
"""

import datetime
from enum import IntEnum
from itertools import chain
from typing import Any, Optional, Tuple, List

import numpy as np
import scipy.stats as ss
import scipy.sparse as sp
from AnyQt.QtCore import Qt, QSize, QRectF, QModelIndex, pyqtSlot, \
    QRegExp, QItemSelection, QItemSelectionRange, QItemSelectionModel
from AnyQt.QtGui import QPainter, QColor
from AnyQt.QtWidgets import QStyledItemDelegate, QGraphicsScene, QTableView, \
    QHeaderView, QStyle, QStyleOptionViewItem

import Orange.statistics.util as ut
from Orange.data import Table, StringVariable, DiscreteVariable, \
    ContinuousVariable, TimeVariable, Domain, Variable
from Orange.widgets import widget, gui
from Orange.widgets.data.utils.histogram import Histogram
from Orange.widgets.settings import Setting, ContextSetting, \
    DomainContextHandler
from Orange.widgets.utils.itemmodels import DomainModel, AbstractSortTableModel
from Orange.widgets.utils.signals import Input, Output
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.utils.state_summary import format_summary_details


def _categorical_entropy(x):
    """Compute the entropy of a dense/sparse matrix, column-wise. Assuming
    categorical values."""
    p = [ut.bincount(row)[0] for row in x.T]
    p = [pk / np.sum(pk) for pk in p]
    return np.fromiter((ss.entropy(pk) for pk in p), dtype=np.float64)


def format_time_diff(start, end, round_up_after=2):
    """Return an approximate human readable time difference between two dates.

    Parameters
    ----------
    start : int
        Unix timestamp
    end : int
        Unix timestamp
    round_up_after : int
        The number of time units before we round up to the next, larger time
        unit e.g. setting to 2 will allow up to 2 days worth of hours to be
        shown, after that the difference is shown in days. Or put another way
        we will show from 1-48 hours before switching to days.

    Returns
    -------
    str

    """
    start = datetime.datetime.fromtimestamp(start)
    end = datetime.datetime.fromtimestamp(end)
    diff = abs(end - start)  # type: datetime.timedelta

    # Get the different resolutions
    seconds = diff.total_seconds()
    minutes = seconds // 60
    hours = minutes // 60
    days = diff.days
    weeks = days // 7
    months = (end.year - start.year) * 12 + end.month - start.month
    years = months // 12

    # Check which resolution is most appropriate
    if years >= round_up_after:
        return '~%d years' % years
    elif months >= round_up_after:
        return '~%d months' % months
    elif weeks >= round_up_after:
        return '~%d weeks' % weeks
    elif days >= round_up_after:
        return '~%d days' % days
    elif hours >= round_up_after:
        return '~%d hours' % hours
    elif minutes >= round_up_after:
        return '~%d minutes' % minutes
    else:
        return '%d seconds' % seconds


class FeatureStatisticsTableModel(AbstractSortTableModel):
    CLASS_VAR, META, ATTRIBUTE = range(3)
    COLOR_FOR_ROLE = {
        CLASS_VAR: QColor(160, 160, 160),
        META: QColor(220, 220, 200),
        ATTRIBUTE: QColor(255, 255, 255),
    }

    HIDDEN_VAR_TYPES = (StringVariable,)

    class Columns(IntEnum):
        ICON, NAME, DISTRIBUTION, CENTER, DISPERSION, MIN, MAX, MISSING = range(8)

        @property
        def name(self):
            return {self.ICON: '',
                    self.NAME: 'Name',
                    self.DISTRIBUTION: 'Distribution',
                    self.CENTER: 'Center',
                    self.DISPERSION: 'Dispersion',
                    self.MIN: 'Min.',
                    self.MAX: 'Max.',
                    self.MISSING: 'Missing',
                    }[self.value]

        @property
        def index(self):
            return self.value

        @classmethod
        def from_index(cls, index):
            return cls(index)

    def __init__(self, data=None, parent=None):
        """

        Parameters
        ----------
        data : Optional[Table]
        parent : Optional[QWidget]

        """
        super().__init__(parent)

        self.table = None  # type: Optional[Table]
        self.domain = None  # type: Optional[Domain]
        self.target_var = None  # type: Optional[Variable]
        self.n_attributes = self.n_instances = 0

        self.__attributes = self.__class_vars = self.__metas = None
        self.__distributions_cache = {}

        no_data = np.array([])
        self._variable_types = self._variable_names = no_data
        self._min = self._max = self._center = no_data
        self._dispersion = no_data
        self._missing = no_data
        # Clear model initially to set default values
        self.clear()

        self.set_data(data)

    def set_data(self, data):
        if data is None:
            self.clear()
            return

        self.beginResetModel()
        self.table = data
        self.domain = domain = data.domain
        self.target_var = None

        self.__attributes = self.__filter_attributes(domain.attributes, self.table.X)
        # We disable pylint warning because the `Y` property squeezes vectors,
        # while we need a 2d array, which `_Y` provides
        self.__class_vars = self.__filter_attributes(domain.class_vars, self.table._Y)  # pylint: disable=protected-access
        self.__metas = self.__filter_attributes(domain.metas, self.table.metas)

        self.n_attributes = len(self.variables)
        self.n_instances = len(data)

        self.__distributions_cache = {}
        self.__compute_statistics()
        self.endResetModel()

    def clear(self):
        self.beginResetModel()
        self.table = self.domain = self.target_var = None
        self.n_attributes = self.n_instances = 0
        self.__attributes = (np.array([]), np.array([]))
        self.__class_vars = (np.array([]), np.array([]))
        self.__metas = (np.array([]), np.array([]))
        self.__distributions_cache.clear()
        self.endResetModel()

    @property
    def variables(self):
        matrices = [self.__attributes[0], self.__class_vars[0], self.__metas[0]]
        if not any(m.size for m in matrices):
            return []
        return np.hstack(matrices)

    @staticmethod
    def _attr_indices(attrs):
        # type: (List) -> Tuple[List[int], List[int], List[int], List[int]]
        """Get the indices of different attribute types eg. discrete."""
        disc_var_idx = [i for i, attr in enumerate(attrs) if isinstance(attr, DiscreteVariable)]
        cont_var_idx = [i for i, attr in enumerate(attrs)
                        if isinstance(attr, ContinuousVariable)
                        and not isinstance(attr, TimeVariable)]
        time_var_idx = [i for i, attr in enumerate(attrs) if isinstance(attr, TimeVariable)]
        string_var_idx = [i for i, attr in enumerate(attrs) if isinstance(attr, StringVariable)]
        return disc_var_idx, cont_var_idx, time_var_idx, string_var_idx

    def __filter_attributes(self, attributes, matrix):
        """Filter out variables which shouldn't be visualized."""
        attributes, matrix = np.asarray(attributes), matrix
        mask = [idx for idx, attr in enumerate(attributes)
                if not isinstance(attr, self.HIDDEN_VAR_TYPES)]
        return attributes[mask], matrix[:, mask]

    def __compute_statistics(self):
        # Since data matrices can of mixed sparsity, we need to compute
        # attributes separately for each of them.
        matrices = [self.__attributes, self.__class_vars, self.__metas]
        # Filter out any matrices with size 0
        matrices = list(filter(lambda tup: tup[1].size, matrices))

        self._variable_types = np.array([type(var) for var in self.variables])
        self._variable_names = np.array([var.name.lower() for var in self.variables])
        self._min = self.__compute_stat(
            matrices,
            discrete_f=lambda x: ut.nanmin(x, axis=0),
            continuous_f=lambda x: ut.nanmin(x, axis=0),
            time_f=lambda x: ut.nanmin(x, axis=0),
        )
        self._dispersion = self.__compute_stat(
            matrices,
            discrete_f=_categorical_entropy,
            continuous_f=lambda x: np.sqrt(ut.nanvar(x, axis=0)) / ut.nanmean(x, axis=0),
        )
        self._missing = self.__compute_stat(
            matrices,
            discrete_f=lambda x: ut.countnans(x, axis=0),
            continuous_f=lambda x: ut.countnans(x, axis=0),
            string_f=lambda x: (x == StringVariable.Unknown).sum(axis=0),
            time_f=lambda x: ut.countnans(x, axis=0),
        )
        self._max = self.__compute_stat(
            matrices,
            discrete_f=lambda x: ut.nanmax(x, axis=0),
            continuous_f=lambda x: ut.nanmax(x, axis=0),
            time_f=lambda x: ut.nanmax(x, axis=0),
        )

        # Since scipy apparently can't do mode on sparse matrices, cast it to
        # dense. This can be very inefficient for large matrices, and should
        # be changed
        def __mode(x, *args, **kwargs):
            if sp.issparse(x):
                x = x.todense(order="C")
            # return ss.mode(x, *args, **kwargs)[0]
            return ut.nanmode(x, *args, **kwargs)[0]  # Temporary replacement for scipy

        self._center = self.__compute_stat(
            matrices,
            discrete_f=lambda x: __mode(x, axis=0),
            continuous_f=lambda x: ut.nanmean(x, axis=0),
            time_f=lambda x: ut.nanmean(x, axis=0),
        )

    def get_statistics_matrix(self, variables=None, return_labels=False):
        """Get the numeric computed statistics in a single matrix. Optionally,
        we can specify for which variables we want the stats. Also, we can get
        the string column names as labels if desired.

        Parameters
        ----------
        variables : Iterable[Union[Variable, int, str]]
            Return statistics for only the variables specified. Accepts all
            formats supported by `domain.index`
        return_labels : bool
            In addition to the statistics matrix, also return string labels for
            the columns of the matrix e.g. 'Mean' or 'Dispersion', as specified
            in `Columns`.

        Returns
        -------
        Union[Tuple[List[str], np.ndarray], np.ndarray]

        """
        if self.table is None:
            return np.atleast_2d([])

        # If a list of variables is given, select only corresponding stats
        # variables can be a list or array, pylint: disable=len-as-condition
        if variables is not None and len(variables) != 0:
            indices = [self.domain.index(var) for var in variables]
        else:
            indices = ...

        matrix = np.vstack((
            self._center[indices], self._dispersion[indices],
            self._min[indices], self._max[indices], self._missing[indices],
        )).T

        # Return string labels for the returned matrix columns e.g. 'Mean',
        # 'Dispersion' if requested
        if return_labels:
            labels = [self.Columns.CENTER.name, self.Columns.DISPERSION.name,
                      self.Columns.MIN.name, self.Columns.MAX.name,
                      self.Columns.MISSING.name]
            return labels, matrix

        return matrix

    def __compute_stat(self, matrices, discrete_f=None, continuous_f=None,
                       time_f=None, string_f=None, default_val=np.nan):
        """Apply functions to appropriate variable types. The default value is
        returned if there is no function defined for specific variable types.
        """
        if not matrices:
            return np.array([])

        def _to_float(data):
            if not np.issubdtype(data.dtype, np.number):
                data = data.astype(np.float64)
            return data

        def _to_object(data):
            if data.dtype is not np.object:
                data = data.astype(np.object)
            return data

        results = []
        for variables, x in matrices:
            result = np.full(len(variables), default_val)

            # While the following caching and checks are messy, the indexing
            # turns out to be a bottleneck for large datasets, so a single
            # indexing operation improves performance
            disc_idx, cont_idx, time_idx, str_idx = self._attr_indices(variables)
            if discrete_f:
                x_ = x[:, disc_idx]
                if x_.size:
                    result[disc_idx] = discrete_f(_to_float(x_))
            if continuous_f:
                x_ = x[:, cont_idx]
                if x_.size:
                    result[cont_idx] = continuous_f(_to_float(x_))
            if time_f:
                x_ = x[:, time_idx]
                if x_.size:
                    result[time_idx] = time_f(_to_float(x_))
            if string_f:
                x_ = x[:, str_idx]
                if x_.size:
                    result[str_idx] = string_f(_to_object(x_))

            results.append(result)

        return np.hstack(results)

    def sortColumnData(self, column):
        """Prepare the arrays with which we will sort the rows. If we want to
        sort based on a single value e.g. the name, return a 1d array.
        Sometimes we may want to sort by multiple criteria, comparing
        continuous variances with discrete entropies makes no sense, so we want
        to group those variable types together.
        """
        # Prepare indices for variable types so we can group them together
        order = [DiscreteVariable, ContinuousVariable, TimeVariable, StringVariable]
        mapping = {var: idx for idx, var in enumerate(order)}
        vmapping = np.vectorize(mapping.__getitem__)
        var_types_indices = vmapping(self._variable_types)

        # Store the variable name sorted indices so we can pass a default
        # order when sorting by multiple keys
        var_name_indices = np.argsort(self._variable_names)

        # Prepare vartype indices so ready when needed
        disc_idx, _, time_idx, str_idx = self._attr_indices(self.variables)

        # Sort by: (type)
        if column == self.Columns.ICON:
            return var_types_indices
        # Sort by: (name)
        elif column == self.Columns.NAME:
            # We use `_variable_names` here and not the indices because the
            # last (or single) row is actually sorted and we don't want to sort
            # the indices
            return self._variable_names
        # Sort by: (None)
        elif column == self.Columns.DISTRIBUTION:
            return np.ones_like(var_types_indices)
        # Sort by: (type, center)
        elif column == self.Columns.CENTER:
            # Sorting discrete or string values by mean makes no sense
            vals = np.array(self._center)
            vals[disc_idx] = var_name_indices[disc_idx]
            vals[str_idx] = var_name_indices[str_idx]
            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T
        # Sort by: (type, dispersion)
        elif column == self.Columns.DISPERSION:
            # Sort time variables by their dispersion, which is not stored in
            # the dispersion array
            vals = np.array(self._dispersion)
            vals[time_idx] = self._max[time_idx] - self._min[time_idx]
            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T
        # Sort by: (type, min)
        elif column == self.Columns.MIN:
            # Sorting discrete or string values by min makes no sense
            vals = np.array(self._min)
            vals[disc_idx] = var_name_indices[disc_idx]
            vals[str_idx] = var_name_indices[str_idx]
            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T
        # Sort by: (type, max)
        elif column == self.Columns.MAX:
            # Sorting discrete or string values by min makes no sense
            vals = np.array(self._max)
            vals[disc_idx] = var_name_indices[disc_idx]
            vals[str_idx] = var_name_indices[str_idx]
            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T
        # Sort by: (missing)
        elif column == self.Columns.MISSING:
            return self._missing

        return None

    def _sortColumnData(self, column):
        """Allow sorting with 2d arrays."""
        data = np.asarray(self.sortColumnData(column))
        data = data[self.mapToSourceRows(Ellipsis)]

        assert data.ndim <= 2, 'Data should be at most 2-dimensional'
        return data

    def _argsortData(self, data, order):
        if data.ndim == 1:
            indices = np.argsort(data, kind='mergesort')
            if order == Qt.DescendingOrder:
                indices = indices[::-1]
            # Always sort NaNs last
            if np.issubdtype(data.dtype, np.number):
                indices = np.roll(indices, -np.isnan(data).sum())
        else:
            assert np.issubdtype(data.dtype, np.number), \
                'We do not deal with non numeric values in sorting by ' \
                'multiple values'
            if order == Qt.DescendingOrder:
                data[:, -1] = -data[:, -1]

            # In order to make sure NaNs always appear at the end, insert a
            # indicator whether NaN or not. Note that the data array must
            # contain an empty column of zeros at index -2 since inserting an
            # extra column after the fact can result in a MemoryError for data
            # with a large amount of variables
            assert np.all(data[:, -2] == 0), \
                'Add an empty column of zeros at index -2 to accomodate NaNs'
            np.isnan(data[:, -1], out=data[:, -2])

            indices = np.lexsort(np.flip(data.T, axis=0))

        return indices

    def headerData(self, section, orientation, role):
        # type: (int, Qt.Orientation, Qt.ItemDataRole) -> Any
        if orientation == Qt.Horizontal:
            if role == Qt.DisplayRole:
                return self.Columns.from_index(section).name

        return None

    def data(self, index, role):
        # type: (QModelIndex, Qt.ItemDataRole) -> Any
        def background():
            if attribute in self.domain.attributes:
                return self.COLOR_FOR_ROLE[self.ATTRIBUTE]
            if attribute in self.domain.metas:
                return self.COLOR_FOR_ROLE[self.META]
            if attribute in self.domain.class_vars:
                return self.COLOR_FOR_ROLE[self.CLASS_VAR]
            return None

        def text_alignment():
            if column == self.Columns.NAME:
                return Qt.AlignLeft | Qt.AlignVCenter
            return Qt.AlignRight | Qt.AlignVCenter

        def decoration():
            if column == self.Columns.ICON:
                return gui.attributeIconDict[attribute]
            return None

        def display():
            # pylint: disable=too-many-branches
            def render_value(value):
                return "" if np.isnan(value) else attribute.str_val(value)

            if column == self.Columns.NAME:
                return attribute.name
            elif column == self.Columns.DISTRIBUTION:
                if isinstance(attribute,
                              (DiscreteVariable, ContinuousVariable)):
                    if row not in self.__distributions_cache:
                        scene = QGraphicsScene(parent=self)
                        histogram = Histogram(
                            data=self.table,
                            variable=attribute,
                            color_attribute=self.target_var,
                            border=(0, 0, 2, 0),
                            border_color='#ccc',
                        )
                        scene.addItem(histogram)
                        self.__distributions_cache[row] = scene
                    return self.__distributions_cache[row]
            elif column == self.Columns.CENTER:
                return render_value(self._center[row])
            elif column == self.Columns.DISPERSION:
                if isinstance(attribute, TimeVariable):
                    return format_time_diff(self._min[row], self._max[row])
                elif isinstance(attribute, DiscreteVariable):
                    return "%.3g" % self._dispersion[row]
                else:
                    return render_value(self._dispersion[row])
            elif column == self.Columns.MIN:
                if not isinstance(attribute, DiscreteVariable) \
                        or attribute.ordered:
                    return render_value(self._min[row])
            elif column == self.Columns.MAX:
                if not isinstance(attribute, DiscreteVariable) \
                        or attribute.ordered:
                    return render_value(self._max[row])
            elif column == self.Columns.MISSING:
                return '%d (%d%%)' % (
                    self._missing[row],
                    100 * self._missing[row] / self.n_instances
                )
            return None

        roles = {Qt.BackgroundRole: background,
                 Qt.TextAlignmentRole: text_alignment,
                 Qt.DecorationRole: decoration,
                 Qt.DisplayRole: display}

        if not index.isValid() or role not in roles:
            return None

        row, column = self.mapToSourceRows(index.row()), index.column()
        # Make sure we're not out of range
        if not 0 <= row <= self.n_attributes:
            return None

        attribute = self.variables[row]
        return roles[role]()

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

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

    def set_target_var(self, variable):
        self.target_var = variable
        self.__distributions_cache.clear()
        start_idx = self.index(0, self.Columns.DISTRIBUTION)
        end_idx = self.index(self.rowCount(), self.Columns.DISTRIBUTION)
        self.dataChanged.emit(start_idx, end_idx)


class FeatureStatisticsTableView(QTableView):
    HISTOGRAM_ASPECT_RATIO = (7, 3)
    MINIMUM_HISTOGRAM_HEIGHT = 50
    MAXIMUM_HISTOGRAM_HEIGHT = 80

    def __init__(self, model, parent=None, **kwargs):
        super().__init__(
            parent=parent,
            showGrid=False,
            cornerButtonEnabled=False,
            sortingEnabled=True,
            selectionBehavior=QTableView.SelectRows,
            selectionMode=QTableView.ExtendedSelection,
            horizontalScrollMode=QTableView.ScrollPerPixel,
            verticalScrollMode=QTableView.ScrollPerPixel,
            **kwargs
        )
        self.setModel(model)

        hheader = self.horizontalHeader()
        hheader.setStretchLastSection(False)
        # Contents precision specifies how many rows should be taken into
        # account when computing the sizes, 0 being the visible rows. This is
        # crucial, since otherwise the `ResizeToContents` section resize mode
        # would call `sizeHint` on every single row in the data before first
        # render. However this, this cannot be used here, since this only
        # appears to work properly when the widget is actually shown. When the
        # widget is not shown, size `sizeHint` is called on every row.
        hheader.setResizeContentsPrecision(5)
        # Set a nice default size so that headers have some space around titles
        hheader.setDefaultSectionSize(100)
        # Set individual column behaviour in `set_data` since the logical
        # indices must be valid in the model, which requires data.
        hheader.setSectionResizeMode(QHeaderView.Interactive)

        columns = model.Columns
        hheader.setSectionResizeMode(columns.ICON.index, QHeaderView.ResizeToContents)
        hheader.setSectionResizeMode(columns.DISTRIBUTION.index, QHeaderView.Stretch)

        vheader = self.verticalHeader()
        vheader.setVisible(False)
        vheader.setSectionResizeMode(QHeaderView.Fixed)
        hheader.sectionResized.connect(self.bind_histogram_aspect_ratio)
        # TODO: This shifts the scrollarea a bit down when opening widget
        # hheader.sectionResized.connect(self.keep_row_centered)

        self.setItemDelegate(NoFocusRectDelegate(parent=self))
        self.setItemDelegateForColumn(
            FeatureStatisticsTableModel.Columns.DISTRIBUTION,
            DistributionDelegate(parent=self),
        )

    def bind_histogram_aspect_ratio(self, logical_index, _, new_size):
        """Force the horizontal and vertical header to maintain the defined
        aspect ratio specified for the histogram."""
        # Prevent function being exectued more than once per resize
        if logical_index is not self.model().Columns.DISTRIBUTION.index:
            return
        ratio_width, ratio_height = self.HISTOGRAM_ASPECT_RATIO
        unit_width = new_size / ratio_width
        new_height = unit_width * ratio_height
        effective_height = max(new_height, self.MINIMUM_HISTOGRAM_HEIGHT)
        effective_height = min(effective_height, self.MAXIMUM_HISTOGRAM_HEIGHT)
        self.verticalHeader().setDefaultSectionSize(effective_height)

    def keep_row_centered(self, logical_index, _1, _2):
        """When resizing the widget when scrolled further down, the
        positions of rows changes. Obviously, the user resized in order to
        better see the row of interest. This keeps that row centered."""
        # TODO: This does not work properly
        # Prevent function being exectued more than once per resize
        if logical_index is not self.model().Columns.DISTRIBUTION.index:
            return
        top_row = self.indexAt(self.rect().topLeft()).row()
        bottom_row = self.indexAt(self.rect().bottomLeft()).row()
        middle_row = top_row + (bottom_row - top_row) // 2
        self.scrollTo(self.model().index(middle_row, 0), QTableView.PositionAtCenter)


class NoFocusRectDelegate(QStyledItemDelegate):
    """Removes the light blue background and border on a focused item."""

    def paint(self, painter, option, index):
        # type: (QPainter, QStyleOptionViewItem, QModelIndex) -> None
        option.state &= ~QStyle.State_HasFocus
        super().paint(painter, option, index)


class DistributionDelegate(QStyledItemDelegate):
    def paint(self, painter, option, index):
        # type: (QPainter, QStyleOptionViewItem, QModelIndex) -> None
        scene = index.data(Qt.DisplayRole)  # type: Optional[QGraphicsScene]
        if scene is None:
            return super().paint(painter, option, index)

        painter.setRenderHint(QPainter.Antialiasing)

        if option.state & QStyle.State_Selected:
            background_color = option.palette.highlight()
        else:
            background_color = index.data(Qt.BackgroundRole)
        if background_color is not None:
            scene.setBackgroundBrush(background_color)

        scene.render(painter, target=QRectF(option.rect), mode=Qt.IgnoreAspectRatio)

        # pylint complains about inconsistent return statements
        return None


class OWFeatureStatistics(widget.OWWidget):
    name = 'Feature Statistics'
    description = 'Show basic statistics for data features.'
    icon = 'icons/FeatureStatistics.svg'

    class Inputs:
        data = Input('Data', Table, default=True)

    class Outputs:
        reduced_data = Output('Reduced Data', Table, default=True)
        statistics = Output('Statistics', Table)

    want_main_area = True
    buttons_area_orientation = Qt.Vertical

    settingsHandler = DomainContextHandler()
    settings_version = 2

    auto_commit = Setting(True)
    color_var = ContextSetting(None)  # type: Optional[Variable]
    # filter_string = ContextSetting('')

    sorting = Setting((0, Qt.DescendingOrder))
    selected_vars = ContextSetting([], schema_only=True)

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

        self.data = None  # type: Optional[Table]

        # TODO: Implement filtering on the model
        # filter_box = gui.vBox(self.controlArea, 'Filter')
        # self.filter_text = gui.lineEdit(
        #     filter_box, self, value='filter_string',
        #     placeholderText='Filter variables by name',
        #     callback=self._filter_table_variables, callbackOnType=True,
        # )
        # shortcut = QShortcut(QKeySequence('Ctrl+f'), self, self.filter_text.setFocus)
        # shortcut.setWhatsThis('Filter variables by name')

        self.color_var_model = DomainModel(
            valid_types=(ContinuousVariable, DiscreteVariable),
            placeholder='None',
        )
        box = gui.vBox(self.controlArea, 'Histogram')
        self.cb_color_var = gui.comboBox(
            box, master=self, value='color_var', model=self.color_var_model,
            label='Color:', orientation=Qt.Horizontal, contentsLength=13,
            searchable=True
        )
        self.cb_color_var.activated.connect(self.__color_var_changed)

        gui.rubber(self.controlArea)
        gui.auto_send(self.buttonsArea, self, "auto_commit")

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

        # Main area
        self.model = FeatureStatisticsTableModel(parent=self)
        self.table_view = FeatureStatisticsTableView(self.model, parent=self)
        self.table_view.selectionModel().selectionChanged.connect(self.on_select)
        self.table_view.horizontalHeader().sectionClicked.connect(self.on_header_click)

        self.mainArea.layout().addWidget(self.table_view)

    @staticmethod
    def sizeHint():
        return QSize(1050, 500)

    def _filter_table_variables(self):
        regex = QRegExp(self.filter_string)
        # If the user explicitly types different cases, we assume they know
        # what they are searching for and account for letter case in filter
        different_case = (
            any(c.islower() for c in self.filter_string) and
            any(c.isupper() for c in self.filter_string)
        )
        if not different_case:
            regex.setCaseSensitivity(Qt.CaseInsensitive)

    @Inputs.data
    def set_data(self, data):
        # Clear outputs and reset widget state
        self.closeContext()
        self.selected_vars = []
        self.model.resetSorting()
        self.Outputs.reduced_data.send(None)
        self.Outputs.statistics.send(None)

        # Setup widget state for new data and restore settings
        self.data = data

        if data is not None:
            self.info.set_input_summary(len(data),
                                        format_summary_details(data))
            self.color_var_model.set_domain(data.domain)
            self.color_var = None
            if self.data.domain.class_vars:
                self.color_var = self.data.domain.class_vars[0]
        else:
            self.info.set_input_summary(self.info.NoInput)
            self.color_var_model.set_domain(None)
            self.color_var = None
        self.model.set_data(data)

        self.openContext(self.data)
        self.__restore_selection()
        self.__restore_sorting()
        # self._filter_table_variables()
        self.__color_var_changed()

        self.commit()

    def __restore_selection(self):
        """Restore the selection on the table view from saved settings."""
        selection_model = self.table_view.selectionModel()
        selection = QItemSelection()
        if self.selected_vars:
            var_indices = {var: i for i, var in enumerate(self.model.variables)}
            selected_indices = [var_indices[var] for var in self.selected_vars]
            for row in self.model.mapFromSourceRows(selected_indices):
                selection.append(QItemSelectionRange(
                    self.model.index(row, 0),
                    self.model.index(row, self.model.columnCount() - 1)
                ))
        selection_model.select(selection, QItemSelectionModel.ClearAndSelect)

    def __restore_sorting(self):
        """Restore the sort column and order from saved settings."""
        sort_column, sort_order = self.sorting
        if self.model.n_attributes and sort_column < self.model.columnCount():
            self.model.sort(sort_column, sort_order)
            self.table_view.horizontalHeader().setSortIndicator(sort_column, sort_order)

    @pyqtSlot(int)
    def on_header_click(self, *_):
        # Store the header states
        sort_order = self.model.sortOrder()
        sort_column = self.model.sortColumn()
        self.sorting = sort_column, sort_order

    @pyqtSlot(int)
    def __color_var_changed(self, *_):
        if self.model is not None:
            self.model.set_target_var(self.color_var)

    def on_select(self):
        selection_indices = list(self.model.mapToSourceRows([
            i.row() for i in self.table_view.selectionModel().selectedRows()
        ]))
        self.selected_vars = list(self.model.variables[selection_indices])
        self.commit()

    def commit(self):
        if not self.selected_vars:
            self.info.set_output_summary(self.info.NoOutput)
            self.Outputs.reduced_data.send(None)
            self.Outputs.statistics.send(None)
            return

        # Send a table with only selected columns to output
        variables = self.selected_vars
        self.info.set_output_summary(len(self.data[:, variables]),
                                     format_summary_details(self.data[:, variables]))
        self.Outputs.reduced_data.send(self.data[:, variables])

        # Send the statistics of the selected variables to ouput
        labels, data = self.model.get_statistics_matrix(variables, return_labels=True)
        var_names = np.atleast_2d([var.name for var in variables]).T
        domain = Domain(
            attributes=[ContinuousVariable(name) for name in labels],
            metas=[StringVariable('Feature')]
        )
        statistics = Table(domain, data, metas=var_names)
        statistics.name = '%s (Feature Statistics)' % self.data.name
        self.Outputs.statistics.send(statistics)

    def send_report(self):
        pass

    @classmethod
    def migrate_context(cls, context, version):
        if not version or version < 2:
            selected_rows = context.values.pop("selected_rows", None)
            if not selected_rows:
                selected_vars = []
            else:
                # This assumes that dict was saved by Python >= 3.6 so dict is
                # ordered; if not, context hasn't had worked anyway.
                all_vars = [
                    (var, tpe)
                    for (var, tpe) in chain(context.attributes.items(),
                                            context.metas.items())
                    # it would be nicer to use cls.HIDDEN_VAR_TYPES, but there
                    # is no suitable conversion function, and StringVariable (3)
                    # was the only hidden var when settings_version < 2, so:
                    if tpe != 3]
                selected_vars = [all_vars[i] for i in selected_rows]
            context.values["selected_vars"] = selected_vars, -3


if __name__ == '__main__':  # pragma: no cover
    WidgetPreview(OWFeatureStatistics).run(Table("iris"))

```
--- END FILE ---

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

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

Here is an example:

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

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

- **Key Commit Details:**
  - **Commit Message:** "[ENH] Feature statistics report."
  - **Changed Function:** `send_report` now calls `self.report_table(view)` instead of just `pass`.
  
- **Test Results:**
  - **Old Commit:** The `test_report` failed with an `AssertionError` stating that `'<table>'` was not found in the generated report.
  - **New Commit:** The `test_report` passes, indicating that the issue has been resolved.

- **Main Problem Highlighted:**
  - The feature statistics report was not generating the expected HTML table, resulting in an empty report section.

- **Expected Behavior:**
  - When generating a feature statistics report, the HTML output should include a `<table>` element displaying the relevant statistics.

- **Actual Behavior:**
  - The generated report lacked the `<table>` element, leading to an empty content section without any displayed statistics.

- **Issue Description Strategy:**
  - Clearly state that the feature statistics report does not render the expected HTML table.
  - Provide a concise example illustrating how triggering the report results in missing table elements.
  - Describe the discrepancy between expected and actual behaviors without revealing the solution.

**Drafted GitHub Issue:**

[ISSUE]
**Title:** Feature Statistics Report Does Not Display HTML Table

**Description:**
When generating a feature statistics report, the output does not include an HTML `<table>` element. As a result, the report appears empty without displaying any of the statistical data.

**Example Scenario:**
1. Load the dataset "iris" into the Feature Statistics widget.
2. Click the "Generate Report" button.
3. Inspect the generated report.

**Buggy Code Illustration:**
```python
def test_report_generation():
    widget.load_data("iris")
    widget.click_generate_report()
    report_html = widget.get_report_html()
    # Expected to find '<table>' in report_html
```

**Expected Behavior:**
The generated report should contain an HTML `<table>` element that displays the feature statistics, providing a clear and structured view of the data.

**Actual Behavior:**
The report lacks the `<table>` element, resulting in an empty report section without any visible statistics.

**Impact:**
This issue prevents users from viewing the feature statistics in a structured table format, hindering data analysis and review.

[/ISSUE]
{
    "TestVariousDataSets.test_does_not_crash_on_data_removal": "PASSED",
    "TestVariousDataSets.test_does_not_crash_on_empty_domain": "PASSED",
    "TestVariousDataSets.test_image_export": "PASSED",
    "TestVariousDataSets.test_minimum_size": "PASSED",
    "TestVariousDataSets.test_msg_base_class": "PASSED",
    "TestVariousDataSets.test_on_data_with_continuous_values_all_the_same": "PASSED",
    "TestVariousDataSets.test_on_data_with_discrete_values_all_the_same": "PASSED",
    "TestVariousDataSets.test_on_data_with_missing_continuous_values": "PASSED",
    "TestVariousDataSets.test_on_data_with_missing_discrete_values": "PASSED",
    "TestVariousDataSets.test_on_data_with_no_missing_values": "PASSED",
    "TestVariousDataSets.test_on_data_with_no_missing_values_full_domain": "PASSED",
    "TestVariousDataSets.test_on_edge_case_datasets": "PASSED",
    "TestVariousDataSets.test_runs_on_iris": "PASSED",
    "TestVariousDataSets.test_switching_to_dataset_with_no_target_var": "PASSED",
    "TestVariousDataSets.test_switching_to_dataset_with_target_var": "PASSED",
    "TestFeatureStatisticsOutputs.test_changing_data_updates_ouput": "PASSED",
    "TestFeatureStatisticsOutputs.test_changing_data_updates_output_with_autocommit": "PASSED",
    "TestFeatureStatisticsOutputs.test_clearing_selection_sends_none_to_output": "PASSED",
    "TestFeatureStatisticsOutputs.test_image_export": "PASSED",
    "TestFeatureStatisticsOutputs.test_minimum_size": "PASSED",
    "TestFeatureStatisticsOutputs.test_msg_base_class": "PASSED",
    "TestFeatureStatisticsOutputs.test_sends_all_samples_to_output": "PASSED",
    "TestFeatureStatisticsOutputs.test_sends_multiple_attribute_table_to_output": "PASSED",
    "TestFeatureStatisticsOutputs.test_sends_multiple_var_types_table_to_output": "PASSED",
    "TestFeatureStatisticsOutputs.test_sends_single_attribute_table_to_output": "PASSED",
    "TestFeatureStatisticsOutputs.test_sends_single_class_var_table_to_output": "PASSED",
    "TestFeatureStatisticsOutputs.test_sends_single_meta_table_to_output": "PASSED",
    "TestFeatureStatisticsUI.test_image_export": "PASSED",
    "TestFeatureStatisticsUI.test_minimum_size": "PASSED",
    "TestFeatureStatisticsUI.test_msg_base_class": "PASSED",
    "TestFeatureStatisticsUI.test_report": "PASSED",
    "TestFeatureStatisticsUI.test_restores_previous_selection": "PASSED",
    "TestFeatureStatisticsUI.test_settings_migration_to_ver21": "PASSED",
    "TestSummary.test_image_export": "PASSED",
    "TestSummary.test_minimum_size": "PASSED",
    "TestSummary.test_msg_base_class": "PASSED",
    "TestSummary.test_summary": "PASSED"
}
diff --git a/Orange/widgets/data/owfeaturestatistics.py b/Orange/widgets/data/owfeaturestatistics.py
index 7d1a55bf2..72692dbd8 100644
--- a/Orange/widgets/data/owfeaturestatistics.py
+++ b/Orange/widgets/data/owfeaturestatistics.py
@@ -850,7 +850,8 @@ class OWFeatureStatistics(widget.OWWidget):
         self.Outputs.statistics.send(statistics)
 
     def send_report(self):
-        pass
+        view = self.table_view
+        self.report_table(view)
 
     @classmethod
     def migrate_context(cls, context, version):

{"Orange/widgets/data/owfeaturestatistics.py": "\"\"\"\n\nTODO:\n  - Sorting by standard deviation: Use coefficient of variation (std/mean)\n    or quartile coefficient of dispersion (Q3 - Q1) / (Q3 + Q1)\n  - Standard deviation for nominal: try out Variation ratio (1 - n_mode/N)\n\"\"\"\n\nimport datetime\nfrom enum import IntEnum\nfrom itertools import chain\nfrom typing import Any, Optional, Tuple, List\n\nimport numpy as np\nimport scipy.stats as ss\nimport scipy.sparse as sp\nfrom AnyQt.QtCore import Qt, QSize, QRectF, QModelIndex, pyqtSlot, \\\n    QRegExp, QItemSelection, QItemSelectionRange, QItemSelectionModel\nfrom AnyQt.QtGui import QPainter, QColor\nfrom AnyQt.QtWidgets import QStyledItemDelegate, QGraphicsScene, QTableView, \\\n    QHeaderView, QStyle, QStyleOptionViewItem\n\nimport Orange.statistics.util as ut\nfrom Orange.data import Table, StringVariable, DiscreteVariable, \\\n    ContinuousVariable, TimeVariable, Domain, Variable\nfrom Orange.widgets import widget, gui\nfrom Orange.widgets.data.utils.histogram import Histogram\nfrom Orange.widgets.settings import Setting, ContextSetting, \\\n    DomainContextHandler\nfrom Orange.widgets.utils.itemmodels import DomainModel, AbstractSortTableModel\nfrom Orange.widgets.utils.signals import Input, Output\nfrom Orange.widgets.utils.widgetpreview import WidgetPreview\nfrom Orange.widgets.utils.state_summary import format_summary_details\n\n\ndef _categorical_entropy(x):\n    \"\"\"Compute the entropy of a dense/sparse matrix, column-wise. Assuming\n    categorical values.\"\"\"\n    p = [ut.bincount(row)[0] for row in x.T]\n    p = [pk / np.sum(pk) for pk in p]\n    return np.fromiter((ss.entropy(pk) for pk in p), dtype=np.float64)\n\n\ndef format_time_diff(start, end, round_up_after=2):\n    \"\"\"Return an approximate human readable time difference between two dates.\n\n    Parameters\n    ----------\n    start : int\n        Unix timestamp\n    end : int\n        Unix timestamp\n    round_up_after : int\n        The number of time units before we round up to the next, larger time\n        unit e.g. setting to 2 will allow up to 2 days worth of hours to be\n        shown, after that the difference is shown in days. Or put another way\n        we will show from 1-48 hours before switching to days.\n\n    Returns\n    -------\n    str\n\n    \"\"\"\n    start = datetime.datetime.fromtimestamp(start)\n    end = datetime.datetime.fromtimestamp(end)\n    diff = abs(end - start)  # type: datetime.timedelta\n\n    # Get the different resolutions\n    seconds = diff.total_seconds()\n    minutes = seconds // 60\n    hours = minutes // 60\n    days = diff.days\n    weeks = days // 7\n    months = (end.year - start.year) * 12 + end.month - start.month\n    years = months // 12\n\n    # Check which resolution is most appropriate\n    if years >= round_up_after:\n        return '~%d years' % years\n    elif months >= round_up_after:\n        return '~%d months' % months\n    elif weeks >= round_up_after:\n        return '~%d weeks' % weeks\n    elif days >= round_up_after:\n        return '~%d days' % days\n    elif hours >= round_up_after:\n        return '~%d hours' % hours\n    elif minutes >= round_up_after:\n        return '~%d minutes' % minutes\n    else:\n        return '%d seconds' % seconds\n\n\nclass FeatureStatisticsTableModel(AbstractSortTableModel):\n    CLASS_VAR, META, ATTRIBUTE = range(3)\n    COLOR_FOR_ROLE = {\n        CLASS_VAR: QColor(160, 160, 160),\n        META: QColor(220, 220, 200),\n        ATTRIBUTE: QColor(255, 255, 255),\n    }\n\n    HIDDEN_VAR_TYPES = (StringVariable,)\n\n    class Columns(IntEnum):\n        ICON, NAME, DISTRIBUTION, CENTER, DISPERSION, MIN, MAX, MISSING = range(8)\n\n        @property\n        def name(self):\n            return {self.ICON: '',\n                    self.NAME: 'Name',\n                    self.DISTRIBUTION: 'Distribution',\n                    self.CENTER: 'Center',\n                    self.DISPERSION: 'Dispersion',\n                    self.MIN: 'Min.',\n                    self.MAX: 'Max.',\n                    self.MISSING: 'Missing',\n                    }[self.value]\n\n        @property\n        def index(self):\n            return self.value\n\n        @classmethod\n        def from_index(cls, index):\n            return cls(index)\n\n    def __init__(self, data=None, parent=None):\n        \"\"\"\n\n        Parameters\n        ----------\n        data : Optional[Table]\n        parent : Optional[QWidget]\n\n        \"\"\"\n        super().__init__(parent)\n\n        self.table = None  # type: Optional[Table]\n        self.domain = None  # type: Optional[Domain]\n        self.target_var = None  # type: Optional[Variable]\n        self.n_attributes = self.n_instances = 0\n\n        self.__attributes = self.__class_vars = self.__metas = None\n        self.__distributions_cache = {}\n\n        no_data = np.array([])\n        self._variable_types = self._variable_names = no_data\n        self._min = self._max = self._center = no_data\n        self._dispersion = no_data\n        self._missing = no_data\n        # Clear model initially to set default values\n        self.clear()\n\n        self.set_data(data)\n\n    def set_data(self, data):\n        if data is None:\n            self.clear()\n            return\n\n        self.beginResetModel()\n        self.table = data\n        self.domain = domain = data.domain\n        self.target_var = None\n\n        self.__attributes = self.__filter_attributes(domain.attributes, self.table.X)\n        # We disable pylint warning because the `Y` property squeezes vectors,\n        # while we need a 2d array, which `_Y` provides\n        self.__class_vars = self.__filter_attributes(domain.class_vars, self.table._Y)  # pylint: disable=protected-access\n        self.__metas = self.__filter_attributes(domain.metas, self.table.metas)\n\n        self.n_attributes = len(self.variables)\n        self.n_instances = len(data)\n\n        self.__distributions_cache = {}\n        self.__compute_statistics()\n        self.endResetModel()\n\n    def clear(self):\n        self.beginResetModel()\n        self.table = self.domain = self.target_var = None\n        self.n_attributes = self.n_instances = 0\n        self.__attributes = (np.array([]), np.array([]))\n        self.__class_vars = (np.array([]), np.array([]))\n        self.__metas = (np.array([]), np.array([]))\n        self.__distributions_cache.clear()\n        self.endResetModel()\n\n    @property\n    def variables(self):\n        matrices = [self.__attributes[0], self.__class_vars[0], self.__metas[0]]\n        if not any(m.size for m in matrices):\n            return []\n        return np.hstack(matrices)\n\n    @staticmethod\n    def _attr_indices(attrs):\n        # type: (List) -> Tuple[List[int], List[int], List[int], List[int]]\n        \"\"\"Get the indices of different attribute types eg. discrete.\"\"\"\n        disc_var_idx = [i for i, attr in enumerate(attrs) if isinstance(attr, DiscreteVariable)]\n        cont_var_idx = [i for i, attr in enumerate(attrs)\n                        if isinstance(attr, ContinuousVariable)\n                        and not isinstance(attr, TimeVariable)]\n        time_var_idx = [i for i, attr in enumerate(attrs) if isinstance(attr, TimeVariable)]\n        string_var_idx = [i for i, attr in enumerate(attrs) if isinstance(attr, StringVariable)]\n        return disc_var_idx, cont_var_idx, time_var_idx, string_var_idx\n\n    def __filter_attributes(self, attributes, matrix):\n        \"\"\"Filter out variables which shouldn't be visualized.\"\"\"\n        attributes, matrix = np.asarray(attributes), matrix\n        mask = [idx for idx, attr in enumerate(attributes)\n                if not isinstance(attr, self.HIDDEN_VAR_TYPES)]\n        return attributes[mask], matrix[:, mask]\n\n    def __compute_statistics(self):\n        # Since data matrices can of mixed sparsity, we need to compute\n        # attributes separately for each of them.\n        matrices = [self.__attributes, self.__class_vars, self.__metas]\n        # Filter out any matrices with size 0\n        matrices = list(filter(lambda tup: tup[1].size, matrices))\n\n        self._variable_types = np.array([type(var) for var in self.variables])\n        self._variable_names = np.array([var.name.lower() for var in self.variables])\n        self._min = self.__compute_stat(\n            matrices,\n            discrete_f=lambda x: ut.nanmin(x, axis=0),\n            continuous_f=lambda x: ut.nanmin(x, axis=0),\n            time_f=lambda x: ut.nanmin(x, axis=0),\n        )\n        self._dispersion = self.__compute_stat(\n            matrices,\n            discrete_f=_categorical_entropy,\n            continuous_f=lambda x: np.sqrt(ut.nanvar(x, axis=0)) / ut.nanmean(x, axis=0),\n        )\n        self._missing = self.__compute_stat(\n            matrices,\n            discrete_f=lambda x: ut.countnans(x, axis=0),\n            continuous_f=lambda x: ut.countnans(x, axis=0),\n            string_f=lambda x: (x == StringVariable.Unknown).sum(axis=0),\n            time_f=lambda x: ut.countnans(x, axis=0),\n        )\n        self._max = self.__compute_stat(\n            matrices,\n            discrete_f=lambda x: ut.nanmax(x, axis=0),\n            continuous_f=lambda x: ut.nanmax(x, axis=0),\n            time_f=lambda x: ut.nanmax(x, axis=0),\n        )\n\n        # Since scipy apparently can't do mode on sparse matrices, cast it to\n        # dense. This can be very inefficient for large matrices, and should\n        # be changed\n        def __mode(x, *args, **kwargs):\n            if sp.issparse(x):\n                x = x.todense(order=\"C\")\n            # return ss.mode(x, *args, **kwargs)[0]\n            return ut.nanmode(x, *args, **kwargs)[0]  # Temporary replacement for scipy\n\n        self._center = self.__compute_stat(\n            matrices,\n            discrete_f=lambda x: __mode(x, axis=0),\n            continuous_f=lambda x: ut.nanmean(x, axis=0),\n            time_f=lambda x: ut.nanmean(x, axis=0),\n        )\n\n    def get_statistics_matrix(self, variables=None, return_labels=False):\n        \"\"\"Get the numeric computed statistics in a single matrix. Optionally,\n        we can specify for which variables we want the stats. Also, we can get\n        the string column names as labels if desired.\n\n        Parameters\n        ----------\n        variables : Iterable[Union[Variable, int, str]]\n            Return statistics for only the variables specified. Accepts all\n            formats supported by `domain.index`\n        return_labels : bool\n            In addition to the statistics matrix, also return string labels for\n            the columns of the matrix e.g. 'Mean' or 'Dispersion', as specified\n            in `Columns`.\n\n        Returns\n        -------\n        Union[Tuple[List[str], np.ndarray], np.ndarray]\n\n        \"\"\"\n        if self.table is None:\n            return np.atleast_2d([])\n\n        # If a list of variables is given, select only corresponding stats\n        # variables can be a list or array, pylint: disable=len-as-condition\n        if variables is not None and len(variables) != 0:\n            indices = [self.domain.index(var) for var in variables]\n        else:\n            indices = ...\n\n        matrix = np.vstack((\n            self._center[indices], self._dispersion[indices],\n            self._min[indices], self._max[indices], self._missing[indices],\n        )).T\n\n        # Return string labels for the returned matrix columns e.g. 'Mean',\n        # 'Dispersion' if requested\n        if return_labels:\n            labels = [self.Columns.CENTER.name, self.Columns.DISPERSION.name,\n                      self.Columns.MIN.name, self.Columns.MAX.name,\n                      self.Columns.MISSING.name]\n            return labels, matrix\n\n        return matrix\n\n    def __compute_stat(self, matrices, discrete_f=None, continuous_f=None,\n                       time_f=None, string_f=None, default_val=np.nan):\n        \"\"\"Apply functions to appropriate variable types. The default value is\n        returned if there is no function defined for specific variable types.\n        \"\"\"\n        if not matrices:\n            return np.array([])\n\n        def _to_float(data):\n            if not np.issubdtype(data.dtype, np.number):\n                data = data.astype(np.float64)\n            return data\n\n        def _to_object(data):\n            if data.dtype is not np.object:\n                data = data.astype(np.object)\n            return data\n\n        results = []\n        for variables, x in matrices:\n            result = np.full(len(variables), default_val)\n\n            # While the following caching and checks are messy, the indexing\n            # turns out to be a bottleneck for large datasets, so a single\n            # indexing operation improves performance\n            disc_idx, cont_idx, time_idx, str_idx = self._attr_indices(variables)\n            if discrete_f:\n                x_ = x[:, disc_idx]\n                if x_.size:\n                    result[disc_idx] = discrete_f(_to_float(x_))\n            if continuous_f:\n                x_ = x[:, cont_idx]\n                if x_.size:\n                    result[cont_idx] = continuous_f(_to_float(x_))\n            if time_f:\n                x_ = x[:, time_idx]\n                if x_.size:\n                    result[time_idx] = time_f(_to_float(x_))\n            if string_f:\n                x_ = x[:, str_idx]\n                if x_.size:\n                    result[str_idx] = string_f(_to_object(x_))\n\n            results.append(result)\n\n        return np.hstack(results)\n\n    def sortColumnData(self, column):\n        \"\"\"Prepare the arrays with which we will sort the rows. If we want to\n        sort based on a single value e.g. the name, return a 1d array.\n        Sometimes we may want to sort by multiple criteria, comparing\n        continuous variances with discrete entropies makes no sense, so we want\n        to group those variable types together.\n        \"\"\"\n        # Prepare indices for variable types so we can group them together\n        order = [DiscreteVariable, ContinuousVariable, TimeVariable, StringVariable]\n        mapping = {var: idx for idx, var in enumerate(order)}\n        vmapping = np.vectorize(mapping.__getitem__)\n        var_types_indices = vmapping(self._variable_types)\n\n        # Store the variable name sorted indices so we can pass a default\n        # order when sorting by multiple keys\n        var_name_indices = np.argsort(self._variable_names)\n\n        # Prepare vartype indices so ready when needed\n        disc_idx, _, time_idx, str_idx = self._attr_indices(self.variables)\n\n        # Sort by: (type)\n        if column == self.Columns.ICON:\n            return var_types_indices\n        # Sort by: (name)\n        elif column == self.Columns.NAME:\n            # We use `_variable_names` here and not the indices because the\n            # last (or single) row is actually sorted and we don't want to sort\n            # the indices\n            return self._variable_names\n        # Sort by: (None)\n        elif column == self.Columns.DISTRIBUTION:\n            return np.ones_like(var_types_indices)\n        # Sort by: (type, center)\n        elif column == self.Columns.CENTER:\n            # Sorting discrete or string values by mean makes no sense\n            vals = np.array(self._center)\n            vals[disc_idx] = var_name_indices[disc_idx]\n            vals[str_idx] = var_name_indices[str_idx]\n            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T\n        # Sort by: (type, dispersion)\n        elif column == self.Columns.DISPERSION:\n            # Sort time variables by their dispersion, which is not stored in\n            # the dispersion array\n            vals = np.array(self._dispersion)\n            vals[time_idx] = self._max[time_idx] - self._min[time_idx]\n            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T\n        # Sort by: (type, min)\n        elif column == self.Columns.MIN:\n            # Sorting discrete or string values by min makes no sense\n            vals = np.array(self._min)\n            vals[disc_idx] = var_name_indices[disc_idx]\n            vals[str_idx] = var_name_indices[str_idx]\n            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T\n        # Sort by: (type, max)\n        elif column == self.Columns.MAX:\n            # Sorting discrete or string values by min makes no sense\n            vals = np.array(self._max)\n            vals[disc_idx] = var_name_indices[disc_idx]\n            vals[str_idx] = var_name_indices[str_idx]\n            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T\n        # Sort by: (missing)\n        elif column == self.Columns.MISSING:\n            return self._missing\n\n        return None\n\n    def _sortColumnData(self, column):\n        \"\"\"Allow sorting with 2d arrays.\"\"\"\n        data = np.asarray(self.sortColumnData(column))\n        data = data[self.mapToSourceRows(Ellipsis)]\n\n        assert data.ndim <= 2, 'Data should be at most 2-dimensional'\n        return data\n\n    def _argsortData(self, data, order):\n        if data.ndim == 1:\n            indices = np.argsort(data, kind='mergesort')\n            if order == Qt.DescendingOrder:\n                indices = indices[::-1]\n            # Always sort NaNs last\n            if np.issubdtype(data.dtype, np.number):\n                indices = np.roll(indices, -np.isnan(data).sum())\n        else:\n            assert np.issubdtype(data.dtype, np.number), \\\n                'We do not deal with non numeric values in sorting by ' \\\n                'multiple values'\n            if order == Qt.DescendingOrder:\n                data[:, -1] = -data[:, -1]\n\n            # In order to make sure NaNs always appear at the end, insert a\n            # indicator whether NaN or not. Note that the data array must\n            # contain an empty column of zeros at index -2 since inserting an\n            # extra column after the fact can result in a MemoryError for data\n            # with a large amount of variables\n            assert np.all(data[:, -2] == 0), \\\n                'Add an empty column of zeros at index -2 to accomodate NaNs'\n            np.isnan(data[:, -1], out=data[:, -2])\n\n            indices = np.lexsort(np.flip(data.T, axis=0))\n\n        return indices\n\n    def headerData(self, section, orientation, role):\n        # type: (int, Qt.Orientation, Qt.ItemDataRole) -> Any\n        if orientation == Qt.Horizontal:\n            if role == Qt.DisplayRole:\n                return self.Columns.from_index(section).name\n\n        return None\n\n    def data(self, index, role):\n        # type: (QModelIndex, Qt.ItemDataRole) -> Any\n        def background():\n            if attribute in self.domain.attributes:\n                return self.COLOR_FOR_ROLE[self.ATTRIBUTE]\n            if attribute in self.domain.metas:\n                return self.COLOR_FOR_ROLE[self.META]\n            if attribute in self.domain.class_vars:\n                return self.COLOR_FOR_ROLE[self.CLASS_VAR]\n            return None\n\n        def text_alignment():\n            if column == self.Columns.NAME:\n                return Qt.AlignLeft | Qt.AlignVCenter\n            return Qt.AlignRight | Qt.AlignVCenter\n\n        def decoration():\n            if column == self.Columns.ICON:\n                return gui.attributeIconDict[attribute]\n            return None\n\n        def display():\n            # pylint: disable=too-many-branches\n            def render_value(value):\n                return \"\" if np.isnan(value) else attribute.str_val(value)\n\n            if column == self.Columns.NAME:\n                return attribute.name\n            elif column == self.Columns.DISTRIBUTION:\n                if isinstance(attribute,\n                              (DiscreteVariable, ContinuousVariable)):\n                    if row not in self.__distributions_cache:\n                        scene = QGraphicsScene(parent=self)\n                        histogram = Histogram(\n                            data=self.table,\n                            variable=attribute,\n                            color_attribute=self.target_var,\n                            border=(0, 0, 2, 0),\n                            border_color='#ccc',\n                        )\n                        scene.addItem(histogram)\n                        self.__distributions_cache[row] = scene\n                    return self.__distributions_cache[row]\n            elif column == self.Columns.CENTER:\n                return render_value(self._center[row])\n            elif column == self.Columns.DISPERSION:\n                if isinstance(attribute, TimeVariable):\n                    return format_time_diff(self._min[row], self._max[row])\n                elif isinstance(attribute, DiscreteVariable):\n                    return \"%.3g\" % self._dispersion[row]\n                else:\n                    return render_value(self._dispersion[row])\n            elif column == self.Columns.MIN:\n                if not isinstance(attribute, DiscreteVariable) \\\n                        or attribute.ordered:\n                    return render_value(self._min[row])\n            elif column == self.Columns.MAX:\n                if not isinstance(attribute, DiscreteVariable) \\\n                        or attribute.ordered:\n                    return render_value(self._max[row])\n            elif column == self.Columns.MISSING:\n                return '%d (%d%%)' % (\n                    self._missing[row],\n                    100 * self._missing[row] / self.n_instances\n                )\n            return None\n\n        roles = {Qt.BackgroundRole: background,\n                 Qt.TextAlignmentRole: text_alignment,\n                 Qt.DecorationRole: decoration,\n                 Qt.DisplayRole: display}\n\n        if not index.isValid() or role not in roles:\n            return None\n\n        row, column = self.mapToSourceRows(index.row()), index.column()\n        # Make sure we're not out of range\n        if not 0 <= row <= self.n_attributes:\n            return None\n\n        attribute = self.variables[row]\n        return roles[role]()\n\n    def rowCount(self, parent=QModelIndex()):\n        return 0 if parent.isValid() else self.n_attributes\n\n    def columnCount(self, parent=QModelIndex()):\n        return 0 if parent.isValid() else len(self.Columns)\n\n    def set_target_var(self, variable):\n        self.target_var = variable\n        self.__distributions_cache.clear()\n        start_idx = self.index(0, self.Columns.DISTRIBUTION)\n        end_idx = self.index(self.rowCount(), self.Columns.DISTRIBUTION)\n        self.dataChanged.emit(start_idx, end_idx)\n\n\nclass FeatureStatisticsTableView(QTableView):\n    HISTOGRAM_ASPECT_RATIO = (7, 3)\n    MINIMUM_HISTOGRAM_HEIGHT = 50\n    MAXIMUM_HISTOGRAM_HEIGHT = 80\n\n    def __init__(self, model, parent=None, **kwargs):\n        super().__init__(\n            parent=parent,\n            showGrid=False,\n            cornerButtonEnabled=False,\n            sortingEnabled=True,\n            selectionBehavior=QTableView.SelectRows,\n            selectionMode=QTableView.ExtendedSelection,\n            horizontalScrollMode=QTableView.ScrollPerPixel,\n            verticalScrollMode=QTableView.ScrollPerPixel,\n            **kwargs\n        )\n        self.setModel(model)\n\n        hheader = self.horizontalHeader()\n        hheader.setStretchLastSection(False)\n        # Contents precision specifies how many rows should be taken into\n        # account when computing the sizes, 0 being the visible rows. This is\n        # crucial, since otherwise the `ResizeToContents` section resize mode\n        # would call `sizeHint` on every single row in the data before first\n        # render. However this, this cannot be used here, since this only\n        # appears to work properly when the widget is actually shown. When the\n        # widget is not shown, size `sizeHint` is called on every row.\n        hheader.setResizeContentsPrecision(5)\n        # Set a nice default size so that headers have some space around titles\n        hheader.setDefaultSectionSize(100)\n        # Set individual column behaviour in `set_data` since the logical\n        # indices must be valid in the model, which requires data.\n        hheader.setSectionResizeMode(QHeaderView.Interactive)\n\n        columns = model.Columns\n        hheader.setSectionResizeMode(columns.ICON.index, QHeaderView.ResizeToContents)\n        hheader.setSectionResizeMode(columns.DISTRIBUTION.index, QHeaderView.Stretch)\n\n        vheader = self.verticalHeader()\n        vheader.setVisible(False)\n        vheader.setSectionResizeMode(QHeaderView.Fixed)\n        hheader.sectionResized.connect(self.bind_histogram_aspect_ratio)\n        # TODO: This shifts the scrollarea a bit down when opening widget\n        # hheader.sectionResized.connect(self.keep_row_centered)\n\n        self.setItemDelegate(NoFocusRectDelegate(parent=self))\n        self.setItemDelegateForColumn(\n            FeatureStatisticsTableModel.Columns.DISTRIBUTION,\n            DistributionDelegate(parent=self),\n        )\n\n    def bind_histogram_aspect_ratio(self, logical_index, _, new_size):\n        \"\"\"Force the horizontal and vertical header to maintain the defined\n        aspect ratio specified for the histogram.\"\"\"\n        # Prevent function being exectued more than once per resize\n        if logical_index is not self.model().Columns.DISTRIBUTION.index:\n            return\n        ratio_width, ratio_height = self.HISTOGRAM_ASPECT_RATIO\n        unit_width = new_size / ratio_width\n        new_height = unit_width * ratio_height\n        effective_height = max(new_height, self.MINIMUM_HISTOGRAM_HEIGHT)\n        effective_height = min(effective_height, self.MAXIMUM_HISTOGRAM_HEIGHT)\n        self.verticalHeader().setDefaultSectionSize(effective_height)\n\n    def keep_row_centered(self, logical_index, _1, _2):\n        \"\"\"When resizing the widget when scrolled further down, the\n        positions of rows changes. Obviously, the user resized in order to\n        better see the row of interest. This keeps that row centered.\"\"\"\n        # TODO: This does not work properly\n        # Prevent function being exectued more than once per resize\n        if logical_index is not self.model().Columns.DISTRIBUTION.index:\n            return\n        top_row = self.indexAt(self.rect().topLeft()).row()\n        bottom_row = self.indexAt(self.rect().bottomLeft()).row()\n        middle_row = top_row + (bottom_row - top_row) // 2\n        self.scrollTo(self.model().index(middle_row, 0), QTableView.PositionAtCenter)\n\n\nclass NoFocusRectDelegate(QStyledItemDelegate):\n    \"\"\"Removes the light blue background and border on a focused item.\"\"\"\n\n    def paint(self, painter, option, index):\n        # type: (QPainter, QStyleOptionViewItem, QModelIndex) -> None\n        option.state &= ~QStyle.State_HasFocus\n        super().paint(painter, option, index)\n\n\nclass DistributionDelegate(QStyledItemDelegate):\n    def paint(self, painter, option, index):\n        # type: (QPainter, QStyleOptionViewItem, QModelIndex) -> None\n        scene = index.data(Qt.DisplayRole)  # type: Optional[QGraphicsScene]\n        if scene is None:\n            return super().paint(painter, option, index)\n\n        painter.setRenderHint(QPainter.Antialiasing)\n\n        if option.state & QStyle.State_Selected:\n            background_color = option.palette.highlight()\n        else:\n            background_color = index.data(Qt.BackgroundRole)\n        if background_color is not None:\n            scene.setBackgroundBrush(background_color)\n\n        scene.render(painter, target=QRectF(option.rect), mode=Qt.IgnoreAspectRatio)\n\n        # pylint complains about inconsistent return statements\n        return None\n\n\nclass OWFeatureStatistics(widget.OWWidget):\n    name = 'Feature Statistics'\n    description = 'Show basic statistics for data features.'\n    icon = 'icons/FeatureStatistics.svg'\n\n    class Inputs:\n        data = Input('Data', Table, default=True)\n\n    class Outputs:\n        reduced_data = Output('Reduced Data', Table, default=True)\n        statistics = Output('Statistics', Table)\n\n    want_main_area = True\n    buttons_area_orientation = Qt.Vertical\n\n    settingsHandler = DomainContextHandler()\n    settings_version = 2\n\n    auto_commit = Setting(True)\n    color_var = ContextSetting(None)  # type: Optional[Variable]\n    # filter_string = ContextSetting('')\n\n    sorting = Setting((0, Qt.DescendingOrder))\n    selected_vars = ContextSetting([], schema_only=True)\n\n    def __init__(self):\n        super().__init__()\n\n        self.data = None  # type: Optional[Table]\n\n        # TODO: Implement filtering on the model\n        # filter_box = gui.vBox(self.controlArea, 'Filter')\n        # self.filter_text = gui.lineEdit(\n        #     filter_box, self, value='filter_string',\n        #     placeholderText='Filter variables by name',\n        #     callback=self._filter_table_variables, callbackOnType=True,\n        # )\n        # shortcut = QShortcut(QKeySequence('Ctrl+f'), self, self.filter_text.setFocus)\n        # shortcut.setWhatsThis('Filter variables by name')\n\n        self.color_var_model = DomainModel(\n            valid_types=(ContinuousVariable, DiscreteVariable),\n            placeholder='None',\n        )\n        box = gui.vBox(self.controlArea, 'Histogram')\n        self.cb_color_var = gui.comboBox(\n            box, master=self, value='color_var', model=self.color_var_model,\n            label='Color:', orientation=Qt.Horizontal, contentsLength=13,\n            searchable=True\n        )\n        self.cb_color_var.activated.connect(self.__color_var_changed)\n\n        gui.rubber(self.controlArea)\n        gui.auto_send(self.buttonsArea, self, \"auto_commit\")\n\n        self.info.set_input_summary(self.info.NoInput)\n        self.info.set_output_summary(self.info.NoOutput)\n\n        # Main area\n        self.model = FeatureStatisticsTableModel(parent=self)\n        self.table_view = FeatureStatisticsTableView(self.model, parent=self)\n        self.table_view.selectionModel().selectionChanged.connect(self.on_select)\n        self.table_view.horizontalHeader().sectionClicked.connect(self.on_header_click)\n\n        self.mainArea.layout().addWidget(self.table_view)\n\n    @staticmethod\n    def sizeHint():\n        return QSize(1050, 500)\n\n    def _filter_table_variables(self):\n        regex = QRegExp(self.filter_string)\n        # If the user explicitly types different cases, we assume they know\n        # what they are searching for and account for letter case in filter\n        different_case = (\n            any(c.islower() for c in self.filter_string) and\n            any(c.isupper() for c in self.filter_string)\n        )\n        if not different_case:\n            regex.setCaseSensitivity(Qt.CaseInsensitive)\n\n    @Inputs.data\n    def set_data(self, data):\n        # Clear outputs and reset widget state\n        self.closeContext()\n        self.selected_vars = []\n        self.model.resetSorting()\n        self.Outputs.reduced_data.send(None)\n        self.Outputs.statistics.send(None)\n\n        # Setup widget state for new data and restore settings\n        self.data = data\n\n        if data is not None:\n            self.info.set_input_summary(len(data),\n                                        format_summary_details(data))\n            self.color_var_model.set_domain(data.domain)\n            self.color_var = None\n            if self.data.domain.class_vars:\n                self.color_var = self.data.domain.class_vars[0]\n        else:\n            self.info.set_input_summary(self.info.NoInput)\n            self.color_var_model.set_domain(None)\n            self.color_var = None\n        self.model.set_data(data)\n\n        self.openContext(self.data)\n        self.__restore_selection()\n        self.__restore_sorting()\n        # self._filter_table_variables()\n        self.__color_var_changed()\n\n        self.commit()\n\n    def __restore_selection(self):\n        \"\"\"Restore the selection on the table view from saved settings.\"\"\"\n        selection_model = self.table_view.selectionModel()\n        selection = QItemSelection()\n        if self.selected_vars:\n            var_indices = {var: i for i, var in enumerate(self.model.variables)}\n            selected_indices = [var_indices[var] for var in self.selected_vars]\n            for row in self.model.mapFromSourceRows(selected_indices):\n                selection.append(QItemSelectionRange(\n                    self.model.index(row, 0),\n                    self.model.index(row, self.model.columnCount() - 1)\n                ))\n        selection_model.select(selection, QItemSelectionModel.ClearAndSelect)\n\n    def __restore_sorting(self):\n        \"\"\"Restore the sort column and order from saved settings.\"\"\"\n        sort_column, sort_order = self.sorting\n        if self.model.n_attributes and sort_column < self.model.columnCount():\n            self.model.sort(sort_column, sort_order)\n            self.table_view.horizontalHeader().setSortIndicator(sort_column, sort_order)\n\n    @pyqtSlot(int)\n    def on_header_click(self, *_):\n        # Store the header states\n        sort_order = self.model.sortOrder()\n        sort_column = self.model.sortColumn()\n        self.sorting = sort_column, sort_order\n\n    @pyqtSlot(int)\n    def __color_var_changed(self, *_):\n        if self.model is not None:\n            self.model.set_target_var(self.color_var)\n\n    def on_select(self):\n        selection_indices = list(self.model.mapToSourceRows([\n            i.row() for i in self.table_view.selectionModel().selectedRows()\n        ]))\n        self.selected_vars = list(self.model.variables[selection_indices])\n        self.commit()\n\n    def commit(self):\n        if not self.selected_vars:\n            self.info.set_output_summary(self.info.NoOutput)\n            self.Outputs.reduced_data.send(None)\n            self.Outputs.statistics.send(None)\n            return\n\n        # Send a table with only selected columns to output\n        variables = self.selected_vars\n        self.info.set_output_summary(len(self.data[:, variables]),\n                                     format_summary_details(self.data[:, variables]))\n        self.Outputs.reduced_data.send(self.data[:, variables])\n\n        # Send the statistics of the selected variables to ouput\n        labels, data = self.model.get_statistics_matrix(variables, return_labels=True)\n        var_names = np.atleast_2d([var.name for var in variables]).T\n        domain = Domain(\n            attributes=[ContinuousVariable(name) for name in labels],\n            metas=[StringVariable('Feature')]\n        )\n        statistics = Table(domain, data, metas=var_names)\n        statistics.name = '%s (Feature Statistics)' % self.data.name\n        self.Outputs.statistics.send(statistics)\n\n    def send_report(self):\n        pass\n\n    @classmethod\n    def migrate_context(cls, context, version):\n        if not version or version < 2:\n            selected_rows = context.values.pop(\"selected_rows\", None)\n            if not selected_rows:\n                selected_vars = []\n            else:\n                # This assumes that dict was saved by Python >= 3.6 so dict is\n                # ordered; if not, context hasn't had worked anyway.\n                all_vars = [\n                    (var, tpe)\n                    for (var, tpe) in chain(context.attributes.items(),\n                                            context.metas.items())\n                    # it would be nicer to use cls.HIDDEN_VAR_TYPES, but there\n                    # is no suitable conversion function, and StringVariable (3)\n                    # was the only hidden var when settings_version < 2, so:\n                    if tpe != 3]\n                selected_vars = [all_vars[i] for i in selected_rows]\n            context.values[\"selected_vars\"] = selected_vars, -3\n\n\nif __name__ == '__main__':  # pragma: no cover\n    WidgetPreview(OWFeatureStatistics).run(Table(\"iris\"))\n"}
A user will ask you to solve a task. You should first draft your thinking process (inner monologue). Then, generate the solution.

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

- **Key Commit Details:**
  - **Commit Message:** "[ENH] Feature statistics report."
  - **Changed Function:** `send_report` now calls `self.report_table(view)` instead of just `pass`.
  
- **Test Results:**
  - **Old Commit:** The `test_report` failed with an `AssertionError` stating that `'<table>'` was not found in the generated report.
  - **New Commit:** The `test_report` passes, indicating that the issue has been resolved.

- **Main Problem Highlighted:**
  - The feature statistics report was not generating the expected HTML table, resulting in an empty report section.

- **Expected Behavior:**
  - When generating a feature statistics report, the HTML output should include a `<table>` element displaying the relevant statistics.

- **Actual Behavior:**
  - The generated report lacked the `<table>` element, leading to an empty content section without any displayed statistics.

- **Issue Description Strategy:**
  - Clearly state that the feature statistics report does not render the expected HTML table.
  - Provide a concise example illustrating how triggering the report results in missing table elements.
  - Describe the discrepancy between expected and actual behaviors without revealing the solution.

**Drafted GitHub Issue:**

[ISSUE]
**Title:** Feature Statistics Report Does Not Display HTML Table

**Description:**
When generating a feature statistics report, the output does not include an HTML `<table>` element. As a result, the report appears empty without displaying any of the statistical data.

**Example Scenario:**
1. Load the dataset "iris" into the Feature Statistics widget.
2. Click the "Generate Report" button.
3. Inspect the generated report.

**Buggy Code Illustration:**
```python
def test_report_generation():
    widget.load_data("iris")
    widget.click_generate_report()
    report_html = widget.get_report_html()
    # Expected to find '<table>' in report_html
```

**Expected Behavior:**
The generated report should contain an HTML `<table>` element that displays the feature statistics, providing a clear and structured view of the data.

**Actual Behavior:**
The report lacks the `<table>` element, resulting in an empty report section without any visible statistics.

**Impact:**
This issue prevents users from viewing the feature statistics in a structured table format, hindering data analysis and review.

[/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/owfeaturestatistics.py
"""

TODO:
  - Sorting by standard deviation: Use coefficient of variation (std/mean)
    or quartile coefficient of dispersion (Q3 - Q1) / (Q3 + Q1)
  - Standard deviation for nominal: try out Variation ratio (1 - n_mode/N)
"""

import datetime
from enum import IntEnum
from itertools import chain
from typing import Any, Optional, Tuple, List

import numpy as np
import scipy.stats as ss
import scipy.sparse as sp
from AnyQt.QtCore import Qt, QSize, QRectF, QModelIndex, pyqtSlot, \
    QRegExp, QItemSelection, QItemSelectionRange, QItemSelectionModel
from AnyQt.QtGui import QPainter, QColor
from AnyQt.QtWidgets import QStyledItemDelegate, QGraphicsScene, QTableView, \
    QHeaderView, QStyle, QStyleOptionViewItem

import Orange.statistics.util as ut
from Orange.data import Table, StringVariable, DiscreteVariable, \
    ContinuousVariable, TimeVariable, Domain, Variable
from Orange.widgets import widget, gui
from Orange.widgets.data.utils.histogram import Histogram
from Orange.widgets.settings import Setting, ContextSetting, \
    DomainContextHandler
from Orange.widgets.utils.itemmodels import DomainModel, AbstractSortTableModel
from Orange.widgets.utils.signals import Input, Output
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.utils.state_summary import format_summary_details


def _categorical_entropy(x):
    """Compute the entropy of a dense/sparse matrix, column-wise. Assuming
    categorical values."""
    p = [ut.bincount(row)[0] for row in x.T]
    p = [pk / np.sum(pk) for pk in p]
    return np.fromiter((ss.entropy(pk) for pk in p), dtype=np.float64)


def format_time_diff(start, end, round_up_after=2):
    """Return an approximate human readable time difference between two dates.

    Parameters
    ----------
    start : int
        Unix timestamp
    end : int
        Unix timestamp
    round_up_after : int
        The number of time units before we round up to the next, larger time
        unit e.g. setting to 2 will allow up to 2 days worth of hours to be
        shown, after that the difference is shown in days. Or put another way
        we will show from 1-48 hours before switching to days.

    Returns
    -------
    str

    """
    start = datetime.datetime.fromtimestamp(start)
    end = datetime.datetime.fromtimestamp(end)
    diff = abs(end - start)  # type: datetime.timedelta

    # Get the different resolutions
    seconds = diff.total_seconds()
    minutes = seconds // 60
    hours = minutes // 60
    days = diff.days
    weeks = days // 7
    months = (end.year - start.year) * 12 + end.month - start.month
    years = months // 12

    # Check which resolution is most appropriate
    if years >= round_up_after:
        return '~%d years' % years
    elif months >= round_up_after:
        return '~%d months' % months
    elif weeks >= round_up_after:
        return '~%d weeks' % weeks
    elif days >= round_up_after:
        return '~%d days' % days
    elif hours >= round_up_after:
        return '~%d hours' % hours
    elif minutes >= round_up_after:
        return '~%d minutes' % minutes
    else:
        return '%d seconds' % seconds


class FeatureStatisticsTableModel(AbstractSortTableModel):
    CLASS_VAR, META, ATTRIBUTE = range(3)
    COLOR_FOR_ROLE = {
        CLASS_VAR: QColor(160, 160, 160),
        META: QColor(220, 220, 200),
        ATTRIBUTE: QColor(255, 255, 255),
    }

    HIDDEN_VAR_TYPES = (StringVariable,)

    class Columns(IntEnum):
        ICON, NAME, DISTRIBUTION, CENTER, DISPERSION, MIN, MAX, MISSING = range(8)

        @property
        def name(self):
            return {self.ICON: '',
                    self.NAME: 'Name',
                    self.DISTRIBUTION: 'Distribution',
                    self.CENTER: 'Center',
                    self.DISPERSION: 'Dispersion',
                    self.MIN: 'Min.',
                    self.MAX: 'Max.',
                    self.MISSING: 'Missing',
                    }[self.value]

        @property
        def index(self):
            return self.value

        @classmethod
        def from_index(cls, index):
            return cls(index)

    def __init__(self, data=None, parent=None):
        """

        Parameters
        ----------
        data : Optional[Table]
        parent : Optional[QWidget]

        """
        super().__init__(parent)

        self.table = None  # type: Optional[Table]
        self.domain = None  # type: Optional[Domain]
        self.target_var = None  # type: Optional[Variable]
        self.n_attributes = self.n_instances = 0

        self.__attributes = self.__class_vars = self.__metas = None
        self.__distributions_cache = {}

        no_data = np.array([])
        self._variable_types = self._variable_names = no_data
        self._min = self._max = self._center = no_data
        self._dispersion = no_data
        self._missing = no_data
        # Clear model initially to set default values
        self.clear()

        self.set_data(data)

    def set_data(self, data):
        if data is None:
            self.clear()
            return

        self.beginResetModel()
        self.table = data
        self.domain = domain = data.domain
        self.target_var = None

        self.__attributes = self.__filter_attributes(domain.attributes, self.table.X)
        # We disable pylint warning because the `Y` property squeezes vectors,
        # while we need a 2d array, which `_Y` provides
        self.__class_vars = self.__filter_attributes(domain.class_vars, self.table._Y)  # pylint: disable=protected-access
        self.__metas = self.__filter_attributes(domain.metas, self.table.metas)

        self.n_attributes = len(self.variables)
        self.n_instances = len(data)

        self.__distributions_cache = {}
        self.__compute_statistics()
        self.endResetModel()

    def clear(self):
        self.beginResetModel()
        self.table = self.domain = self.target_var = None
        self.n_attributes = self.n_instances = 0
        self.__attributes = (np.array([]), np.array([]))
        self.__class_vars = (np.array([]), np.array([]))
        self.__metas = (np.array([]), np.array([]))
        self.__distributions_cache.clear()
        self.endResetModel()

    @property
    def variables(self):
        matrices = [self.__attributes[0], self.__class_vars[0], self.__metas[0]]
        if not any(m.size for m in matrices):
            return []
        return np.hstack(matrices)

    @staticmethod
    def _attr_indices(attrs):
        # type: (List) -> Tuple[List[int], List[int], List[int], List[int]]
        """Get the indices of different attribute types eg. discrete."""
        disc_var_idx = [i for i, attr in enumerate(attrs) if isinstance(attr, DiscreteVariable)]
        cont_var_idx = [i for i, attr in enumerate(attrs)
                        if isinstance(attr, ContinuousVariable)
                        and not isinstance(attr, TimeVariable)]
        time_var_idx = [i for i, attr in enumerate(attrs) if isinstance(attr, TimeVariable)]
        string_var_idx = [i for i, attr in enumerate(attrs) if isinstance(attr, StringVariable)]
        return disc_var_idx, cont_var_idx, time_var_idx, string_var_idx

    def __filter_attributes(self, attributes, matrix):
        """Filter out variables which shouldn't be visualized."""
        attributes, matrix = np.asarray(attributes), matrix
        mask = [idx for idx, attr in enumerate(attributes)
                if not isinstance(attr, self.HIDDEN_VAR_TYPES)]
        return attributes[mask], matrix[:, mask]

    def __compute_statistics(self):
        # Since data matrices can of mixed sparsity, we need to compute
        # attributes separately for each of them.
        matrices = [self.__attributes, self.__class_vars, self.__metas]
        # Filter out any matrices with size 0
        matrices = list(filter(lambda tup: tup[1].size, matrices))

        self._variable_types = np.array([type(var) for var in self.variables])
        self._variable_names = np.array([var.name.lower() for var in self.variables])
        self._min = self.__compute_stat(
            matrices,
            discrete_f=lambda x: ut.nanmin(x, axis=0),
            continuous_f=lambda x: ut.nanmin(x, axis=0),
            time_f=lambda x: ut.nanmin(x, axis=0),
        )
        self._dispersion = self.__compute_stat(
            matrices,
            discrete_f=_categorical_entropy,
            continuous_f=lambda x: np.sqrt(ut.nanvar(x, axis=0)) / ut.nanmean(x, axis=0),
        )
        self._missing = self.__compute_stat(
            matrices,
            discrete_f=lambda x: ut.countnans(x, axis=0),
            continuous_f=lambda x: ut.countnans(x, axis=0),
            string_f=lambda x: (x == StringVariable.Unknown).sum(axis=0),
            time_f=lambda x: ut.countnans(x, axis=0),
        )
        self._max = self.__compute_stat(
            matrices,
            discrete_f=lambda x: ut.nanmax(x, axis=0),
            continuous_f=lambda x: ut.nanmax(x, axis=0),
            time_f=lambda x: ut.nanmax(x, axis=0),
        )

        # Since scipy apparently can't do mode on sparse matrices, cast it to
        # dense. This can be very inefficient for large matrices, and should
        # be changed
        def __mode(x, *args, **kwargs):
            if sp.issparse(x):
                x = x.todense(order="C")
            # return ss.mode(x, *args, **kwargs)[0]
            return ut.nanmode(x, *args, **kwargs)[0]  # Temporary replacement for scipy

        self._center = self.__compute_stat(
            matrices,
            discrete_f=lambda x: __mode(x, axis=0),
            continuous_f=lambda x: ut.nanmean(x, axis=0),
            time_f=lambda x: ut.nanmean(x, axis=0),
        )

    def get_statistics_matrix(self, variables=None, return_labels=False):
        """Get the numeric computed statistics in a single matrix. Optionally,
        we can specify for which variables we want the stats. Also, we can get
        the string column names as labels if desired.

        Parameters
        ----------
        variables : Iterable[Union[Variable, int, str]]
            Return statistics for only the variables specified. Accepts all
            formats supported by `domain.index`
        return_labels : bool
            In addition to the statistics matrix, also return string labels for
            the columns of the matrix e.g. 'Mean' or 'Dispersion', as specified
            in `Columns`.

        Returns
        -------
        Union[Tuple[List[str], np.ndarray], np.ndarray]

        """
        if self.table is None:
            return np.atleast_2d([])

        # If a list of variables is given, select only corresponding stats
        # variables can be a list or array, pylint: disable=len-as-condition
        if variables is not None and len(variables) != 0:
            indices = [self.domain.index(var) for var in variables]
        else:
            indices = ...

        matrix = np.vstack((
            self._center[indices], self._dispersion[indices],
            self._min[indices], self._max[indices], self._missing[indices],
        )).T

        # Return string labels for the returned matrix columns e.g. 'Mean',
        # 'Dispersion' if requested
        if return_labels:
            labels = [self.Columns.CENTER.name, self.Columns.DISPERSION.name,
                      self.Columns.MIN.name, self.Columns.MAX.name,
                      self.Columns.MISSING.name]
            return labels, matrix

        return matrix

    def __compute_stat(self, matrices, discrete_f=None, continuous_f=None,
                       time_f=None, string_f=None, default_val=np.nan):
        """Apply functions to appropriate variable types. The default value is
        returned if there is no function defined for specific variable types.
        """
        if not matrices:
            return np.array([])

        def _to_float(data):
            if not np.issubdtype(data.dtype, np.number):
                data = data.astype(np.float64)
            return data

        def _to_object(data):
            if data.dtype is not np.object:
                data = data.astype(np.object)
            return data

        results = []
        for variables, x in matrices:
            result = np.full(len(variables), default_val)

            # While the following caching and checks are messy, the indexing
            # turns out to be a bottleneck for large datasets, so a single
            # indexing operation improves performance
            disc_idx, cont_idx, time_idx, str_idx = self._attr_indices(variables)
            if discrete_f:
                x_ = x[:, disc_idx]
                if x_.size:
                    result[disc_idx] = discrete_f(_to_float(x_))
            if continuous_f:
                x_ = x[:, cont_idx]
                if x_.size:
                    result[cont_idx] = continuous_f(_to_float(x_))
            if time_f:
                x_ = x[:, time_idx]
                if x_.size:
                    result[time_idx] = time_f(_to_float(x_))
            if string_f:
                x_ = x[:, str_idx]
                if x_.size:
                    result[str_idx] = string_f(_to_object(x_))

            results.append(result)

        return np.hstack(results)

    def sortColumnData(self, column):
        """Prepare the arrays with which we will sort the rows. If we want to
        sort based on a single value e.g. the name, return a 1d array.
        Sometimes we may want to sort by multiple criteria, comparing
        continuous variances with discrete entropies makes no sense, so we want
        to group those variable types together.
        """
        # Prepare indices for variable types so we can group them together
        order = [DiscreteVariable, ContinuousVariable, TimeVariable, StringVariable]
        mapping = {var: idx for idx, var in enumerate(order)}
        vmapping = np.vectorize(mapping.__getitem__)
        var_types_indices = vmapping(self._variable_types)

        # Store the variable name sorted indices so we can pass a default
        # order when sorting by multiple keys
        var_name_indices = np.argsort(self._variable_names)

        # Prepare vartype indices so ready when needed
        disc_idx, _, time_idx, str_idx = self._attr_indices(self.variables)

        # Sort by: (type)
        if column == self.Columns.ICON:
            return var_types_indices
        # Sort by: (name)
        elif column == self.Columns.NAME:
            # We use `_variable_names` here and not the indices because the
            # last (or single) row is actually sorted and we don't want to sort
            # the indices
            return self._variable_names
        # Sort by: (None)
        elif column == self.Columns.DISTRIBUTION:
            return np.ones_like(var_types_indices)
        # Sort by: (type, center)
        elif column == self.Columns.CENTER:
            # Sorting discrete or string values by mean makes no sense
            vals = np.array(self._center)
            vals[disc_idx] = var_name_indices[disc_idx]
            vals[str_idx] = var_name_indices[str_idx]
            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T
        # Sort by: (type, dispersion)
        elif column == self.Columns.DISPERSION:
            # Sort time variables by their dispersion, which is not stored in
            # the dispersion array
            vals = np.array(self._dispersion)
            vals[time_idx] = self._max[time_idx] - self._min[time_idx]
            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T
        # Sort by: (type, min)
        elif column == self.Columns.MIN:
            # Sorting discrete or string values by min makes no sense
            vals = np.array(self._min)
            vals[disc_idx] = var_name_indices[disc_idx]
            vals[str_idx] = var_name_indices[str_idx]
            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T
        # Sort by: (type, max)
        elif column == self.Columns.MAX:
            # Sorting discrete or string values by min makes no sense
            vals = np.array(self._max)
            vals[disc_idx] = var_name_indices[disc_idx]
            vals[str_idx] = var_name_indices[str_idx]
            return np.vstack((var_types_indices, np.zeros_like(vals), vals)).T
        # Sort by: (missing)
        elif column == self.Columns.MISSING:
            return self._missing

        return None

    def _sortColumnData(self, column):
        """Allow sorting with 2d arrays."""
        data = np.asarray(self.sortColumnData(column))
        data = data[self.mapToSourceRows(Ellipsis)]

        assert data.ndim <= 2, 'Data should be at most 2-dimensional'
        return data

    def _argsortData(self, data, order):
        if data.ndim == 1:
            indices = np.argsort(data, kind='mergesort')
            if order == Qt.DescendingOrder:
                indices = indices[::-1]
            # Always sort NaNs last
            if np.issubdtype(data.dtype, np.number):
                indices = np.roll(indices, -np.isnan(data).sum())
        else:
            assert np.issubdtype(data.dtype, np.number), \
                'We do not deal with non numeric values in sorting by ' \
                'multiple values'
            if order == Qt.DescendingOrder:
                data[:, -1] = -data[:, -1]

            # In order to make sure NaNs always appear at the end, insert a
            # indicator whether NaN or not. Note that the data array must
            # contain an empty column of zeros at index -2 since inserting an
            # extra column after the fact can result in a MemoryError for data
            # with a large amount of variables
            assert np.all(data[:, -2] == 0), \
                'Add an empty column of zeros at index -2 to accomodate NaNs'
            np.isnan(data[:, -1], out=data[:, -2])

            indices = np.lexsort(np.flip(data.T, axis=0))

        return indices

    def headerData(self, section, orientation, role):
        # type: (int, Qt.Orientation, Qt.ItemDataRole) -> Any
        if orientation == Qt.Horizontal:
            if role == Qt.DisplayRole:
                return self.Columns.from_index(section).name

        return None

    def data(self, index, role):
        # type: (QModelIndex, Qt.ItemDataRole) -> Any
        def background():
            if attribute in self.domain.attributes:
                return self.COLOR_FOR_ROLE[self.ATTRIBUTE]
            if attribute in self.domain.metas:
                return self.COLOR_FOR_ROLE[self.META]
            if attribute in self.domain.class_vars:
                return self.COLOR_FOR_ROLE[self.CLASS_VAR]
            return None

        def text_alignment():
            if column == self.Columns.NAME:
                return Qt.AlignLeft | Qt.AlignVCenter
            return Qt.AlignRight | Qt.AlignVCenter

        def decoration():
            if column == self.Columns.ICON:
                return gui.attributeIconDict[attribute]
            return None

        def display():
            # pylint: disable=too-many-branches
            def render_value(value):
                return "" if np.isnan(value) else attribute.str_val(value)

            if column == self.Columns.NAME:
                return attribute.name
            elif column == self.Columns.DISTRIBUTION:
                if isinstance(attribute,
                              (DiscreteVariable, ContinuousVariable)):
                    if row not in self.__distributions_cache:
                        scene = QGraphicsScene(parent=self)
                        histogram = Histogram(
                            data=self.table,
                            variable=attribute,
                            color_attribute=self.target_var,
                            border=(0, 0, 2, 0),
                            border_color='#ccc',
                        )
                        scene.addItem(histogram)
                        self.__distributions_cache[row] = scene
                    return self.__distributions_cache[row]
            elif column == self.Columns.CENTER:
                return render_value(self._center[row])
            elif column == self.Columns.DISPERSION:
                if isinstance(attribute, TimeVariable):
                    return format_time_diff(self._min[row], self._max[row])
                elif isinstance(attribute, DiscreteVariable):
                    return "%.3g" % self._dispersion[row]
                else:
                    return render_value(self._dispersion[row])
            elif column == self.Columns.MIN:
                if not isinstance(attribute, DiscreteVariable) \
                        or attribute.ordered:
                    return render_value(self._min[row])
            elif column == self.Columns.MAX:
                if not isinstance(attribute, DiscreteVariable) \
                        or attribute.ordered:
                    return render_value(self._max[row])
            elif column == self.Columns.MISSING:
                return '%d (%d%%)' % (
                    self._missing[row],
                    100 * self._missing[row] / self.n_instances
                )
            return None

        roles = {Qt.BackgroundRole: background,
                 Qt.TextAlignmentRole: text_alignment,
                 Qt.DecorationRole: decoration,
                 Qt.DisplayRole: display}

        if not index.isValid() or role not in roles:
            return None

        row, column = self.mapToSourceRows(index.row()), index.column()
        # Make sure we're not out of range
        if not 0 <= row <= self.n_attributes:
            return None

        attribute = self.variables[row]
        return roles[role]()

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

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

    def set_target_var(self, variable):
        self.target_var = variable
        self.__distributions_cache.clear()
        start_idx = self.index(0, self.Columns.DISTRIBUTION)
        end_idx = self.index(self.rowCount(), self.Columns.DISTRIBUTION)
        self.dataChanged.emit(start_idx, end_idx)


class FeatureStatisticsTableView(QTableView):
    HISTOGRAM_ASPECT_RATIO = (7, 3)
    MINIMUM_HISTOGRAM_HEIGHT = 50
    MAXIMUM_HISTOGRAM_HEIGHT = 80

    def __init__(self, model, parent=None, **kwargs):
        super().__init__(
            parent=parent,
            showGrid=False,
            cornerButtonEnabled=False,
            sortingEnabled=True,
            selectionBehavior=QTableView.SelectRows,
            selectionMode=QTableView.ExtendedSelection,
            horizontalScrollMode=QTableView.ScrollPerPixel,
            verticalScrollMode=QTableView.ScrollPerPixel,
            **kwargs
        )
        self.setModel(model)

        hheader = self.horizontalHeader()
        hheader.setStretchLastSection(False)
        # Contents precision specifies how many rows should be taken into
        # account when computing the sizes, 0 being the visible rows. This is
        # crucial, since otherwise the `ResizeToContents` section resize mode
        # would call `sizeHint` on every single row in the data before first
        # render. However this, this cannot be used here, since this only
        # appears to work properly when the widget is actually shown. When the
        # widget is not shown, size `sizeHint` is called on every row.
        hheader.setResizeContentsPrecision(5)
        # Set a nice default size so that headers have some space around titles
        hheader.setDefaultSectionSize(100)
        # Set individual column behaviour in `set_data` since the logical
        # indices must be valid in the model, which requires data.
        hheader.setSectionResizeMode(QHeaderView.Interactive)

        columns = model.Columns
        hheader.setSectionResizeMode(columns.ICON.index, QHeaderView.ResizeToContents)
        hheader.setSectionResizeMode(columns.DISTRIBUTION.index, QHeaderView.Stretch)

        vheader = self.verticalHeader()
        vheader.setVisible(False)
        vheader.setSectionResizeMode(QHeaderView.Fixed)
        hheader.sectionResized.connect(self.bind_histogram_aspect_ratio)
        # TODO: This shifts the scrollarea a bit down when opening widget
        # hheader.sectionResized.connect(self.keep_row_centered)

        self.setItemDelegate(NoFocusRectDelegate(parent=self))
        self.setItemDelegateForColumn(
            FeatureStatisticsTableModel.Columns.DISTRIBUTION,
            DistributionDelegate(parent=self),
        )

    def bind_histogram_aspect_ratio(self, logical_index, _, new_size):
        """Force the horizontal and vertical header to maintain the defined
        aspect ratio specified for the histogram."""
        # Prevent function being exectued more than once per resize
        if logical_index is not self.model().Columns.DISTRIBUTION.index:
            return
        ratio_width, ratio_height = self.HISTOGRAM_ASPECT_RATIO
        unit_width = new_size / ratio_width
        new_height = unit_width * ratio_height
        effective_height = max(new_height, self.MINIMUM_HISTOGRAM_HEIGHT)
        effective_height = min(effective_height, self.MAXIMUM_HISTOGRAM_HEIGHT)
        self.verticalHeader().setDefaultSectionSize(effective_height)

    def keep_row_centered(self, logical_index, _1, _2):
        """When resizing the widget when scrolled further down, the
        positions of rows changes. Obviously, the user resized in order to
        better see the row of interest. This keeps that row centered."""
        # TODO: This does not work properly
        # Prevent function being exectued more than once per resize
        if logical_index is not self.model().Columns.DISTRIBUTION.index:
            return
        top_row = self.indexAt(self.rect().topLeft()).row()
        bottom_row = self.indexAt(self.rect().bottomLeft()).row()
        middle_row = top_row + (bottom_row - top_row) // 2
        self.scrollTo(self.model().index(middle_row, 0), QTableView.PositionAtCenter)


class NoFocusRectDelegate(QStyledItemDelegate):
    """Removes the light blue background and border on a focused item."""

    def paint(self, painter, option, index):
        # type: (QPainter, QStyleOptionViewItem, QModelIndex) -> None
        option.state &= ~QStyle.State_HasFocus
        super().paint(painter, option, index)


class DistributionDelegate(QStyledItemDelegate):
    def paint(self, painter, option, index):
        # type: (QPainter, QStyleOptionViewItem, QModelIndex) -> None
        scene = index.data(Qt.DisplayRole)  # type: Optional[QGraphicsScene]
        if scene is None:
            return super().paint(painter, option, index)

        painter.setRenderHint(QPainter.Antialiasing)

        if option.state & QStyle.State_Selected:
            background_color = option.palette.highlight()
        else:
            background_color = index.data(Qt.BackgroundRole)
        if background_color is not None:
            scene.setBackgroundBrush(background_color)

        scene.render(painter, target=QRectF(option.rect), mode=Qt.IgnoreAspectRatio)

        # pylint complains about inconsistent return statements
        return None


class OWFeatureStatistics(widget.OWWidget):
    name = 'Feature Statistics'
    description = 'Show basic statistics for data features.'
    icon = 'icons/FeatureStatistics.svg'

    class Inputs:
        data = Input('Data', Table, default=True)

    class Outputs:
        reduced_data = Output('Reduced Data', Table, default=True)
        statistics = Output('Statistics', Table)

    want_main_area = True
    buttons_area_orientation = Qt.Vertical

    settingsHandler = DomainContextHandler()
    settings_version = 2

    auto_commit = Setting(True)
    color_var = ContextSetting(None)  # type: Optional[Variable]
    # filter_string = ContextSetting('')

    sorting = Setting((0, Qt.DescendingOrder))
    selected_vars = ContextSetting([], schema_only=True)

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

        self.data = None  # type: Optional[Table]

        # TODO: Implement filtering on the model
        # filter_box = gui.vBox(self.controlArea, 'Filter')
        # self.filter_text = gui.lineEdit(
        #     filter_box, self, value='filter_string',
        #     placeholderText='Filter variables by name',
        #     callback=self._filter_table_variables, callbackOnType=True,
        # )
        # shortcut = QShortcut(QKeySequence('Ctrl+f'), self, self.filter_text.setFocus)
        # shortcut.setWhatsThis('Filter variables by name')

        self.color_var_model = DomainModel(
            valid_types=(ContinuousVariable, DiscreteVariable),
            placeholder='None',
        )
        box = gui.vBox(self.controlArea, 'Histogram')
        self.cb_color_var = gui.comboBox(
            box, master=self, value='color_var', model=self.color_var_model,
            label='Color:', orientation=Qt.Horizontal, contentsLength=13,
            searchable=True
        )
        self.cb_color_var.activated.connect(self.__color_var_changed)

        gui.rubber(self.controlArea)
        gui.auto_send(self.buttonsArea, self, "auto_commit")

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

        # Main area
        self.model = FeatureStatisticsTableModel(parent=self)
        self.table_view = FeatureStatisticsTableView(self.model, parent=self)
        self.table_view.selectionModel().selectionChanged.connect(self.on_select)
        self.table_view.horizontalHeader().sectionClicked.connect(self.on_header_click)

        self.mainArea.layout().addWidget(self.table_view)

    @staticmethod
    def sizeHint():
        return QSize(1050, 500)

    def _filter_table_variables(self):
        regex = QRegExp(self.filter_string)
        # If the user explicitly types different cases, we assume they know
        # what they are searching for and account for letter case in filter
        different_case = (
            any(c.islower() for c in self.filter_string) and
            any(c.isupper() for c in self.filter_string)
        )
        if not different_case:
            regex.setCaseSensitivity(Qt.CaseInsensitive)

    @Inputs.data
    def set_data(self, data):
        # Clear outputs and reset widget state
        self.closeContext()
        self.selected_vars = []
        self.model.resetSorting()
        self.Outputs.reduced_data.send(None)
        self.Outputs.statistics.send(None)

        # Setup widget state for new data and restore settings
        self.data = data

        if data is not None:
            self.info.set_input_summary(len(data),
                                        format_summary_details(data))
            self.color_var_model.set_domain(data.domain)
            self.color_var = None
            if self.data.domain.class_vars:
                self.color_var = self.data.domain.class_vars[0]
        else:
            self.info.set_input_summary(self.info.NoInput)
            self.color_var_model.set_domain(None)
            self.color_var = None
        self.model.set_data(data)

        self.openContext(self.data)
        self.__restore_selection()
        self.__restore_sorting()
        # self._filter_table_variables()
        self.__color_var_changed()

        self.commit()

    def __restore_selection(self):
        """Restore the selection on the table view from saved settings."""
        selection_model = self.table_view.selectionModel()
        selection = QItemSelection()
        if self.selected_vars:
            var_indices = {var: i for i, var in enumerate(self.model.variables)}
            selected_indices = [var_indices[var] for var in self.selected_vars]
            for row in self.model.mapFromSourceRows(selected_indices):
                selection.append(QItemSelectionRange(
                    self.model.index(row, 0),
                    self.model.index(row, self.model.columnCount() - 1)
                ))
        selection_model.select(selection, QItemSelectionModel.ClearAndSelect)

    def __restore_sorting(self):
        """Restore the sort column and order from saved settings."""
        sort_column, sort_order = self.sorting
        if self.model.n_attributes and sort_column < self.model.columnCount():
            self.model.sort(sort_column, sort_order)
            self.table_view.horizontalHeader().setSortIndicator(sort_column, sort_order)

    @pyqtSlot(int)
    def on_header_click(self, *_):
        # Store the header states
        sort_order = self.model.sortOrder()
        sort_column = self.model.sortColumn()
        self.sorting = sort_column, sort_order

    @pyqtSlot(int)
    def __color_var_changed(self, *_):
        if self.model is not None:
            self.model.set_target_var(self.color_var)

    def on_select(self):
        selection_indices = list(self.model.mapToSourceRows([
            i.row() for i in self.table_view.selectionModel().selectedRows()
        ]))
        self.selected_vars = list(self.model.variables[selection_indices])
        self.commit()

    def commit(self):
        if not self.selected_vars:
            self.info.set_output_summary(self.info.NoOutput)
            self.Outputs.reduced_data.send(None)
            self.Outputs.statistics.send(None)
            return

        # Send a table with only selected columns to output
        variables = self.selected_vars
        self.info.set_output_summary(len(self.data[:, variables]),
                                     format_summary_details(self.data[:, variables]))
        self.Outputs.reduced_data.send(self.data[:, variables])

        # Send the statistics of the selected variables to ouput
        labels, data = self.model.get_statistics_matrix(variables, return_labels=True)
        var_names = np.atleast_2d([var.name for var in variables]).T
        domain = Domain(
            attributes=[ContinuousVariable(name) for name in labels],
            metas=[StringVariable('Feature')]
        )
        statistics = Table(domain, data, metas=var_names)
        statistics.name = '%s (Feature Statistics)' % self.data.name
        self.Outputs.statistics.send(statistics)

    def send_report(self):
        pass

    @classmethod
    def migrate_context(cls, context, version):
        if not version or version < 2:
            selected_rows = context.values.pop("selected_rows", None)
            if not selected_rows:
                selected_vars = []
            else:
                # This assumes that dict was saved by Python >= 3.6 so dict is
                # ordered; if not, context hasn't had worked anyway.
                all_vars = [
                    (var, tpe)
                    for (var, tpe) in chain(context.attributes.items(),
                                            context.metas.items())
                    # it would be nicer to use cls.HIDDEN_VAR_TYPES, but there
                    # is no suitable conversion function, and StringVariable (3)
                    # was the only hidden var when settings_version < 2, so:
                    if tpe != 3]
                selected_vars = [all_vars[i] for i in selected_rows]
            context.values["selected_vars"] = selected_vars, -3


if __name__ == '__main__':  # pragma: no cover
    WidgetPreview(OWFeatureStatistics).run(Table("iris"))

```
--- END FILE ---

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

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

Here is an example:

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

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