Skip to content

Commit 9f3a965

Browse files
authored
Merge pull request #4254 from janezd/select-rows-change-vartype
[FIX] Select Rows: Fix crash on changed variable type
2 parents 5c66345 + fa537b1 commit 9f3a965

File tree

2 files changed

+121
-19
lines changed

2 files changed

+121
-19
lines changed

Orange/widgets/data/owselectrows.py

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,26 +44,47 @@ def is_valid_item(self, setting, condition, attrs, metas):
4444
return varname in attrs or varname in metas
4545

4646
def encode_setting(self, context, setting, value):
47-
if setting.name == 'conditions':
48-
CONTINUOUS = vartype(ContinuousVariable("x"))
49-
for i, (attr, op, values) in enumerate(value):
50-
if context.attributes.get(attr) == CONTINUOUS:
51-
if values and isinstance(values[0], str):
52-
values = [QLocale().toDouble(v)[0] for v in values]
53-
value[i] = (attr, op, values)
54-
return super().encode_setting(context, setting, value)
47+
if setting.name != 'conditions':
48+
return super().encode_settings(context, setting, value)
49+
50+
encoded = []
51+
CONTINUOUS = vartype(ContinuousVariable("x"))
52+
for attr, op, values in value:
53+
vtype = context.attributes.get(attr)
54+
if vtype == CONTINUOUS and values and isinstance(values[0], str):
55+
values = [QLocale().toDouble(v)[0] for v in values]
56+
encoded.append((attr, vtype, op, values))
57+
return encoded
5558

5659
def decode_setting(self, setting, value, domain=None):
5760
value = super().decode_setting(setting, value, domain)
5861
if setting.name == 'conditions':
59-
for i, (attr, op, values) in enumerate(value):
62+
# Use this after 2022/2/2:
63+
# for i, (attr, _, op, values) in enumerate(value):
64+
for i, condition in enumerate(value):
65+
attr = condition[0]
66+
op, values = condition[-2:]
67+
6068
var = attr in domain and domain[attr]
6169
if var and var.is_continuous and not isinstance(var, TimeVariable):
62-
value[i] = (attr, op,
63-
list([QLocale().toString(float(i), 'f')
64-
for i in values]))
70+
values = [QLocale().toString(float(i), 'f') for i in values]
71+
value[i] = (attr, op, values)
6572
return value
6673

74+
def match(self, context, domain, attrs, metas):
75+
if (attrs, metas) == (context.attributes, context.metas):
76+
return self.PERFECT_MATCH
77+
78+
conditions = context.values["conditions"]
79+
all_vars = attrs
80+
all_vars.update(metas)
81+
# Use this after 2022/2/2:
82+
# if all(all_vars.get(name) == tpe for name, tpe, *_ in conditions):
83+
if all(all_vars.get(name) == tpe if len(rest) == 2 else name in all_vars
84+
for name, tpe, *rest in conditions):
85+
return 0.5
86+
return self.NO_MATCH
87+
6788

6889
class FilterDiscreteType(enum.Enum):
6990
Equal = "Equal"
@@ -105,6 +126,8 @@ class Outputs:
105126
purge_classes = Setting(False, schema_only=True)
106127
auto_commit = Setting(True)
107128

129+
settings_version = 2
130+
108131
Operators = {
109132
ContinuousVariable: [
110133
(FilterContinuous.Equal, "equals"),
@@ -692,6 +715,14 @@ def send_report(self):
692715
("Non-matching data",
693716
nonmatch_inst > 0 and "{} instances".format(nonmatch_inst))))
694717

718+
# Uncomment this on 2022/2/2
719+
#
720+
# @classmethod
721+
# def migrate_context(cls, context, version):
722+
# if not version or version < 2:
723+
# # Just remove; can't migrate because variables types are unknown
724+
# context.values["conditions"] = []
725+
695726

696727
class CheckBoxPopup(QWidget):
697728
def __init__(self, var, lc, widget_parent=None, widget=None):

Orange/widgets/data/tests/test_owselectrows.py

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
# Test methods with long descriptive names can omit docstrings
22
# pylint: disable=missing-docstring
3+
import time
4+
35
from AnyQt.QtCore import QLocale, Qt
46
from AnyQt.QtTest import QTest
57
from AnyQt.QtWidgets import QLineEdit, QComboBox
68

79
import numpy as np
810

911
from Orange.data import (
10-
Table, ContinuousVariable, StringVariable, DiscreteVariable)
12+
Table, ContinuousVariable, StringVariable, DiscreteVariable, Domain)
1113
from Orange.widgets.data.owselectrows import (
1214
OWSelectRows, FilterDiscreteType, SelectRowsContextHandler)
1315
from Orange.widgets.tests.base import WidgetTest, datasets
@@ -16,6 +18,7 @@
1618
from Orange.widgets.tests.utils import simulate, override_locale
1719
from Orange.widgets.utils.annotated_data import ANNOTATED_DATA_FEATURE_NAME
1820
from Orange.tests import test_filename
21+
from orangewidget.settings import VERSION_KEY
1922

2023
CFValues = {
2124
FilterContinuous.Equal: ["5.4"],
@@ -132,22 +135,22 @@ def test_stores_settings_in_invariant_locale(self):
132135
context = self.widget.current_context
133136
self.send_signal(self.widget.Inputs.data, None)
134137
saved_condition = context.values["conditions"][0]
135-
self.assertEqual(saved_condition[2][0], 5.2)
138+
self.assertEqual(saved_condition[3][0], 5.2)
136139

137140
@override_locale(QLocale.C)
138141
def test_restores_continuous_filter_in_c_locale(self):
139142
iris = Table("iris")[:5]
140143
# Settings with string value
141144
self.widget = self.widget_with_context(
142-
iris.domain, [["sepal length", 2, ("5.2",)]])
145+
iris.domain, [["sepal length", 102, 2, ("5.2",)]])
143146
self.send_signal(self.widget.Inputs.data, iris)
144147

