Repository: biolab/orange3

Issue:
EditDomain: Unlink variable from its source variable
**What's wrong?**
In the edit domain, the "Unlink variable from its source" checkbox is always disabled (or so it seems).

![Screenshot 2023-09-06 at 11 40 29](https://github.com/biolab/orange3/assets/15876321/e75c1086-fcad-4433-82f8-880351893abe)


This creates a problem later when we want to apply transformations. I think this workflow is enough to illustrate what the problem is:

![Screenshot 2023-09-06 at 11 50 36](https://github.com/biolab/orange3/assets/15876321/eba94a70-4b91-4922-b1e0-806c93f3e73c)


Essentially, Orange sees 'Time' as two different variables because we performed transformations in the edit domain. However, in this case, one would like to treat those two variables as if they are the same.


What needs discussion:
- Is this checkbox disabled for a reason or is this a bug?
- Should we make this option checked by default (for those who know whats up, they will disable it)?
- What about apply domain? Should we support something similar there?


**How can we reproduce the problem?** 
File -> Edit Domain -> try to click on "Unlink variable from its source"





Discussion:
First a clarification: one domain has an attribute A of type string, and edit domain converts it to numeric. In the other domain, A is already numeric and hence doesn't match the attribute coming from Apply domain (because it doesn't have compute value).

The widget works as intended. Input variables have different properties, like name, type, attributes, compute value... Edit Domain can change them, that is, rename, change type, change attributes, remove compute value. The function of this checkbox is -- like the function of all other options -- to change the existing properties, that is, to *remove the existing compute value*.

The widget always outputs a variable with compute value. Otherwise, transformations couldn't be applied on other data.

We could add another checkbox that would remove compute value from the output variable. How to describe it? @JakaKokosar suggested that the existing checkbox is about *inheriting* the connection with an older variable, while the new compute value (that currently can't be removed) is about *creating* the connection between the modified and the existing variable.

Interestingly, this problem can be solved by adding another Edit Domain, which can remove compute value from the variable created in the current Edit Domain.
I looked into that. But.

- Edit Domain constructs a new domain (with compute_values for all variables with any transformations.)
- It uses this domain to transform the data.
- Then it constructs another domain in which those variables that have the "unlink" checkbox checked are copied with `compute_value=None`.
- It replaces the domain (i.e. uses `Table.from_numpy` with that domain and the transformed arrays from the previous step). (This is skipped if the previous step doesn't unlink any variables.)

The checkbox is disabled for variables that don't have prior compute value. So the above steps only remove the existing compute value.

If we add a check box that would remove the new compute value, the above steps would be exactly the same! The distinction is only in *which* compute value is removed - the old one or the new one. We would have two checkboxes whose function is difficult to explain to the user, but they would do the exact same thing -- remove the compute value if *any of them* is checked.

As a further point, the code that checks whether the unlinking is required already looks like this:

```
    return trs is not None \
           and any(isinstance(tr, Unlink) for tr in trs) \
           and (var.compute_value is not None or len(trs) > 1)
```

To my understanding, the current UI doesn't even allow `any(isinstance(tr, Unlink) for tr in trs)` unless `var.compute_value is not None`. :)

Resolving patch:
diff --git a/Orange/widgets/data/oweditdomain.py b/Orange/widgets/data/oweditdomain.py
--- a/Orange/widgets/data/oweditdomain.py
+++ b/Orange/widgets/data/oweditdomain.py
@@ -107,8 +107,7 @@ class Categorical(
     _DataType, NamedTuple("Categorical", [
         ("name", str),
         ("categories", Tuple[str, ...]),
-        ("annotations", AnnotationsType),
-        ("linked", bool)
+        ("annotations", AnnotationsType)
     ])): pass
 
 
@@ -117,24 +116,21 @@ class Real(
         ("name", str),
         # a precision (int, and a format specifier('f', 'g', or '')
         ("format", Tuple[int, str]),
-        ("annotations", AnnotationsType),
-        ("linked", bool)
+        ("annotations", AnnotationsType)
     ])): pass
 
 
 class String(
     _DataType, NamedTuple("String", [
         ("name", str),
-        ("annotations", AnnotationsType),
-        ("linked", bool)
+        ("annotations", AnnotationsType)
     ])): pass
 
 
 class Time(
     _DataType, NamedTuple("Time", [
         ("name", str),
-        ("annotations", AnnotationsType),
-        ("linked", bool)
+        ("annotations", AnnotationsType)
     ])): pass
 
 
@@ -248,7 +244,7 @@ def __call__(self, vector: DataVector) -> StringVector:
         if isinstance(var, String):
             return vector
         return StringVector(
-            String(var.name, var.annotations, False),
+            String(var.name, var.annotations),
             lambda: as_string(vector.data()),
         )
 
@@ -268,11 +264,11 @@ def data() -> MArray:
                 a = categorical_to_string_vector(d, var.values)
                 return MArray(as_float_or_nan(a, where=a.mask), mask=a.mask)
             return RealVector(
-                Real(var.name, (6, 'g'), var.annotations, var.linked), data
+                Real(var.name, (6, 'g'), var.annotations), data
             )
         elif isinstance(var, Time):
             return RealVector(
-                Real(var.name, (6, 'g'), var.annotations, var.linked),
+                Real(var.name, (6, 'g'), var.annotations),
                 lambda: vector.data().astype(float)
             )
         elif isinstance(var, String):
@@ -280,7 +276,7 @@ def data():
                 s = vector.data()
                 return MArray(as_float_or_nan(s, where=s.mask), mask=s.mask)
             return RealVector(
-                Real(var.name, (6, "g"), var.annotations, var.linked), data
+                Real(var.name, (6, "g"), var.annotations), data
             )
         raise AssertionError
 
@@ -296,7 +292,7 @@ def __call__(self, vector: DataVector) -> CategoricalVector:
         if isinstance(var, (Real, Time, String)):
             data, values = categorical_from_vector(vector.data())
             return CategoricalVector(
-                Categorical(var.name, values, var.annotations, var.linked),
+                Categorical(var.name, values, var.annotations),
                 lambda: data
             )
         raise AssertionError
@@ -310,7 +306,7 @@ def __call__(self, vector: DataVector) -> TimeVector:
             return vector
         elif isinstance(var, Real):
             return TimeVector(
-                Time(var.name, var.annotations, var.linked),
+                Time(var.name, var.annotations),
                 lambda: vector.data().astype("M8[us]")
             )
         elif isinstance(var, Categorical):
@@ -320,7 +316,7 @@ def data():
                 dt = pd.to_datetime(s, errors="coerce").values.astype("M8[us]")
                 return MArray(dt, mask=d.mask)
             return TimeVector(
-                Time(var.name, var.annotations, var.linked), data
+                Time(var.name, var.annotations), data
             )
         elif isinstance(var, String):
             def data():
@@ -328,7 +324,7 @@ def data():
                 dt = pd.to_datetime(s, errors="coerce").values.astype("M8[us]")
                 return MArray(dt, mask=s.mask)
             return TimeVector(
-                Time(var.name, var.annotations, var.linked), data
+                Time(var.name, var.annotations), data
             )
         raise AssertionError
 
@@ -636,7 +632,7 @@ def set_data(self, var, transform=()):
         else:
             self.add_label_action.actionGroup().setEnabled(False)
 
-        self.unlink_var_cb.setDisabled(var is None or not var.linked)
+        self.unlink_var_cb.setDisabled(var is None)
 
     def get_data(self):
         """Retrieve the modified variable.
@@ -650,7 +646,7 @@ def get_data(self):
             tr.append(Rename(name))
         if self.var.annotations != labels:
             tr.append(Annotate(labels))
-        if self.var.linked and self.unlink_var_cb.isChecked():
+        if self.unlink_var_cb.isChecked():
             tr.append(Unlink())
         return self.var, tr
 
@@ -2033,7 +2029,7 @@ class Outputs:
     class Error(widget.OWWidget.Error):
         duplicate_var_name = widget.Msg("A variable name is duplicated.")
 
-    settings_version = 3
+    settings_version = 4
 
     _domain_change_hints = Setting({}, schema_only=True)
     _merge_dialog_settings = Setting({}, schema_only=True)
@@ -2324,8 +2320,7 @@ def state(i):
         state = [state(i) for i in range(model.rowCount())]
         input_vars = data.domain.variables + data.domain.metas
         if self.output_table_name in ("", data.name) \
-                and not any(requires_transform(var, trs)
-                            for var, (_, trs) in zip(input_vars, state)):
+                and all(tr is None or not tr for _, tr in state):
             self.Outputs.data.send(data)
             return
 
@@ -2476,6 +2471,13 @@ def migrate_settings(cls, settings, version):
             settings["_domain_change_hints"] = hints
             del settings["context_settings"]
 
+        if version < 4 and "_domain_change_hints" in settings:
+            settings["_domain_change_hints"] = {
+                (name, desc[:-1]): trs
+                for (name, desc), trs in settings["_domain_change_hints"].items()
+            }
+
+
 def enumerate_columns(
         table: Orange.data.Table
 ) -> Iterable[Tuple[int, str, Orange.data.Variable, Callable[[], ndarray]]]:
@@ -2649,15 +2651,14 @@ def abstract(var):
         (key, str(value))
         for key, value in var.attributes.items()
     ))
-    linked = var.compute_value is not None
     if isinstance(var, Orange.data.DiscreteVariable):
-        return Categorical(var.name, tuple(var.values), annotations, linked)
+        return Categorical(var.name, tuple(var.values), annotations)
     elif isinstance(var, Orange.data.TimeVariable):
-        return Time(var.name, annotations, linked)
+        return Time(var.name, annotations)
     elif isinstance(var, Orange.data.ContinuousVariable):
-        return Real(var.name, (var.number_of_decimals, 'f'), annotations, linked)
+        return Real(var.name, (var.number_of_decimals, 'f'), annotations)
     elif isinstance(var, Orange.data.StringVariable):
-        return String(var.name, annotations, linked)
+        return String(var.name, annotations)
     else:
         raise TypeError
 
@@ -2687,23 +2688,13 @@ def apply_transform(var, table, trs):
 
 
 def requires_unlink(var: Orange.data.Variable, trs: List[Transform]) -> bool:
-    # Variable is only unlinked if it has compute_value  or if it has other
-    # transformations (that might had added compute_value)
+    # Variable is only unlinked if it has compute_value or if it has other
+    # transformations (that might have added compute_value)
     return trs is not None \
            and any(isinstance(tr, Unlink) for tr in trs) \
            and (var.compute_value is not None or len(trs) > 1)
 
 
-def requires_transform(var: Orange.data.Variable, trs: List[Transform]) -> bool:
-    # Unlink is treated separately: Unlink is required only if the variable
-    # has compute_value. Hence tranform is required if it has any
-    # transformations other than Unlink, or if unlink is indeed required.
-    return trs is not None and (
-        not all(isinstance(tr, Unlink) for tr in trs)
-        or requires_unlink(var, trs)
-    )
-
-
 @singledispatch
 def apply_transform_var(var, trs):
     # type: (Orange.data.Variable, List[Transform]) -> Orange.data.Variable


Test patch:
diff --git a/Orange/widgets/data/tests/test_oweditdomain.py b/Orange/widgets/data/tests/test_oweditdomain.py
--- a/Orange/widgets/data/tests/test_oweditdomain.py
+++ b/Orange/widgets/data/tests/test_oweditdomain.py
@@ -48,26 +48,26 @@
 
 class TestReport(TestCase):
     def test_rename(self):
-        var = Real("X", (-1, ""), (), False)
+        var = Real("X", (-1, ""), ())
         tr = Rename("Y")
         val = report_transform(var, [tr])
         self.assertIn("X", val)
         self.assertIn("Y", val)
 
     def test_annotate(self):
-        var = Real("X", (-1, ""), (("a", "1"), ("b", "z")), False)
+        var = Real("X", (-1, ""), (("a", "1"), ("b", "z")))
         tr = Annotate((("a", "2"), ("j", "z")))
         r = report_transform(var, [tr])
         self.assertIn("a", r)
         self.assertIn("b", r)
 
     def test_unlinke(self):
-        var = Real("X", (-1, ""), (("a", "1"), ("b", "z")), True)
+        var = Real("X", (-1, ""), (("a", "1"), ("b", "z")))
         r = report_transform(var, [Unlink()])
         self.assertIn("unlinked", r)
 
     def test_categories_mapping(self):
-        var = Categorical("C", ("a", "b", "c"), (), False)
+        var = Categorical("C", ("a", "b", "c"), ())
         tr = CategoriesMapping(
             (("a", "aa"),
              ("b", None),
@@ -81,7 +81,7 @@ def test_categories_mapping(self):
         self.assertIn("
", r)
 
     def test_categorical_merge_mapping(self):
-        var = Categorical("C", ("a", "b1", "b2"), (), False)
+        var = Categorical("C", ("a", "b1", "b2"), ())
         tr = CategoriesMapping(
             (("a", "a"),
              ("b1", "b"),
@@ -92,7 +92,7 @@ def test_categorical_merge_mapping(self):
         self.assertIn('b', r)
 
     def test_reinterpret(self):
-        var = String("T", (), False)
+        var = String("T", ())
         for tr in (AsContinuous(), AsCategorical(), AsTime()):
             t = report_transform(var, [tr])
             self.assertIn("→ (", t)
@@ -274,7 +274,7 @@ def enter_text(widget, text):
         output = self.get_output(self.widget.Outputs.data)
         self.assertIsInstance(output, Table)
 
-    def test_unlink(self):
+    def test_unlink_inherited(self):
         var0, var1, var2 = [ContinuousVariable("x", compute_value=Mock()),
                             ContinuousVariable("y", compute_value=Mock()),
                             ContinuousVariable("z")]
@@ -286,7 +286,6 @@ def test_unlink(self):
         for i in range(3):
             self.widget.domain_view.setCurrentIndex(index(i))
             editor = self.widget.findChild(ContinuousVariableEditor)
-            self.assertIs(editor.unlink_var_cb.isEnabled(), i < 2)
             editor._set_unlink(i == 1)
 
         self.widget.commit()
@@ -300,6 +299,31 @@ def test_unlink(self):
         self.assertIsNone(out1.compute_value)
         self.assertIsNone(out2.compute_value)
 
+    def test_unlink_forward(self):
+        var0, var1, var2, var3 = [ContinuousVariable("x", compute_value=Mock()),
+                                  ContinuousVariable("y", compute_value=Mock()),
+                                  ContinuousVariable("z"),
+                                  ContinuousVariable("w")]
+        domain = Domain([var0, var1, var2, var3], None)
+        table = Table.from_numpy(domain, np.zeros((5, 4)), np.zeros((5, 0)))
+        self.send_signal(self.widget.Inputs.data, table)
+
+        index = self.widget.domain_view.model().index
+        for i in [0, 2, 3]:
+            self.widget.domain_view.setCurrentIndex(index(i))
+            editor = self.widget.findChild(ContinuousVariableEditor)
+            editor.name_edit.setText(f"v{i}")
+            editor.on_name_changed()
+            editor._set_unlink(i != 3)
+
+        self.widget.commit()
+        out = self.get_output(self.widget.Outputs.data)
+        out0, out1, out2, out3 = out.domain.variables
+        self.assertIsNone(out0.compute_value)
+        self.assertIsNotNone(out1.compute_value)
+        self.assertIsNone(out2.compute_value)
+        self.assertIsNotNone(out3.compute_value)
+
     def test_time_variable_preservation(self):
         """Test if time variables preserve format specific attributes"""
         table = Table(test_filename("datasets/cyber-security-breaches.tab"))
@@ -320,8 +344,7 @@ def test_restore(self):
         iris = self.iris
         viris = (
             "Categorical",
-            ("iris", ("Iris-setosa", "Iris-versicolor", "Iris-virginica"), (),
-             False)
+            ("iris", ("Iris-setosa", "Iris-versicolor", "Iris-virginica"), ())
         )
         w = self.widget
 
@@ -490,7 +513,7 @@ def data(fr, to):
         self.assertEqual([var.name for var in outp.domain.attributes],
                          ["v0", "v1", "v2", "xv3", "v4"])
 
-    def test_migrate_settings_hints_2_to_3(self):
+    def test_migrate_settings_hints_2_to_4(self):
         settings = {
             '__version__': 2,
             'context_settings':
@@ -520,11 +543,11 @@ def test_migrate_settings_hints_2_to_3(self):
                       '__version__': 2}),
                 ]}
         migrated_hints = {
-            ('Categorical', ('b', ('mir4', 'mir1', 'mir2'), (), False)):
+            ('Categorical', ('b', ('mir4', 'mir1', 'mir2'), ())):
                 [('Rename', ('disease bmir',))],
-            ('Categorical', ('c', ('mir4', 'mir1', 'mir2'), (), False)):
+            ('Categorical', ('c', ('mir4', 'mir1', 'mir2'), ())):
                 [('Rename', ('disease mirs',))],
-            ('Categorical', ('a', ('mir1', 'mir4', 'mir2'), (), False)):
+            ('Categorical', ('a', ('mir1', 'mir4', 'mir2'), ())):
                  [('Rename', ('disease mir',))],
         }
         widget = self.create_widget(OWEditDomain, stored_settings=settings)
@@ -533,7 +556,7 @@ def test_migrate_settings_hints_2_to_3(self):
         self.assertEqual(list(widget._domain_change_hints), list(migrated_hints))
         self.assertEqual(widget.output_table_name, "far")
 
-    def test_migrate_settings_2_to_3_realworld(self):
+    def test_migrate_settings_2_to_4_realworld(self):
         settings = {
             'controlAreaVisible': True,
             '__version__': 2,
@@ -558,12 +581,12 @@ def test_migrate_settings_2_to_3_realworld(self):
         widget = self.create_widget(OWEditDomain, stored_settings=settings)
         self.assertEqual(
             widget._domain_change_hints,
-            {('Real', ('sepal length', (1, 'f'), (), False)):
+            {('Real', ('sepal length', (1, 'f'), ())):
                  [('AsString', ())],
-             ('Real', ('sepal width', (1, 'f'), (), False)):
+             ('Real', ('sepal width', (1, 'f'), ())):
                  [('AsTime', ()),
                   ('StrpTime', ('Detect automatically', None, 1, 1))],
-             ('Real', ('petal width', (1, 'f'), (), False)):
+             ('Real', ('petal width', (1, 'f'), ())):
                  [('Annotate', ((('a', 'b'),),))]}
         )
 
@@ -591,13 +614,39 @@ def test_migrate_settings_name_2_to_3(self):
         widget = self.create_widget(OWEditDomain, stored_settings=settings)
         self.assertEqual(widget.output_table_name, "far")
 
+    def test_migrate_settings_3_to_4(self):
+        settings = {
+            '_domain_change_hints': {
+                ('Real', ('age', (0, 'f'), (), False)):
+                    [('Unlink', ())],
+                ('Categorical', ('gender', ('female', 'male'), (), False)):
+                    [('CategoriesMapping', ([('female', 'woman'), ('male', 'man')],))],
+                ('Categorical', ('chest pain', ('asymptomatic', 'atypical ang',
+                                                'non-anginal', 'typical ang'), (), False)):
+                    [('AsString', ())]},
+            '_merge_dialog_settings': {},
+            'controlAreaVisible': True}
+
+        widget = self.create_widget(OWEditDomain, stored_settings=settings)
+        self.assertEqual(
+            widget._domain_change_hints,
+            {('Real', ('age', (0, 'f'), ())):
+                    [('Unlink', ())],
+             ('Categorical', ('gender', ('female', 'male'), ())):
+                    [('CategoriesMapping',
+                      ([('female', 'woman'), ('male', 'man')],))],
+             ('Categorical', ('chest pain', ('asymptomatic', 'atypical ang',
+                                             'non-anginal', 'typical ang'), ())):
+                    [('AsString', ())]}
+        )
+
 
 class TestEditors(GuiTest):
     def test_variable_editor(self):
         w = VariableEditor()
         self.assertEqual(w.get_data(), (None, []))
 
-        v = String("S", (("A", "1"), ("B", "b")), False)
+        v = String("S", (("A", "1"), ("B", "b")))
         w.set_data(v, [])
 
         self.assertEqual(w.name_edit.text(), v.name)
@@ -622,7 +671,7 @@ def test_continuous_editor(self):
         w = ContinuousVariableEditor()
         self.assertEqual(w.get_data(), (None, []))
 
-        v = Real("X", (-1, ""), (("A", "1"), ("B", "b")), False)
+        v = Real("X", (-1, ""), (("A", "1"), ("B", "b")))
         w.set_data(v, [])
 
         self.assertEqual(w.name_edit.text(), v.name)
@@ -637,7 +686,7 @@ def test_discrete_editor(self):
         w = DiscreteVariableEditor()
         self.assertEqual(w.get_data(), (None, []))
 
-        v = Categorical("C", ("a", "b", "c"), (("A", "1"), ("B", "b")), False)
+        v = Categorical("C", ("a", "b", "c"), (("A", "1"), ("B", "b")))
         values = [0, 0, 0, 1, 1, 2]
         w.set_data_categorical(v, values)
 
@@ -689,7 +738,7 @@ def test_discrete_editor(self):
     def test_discrete_editor_add_remove_action(self):
         w = DiscreteVariableEditor()
         v = Categorical("C", ("a", "b", "c"),
-                        (("A", "1"), ("B", "b")), False)
+                        (("A", "1"), ("B", "b")))
         values = [0, 0, 0, 1, 1, 2]
         w.set_data_categorical(v, values)
         action_add = w.add_new_item
@@ -737,7 +786,7 @@ def test_discrete_editor_merge_action(self):
         """
         w = DiscreteVariableEditor()
         v = Categorical("C", ("a", "b", "c"),
-                        (("A", "1"), ("B", "b")), False)
+                        (("A", "1"), ("B", "b")))
 
         w.set_data_categorical(
             v, [0, 0, 0, 1, 1, 2],
@@ -767,7 +816,7 @@ def test_discrete_editor_merge_action(self):
     def test_discrete_editor_rename_selected_items_action(self):
         w = DiscreteVariableEditor()
         v = Categorical("C", ("a", "b", "c"),
-                        (("A", "1"), ("B", "b")), False)
+                        (("A", "1"), ("B", "b")))
         w.set_data_categorical(v, [])
         action = w.rename_selected_items
         view = w.values_edit
