diff --git a/Orange/widgets/unsupervised/owhierarchicalclustering.py b/Orange/widgets/unsupervised/owhierarchicalclustering.py index 62b742cd0a8..f71cc12ba72 100644 --- a/Orange/widgets/unsupervised/owhierarchicalclustering.py +++ b/Orange/widgets/unsupervised/owhierarchicalclustering.py @@ -19,11 +19,12 @@ from Orange.widgets.utils.localization import pl from orangewidget.utils.itemmodels import PyListModel +from orangewidget.utils.signals import LazyValue import Orange.data from Orange.data.domain import filter_visible from Orange.data import Domain, DiscreteVariable, ContinuousVariable, \ - StringVariable + StringVariable, Table import Orange.misc from Orange.clustering.hierarchical import \ postorder, preorder, Tree, tree_from_linkage, dist_matrix_linkage, \ @@ -32,8 +33,11 @@ from Orange.widgets import widget, gui, settings from Orange.widgets.utils import itemmodels, combobox -from Orange.widgets.utils.annotated_data import (create_annotated_table, - ANNOTATED_DATA_SIGNAL_NAME) +from Orange.widgets.utils.annotated_data import (lazy_annotated_table, + ANNOTATED_DATA_SIGNAL_NAME, + domain_with_annotation_column, + add_columns, + create_annotated_table) from Orange.widgets.utils.widgetpreview import WidgetPreview from Orange.widgets.visualize.utils.plotutils import AxisItem from Orange.widgets.widget import Input, Output, Msg @@ -776,71 +780,73 @@ def commit(self): for node in selection] selected_indices = list(chain(*maps)) - unselected_indices = sorted(set(range(self.root.value.last)) - - set(selected_indices)) if not selected_indices: self.Outputs.selected_data.send(None) - annotated_data = create_annotated_table(items, []) \ + annotated_data = lazy_annotated_table(items, []) \ if self.selection_method == 0 and self.matrix.axis else None self.Outputs.annotated_data.send(annotated_data) return - selected_data = None + selected_data = annotated_data = None if isinstance(items, Orange.data.Table) and self.matrix.axis == 1: # Select rows - c = np.zeros(self.matrix.shape[0]) + data, domain = items, items.domain + c = np.full(self.matrix.shape[0], len(maps)) for i, indices in enumerate(maps): c[indices] = i - c[unselected_indices] = len(maps) - - mask = c != len(maps) - - data, domain = items, items.domain - attrs = domain.attributes - classes = domain.class_vars - metas = domain.metas - var_name = get_unique_names(domain, "Cluster") + clust_name = get_unique_names(domain, "Cluster") values = [f"C{i + 1}" for i in range(len(maps))] - clust_var = Orange.data.DiscreteVariable( - var_name, values=values + ["Other"]) - domain = Orange.data.Domain(attrs, classes, metas + (clust_var,)) - data = items.transform(domain) - with data.unlocked(data.metas): - data.set_column(clust_var, c) - - if selected_indices: - selected_data = data[mask] - clust_var = Orange.data.DiscreteVariable( - var_name, values=values) - selected_data.domain = Domain( - attrs, classes, metas + (clust_var, )) - - annotated_data = create_annotated_table(data, selected_indices) + sel_clust_var = Orange.data.DiscreteVariable( + name=clust_name, values=values) + sel_domain = add_columns(domain, metas=(sel_clust_var,)) + selected_data = LazyValue[Table]( + lambda: items.add_column( + sel_clust_var, c, to_metas=True)[c != len(maps)], + domain=sel_domain, length=len(selected_indices)) + + ann_clust_var = Orange.data.DiscreteVariable( + name=clust_name, values=values + ["Other"] + ) + ann_domain = add_columns( + domain_with_annotation_column(data)[0], metas=(ann_clust_var, )) + annotated_data = LazyValue[Table]( + lambda: create_annotated_table( + data=items.add_column(ann_clust_var, c, to_metas=True), + selected_indices=selected_indices), + domain=ann_domain, length=len(items) + ) elif isinstance(items, Orange.data.Table) and self.matrix.axis == 0: # Select columns attrs = [] + unselected_indices = sorted(set(range(self.root.value.last)) - + set(selected_indices)) for clust, indices in chain(enumerate(maps, start=1), [(0, unselected_indices)]): for i in indices: attr = items.domain[i].copy() attr.attributes["cluster"] = clust attrs.append(attr) - domain = Orange.data.Domain( + all_domain = Orange.data.Domain( # len(unselected_indices) can be 0 attrs[:len(attrs) - len(unselected_indices)], items.domain.class_vars, items.domain.metas) - selected_data = items.from_table(domain, items) - domain = Orange.data.Domain( + selected_data = LazyValue[Table]( + lambda: items.from_table(all_domain, items), + domain=all_domain, length=len(items)) + + sel_domain = Orange.data.Domain( attrs, items.domain.class_vars, items.domain.metas) - annotated_data = items.from_table(domain, items) + annotated_data = LazyValue[Table]( + lambda: items.from_table(sel_domain, items), + domain=sel_domain, length=len(items)) self.Outputs.selected_data.send(selected_data) self.Outputs.annotated_data.send(annotated_data) diff --git a/Orange/widgets/utils/annotated_data.py b/Orange/widgets/utils/annotated_data.py index 46149c15291..401520d3201 100644 --- a/Orange/widgets/utils/annotated_data.py +++ b/Orange/widgets/utils/annotated_data.py @@ -1,5 +1,10 @@ +from typing import Union + import numpy as np -from Orange.data import Domain, DiscreteVariable + +from orangewidget.utils.signals import LazyValue + +from Orange.data import Domain, DiscreteVariable, Table from Orange.data.util import get_unique_names ANNOTATED_DATA_SIGNAL_NAME = "Data" @@ -30,16 +35,26 @@ def add_columns(domain, attributes=(), class_vars=(), metas=()): return Domain(attributes, class_vars, metas) +def domain_with_annotation_column( + data: Union[Table, Domain], + values=("No", "Yes"), + var_name=ANNOTATED_DATA_FEATURE_NAME): + domain = data if isinstance(data, Domain) else data.domain + var = DiscreteVariable(get_unique_names(domain, var_name), values) + class_vars, metas = domain.class_vars, domain.metas + if not domain.class_vars: + class_vars += (var, ) + else: + metas += (var, ) + return Domain(domain.attributes, class_vars, metas), var + + def _table_with_annotation_column(data, values, column_data, var_name): - var = DiscreteVariable(get_unique_names(data.domain, var_name), values) - class_vars, metas = data.domain.class_vars, data.domain.metas + domain, var = domain_with_annotation_column(data, values, var_name) if not data.domain.class_vars: - class_vars += (var, ) column_data = column_data.reshape((len(data), )) else: - metas += (var, ) column_data = column_data.reshape((len(data), 1)) - domain = Domain(data.domain.attributes, class_vars, metas) table = data.transform(domain) with table.unlocked(table.Y if not data.domain.class_vars else table.metas): table[:, var] = column_data @@ -65,17 +80,20 @@ def create_annotated_table(data, selected_indices): data, ("No", "Yes"), annotated, ANNOTATED_DATA_FEATURE_NAME) +def lazy_annotated_table(data, selected_indices): + domain, _ = domain_with_annotation_column(data) + return LazyValue[Table]( + lambda: create_annotated_table(data, selected_indices), + length=len(data), domain=domain) + + def create_groups_table(data, selection, include_unselected=True, var_name=ANNOTATED_DATA_FEATURE_NAME, values=None): if data is None: return None - max_sel = np.max(selection) - if values is None: - values = ["G{}".format(i + 1) for i in range(max_sel)] - if include_unselected: - values.append("Unselected") + values, max_sel = group_values(selection, include_unselected, values) if include_unselected: # Place Unselected instances in the "last group", so that the group # colors and scatter diagram marker colors will match @@ -88,3 +106,24 @@ def create_groups_table(data, selection, data = data[mask] selection = selection[mask] - 1 return _table_with_annotation_column(data, values, selection, var_name) + + +def lazy_groups_table(data, selection, include_unselected=True, + var_name=ANNOTATED_DATA_FEATURE_NAME, values=None): + length = len(data) if include_unselected else np.sum(selection != 0) + values, _ = group_values(selection, include_unselected, values) + domain, _ = domain_with_annotation_column(data, values, var_name) + return LazyValue[Table]( + lambda: create_groups_table(data, selection, include_unselected, + var_name, values), + length=length, domain=domain + ) + + +def group_values(selection, include_unselected, values): + max_sel = np.max(selection) + if values is None: + values = ["G{}".format(i + 1) for i in range(max_sel)] + if include_unselected: + values.append("Unselected") + return values, max_sel diff --git a/Orange/widgets/utils/state_summary.py b/Orange/widgets/utils/state_summary.py index d43037dcf3b..964b48eaba6 100644 --- a/Orange/widgets/utils/state_summary.py +++ b/Orange/widgets/utils/state_summary.py @@ -1,10 +1,11 @@ from datetime import date from html import escape +from typing import Union from AnyQt.QtCore import Qt from Orange.widgets.utils.localization import pl -from orangewidget.utils.signals import summarize, PartialSummary +from orangewidget.utils.signals import summarize, PartialSummary, LazyValue from Orange.widgets.utils.itemmodels import TableModel from Orange.widgets.utils.tableview import TableView from Orange.widgets.utils.distmatrixmodel import \ @@ -12,7 +13,7 @@ from Orange.data import ( StringVariable, DiscreteVariable, ContinuousVariable, TimeVariable, - Table + Table, Domain ) from Orange.evaluation import Results @@ -62,64 +63,65 @@ def format_variables_string(variables): # `format` is a good name for the argument, pylint: disable=redefined-builtin -def format_summary_details(data, format=Qt.PlainText): +def format_summary_details(data: Union[Table, Domain], + format=Qt.PlainText, missing=None): """ A function that forms the entire descriptive part of the input/output summary. :param data: A dataset - :type data: Orange.data.Table + :type data: Orange.data.Table or Orange.data.Domain :return: A formatted string """ if data is None: return "" - if format == Qt.PlainText: - def b(s): - return s - else: - def b(s): - return f"{s}" - - features_missing = "" - if len(data) * len(data.domain.attributes) < COMPUTE_NANS_LIMIT: - features_missing = missing_values(data.get_nan_frequency_attribute()) - n_features = len(data.domain.variables) + len(data.domain.metas) - name = getattr(data, "name", None) - if name == "untitled": + features_missing = "" if missing is None else missing_values(missing) + if isinstance(data, Domain): + domain = data name = None - - basic = f'{len(data):n} {pl(len(data), "instance")}, ' \ - f'{n_features} {pl(n_features, "variable")}' - - features = format_variables_string(data.domain.attributes) - features = f'Features: {features} {features_missing}' - - targets = format_variables_string(data.domain.class_vars) + basic = "" + else: + assert isinstance(data, Table) + domain = data.domain + if not features_missing and \ + len(data) * len(domain.attributes) < COMPUTE_NANS_LIMIT: + features_missing \ + = missing_values(data.get_nan_frequency_attribute()) + name = getattr(data, "name", None) + if name == "untitled": + name = None + basic = f'{len(data):n} {pl(len(data), "instance")}, ' + + n_features = len(domain.variables) + len(domain.metas) + basic += f'{n_features} {pl(n_features, "variable")}' + + features = format_variables_string(domain.attributes) + features = f'Features: {features}{features_missing}' + + targets = format_variables_string(domain.class_vars) targets = f'Target: {targets}' - metas = format_variables_string(data.domain.metas) + metas = format_variables_string(domain.metas) metas = f'Metas: {metas}' if format == Qt.PlainText: - details = "" - if name: - details += f"{name}: " + details = f"{name}: " if name else "Table with " details += f"{basic}\n{features}\n{targets}" - if data.domain.metas: + if domain.metas: details += f"\n{metas}" else: descs = [] if name: descs.append(_nobr(f"{escape(name)}: {basic}")) else: - descs.append(_nobr(basic)) + descs.append(_nobr(f"Table with {basic}")) - if data.domain.variables: + if domain.variables: descs.append(_nobr(features)) - if data.domain.class_vars: + if domain.class_vars: descs.append(_nobr(targets)) - if data.domain.metas: + if domain.metas: descs.append(_nobr(metas)) details = '
'.join(descs) @@ -129,11 +131,11 @@ def b(s): def missing_values(value): if value: - return f'({value*100:.1f}% missing values)' + return f' ({value*100:.1f}% missing values)' elif value is None: return '' else: - return '(no missing values)' + return ' (no missing values)' def format_multiple_summaries(data_list, type_io='input'): @@ -176,15 +178,31 @@ def _nobr(s): @summarize.register def summarize_table(data: Table): # pylint: disable=function-redefined - def previewer(): - view = TableView(selectionMode=TableView.NoSelection) - view.setModel(TableModel(data)) - return view - return PartialSummary( data.approx_len(), format_summary_details(data, format=Qt.RichText), - previewer) + lambda: _table_previewer(data)) + + +@summarize.register +def summarize_table(data: LazyValue[Table]): + if data.is_cached: + return summarize(data.get_value()) + + length = getattr(data, "length", "?") + details = format_summary_details(data.domain, format=Qt.RichText, + missing=getattr(data, "missing", None)) \ + if hasattr(data, "domain") else "data available, but not prepared yet" + return PartialSummary( + length, + details, + lambda: _table_previewer(data.get_value())) + + +def _table_previewer(data): + view = TableView(selectionMode=TableView.NoSelection) + view.setModel(TableModel(data)) + return view @summarize.register diff --git a/Orange/widgets/utils/tests/test_annotated_data.py b/Orange/widgets/utils/tests/test_annotated_data.py index a2b5647d3e6..47bdda3910a 100644 --- a/Orange/widgets/utils/tests/test_annotated_data.py +++ b/Orange/widgets/utils/tests/test_annotated_data.py @@ -1,12 +1,16 @@ +from unittest.mock import patch + import random import unittest import numpy as np -from Orange.data import Table, Domain, StringVariable, DiscreteVariable +from Orange.data import Table, Domain, StringVariable, DiscreteVariable, \ + ContinuousVariable from Orange.data.filter import SameValue from Orange.widgets.utils.annotated_data import ( - create_annotated_table, create_groups_table, ANNOTATED_DATA_FEATURE_NAME + create_annotated_table, create_groups_table, ANNOTATED_DATA_FEATURE_NAME, + lazy_annotated_table, lazy_groups_table, domain_with_annotation_column ) @@ -15,6 +19,42 @@ def setUp(self): random.seed(42) self.zoo = Table("zoo") + def test_domain_with_annotation_column(self): + a, b, c = (ContinuousVariable(x) for x in "abc") + + x = [[1, 2, 3], [4, 5, 6]] + + for data in (dabc := Domain([a, b, c]), Table.from_list(dabc, x)): + dom, var = domain_with_annotation_column(data) + self.assertEqual(dom.attributes, (a, b, c)) + self.assertIs(dom.class_var, var) + self.assertEqual(var.name, ANNOTATED_DATA_FEATURE_NAME) + self.assertEqual(var.values, ("No", "Yes")) + + dom, var = domain_with_annotation_column( + data, values=tuple("xyz"), var_name="d") + self.assertEqual(dom.attributes, (a, b, c)) + self.assertIs(dom.class_var, var) + self.assertEqual(var.name, "d") + self.assertEqual(var.values, tuple("xyz")) + + for data in (dabc := Domain([a, b], c), Table.from_list(dabc, x)): + dom, var = domain_with_annotation_column( + data, values=tuple("xyz"), var_name="d") + self.assertEqual(dom.attributes, (a, b)) + self.assertIs(dom.class_var, c) + self.assertEqual(dom.metas, (var, )) + self.assertEqual(var.name, "d") + self.assertEqual(var.values, tuple("xyz")) + + dom, var = domain_with_annotation_column( + data, values=tuple("xyz"), var_name="c") + self.assertEqual(dom.attributes, (a, b)) + self.assertIs(dom.class_var, c) + self.assertEqual(dom.metas, (var, )) + self.assertEqual(var.name, "c (1)") + self.assertEqual(var.values, tuple("xyz")) + def test_create_annotated_table(self): annotated = create_annotated_table(self.zoo, list(range(10))) @@ -129,3 +169,52 @@ def test_create_groups_table_set_values(self): values = ("this", "that", "rest") table = create_groups_table(self.zoo, selection, values=values) self.assertEqual(tuple(table.domain["Selected"].values), values) + + @patch("Orange.widgets.utils.annotated_data.create_annotated_table") + def test_lazy_annotated_table(self, creator): + selected_indices = np.array([1, 2, 3]) + lazy_table = lazy_annotated_table(self.zoo, selected_indices) + self.assertEqual(lazy_table.length, len(self.zoo)) + self.assertEqual(lazy_table.domain.attributes, self.zoo.domain.attributes) + self.assertEqual(lazy_table.domain.class_var, self.zoo.domain.class_var) + self.assertEqual(len(lazy_table.domain.metas), 2) + var = lazy_table.domain.metas[1] + self.assertIsInstance(var, DiscreteVariable) + self.assertEqual(var.name, ANNOTATED_DATA_FEATURE_NAME) + creator.assert_not_called() + self.assertIs(lazy_table.get_value(), creator.return_value) + + @patch("Orange.widgets.utils.annotated_data.create_groups_table") + def test_lazy_groups_table(self, creator): + group_indices = np.zeros(len(self.zoo), dtype=int) + group_indices[10:15] = 1 + + lazy_table = lazy_groups_table(self.zoo, group_indices) + self.assertEqual(lazy_table.length, len(self.zoo)) + self.assertEqual(lazy_table.domain.attributes, self.zoo.domain.attributes) + self.assertEqual(lazy_table.domain.class_var, self.zoo.domain.class_var) + self.assertEqual(len(lazy_table.domain.metas), 2) + var = lazy_table.domain.metas[1] + self.assertIsInstance(var, DiscreteVariable) + self.assertEqual(var.name, ANNOTATED_DATA_FEATURE_NAME) + creator.assert_not_called() + self.assertIs(lazy_table.get_value(), creator.return_value) + creator.reset_mock() + + lazy_table = lazy_groups_table( + self.zoo, group_indices, include_unselected=False, var_name="foo", + values=("bar", "baz")) + self.assertEqual(lazy_table.length, 5) + self.assertEqual(lazy_table.domain.attributes, self.zoo.domain.attributes) + self.assertEqual(lazy_table.domain.class_var, self.zoo.domain.class_var) + self.assertEqual(len(lazy_table.domain.metas), 2) + var = lazy_table.domain.metas[1] + self.assertIsInstance(var, DiscreteVariable) + self.assertEqual(var.name, "foo") + self.assertEqual(var.values, ("bar", "baz")) + creator.assert_not_called() + self.assertIs(lazy_table.get_value(), creator.return_value) + + +if __name__ == "__main__": + unittest.main() diff --git a/Orange/widgets/utils/tests/test_state_summary.py b/Orange/widgets/utils/tests/test_state_summary.py index 3d09b49e131..fe5ea27bab8 100644 --- a/Orange/widgets/utils/tests/test_state_summary.py +++ b/Orange/widgets/utils/tests/test_state_summary.py @@ -1,10 +1,15 @@ import unittest -from unittest.mock import patch +from unittest.mock import patch, Mock import datetime from collections import namedtuple import numpy as np +from AnyQt.QtCore import Qt + +from orangecanvas.scheme.signalmanager import LazyValue +from orangewidget.utils.signals import summarize + from Orange.data import Table, Domain, StringVariable, ContinuousVariable, \ DiscreteVariable, TimeVariable from Orange.widgets.tests.base import WidgetTest @@ -116,6 +121,12 @@ def test_details(self): f'Metas: string' self.assertEqual(details, format_summary_details(data)) + details = f'Table with {n_features} variables\n' \ + f'Features: {len(data.domain.attributes)} categorical\n' \ + f'Target: categorical\n' \ + f'Metas: string' + self.assertEqual(details, format_summary_details(data.domain)) + data = Table('housing') n_features = len(data.domain.variables) + len(data.domain.metas) details = f'housing: {len(data)} instances, ' \ @@ -139,7 +150,7 @@ def test_details(self): target=[rgb_full, rgb_missing], metas=[ints_full, ints_missing] ) n_features = len(data.domain.variables) + len(data.domain.metas) - details = f'{len(data)} instances, ' \ + details = f'Table with {len(data)} instances, ' \ f'{n_features} variables\n' \ f'Features: {len(data.domain.attributes)} numeric ' \ f'(10.0% missing values)\n' \ @@ -153,7 +164,7 @@ def test_details(self): metas=[string_full, string_missing] ) n_features = len(data.domain.variables) + len(data.domain.metas) - details = f'{len(data)} instances, ' \ + details = f'Table with {len(data)} instances, ' \ f'{n_features} variables\n' \ f'Features: {len(data.domain.attributes)} ' \ f'(2 categorical, 1 numeric, 1 time) (5.0% missing values)\n' \ @@ -164,7 +175,7 @@ def test_details(self): data = make_table([time_full, time_missing], target=[ints_missing], metas=None) - details = f'{len(data)} instances, ' \ + details = f'Table with {len(data)} instances, ' \ f'{len(data.domain.variables)} variables\n' \ f'Features: {len(data.domain.attributes)} time ' \ f'(10.0% missing values)\n' \ @@ -172,7 +183,7 @@ def test_details(self): self.assertEqual(details, format_summary_details(data)) data = make_table([rgb_full, ints_full], target=None, metas=None) - details = f'{len(data)} instances, ' \ + details = f'Table with {len(data)} instances, ' \ f'{len(data.domain.variables)} variables\n' \ f'Features: {len(data.domain.variables)} categorical ' \ f'(no missing values)\n' \ @@ -180,16 +191,16 @@ def test_details(self): self.assertEqual(details, format_summary_details(data)) data = make_table([rgb_full], target=None, metas=None) - details = f'{len(data)} instances, ' \ + details = f'Table with {len(data)} instances, ' \ f'{len(data.domain.variables)} variable\n' \ f'Features: categorical (no missing values)\n' \ f'Target: —' self.assertEqual(details, format_summary_details(data)) data = Table.from_numpy(domain=None, X=np.random.random((10000, 1000))) - details = f'{len(data):n} instances, ' \ + details = f'Table with {len(data):n} instances, ' \ f'{len(data.domain.variables)} variables\n' \ - f'Features: {len(data.domain.variables)} numeric \n' \ + f'Features: {len(data.domain.variables)} numeric\n' \ f'Target: —' with patch.object(Table, "get_nan_frequency_attribute") as mock: self.assertEqual(details, format_summary_details(data)) @@ -248,5 +259,61 @@ def test_multiple_summaries(self): format_multiple_summaries(outputs, type_io='output')) +class TestSummarize(unittest.TestCase): + @patch("Orange.widgets.utils.state_summary._table_previewer") + def test_summarize_table(self, previewer): + data = Table('zoo') + summary = summarize(data) + self.assertEqual(summary.summary, len(data)) + self.assertEqual(summary.details, + format_summary_details(data, format=Qt.RichText)) + previewer.assert_not_called() + summary.preview_func() + previewer.assert_called_with(data) + + @patch("Orange.widgets.utils.state_summary._table_previewer") + def test_summarize_lazy_table(self, previewer): + data = Table('zoo') + + # lazy_data of unknown length and domain + lazy_data = LazyValue[Table](lambda: data) + lazy_data.get_value = Mock(return_value=data) + summary = summarize(lazy_data) + self.assertEqual(summary.summary, "?") + self.assertIsInstance(summary.details, str) + lazy_data.get_value.assert_not_called() + previewer.assert_not_called() + summary.preview_func() + lazy_data.get_value.assert_called() + previewer.assert_called_with(data) + previewer.reset_mock() + + # lazy_data with length and domain hint + lazy_data = LazyValue[Table]( + lambda: data, length=123, domain=data.domain) + lazy_data.get_value = Mock(return_value=data) + summary = summarize(lazy_data) + self.assertEqual(summary.summary, 123) + self.assertEqual(summary.details, + format_summary_details(data.domain, format=Qt.RichText)) + lazy_data.get_value.assert_not_called() + previewer.assert_not_called() + summary.preview_func() + lazy_data.get_value.assert_called() + previewer.assert_called_with(data) + previewer.reset_mock() + + # lazy_data that is already cached: complete summary even without hints + lazy_data = LazyValue[Table](lambda: data) + lazy_data.get_value() + summary = summarize(lazy_data) + self.assertEqual(summary.summary, len(data)) + self.assertEqual(summary.details, + format_summary_details(data, format=Qt.RichText)) + previewer.assert_not_called() + summary.preview_func() + previewer.assert_called_with(data) + + if __name__ == "__main__": unittest.main() diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index f33d4c8a7b8..7a7ba552598 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -61,7 +61,7 @@ requirements: - pandas >=1.4.0,!=1.5.0,!=2.0.0 - pyyaml - orange-canvas-core >=0.1.30,<0.2a - - orange-widget-base >=4.20.0 + - orange-widget-base >=4.21.0 - openpyxl - httpx >=0.21 - baycomp >=1.0.2 diff --git a/requirements-gui.txt b/requirements-gui.txt index 52392deb7b8..12466671d8e 100644 --- a/requirements-gui.txt +++ b/requirements-gui.txt @@ -1,5 +1,5 @@ orange-canvas-core>=0.1.30,<0.2a -orange-widget-base>=4.20.0 +orange-widget-base>=4.21.0 AnyQt>=0.2.0 diff --git a/tox.ini b/tox.ini index 376f8f9a4a7..21f5e88f041 100644 --- a/tox.ini +++ b/tox.ini @@ -39,7 +39,7 @@ deps = latest: https://github.com/biolab/orange-canvas-core/archive/refs/heads/master.zip#egg=orange-canvas-core latest: https://github.com/biolab/orange-widget-base/archive/refs/heads/master.zip#egg=orange-widget-base oldest: orange-canvas-core==0.1.30 - oldest: orange-widget-base==4.20.0 + oldest: orange-widget-base==4.21.0 oldest: AnyQt==0.2.0 oldest: pyqtgraph>=0.13.1 oldest: matplotlib==3.2.0