eric7/PipInterface/PipPackagesWidget.py

branch
eric7
changeset 8978
38c3ddf21537
parent 8977
663521af48b2
child 8985
30e9e592732d
--- a/eric7/PipInterface/PipPackagesWidget.py	Sun Mar 13 19:59:03 2022 +0100
+++ b/eric7/PipInterface/PipPackagesWidget.py	Mon Mar 14 19:49:48 2022 +0100
@@ -24,6 +24,7 @@
 from EricWidgets import EricMessageBox
 from EricGui.EricOverrideCursor import EricOverrideCursor
 
+from .PipVulnerabilityChecker import Package, VulnerabilityCheckError
 from .Ui_PipPackagesWidget import Ui_PipPackagesWidget
 
 import UI.PixmapCache
@@ -305,7 +306,8 @@
                 Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive
             )
             if len(pipList) > 0:
-                pipVersionTuple = Globals.versionToTuple(pipList[0].text(1))
+                pipVersionTuple = Globals.versionToTuple(
+                    pipList[0].text(PipPackagesWidget.InstalledVersionColumn))
         
         return pipVersionTuple
     
@@ -465,94 +467,121 @@
         """
         self.__refreshPackagesList()
     
-    @pyqtSlot()
-    def on_packagesList_itemSelectionChanged(self):
+    def __showPackageInformation(self, packageName):
+        """
+        Private method to show information for a package.
+        
+        @param packageName name of the package
+        @type str
         """
-        Private slot handling the selection of a package.
+        environment = self.environmentsComboBox.currentText()
+        interpreter = self.__pip.getVirtualenvInterpreter(environment)
+        if not interpreter:
+            return
+        
+        args = ["-m", "pip", "show"]
+        if self.verboseCheckBox.isChecked():
+            args.append("--verbose")
+        if self.installedFilesCheckBox.isChecked():
+            args.append("--files")
+        args.append(packageName)
+        
+        with EricOverrideCursor():
+            success, output = self.__pip.runProcess(args, interpreter)
+            
+            if success and output:
+                mode = self.ShowProcessGeneralMode
+                for line in output.splitlines():
+                    line = line.rstrip()
+                    if line != "---":
+                        if mode != self.ShowProcessGeneralMode:
+                            if line[0] == " ":
+                                QTreeWidgetItem(
+                                    self.infoWidget,
+                                    [" ", line.strip()])
+                            else:
+                                mode = self.ShowProcessGeneralMode
+                        if mode == self.ShowProcessGeneralMode:
+                            try:
+                                label, info = line.split(": ", 1)
+                            except ValueError:
+                                label = line[:-1]
+                                info = ""
+                            label = label.lower()
+                            if label in self.__infoLabels:
+                                QTreeWidgetItem(
+                                    self.infoWidget,
+                                    [self.__infoLabels[label], info])
+                            if label == "files":
+                                mode = self.ShowProcessFilesListMode
+                            elif label == "classifiers":
+                                mode = self.ShowProcessClassifiersMode
+                            elif label == "entry-points":
+                                mode = self.ShowProcessEntryPointsMode
+                self.infoWidget.scrollToTop()
+            
+            header = self.infoWidget.header()
+            header.setStretchLastSection(False)
+            header.resizeSections(QHeaderView.ResizeMode.ResizeToContents)
+            if (
+                header.sectionSize(0) + header.sectionSize(1) <
+                header.width()
+            ):
+                header.setStretchLastSection(True)
+    
+    @pyqtSlot(QTreeWidgetItem, int)
+    def on_packagesList_itemClicked(self, item, column):
+        """
+        Private slot reacting on a package item click.
+        
+        @param item reference to the clicked item
+        @type QTreeWidgetItem
+        @param column clicked column
+        @type int
         """
         self.infoWidget.clear()
         
-        if len(self.packagesList.selectedItems()) == 1:
-            itm = self.packagesList.selectedItems()[0]
-            
-            environment = self.environmentsComboBox.currentText()
-            interpreter = self.__pip.getVirtualenvInterpreter(environment)
-            if not interpreter:
-                return
-            
-            args = ["-m", "pip", "show"]
-            if self.verboseCheckBox.isChecked():
-                args.append("--verbose")
-            if self.installedFilesCheckBox.isChecked():
-                args.append("--files")
-            args.append(itm.text(0))
-            
-            with EricOverrideCursor():
-                success, output = self.__pip.runProcess(args, interpreter)
-                
-                if success and output:
-                    mode = self.ShowProcessGeneralMode
-                    for line in output.splitlines():
-                        line = line.rstrip()
-                        if line != "---":
-                            if mode != self.ShowProcessGeneralMode:
-                                if line[0] == " ":
-                                    QTreeWidgetItem(
-                                        self.infoWidget,
-                                        [" ", line.strip()])
-                                else:
-                                    mode = self.ShowProcessGeneralMode
-                            if mode == self.ShowProcessGeneralMode:
-                                try:
-                                    label, info = line.split(": ", 1)
-                                except ValueError:
-                                    label = line[:-1]
-                                    info = ""
-                                label = label.lower()
-                                if label in self.__infoLabels:
-                                    QTreeWidgetItem(
-                                        self.infoWidget,
-                                        [self.__infoLabels[label], info])
-                                if label == "files":
-                                    mode = self.ShowProcessFilesListMode
-                                elif label == "classifiers":
-                                    mode = self.ShowProcessClassifiersMode
-                                elif label == "entry-points":
-                                    mode = self.ShowProcessEntryPointsMode
-                    self.infoWidget.scrollToTop()
-                
-                header = self.infoWidget.header()
-                header.setStretchLastSection(False)
-                header.resizeSections(QHeaderView.ResizeMode.ResizeToContents)
-                if (
-                    header.sectionSize(0) + header.sectionSize(1) <
-                    header.width()
-                ):
-                    header.setStretchLastSection(True)
+        if (
+            column == PipPackagesWidget.VulnerabilityColumn and
+            bool(item.text(PipPackagesWidget.VulnerabilityColumn))
+        ):
+            self.__showVulnerabilityInformation(
+                item.text(PipPackagesWidget.PackageColumn),
+                item.text(PipPackagesWidget.InstalledVersionColumn),
+                item.data(PipPackagesWidget.VulnerabilityColumn, 
+                          PipPackagesWidget.VulnerabilityRole)
+            )
+        else:
+            self.__showPackageInformation(
+                item.text(PipPackagesWidget.PackageColumn)
+            )
         
         self.__updateActionButtons()
     
     @pyqtSlot(QTreeWidgetItem, int)
-    def on_packagesList_itemActivated(self, item, column):
+    def on_packagesList_itemDoubleClicked(self, item, column):
         """
