src/eric7/PipInterface/PipPackagesWidget.py

Mon, 06 Mar 2023 09:50:18 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 06 Mar 2023 09:50:18 +0100
branch
eric7
changeset 9851
ec12090e9cd9
parent 9717
02544efa689b
child 9940
a57c188857e9
child 10069
435cc5875135
permissions
-rw-r--r--

Modified the pip licenses dialog to show the license data for packages contained in the packages list only.

# -*- coding: utf-8 -*-

# Copyright (c) 2019 - 2023 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the pip packages management widget.
"""

import contextlib
import html.parser
import os
import textwrap

from packaging.specifiers import SpecifierSet
from PyQt6.QtCore import Qt, QUrl, QUrlQuery, pyqtSlot
from PyQt6.QtGui import QIcon
from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest
from PyQt6.QtWidgets import (
    QAbstractItemView,
    QApplication,
    QDialog,
    QHeaderView,
    QMenu,
    QToolButton,
    QTreeWidgetItem,
    QWidget,
)

from eric7 import Globals, Preferences
from eric7.EricGui import EricPixmapCache
from eric7.EricGui.EricOverrideCursor import EricOverrideCursor
from eric7.EricWidgets import EricMessageBox
from eric7.EricWidgets.EricApplication import ericApp

from .PipVulnerabilityChecker import Package, VulnerabilityCheckError
from .Ui_PipPackagesWidget import Ui_PipPackagesWidget


class PypiSearchResultsParser(html.parser.HTMLParser):
    """
    Class implementing the parser for the PyPI search result page.
    """

    ClassPrefix = "package-snippet__"

    def __init__(self, data):
        """
        Constructor

        @param data data to be parsed
        @type str
        """
        super().__init__()
        self.__results = []
        self.__activeClass = None
        self.feed(data)

    def __getClass(self, attrs):
        """
        Private method to extract the class attribute out of the list of
        attributes.

        @param attrs list of tag attributes as (name, value) tuples
        @type list of tuple of (str, str)
        @return value of the 'class' attribute or None
        @rtype str
        """
        for name, value in attrs:
            if name == "class":
                return value

        return None

    def __getDate(self, attrs):
        """
        Private method to extract the datetime attribute out of the list of
        attributes and process it.

        @param attrs list of tag attributes as (name, value) tuples
        @type list of tuple of (str, str)
        @return value of the 'class' attribute or None
        @rtype str
        """
        for name, value in attrs:
            if name == "datetime":
                return value.split("T")[0]

        return None

    def handle_starttag(self, tag, attrs):
        """
        Public method to process the start tag.

        @param tag tag name (all lowercase)
        @type str
        @param attrs list of tag attributes as (name, value) tuples
        @type list of tuple of (str, str)
        """
        if tag == "a" and self.__getClass(attrs) == "package-snippet":
            self.__results.append({})

        if tag in ("span", "p"):
            tagClass = self.__getClass(attrs)
            if tagClass in (
                "package-snippet__name",
                "package-snippet__description",
                "package-snippet__version",
                "package-snippet__released",
                "package-snippet__created",
            ):
                self.__activeClass = tagClass
            else:
                self.__activeClass = None
        elif tag == "time":
            attributeName = self.__activeClass.replace(self.ClassPrefix, "")
            self.__results[-1][attributeName] = self.__getDate(attrs)
            self.__activeClass = None
        else:
            self.__activeClass = None

    def handle_data(self, data):
        """
        Public method process arbitrary data.

        @param data data to be processed
        @type str
        """
        if self.__activeClass is not None:
            attributeName = self.__activeClass.replace(self.ClassPrefix, "")
            self.__results[-1][attributeName] = data

    def handle_endtag(self, tag):
        """
        Public method to process the end tag.

        @param tag tag name (all lowercase)
        @type str
        """
        self.__activeClass = None

    def getResults(self):
        """
        Public method to get the extracted search results.

        @return extracted result data
        @rtype list of dict
        """
        return self.__results


class PipPackagesWidget(QWidget, Ui_PipPackagesWidget):
    """
    Class implementing the pip packages management widget.
    """

    ShowProcessGeneralMode = 0
    ShowProcessClassifiersMode = 1
    ShowProcessEntryPointsMode = 2
    ShowProcessFilesListMode = 3

    SearchVersionRole = Qt.ItemDataRole.UserRole + 1
    VulnerabilityRole = Qt.ItemDataRole.UserRole + 2

    PackageColumn = 0
    InstalledVersionColumn = 1
    AvailableVersionColumn = 2
    VulnerabilityColumn = 3

    DepPackageColumn = 0
    DepInstalledVersionColumn = 1
    DepRequiredVersionColumn = 2

    def __init__(self, pip, parent=None):
        """
        Constructor

        @param pip reference to the global pip interface
        @type Pip
        @param parent reference to the parent widget
        @type QWidget
        """
        super().__init__(parent)
        self.setupUi(self)

        self.layout().setContentsMargins(0, 3, 0, 0)

        self.viewToggleButton.setIcon(EricPixmapCache.getIcon("viewListTree"))

        self.pipMenuButton.setObjectName("pip_supermenu_button")
        self.pipMenuButton.setIcon(EricPixmapCache.getIcon("superMenu"))
        self.pipMenuButton.setToolTip(self.tr("pip Menu"))
        self.pipMenuButton.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
        self.pipMenuButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
        self.pipMenuButton.setFocusPolicy(Qt.FocusPolicy.NoFocus)
        self.pipMenuButton.setAutoRaise(True)
        self.pipMenuButton.setShowMenuInside(True)

        self.refreshButton.setIcon(EricPixmapCache.getIcon("reload"))
        self.upgradeButton.setIcon(EricPixmapCache.getIcon("1uparrow"))
        self.upgradeAllButton.setIcon(EricPixmapCache.getIcon("2uparrow"))
        self.uninstallButton.setIcon(EricPixmapCache.getIcon("minus"))
        self.showPackageDetailsButton.setIcon(EricPixmapCache.getIcon("info"))
        self.searchToggleButton_1.setIcon(EricPixmapCache.getIcon("find"))
        self.searchToggleButton_2.setIcon(EricPixmapCache.getIcon("find"))
        self.searchButton.setIcon(EricPixmapCache.getIcon("findNext"))
        self.searchMoreButton.setIcon(EricPixmapCache.getIcon("plus"))
        self.installButton.setIcon(EricPixmapCache.getIcon("plus"))
        self.installUserSiteButton.setIcon(EricPixmapCache.getIcon("addUser"))
        self.showDetailsButton.setIcon(EricPixmapCache.getIcon("info"))

        self.refreshDependenciesButton.setIcon(EricPixmapCache.getIcon("reload"))
        self.showDepPackageDetailsButton.setIcon(EricPixmapCache.getIcon("info"))
        self.dependencyRepairButton.setIcon(EricPixmapCache.getIcon("repair"))

        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:"),
            "version": self.tr("Version:"),
            "location": self.tr("Location:"),
            "requires": self.tr("Requires:"),
            "summary": self.tr("Summary:"),
            "home-page": self.tr("Homepage:"),
            "author": self.tr("Author:"),
            "author-email": self.tr("Author Email:"),
            "license": self.tr("License:"),
            "metadata-version": self.tr("Metadata Version:"),
            "installer": self.tr("Installer:"),
            "classifiers": self.tr("Classifiers:"),
            "entry-points": self.tr("Entry Points:"),
            "files": self.tr("Files:"),
        }
        self.infoWidget.setHeaderLabels(["Key", "Value"])
        self.dependencyInfoWidget.setHeaderLabels(["Key", "Value"])

        venvManager = ericApp().getObject("VirtualEnvManager")
        venvManager.virtualEnvironmentAdded.connect(self.on_refreshButton_clicked)
        venvManager.virtualEnvironmentRemoved.connect(self.on_refreshButton_clicked)
        self.__selectedEnvironment = None

        project = ericApp().getObject("Project")
        project.projectOpened.connect(self.__projectOpened)
        project.projectClosed.connect(self.__projectClosed)

        self.__initPipMenu()
        self.__populateEnvironments()
        self.__updateActionButtons()
        self.__updateDepActionButtons()

        self.statusLabel.hide()
        self.searchWidget.hide()
        self.__lastSearchPage = 0

        self.__queryName = []
        self.__querySummary = []

        self.__replies = []

        self.__packageDetailsDialog = None

        self.viewsStackWidget.setCurrentWidget(self.packagesPage)

    @pyqtSlot()
    def __projectOpened(self):
        """
        Private slot to handle the projectOpened signal.
        """
        projectVenv = self.__pip.getProjectEnvironmentString()
        if projectVenv:
            self.environmentsComboBox.insertItem(1, projectVenv)

    @pyqtSlot(bool)
    def __projectClosed(self, shutdown):
        """
        Private slot to handle the projectClosed signal.

        @param shutdown flag indicating the IDE shutdown
        @type bool
        """
        if not shutdown:
            if self.environmentsComboBox.currentIndex() == 1:
                self.environmentsComboBox.setCurrentIndex(0)

            # the project entry is always at index 1
            self.environmentsComboBox.removeItem(1)

    def __populateEnvironments(self):
        """
        Private method to get a list of environments and populate the selector.
        """
        self.environmentsComboBox.addItem("")
        projectVenv = self.__pip.getProjectEnvironmentString()
        if projectVenv:
            self.environmentsComboBox.addItem(projectVenv)
        self.environmentsComboBox.addItems(
            self.__pip.getVirtualenvNames(
                noRemote=True, noConda=Preferences.getPip("ExcludeCondaEnvironments")
            )
        )

    def __isPipAvailable(self):
        """
        Private method to check, if the pip package is available for the
        selected environment.

        @return flag indicating availability
        @rtype bool
        """
        available = False

        venvName = self.environmentsComboBox.currentText()
        if venvName:
            available = (
                len(
                    self.packagesList.findItems(
                        "pip",
                        Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive,
                    )
                )
                == 1
            )

        return available

    def __availablePipVersion(self):
        """
        Private method to get the pip version of the selected environment.

        @return tuple containing the version number or tuple with all zeros
            in case pip is not available
        @rtype tuple of int
        """
        pipVersionTuple = (0, 0, 0)
        venvName = self.environmentsComboBox.currentText()
        if venvName:
            pipList = self.packagesList.findItems(
                "pip", Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive
            )
            if len(pipList) > 0:
                pipVersionTuple = Globals.versionToTuple(
                    pipList[0].text(PipPackagesWidget.InstalledVersionColumn)
                )

        return pipVersionTuple

    def getPip(self):
        """
        Public method to get a reference to the pip interface object.

        @return reference to the pip interface object
        @rtype Pip
        """
        return self.__pip

    #######################################################################
    ## Slots handling widget signals below
    #######################################################################

    def __selectedUpdateableItems(self):
        """
        Private method to get a list of selected items that can be updated.

        @return list of selected items that can be updated
        @rtype list of QTreeWidgetItem
        """
        return [
            itm
            for itm in self.packagesList.selectedItems()
            if bool(itm.text(PipPackagesWidget.AvailableVersionColumn))
        ]

    def __allPackageNames(self):
        """
        Private method to get a list of all package names.

        @return list of all package names
        @rtype list of str
        """
        packages = []
        for index in range(self.packagesList.topLevelItemCount()):
            packages.append(self.packagesList.topLevelItem(index).text(0))
        return packages

    def __allUpdateableItems(self):
        """
        Private method to get a list of all items that can be updated.

        @return list of all items that can be updated
        @rtype list of QTreeWidgetItem
        """
        updateableItems = []
        for index in range(self.packagesList.topLevelItemCount()):
            itm = self.packagesList.topLevelItem(index)
            if itm.text(PipPackagesWidget.AvailableVersionColumn):
                updateableItems.append(itm)

        return updateableItems

    def __updateActionButtons(self):
        """
        Private method to set the state of the action buttons.
        """
        if self.__isPipAvailable():
            self.upgradeButton.setEnabled(bool(self.__selectedUpdateableItems()))
            self.uninstallButton.setEnabled(bool(self.packagesList.selectedItems()))
            self.upgradeAllButton.setEnabled(bool(self.__allUpdateableItems()))
            self.showPackageDetailsButton.setEnabled(
                len(self.packagesList.selectedItems()) == 1
            )
        else:
            self.upgradeButton.setEnabled(False)
            self.uninstallButton.setEnabled(False)
            self.upgradeAllButton.setEnabled(False)
            self.showPackageDetailsButton.setEnabled(False)

    def __refreshPackagesList(self):
        """
        Private method to refresh the packages list.
        """
        self.packagesList.clear()
        venvName = self.environmentsComboBox.currentText()
        if venvName:
            interpreter = self.__pip.getVirtualenvInterpreter(venvName)
            if interpreter:
                self.statusLabel.show()
                self.statusLabel.setText(self.tr("Getting installed packages..."))

                with EricOverrideCursor():
                    # 1. populate with installed packages
                    self.packagesList.setUpdatesEnabled(False)
                    installedPackages = self.__pip.getInstalledPackages(
                        venvName,
                        localPackages=self.localCheckBox.isChecked(),
                        notRequired=self.notRequiredCheckBox.isChecked(),
                        usersite=self.userCheckBox.isChecked(),
                    )
                    for package, version in installedPackages:
                        QTreeWidgetItem(self.packagesList, [package, version, "", ""])
                    self.packagesList.setUpdatesEnabled(True)
                    self.statusLabel.setText(self.tr("Getting outdated packages..."))
                    QApplication.processEvents()

                    # 2. update with update information
                    self.packagesList.setUpdatesEnabled(False)
                    outdatedPackages = self.__pip.getOutdatedPackages(
                        venvName,
                        localPackages=self.localCheckBox.isChecked(),
                        notRequired=self.notRequiredCheckBox.isChecked(),
                        usersite=self.userCheckBox.isChecked(),
                    )
                    for package, _version, latest in outdatedPackages:
                        items = self.packagesList.findItems(
                            package,
                            Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive,
                        )
                        if items:
                            itm = items[0]
                            itm.setText(
                                PipPackagesWidget.AvailableVersionColumn, latest
                            )

                    self.packagesList.sortItems(
                        PipPackagesWidget.PackageColumn, Qt.SortOrder.AscendingOrder
                    )
                    for col in range(self.packagesList.columnCount()):
                        self.packagesList.resizeColumnToContents(col)
                    self.packagesList.setUpdatesEnabled(True)

                    # 3. update with vulnerability information
                    if self.vulnerabilityCheckBox.isChecked():
                        self.__updateVulnerabilityData()
                self.statusLabel.hide()

        self.__updateActionButtons()
        self.__updateSearchActionButtons()
        self.__updateSearchButton()
        self.__updateSearchMoreButton(False)

    @pyqtSlot(str)
    def on_environmentsComboBox_currentTextChanged(self, name):
        """
        Private slot handling the selection of a Python environment.

        @param name name of the selected Python environment
        @type str
        """
        if name != self.__selectedEnvironment:
            if not name:
                self.environmentPathLabel.setPath("")
                self.searchNameEdit.clear()
                self.searchNameEdit.setEnabled(False)
            else:
                self.environmentPathLabel.setPath(
                    self.__pip.getVirtualenvInterpreter(name)
                )
                self.searchNameEdit.setEnabled(True)

            if self.viewToggleButton.isChecked():
                self.__refreshDependencyTree()
            else:
                self.__refreshPackagesList()
            self.__selectedEnvironment = name

    @pyqtSlot()
    def on_localCheckBox_clicked(self):
        """
        Private slot handling the switching of the local mode.
        """
        self.__refreshPackagesList()

    @pyqtSlot()
    def on_notRequiredCheckBox_clicked(self):
        """
        Private slot handling the switching of the 'not required' mode.
        """
        self.__refreshPackagesList()

    @pyqtSlot()
    def on_userCheckBox_clicked(self):
        """
        Private slot handling the switching of the 'user-site' mode.
        """
        self.__refreshPackagesList()

    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)
        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 and line != "---":
                        if mode != self.ShowProcessGeneralMode:
                            if line[0] == " ":
                                QTreeWidgetItem(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(
                                    infoWidget, [self.__infoLabels[label], info]
                                )
                            if label == "files":
                                mode = self.ShowProcessFilesListMode
                            elif label == "classifiers":
                                mode = self.ShowProcessClassifiersMode
                            elif label == "entry-points":
                                mode = self.ShowProcessEntryPointsMode
                infoWidget.scrollToTop()

            header = infoWidget.header()
            header.setStretchLastSection(False)
            header.resizeSections(QHeaderView.ResizeMode.ResizeToContents)
            if header.sectionSize(0) + header.sectionSize(1) < header.width():
                header.setStretchLastSection(True)

    @pyqtSlot()
    def on_packagesList_itemSelectionChanged(self):
        """
        Private slot reacting on a change of selected items.
        """
        if len(self.packagesList.selectedItems()) == 0:
            self.infoWidget.clear()

    @pyqtSlot(QTreeWidgetItem, int)
    def on_packagesList_itemPressed(self, item, column):
        """
        Private slot reacting on a package item being pressed.

        @param item reference to the pressed item
        @type QTreeWidgetItem
        @param column pressed column
        @type int
        """
        self.infoWidget.clear()

        if item is not None:
            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.infoWidget
                )

        self.__updateActionButtons()

    @pyqtSlot(QTreeWidgetItem, int)
    def on_packagesList_itemActivated(self, item, column):
        """
        Private slot reacting on a package item being activated.

        @param item reference to the activated item
        @type QTreeWidgetItem
        @param column activated column
        @type int
        """
        packageName = item.text(PipPackagesWidget.PackageColumn)
        upgradable = bool(item.text(PipPackagesWidget.AvailableVersionColumn))
        if column == PipPackagesWidget.InstalledVersionColumn:
            # show details for installed version
            packageVersion = item.text(PipPackagesWidget.InstalledVersionColumn)
        else:
            # show details for available version or installed one
            if item.text(PipPackagesWidget.AvailableVersionColumn):
                packageVersion = item.text(PipPackagesWidget.AvailableVersionColumn)
            else:
                packageVersion = item.text(PipPackagesWidget.InstalledVersionColumn)

        vulnerabilities = (
            item.data(
                PipPackagesWidget.VulnerabilityColumn,
                PipPackagesWidget.VulnerabilityRole,
            )
            if bool(item.text(PipPackagesWidget.VulnerabilityColumn))
            else []
        )

        self.__showPackageDetails(
            packageName,
            packageVersion,
            vulnerabilities=vulnerabilities,
            upgradable=upgradable,
        )

    @pyqtSlot(bool)
    def on_verboseCheckBox_clicked(self, checked):
        """
        Private slot to handle a change of the verbose package information
        checkbox.

        @param checked state of the checkbox
        @type bool
        """
        self.on_packagesList_itemPressed(
            self.packagesList.currentItem(), self.packagesList.currentColumn()
        )

    @pyqtSlot(bool)
    def on_installedFilesCheckBox_clicked(self, checked):
        """
        Private slot to handle a change of the installed files information
        checkbox.

        @param checked state of the checkbox
        @type bool
        """
        self.on_packagesList_itemPressed(
            self.packagesList.currentItem(), self.packagesList.currentColumn()
        )

    @pyqtSlot()
    def on_refreshButton_clicked(self):
        """
        Private slot to refresh the display.
        """
        currentEnvironment = self.environmentsComboBox.currentText()
        self.environmentsComboBox.clear()
        self.packagesList.clear()

        with EricOverrideCursor():
            self.__populateEnvironments()

            index = self.environmentsComboBox.findText(
                currentEnvironment,
                Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive,
            )
            if index != -1:
                self.environmentsComboBox.setCurrentIndex(index)

        self.__updateActionButtons()

    @pyqtSlot()
    def on_upgradeButton_clicked(self):
        """
        Private slot to upgrade selected packages of the selected environment.
        """
        packages = [
            itm.text(PipPackagesWidget.PackageColumn)
            for itm in self.__selectedUpdateableItems()
        ]
        if packages:
            self.executeUpgradePackages(packages)

    @pyqtSlot()
    def on_upgradeAllButton_clicked(self):
        """
        Private slot to upgrade all packages of the selected environment.
        """
        packages = [
            itm.text(PipPackagesWidget.PackageColumn)
            for itm in self.__allUpdateableItems()
        ]
        if packages:
            self.executeUpgradePackages(packages)

    @pyqtSlot()
    def on_uninstallButton_clicked(self):
        """
        Private slot to remove selected packages of the selected environment.
        """
        packages = [
            itm.text(PipPackagesWidget.PackageColumn)
            for itm in self.packagesList.selectedItems()
        ]
        self.executeUninstallPackages(packages)

    def executeUninstallPackages(self, packages):
        """
        Public method to uninstall the given list of packages.

        @param packages list of package names to be uninstalled
        @type list of str
        """
        if packages:
            ok = self.__pip.uninstallPackages(
                packages, venvName=self.environmentsComboBox.currentText()
            )
            if ok:
                self.on_refreshButton_clicked()

    def executeUpgradePackages(self, packages):
        """
        Public method to execute the pip upgrade command.

        @param packages list of package names to be upgraded
        @type list of str
        """
        ok = self.__pip.upgradePackages(
            packages,
            venvName=self.environmentsComboBox.currentText(),
            userSite=self.userCheckBox.isChecked(),
        )
        if ok:
            self.on_refreshButton_clicked()

    @pyqtSlot()
    def on_showPackageDetailsButton_clicked(self):
        """
        Private slot to show information for the selected package.
        """
        item = self.packagesList.selectedItems()[0]
        if item:
            packageName = item.text(PipPackagesWidget.PackageColumn)
            upgradable = bool(item.text(PipPackagesWidget.AvailableVersionColumn))
            # show details for available version or installed one
            if item.text(PipPackagesWidget.AvailableVersionColumn):
                packageVersion = item.text(PipPackagesWidget.AvailableVersionColumn)
            else:
                packageVersion = item.text(PipPackagesWidget.InstalledVersionColumn)

            vulnerabilities = (
                item.data(
                    PipPackagesWidget.VulnerabilityColumn,
                    PipPackagesWidget.VulnerabilityRole,
                )
                if bool(item.text(PipPackagesWidget.VulnerabilityColumn))
                else []
            )

            self.__showPackageDetails(
                packageName,
                packageVersion,
                vulnerabilities=vulnerabilities,
                upgradable=upgradable,
            )

    #######################################################################
    ## Search widget related methods below
    #######################################################################

    def __updateSearchActionButtons(self):
        """
        Private method to update the action button states of the search widget.
        """
        installEnable = (
            len(self.searchResultList.selectedItems()) > 0
            and self.environmentsComboBox.currentIndex() > 0
            and self.__isPipAvailable()
        )
        self.installButton.setEnabled(installEnable)
        self.installUserSiteButton.setEnabled(installEnable)

        self.showDetailsButton.setEnabled(
            len(self.searchResultList.selectedItems()) == 1 and self.__isPipAvailable()
        )

    def __updateSearchButton(self):
        """
        Private method to update the state of the search button.
        """
        self.searchButton.setEnabled(
            bool(self.searchNameEdit.text()) and self.__isPipAvailable()
        )

    def __updateSearchMoreButton(self, enable):
        """
        Private method to update the state of the search more button.

        @param enable flag indicating the desired enable state
        @type bool
        """
        self.searchMoreButton.setEnabled(
            enable and bool(self.searchNameEdit.text()) and self.__isPipAvailable()
        )

    @pyqtSlot(bool)
    def on_searchToggleButton_1_toggled(self, checked):
        """
        Private slot to toggle the search widget.

        @param checked state of the search widget button
        @type bool
        """
        self.searchWidget.setVisible(checked)
        self.searchToggleButton_2.setChecked(checked)

        if checked:
            self.searchNameEdit.setFocus(Qt.FocusReason.OtherFocusReason)
            self.searchNameEdit.selectAll()

            self.__updateSearchActionButtons()
            self.__updateSearchButton()
            self.__updateSearchMoreButton(False)

    @pyqtSlot(bool)
    def on_searchToggleButton_2_toggled(self, checked):
        """
        Private slot to toggle the search widget.

        @param checked state of the search widget button
        @type bool
        """
        self.searchToggleButton_1.setChecked(checked)

    @pyqtSlot(str)
    def on_searchNameEdit_textChanged(self, txt):
        """
        Private slot handling a change of the search term.

        @param txt search term
        @type str
        """
        self.__updateSearchButton()

    @pyqtSlot()
    def on_searchNameEdit_returnPressed(self):
        """
        Private slot initiating a search via a press of the Return key.
        """
        if bool(self.searchNameEdit.text()) and self.__isPipAvailable():
            self.__searchFirst()

    @pyqtSlot()
    def on_searchButton_clicked(self):
        """
        Private slot handling a press of the search button.
        """
        self.__searchFirst()

    @pyqtSlot()
    def on_searchMoreButton_clicked(self):
        """
        Private slot handling a press of the search more button.
        """
        self.__search(self.__lastSearchPage + 1)

    @pyqtSlot()
    def on_searchResultList_itemSelectionChanged(self):
        """
        Private slot handling changes of the search result selection.
        """
        self.__updateSearchActionButtons()

    def __searchFirst(self):
        """
        Private method to perform the search for packages.
        """
        self.searchResultList.clear()
        self.searchInfoLabel.clear()

        self.__updateSearchMoreButton(False)

        self.__search()

    def __search(self, page=1):
        """
        Private method to perform the search by calling the PyPI search URL.

        @param page search page to retrieve (defaults to 1)
        @type int (optional)
        """
        self.__lastSearchPage = page

        self.searchButton.setEnabled(False)

        searchTerm = self.searchNameEdit.text().strip()
        searchTerm = bytes(QUrl.toPercentEncoding(searchTerm)).decode()
        urlQuery = QUrlQuery()
        urlQuery.addQueryItem("q", searchTerm)
        urlQuery.addQueryItem("page", str(page))
        url = QUrl(self.__pip.getIndexUrlSearch())
        url.setQuery(urlQuery)

        request = QNetworkRequest(QUrl(url))
        request.setAttribute(
            QNetworkRequest.Attribute.CacheLoadControlAttribute,
            QNetworkRequest.CacheLoadControl.AlwaysNetwork,
        )
        reply = self.__pip.getNetworkAccessManager().get(request)
        reply.finished.connect(lambda: self.__searchResponse(reply))
        self.__replies.append(reply)

    def __searchResponse(self, reply):
        """
        Private method to extract the search result data from the response.

        @param reply reference to the reply object containing the data
        @type QNetworkReply
        """
        if reply in self.__replies:
            self.__replies.remove(reply)

        urlQuery = QUrlQuery(reply.url())
        searchTerm = urlQuery.queryItemValue("q")

        if reply.error() != QNetworkReply.NetworkError.NoError:
            EricMessageBox.warning(
                None,
                self.tr("Search PyPI"),
                self.tr(
                    "<p>Received an error while searching for <b>{0}</b>.</p>"
                    "<p>Error: {1}</p>"
                ).format(searchTerm, reply.errorString()),
            )
            reply.deleteLater()
            return

        data = bytes(reply.readAll()).decode()
        reply.deleteLater()

        results = PypiSearchResultsParser(data).getResults()
        if results:
            # PyPI returns max. 20 entries per page
            if len(results) < 20:
                msg = self.tr(
                    "%n package(s) found.",
                    "",
                    (self.__lastSearchPage - 1) * 20 + len(results),
                )
                self.__updateSearchMoreButton(False)
            else:
                msg = self.tr("Showing first {0} packages found.").format(
                    self.__lastSearchPage * 20
                )
                self.__updateSearchMoreButton(True)
            self.searchInfoLabel.setText(msg)
            lastItem = self.searchResultList.topLevelItem(
                self.searchResultList.topLevelItemCount() - 1
            )
        else:
            self.__updateSearchMoreButton(False)
            if self.__lastSearchPage == 1:
                EricMessageBox.warning(
                    self,
                    self.tr("Search PyPI"),
                    self.tr("""<p>There were no results for <b>{0}</b>.</p>""").format(
                        searchTerm
                    ),
                )
                self.searchInfoLabel.setText(
                    self.tr("""<p>There were no results for <b>{0}</b>.</p>""").format(
                        searchTerm
                    )
                )
            else:
                EricMessageBox.warning(
                    self,
                    self.tr("Search PyPI"),
                    self.tr(
                        """<p>There were no more results for <b>{0}</b>.</p>"""
                    ).format(searchTerm),
                )
            lastItem = None

        wrapper = textwrap.TextWrapper(width=80)
        for result in results:
            try:
                description = "\n".join(
                    [
                        wrapper.fill(line)
                        for line in result["description"].strip().splitlines()
                    ]
                )
            except KeyError:
                description = ""
            date = result["released"] if "released" in result else result["created"]
            itm = QTreeWidgetItem(
                self.searchResultList,
                [
                    result["name"].strip(),
                    result["version"],
                    date.strip(),
                    description,
                ],
            )
            itm.setData(0, self.SearchVersionRole, result["version"])

        if lastItem:
            self.searchResultList.scrollToItem(
                lastItem, QAbstractItemView.ScrollHint.PositionAtTop
            )

        header = self.searchResultList.header()
        header.setStretchLastSection(False)
        header.resizeSections(QHeaderView.ResizeMode.ResizeToContents)
        headerSize = 0
        for col in range(header.count()):
            headerSize += header.sectionSize(col)
        if headerSize < header.width():
            header.setStretchLastSection(True)

        self.__finishSearch()

    def __finishSearch(self):
        """
        Private slot performing the search finishing actions.
        """
        self.__updateSearchActionButtons()
        self.__updateSearchButton()

        self.searchNameEdit.setFocus(Qt.FocusReason.OtherFocusReason)

    @pyqtSlot()
    def on_installButton_clicked(self):
        """
        Private slot to handle pressing the Install button..
        """
        packages = [
            itm.text(0).strip() for itm in self.searchResultList.selectedItems()
        ]
        self.executeInstallPackages(packages)

    @pyqtSlot()
    def on_installUserSiteButton_clicked(self):
        """
        Private slot to handle pressing the Install to User-Site button..
        """
        packages = [
            itm.text(0).strip() for itm in self.searchResultList.selectedItems()
        ]
        self.executeInstallPackages(packages, userSite=True)

    def executeInstallPackages(self, packages, userSite=False):
        """
        Public method to install the given list of packages.

        @param packages list of package names to be installed
        @type list of str
        @param userSite flag indicating to install to the user directory
        @type bool
        """
        venvName = self.environmentsComboBox.currentText()
        if venvName and packages:
            self.__pip.installPackages(packages, venvName=venvName, userSite=userSite)
            self.on_refreshButton_clicked()

    @pyqtSlot()
    def on_showDetailsButton_clicked(self):
        """
        Private slot to handle pressing the Show Details button.
        """
        self.__showSearchedDetails()

    @pyqtSlot(QTreeWidgetItem, int)
    def on_searchResultList_itemActivated(self, item, column):
        """
        Private slot reacting on an search result item activation.

        @param item reference to the activated item
        @type QTreeWidgetItem
        @param column activated column
        @type int
        """
        self.__showSearchedDetails(item)

    def __showSearchedDetails(self, item=None):
        """
        Private slot to show details about the selected search result package.

        @param item reference to the search result item to show details for
        @type QTreeWidgetItem
        """
        self.showDetailsButton.setEnabled(False)

        if not item:
            item = self.searchResultList.selectedItems()[0]

        packageVersion = item.data(0, self.SearchVersionRole)
        packageName = item.text(0)

        self.__showPackageDetails(packageName, packageVersion, installable=True)

    def __showPackageDetails(
        self,
        packageName,
        packageVersion,
        vulnerabilities=None,
        upgradable=False,
        installable=False,
    ):
        """
        Private method to populate the package details dialog.

        @param packageName name of the package to show details for
        @type str
        @param packageVersion version of the package
        @type str
        @param vulnerabilities list of known vulnerabilities (defaults to None)
        @type list (optional)
        @param upgradable flag indicating that the package may be upgraded
            (defaults to False)
        @type bool (optional)
        @param installable flag indicating that the package may be installed
            (defaults to False)
        @type bool (optional)
        """
        from .PipPackageDetailsDialog import PipPackageDetailsDialog

        with EricOverrideCursor():
            packageData = self.__pip.getPackageDetails(packageName, packageVersion)

        if packageData:
            self.showDetailsButton.setEnabled(True)

            if installable:
                buttonsMode = PipPackageDetailsDialog.ButtonInstall
            elif upgradable:
                buttonsMode = (
                    PipPackageDetailsDialog.ButtonRemove
                    | PipPackageDetailsDialog.ButtonUpgrade
                )
            else:
                buttonsMode = PipPackageDetailsDialog.ButtonRemove

            if self.__packageDetailsDialog is not None:
                self.__packageDetailsDialog.close()

            self.__packageDetailsDialog = PipPackageDetailsDialog(
                packageData,
                vulnerabilities=vulnerabilities,
                buttonsMode=buttonsMode,
                parent=self,
            )
            self.__packageDetailsDialog.show()
        else:
            EricMessageBox.warning(
                self,
                self.tr("Search PyPI"),
                self.tr(
                    """<p>No package details info for <b>{0}</b>"""
                    """ available.</p>"""
                ).format(packageName),
            )

    #######################################################################
    ## Menu related methods below
    #######################################################################

    def __initPipMenu(self):
        """
        Private method to create the super menu and attach it to the super
        menu button.
        """
        ###################################################################
        ## Menu with pip related actions
        ###################################################################

        self.__pipSubmenu = QMenu(self.tr("Pip"))
        self.__installPipAct = self.__pipSubmenu.addAction(
            self.tr("Install Pip"), self.__installPip
        )
        self.__installPipUserAct = self.__pipSubmenu.addAction(
            self.tr("Install Pip to User-Site"), self.__installPipUser
        )
        self.__repairPipAct = self.__pipSubmenu.addAction(
            self.tr("Repair Pip"), self.__repairPip
        )

        ###################################################################
        ## Menu with install related actions
        ###################################################################

        self.__installSubmenu = QMenu(self.tr("Install"))
        self.__installPackagesAct = self.__installSubmenu.addAction(
            self.tr("Install Packages"), self.__installPackages
        )
        self.__installLocalPackageAct = self.__installSubmenu.addAction(
            self.tr("Install Local Package"), self.__installLocalPackage
        )
        self.__reinstallPackagesAct = self.__installSubmenu.addAction(
            self.tr("Re-Install Selected Packages"), self.__reinstallPackages
        )

        ###################################################################
        ## Menu for requirements and constraints management
        ###################################################################

        self.__requirementsSubenu = QMenu(self.tr("Requirements/Constraints"))
        self.__installRequirementsAct = self.__requirementsSubenu.addAction(
            self.tr("Install Requirements"), self.__installRequirements
        )
        self.__uninstallRequirementsAct = self.__requirementsSubenu.addAction(
            self.tr("Uninstall Requirements"), self.__uninstallRequirements
        )
        self.__generateRequirementsAct = self.__requirementsSubenu.addAction(
            self.tr("Generate Requirements..."), self.__generateRequirements
        )
        self.__requirementsSubenu.addSeparator()
        self.__generateConstraintsAct = self.__requirementsSubenu.addAction(
            self.tr("Generate Constraints..."), self.__generateConstraints
        )

        ###################################################################
        ## Menu for requirements and constraints management
        ###################################################################

        self.__cacheSubmenu = QMenu(self.tr("Cache"))
        self.__cacheInfoAct = self.__cacheSubmenu.addAction(
            self.tr("Show Cache Info..."), self.__showCacheInfo
        )
        self.__cacheShowListAct = self.__cacheSubmenu.addAction(
            self.tr("Show Cached Files..."), self.__showCacheList
        )
        self.__cacheRemoveAct = self.__cacheSubmenu.addAction(
            self.tr("Remove Cached Files..."), self.__removeCachedFiles
        )
        self.__cachePurgeAct = self.__cacheSubmenu.addAction(
            self.tr("Purge Cache..."), self.__purgeCache
        )

        ###################################################################
        ## Main menu
        ###################################################################

        self.__pipMenu = QMenu()
        self.__pipSubmenuAct = self.__pipMenu.addMenu(self.__pipSubmenu)
        self.__pipMenu.addSeparator()
        self.__installSubmenuAct = self.__pipMenu.addMenu(self.__installSubmenu)
        self.__pipMenu.addSeparator()
        self.__requirementsSubmenuAct = self.__pipMenu.addMenu(
            self.__requirementsSubenu
        )
        self.__pipMenu.addSeparator()
        self.__showLicensesDialogAct = self.__pipMenu.addAction(
            self.tr("Show Licenses..."), self.__showLicensesDialog
        )
        self.__pipMenu.addSeparator()
        self.__checkVulnerabilityAct = self.__pipMenu.addAction(
            self.tr("Check Vulnerabilities"), self.__checkVulnerability
        )
        # updateVulnerabilityDbAct
        self.__pipMenu.addAction(
            self.tr("Update Vulnerability Database"), self.__updateVulnerabilityDbCache
        )
        self.__pipMenu.addSeparator()
        self.__cyclonedxAct = self.__pipMenu.addAction(
            self.tr("Create SBOM file"), self.__createSBOMFile
        )
        self.__pipMenu.addSeparator()
        self.__cacheSubmenuAct = self.__pipMenu.addMenu(self.__cacheSubmenu)
        self.__pipMenu.addSeparator()
        # editUserConfigAct
        self.__pipMenu.addAction(
            self.tr("Edit User Configuration..."), self.__editUserConfiguration
        )
        self.__editVirtualenvConfigAct = self.__pipMenu.addAction(
            self.tr("Edit Environment Configuration..."),
            self.__editVirtualenvConfiguration,
        )
        self.__pipMenu.addSeparator()
        # pipConfigAct
        self.__pipMenu.addAction(self.tr("Configure..."), self.__pipConfigure)

        self.__pipMenu.aboutToShow.connect(self.__aboutToShowPipMenu)

        self.pipMenuButton.setMenu(self.__pipMenu)

    def __aboutToShowPipMenu(self):
        """
        Private slot to set the action enabled status.
        """
        enable = bool(self.environmentsComboBox.currentText())
        enablePip = self.__isPipAvailable()
        enablePipCache = self.__availablePipVersion() >= (20, 1, 0)

        self.__pipSubmenuAct.setEnabled(enable)
        self.__installPipAct.setEnabled(not enablePip)
        self.__installPipUserAct.setEnabled(not enablePip)
        self.__repairPipAct.setEnabled(enablePip)

        self.__installSubmenu.setEnabled(enablePip)

        self.__requirementsSubmenuAct.setEnabled(enablePip)

        self.__cacheSubmenuAct.setEnabled(enablePipCache)

        self.__editVirtualenvConfigAct.setEnabled(enable)

        self.__checkVulnerabilityAct.setEnabled(
            enable & self.vulnerabilityCheckBox.isEnabled()
        )

        self.__cyclonedxAct.setEnabled(enable)

        self.__showLicensesDialogAct.setEnabled(enable)

    @pyqtSlot()
    def __installPip(self):
        """
        Private slot to install pip into the selected environment.
        """
        venvName = self.environmentsComboBox.currentText()
        if venvName:
            self.__pip.installPip(venvName)
            self.on_refreshButton_clicked()

    @pyqtSlot()
    def __installPipUser(self):
        """
        Private slot to install pip into the user site for the selected
        environment.
        """
        venvName = self.environmentsComboBox.currentText()
        if venvName:
            self.__pip.installPip(venvName, userSite=True)
            self.on_refreshButton_clicked()

    @pyqtSlot()
    def __repairPip(self):
        """
        Private slot to repair the pip installation of the selected
        environment.
        """
        venvName = self.environmentsComboBox.currentText()
        if venvName:
            self.__pip.repairPip(venvName)
            self.on_refreshButton_clicked()

    @pyqtSlot()
    def __installPackages(self):
        """
        Private slot to install packages to be given by the user.
        """
        from .PipPackagesInputDialog import PipPackagesInputDialog

        venvName = self.environmentsComboBox.currentText()
        if venvName:
            dlg = PipPackagesInputDialog(self, self.tr("Install Packages"))
            if dlg.exec() == QDialog.DialogCode.Accepted:
                packages, user = dlg.getData()
                self.executeInstallPackages(packages, userSite=user)

    @pyqtSlot()
    def __installLocalPackage(self):
        """
        Private slot to install a package available on local storage.
        """
        from .PipFileSelectionDialog import PipFileSelectionDialog

        venvName = self.environmentsComboBox.currentText()
        if venvName:
            dlg = PipFileSelectionDialog(self, "package")
            if dlg.exec() == QDialog.DialogCode.Accepted:
                package, user = dlg.getData()
                if package and os.path.exists(package):
                    self.executeInstallPackages([package], userSite=user)

    @pyqtSlot()
    def __reinstallPackages(self):
        """
        Private slot to force a re-installation of the selected packages.
        """
        packages = [
            itm.text(PipPackagesWidget.PackageColumn)
            for itm in self.packagesList.selectedItems()
        ]
        venvName = self.environmentsComboBox.currentText()
        if venvName and packages:
            self.__pip.installPackages(packages, venvName=venvName, forceReinstall=True)
            self.on_refreshButton_clicked()

    @pyqtSlot()
    def __installRequirements(self):
        """
        Private slot to install packages as given in a requirements file.
        """
        venvName = self.environmentsComboBox.currentText()
        if venvName:
            self.__pip.installRequirements(venvName)
            self.on_refreshButton_clicked()

    @pyqtSlot()
    def __uninstallRequirements(self):
        """
        Private slot to uninstall packages as given in a requirements file.
        """
        venvName = self.environmentsComboBox.currentText()
        if venvName:
            self.__pip.uninstallRequirements(venvName)
            self.on_refreshButton_clicked()

    @pyqtSlot()
    def __generateRequirements(self):
        """
        Private slot to generate the contents for a requirements file.
        """
        from .PipFreezeDialog import PipFreezeDialog, PipFreezeDialogModes

        venvName = self.environmentsComboBox.currentText()
        if venvName:
            self.__freezeDialog = PipFreezeDialog(
                self.__pip, mode=PipFreezeDialogModes.Requirements, parent=self
            )
            self.__freezeDialog.show()
            self.__freezeDialog.start(venvName)

    @pyqtSlot()
    def __generateConstraints(self):
        """
        Private slot to generate the contents for a constraints file.
        """
        from .PipFreezeDialog import PipFreezeDialog, PipFreezeDialogModes

        venvName = self.environmentsComboBox.currentText()
        if venvName:
            self.__freezeDialog = PipFreezeDialog(
                self.__pip, mode=PipFreezeDialogModes.Constraints, parent=self
            )
            self.__freezeDialog.show()
            self.__freezeDialog.start(venvName)

    @pyqtSlot()
    def __editUserConfiguration(self):
        """
        Private slot to edit the user configuration.
        """
        self.__editConfiguration()

    @pyqtSlot()
    def __editVirtualenvConfiguration(self):
        """
        Private slot to edit the configuration of the selected environment.
        """
        venvName = self.environmentsComboBox.currentText()
        if venvName:
            self.__editConfiguration(venvName=venvName)

    def __editConfiguration(self, venvName=""):
        """
        Private method to edit a configuration.

        @param venvName name of the environment to act upon
        @type str
        """
        from eric7.QScintilla.MiniEditor import MiniEditor

        if venvName:
            cfgFile = self.__pip.getVirtualenvConfig(venvName)
            if not cfgFile:
                return
        else:
            cfgFile = self.__pip.getUserConfig()
        cfgDir = os.path.dirname(cfgFile)
        if not cfgDir:
            EricMessageBox.critical(
                None,
                self.tr("Edit Configuration"),
                self.tr("""No valid configuration path determined. Aborting"""),
            )
            return

        try:
            if not os.path.isdir(cfgDir):
                os.makedirs(cfgDir)
        except OSError:
            EricMessageBox.critical(
                None,
                self.tr("Edit Configuration"),
                self.tr("""No valid configuration path determined. Aborting"""),
            )
            return

        if not os.path.exists(cfgFile):
            with contextlib.suppress(OSError), open(cfgFile, "w") as f:
                f.write("[global]\n")

        # check, if the destination is writeable
        if not os.access(cfgFile, os.W_OK):
            EricMessageBox.critical(
                None,
                self.tr("Edit Configuration"),
                self.tr("""No valid configuration path determined. Aborting"""),
            )
            return

        self.__editor = MiniEditor(cfgFile, "Properties")
        self.__editor.show()

    def __pipConfigure(self):
        """
        Private slot to open the configuration page.
        """
        ericApp().getObject("UserInterface").showPreferences("pipPage")

    @pyqtSlot()
    def __showCacheInfo(self):
        """
        Private slot to show information about the cache.
        """
        venvName = self.environmentsComboBox.currentText()
        if venvName:
            self.__pip.showCacheInfo(venvName)

    @pyqtSlot()
    def __showCacheList(self):
        """
        Private slot to show a list of cached files.
        """
        venvName = self.environmentsComboBox.currentText()
        if venvName:
            self.__pip.cacheList(venvName)

    @pyqtSlot()
    def __removeCachedFiles(self):
        """
        Private slot to remove files from the pip cache.
        """
        venvName = self.environmentsComboBox.currentText()
        if venvName:
            self.__pip.cacheRemove(venvName)

    @pyqtSlot()
    def __purgeCache(self):
        """
        Private slot to empty the pip cache.
        """
        venvName = self.environmentsComboBox.currentText()
        if venvName:
            self.__pip.cachePurge(venvName)

    ##################################################################
    ## Interface to the vulnerability checks below
    ##################################################################

    @pyqtSlot(bool)
    def on_vulnerabilityCheckBox_clicked(self, checked):
        """
        Private slot handling a change of the automatic vulnerability checks.

        @param checked flag indicating the state of the check box
        @type bool
        """
        if checked:
            self.__updateVulnerabilityData(clearFirst=True)

        self.packagesList.header().setSectionHidden(
            PipPackagesWidget.VulnerabilityColumn, not checked
        )

    @pyqtSlot()
    def __checkVulnerability(self):
        """
        Private slot to update and show the vulnerability data (called from the menu).
        """
        self.vulnerabilityCheckBox.setChecked(True)
        self.on_vulnerabilityCheckBox_clicked(True)

    @pyqtSlot()
    def __clearVulnerabilityInfo(self):
        """
        Private slot to clear the vulnerability info.
        """
        for row in range(self.packagesList.topLevelItemCount()):
            itm = self.packagesList.topLevelItem(row)
            itm.setText(PipPackagesWidget.VulnerabilityColumn, "")
            itm.setToolTip(PipPackagesWidget.VulnerabilityColumn, "")
            itm.setIcon(PipPackagesWidget.VulnerabilityColumn, QIcon())
            itm.setData(
                PipPackagesWidget.VulnerabilityColumn,
                PipPackagesWidget.VulnerabilityRole,
                None,
            )

    @pyqtSlot()
    def __updateVulnerabilityData(self, clearFirst=True):
        """
        Private slot to update the shown vulnerability info.

        @param clearFirst flag indicating to clear the vulnerability info first
            (defaults to True)
        @type bool (optional)
        """
        if clearFirst:
            self.__clearVulnerabilityInfo()

        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,
                        EricPixmapCache.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)

    #######################################################################
    ## 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.getDependencyTree(
                        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["installed_version"] == "?":
            itm.setText(PipPackagesWidget.DepInstalledVersionColumn, self.tr("unknown"))

        if dependency["required_version"].lower() not in ("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,
                    EricPixmapCache.getIcon("warning"),
                )

        elif dependency["required_version"].lower() == "any":
            itm.setText(PipPackagesWidget.DepRequiredVersionColumn, self.tr("any"))

        elif dependency["required_version"] == "?":
            itm.setText(PipPackagesWidget.DepRequiredVersionColumn, self.tr("unknown"))

        # 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()

        self.__updateDepActionButtons()

    @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
        )

        self.dependencyRepairButton.setEnabled(
            any(
                not itm.icon(PipPackagesWidget.DepRequiredVersionColumn).isNull()
                for itm in self.dependenciesList.selectedItems()
            )
        )

    @pyqtSlot()
    def on_dependencyRepairButton_clicked(self):
        """
        Private slot to repair all selected dependencies.
        """
        packages = []
        for itm in self.dependenciesList.selectedItems():
            if not itm.icon(PipPackagesWidget.DepRequiredVersionColumn).isNull():
                packages.append(
                    "{0}{1}".format(
                        itm.text(PipPackagesWidget.DepPackageColumn),
                        itm.text(PipPackagesWidget.DepRequiredVersionColumn),
                    )
                )

        venvName = self.environmentsComboBox.currentText()
        if venvName and packages:
            self.__pip.installPackages(
                packages, venvName=venvName, userSite=self.userDepCheckBox.isChecked()
            )
            self.on_refreshDependenciesButton_clicked()

    ##################################################################
    ## Interface to show the licenses dialog below
    ##################################################################

    @pyqtSlot()
    def __showLicensesDialog(self):
        """
        Private slot to show a dialog with the licenses of the selected
        environment.
        """
        from .PipLicensesDialog import PipLicensesDialog

        environment = self.environmentsComboBox.currentText()
        dlg = PipLicensesDialog(
            self.__pip,
            environment,
            packages=self.__allPackageNames(),
            parent=self,
        )
        dlg.exec()

    ##################################################################
    ## Interface to create a SBOM file using CycloneDX
    ##################################################################

    @pyqtSlot()
    def __createSBOMFile(self):
        """
        Private slot to create a "Software Bill Of Material" file.
        """
        import CycloneDXInterface  # __IGNORE_WARNING_I102__

        venvName = self.environmentsComboBox.currentText()
        if venvName == self.__pip.getProjectEnvironmentString():
            venvName = "<project>"
        CycloneDXInterface.createCycloneDXFile(venvName)

eric ide

mercurial