eric7/PipInterface/PipPackagesWidget.py

branch
eric7
changeset 8997
d8946c2a22b5
parent 8985
30e9e592732d
child 8999
723f61499a79
--- a/eric7/PipInterface/PipPackagesWidget.py	Tue Mar 22 19:31:29 2022 +0100
+++ b/eric7/PipInterface/PipPackagesWidget.py	Wed Mar 23 20:21:42 2022 +0100
@@ -12,6 +12,8 @@
 import html.parser
 import contextlib
 
+from packaging.specifiers import SpecifierSet
+
 from PyQt6.QtCore import pyqtSlot, Qt, QUrl, QUrlQuery
 from PyQt6.QtGui import QIcon
 from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest
@@ -157,6 +159,10 @@
     AvailableVersionColumn = 2
     VulnerabilityColumn = 3
     
+    DepPackageColumn = 0
+    DepInstalledVersionColumn = 1
+    DepRequiredVersionColumn = 2
+    
     def __init__(self, pip, parent=None):
         """
         Constructor
@@ -171,6 +177,8 @@
         
         self.layout().setContentsMargins(0, 3, 0, 0)
         
+        self.viewToggleButton.setIcon(UI.PixmapCache.getIcon("viewListTree"))
+        
         self.pipMenuButton.setObjectName(
             "pip_supermenu_button")
         self.pipMenuButton.setIcon(UI.PixmapCache.getIcon("superMenu"))
@@ -183,21 +191,38 @@
         self.pipMenuButton.setAutoRaise(True)
         self.pipMenuButton.setShowMenuInside(True)
         
-        self.refreshButton.setIcon(UI.PixmapCache.getIcon("reload"))
-        self.upgradeButton.setIcon(UI.PixmapCache.getIcon("1uparrow"))
-        self.upgradeAllButton.setIcon(UI.PixmapCache.getIcon("2uparrow"))
-        self.uninstallButton.setIcon(UI.PixmapCache.getIcon("minus"))
-        self.showPackageDetailsButton.setIcon(UI.PixmapCache.getIcon("info"))
-        self.searchToggleButton.setIcon(UI.PixmapCache.getIcon("find"))
-        self.searchButton.setIcon(UI.PixmapCache.getIcon("findNext"))
-        self.installButton.setIcon(UI.PixmapCache.getIcon("plus"))
-        self.installUserSiteButton.setIcon(UI.PixmapCache.getIcon("addUser"))
-        self.showDetailsButton.setIcon(UI.PixmapCache.getIcon("info"))
+        self.refreshButton.setIcon(
+            UI.PixmapCache.getIcon("reload"))
+        self.upgradeButton.setIcon(
+            UI.PixmapCache.getIcon("1uparrow"))
+        self.upgradeAllButton.setIcon(
+            UI.PixmapCache.getIcon("2uparrow"))
+        self.uninstallButton.setIcon(
+            UI.PixmapCache.getIcon("minus"))
+        self.showPackageDetailsButton.setIcon(
+            UI.PixmapCache.getIcon("info"))
+        self.searchToggleButton.setIcon(
+            UI.PixmapCache.getIcon("find"))
+        self.searchButton.setIcon(
+            UI.PixmapCache.getIcon("findNext"))
+        self.installButton.setIcon(
+            UI.PixmapCache.getIcon("plus"))
+        self.installUserSiteButton.setIcon(
+            UI.PixmapCache.getIcon("addUser"))
+        self.showDetailsButton.setIcon(
+            UI.PixmapCache.getIcon("info"))
+        
+        self.refreshDependenciesButton.setIcon(
+            UI.PixmapCache.getIcon("reload"))
+        self.showDepPackageDetailsButton.setIcon(
+            UI.PixmapCache.getIcon("info"))
         
         self.__pip = pip
         
         self.packagesList.header().setSortIndicator(
             PipPackagesWidget.PackageColumn, Qt.SortOrder.AscendingOrder)
+        self.dependenciesList.header().setSortIndicator(
+            PipPackagesWidget.DepPackageColumn, Qt.SortOrder.AscendingOrder)
         
         self.__infoLabels = {
             "name": self.tr("Name:"),
@@ -216,6 +241,7 @@
             "files": self.tr("Files:"),
         }
         self.infoWidget.setHeaderLabels(["Key", "Value"])
+        self.dependencyInfoWidget.setHeaderLabels(["Key", "Value"])
         
         venvManager = ericApp().getObject("VirtualEnvManager")
         venvManager.virtualEnvironmentAdded.connect(
@@ -232,6 +258,7 @@
         self.__initPipMenu()
         self.__populateEnvironments()
         self.__updateActionButtons()
+        self.__updateDepActionButtons()
         
         self.statusLabel.hide()
         self.searchWidget.hide()
@@ -242,6 +269,8 @@
         self.__replies = []
         
         self.__packageDetailsDialog = None
+        
+        self.viewsStackWidget.setCurrentWidget(self.packagesPage)
     
     @pyqtSlot(bool)
     def __projectClosed(self, shutdown):
@@ -372,7 +401,7 @@
     
     def __refreshPackagesList(self):
         """
