Repository: biolab/orange3

Issue:
CreateClass should create variables through make
<!--
This is an issue template. Please fill in the relevant details in the
sections below.
-->

##### Orange version
<!-- From menu _Help→About→Version_ or code `Orange.version.full_version` -->
3.5.0.dev0+cf2da75

##### Expected behavior
CreateClass uses `make` to create class variables so that one is able to create the same class variable as present in train data.


##### Actual behavior
CreateClass doesn't use `make`. Hence created class outputs a different variable.


##### Steps to reproduce the behavior
Load two image data sets and pass them through embedding. Then try to create the same target variables as present in the train data and use it in `Test & Score` as test data.


##### Additional info (worksheets, data, screenshots, ...)

![screen shot 2017-05-29 at 11 24 20](https://cloud.githubusercontent.com/assets/713026/26544246/65262296-4461-11e7-8dc3-d4e6b893a8de.png)




Discussion:
It reports the same error if you use _File_ widget instead of _Import Images_ and _Image Embedding_.
I've got the same issue with Neural Net. I tired using categories from a file marked as a target variable and via creating classes, but it still says "Test and train data sets have different target variables".
In fact, even choosing the same file on both ends doesn't help. 
On the other hand, we have the opposite idea in #2943, where we argue against variable reuse. 🤷‍♂️ 
We are leaning towards removing (the need for) `make`. Hence this should probably wait until that decision.

Resolving patch:
diff --git a/Orange/widgets/data/owcreateclass.py b/Orange/widgets/data/owcreateclass.py
--- a/Orange/widgets/data/owcreateclass.py
+++ b/Orange/widgets/data/owcreateclass.py
@@ -148,6 +148,11 @@ class Outputs:
     TRANSFORMERS = {StringVariable: ValueFromStringSubstring,
                     DiscreteVariable: ValueFromDiscreteSubstring}
 
+    # Cached variables are used so that two instances of the widget with the
+    # same settings will create the same variable. The usual `make` wouldn't
+    # work here because variables with `compute_value` are not reused.
+    cached_variables = {}
+
     class Warning(widget.OWWidget.Warning):
         no_nonnumeric_vars = Msg("Data contains only numeric variables.")
 
@@ -465,25 +470,36 @@ def apply(self):
         if not self.class_name or self.class_name in domain:
             self.Outputs.data.send(None)
             return
+        new_class = self._create_variable()
+        new_domain = Domain(
+            domain.attributes, new_class, domain.metas + domain.class_vars)
+        new_data = self.data.transform(new_domain)
+        self.Outputs.data.send(new_data)
+
+    def _create_variable(self):
         rules = self.active_rules
         # Transposition + stripping
         valid_rules = [label or pattern or n_matches
                        for (label, pattern), n_matches in
                        zip(rules, self.match_counts)]
-        patterns = [pattern
-                    for (_, pattern), valid in zip(rules, valid_rules)
-                    if valid]
-        names = [name for name, valid in zip(self.class_labels(), valid_rules)
-                 if valid]
+        patterns = tuple(
+            pattern for (_, pattern), valid in zip(rules, valid_rules) if valid)
+        names = tuple(
+            name for name, valid in zip(self.class_labels(), valid_rules)
+            if valid)
         transformer = self.TRANSFORMERS[type(self.attribute)]
+
+        var_key = (self.attribute, self.class_name, names,
+                   patterns, self.case_sensitive, self.match_beginning)
+        if var_key in self.cached_variables:
+            return self.cached_variables[var_key]
+
         compute_value = transformer(
             self.attribute, patterns, self.case_sensitive, self.match_beginning)
-        new_class = DiscreteVariable(
+        new_var = DiscreteVariable(
             self.class_name, names, compute_value=compute_value)
-        new_domain = Domain(
-            domain.attributes, new_class, domain.metas + domain.class_vars)
-        new_data = self.data.transform(new_domain)
-        self.Outputs.data.send(new_data)
+        self.cached_variables[var_key] = new_var
+        return new_var
 
     def send_report(self):
         # Pylint gives false positives: these functions are always called from


Test patch:
diff --git a/Orange/widgets/data/tests/test_owcreateclass.py b/Orange/widgets/data/tests/test_owcreateclass.py
--- a/Orange/widgets/data/tests/test_owcreateclass.py
+++ b/Orange/widgets/data/tests/test_owcreateclass.py
@@ -135,8 +135,10 @@ def _test_default_rules(self):
             self.assertEqual(label.placeholderText(), f"C{i}")
             self.assertEqual(pattern.text(), "")
 
-    def _set_attr(self, attr):
-        attr_combo = self.widget.controls.attribute
+    def _set_attr(self, attr, widget=None):
+        if widget is None:
+            widget = self.widget
+        attr_combo = widget.controls.attribute
         idx = attr_combo.model().indexOf(attr)
         attr_combo.setCurrentIndex(idx)
         attr_combo.activated.emit(idx)
@@ -385,6 +387,24 @@ def assertError(class_name, class_name_empty, class_name_duplicated, is_out):
         widget.apply()
         self.assertEqual(widget.class_name, "class")
 
+    def test_same_class(self):
+        widget1 = self.create_widget(OWCreateClass)
+        self.send_signal(widget1.Inputs.data, self.zoo, widget=widget1)
+        self._set_attr(self.zoo.domain.metas[0], widget=widget1)
+        widget1.line_edits[0][1].setText("a")
+        widget1.apply()
+
+        widget2 = self.create_widget(OWCreateClass)
+        self.send_signal(widget2.Inputs.data, self.zoo, widget=widget2)
+        self._set_attr(self.zoo.domain.metas[0], widget=widget2)
+        widget2.line_edits[0][1].setText("a")
+        widget2.apply()
+
+        self.assertIs(
+            self.get_output(widget1.Outputs.data, widget=widget1).domain.class_var,
+            self.get_output(widget2.Outputs.data, widget=widget2).domain.class_var
+        )
+
 
 if __name__ == "__main__":
     unittest.main()