@@ -794,7 +843,7 @@ def test_discrete_editor_rename_selected_items_action(self):
     def test_discrete_editor_context_menu(self):
         w = DiscreteVariableEditor()
         v = Categorical("C", ("a", "b", "c"),
-                        (("A", "1"), ("B", "b")), False)
+                        (("A", "1"), ("B", "b")))
         w.set_data_categorical(v, [])
         view = w.values_edit
         model = view.model()
@@ -812,7 +861,7 @@ def test_time_editor(self):
         w = TimeVariableEditor()
         self.assertEqual(w.get_data(), (None, []))
 
-        v = Time("T", (("A", "1"), ("B", "b")), False)
+        v = Time("T", (("A", "1"), ("B", "b")))
         w.set_data(v,)
 
         self.assertEqual(w.name_edit.text(), v.name)
@@ -825,19 +874,19 @@ def test_time_editor(self):
 
     DataVectors = [
         CategoricalVector(
-            Categorical("A", ("a", "aa"), (), False), lambda:
+            Categorical("A", ("a", "aa"), ()), lambda:
                 MArray([0, 1, 2], mask=[False, False, True])
         ),
         RealVector(
-            Real("B", (6, "f"), (), False), lambda:
+            Real("B", (6, "f"), ()), lambda:
                 MArray([0.1, 0.2, 0.3], mask=[True, False, True])
         ),
         TimeVector(
-            Time("T", (), False), lambda:
+            Time("T", ()), lambda:
                 MArray([0, 100, 200], dtype="M8[us]", mask=[True, False, True])
         ),
         StringVector(
-            String("S", (), False), lambda:
+            String("S", ()), lambda:
                 MArray(["0", "1", "2"], dtype=object, mask=[True, False, True])
         ),
     ]
