Repository: spyder-ide/spyder

Issue:
Improved dependency dialog
## Description of your problem

The current dialog is not quite clear:

![grafik](https://user-images.githubusercontent.com/2836374/32403618-f36b201a-c13e-11e7-9a23-8c5762735896.png)

- it has a lengthy textual disclaimer.
- at first I got confused, because "required" does refer to the version number, whereas I thought it was used in the sense of mandatory.
- no obvious distinction bewteen mandatory and optional dependencies: I cannot see if I'm missing some optional packages that might add additional features.

**Proposal:**

I propose to exchange the table for a tree view:

~~~
+ Mandatory Packages
  - IPython
  - ...
+ Optional Packages
  - numpy
  - ...
~~~

The columns Required, Installed and Provided features will remain.

The "(OK)" text from Installed is removed. Instead, there will be an icon at the front indicating if there is an issue.

If there is interest, I can work on a pull request. Is this something for 3.x or for 4.x?


Discussion:
Hey Tim, sounds good to me! We were not aware the dialog is not clear as it is, so I'm happy to see improvements to it, specially because Spyder 4 is going to bring at least 3 more new dependencies (docrepr, spykernel and pyls).

> If there is interest, I can work on a pull request. Is this something for 3.x or for 4.x?

Sure, please go ahead! Spyder 3 is now our maintenance branch, so it only receives bug fixes and very minor features. So please make this work against master.

Resolving patch:
diff --git a/spyder/utils/icon_manager.py b/spyder/utils/icon_manager.py
--- a/spyder/utils/icon_manager.py
+++ b/spyder/utils/icon_manager.py
@@ -217,6 +217,9 @@
     'folding.arrow_right_on':  [('fa.caret-right',), {}],
     'folding.arrow_down_off':  [('fa.caret-down',), {'color': 'gray'}],
     'folding.arrow_down_on':   [('fa.caret-down',), {}],
+    'dependency_ok':           [('fa.check',), {}],
+    'dependency_warning':      [('fa.warning',), {'color': 'orange'}],
+    'dependency_error':        [('fa.warning',), {'color': 'darkred'}],
 }
 
 
diff --git a/spyder/widgets/dependencies.py b/spyder/widgets/dependencies.py
--- a/spyder/widgets/dependencies.py
+++ b/spyder/widgets/dependencies.py
@@ -10,12 +10,11 @@
 import sys
 
 # Third party imports
-from qtpy.compat import to_qvariant
-from qtpy.QtCore import Qt, QModelIndex, QAbstractTableModel
+from qtpy.QtCore import Qt
 from qtpy.QtGui import QColor
 from qtpy.QtWidgets import (QApplication, QDialog, QDialogButtonBox,
-                            QHBoxLayout, QItemDelegate, QLabel, QPushButton,
-                            QTableView, QVBoxLayout)
+                            QHBoxLayout, QVBoxLayout, QLabel, QPushButton,
+                            QTreeWidget, QTreeWidgetItem)
 
 # Local imports
 from spyder import __version__
@@ -23,109 +22,44 @@
 from spyder.utils import icon_manager as ima
 
 