145148
values = self.widget.conditions[0][2]
146149
self.assertTrue(values[0].startswith("5.2"))
147150

148151
# Settings with float value
149152
self.widget = self.widget_with_context(
150-
iris.domain, [["sepal length", 2, (5.2,)]])
153+
iris.domain, [["sepal length", 102, 2, (5.2,)]])
151154
self.send_signal(self.widget.Inputs.data, iris)
152155

153156
values = self.widget.conditions[0][2]
@@ -158,20 +161,33 @@ def test_restores_continuous_filter_in_sl_SI_locale(self):
158161
iris = Table("iris")[:5]
159162
# Settings with string value
160163
self.widget = self.widget_with_context(
161-
iris.domain, [["sepal length", 2, ("5.2",)]])
164+
iris.domain, [["sepal length", 102, 2, ("5.2",)]])
162165
self.send_signal(self.widget.Inputs.data, iris)
163166

164167
values = self.widget.conditions[0][2]
165168
self.assertTrue(values[0].startswith("5,2"))
166169

167170
# Settings with float value
168171
self.widget = self.widget_with_context(
169-
iris.domain, [["sepal length", 2, (5.2,)]])
172+
iris.domain, [["sepal length", 102, 2, (5.2,)]])
170173
self.send_signal(self.widget.Inputs.data, iris)
171174

172175
values = self.widget.conditions[0][2]
173176
self.assertTrue(values[0].startswith("5,2"))
174177

178+
@override_locale(QLocale.C)
179+
def test_partial_matches(self):
180+
iris = Table("iris")
181+
domain = iris.domain
182+
self.widget = self.widget_with_context(
183+
domain, [[domain[0].name, 2, ("5.2",)]])
184+
iris2 = iris.transform(Domain(domain.attributes[:2], None))
185+
self.send_signal(self.widget.Inputs.data, iris2)
186+
condition = self.widget.conditions[0]
187+
self.assertEqual(condition[0], "sepal length")
188+
self.assertEqual(condition[1], 2)
189+
self.assertTrue(condition[2][0].startswith("5.2"))
190+
175191
def test_load_settings(self):
176192
iris = Table("iris")[:5]
177193
self.send_signal(self.widget.Inputs.data, iris)
@@ -242,10 +258,65 @@ def test_annotated_data(self):
242258
np.testing.assert_equal(annotations[:50], True)
243259
np.testing.assert_equal(annotations[50:], False)
244260

261+
def test_change_var_type(self):
262+
iris = Table("iris")
263+
domain = iris.domain
264+
265+
self.send_signal(self.widget.Inputs.data, iris)
266+
self.widget.remove_all_button.click()
267+
self.enterFilter(domain[0], "is below", "5.2")
268+
269+
var0vals = list({str(x) for x in iris.X[:, 0]})
270+
new_domain = Domain(
271+
(DiscreteVariable(domain[0].name, values=var0vals), )
272+
+ domain.attributes[1:],
273+
domain.class_var)
274+
new_iris = iris.transform(new_domain)
275+
self.send_signal(self.widget.Inputs.data, new_iris)
276+
277+
# Uncomment this on 2022/2/2
278+
#
279+
# def test_migration_to_version_1(self):
280+
# iris = Table("iris")
281+
#
282+
# ch = SelectRowsContextHandler()
283+
# context = ch.new_context(iris.domain, *ch.encode_domain(iris.domain))
284+
# context.values = dict(conditions=[["petal length", 2, (5.2,)]])
285+
# settings = dict(context_settings=[context])
286+
# widget = self.create_widget(OWSelectRows, settings)
287+
# self.assertEqual(widget.conditions, [])
288+
289+
@override_locale(QLocale.C)
290+
def test_support_old_settings(self):
291+
iris = Table("iris")
292+
self.widget = self.widget_with_context(
293+
iris.domain, [["sepal length", 2, ("5.2",)]])
294+
self.send_signal(self.widget.Inputs.data, iris)
295+
condition = self.widget.conditions[0]
296+
self.assertEqual(condition[0], "sepal length")
297+
self.assertEqual(condition[1], 2)
298+
self.assertTrue(condition[2][0].startswith("5.2"))
299+
300+
def test_end_support_for_version_1(self):
301+
if time.gmtime() >= (2022, 2, 2):
302+
self.fail("""
303+
Happy 22/2/2!
304+
305+
Now remove support for version==None settings in
306+
SelectRowsContextHandler.decode_setting and SelectRowsContextHandler.match,
307+
and uncomment OWSelectRows.migrate.
308+
309+
In tests, uncomment test_migration_to_version_1,
310+
and remove test_support_old_settings and this test.
311+
312+
Basically, revert this commit.
313+
""")
314+
245315
def widget_with_context(self, domain, conditions):
246316
ch = SelectRowsContextHandler()
247317
context = ch.new_context(domain, *ch.encode_domain(domain))
248-
context.values = dict(conditions=conditions)
318+
context.values = {"conditions": conditions,
319+
VERSION_KEY: OWSelectRows.settings_version}
249320
settings = dict(context_settings=[context])
250321

251322
return self.create_widget(OWSelectRows, settings)

0 commit comments

Comments
 (0)