-        Private method to referesh the packages list.
+        Private method to refresh the packages list.
         """
         self.packagesList.clear()
         venvName = self.environmentsComboBox.currentText()
@@ -444,7 +473,10 @@
         @param index index of the selected Python environment
         @type int
         """
-        self.__refreshPackagesList()
+        if self.viewToggleButton.isChecked():
+            self.__refreshDependencyTree()
+        else:
+            self.__refreshPackagesList()
     
     @pyqtSlot()
     def on_localCheckBox_clicked(self):
@@ -467,12 +499,14 @@
         """
         self.__refreshPackagesList()
     
-    def __showPackageInformation(self, packageName):
+    def __showPackageInformation(self, packageName, infoWidget):
         """
         Private method to show information for a package.
         
         @param packageName name of the package
         @type str
+        @param infoWidget reference to the widget to contain the information
+        @type QTreeWidget
         """
         environment = self.environmentsComboBox.currentText()
         interpreter = self.__pip.getVirtualenvInterpreter(environment)
@@ -497,7 +531,7 @@
                         if mode != self.ShowProcessGeneralMode:
                             if line[0] == " ":
                                 QTreeWidgetItem(
-                                    self.infoWidget,
+                                    infoWidget,
                                     [" ", line.strip()])
                             else:
                                 mode = self.ShowProcessGeneralMode
@@ -510,7 +544,7 @@
                             label = label.lower()
                             if label in self.__infoLabels:
                                 QTreeWidgetItem(
-                                    self.infoWidget,
+                                    infoWidget,
                                     [self.__infoLabels[label], info])
                             if label == "files":
                                 mode = self.ShowProcessFilesListMode
@@ -518,9 +552,9 @@
                                 mode = self.ShowProcessClassifiersMode
                             elif label == "entry-points":
                                 mode = self.ShowProcessEntryPointsMode
-                self.infoWidget.scrollToTop()
+                infoWidget.scrollToTop()
             
-            header = self.infoWidget.header()
+            header = infoWidget.header()
             header.setStretchLastSection(False)
             header.resizeSections(QHeaderView.ResizeMode.ResizeToContents)
             if (
@@ -562,7 +596,8 @@
                 )
             else:
                 self.__showPackageInformation(
-                    item.text(PipPackagesWidget.PackageColumn)
+                    item.text(PipPackagesWidget.PackageColumn),
+                    self.infoWidget
                 )
         
         self.__updateActionButtons()
@@ -1488,3 +1523,204 @@
         
         header = self.infoWidget.header()
         header.setStretchLastSection(True)
+    
+    #######################################################################
+    ## Dependency tree related methods below
+    #######################################################################
+    
+    @pyqtSlot(bool)
+    def on_viewToggleButton_toggled(self, checked):
+        """
+        Private slot handling the view selection.
+        
+        @param checked state of the toggle button
+        @type bool
+        """
+        if checked:
+            self.viewsStackWidget.setCurrentWidget(
+                self.dependenciesPage)
+            self.__refreshDependencyTree()
+        else:
+            self.viewsStackWidget.setCurrentWidget(
+                self.packagesPage)
+            self.__refreshPackagesList()
+    
+    @pyqtSlot(bool)
+    def on_requiresButton_toggled(self, checked):
+        """
+        Private slot handling the selection of the view type.
+        
+        @param checked state of the radio button (unused)
+        @type bool
+        """
+        self.__refreshDependencyTree()
+    
+    @pyqtSlot()
+    def on_localDepCheckBox_clicked(self):
+        """
+        Private slot handling the switching of the local mode.
+        """
+        self.__refreshDependencyTree()
+    
+    @pyqtSlot()
+    def on_userDepCheckBox_clicked(self):
+        """
+        Private slot handling the switching of the 'user-site' mode.
+        """
+        self.__refreshDependencyTree()
+    
+    def __refreshDependencyTree(self):
+        """
+        Private method to refresh the dependency tree.
+        """
+        self.dependenciesList.clear()
+        venvName = self.environmentsComboBox.currentText()
+        if venvName:
+            interpreter = self.__pip.getVirtualenvInterpreter(venvName)
+            if interpreter:
+                with EricOverrideCursor():
+                    dependencies = self.__pip.getDependecyTree(
+                        venvName,
+                        localPackages=self.localDepCheckBox.isChecked(),
+                        usersite=self.userDepCheckBox.isChecked(),
+                        reverse = self.requiredByButton.isChecked(),
+                    )
+                    
+                    self.dependenciesList.setUpdatesEnabled(False)
+                    for dependency in dependencies:
+                        self.__addDependency(dependency, self.dependenciesList)
+                    
+                    self.dependenciesList.sortItems(
+                        PipPackagesWidget.DepPackageColumn,
+                        Qt.SortOrder.AscendingOrder)
+                    for col in range(self.dependenciesList.columnCount()):
+                        self.dependenciesList.resizeColumnToContents(col)
+                    self.dependenciesList.setUpdatesEnabled(True)
+        
+        self.__updateDepActionButtons()
+    
+    def __addDependency(self, dependency, parent):
+        """
+        Private method to add a dependency branch to a given parent.
+        
+        @param dependency dependency to be added
+        @type dict
+        @param parent reference to the parent item
+        @type QTreeWidget or QTreeWidgetItem
+        """
+        itm = QTreeWidgetItem(parent, [
+            dependency["package_name"],
+            dependency["installed_version"],
+            dependency["required_version"],
+        ])
+        itm.setExpanded(True)
+        
+        if dependency["required_version"].lower() != "any":
+            spec = (
+                "=={0}".format(dependency["required_version"])
+                if dependency["required_version"][0] in "0123456789" else
+                dependency["required_version"]
+            )
+            specifierSet = SpecifierSet(specifiers=spec)
+            if not specifierSet.contains(dependency["installed_version"]):
+                itm.setIcon(PipPackagesWidget.DepRequiredVersionColumn,
+                            UI.PixmapCache.getIcon("warning"))
+        
+        if dependency["required_version"].lower() == "any":
+            itm.setText(PipPackagesWidget.DepRequiredVersionColumn,
+                        self.tr("any"))
+        
+        # recursively add sub-dependencies
+        for dep in dependency["dependencies"]:
+            self.__addDependency(dep, itm)
+    
+    @pyqtSlot(QTreeWidgetItem, int)
+    def on_dependenciesList_itemActivated(self, item, column):
+        """
+        Private slot reacting on a package item of the dependency tree being
+        activated.
+        
+        @param item reference to the activated item
+        @type QTreeWidgetItem
+        @param column activated column
+        @type int
+        """
+        packageName = item.text(PipPackagesWidget.DepPackageColumn)
+        packageVersion = item.text(
+            PipPackagesWidget.DepInstalledVersionColumn)
+        
+        self.__showPackageDetails(packageName, packageVersion)
+    
+    @pyqtSlot()
+    def on_dependenciesList_itemSelectionChanged(self):
+        """
+        Private slot reacting on a change of selected items of the dependency
+        tree.
+        """
+        if len(self.dependenciesList.selectedItems()) == 0:
+            self.dependencyInfoWidget.clear()
+    
+    @pyqtSlot(QTreeWidgetItem, int)
+    def on_dependenciesList_itemPressed(self, item, column):
+        """
+        Private slot reacting on a package item of the dependency tree being
+        pressed.
+        
+        @param item reference to the pressed item
+        @type QTreeWidgetItem
+        @param column pressed column
+        @type int
+        """
+        self.dependencyInfoWidget.clear()
+        
+        if item is not None:
+            self.__showPackageInformation(
+                item.text(PipPackagesWidget.DepPackageColumn),
+                self.dependencyInfoWidget
+            )
+        
+        self.__updateDepActionButtons()
+    
+    @pyqtSlot()
+    def on_refreshDependenciesButton_clicked(self):
+        """
+        Private slot to refresh the dependency tree.
+        """
+        currentEnvironment = self.environmentsComboBox.currentText()
+        self.environmentsComboBox.clear()
+        self.dependenciesList.clear()
+        
+        with EricOverrideCursor():
+            self.__populateEnvironments()
+            
+            index = self.environmentsComboBox.findText(
+                currentEnvironment,
+                Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive
+            )
+            if index != -1:
+                self.environmentsComboBox.setCurrentIndex(index)
+        
+        self.__updateDepActionButtons()
+    
+    @pyqtSlot()
+    def on_showDepPackageDetailsButton_clicked(self):
+        """
+        Private slot to show information for the selected package of the
+        dependency tree.
+        """
+        item = self.dependenciesList.selectedItems()[0]
+        if item:
+            packageName = item.text(PipPackagesWidget.DepPackageColumn)
+            packageVersion = item.text(
+                PipPackagesWidget.DepInstalledVersionColumn)
+            
+            self.__showPackageDetails(packageName, packageVersion)
+    
+    def __updateDepActionButtons(self):
+        """
+        Private method to set the state of the dependency page action buttons.
+        """
+        self.showDepPackageDetailsButton.setEnabled(
+            len(self.dependenciesList.selectedItems()) == 1 and
+            self.__isPipAvailable()
+        )

eric ide

mercurial