-        Private slot reacting on a package item activation.
+        Private slot reacting on a package item double click.
         
-        @param item reference to the activated item
+        @param item reference to the double clicked item
         @type QTreeWidgetItem
-        @param column activated column
+        @param column double clicked column
         @type int
         """
-        packageName = item.text(0)
-        upgradable = bool(item.text(2))
-        if column == 1:
+        packageName = item.text(PipPackagesWidget.PackageColumn)
+        upgradable = bool(item.text(PipPackagesWidget.AvailableVersionColumn))
+        if column == PipPackagesWidget.InstalledVersionColumn:
             # show details for installed version
-            packageVersion = item.text(1)
+            packageVersion = item.text(
+                PipPackagesWidget.InstalledVersionColumn)
         else:
             # show details for available version or installed one
-            if item.text(2):
-                packageVersion = item.text(2)
+            if item.text(PipPackagesWidget.AvailableVersionColumn):
+                packageVersion = item.text(
+                    PipPackagesWidget.AvailableVersionColumn)
             else:
-                packageVersion = item.text(1)
+                packageVersion = item.text(
+                    PipPackagesWidget.InstalledVersionColumn)
         
         self.__showPackageDetails(packageName, packageVersion,
                                   upgradable=upgradable)
@@ -566,7 +595,8 @@
         @param checked state of the checkbox
         @type bool
         """
-        self.on_packagesList_itemSelectionChanged()
+        self.on_packagesList_itemClicked(self.packagesList.currentItem(),
+                                         self.packagesList.currentColumn())
     
     @pyqtSlot(bool)
     def on_installedFilesCheckBox_clicked(self, checked):
