diff --git a/Orange/canvas/report/owreport.py b/Orange/canvas/report/owreport.py index 4e32ac18da9..e9ce707d809 100644 --- a/Orange/canvas/report/owreport.py +++ b/Orange/canvas/report/owreport.py @@ -116,7 +116,8 @@ def __init__(self): self.report_changed = False index_file = pkg_resources.resource_filename(__name__, "index.html") - self.report_html_template = open(index_file, "r").read() + with open(index_file, "r") as f: + self.report_html_template = f.read() def _setup_ui_(self): self.table_model = ReportItemModel(0, len(Column.__members__)) diff --git a/Orange/canvas/report/tests/test_report.py b/Orange/canvas/report/tests/test_report.py index c6725609e4e..de7ac0ae27b 100644 --- a/Orange/canvas/report/tests/test_report.py +++ b/Orange/canvas/report/tests/test_report.py @@ -1,7 +1,5 @@ import unittest -import pickle -import sys -from PyQt4.QtGui import QApplication, QFont, QBrush +from PyQt4.QtGui import QFont, QBrush from PyQt4.QtCore import Qt from Orange.data.table import Table from Orange.classification import LogisticRegressionLearner @@ -11,7 +9,7 @@ from Orange.distance import Euclidean from Orange.canvas.report.owreport import OWReport from Orange.widgets import gui -from Orange.widgets.tests.base import GuiTest +from Orange.widgets.tests.base import WidgetTest from Orange.widgets.classify.owclassificationtree import OWClassificationTree from Orange.widgets.classify.owclassificationtreegraph import OWClassificationTreeGraph from Orange.widgets.classify.owknn import OWKNNLearner @@ -40,7 +38,6 @@ from Orange.widgets.data.owcolor import OWColor from Orange.widgets.data.owpreprocess import OWPreprocess from Orange.widgets.evaluate.owcalibrationplot import OWCalibrationPlot -from Orange.widgets.evaluate.owconfusionmatrix import OWConfusionMatrix from Orange.widgets.evaluate.owliftcurve import OWLiftCurve from Orange.widgets.evaluate.owrocanalysis import OWROCAnalysis from Orange.widgets.evaluate.owtestlearners import OWTestLearners @@ -68,26 +65,18 @@ from Orange.widgets.visualize.owvenndiagram import OWVennDiagram -class TestReport(GuiTest): - @unittest.skip('Segfaults. Or something.') +class TestReport(WidgetTest): def test_report(self): count = 5 for i in range(count): rep = OWReport.get_instance() - file = OWFile() + file = self.create_widget(OWFile) file.create_report_html() rep.make_report(file) self.assertEqual(rep.table_model.rowCount(), count) - @unittest.skip("Report extends OWWidget which is not picklable") - def test_report_pickle(self): - rep = OWReport().get_instance() - p = pickle.dumps(rep) - rep2 = pickle.loads(p) - self.assertEqual(type(rep), type(rep2)) - def test_report_table(self): - rep = OWReport().get_instance() + rep = OWReport.get_instance() model = PyTableModel([['x', 1, 2], ['y', 2, 2]]) model.setHorizontalHeaderLabels(['a', 'b', 'c']) @@ -131,7 +120,7 @@ def test_report_table(self): @unittest.skip('Segfaults. Dunno. @astaric says it might be something on the QWidget.') -class TestReportWidgets(GuiTest): +class TestReportWidgets(WidgetTest): clas_widgets = [OWClassificationTree, OWKNNLearner, OWLogisticRegression, OWMajority, OWNaiveBayes, OWRandomForest, OWSVMClassification] diff --git a/Orange/widgets/classify/owadaboost.py b/Orange/widgets/classify/owadaboost.py index e7877d028e5..9c6fedf2cb8 100644 --- a/Orange/widgets/classify/owadaboost.py +++ b/Orange/widgets/classify/owadaboost.py @@ -47,7 +47,8 @@ def create_learner(self): return self.LEARNER( base_estimator=self.base_estimator, n_estimators=self.n_estimators, - preprocessors=self.preprocessors + preprocessors=self.preprocessors, + algorithm=self.losses[self.algorithm] ) def set_base_learner(self, model): diff --git a/Orange/widgets/classify/owsvmclassification.py b/Orange/widgets/classify/owsvmclassification.py index a1bbcc64cfe..87e70449e7a 100644 --- a/Orange/widgets/classify/owsvmclassification.py +++ b/Orange/widgets/classify/owsvmclassification.py @@ -33,9 +33,9 @@ def _add_kernel_box(self): # Initialize with the widest label to measure max width self.kernel_eq = self.kernels[-1][1] - self.kernel_box = box = gui.hBox(self.controlArea, "Kernel") + box = gui.hBox(self.controlArea, "Kernel") - buttonbox = gui.radioButtonsInBox( + self.kernel_box = buttonbox = gui.radioButtonsInBox( box, self, "kernel_type", btnLabels=[k[0] for k in self.kernels], callback=self._on_kernel_changed, addSpace=20) buttonbox.layout().setSpacing(10) @@ -70,15 +70,15 @@ def _add_optimization_box(self): self.optimization_box, self, "tol", 1e-6, 1.0, 1e-5, label="Numerical tolerance:", decimals=6, alignment=Qt.AlignRight, controlWidth=100, - callback=self.settings_changed - ) + callback=self.settings_changed) def add_main_layout(self): self._add_type_box() self._add_kernel_box() self._add_optimization_box() + self._show_right_kernel() - def _on_kernel_changed(self): + def _show_right_kernel(self): enabled = [[False, False, False], # linear [True, True, True], # poly [True, False, False], # rbf @@ -89,6 +89,8 @@ def _on_kernel_changed(self): for spin, enabled in zip(self._kernel_params, mask): [spin.box.hide, spin.box.show][enabled]() + def _on_kernel_changed(self): + self._show_right_kernel() self.settings_changed() def _report_kernel_parameters(self, items): @@ -97,12 +99,12 @@ def _report_kernel_parameters(self, items): elif self.kernel_type == 1: items["Kernel"] = \ "Polynomial, ({g:.4} x⋅y + {c:.4}){d}".format( - g=self.gamma, c=self.coef0, d=self.degree) + g=self.gamma, c=self.coef0, d=self.degree) elif self.kernel_type == 2: items["Kernel"] = "RBF, exp(-{:.4}|x-y|²)".format(self.gamma) else: items["Kernel"] = "Sigmoid, tanh({g:.4} x⋅y + {c:.4})".format( - g=self.gamma, c=self.coef0) + g=self.gamma, c=self.coef0) def update_model(self): super().update_model() @@ -136,8 +138,8 @@ class OWSVMClassification(OWBaseSVM): def _add_type_box(self): form = QtGui.QGridLayout() self.type_box = box = gui.radioButtonsInBox( - self.controlArea, self, "svmtype", [], box="SVM Type", - orientation=form, callback=self.settings_changed) + self.controlArea, self, "svmtype", [], box="SVM Type", + orientation=form, callback=self.settings_changed) form.addWidget(gui.appendRadioButton(box, "C-SVM", addToLayout=False), 0, 0, Qt.AlignLeft) diff --git a/Orange/widgets/classify/tests/test_owadaboostclassification.py b/Orange/widgets/classify/tests/test_owadaboostclassification.py new file mode 100644 index 00000000000..a69f890a69e --- /dev/null +++ b/Orange/widgets/classify/tests/test_owadaboostclassification.py @@ -0,0 +1,47 @@ +# Test methods with long descriptive names can omit docstrings +# pylint: disable=missing-docstring +from PyQt4 import QtGui + +from Orange.widgets.tests.base import WidgetTest +from Orange.widgets.classify.owadaboost import OWAdaBoostClassification + + + +class TestOWAdaBoostClassification(WidgetTest): + + def setUp(self): + self.widget = self.create_widget(OWAdaBoostClassification) + self.spinners = [] + self.spinners.append(self.widget.findChildren(QtGui.QSpinBox)[0]) + self.spinners.append(self.widget.findChildren(QtGui.QDoubleSpinBox)[0]) + self.combobox_algorithm = self.widget.findChildren(QtGui.QComboBox)[0] + + def test_visible_boxes(self): + """ Check if boxes are visible """ + self.assertEqual(self.spinners[0].isHidden(), False) + self.assertEqual(self.spinners[1].isHidden(), False) + self.assertEqual(self.combobox_algorithm.isHidden(), False) + + def test_parameters_on_output(self): + """ Check right paramaters on output """ + self.widget.apply() + learner_params = self.widget.learner.params + self.assertEqual(learner_params.get("n_estimators"), self.spinners[0].value()) + self.assertEqual(learner_params.get("learning_rate"), self.spinners[1].value()) + self.assertEqual(learner_params.get('algorithm'), self.combobox_algorithm.currentText()) + + + def test_output_algorithm(self): + """ Check if right learning algorithm is on output when we change algorithm """ + for index, algorithmName in enumerate(self.widget.losses): + self.combobox_algorithm.setCurrentIndex(index) + self.combobox_algorithm.activated.emit(index) + self.assertEqual(self.combobox_algorithm.currentText(), algorithmName) + self.widget.apply() + self.assertEqual(self.widget.learner.params.get("algorithm").capitalize(), + self.combobox_algorithm.currentText().capitalize()) + + def test_learner_on_output(self): + """ Check if learner is on output after create widget and apply """ + self.widget.apply() + self.assertNotEqual(self.widget.learner, None) diff --git a/Orange/widgets/classify/tests/test_owknnclassification.py b/Orange/widgets/classify/tests/test_owknnclassification.py new file mode 100644 index 00000000000..8d891efc04d --- /dev/null +++ b/Orange/widgets/classify/tests/test_owknnclassification.py @@ -0,0 +1,53 @@ +# Test methods with long descriptive names can omit docstrings +# pylint: disable=missing-docstring +from PyQt4 import QtGui + +from Orange.widgets.tests.base import WidgetTest +from Orange.widgets.classify.owknn import OWKNNLearner + + +class TestOwKnnClassification(WidgetTest): + def setUp(self): + self.widget = self.create_widget(OWKNNLearner) + self.combo_box = self.widget.findChildren(QtGui.QComboBox) + self.spinner = self.widget.findChildren(QtGui.QSpinBox) + + def test_boxes_visible(self): + """ Check if all boxes visible """ + self.assertEqual(self.combo_box[0].isHidden(), False) + self.assertEqual(self.combo_box[1].isHidden(), False) + self.assertEqual(self.spinner[0].isHidden(), False) + + def test_values_on_output(self): + """ Check if all values right on output """ + self.widget.apply() + learner = self.widget.learner.params + self.assertEqual(learner.get("n_neighbors"), self.spinner[0].value()) + self.assertEqual(learner.get("metric").capitalize(), self.combo_box[0].currentText()) + self.assertEqual(learner.get("weights").capitalize(), self.combo_box[1].currentText()) + + def test_selected_values_metrics(self): + """ Check right value of combobox metric is right on output """ + for index, metric in enumerate(self.widget.metrics): + self.combo_box[0].activated.emit(index) + self.combo_box[0].setCurrentIndex(index) + self.assertEqual(self.combo_box[0].currentText().capitalize(), metric.capitalize()) + self.widget.apply() + self.assertEqual(self.widget.learner.params.get("metric").capitalize(), + self.combo_box[0].currentText().capitalize()) + + def test_selected_values_weights(self): + """ Check right value of combobox metric is right on output """ + for index, metric in enumerate(self.widget.weights): + self.combo_box[1].activated.emit(index) + self.combo_box[1].setCurrentIndex(index) + self.assertEqual(self.combo_box[1].currentText().capitalize(), metric.capitalize()) + self.widget.apply() + self.assertEqual(self.widget.learner.params.get("weights").capitalize(), + self.combo_box[1].currentText().capitalize()) + + def test_learner_on_output(self): + """ Check if learner is on output after create widget and apply """ + self.widget.apply() + self.assertNotEqual(self.widget.learner, None) + diff --git a/Orange/widgets/classify/tests/test_owlogisticregression.py b/Orange/widgets/classify/tests/test_owlogisticregression.py index 91a3d41ee62..2152f039eea 100644 --- a/Orange/widgets/classify/tests/test_owlogisticregression.py +++ b/Orange/widgets/classify/tests/test_owlogisticregression.py @@ -1,12 +1,12 @@ +# Test methods with long descriptive names can omit docstrings +# pylint: disable=missing-docstring import unittest -import bottleneck as bn - from Orange.data import Table from Orange.statistics.util import stats from Orange.classification import LogisticRegressionLearner from Orange.widgets.classify.owlogisticregression import create_coef_table, OWLogisticRegression -from Orange.widgets.tests.base import GuiTest +from Orange.widgets.tests.base import WidgetTest class LogisticRegressionTest(unittest.TestCase): @@ -30,7 +30,7 @@ def test_coef_table_multiple(self): len(classifier.domain.class_var.values)) -class TestOWLogisticRegression(GuiTest): +class TestOWLogisticRegression(WidgetTest): def test_data_before_apply(self): - widget = OWLogisticRegression() + widget = self.create_widget(OWLogisticRegression) widget.set_data(Table("iris")) diff --git a/Orange/widgets/classify/tests/test_owsvmclassification.py b/Orange/widgets/classify/tests/test_owsvmclassification.py new file mode 100644 index 00000000000..6cb77cf4d3e --- /dev/null +++ b/Orange/widgets/classify/tests/test_owsvmclassification.py @@ -0,0 +1,107 @@ +# Test methods with long descriptive names can omit docstrings +# pylint: disable=missing-docstring +from PyQt4 import QtGui + +from Orange.data import Table +from Orange.widgets.classify.owsvmclassification import OWSVMClassification +from Orange.widgets.tests.base import WidgetTest + + +class TestOWSVMClassification(WidgetTest): + def setUp(self): + self.widget = self.create_widget(OWSVMClassification) + self.widget.spin_boxes = self.widget.findChildren(QtGui.QDoubleSpinBox) + # max iter spin + self.widget.spin_boxes.append(self.widget.findChildren(QtGui.QSpinBox)[0]) + # max iter checkbox + self.widget.max_iter_check_box = self.widget.findChildren(QtGui.QCheckBox)[0] + self.spin_boxes = self.widget.spin_boxes + self.event_data = None + + def test_kernel_equation_run(self): + """ Check if right text is written for specific kernel """ + for i in range(0, 4): + if self.widget.kernel_box.buttons[i].isChecked(): + self.assertEqual(self.widget.kernel_eq, self.widget.kernels[i][1]) + + def test_kernel_equation(self): + """ Check if right text is written for specific kernel after click """ + for index in range(0, 4): + self.widget.kernel_box.buttons[index].click() + self.assertEqual(self.widget.kernel_eq, self.widget.kernels[index][1]) + + def test_kernel_display_run(self): + """ Check if right spinner box for selected kernel are visible after widget start """ + for button_pos, value in ((0, [False, False, False]), + (1, [True, True, True]), + (2, [True, False, False]), + (3, [True, True, False])): + if self.widget.kernel_box.buttons[button_pos].isChecked(): + self.assertEqual([not self.spin_boxes[i].box.isHidden() for i in range(2, 5)], + value) + break + + def test_kernel_display(self): + """ Check if right spinner box for selected kernel are visible after we select kernel """ + for button_pos, value in ((0, [False, False, False]), + (1, [True, True, True]), + (2, [True, False, False]), + (3, [True, True, False])): + self.widget.kernel_box.buttons[button_pos].click() + self.widget.kernel_box.buttons[button_pos].isChecked() + self.assertEqual([not self.spin_boxes[i].box.isHidden() for i in range(2, 5)], value) + + def test_optimization_box_visible(self): + """ Check if both spinner box is visible after starting widget """ + self.assertEqual(self.spin_boxes[5].box.isHidden(), False) + self.assertEqual(self.spin_boxes[6].box.isHidden(), False) + + def test_optimization_box_checked(self): + """ Check if spinner box for iteration limit is enabled or disabled """ + for value in (True, False): + self.widget.max_iter_check_box.setChecked(value) + self.assertEqual(self.widget.max_iter_check_box.isChecked(), value) + self.assertEqual(self.spin_boxes[6].isEnabled(), value) + + def test_type_button_checked(self): + """ Check if SVM type is selected after click """ + self.widget.type_box.buttons[0].click() + self.assertEqual(self.widget.type_box.buttons[0].isChecked(), True) + self.widget.type_box.buttons[1].click() + self.assertEqual(self.widget.type_box.buttons[1].isChecked(), True) + + def test_type_button_properties_visible(self): + """ Check if spinner box in SVM type are visible """ + self.assertEqual(not self.spin_boxes[0].isHidden(), True) + self.assertEqual(not self.spin_boxes[1].isHidden(), True) + + def test_data_before_apply(self): + """ Check if data are set """ + self.widget.set_data(Table("iris")[:100]) + self.widget.apply() + self.assertEqual(len(self.widget.data), 100) + + def test_output_signal_learner(self): + """ Check if we have on output learner """ + self.widget.kernel_box.buttons[0].click() + self.widget.set_data(Table("iris")[:100]) + self.widget.apply() + self.assertNotEqual(self.widget.learner, None) + + def test_output_params(self): + """ Check ouput params """ + self.widget.kernel_box.buttons[0].click() + self.widget.set_data(Table("iris")[:100]) + self.widget.max_iter_check_box.setChecked(True) + self.widget.apply() + self.widget.type_box.buttons[0].click() + params = self.widget.learner.params + self.assertEqual(params.get('C'), self.spin_boxes[0].value()) + self.widget.type_box.buttons[1].click() + params = self.widget.learner.params + self.assertEqual(params.get('nu'), self.spin_boxes[1].value()) + self.assertEqual(params.get('gamma'), self.spin_boxes[2].value()) + self.assertEqual(params.get('coef0'), self.spin_boxes[3].value()) + self.assertEqual(params.get('degree'), self.spin_boxes[4].value()) + self.assertEqual(params.get('tol'), self.spin_boxes[5].value()) + self.assertEqual(params.get('max_iter'), self.spin_boxes[6].value()) diff --git a/Orange/widgets/data/tests/test_owfile.py b/Orange/widgets/data/tests/test_owfile.py index eff8aaf73d9..9094b409fa2 100644 --- a/Orange/widgets/data/tests/test_owfile.py +++ b/Orange/widgets/data/tests/test_owfile.py @@ -8,15 +8,18 @@ import Orange from Orange.widgets.data.owfile import OWFile -from Orange.widgets.tests.base import GuiTest +from Orange.widgets.tests.base import WidgetTest TITANIC_URL = path.join(path.dirname(Orange.__file__), 'datasets', 'titanic.tab') -class TestOWFile(GuiTest): +class TestOWFile(WidgetTest): + # Attribute used to store event data so it does not get garbage + # collected before event is processed. + event_data = None + def setUp(self): - self.widget = OWFile() - self.event_data = None + self.widget = self.create_widget(OWFile) def test_dragEnterEvent_accepts_urls(self): event = self._drag_enter_event(TITANIC_URL) diff --git a/Orange/widgets/regression/owadaboostregression.py b/Orange/widgets/regression/owadaboostregression.py index b1d4d304f57..03944bf29ab 100644 --- a/Orange/widgets/regression/owadaboostregression.py +++ b/Orange/widgets/regression/owadaboostregression.py @@ -31,7 +31,8 @@ def create_learner(self): return self.LEARNER( base_estimator=self.base_estimator, n_estimators=self.n_estimators, - preprocessors=self.preprocessors + preprocessors=self.preprocessors, + loss=self.losses[self.loss].lower() ) def get_learner_parameters(self): diff --git a/Orange/widgets/tests/base.py b/Orange/widgets/tests/base.py index 2f2433e99e7..bfdc6fae0c4 100644 --- a/Orange/widgets/tests/base.py +++ b/Orange/widgets/tests/base.py @@ -1,14 +1,148 @@ import unittest from PyQt4.QtGui import QApplication +import sip + +from Orange.canvas.report.owreport import OWReport app = None +class DummySignalManager: + def __init__(self): + self.outputs = {} + + def send(self, widget, signal_name, value, id): + self.outputs[(widget, signal_name)] = value + + class GuiTest(unittest.TestCase): + """Base class for tests that require a QApplication instance + + GuiTest ensures that a QApplication exists before tests are run an + """ @classmethod def setUpClass(cls): + """Prepare for test execution. + + Ensure that a (single copy of) QApplication has been created + """ global app if app is None: app = QApplication([]) + +class WidgetTest(GuiTest): + """Base class for widget tests + + Contains helper methods widget creation and working with signals. + + All widgets should be created by the create_widget method, as it + remembers created widgets and properly destroys them in tearDownClass + to avoid segmentation faults when QApplication gets destroyed. + """ + + #: list[OwWidget] + widgets = [] + + @classmethod + def setUpClass(cls): + """Prepare environment for test execution + + Prepare a list for tracking created widgets and construct a + dummy signal manager. Monkey patch OWReport.get_instance to + return a manually_created instance. + """ + super().setUpClass() + + cls.widgets = [] + + cls.signal_manager = DummySignalManager() + + report = OWReport() + cls.widgets.append(report) + OWReport.get_instance = lambda: report + + @classmethod + def tearDownClass(cls): + """Cleanup after tests + + Process any pending events and properly destroy created widgets by + calling their onDeleteWidget method which does the widget-specific + cleanup. + + NOTE: sip.delete is mandatory. In some cases, widgets are deleted by + python while some references in QApplication remain + (QApplication::topLevelWidgets()), causing a segmentation fault when + QApplication is destroyed. + """ + app.processEvents() + for w in cls.widgets: + w.onDeleteWidget() + sip.delete(w) + + def create_widget(self, cls, stored_settings=None): + """Create a widget instance. + + Parameters + ---------- + cls : WidgetMetaClass + Widget class to instantiate + stored_settings : dict + Default values for settings + + Returns + ------- + Widget instance : cls + """ + widget = cls.__new__(cls, signal_manager=self.signal_manager, + stored_settings=stored_settings) + widget.__init__() + self.process_events() + self.widgets.append(widget) + return widget + + @staticmethod + def process_events(): + """Process Qt events. + + Needs to be called manually as QApplication.exec is never called. + """ + app.processEvents() + + def send_signal(self, input_name, value, id=None, widget=None): + """ Send signal to widget by calling appropriate triggers. + + Parameters + ---------- + input_name : str + value : Object + id : int + channel id, used for inputs with flag Multiple + widget : Optional[OWWidget] + widget to send signal to. If not set, self.widget is used + """ + if widget is None: + widget = self.widget + for input_signal in widget.inputs: + if input_signal.name == input_name: + getattr(widget, input_signal.handler)(value) + break + widget.handleNewSignals() + + def get_ouput(self, output_name, widget=None): + """Return the last output that has been sent from the widget. + + Parameters + ---------- + output_name : str + widget : Optional[OWWidget] + widget whose output is returned. If not set, self.widget is used + + Returns + ------- + The last sent value of given output or None if nothing has been sent. + """ + if widget is None: + widget = self.widget + self.signal_manager.outputs.get((widget, output_name), None) diff --git a/Orange/widgets/tests/test_widget.py b/Orange/widgets/tests/test_widget.py index 9608705441f..334a00ff545 100644 --- a/Orange/widgets/tests/test_widget.py +++ b/Orange/widgets/tests/test_widget.py @@ -1,6 +1,7 @@ -from unittest import TestCase -from Orange.widgets.gui import CONTROLLED_ATTRIBUTES, ATTRIBUTE_CONTROLLERS, OWComponent -from Orange.widgets.tests.base import GuiTest +# Test methods with long descriptive names can omit docstrings +# pylint: disable=missing-docstring +from Orange.widgets.gui import CONTROLLED_ATTRIBUTES, OWComponent +from Orange.widgets.tests.base import WidgetTest from Orange.widgets.widget import OWWidget @@ -9,20 +10,18 @@ class DummyComponent(OWComponent): class MyWidget(OWWidget): - def __init__(self, depth=1): + def __init__(self): super().__init__() self.field = 42 self.component = DummyComponent(self) - if depth: - self.widget = MyWidget(depth=depth-1) - else: - self.widget = None + self.widget = None -class WidgetTestCase(GuiTest): +class WidgetTestCase(WidgetTest): def test_setattr(self): - widget = MyWidget() + widget = self.create_widget(MyWidget) + widget.widget = self.create_widget(MyWidget) setattr(widget, 'field', 1) self.assertEqual(widget.field, 1) @@ -43,7 +42,11 @@ def test_setattr(self): setattr(widget, 'unknown_field2.field', 6) def test_notify_controller_on_attribute_change(self): - widget = MyWidget(depth=3) + widget = self.create_widget(MyWidget) + widget.widget = self.create_widget(MyWidget) + widget.widget.widget = self.create_widget(MyWidget) + widget.widget.widget.widget = self.create_widget(MyWidget) + delattr(widget.widget, CONTROLLED_ATTRIBUTES) delattr(widget.widget.widget, CONTROLLED_ATTRIBUTES) delattr(widget.widget.widget.widget, CONTROLLED_ATTRIBUTES) diff --git a/Orange/widgets/visualize/owscatterplot.py b/Orange/widgets/visualize/owscatterplot.py index be5ff862b5a..a6b8858b7b2 100644 --- a/Orange/widgets/visualize/owscatterplot.py +++ b/Orange/widgets/visualize/owscatterplot.py @@ -370,17 +370,21 @@ def get_shown_attributes(self): def init_attr_values(self): self.cb_attr_x.clear() - self.cb_attr_y.clear() self.attr_x = None + self.cb_attr_y.clear() self.attr_y = None self.cb_attr_color.clear() self.cb_attr_color.addItem("(Same color)") + self.graph.attr_color = None self.cb_attr_label.clear() self.cb_attr_label.addItem("(No labels)") + self.graph.attr_label = None self.cb_attr_shape.clear() self.cb_attr_shape.addItem("(Same shape)") + self.graph.attr_shape = None self.cb_attr_size.clear() self.cb_attr_size.addItem("(Same size)") + self.graph.attr_size = None if not self.data: return @@ -462,7 +466,7 @@ def send_data(self): unselection[selection] = False unselected = self.data[unselection] self.send("Selected Data", selected) - if len(unselected) == 0: + if unselected is None or len(unselected) == 0: self.send("Other Data", None) else: self.send("Other Data", unselected) diff --git a/Orange/widgets/visualize/owscatterplotgraph.py b/Orange/widgets/visualize/owscatterplotgraph.py index cca3f811877..b947429a977 100644 --- a/Orange/widgets/visualize/owscatterplotgraph.py +++ b/Orange/widgets/visualize/owscatterplotgraph.py @@ -527,6 +527,7 @@ def __init__(self, scatter_widget, parent=None, _="None"): def new_data(self, data, subset_data=None, **args): self.plot_widget.clear() + self.remove_legend() self.density_img = None self.scatterplot_item = None diff --git a/Orange/widgets/visualize/tests/test_owscatterplot.py b/Orange/widgets/visualize/tests/test_owscatterplot.py new file mode 100644 index 00000000000..655856ba73d --- /dev/null +++ b/Orange/widgets/visualize/tests/test_owscatterplot.py @@ -0,0 +1,47 @@ +# Test methods with long descriptive names can omit docstrings +# pylint: disable=missing-docstring +import numpy as np + +from Orange.data import Table +from Orange.widgets.tests.base import WidgetTest +from Orange.widgets.visualize.owscatterplot import OWScatterPlot + + +class TestOWScatterPlot(WidgetTest): + def setUp(self): + self.widget = self.create_widget(OWScatterPlot) + self.iris = Table("iris") + + def test_set_data(self): + # Connect iris to scatter plot + self.send_signal("Data", self.iris) + + # First two attribute should be selected as x an y + self.assertEqual(self.widget.attr_x, self.iris.domain[0].name) + self.assertEqual(self.widget.attr_y, self.iris.domain[1].name) + + # Class var should be selected as color + self.assertEqual(self.widget.graph.attr_color, + self.iris.domain.class_var.name) + + # Change which attributes are displayed + self.widget.attr_x = self.iris.domain[2].name + self.widget.attr_y = self.iris.domain[3].name + + # Disconnect the data + self.send_signal("Data", None) + + # removing data should have cleared attributes + self.assertEqual(self.widget.attr_x, None) + self.assertEqual(self.widget.attr_y, None) + self.assertEqual(self.widget.graph.attr_color, None) + + # and remove the legend + self.assertEqual(self.widget.graph.legend, None) + + # Connect iris again + # same attributes that were used last time should be selected + self.send_signal("Data", self.iris) + + self.assertEqual(self.widget.attr_x, self.iris.domain[2].name) + self.assertEqual(self.widget.attr_y, self.iris.domain[3].name)