@@ -1050,18 +1099,15 @@ def test_unlink(self):
         cbox = w.unlink_var_cb
         self.assertEqual(w.get_data(), (None, []))
 
-        v = Real("X", (-1, ""), (("A", "1"), ("B", "b")), False)
+        v = Real("X", (-1, ""), (("A", "1"), ("B", "b")))
         w.set_data(v, [])
-        self.assertFalse(cbox.isEnabled())
 
-        v = Real("X", (-1, ""), (("A", "1"), ("B", "b")), True)
+        v = Real("X", (-1, ""), (("A", "1"), ("B", "b")))
         w.set_data(v, [Unlink()])
-        self.assertTrue(cbox.isEnabled())
         self.assertTrue(cbox.isChecked())
 
-        v = Real("X", (-1, ""), (("A", "1"), ("B", "b")), True)
+        v = Real("X", (-1, ""), (("A", "1"), ("B", "b")))
         w.set_data(v, [])
-        self.assertTrue(cbox.isEnabled())
         self.assertFalse(cbox.isChecked())
 
         cbox.setChecked(True)
@@ -1096,8 +1142,8 @@ def set_data(row, data, role):
             model.setData(model.index(row), data, role)
 
         model[:] = [
-            RealVector(Real("A", (3, "g"), (), False), lambda: MArray([])),
-            RealVector(Real("B", (3, "g"), (), False), lambda: MArray([])),
+            RealVector(Real("A", (3, "g"), ()), lambda: MArray([])),
+            RealVector(Real("B", (3, "g"), ()), lambda: MArray([])),
         ]
         self.assertEqual(data(0, Qt.DisplayRole), "A")
         self.assertEqual(data(1, Qt.DisplayRole), "B")