@@ -577,7 +607,8 @@
         @param checked state of the checkbox
         @type bool
         """
-        self.on_packagesList_itemSelectionChanged()
+        self.on_packagesList_itemClicked(self.packagesList.currentItem(),
+                                         self.packagesList.currentColumn())
     
     @pyqtSlot()
     def on_refreshButton_clicked(self):
@@ -605,7 +636,8 @@
         """
         Private slot to upgrade selected packages of the selected environment.
         """
-        packages = [itm.text(0) for itm in self.__selectedUpdateableItems()]
+        packages = [itm.text(PipPackagesWidget.PackageColumn)
+                    for itm in self.__selectedUpdateableItems()]
         if packages:
             self.executeUpgradePackages(packages)
     
@@ -614,7 +646,8 @@
         """
         Private slot to upgrade all packages of the selected environment.
         """
-        packages = [itm.text(0) for itm in self.__allUpdateableItems()]
+        packages = [itm.text(PipPackagesWidget.PackageColumn)
+                    for itm in self.__allUpdateableItems()]
         if packages:
             self.executeUpgradePackages(packages)
     
@@ -662,12 +695,15 @@
         item = self.packagesList.selectedItems()[0]
         if item:
             packageName = item.text(PipPackagesWidget.PackageColumn)
-            upgradable = bool(item.text(2))
+            upgradable = bool(item.text(
+                PipPackagesWidget.AvailableVersionColumn))
             # show details for available version or installed one
-            if item.text(2):
-                packageVersion = item.text(2)
+            if item.text(PipPackagesWidget.AvailableVersionColumn):
+                packageVersion = item.text(
+                    PipPackagesWidget.AvailableVersionColumn)
             else:
-                packageVersion = item.text(1)
+                packageVersion = item.text(
+                    PipPackagesWidget.InstalledVersionColumn)
             
             self.__showPackageDetails(packageName, packageVersion,
                                       upgradable=upgradable)
@@ -1024,6 +1060,14 @@
             self.tr("Generate Requirements..."),
             self.__generateRequirements)
         self.__pipMenu.addSeparator()
+        self.__checkVulnerabilityAct = self.__pipMenu.addAction(
+            self.tr("Check Vulnerabilities"),
+            self.__updateVulnerabilityData)
+        # updateVulnerabilityDbAct
+        self.__pipMenu.addAction(
+            self.tr("Update Vulnerability Database"),
+            self.__updateVulnerabilityDbCache)
+        self.__pipMenu.addSeparator()
         self.__cacheInfoAct = self.__pipMenu.addAction(
             self.tr("Show Cache Info..."),
             self.__showCacheInfo)
@@ -1080,6 +1124,8 @@
         self.__cachePurgeAct.setEnabled(enablePipCache)
         
         self.__editVirtualenvConfigAct.setEnabled(enable)
+        self.__checkVulnerabilityAct.setEnabled(
+            enable & self.vulnerabilityCheckBox.isEnabled())
     
     @pyqtSlot()
     def __installPip(self):
@@ -1334,8 +1380,102 @@
         if clearFirst:
             self.__clearVulnerabilityInfo()
         
-        packages = []       # TODO: fill this list with real data
+        packages = []
+        for row in range(self.packagesList.topLevelItemCount()):
+            itm = self.packagesList.topLevelItem(row)
+            packages.append(Package(
+                name=itm.text(PipPackagesWidget.PackageColumn),
+                version=itm.text(PipPackagesWidget.InstalledVersionColumn)
+            ))
         
         error, vulnerabilities = (
             self.__pip.getVulnerabilityChecker().check(packages)
         )
+        if error == VulnerabilityCheckError.OK:
+            for package in vulnerabilities:
+                items = self.packagesList.findItems(
+                    package,
+                    Qt.MatchFlag.MatchExactly |
+                    Qt.MatchFlag.MatchCaseSensitive
+                )
+                if items:
+                    itm = items[0]
+                    itm.setData(
+                        PipPackagesWidget.VulnerabilityColumn,
+                        PipPackagesWidget.VulnerabilityRole,
+                        vulnerabilities[package]
+                    )
+                    affected = {v.spec for v in vulnerabilities[package]}
+                    itm.setText(
+                        PipPackagesWidget.VulnerabilityColumn,
+                        ', '.join(affected)
+                    )
+                    itm.setIcon(
+                        PipPackagesWidget.VulnerabilityColumn,
+                        UI.PixmapCache.getIcon("securityLow")
+                    )
+        
+        elif error in (VulnerabilityCheckError.FullDbUnavailable,
+                       VulnerabilityCheckError.SummaryDbUnavailable):
+            self.vulnerabilityCheckBox.setChecked(False)
+            self.vulnerabilityCheckBox.setEnabled(False)
+            self.packagesList.setColumnHidden(
+                PipPackagesWidget.VulnerabilityColumn, True)
+    
+    @pyqtSlot()
+    def __updateVulnerabilityDbCache(self):
+        """
+        Private slot to initiate an update of the local cache of the
+        vulnerability database.
+        """
+        with EricOverrideCursor():
+            self.__pip.getVulnerabilityChecker().updateVulnerabilityDb()
+    
+    def __showVulnerabilityInformation(self, packageName, packageVersion,
+                                       vulnerabilities):
+        """
+        Private method to show the detected vulnerability data.
+        
+        @param packageName name of the package
+        @type str
+        @param packageVersion installed version number
+        @type str
+        @param vulnerabilities list of vulnerabilities
+        @type list of Vulnerability
+        """
+        header = (
+            self.tr("{0} {1}", "package name, package version")
+            .format(packageName, packageVersion)
+        )
+        topItem = QTreeWidgetItem(self.infoWidget, [header])
+        topItem.setFirstColumnSpanned(True)
+        topItem.setExpanded(True)
+        font = topItem.font(0)
+        font.setBold(True)
+        topItem.setFont(0, font)
+        
+        for vulnerability in vulnerabilities:
+            title = (
+                vulnerability.cve
+                if vulnerability.cve else
+                vulnerability.vulnerabilityId
+            )
+            titleItem = QTreeWidgetItem(topItem, [title])
+            titleItem.setFirstColumnSpanned(True)
+            titleItem.setExpanded(True)
+            
+            QTreeWidgetItem(
+                titleItem,
+                [self.tr("Affected Version:"), vulnerability.spec])
+            itm = QTreeWidgetItem(
+                titleItem,
+                [self.tr("Advisory:"), vulnerability.advisory])
+            itm.setToolTip(1, "<p>{0}</p>".format(
+                vulnerability.advisory.replace("\r\n", "<br/>")
+            ))
+        
+        self.infoWidget.scrollToTop()
+        self.infoWidget.resizeColumnToContents(0)
+        
+        header = self.infoWidget.header()
+        header.setStretchLastSection(True)

eric ide

mercurial