-class DependenciesTableModel(QAbstractTableModel):
-    def __init__(self, parent, dependencies):
-        QAbstractTableModel.__init__(self, parent)
-        self.dependencies = None
-        self.set_data(dependencies)
-    
-    def set_data(self, dependencies):
-        """Set model data"""
-        self.dependencies = dependencies
-        self.reset()   
-    
-    def rowCount(self, qindex=QModelIndex()):
-        """Array row number"""
-        return len(self.dependencies)
-    
-    def columnCount(self, qindex=QModelIndex()):
-        """Array column count"""
-        return 4
-
-    def sort(self, column, order=Qt.DescendingOrder):
-        """Overriding sort method"""
-        if column == 0:
-            self.dependencies.sort(key=lambda dep: getattr(dep, 'modname'))
-        elif column == 1:
-            pass
-        elif column == 2:
-            pass
-        elif column == 3:
-            pass
-        self.reset()
-
-    def headerData(self, section, orientation, role=Qt.DisplayRole):
-        """Overriding method headerData"""
-        if role != Qt.DisplayRole:
-            return to_qvariant()
-        i_column = int(section)
-        if orientation == Qt.Horizontal:
-            headers = (_("Module"), _(" Required "),
-                       _(" Installed "), _("Provided features"))
-            return to_qvariant( headers[i_column] )
-        else:
-            return to_qvariant()
-    
-    def get_value(self, index):
-        """Return current value"""
-        dep = self.dependencies[index.row()]
-        return (dep.modname, dep.required_version,
-                dep.get_installed_version(), dep.features)[index.column()]
-    
-    def data(self, index, role=Qt.DisplayRole):
-        """Return data at table index"""
-        if not index.isValid():
-            return to_qvariant()
-        dep = self.dependencies[index.row()]
-        if role == Qt.DisplayRole:
-            if index.column() == 0:
-                value = self.get_value(index)
-                return to_qvariant(value)
+class DependenciesTreeWidget(QTreeWidget):
+
+    def update_dependencies(self, dependencies):
+        self.clear()
+        headers = (_("Module"), _(" Required "),
+                   _(" Installed "), _("Provided features"))
+        self.setHeaderLabels(headers)
+        mandatory_item = QTreeWidgetItem(["Mandatory"])
+        font = mandatory_item.font(0)
+        font.setBold(True)
+        mandatory_item.setFont(0, font)
+        optional_item = QTreeWidgetItem(["Optional"])
+        optional_item.setFont(0, font)
+        self.addTopLevelItems([mandatory_item, optional_item])
+
+        for dependency in dependencies:
+            item = QTreeWidgetItem([dependency.modname,
+                                    dependency.required_version,
+                                    dependency.installed_version,
+                                    dependency.features])
+            if dependency.check():
+                item.setIcon(0, ima.icon('dependency_ok'))
+            elif dependency.optional:
+                item.setIcon(0, ima.icon('dependency_warning'))
+                item.setForeground(2, QColor('#ff6a00'))
             else:
-                value = self.get_value(index)
-                return to_qvariant(value)
-        elif role == Qt.TextAlignmentRole:
-            return to_qvariant(int(Qt.AlignLeft|Qt.AlignVCenter))
-        elif role == Qt.BackgroundColorRole:
-            from spyder.dependencies import Dependency
-            status = dep.get_status()
-            if status == Dependency.NOK:
-                color = QColor(Qt.red)
-                color.setAlphaF(.25)
-                return to_qvariant(color)
-
-    def reset(self):
-        self.beginResetModel()
-        self.endResetModel()
-
-
-class DependenciesDelegate(QItemDelegate):
-    def __init__(self, parent=None):
-        QItemDelegate.__init__(self, parent)
-
-
-class DependenciesTableView(QTableView):
-    def __init__(self, parent, data):
-        QTableView.__init__(self, parent)
-        self.model = DependenciesTableModel(self, data)
-        self.setModel(self.model)
-        self.delegate = DependenciesDelegate(self)
-        self.setItemDelegate(self.delegate)
-        self.setup_table()
-        
-    def setup_table(self):
-        """Setup table"""
-        self.horizontalHeader().setStretchLastSection(True)
-        self.adjust_columns()
-        self.columnAt(0)
-        # Sorting columns
-        self.setSortingEnabled(False)
-        self.sortByColumn(0, Qt.DescendingOrder)
-    
-    def adjust_columns(self):
-        """Resize three first columns to contents"""
-        for col in range(3):
-            self.resizeColumnToContents(col)    
+                item.setIcon(0, ima.icon('dependency_error'))
+                item.setForeground(2, QColor(Qt.darkRed))
+            if dependency.optional:
+                optional_item.addChild(item)
+            else:
+                mandatory_item.addChild(item)
+        self.expandAll()
+
+    def resize_columns_to_contents(self):
+        for col in range(self.columnCount()):
+            self.resizeColumnToContents(col)
+
 
 class DependenciesDialog(QDialog):
     def __init__(self, parent):
@@ -135,24 +69,13 @@ def __init__(self, parent):
         self.setWindowIcon(ima.icon('tooloptions'))
         self.setModal(True)
 
