Skip to content

Commit 972ec52

Browse files
authored
Merge pull request #3231 from ales-erjavec/oweditdomain-record-transformations
[ENH] Edit Domain: Record transformations
2 parents f1a9d69 + 019ab8f commit 972ec52

File tree

8 files changed

+1435
-619
lines changed

8 files changed

+1435
-619
lines changed

Orange/widgets/data/oweditdomain.py

Lines changed: 1179 additions & 476 deletions
Large diffs are not rendered by default.
Lines changed: 162 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,57 @@
11
# Test methods with long descriptive names can omit docstrings
2-
# pylint: disable=missing-docstring
3-
2+
# pylint: disable=all
43
from unittest import TestCase
54
import numpy as np
5+
from numpy.testing import assert_array_equal
66

77
from AnyQt.QtCore import QModelIndex, QItemSelectionModel, Qt
8-
9-
from Orange.data import ContinuousVariable, DiscreteVariable, \
10-
StringVariable, TimeVariable, Table, Domain
11-
from Orange.widgets.data.oweditdomain import EditDomainReport, OWEditDomain, \
12-
ContinuousVariableEditor, DiscreteVariableEditor, VariableEditor, \
13-
TimeVariableEditor
8+
from AnyQt.QtWidgets import QAction
9+
from AnyQt.QtTest import QTest
10+
11+
from Orange.data import (
12+
ContinuousVariable, DiscreteVariable, StringVariable, TimeVariable,
13+
Table, Domain
14+
)
15+
from Orange.preprocess.transformation import Identity, Lookup
16+
from Orange.widgets.data.oweditdomain import (
17+
OWEditDomain,
18+
ContinuousVariableEditor, DiscreteVariableEditor, VariableEditor,
19+
TimeVariableEditor, Categorical, Real, Time, String,
20+
Rename, Annotate, CategoriesMapping, report_transform,
21+
apply_transform
22+
)
1423
from Orange.widgets.data.owcolor import OWColor, ColorRole
1524
from Orange.widgets.tests.base import WidgetTest, GuiTest
1625

17-
SECTION_NAME = "NAME"
1826

19-
20-
class TestEditDomainReport(TestCase):
21-
# This tests test private methods
22-
# pylint: disable=protected-access
23-
24-
def setUp(self):
25-
self.report = EditDomainReport([], [])
26-
27-
def test_section_yields_nothing_for_no_changes(self):
28-
result = self.report._section(SECTION_NAME, [])
29-
self.assertEmpty(result)
30-
31-
def test_section_yields_header_for_changes(self):
32-
result = self.report._section(SECTION_NAME, ["a"])
33-
self.assertTrue(any(SECTION_NAME in item for item in result))
34-
35-
def test_value_changes_yields_nothing_for_no_change(self):
36-
a = DiscreteVariable("a", values="abc")
37-
self.assertEmpty(self.report._value_changes(a, a))
38-
39-
def test_value_changes_yields_nothing_for_continuous_variables(self):
40-
v1, v2 = ContinuousVariable("a"), ContinuousVariable("b")
41-
self.assertEmpty(self.report._value_changes(v1, v2))
42-
43-
def test_value_changes_yields_changed_values(self):
44-
v1, v2 = DiscreteVariable("a", "ab"), DiscreteVariable("b", "ac")
45-
self.assertNotEmpty(self.report._value_changes(v1, v2))
46-
47-
def test_label_changes_yields_nothing_for_no_change(self):
48-
v1 = ContinuousVariable("a")
49-
v1.attributes["a"] = "b"
50-
self.assertEmpty(self.report._value_changes(v1, v1))
51-
52-
def test_label_changes_yields_added_labels(self):
53-
v1 = ContinuousVariable("a")
54-
v2 = v1.copy(None)
55-
v2.attributes["a"] = "b"
56-
self.assertNotEmpty(self.report._label_changes(v1, v2))
57-
58-
def test_label_changes_yields_removed_labels(self):
59-
v1 = ContinuousVariable("a")
60-
v1.attributes["a"] = "b"
61-
v2 = v1.copy(None)
62-
del v2.attributes["a"]
63-
self.assertNotEmpty(self.report._label_changes(v1, v2))
64-
65-
def test_label_changes_yields_modified_labels(self):
66-
v1 = ContinuousVariable("a")
67-
v1.attributes["a"] = "b"
68-
v2 = v1.copy(None)
69-
v2.attributes["a"] = "c"
70-
self.assertNotEmpty(self.report._label_changes(v1, v2))
71-
72-
def assertEmpty(self, iterable):
73-
self.assertRaises(StopIteration, lambda: next(iter(iterable)))
74-
75-
def assertNotEmpty(self, iterable):
76-
try:
77-
next(iter(iterable))
78-
except StopIteration:
79-
self.fail("Iterator did not produce any lines")
27+
class TestReport(TestCase):
28+
def test_rename(self):
29+
var = Real("X", (-1, ""), ())
30+
tr = Rename("Y")
31+
val = report_transform(var, [tr])
32+
self.assertIn("X", val)
33+
self.assertIn("Y", val)
34+
35+
def test_annotate(self):
36+
var = Real("X", (-1, ""), (("a", "1"), ("b", "z")))
37+
tr = Annotate((("a", "2"), ("j", "z")))
38+
r = report_transform(var, [tr])
39+
self.assertIn("a", r)
40+
self.assertIn("b", r)
41+
42+
def test_categories_mapping(self):
43+
var = Categorical("C", ("a", "b", "c"), None, ())
44+
tr = CategoriesMapping(
45+
(("a", "aa"),
46+
("b", None),
47+
("c", "cc"),
48+
(None, "ee")),
49+
)
50+
r = report_transform(var, [tr])
51+
self.assertIn("a", r)
52+
self.assertIn("aa", r)
53+
self.assertIn("b", r)
54+
self.assertIn("<s>", r)
8055

