From 055c5c7e65c12a40d6b57e956f2343c219ed99f0 Mon Sep 17 00:00:00 2001 From: janezd Date: Tue, 30 Dec 2025 21:20:39 +0100 Subject: [PATCH 1/2] Create Class: Remove context settings in favour of schema-only non-context --- Orange/widgets/data/owcreateclass.py | 29 +++++++++---- .../widgets/data/tests/test_owcreateclass.py | 43 +++++++++---------- i18n/si/msgs.jaml | 6 +++ 3 files changed, 48 insertions(+), 30 deletions(-) diff --git a/Orange/widgets/data/owcreateclass.py b/Orange/widgets/data/owcreateclass.py index ffefb558054..d51f49f0fd9 100644 --- a/Orange/widgets/data/owcreateclass.py +++ b/Orange/widgets/data/owcreateclass.py @@ -13,7 +13,7 @@ from Orange.statistics.util import bincount from Orange.preprocess.transformation import Transformation, Lookup from Orange.widgets import gui, widget -from Orange.widgets.settings import DomainContextHandler, ContextSetting +from Orange.widgets.settings import DomainContextHandler, ContextSetting, Setting from Orange.widgets.utils.itemmodels import DomainModel from Orange.widgets.utils.localization import pl from Orange.widgets.utils.widgetpreview import WidgetPreview @@ -229,12 +229,14 @@ class Outputs: buttons_area_orientation = Qt.Vertical settingsHandler = DomainContextHandler() - attribute = ContextSetting(None) - class_name = ContextSetting("class") - rules = ContextSetting({}) - match_beginning = ContextSetting(False) - case_sensitive = ContextSetting(False) - regular_expressions = ContextSetting(False) + attribute = ContextSetting(None, schema_only=True) + class_name = Setting("class", schema_only=True) + rules = Setting({}, schema_only=True) + match_beginning = Setting(False, schema_only=True) + case_sensitive = Setting(False, schema_only=True) + regular_expressions = Setting(False, schema_only=True) + + settings_version = 2 TRANSFORMERS = {StringVariable: ValueFromStringSubstring, DiscreteVariable: ValueFromDiscreteSubstring} @@ -351,7 +353,6 @@ def rules_to_edits(self): def set_data(self, data): """Input data signal handler.""" self.closeContext() - self.rules = {} self.data = data model = self.controls.attribute.model() model.set_domain(data.domain if data is not None else None) @@ -693,6 +694,18 @@ def _count_part(): self.report_items("Output", [("Class name", self.class_name)]) self.report_raw(f"
    {output}
") + @classmethod + def migrate_settings(cls, settings, version): + if version < 2: + contexts = settings.pop("context_settings", []) + if contexts: + print(contexts[0].values) + print(settings) + settings.update( + {name: contexts[0].values[name][0] + for name in ("class_name", "rules", "match_beginning", + "case_sensitive")}) + if __name__ == "__main__": # pragma: no cover WidgetPreview(OWCreateClass).run(Table("zoo")) diff --git a/Orange/widgets/data/tests/test_owcreateclass.py b/Orange/widgets/data/tests/test_owcreateclass.py index f265011820e..5003fb0cdc2 100644 --- a/Orange/widgets/data/tests/test_owcreateclass.py +++ b/Orange/widgets/data/tests/test_owcreateclass.py @@ -5,6 +5,7 @@ import numpy as np +from orangewidget.settings import Context from Orange.data import Table, StringVariable, DiscreteVariable, Domain from Orange.widgets.data.owcreateclass import ( OWCreateClass, @@ -503,7 +504,7 @@ def _check_thal(self): np.testing.assert_equal(classes[fixed], 1) self.assertTrue(np.all(np.isnan(classes[~(reversable | fixed)]))) - def test_flow_and_context_handling(self): + def test_flow(self): widget = self.widget self.send_signal(self.widget.Inputs.data, self.heart) self._test_default_rules() @@ -545,27 +546,6 @@ def test_flow_and_context_handling(self): self._set_attr(thal) self._check_thal() - prev_rules = widget.rules - self.send_signal(self.widget.Inputs.data, self.zoo) - self.assertIsNot(widget.rules, prev_rules) - - self.send_signal(self.widget.Inputs.data, self.heart) - self._check_thal() - - # Check that sending None as data does not ruin the context, and that - # the empty context does not match the true one later - self.send_signal(self.widget.Inputs.data, None) - self.assertIsNot(widget.rules, prev_rules) - - self.send_signal(self.widget.Inputs.data, self.heart) - self._check_thal() - - self.send_signal(self.widget.Inputs.data, self.no_attributes) - self.assertIsNot(widget.rules, prev_rules) - - self.send_signal(self.widget.Inputs.data, self.heart) - self._check_thal() - def test_add_remove_lines(self): widget = self.widget self.send_signal(self.widget.Inputs.data, self.heart) @@ -690,6 +670,25 @@ def test_same_class(self): self.get_output(widget2.Outputs.data, widget=widget2).domain.class_var ) + def test_migrate_settings_1_2(self): + settings = {"__version__": 1, "context_settings": [Context( + values= { + 'attribute': ('type', 101), + 'case_sensitive': (True, -2), + 'class_name': ('class', -2), + 'match_beginning': (True, -2), + 'regular_expressions': (False, -2), + 'rules': ({'type': [['', 'am'], ['', 'er'], ['', '']]}, -2), + '__version__': 1}, + attributes = {'hair': 1, 'feathers': 1, 'eggs': 1, 'type': 1}, + metas = {'name': 3})]} + w = self.create_widget(OWCreateClass, stored_settings=settings) + self.assertTrue(w.case_sensitive) + self.assertTrue(w.match_beginning) + self.assertFalse(w.regular_expressions) + self.assertEqual(w.class_name, "class") + self.assertEqual(w.rules["type"], [['', 'am'], ['', 'er'], ['', '']]) + if __name__ == "__main__": unittest.main() diff --git a/i18n/si/msgs.jaml b/i18n/si/msgs.jaml index 6fc02647808..679aa30bbb5 100644 --- a/i18n/si/msgs.jaml +++ b/i18n/si/msgs.jaml @@ -4873,6 +4873,12 @@ widgets/data/owcreateclass.py: Output: Izhod Class name: Ime razreda
    {output}
: false + def `migrate_settings`: + context_settings: false + class_name: false + rules: false + match_beginning: false + case_sensitive: false __main__: false zoo: false widgets/data/owcreateinstance.py: From feac9442f8388d148ba9561c5cf5be5cfea01689 Mon Sep 17 00:00:00 2001 From: janezd Date: Wed, 31 Dec 2025 13:06:13 +0100 Subject: [PATCH 2/2] Create Class: Improve layout --- Orange/widgets/data/owcreateclass.py | 16 +++++++++++----- Orange/widgets/data/tests/test_owcreateclass.py | 13 ++++++++++--- i18n/si/msgs.jaml | 4 ++-- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/Orange/widgets/data/owcreateclass.py b/Orange/widgets/data/owcreateclass.py index d51f49f0fd9..60ae07fe2f9 100644 --- a/Orange/widgets/data/owcreateclass.py +++ b/Orange/widgets/data/owcreateclass.py @@ -272,15 +272,15 @@ def __init__(self): #: list of list of QLabel: pairs of labels with counts self.counts = [] - gui.lineEdit( + le = gui.lineEdit( self.controlArea, self, "class_name", orientation=Qt.Horizontal, box="New Class Name") + le.setStyleSheet("QLineEdit { padding-left: 4px; }") - variable_select_box = gui.vBox(self.controlArea, "Match by Substring") + variable_select_box = gui.vBox(self.controlArea, box="Source column and patterns") combo = gui.comboBox( - variable_select_box, self, "attribute", label="From column:", - orientation=Qt.Horizontal, searchable=True, + variable_select_box, self, "attribute", searchable=True, callback=self.update_rules, model=DomainModel(valid_types=(StringVariable, DiscreteVariable))) # Don't use setSizePolicy keyword argument here: it applies to box, @@ -330,8 +330,8 @@ def __init__(self): gui.button(self.buttonsArea, self, "Apply", callback=self.apply) - # TODO: Resizing upon changing the number of rules does not work self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) + self.update_dynamic_height(initial=True) @property def active_rules(self): @@ -349,6 +349,12 @@ def rules_to_edits(self): for edit, text in zip(editr, textr): edit.setText(text) + def update_dynamic_height(self, initial=False): + self.updateGeometry() + current_width = 350 if initial else self.width() + target_height = self.layout().sizeHint().height() + self.resize(current_width, target_height) + @Inputs.data def set_data(self, data): """Input data signal handler.""" diff --git a/Orange/widgets/data/tests/test_owcreateclass.py b/Orange/widgets/data/tests/test_owcreateclass.py index 5003fb0cdc2..a63a1dfe30a 100644 --- a/Orange/widgets/data/tests/test_owcreateclass.py +++ b/Orange/widgets/data/tests/test_owcreateclass.py @@ -678,16 +678,23 @@ def test_migrate_settings_1_2(self): 'class_name': ('class', -2), 'match_beginning': (True, -2), 'regular_expressions': (False, -2), - 'rules': ({'type': [['', 'am'], ['', 'er'], ['', '']]}, -2), + 'rules': ({'type': [['cam', 'am'], ['cer', 'er'], ['', '']], + 'eggs': [['de', 'e1'], ['', '']]}, + -2), '__version__': 1}, attributes = {'hair': 1, 'feathers': 1, 'eggs': 1, 'type': 1}, metas = {'name': 3})]} w = self.create_widget(OWCreateClass, stored_settings=settings) + self.send_signal(w.Inputs.data, self.zoo, widget=w) + self.assertEqual(w.active_rules, [['cam', 'am'], ['cer', 'er'], ['', '']]) + self.assertEqual(w.class_name, "class") self.assertTrue(w.case_sensitive) self.assertTrue(w.match_beginning) self.assertFalse(w.regular_expressions) - self.assertEqual(w.class_name, "class") - self.assertEqual(w.rules["type"], [['', 'am'], ['', 'er'], ['', '']]) + + w.attribute = self.zoo.domain["eggs"] + self.assertEqual(w.active_rules, [['de', 'e1'], ['', '']]) + if __name__ == "__main__": diff --git a/i18n/si/msgs.jaml b/i18n/si/msgs.jaml index 679aa30bbb5..c6ea8843085 100644 --- a/i18n/si/msgs.jaml +++ b/i18n/si/msgs.jaml @@ -4820,9 +4820,9 @@ widgets/data/owcreateclass.py: def `__init__`: class_name: false New Class Name: Ime novega razreda - Match by Substring: Vzorci in razredi + 'QLineEdit { padding-left: 4px; }': false + Source column and patterns: Izvorni stolpec in vzorci attribute: false - From column:: Iz stolpca: Name: Ime Substring: Vzorec Count: Primerov