Repository: biolab/orange3

Issue:
Opening report on Predictions widget crashes
**Describe the bug**
Opening a report on Predictions widget results in a crash.

**To Reproduce**
File (Iris) -> Random Forest -> Predictions -> Click Report icon

**Orange version:**
Master

**Expected behavior**
Open the report without the crash.



Discussion:

Resolving patch:
diff --git a/Orange/widgets/evaluate/owpredictions.py b/Orange/widgets/evaluate/owpredictions.py
--- a/Orange/widgets/evaluate/owpredictions.py
+++ b/Orange/widgets/evaluate/owpredictions.py
@@ -11,6 +11,8 @@
     Qt, QSize, QRect, QRectF, QPoint, QLocale,
     QModelIndex, QAbstractTableModel, QSortFilterProxyModel, pyqtSignal, QTimer)
 
+from orangewidget.report import plural
+
 import Orange
 from Orange.evaluation import Results
 from Orange.base import Model
@@ -293,19 +295,29 @@ def _set_errors(self):
             self.Warning.wrong_targets.clear()
 
     def _update_info(self):
-        n_predictors = len(self.predictors)
-        if not self.data and not n_predictors:
+        if not self.data and not self.predictors:
             self.info.set_input_summary(self.info.NoInput)
             return
 
-        n_valid = len(self._non_errored_predictors())
         summary = str(len(self.data)) if self.data else "0"
-        details = f"{len(self.data)} instances" if self.data else "No data"
-        details += f"\n{n_predictors} models" if n_predictors else "No models"
-        if n_valid != n_predictors:
-            details += f" ({n_predictors - n_valid} failed)"
+        details = self._get_details()
         self.info.set_input_summary(summary, details)
 
+    def _get_details(self):
+        n_predictors = len(self.predictors)
+        if self.data:
+            details = plural("{number} instance{s}", len(self.data))
+        else:
+            details = "No data"
+        if n_predictors:
+            n_valid = len(self._non_errored_predictors())
+            details += plural("\n{number} model{s}", n_predictors)
+            if n_valid != n_predictors:
+                details += plural(" ({number} failed)", n_predictors - n_valid)
+        else:
+            details += "\nNo models"
+        return details
+
     def _invalidate_predictions(self):
         for inputid, pred in list(self.predictors.items()):
             self.predictors[inputid] = pred._replace(results=None)
@@ -560,10 +572,12 @@ def _add_regression_out_columns(slot, newmetas, newcolumns):
     def send_report(self):
         def merge_data_with_predictions():
             data_model = self.dataview.model()
-            predictions_model = self.predictionsview.model()
+            predictions_view = self.predictionsview
+            predictions_model = predictions_view.model()
 
             # use ItemDelegate to style prediction values
-            style = lambda x: self.predictionsview.itemDelegate().displayText(x, QLocale())
+            delegates = [predictions_view.itemDelegateForColumn(i)
+                         for i in range(predictions_model.columnCount())]
 
             # iterate only over visible columns of data's QTableView
             iter_data_cols = list(filter(lambda x: not self.dataview.isColumnHidden(x),
@@ -579,13 +593,15 @@ def merge_data_with_predictions():
             # print data & predictions
             for i in range(data_model.rowCount()):
                 yield [data_model.headerData(i, Qt.Vertical, Qt.DisplayRole)] + \
-                      [style(predictions_model.data(predictions_model.index(i, j)))
-                       for j in range(predictions_model.columnCount())] + \
+                      [delegate.displayText(
+                          predictions_model.data(predictions_model.index(i, j)),
+                          QLocale())
+                       for j, delegate in enumerate(delegates)] + \
                       [data_model.data(data_model.index(i, j))
                        for j in iter_data_cols]
 
         if self.data:
-            text = self.infolabel.text().replace('\n', '<br>')
+            text = self._get_details().replace('\n', '<br>')
             if self.selected_classes:
                 text += '<br>Showing probabilities for: '
                 text += ', '. join([self.class_values[i]
@@ -594,6 +610,8 @@ def merge_data_with_predictions():
             self.report_table("Data & Predictions", merge_data_with_predictions(),
                               header_rows=1, header_columns=1)
 
+            self.report_table("Scores", self.score_table.view)
+
     def resizeEvent(self, event):
         super().resizeEvent(event)
         self._update_splitter()


Test patch:
diff --git a/Orange/widgets/evaluate/tests/test_owpredictions.py b/Orange/widgets/evaluate/tests/test_owpredictions.py
--- a/Orange/widgets/evaluate/tests/test_owpredictions.py
+++ b/Orange/widgets/evaluate/tests/test_owpredictions.py
@@ -412,6 +412,8 @@ def test_colors_continuous(self):
         colors = self.widget._get_colors()
         self.assertEqual(3, len(colors))
 
+        self.widget.send_report()  # just a quick check that it doesn't crash
+
 
 if __name__ == "__main__":
     import unittest