8156

8257
class TestOWEditDomain(WidgetTest):
@@ -137,7 +112,7 @@ def test_list_attributes_remain_lists(self):
137112
idx = editor.labels_model.index(0, 1)
138113
editor.labels_model.setData(idx, "[1, 2, 4]", Qt.EditRole)
139114

140-
self.widget.unconditional_commit()
115+
self.widget.commit()
141116
t2 = self.get_output(self.widget.Outputs.data)
142117
self.assertEqual(t2.domain["a"].attributes["list"], [1, 2, 4])
143118

@@ -156,15 +131,20 @@ def test_duplicate_names(self):
156131
self.widget.domain_view.setCurrentIndex(idx)
157132
editor = self.widget.editor_stack.findChild(ContinuousVariableEditor)
158133

159-
editor.name_edit.setText("iris")
160-
editor.commit()
134+
def enter_text(widget, text):
135+
# type: (QLineEdit, str) -> None
136+
widget.selectAll()
137+
QTest.keyClick(widget, Qt.Key_Delete)
138+
QTest.keyClicks(widget, text)
139+
QTest.keyClick(widget, Qt.Key_Return)
140+
141+
enter_text(editor.name_edit, "iris")
161142
self.widget.commit()
162143
self.assertTrue(self.widget.Error.duplicate_var_name.is_shown())
163144
output = self.get_output(self.widget.Outputs.data)
164145
self.assertIsNone(output)
165146

166-
editor.name_edit.setText("sepal height")
167-
editor.commit()
147+
enter_text(editor.name_edit, "sepal height")
168148
self.widget.commit()
169149
self.assertFalse(self.widget.Error.duplicate_var_name.is_shown())
170150
output = self.get_output(self.widget.Outputs.data)
@@ -176,10 +156,12 @@ def test_time_variable_preservation(self):
176156
self.send_signal(self.widget.Inputs.data, table)
177157
output = self.get_output(self.widget.Outputs.data)
178158
self.assertEqual(str(table[0, 4]), str(output[0, 4]))
159+
view = self.widget.variables_view
160+
view.setCurrentIndex(view.model().index(4))
179161

