diff --git a/Orange/widgets/data/owconcatenate.py b/Orange/widgets/data/owconcatenate.py
index 3ecc73681ba..3bd8cbae6f0 100644
--- a/Orange/widgets/data/owconcatenate.py
+++ b/Orange/widgets/data/owconcatenate.py
@@ -5,11 +5,10 @@
Concatenate (append) two or more datasets.
"""
-
from collections import OrderedDict, namedtuple, defaultdict
from functools import reduce
from itertools import chain, count
-from typing import List
+from typing import List, Optional, Sequence
import numpy as np
from AnyQt.QtWidgets import QFormLayout
@@ -21,9 +20,9 @@
from Orange.widgets import widget, gui, settings
from Orange.widgets.settings import Setting
from Orange.widgets.utils.annotated_data import add_columns
-from Orange.widgets.utils.sql import check_sql_input
+from Orange.widgets.utils.sql import check_sql_input, check_sql_input_sequence
from Orange.widgets.utils.widgetpreview import WidgetPreview
-from Orange.widgets.widget import Input, Output, Msg
+from Orange.widgets.widget import Input, MultiInput, Output, Msg
class OWConcatenate(widget.OWWidget):
@@ -35,10 +34,9 @@ class OWConcatenate(widget.OWWidget):
class Inputs:
primary_data = Input("Primary Data", Orange.data.Table)
- additional_data = Input("Additional Data",
- Orange.data.Table,
- multiple=True,
- default=True)
+ additional_data = MultiInput(
+ "Additional Data", Orange.data.Table, default=True
+ )
class Outputs:
data = Output("Data", Orange.data.Table)
@@ -86,7 +84,7 @@ def __init__(self):
super().__init__()
self.primary_data = None
- self.more_data = OrderedDict()
+ self._more_data_input: List[Optional[Orange.data.Table]] = []
self.mergebox = gui.vBox(self.controlArea, "Variable Merging")
box = gui.radioButtons(
@@ -158,12 +156,22 @@ def set_primary_data(self, data):
self.primary_data = data
@Inputs.additional_data
- @check_sql_input
- def set_more_data(self, data=None, sig_id=None):
- if data is not None:
- self.more_data[sig_id] = data
- elif sig_id in self.more_data:
- del self.more_data[sig_id]
+ @check_sql_input_sequence
+ def set_more_data(self, index, data):
+ self._more_data_input[index] = data
+
+ @Inputs.additional_data.insert
+ @check_sql_input_sequence
+ def insert_more_data(self, index, data):
+ self._more_data_input.insert(index, data)
+
+ @Inputs.additional_data.remove
+ def remove_more_data(self, index):
+ self._more_data_input.pop(index)
+
+ @property
+ def more_data(self) -> Sequence[Orange.data.Table]:
+ return [t for t in self._more_data_input if t is not None]
def handleNewSignals(self):
self.mergebox.setDisabled(self.primary_data is not None)
@@ -177,8 +185,8 @@ def incompatible_types(self):
types_ = set()
if self.primary_data is not None:
types_.add(type(self.primary_data))
- for key in self.more_data:
- types_.add(type(self.more_data[key]))
+ for table in self.more_data:
+ types_.add(type(table))
if len(types_) > 1:
return True
@@ -188,13 +196,13 @@ def apply(self):
self.Warning.renamed_variables.clear()
tables, domain, source_var = [], None, None
if self.primary_data is not None:
- tables = [self.primary_data] + list(self.more_data.values())
+ tables = [self.primary_data] + list(self.more_data)
domain = self.primary_data.domain
elif self.more_data:
if self.ignore_compute_value:
tables = self._dumb_tables()
else:
- tables = self.more_data.values()
+ tables = self.more_data
domains = [table.domain for table in tables]
domain = self.merge_domains(domains)
@@ -230,7 +238,7 @@ def enumerated_parts(domain):
return enumerate((domain.attributes, domain.class_vars, domain.metas))
compute_value_groups = defaultdict(set)
- for table in self.more_data.values():
+ for table in self.more_data:
for part, part_vars in enumerated_parts(table.domain):
for var in part_vars:
desc = (var.name, type(var), part)
@@ -240,7 +248,7 @@ def enumerated_parts(domain):
if len(compute_values) > 1}
dumb_tables = []
- for table in self.more_data.values():
+ for table in self.more_data:
dumb_domain = Orange.data.Domain(
*[[var.copy(compute_value=None)
if (var.name, type(var), part) in to_dumbify
@@ -352,5 +360,5 @@ def _unique_vars(seq: List[Orange.data.Variable]):
if __name__ == "__main__": # pragma: no cover
WidgetPreview(OWConcatenate).run(
- set_more_data=[(Orange.data.Table("iris"), 0),
- (Orange.data.Table("zoo"), 1)])
+ insert_more_data=[(0, Orange.data.Table("iris")),
+ (1, Orange.data.Table("zoo"))])
diff --git a/Orange/widgets/data/owpythonscript.py b/Orange/widgets/data/owpythonscript.py
index 656adeed74d..fe527a37170 100644
--- a/Orange/widgets/data/owpythonscript.py
+++ b/Orange/widgets/data/owpythonscript.py
@@ -35,7 +35,7 @@
from Orange.widgets.utils import itemmodels
from Orange.widgets.settings import Setting
from Orange.widgets.utils.widgetpreview import WidgetPreview
-from Orange.widgets.widget import OWWidget, Input, Output
+from Orange.widgets.widget import OWWidget, MultiInput, Output
if TYPE_CHECKING:
from typing_extensions import TypedDict
@@ -526,14 +526,18 @@ class OWPythonScript(OWWidget):
keywords = ["program", "function"]
class Inputs:
- data = Input("Data", Table, replaces=["in_data"],
- default=True, multiple=True)
- learner = Input("Learner", Learner, replaces=["in_learner"],
- default=True, multiple=True)
- classifier = Input("Classifier", Model, replaces=["in_classifier"],
- default=True, multiple=True)
- object = Input("Object", object, replaces=["in_object"],
- default=False, multiple=True)
+ data = MultiInput(
+ "Data", Table, replaces=["in_data"], default=True
+ )
+ learner = MultiInput(
+ "Learner", Learner, replaces=["in_learner"], default=True
+ )
+ classifier = MultiInput(
+ "Classifier", Model, replaces=["in_classifier"], default=True
+ )
+ object = MultiInput(
+ "Object", object, replaces=["in_object"], default=False
+ )
class Outputs:
data = Output("Data", Table, replaces=["out_data"])
@@ -562,7 +566,7 @@ def __init__(self):
super().__init__()
for name in self.signal_names:
- setattr(self, name, {})
+ setattr(self, name, [])
self.splitCanvas = QSplitter(Qt.Vertical, self.mainArea)
self.mainArea.layout().addWidget(self.splitCanvas)
@@ -779,29 +783,65 @@ def _saveState(self):
self.scriptText = self.text.toPlainText()
self.splitterState = bytes(self.splitCanvas.saveState())
- def handle_input(self, obj, sig_id, signal):
+ def set_input(self, index, obj, signal):
dic = getattr(self, signal)
- if obj is None:
- if sig_id in dic.keys():
- del dic[sig_id]
- else:
- dic[sig_id] = obj
+ dic[index] = obj
+
+ def insert_input(self, index, obj, signal):
+ dic = getattr(self, signal)
+ dic.insert(index, obj)
+
+ def remove_input(self, index, signal):
+ dic = getattr(self, signal)
+ dic.pop(index)
@Inputs.data
- def set_data(self, data, sig_id):
- self.handle_input(data, sig_id, "data")
+ def set_data(self, index, data):
+ self.set_input(index, data, "data")
+
+ @Inputs.data.insert
+ def insert_data(self, index, data):
+ self.insert_input(index, data, "data")
+
+ @Inputs.data.remove
+ def remove_data(self, index):
+ self.remove_input(index, "data")
@Inputs.learner
- def set_learner(self, data, sig_id):
- self.handle_input(data, sig_id, "learner")
+ def set_learner(self, index, learner):
+ self.set_input(index, learner, "learner")
+
+ @Inputs.learner.insert
+ def insert_learner(self, index, learner):
+ self.insert_input(index, learner, "learner")
+
+ @Inputs.learner.remove
+ def remove_learner(self, index):
+ self.remove_input(index, "learner")
@Inputs.classifier
- def set_classifier(self, data, sig_id):
- self.handle_input(data, sig_id, "classifier")
+ def set_classifier(self, index, classifier):
+ self.set_input(index, classifier, "classifier")
+
+ @Inputs.classifier.insert
+ def insert_classifier(self, index, classifier):
+ self.insert_input(index, classifier, "classifier")
+
+ @Inputs.classifier.remove
+ def remove_classifier(self, index):
+ self.remove_input(index, "classifier")
@Inputs.object
- def set_object(self, data, sig_id):
- self.handle_input(data, sig_id, "object")
+ def set_object(self, index, object):
+ self.set_input(index, object, "object")
+
+ @Inputs.object.insert
+ def insert_object(self, index, object):
+ self.insert_input(index, object, "object")
+
+ @Inputs.object.remove
+ def remove_object(self, index):
+ self.remove_input(index, "object")
def handleNewSignals(self):
# update fake signature labels
@@ -925,7 +965,7 @@ def initial_locals_state(self):
d = {}
for name in self.signal_names:
value = getattr(self, name)
- all_values = list(value.values())
+ all_values = list(value)
one_value = all_values[0] if len(all_values) == 1 else None
d["in_" + name + "s"] = all_values
d["in_" + name] = one_value
diff --git a/Orange/widgets/data/owrank.py b/Orange/widgets/data/owrank.py
index 4b746473989..85c71b05d06 100644
--- a/Orange/widgets/data/owrank.py
+++ b/Orange/widgets/data/owrank.py
@@ -1,6 +1,6 @@
import logging
import warnings
-from collections import OrderedDict, namedtuple
+from collections import namedtuple
from functools import partial
from itertools import chain
from types import SimpleNamespace
@@ -33,7 +33,7 @@
from Orange.widgets.utils.itemmodels import PyTableModel
from Orange.widgets.utils.sql import check_sql_input
from Orange.widgets.utils.widgetpreview import WidgetPreview
-from Orange.widgets.widget import AttributeList, Input, Msg, Output, OWWidget
+from Orange.widgets.widget import AttributeList, Input, MultiInput, Output, Msg, OWWidget
log = logging.getLogger(__name__)
@@ -256,7 +256,7 @@ class OWRank(OWWidget, ConcurrentWidgetMixin):
class Inputs:
data = Input("Data", Table)
- scorer = Input("Scorer", score.Scorer, multiple=True)
+ scorer = MultiInput("Scorer", score.Scorer, filter_none=True)
class Outputs:
reduced_data = Output("Reduced Data", Table, default=True)
@@ -292,7 +292,7 @@ class Warning(OWWidget.Warning):
def __init__(self):
OWWidget.__init__(self)
ConcurrentWidgetMixin.__init__(self)
- self.scorers = OrderedDict()
+ self.scorers: List[ScoreMeta] = []
self.out_domain_desc = None
self.data = None
self.problem_type_mode = ProblemType.CLASSIFICATION
@@ -440,18 +440,27 @@ def handleNewSignals(self):
self.on_select()
@Inputs.scorer
- def set_learner(self, scorer, id): # pylint: disable=redefined-builtin
- if scorer is None:
- self.scorers.pop(id, None)
- else:
- # Avoid caching a (possibly stale) previous instance of the same
- # Scorer passed via the same signal
- if id in self.scorers:
- self.scorers_results = {}
+ def set_learner(self, index, scorer):
+ self.scorers[index] = ScoreMeta(
+ scorer.name, scorer.name, scorer,
+ ProblemType.from_variable(scorer.class_type),
+ False
+ )
+ self.scorers_results = {}
- self.scorers[id] = ScoreMeta(scorer.name, scorer.name, scorer,
- ProblemType.from_variable(scorer.class_type),
- False)
+ @Inputs.scorer.insert
+ def insert_learner(self, index: int, scorer):
+ self.scorers.insert(index, ScoreMeta(
+ scorer.name, scorer.name, scorer,
+ ProblemType.from_variable(scorer.class_type),
+ False
+ ))
+ self.scorers_results = {}
+
+ @Inputs.scorer.remove
+ def remove_learner(self, index):
+ self.scorers.pop(index)
+ self.scorers_results = {}
def _get_methods(self):
return [
@@ -469,7 +478,7 @@ def _get_methods(self):
def _get_scorers(self):
scorers = []
- for scorer in self.scorers.values():
+ for scorer in self.scorers:
if scorer.problem_type in (
self.problem_type_mode,
ProblemType.UNSUPERVISED,
diff --git a/Orange/widgets/data/owtable.py b/Orange/widgets/data/owtable.py
index c8af2dacf4e..f8fa6b302af 100644
--- a/Orange/widgets/data/owtable.py
+++ b/Orange/widgets/data/owtable.py
@@ -3,7 +3,8 @@
import itertools
import concurrent.futures
-from collections import OrderedDict, namedtuple
+from collections import namedtuple
+from typing import List, Optional
from math import isnan
@@ -37,7 +38,7 @@
from Orange.widgets.utils.tableview import TableView, \
table_selection_to_mime_data
from Orange.widgets.utils.widgetpreview import WidgetPreview
-from Orange.widgets.widget import OWWidget, Input, Output
+from Orange.widgets.widget import OWWidget, MultiInput, Output
from Orange.widgets.utils import datacaching
from Orange.widgets.utils.annotated_data import (create_annotated_table,
ANNOTATED_DATA_SIGNAL_NAME)
@@ -183,7 +184,7 @@ class OWDataTable(OWWidget):
keywords = []
class Inputs:
- data = Input("Data", Table, multiple=True, auto_summary=False)
+ data = MultiInput("Data", Table, auto_summary=False, filter_none=True)
class Outputs:
selected_data = Output("Selected Data", Table, default=True)
@@ -205,9 +206,7 @@ class Outputs:
def __init__(self):
super().__init__()
-
- self._inputs = OrderedDict()
-
+ self._inputs: List[TableSlot] = []
self.__pending_selected_rows = self.selected_rows
self.selected_rows = None
self.__pending_selected_cols = self.selected_cols
@@ -256,79 +255,90 @@ def copy_to_clipboard(self):
def sizeHint():
return QSize(800, 500)
+ def _create_table_view(self):
+ view = DataTableView()
+ view.setSortingEnabled(True)
+ view.setItemDelegate(TableDataDelegate(view))
+
+ if self.select_rows:
+ view.setSelectionBehavior(QTableView.SelectRows)
+
+ header = view.horizontalHeader()
+ header.setSectionsMovable(True)
+ header.setSectionsClickable(True)
+ header.setSortIndicatorShown(True)
+ header.setSortIndicator(-1, Qt.AscendingOrder)
+
+ # QHeaderView does not 'reset' the model sort column,
+ # because there is no guaranty (requirement) that the
+ # models understand the -1 sort column.
+ def sort_reset(index, order):
+ if view.model() is not None and index == -1:
+ view.model().sort(index, order)
+ header.sortIndicatorChanged.connect(sort_reset)
+ return view
+
@Inputs.data
- def set_dataset(self, data, tid=None):
+ def set_dataset(self, index: int, data: Table):
"""Set the input dataset."""
- if data is not None:
- datasetname = getattr(data, "name", "Data")
- if tid in self._inputs:
- # update existing input slot
- slot = self._inputs[tid]
- view = slot.view
- # reset the (header) view state.
- view.setModel(None)
- view.horizontalHeader().setSortIndicator(-1, Qt.AscendingOrder)
- assert self.tabs.indexOf(view) != -1
- self.tabs.setTabText(self.tabs.indexOf(view), datasetname)
- else:
- view = DataTableView()
- view.setSortingEnabled(True)
- view.setItemDelegate(TableDataDelegate(view))
-
- if self.select_rows:
- view.setSelectionBehavior(QTableView.SelectRows)
-
- header = view.horizontalHeader()
- header.setSectionsMovable(True)
- header.setSectionsClickable(True)
- header.setSortIndicatorShown(True)
- header.setSortIndicator(-1, Qt.AscendingOrder)
-
- # QHeaderView does not 'reset' the model sort column,
- # because there is no guaranty (requirement) that the
- # models understand the -1 sort column.
- def sort_reset(index, order):
- if view.model() is not None and index == -1:
- view.model().sort(index, order)
-
- header.sortIndicatorChanged.connect(sort_reset)
- self.tabs.addTab(view, datasetname)
-
- view.dataset = data
- self.tabs.setCurrentWidget(view)
-
- self._setup_table_view(view, data)
- slot = TableSlot(tid, data, table_summary(data), view)
- view.input_slot = slot
- self._inputs[tid] = slot
-
- self.tabs.setCurrentIndex(self.tabs.indexOf(view))
-
- self._set_input_summary(slot)
-
- if isinstance(slot.summary.len, concurrent.futures.Future):
- def update(_):
- QMetaObject.invokeMethod(
- self, "_update_info", Qt.QueuedConnection)
-
- slot.summary.len.add_done_callback(update)
-
- elif tid in self._inputs:
- slot = self._inputs.pop(tid)
- view = slot.view
- view.hide()
- view.deleteLater()
- self.tabs.removeTab(self.tabs.indexOf(view))
-
- current = self.tabs.currentWidget()
- if current is not None:
- self._set_input_summary(current.input_slot)
- else:
- self._set_input_summary(None)
+ datasetname = getattr(data, "name", "Data")
+ slot = self._inputs[index]
+ view = slot.view
+ # reset the (header) view state.
+ view.setModel(None)
+ view.horizontalHeader().setSortIndicator(-1, Qt.AscendingOrder)
+ assert self.tabs.indexOf(view) != -1
+ self.tabs.setTabText(self.tabs.indexOf(view), datasetname)
+ view.dataset = data
+ slot = TableSlot(index, data, table_summary(data), view)
+ view.input_slot = slot
+ self._inputs[index] = slot
+ self._setup_table_view(view, data)
+ self.tabs.setCurrentWidget(view)
+
+ @Inputs.data.insert
+ def insert_dataset(self, index: int, data: Table):
+ datasetname = getattr(data, "name", "Data")
+ view = self._create_table_view()
+ slot = TableSlot(None, data, table_summary(data), view)
+ view.dataset = data
+ view.input_slot = slot
+ self._inputs.insert(index, slot)
+ self.tabs.insertTab(index, view, datasetname)
+ self._setup_table_view(view, data)
+ self.tabs.setCurrentWidget(view)
+
+ @Inputs.data.remove
+ def remove_dataset(self, index):
+ slot = self._inputs.pop(index)
+ view = slot.view
+ self.tabs.removeTab(self.tabs.indexOf(view))
+ view.setModel(None)
+ view.hide()
+ view.deleteLater()
- self.tabs.tabBar().setVisible(self.tabs.count() > 1)
+ current = self.tabs.currentWidget()
+ if current is not None:
+ self._set_input_summary(current.input_slot)
- if data and self.__pending_selected_rows is not None:
+ def handleNewSignals(self):
+ super().handleNewSignals()
+ self.tabs.tabBar().setVisible(self.tabs.count() > 1)
+ data: Optional[Table] = None
+ current = self.tabs.currentWidget()
+ slot = None
+ if current is not None:
+ data = current.dataset
+ slot = current.input_slot
+
+ if slot and isinstance(slot.summary.len, concurrent.futures.Future):
+ def update(_):
+ QMetaObject.invokeMethod(
+ self, "_update_info", Qt.QueuedConnection)
+ slot.summary.len.add_done_callback(update)
+ self._set_input_summary(slot)
+
+ if data is not None and self.__pending_selected_rows is not None:
self.selected_rows = self.__pending_selected_rows
self.__pending_selected_rows = None
else:
@@ -346,12 +356,7 @@ def update(_):
def _setup_table_view(self, view, data):
"""Setup the `view` (QTableView) with `data` (Orange.data.Table)
"""
- if data is None:
- view.setModel(None)
- return
-
datamodel = RichTableModel(data)
-
rowcount = data.approx_len()
if self.color_by_class and data.domain.has_discrete_class:
@@ -849,6 +854,8 @@ def is_sortable(table):
if __name__ == "__main__": # pragma: no cover
WidgetPreview(OWDataTable).run(
- [(Table("iris"), "iris"),
- (Table("brown-selected"), "brown-selected"),
- (Table("housing"), "housing")])
+ insert_dataset=[
+ (0, Table("iris")),
+ (1, Table("brown-selected")),
+ (2, Table("housing"))
+ ])
diff --git a/Orange/widgets/data/tests/test_owconcatenate.py b/Orange/widgets/data/tests/test_owconcatenate.py
index 25a0fa56089..f81b6cf41c6 100644
--- a/Orange/widgets/data/tests/test_owconcatenate.py
+++ b/Orange/widgets/data/tests/test_owconcatenate.py
@@ -4,6 +4,7 @@
from unittest.mock import patch, Mock
import numpy as np
+from numpy.testing import assert_array_equal
from Orange.data import (
Table, Domain, ContinuousVariable, DiscreteVariable, StringVariable
@@ -493,6 +494,27 @@ def test_ignore_compute_value(self):
self.assertEqual(len(output.domain.metas), 1)
self.assertIs(output.domain.metas[0].compute_value.variable, ma4) # renamed
+ def test_explicit_closing(self):
+ w = self.widget
+ self.send_signal(w.Inputs.additional_data, self.iris[:1], 0)
+ self.send_signal(w.Inputs.additional_data, self.iris[1:2], 1)
+ self.send_signal(w.Inputs.additional_data, self.iris[2:3], 2)
+
+ def assert_output_equal(expected: np.ndarray):
+ out = self.get_output(w.Outputs.data)
+ assert_array_equal(out.X, expected)
+
+ assert_output_equal(self.iris[:3].X)
+ self.send_signal(w.Inputs.additional_data, None, 1)
+ assert_output_equal(self.iris[:3:2].X)
+ self.send_signal(w.Inputs.additional_data, self.iris[1:2], 1)
+ assert_output_equal(self.iris[:3].X)
+ self.send_signal(w.Inputs.additional_data,
+ w.Inputs.additional_data.closing_sentinel, 1)
+ assert_output_equal(self.iris[:3:2].X)
+ self.send_signal(w.Inputs.additional_data, self.iris[1:2], 1)
+ assert_output_equal(np.vstack((self.iris[:3:2].X, self.iris[1:2].X)))
+
if __name__ == "__main__":
unittest.main()
diff --git a/Orange/widgets/data/tests/test_owpythonscript.py b/Orange/widgets/data/tests/test_owpythonscript.py
index 6158e372b5d..b754ea10573 100644
--- a/Orange/widgets/data/tests/test_owpythonscript.py
+++ b/Orange/widgets/data/tests/test_owpythonscript.py
@@ -13,7 +13,7 @@
from Orange.widgets.data.owpythonscript import OWPythonScript, \
read_file_content, Script, OWPythonScriptDropHandler
from Orange.widgets.tests.base import WidgetTest
-from Orange.widgets.widget import OWWidget
+from Orange.widgets.widget import OWWidget, Input
# import tests for python editor
from Orange.widgets.data.utils.pythoneditor.tests.test_api import *
@@ -44,11 +44,13 @@ def test_inputs(self):
("Learner", self.learner),
("Classifier", self.model),
("Object", "object")):
- self.assertEqual(getattr(self.widget, input_.lower()), {})
+ self.assertEqual(getattr(self.widget, input_.lower()), [])
self.send_signal(input_, data, 1)
- self.assertEqual(getattr(self.widget, input_.lower()), {1: data})
+ self.assertEqual(getattr(self.widget, input_.lower()), [data])
self.send_signal(input_, None, 1)
- self.assertEqual(getattr(self.widget, input_.lower()), {})
+ self.assertEqual(getattr(self.widget, input_.lower()), [None])
+ self.send_signal(input_, Input.Closed, 1)
+ self.assertEqual(getattr(self.widget, input_.lower()), [])
def test_outputs(self):
"""Check widget's outputs"""
@@ -126,12 +128,19 @@ def test_multiple_signals(self):
self.send_signal("Data", None, 2)
click()
+ datas = console_locals["in_datas"]
+ self.assertEqual(len(datas), 2)
+ self.assertIs(datas[0], self.iris)
+ self.assertIs(datas[1], None)
+
+ self.send_signal("Data", Input.Closed, 2)
+ click()
self.assertIs(console_locals["in_data"], self.iris)
datas = console_locals["in_datas"]
self.assertEqual(len(datas), 1)
self.assertIs(datas[0], self.iris)
- self.send_signal("Data", None, 1)
+ self.send_signal("Data", Input.Closed, 1)
click()
self.assertIsNone(console_locals["in_data"])
self.assertEqual(console_locals["in_datas"], [])
diff --git a/Orange/widgets/data/tests/test_owrank.py b/Orange/widgets/data/tests/test_owrank.py
index c015f313dd3..96d23f5ef2a 100644
--- a/Orange/widgets/data/tests/test_owrank.py
+++ b/Orange/widgets/data/tests/test_owrank.py
@@ -59,16 +59,16 @@ def test_input_data_disconnect(self):
def test_input_scorer(self):
"""Check widget's scorer with scorer on the input"""
- self.assertEqual(self.widget.scorers, {})
+ self.assertEqual(self.widget.scorers, [])
self.send_signal(self.widget.Inputs.scorer, self.log_reg, 1)
self.wait_until_finished()
- value = self.widget.scorers[1]
+ value = self.widget.scorers[0]
self.assertEqual(self.log_reg, value.scorer)
self.assertIsInstance(value.scorer, Scorer)
def test_input_scorer_fitter(self):
heart_disease = Table('heart_disease')
- self.assertEqual(self.widget.scorers, {})
+ self.assertEqual(self.widget.scorers, [])
model = self.widget.ranksModel
@@ -94,14 +94,14 @@ def test_input_scorer_fitter(self):
self.assertIn(name, last_column)
self.send_signal("Scorer", None, 1)
- self.assertEqual(self.widget.scorers, {})
+ self.assertEqual(self.widget.scorers, [])
def test_input_scorer_disconnect(self):
"""Check widget's scorer after disconnecting scorer on the input"""
self.send_signal(self.widget.Inputs.scorer, self.log_reg, 1)
self.assertEqual(len(self.widget.scorers), 1)
self.send_signal(self.widget.Inputs.scorer, None, 1)
- self.assertEqual(self.widget.scorers, {})
+ self.assertEqual(self.widget.scorers, [])
def test_output_data(self):
"""Check data on the output after apply"""
diff --git a/Orange/widgets/evaluate/owpredictions.py b/Orange/widgets/evaluate/owpredictions.py
index f91ae6f92b0..dc613c5befb 100644
--- a/Orange/widgets/evaluate/owpredictions.py
+++ b/Orange/widgets/evaluate/owpredictions.py
@@ -1,9 +1,8 @@
-from collections import namedtuple
from contextlib import contextmanager
from functools import partial
from operator import itemgetter
from itertools import chain
-from typing import Set, List, Sequence, Union
+from typing import Set, Sequence, Union, Optional, List, NamedTuple
import numpy
from AnyQt.QtWidgets import (
@@ -28,7 +27,7 @@
from Orange.widgets.evaluate.utils import (
ScoreTable, usable_scorers, learner_name, scorer_caller)
from Orange.widgets.utils.widgetpreview import WidgetPreview
-from Orange.widgets.widget import OWWidget, Msg, Input, Output
+from Orange.widgets.widget import OWWidget, Msg, Input, Output, MultiInput
from Orange.widgets.utils.itemmodels import TableModel
from Orange.widgets.utils.sql import check_sql_input
from Orange.widgets.utils.state_summary import format_summary_details
@@ -36,12 +35,12 @@
from Orange.widgets.utils.itemdelegates import TableDataDelegate
# Input slot for the Predictors channel
-PredictorSlot = namedtuple(
- "PredictorSlot",
- ["predictor", # The `Model` instance
- "name", # Predictor name
- "results"] # Computed prediction results or None.
-)
+PredictorSlot = NamedTuple(
+ "PredictorSlot", [
+ ("predictor", Model), # The `Model` instance
+ ("name", str), # Predictor name
+ ("results", Optional[Results]), # Computed prediction results or None.
+])
class OWPredictions(OWWidget):
@@ -55,7 +54,7 @@ class OWPredictions(OWWidget):
class Inputs:
data = Input("Data", Orange.data.Table)
- predictors = Input("Predictors", Model, multiple=True)
+ predictors = MultiInput("Predictors", Model, filter_none=True)
class Outputs:
predictions = Output("Predictions", Orange.data.Table)
@@ -81,7 +80,7 @@ def __init__(self):
super().__init__()
self.data = None # type: Optional[Orange.data.Table]
- self.predictors = {} # type: Dict[object, PredictorSlot]
+ self.predictors = [] # type: List[PredictorSlot]
self.class_values = [] # type: List[str]
self._delegates = []
self.left_width = 10
@@ -186,21 +185,25 @@ def _store_selection(self):
def class_var(self):
return self.data and self.data.domain.class_var
- # pylint: disable=redefined-builtin
@Inputs.predictors
- def set_predictor(self, predictor=None, id=None):
- if id in self.predictors:
- if predictor is not None:
- self.predictors[id] = self.predictors[id]._replace(
- predictor=predictor, name=predictor.name, results=None)
- else:
- del self.predictors[id]
- elif predictor is not None:
- self.predictors[id] = PredictorSlot(predictor, predictor.name, None)
+ def set_predictor(self, index, predictor: Model):
+ item = self.predictors[index]
+ self.predictors[index] = item._replace(
+ predictor=predictor, name=predictor.name, results=None
+ )
+
+ @Inputs.predictors.insert
+ def insert_predictor(self, index, predictor: Model):
+ item = PredictorSlot(predictor, predictor.name, None)
+ self.predictors.insert(index, item)
+
+ @Inputs.predictors.remove
+ def remove_predictor(self, index):
+ self.predictors.pop(index)
def _set_class_values(self):
class_values = []
- for slot in self.predictors.values():
+ for slot in self.predictors:
class_var = slot.predictor.domain.class_var
if class_var and class_var.is_discrete:
for value in class_var.values:
@@ -236,7 +239,7 @@ def _call_predictors(self):
else:
classless_data = self.data
- for inputid, slot in self.predictors.items():
+ for index, slot in enumerate(self.predictors):
if isinstance(slot.results, Results):
continue
@@ -248,7 +251,7 @@ def _call_predictors(self):
pred = predictor(classless_data, Model.Value)
prob = numpy.zeros((len(pred), 0))
except (ValueError, DomainTransformationError) as err:
- self.predictors[inputid] = \
+ self.predictors[index] = \
slot._replace(results=f"{predictor.name}: {err}")
continue
@@ -261,7 +264,7 @@ def _call_predictors(self):
results.unmapped_probabilities = prob
results.unmapped_predicted = pred
results.probabilities = results.predicted = None
- self.predictors[inputid] = slot._replace(results=results)
+ self.predictors[index] = slot._replace(results=results)
target = predictor.domain.class_var
if target != self.class_var:
@@ -280,8 +283,8 @@ def _update_scores(self):
scorers = usable_scorers(self.class_var) if self.class_var else []
self.score_table.update_header(scorers)
errors = []
- for inputid, pred in self.predictors.items():
- results = self.predictors[inputid].results
+ for pred in self.predictors:
+ results = pred.results
if not isinstance(results, Results) or results.predicted is None:
continue
row = [QStandardItem(learner_name(pred.predictor)),
@@ -315,14 +318,14 @@ def _set_errors(self):
# in _call_predictors
errors = "\n".join(
f"- {p.predictor.name}: {p.results}"
- for p in self.predictors.values()
+ for p in self.predictors
if isinstance(p.results, str) and p.results)
self.Error.predictor_failed(errors, shown=bool(errors))
if self.class_var:
inv_targets = "\n".join(
f"- {pred.name} predicts '{pred.domain.class_var.name}'"
- for pred in (p.predictor for p in self.predictors.values()
+ for pred in (p.predictor for p in self.predictors
if isinstance(p.results, Results)
and p.results.probabilities is None))
self.Warning.wrong_targets(inv_targets, shown=bool(inv_targets))
@@ -333,7 +336,7 @@ def _get_details(self):
details = "Data:
"
details += format_summary_details(self.data, format=Qt.RichText)
details += "