Repository: biolab/orange3

Issue:
ValueError:  Exhaustive binarization does not handle attributes with more than 16 values
https://sentry.io/biolab/orange3/issues/196743565/

```
ValueError:  Exhaustive binarization does not handle attributes with more than 16 values
(1 additional frame(s) were not displayed)
...
  File "/Users/anze/dev/orange3/Orange/widgets/utils/sql.py", line 28, in new_f

    return f(widget, data, *args, **kwargs)
  File "/Users/anze/dev/orange3/Orange/widgets/utils/owlearnerwidget.py", line 167, in set_data

    self.update_model()
  File "/Users/anze/dev/orange3/Orange/widgets/utils/owlearnerwidget.py", line 187, in update_model

    self.model = self.learner(self.data)
  File "/Users/anze/dev/orange3/Orange/base.py", line 118, in __call__

    model = self.fit_storage(data)
  File "/Users/anze/dev/orange3/Orange/classification/tree.py", line 202, in fit_storage

    format(self.MAX_BINARIZATION))
```


Discussion:

Resolving patch:
diff --git a/Orange/widgets/classify/owclassificationtree.py b/Orange/widgets/classify/owclassificationtree.py
--- a/Orange/widgets/classify/owclassificationtree.py
+++ b/Orange/widgets/classify/owclassificationtree.py
@@ -2,8 +2,10 @@
 
 from Orange.data import Table
 from Orange.modelling.tree import TreeLearner
+from Orange.classification.tree import TreeLearner as ClassificationTreeLearner
 from Orange.widgets.model.owtree import OWTreeLearner
 from Orange.widgets.utils.owlearnerwidget import OWBaseLearner
+from Orange.widgets.widget import Msg
 
 
 class OWTreeLearner(OWTreeLearner):
@@ -21,6 +23,29 @@ class OWTreeLearner(OWTreeLearner):
           "limit_majority", "sufficient_majority", 51, 100),) + \
         OWTreeLearner.spin_boxes[-1:]
 
+    class Error(OWTreeLearner.Error):
+        cannot_binarize = Msg("Binarization cannot handle '{}'\n"
+                              "because it has {} values. "
+                              "Binarization can handle up to {}.\n"
+                              "Disable 'Induce binary tree' to proceed.")
+
+    def check_data(self):
+        self.Error.cannot_binarize.clear()
+        if not super().check_data():
+            return False
+        if not self.binary_trees:
+            return True
+        max_values, max_attr = max(
+            ((len(attr.values), attr)
+             for attr in self.data.domain.attributes if attr.is_discrete),
+            default=(0, None))
+        MAX_BINARIZATION = ClassificationTreeLearner.MAX_BINARIZATION
+        if max_values > MAX_BINARIZATION:
+            self.Error.cannot_binarize(
+                max_attr.name, max_values, MAX_BINARIZATION)
+            return False
+        return True
+
     def learner_kwargs(self):
         opts = super().learner_kwargs()
         opts['sufficient_majority'] = \
diff --git a/Orange/widgets/utils/messages.py b/Orange/widgets/utils/messages.py
--- a/Orange/widgets/utils/messages.py
+++ b/Orange/widgets/utils/messages.py
@@ -335,7 +335,7 @@ def update_message_state(self):
         elif self.message_bar is not None:
             font_size = self.message_bar.fontInfo().pixelSize()
             group = messages[0].group
-            text = str(messages[0]) if len(messages) == 1 \
+            text = str(messages[0]).split("\n")[0] if len(messages) == 1 \
                 else "{} problems during execution".format(len(messages))
             # TODO: fix tooltip background color - it is not white
             tooltip = ''.join(
@@ -346,7 +346,8 @@ def update_message_state(self):
                 &nbsp;&nbsp;&nbsp;</nobr>
                 <span style="font-size:9pt"><br></span>
                 </p>'''.
-                format(msg.group.bar_background, font_size, str(msg))
+                format(msg.group.bar_background, font_size,
+                       str(msg).replace("\n", "<br/>&nbsp;&nbsp;&nbsp; "))
                 for msg in messages)
             self._set_message_bar(group, text, tooltip)
 


Test patch:
diff --git a/Orange/widgets/classify/tests/test_owclassificationtree.py b/Orange/widgets/classify/tests/test_owclassificationtree.py
--- a/Orange/widgets/classify/tests/test_owclassificationtree.py
+++ b/Orange/widgets/classify/tests/test_owclassificationtree.py
@@ -1,5 +1,7 @@
 # Test methods with long descriptive names can omit docstrings
 # pylint: disable=missing-docstring
+from Orange.data import Table, Domain, DiscreteVariable
+from Orange.classification.tree import TreeLearner as ClassificationTreeLearner
 from Orange.base import Model
 from Orange.widgets.classify.owclassificationtree import OWTreeLearner
 from Orange.widgets.tests.base import (WidgetTest, DefaultParameterMapping,
@@ -7,6 +9,11 @@
 
 
 class TestOWClassificationTree(WidgetTest, WidgetLearnerTestMixin):
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        cls.iris = Table("iris")
+
     def setUp(self):
         self.widget = self.create_widget(
             OWTreeLearner, stored_settings={"auto_apply": False})
@@ -34,3 +41,50 @@ def test_parameters_unchecked(self):
                            for par, val in zip(self.parameters, (None, 2, 1))]
         self.test_parameters()
 
+    def test_cannot_binarize(self):
+        widget = self.widget
+        error_shown = widget.Error.cannot_binarize.is_shown
+        self.assertFalse(error_shown())
+        self.send_signal("Data", self.iris)
+
+        # The widget outputs ClassificationTreeLearner.
+        # If not, below tests may not make sense
+        learner = self.get_output("Learner")
+        dlearner = learner.get_learner(learner.CLASSIFICATION)
+        self.assertTrue(dlearner, ClassificationTreeLearner)
+
+        # No error on Iris
+        max_binarization = dlearner.MAX_BINARIZATION
+        self.assertFalse(error_shown())
+
+        # Error when too many values
+        domain = Domain([
+            DiscreteVariable(
+                values=[str(x) for x in range(max_binarization + 1)])],
+            DiscreteVariable(values="01"))
+        self.send_signal("Data", Table(domain, [[0, 0], [1, 1]]))
+        self.assertTrue(error_shown())
+        # No more error on Iris
+        self.send_signal("Data", self.iris)
+        self.assertFalse(error_shown())
+
+        # Checking and unchecking binarization works
+        widget.controls.binary_trees.click()
+        self.assertFalse(widget.binary_trees)
+        widget.unconditional_apply()
+        self.send_signal("Data", Table(domain, [[0, 0], [1, 1]]))
+        self.assertFalse(error_shown())
+        widget.controls.binary_trees.click()
+        widget.unconditional_apply()
+        self.assertTrue(error_shown())
+        widget.controls.binary_trees.click()
+        widget.unconditional_apply()
+        self.assertFalse(error_shown())
+
+        # If something is wrong with the data, no error appears
+        domain = Domain([
+            DiscreteVariable(
+                values=[str(x) for x in range(max_binarization + 1)])],
+            DiscreteVariable(values="01"))
+        self.send_signal("Data", Table(domain))
+        self.assertFalse(error_shown())