180162
editor = self.widget.editor_stack.findChild(TimeVariableEditor)
181163
editor.name_edit.setText("Date")
182-
editor.commit()
164+
editor.variable_changed.emit()
183165
self.widget.commit()
184166
output = self.get_output(self.widget.Outputs.data)
185167
self.assertEqual(str(table[0, 4]), str(output[0, 4]))
@@ -188,54 +170,68 @@ def test_time_variable_preservation(self):
188170
class TestEditors(GuiTest):
189171
def test_variable_editor(self):
190172
w = VariableEditor()
191-
self.assertIs(w.get_data(), None)
173+
self.assertEqual(w.get_data(), (None, []))
192174

193-
v = StringVariable(name="S")
194-
v.attributes.update({"A": 1, "B": "b"},)
195-
w.set_data(v)
175+
v = String("S", (("A", "1"), ("B", "b")))
176+
w.set_data(v, [])
196177

197178
self.assertEqual(w.name_edit.text(), v.name)
198-
self.assertEqual(w.labels_model.get_dict(), v.attributes)
199-
self.assertTrue(w.is_same())
179+
self.assertEqual(w.labels_model.get_dict(),
180+
{"A": "1", "B": "b"})
181+
self.assertEqual(w.get_data(), (v, []))
200182

201183
w.set_data(None)
202184
self.assertEqual(w.name_edit.text(), "")
203185
self.assertEqual(w.labels_model.get_dict(), {})
204-
self.assertIs(w.get_data(), None)
186+
self.assertEqual(w.get_data(), (None, []))
187+
188+
w.set_data(v, [Rename("T"), Annotate((("a", "1"), ("b", "2")))])
189+
self.assertEqual(w.name_edit.text(), "T")
190+
self.assertEqual(w.labels_model.rowCount(), 2)
191+
add = w.findChild(QAction, "action-add-label")
192+
add.trigger()
193+
remove = w.findChild(QAction, "action-delete-label")
194+
remove.trigger()
205195

206196
def test_continuous_editor(self):
207197
w = ContinuousVariableEditor()
208-
self.assertIs(w.get_data(), None)
198+
self.assertEqual(w.get_data(), (None, []))
209199

210-
v = ContinuousVariable("X", number_of_decimals=5)
211-
v.attributes.update({"A": 1, "B": "b"})
212-
w.set_data(v)
200+
v = Real("X", (-1, ""), (("A", "1"), ("B", "b")))
201+
w.set_data(v, [])
213202

214203
self.assertEqual(w.name_edit.text(), v.name)
215-
self.assertEqual(w.labels_model.get_dict(), v.attributes)
216-
self.assertTrue(w.is_same())
204+
self.assertEqual(w.labels_model.get_dict(), dict(v.annotations))
217205

218206
w.set_data(None)
219207
self.assertEqual(w.name_edit.text(), "")
220208
self.assertEqual(w.labels_model.get_dict(), {})
221-
self.assertIs(w.get_data(), None)
209+
self.assertEqual(w.get_data(), (None, []))
222210

223211
def test_discrete_editor(self):
224212
w = DiscreteVariableEditor()
225-
self.assertIs(w.get_data(), None)
213+
self.assertEqual(w.get_data(), (None, []))
226214

227-
v = DiscreteVariable("C", values=["a", "b", "c"])
228-
v.attributes.update({"A": 1, "B": "b"})
215+
v = Categorical("C", ("a", "b", "c"), None,
216+
(("A", "1"), ("B", "b")))
229217
w.set_data(v)
230218

231219
self.assertEqual(w.name_edit.text(), v.name)
232-
self.assertEqual(w.labels_model.get_dict(), v.attributes)
233-
self.assertTrue(w.is_same())
234-
220+
self.assertEqual(w.labels_model.get_dict(), dict(v.annotations))
221+
self.assertEqual(w.get_data(), (v, []))
235222
w.set_data(None)
236223
self.assertEqual(w.name_edit.text(), "")
237224
self.assertEqual(w.labels_model.get_dict(), {})
238-
self.assertIs(w.get_data(), None)
225+
self.assertEqual(w.get_data(), (None, []))
226+
mapping = [
227+
("c", "C"),
228+
("a", "A"),
229+
("b", None),
230+
(None, "b")
231+
]
232+
w.set_data(v, [CategoriesMapping(mapping)])
233+
w.grab() # run delegate paint method
234+
self.assertEqual(w.get_data(), (v, [CategoriesMapping(mapping)]))
239235

