Skip to content

Commit 08aef5d

Browse files
authored
Merge pull request #1552 from astaric/fix-edit-domain
[FIX] Fix compatibility with Color widget
2 parents 9c6ac61 + 88c0745 commit 08aef5d

File tree

6 files changed

+142
-19
lines changed

6 files changed

+142
-19
lines changed

Orange/data/variable.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -471,10 +471,11 @@ def number_of_decimals(self):
471471
@property
472472
def colors(self):
473473
if self._colors is None:
474-
if "colors" in self.attributes:
474+
try:
475475
col1, col2, black = self.attributes["colors"]
476476
self._colors = (hex_to_color(col1), hex_to_color(col2), black)
477-
else:
477+
except (KeyError, ValueError):
478+
# Stored colors were not available or invalid, use defaults
478479
self._colors = ((0, 0, 255), (255, 255, 0), False)
479480
return self._colors
480481

Orange/widgets/data/oweditdomain.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def get_qualified(module, name):
3737
return getattr(module, name)
3838

3939

40-
def variable_description(var):
40+
def variable_description(var, skip_attributes=False):
4141
"""Return a variable descriptor.
4242
4343
A descriptor is a hashable tuple which should uniquely define
@@ -46,18 +46,21 @@ def variable_description(var):
4646
4747
"""
4848
var_type = type(var)
49+
attributes = ()
50+
if not skip_attributes:
51+
attributes = tuple(sorted(var.attributes.items()))
4952
if var.is_discrete:
5053
return (var_type.__module__,
5154
var_type.__name__,
5255
var.name,
5356
(("values", tuple(var.values)),),
54-
tuple(sorted(var.attributes.items())))
57+
attributes)
5558
else:
5659
return (var_type.__module__,
5760
var_type.__name__,
5861
var.name,
5962
(),
60-
tuple(sorted(var.attributes.items())))
63+
attributes)
6164

6265

6366
def variable_from_description(description, compute_value=None):
@@ -101,12 +104,13 @@ def set_dict(self, dict):
101104
self.appendRow([key_item, value_item])
102105

103106
def get_dict(self):
104-
dict = {}
105-
for row in range(self.rowCount()):
106-
key_item = self.item(row, 0)
107-
value_item = self.item(row, 1)
108-
dict[str(key_item.text())] = str(value_item.text())
109-
return dict
107+
# Use the same functionality that parses attributes
108+
# when reading text files
109+
return Orange.data.Flags([
110+
"{}={}".format(self.item(row, 0).text(),
111+
self.item(row, 1).text())
112+
for row in range(self.rowCount())
113+
]).attributes
110114

111115

112116
class VariableEditor(QWidget):
@@ -423,7 +427,7 @@ def reset_selected(self):
423427
ind = self.selected_var_index()
424428
if ind >= 0:
425429
var = self.input_vars[ind]
426-
desc = variable_description(var)
430+
desc = variable_description(var, skip_attributes=True)
427431
if desc in self.domain_change_hints:
428432
del self.domain_change_hints[desc]
429433