@@ -1123,7 +1169,7 @@ def get_style_option(row: int) -> QStyleOptionViewItem:
             delegate.initStyleOption(opt, model.index(row))
             return opt
 
-        set_item(0, {Qt.EditRole: Categorical("a", (), (), False)})
+        set_item(0, {Qt.EditRole: Categorical("a", (), ())})
         delegate = VariableEditDelegate()
         opt = get_style_option(0)
         self.assertEqual(opt.text, "a")
@@ -1138,7 +1184,7 @@ def get_style_option(row: int) -> QStyleOptionViewItem:
         self.assertIn("reinterpreted", opt.text)
         self.assertTrue(opt.font.italic())
         set_item(1, {
-            Qt.EditRole: String("b", (), False),
+            Qt.EditRole: String("b", ()),
             TransformRole: [Rename("a")]
         })
         opt = get_style_option(1)
@@ -1555,7 +1601,7 @@ def test_equality(self):
 class TestGroupLessFrequentItemsDialog(GuiTest):
     def setUp(self) -> None:
         self.v = Categorical("C", ("a", "b", "c"),
-                        (("A", "1"), ("B", "b")), False)
+                        (("A", "1"), ("B", "b")))
         self.data = [0, 0, 0, 1, 1, 2]
 
     def test_dialog_open(self):