240236
# test selection/deselection in the view
241237
w.set_data(v)
@@ -249,21 +245,79 @@ def test_discrete_editor(self):
249245

250246
def test_time_editor(self):
251247
w = TimeVariableEditor()
252-
self.assertIs(w.get_data(), None)
248+
self.assertEqual(w.get_data(), (None, []))
253249

254-
v = TimeVariable("T", have_date=1)
255-
v.attributes.update({"A": 1, "B": "b"})
256-
w.set_data(v)
250+
v = Time("T", (("A", "1"), ("B", "b")))
251+
w.set_data(v,)
257252

258253
self.assertEqual(w.name_edit.text(), v.name)
259-
self.assertEqual(w.labels_model.get_dict(), v.attributes)
260-
self.assertTrue(w.is_same())
261-
262-
var = w.get_data()
263-
self.assertTrue(var.have_date)
264-
self.assertFalse(var.have_time)
254+
self.assertEqual(w.labels_model.get_dict(), dict(v.annotations))
265255

266256
w.set_data(None)
267257
self.assertEqual(w.name_edit.text(), "")
268258
self.assertEqual(w.labels_model.get_dict(), {})
269-
self.assertIs(w.get_data(), None)
259+
self.assertEqual(w.get_data(), (None, []))
260+
261+
262+
class TestTransforms(TestCase):
263+
def _test_common(self, var):
264+
tr = [Rename(var.name + "_copy"), Annotate((("A", "1"),))]
265+
XX = apply_transform(var, tr)
266+
self.assertEqual(XX.name, var.name + "_copy")
267+
self.assertEqual(XX.attributes, {"A": 1})
268+
self.assertIsInstance(XX.compute_value, Identity)
269+
self.assertIs(XX.compute_value.variable, var)
270+
271+
def test_continous(self):
272+
X = ContinuousVariable("X")
273+
self._test_common(X)
274+
275+
def test_string(self):
276+
X = StringVariable("S")
277+
self._test_common(X)
278+
279+
def test_time(self):
280+
X = TimeVariable("X")
281+
self._test_common(X)
282+
283+
def test_discrete(self):
284+
D = DiscreteVariable("D", values=("a", "b"))
285+
self._test_common(D)
286+
287+
def test_discrete_rename(self):
288+
D = DiscreteVariable("D", values=("a", "b"))
289+
DD = apply_transform(D, [CategoriesMapping((("a", "A"), ("b", "B")))])
290+
self.assertSequenceEqual(DD.values, ["A", "B"])
291+
self.assertIs(DD.compute_value.variable, D)
292+
293+
def test_discrete_reorder(self):
294+
D = DiscreteVariable("D", values=("2", "3", "1", "0"))
295+
DD = apply_transform(D, [CategoriesMapping((("0", "0"), ("1", "1"),
296+
("2", "2"), ("3", "3")))])
297+
self.assertSequenceEqual(DD.values, ["0", "1", "2", "3"])
298+
self._assertLookupEquals(
299+
DD.compute_value, Lookup(D, np.array([2, 3, 1, 0]))
300+
)
301+
302+
def test_discrete_add_drop(self):
303+
D = DiscreteVariable("D", values=("2", "3", "1", "0"), base_value=1)
304+
mapping = (
305+
("0", None),
306+
("1", "1"),
307+
("2", "2"),
308+
("3", None),
309+
(None, "A"),
310+
)
311+
tr = [CategoriesMapping(mapping)]
312+
DD = apply_transform(D, tr)
313+
self.assertSequenceEqual(DD.values, ["1", "2", "A"])
314+
self._assertLookupEquals(
315+
DD.compute_value, Lookup(D, np.array([1, np.nan, 0, np.nan]))
316+
)
317+
self.assertEqual(DD.base_value, -1)
318+
319+
def _assertLookupEquals(self, first, second):
320+
self.assertIsInstance(first, Lookup)
321+
self.assertIsInstance(second, Lookup)
322+
self.assertIs(first.variable, second.variable)
323+
assert_array_equal(first.lookup_table, second.lookup_table)

0 commit comments

Comments
 (0)