-        self.view = DependenciesTableView(self, [])
-
-        opt_mods = ['NumPy', 'Matplotlib', 'Pandas', 'SymPy', 'Cython']
-        self.label = QLabel(_("Spyder depends on several Python modules to "
-                              "provide the right functionality for all its "
-                              "panes. The table below shows the required "
-                              "and installed versions (if any) of all of "
-                              "them.<br><br>"
-                              "<b>Note</b>: You can safely use Spyder "
-                              "without the following modules installed: "
-                              "<b>%s</b> and <b>%s</b>.<br><br>"
-                              "Please also note that new "
-                              "dependencies or changed ones will be correctly "
-                              "detected only after Spyder is restarted.")
-                              % (', '.join(opt_mods[:-1]), opt_mods[-1]))
-        self.label.setWordWrap(True)
-        self.label.setAlignment(Qt.AlignJustify)
-        self.label.setContentsMargins(5, 8, 12, 10)
+        self.treewidget = DependenciesTreeWidget(self)
+
+        self.label = QLabel(_("Optional modules are not required to run "
+                              "Spyder but enhance its functions."))
+        self.label2 = QLabel(_("<b>Note:</b> New dependencies or changed ones "
+                               "will be correctly detected only after Spyder "
+                               "is restarted."))
 
         btn = QPushButton(_("Copy to clipboard"), )
         btn.clicked.connect(self.copy_to_clipboard)
@@ -164,26 +87,18 @@ def __init__(self, parent):
         hlayout.addWidget(bbox)
 
         vlayout = QVBoxLayout()
+        vlayout.addWidget(self.treewidget)
         vlayout.addWidget(self.label)
-        vlayout.addWidget(self.view)
+        vlayout.addWidget(self.label2)
         vlayout.addLayout(hlayout)
 
         self.setLayout(vlayout)
-        self.resize(630, 420)
-
-    def exec_(self):
-        self.show()
-        # we need an explicit show() because resizeRowsToContents() does only
-        # work correctly after the widget has been drawn. Without this the
-        # dimensions are not known yet.
-        self.view.resizeRowsToContents()
-        super(DependenciesDialog, self).exec_()
+        self.resize(840, 560)
 
     def set_data(self, dependencies):
-        self.view.model.set_data(dependencies)
-        self.view.adjust_columns()
-        self.view.sortByColumn(0, Qt.DescendingOrder)
-    
+        self.treewidget.update_dependencies(dependencies)
+        self.treewidget.resize_columns_to_contents()
+
     def copy_to_clipboard(self):
         from spyder.dependencies import status
         QApplication.clipboard().setText(status())
@@ -194,10 +109,12 @@ def test():
     from spyder import dependencies
     
     # Test sample
-    dependencies.add("IPython", "Enhanced Python interpreter", ">=0.13")
+    dependencies.add("IPython", "Enhanced Python interpreter", ">=20.0")
     dependencies.add("matplotlib", "Interactive data plotting", ">=1.0")
-    dependencies.add("sympy", "Symbolic Mathematics", ">=10.0")
+    dependencies.add("sympy", "Symbolic Mathematics", ">=10.0", optional=True)
     dependencies.add("foo", "Non-existent module", ">=1.0")
+    dependencies.add("numpy", "Edit arrays in Variable Explorer", ">=0.10",
+                     optional=True)
     
     from spyder.utils.qthelpers import qapplication
     app = qapplication()


Test patch:
diff --git a/spyder/widgets/tests/test_dependencies.py b/spyder/widgets/tests/test_dependencies.py
--- a/spyder/widgets/tests/test_dependencies.py
+++ b/spyder/widgets/tests/test_dependencies.py
@@ -27,6 +27,7 @@ def test_dependencies(qtbot):
     # Test sample
     dependencies.add("zmq", "Run introspection services", ">=10.0")
     dependencies.add("foo", "Non-existent module", ">=1.0")
+    dependencies.add("bar", "Non-existing optional module", ">=10.0", optional=True)
 
     dlg = setup_dependencies(qtbot)
     dlg.set_data(dependencies.DEPENDENCIES)