@@ -454,7 +458,7 @@ def _initialize(self):
454458
def _restore(self):
455459
# Restore the variable states from saved settings.
456460
def transform(var):
457-
vdesc = variable_description(var)
461+
vdesc = variable_description(var, skip_attributes=True)
458462
if vdesc in self.domain_change_hints:
459463
return variable_from_description(
460464
self.domain_change_hints[vdesc],
@@ -513,8 +517,8 @@ def _on_variable_changed(self):
513517

514518

515519
# Store the transformation hint.
516-
self.domain_change_hints[variable_description(old_var)] = \
517-
variable_description(new_var)
520+
old_var_desc = variable_description(old_var, skip_attributes=True)
521+
self.domain_change_hints[old_var_desc] = variable_description(new_var)
518522

519523
self._invalidate()
520524

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Test methods with long descriptive names can omit docstrings
2+
# pylint: disable=missing-docstring
3+
from Orange.data import Table, ContinuousVariable, Domain
4+
from Orange.widgets.data.owcolor import OWColor
5+
from Orange.widgets.tests.base import WidgetTest
6+
7+
8+
class TestOWColor(WidgetTest):
9+
def setUp(self):
10+
self.widget = self.create_widget(OWColor)
11+
12+
self.iris = Table("iris")
13+
14+
def test_reuse_old_settings(self):
15+
self.send_signal("Data", self.iris)
16+
17+
assert isinstance(self.widget, OWColor)
18+
self.widget.saveSettings()
19+
20+
w = self.create_widget(OWColor, reset_default_settings=False)
21+
self.send_signal("Data", self.iris, widget=w)
22+
23+
def test_invalid_input_colors(self):
24+
a = ContinuousVariable("a")
25+
a.attributes["colors"] = "invalid"
26+
a.colors
27+
t = Table(Domain([a]))
28+
29+
self.send_signal("Data", t)

Orange/widgets/data/tests/test_oweditdomain.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22
# pylint: disable=missing-docstring
33

44
from unittest import TestCase
5+
import numpy as np
56

6-
from Orange.data import ContinuousVariable, DiscreteVariable
7-
from Orange.widgets.data.oweditdomain import EditDomainReport
7+
from PyQt4.QtCore import QModelIndex, Qt
8+
9+
from Orange.data import ContinuousVariable, DiscreteVariable, Table, Domain
10+
from Orange.widgets.data.oweditdomain import EditDomainReport, OWEditDomain, \
11+
ContinuousVariableEditor
12+
from Orange.widgets.data.owcolor import OWColor, ColorRole
13+
from Orange.widgets.tests.base import WidgetTest
814

915
SECTION_NAME = "NAME"
1016

@@ -69,3 +75,66 @@ def assertNotEmpty(self, iterable):
6975
next(iter(iterable))
7076
except StopIteration:
7177
self.fail("Iterator did not produce any lines")
78+
79+
80+
class TestOWEditDomain(WidgetTest):
81+
def setUp(self):
82+
self.widget = self.create_widget(OWEditDomain)
83+
self.iris = Table("iris")
84+
85+
def test_input_data(self):
86+
"""Check widget's data with data on the input"""
87+
self.assertEqual(self.widget.data, None)
88+
self.send_signal("Data", self.iris)
89+
self.assertEqual(self.widget.data, self.iris)
90+
91+
def test_input_data_disconnect(self):
92+
"""Check widget's data after disconnecting data on the input"""
93+
self.send_signal("Data", self.iris)
94+
self.assertEqual(self.widget.data, self.iris)
95+
self.send_signal("Data", None)
96+
self.assertEqual(self.widget.data, None)
97+
98+
def test_output_data(self):
99+
"""Check data on the output after apply"""
100+
self.send_signal("Data", self.iris)
101+
output = self.get_output("Data")
102+
np.testing.assert_array_equal(output.X, self.iris.X)
103+
np.testing.assert_array_equal(output.Y, self.iris.Y)
104+
self.assertEqual(output.domain, self.iris.domain)
105+
106+
def test_input_from_owcolor(self):
107+
"""Check widget's data sent from OWColor widget"""
108+
owcolor = self.create_widget(OWColor)
109+
self.send_signal("Data", self.iris, widget=owcolor)
110+
owcolor.disc_model.setData(QModelIndex(), (250, 97, 70, 255), ColorRole)
111+
owcolor.cont_model.setData(
112+
QModelIndex(), ((255, 80, 114, 255), (255, 255, 0, 255), False),
113+
ColorRole)
114+
owcolor_output = self.get_output("Data", owcolor)
115+
self.send_signal("Data", owcolor_output)
116+
self.assertEqual(self.widget.data, owcolor_output)
117+
self.assertIsNotNone(self.widget.data.domain.class_vars[-1].colors)
118+
119+
def test_list_attributes_remain_lists(self):
120+
a = ContinuousVariable("a")
121+
a.attributes["list"] = [1, 2, 3]
122+
d = Domain([a])
123+
t = Table(d)
124+
125+
self.send_signal("Data", t)
126+
127+
assert isinstance(self.widget, OWEditDomain)
128+
# select first variable
129+
idx = self.widget.domain_view.model().index(0)
130+
self.widget.domain_view.setCurrentIndex(idx)
131+
132+
# change first attribute value
133+
editor = self.widget.editor_stack.findChild(ContinuousVariableEditor)
134+
assert isinstance(editor, ContinuousVariableEditor)
135+
idx = editor.labels_model.index(0, 1)
136+
editor.labels_model.setData(idx, "[1, 2, 4]", Qt.EditRole)
137+
138+
self.widget.unconditional_commit()
139+
t2 = self.get_output("Data")
140+
self.assertEqual(t2.domain["a"].attributes["list"], [1, 2, 4])

Orange/widgets/settings.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,13 @@ def new_context(self, domain, attributes, class_vars, metas):
10831083
context.class_vars = class_vars
10841084
return context
10851085

1086+
def clone_context(self, old_context, *args):
1087+
"""Copy of context is always valid, since widgets are using
1088+
the same domain."""
1089+
context = self.new_context(*args)
1090+
context.values = copy.deepcopy(old_context.values)
1091+
return context
1092+
10861093
def encode_domain(self, domain):
10871094
"""Encode domain into tuples (name, type)
10881095
A tuple is returned for each of attributes, class_vars and metas.

Orange/widgets/tests/base.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def tearDownClass(cls):
8989
w.onDeleteWidget()
9090
sip.delete(w)
9191

92-
def create_widget(self, cls, stored_settings=None):
92+
def create_widget(self, cls, stored_settings=None, reset_default_settings=True):
9393
"""Create a widget instance.
9494
9595
Parameters
@@ -98,12 +98,16 @@ def create_widget(self, cls, stored_settings=None):
9898
Widget class to instantiate
9999
stored_settings : dict
100100
Default values for settings
101+
reset_default_settings : bool
102+
If set, widget will start with default values for settings,
103+
if not, values accumulated through the session will be used
101104
102105
Returns
103106
-------
104107
Widget instance : cls
105108
"""
106-
self.reset_default_settings(cls)
109+
if reset_default_settings:
110+
self.reset_default_settings(cls)
107111
widget = cls.__new__(cls, signal_manager=self.signal_manager,
108112
stored_settings=stored_settings)
109113
widget.__init__()
@@ -138,6 +142,15 @@ def process_events():
138142
"""
139143
app.processEvents()
140144

145+
def show(self, widget=None):
146+
"""Show widget in interactive mode.
147+
148+
Useful for debugging tests, as widget can be inspected manually.
149+
"""
150+
widget = widget or self.widget
151+
widget.show()
152+
app.exec()
153+
141154
def send_signal(self, input_name, value, id=None, widget=None):
142155
""" Send signal to widget by calling appropriate triggers.
143156

0 commit comments

Comments
 (0)