eric7/PipInterface/PipPackagesWidget.py

branch
eric7
changeset 8312
800c432b34c8
parent 8240
93b8a353c4bf
child 8318
962bce857696
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/PipInterface/PipPackagesWidget.py	Sat May 15 18:45:04 2021 +0200
@@ -0,0 +1,1272 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the pip packages management widget.
+"""
+
+import textwrap
+import os
+import html.parser
+import contextlib
+
+from PyQt5.QtCore import pyqtSlot, Qt, QUrl, QUrlQuery
+from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
+from PyQt5.QtWidgets import (
+    QWidget, QToolButton, QApplication, QHeaderView, QTreeWidgetItem,
+    QMenu, QDialog
+)
+
+from E5Gui.E5Application import e5App
+from E5Gui import E5MessageBox
+from E5Gui.E5OverrideCursor import E5OverrideCursor
+
+from .Ui_PipPackagesWidget import Ui_PipPackagesWidget
+
+import UI.PixmapCache
+import Globals
+import Preferences
+
+
+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",
+            ):
+                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
+    
+    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.pipMenuButton.setObjectName(
+            "pip_supermenu_button")
+        self.pipMenuButton.setIcon(UI.PixmapCache.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(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.__pip = pip
+        
+        self.packagesList.header().setSortIndicator(
+            0, 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"])
+        
+        venvManager = e5App().getObject("VirtualEnvManager")
+        venvManager.virtualEnvironmentAdded.connect(
+            self.on_refreshButton_clicked)
+        venvManager.virtualEnvironmentRemoved.connect(
+            self.on_refreshButton_clicked)
+        
+        project = e5App().getObject("Project")
+        project.projectOpened.connect(
+            self.on_refreshButton_clicked)
+        project.projectClosed.connect(
+            self.on_refreshButton_clicked)
+        
+        self.__initPipMenu()
+        self.__populateEnvironments()
+        self.__updateActionButtons()
+        
+        self.statusLabel.hide()
+        self.searchWidget.hide()
+        
+        self.__queryName = []
+        self.__querySummary = []
+        
+        self.__replies = []
+        
+        self.__packageDetailsDialog = None
+    
+    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(1))
+        
+        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(2))
+        ]
+    
+    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(2):
+                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 referesh 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 E5OverrideCursor():
+                    # 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(2, latest)
+                    
+                    self.packagesList.sortItems(0, Qt.SortOrder.AscendingOrder)
+                    for col in range(self.packagesList.columnCount()):
+                        self.packagesList.resizeColumnToContents(col)
+                    self.packagesList.setUpdatesEnabled(True)
+                self.statusLabel.hide()
+        
+        self.__updateActionButtons()
+        self.__updateSearchActionButtons()
+        self.__updateSearchButton()
+    
+    @pyqtSlot(int)
+    def on_environmentsComboBox_currentIndexChanged(self, index):
+        """
+        Private slot handling the selection of a Python environment.
+        
+        @param index index of the selected Python environment
+        @type int
+        """
+        self.__refreshPackagesList()
+    
+    @pyqtSlot(bool)
+    def on_localCheckBox_clicked(self, checked):
+        """
+        Private slot handling the switching of the local mode.
+        
+        @param checked state of the local check box
+        @type bool
+        """
+        self.__refreshPackagesList()
+    
+    @pyqtSlot(bool)
+    def on_notRequiredCheckBox_clicked(self, checked):
+        """
+        Private slot handling the switching of the 'not required' mode.
+        
+        @param checked state of the 'not required' check box
+        @type bool
+        """
+        self.__refreshPackagesList()
+    
+    @pyqtSlot(bool)
+    def on_userCheckBox_clicked(self, checked):
+        """
+        Private slot handling the switching of the 'user-site' mode.
+        
+        @param checked state of the 'user-site' check box
+        @type bool
+        """
+        self.__refreshPackagesList()
+    
+    @pyqtSlot()
+    def on_packagesList_itemSelectionChanged(self):
+        """
+        Private slot handling the selection of a package.
+        """
+        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 E5OverrideCursor():
+                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)
+        
+        self.__updateActionButtons()
+    
+    @pyqtSlot(QTreeWidgetItem, int)
+    def on_packagesList_itemActivated(self, item, column):
+        """
+        Private slot reacting on a package item activation.
+        
+        @param item reference to the activated item
+        @type QTreeWidgetItem
+        @param column activated column
+        @type int
+        """
+        packageName = item.text(0)
+        upgradable = bool(item.text(2))
+        if column == 1:
+            # show details for installed version
+            packageVersion = item.text(1)
+        else:
+            # show details for available version or installed one
+            if item.text(2):
+                packageVersion = item.text(2)
+            else:
+                packageVersion = item.text(1)
+        
+        self.__showPackageDetails(packageName, packageVersion,
+                                  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_itemSelectionChanged()
+    
+    @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_itemSelectionChanged()
+    
+    @pyqtSlot()
+    def on_refreshButton_clicked(self):
+        """
+        Private slot to refresh the display.
+        """
+        currentEnvironment = self.environmentsComboBox.currentText()
+        self.environmentsComboBox.clear()
+        self.packagesList.clear()
+        
+        with E5OverrideCursor():
+            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(0) 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(0) 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(0) 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(0)
+            upgradable = bool(item.text(2))
+            # show details for available version or installed one
+            if item.text(2):
+                packageVersion = item.text(2)
+            else:
+                packageVersion = item.text(1)
+            
+            self.__showPackageDetails(packageName, packageVersion,
+                                      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.searchEditName.text()) and
+            self.__isPipAvailable()
+        )
+    
+    @pyqtSlot(bool)
+    def on_searchToggleButton_toggled(self, checked):
+        """
+        Private slot to togle the search widget.
+        
+        @param checked state of the search widget button
+        @type bool
+        """
+        self.searchWidget.setVisible(checked)
+        
+        if checked:
+            self.searchEditName.setFocus(Qt.FocusReason.OtherFocusReason)
+            self.searchEditName.selectAll()
+            
+            self.__updateSearchActionButtons()
+            self.__updateSearchButton()
+    
+    @pyqtSlot(str)
+    def on_searchEditName_textChanged(self, txt):
+        """
+        Private slot handling a change of the search term.
+        
+        @param txt search term
+        @type str
+        """
+        self.__updateSearchButton()
+    
+    @pyqtSlot()
+    def on_searchEditName_returnPressed(self):
+        """
+        Private slot initiating a search via a press of the Return key.
+        """
+        if (
+            bool(self.searchEditName.text()) and
+            self.__isPipAvailable()
+        ):
+            self.__search()
+    
+    @pyqtSlot()
+    def on_searchButton_clicked(self):
+        """
+        Private slot handling a press of the search button.
+        """
+        self.__search()
+    
+    @pyqtSlot()
+    def on_searchResultList_itemSelectionChanged(self):
+        """
+        Private slot handling changes of the search result selection.
+        """
+        self.__updateSearchActionButtons()
+    
+    def __search(self):
+        """
+        Private method to perform the search by calling the PyPI search URL.
+        """
+        self.searchResultList.clear()
+        self.searchInfoLabel.clear()
+        
+        self.searchButton.setEnabled(False)
+        
+        searchTerm = self.searchEditName.text().strip()
+        searchTerm = bytes(QUrl.toPercentEncoding(searchTerm)).decode()
+        urlQuery = QUrlQuery()
+        urlQuery.addQueryItem("q", searchTerm)
+        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:
+            E5MessageBox.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:
+            if len(results) < 20:
+                msg = self.tr("%n package(s) found.", "", len(results))
+            else:
+                msg = self.tr("Showing first 20 packages found.")
+            self.searchInfoLabel.setText(msg)
+        else:
+            E5MessageBox.warning(
+                self,
+                self.tr("Search PyPI"),
+                self.tr("""<p>There were no results for <b>{0}</b>.</p>"""))
+            self.searchInfoLabel.setText(
+                self.tr("""<p>There were no results for <b>{0}</b>.</p>"""))
+        
+        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 = ""
+            itm = QTreeWidgetItem(
+                self.searchResultList, [
+                    result['name'].strip(),
+                    result['version'],
+                    result["released"].strip(),
+                    description,
+                ])
+            itm.setData(0, self.SearchVersionRole, result['version'])
+        
+        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.searchEditName.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,
+                             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 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)
+        """
+        with E5OverrideCursor():
+            packageData = self.__pip.getPackageDetails(
+                packageName, packageVersion)
+        
+        if packageData:
+            from .PipPackageDetailsDialog import PipPackageDetailsDialog
+            
+            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, buttonsMode=buttonsMode,
+                                        parent=self)
+            )
+            self.__packageDetailsDialog.show()
+        else:
+            E5MessageBox.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.
+        """
+        self.__pipMenu = QMenu()
+        self.__installPipAct = self.__pipMenu.addAction(
+            self.tr("Install Pip"),
+            self.__installPip)
+        self.__installPipUserAct = self.__pipMenu.addAction(
+            self.tr("Install Pip to User-Site"),
+            self.__installPipUser)
+        self.__repairPipAct = self.__pipMenu.addAction(
+            self.tr("Repair Pip"),
+            self.__repairPip)
+        self.__pipMenu.addSeparator()
+        self.__installPackagesAct = self.__pipMenu.addAction(
+            self.tr("Install Packages"),
+            self.__installPackages)
+        self.__installLocalPackageAct = self.__pipMenu.addAction(
+            self.tr("Install Local Package"),
+            self.__installLocalPackage)
+        self.__pipMenu.addSeparator()
+        self.__installRequirementsAct = self.__pipMenu.addAction(
+            self.tr("Install Requirements"),
+            self.__installRequirements)
+        self.__reinstallPackagesAct = self.__pipMenu.addAction(
+            self.tr("Re-Install Selected Packages"),
+            self.__reinstallPackages)
+        self.__uninstallRequirementsAct = self.__pipMenu.addAction(
+            self.tr("Uninstall Requirements"),
+            self.__uninstallRequirements)
+        self.__generateRequirementsAct = self.__pipMenu.addAction(
+            self.tr("Generate Requirements..."),
+            self.__generateRequirements)
+        self.__pipMenu.addSeparator()
+        self.__cacheInfoAct = self.__pipMenu.addAction(
+            self.tr("Show Cache Info..."),
+            self.__showCacheInfo)
+        self.__cacheShowListAct = self.__pipMenu.addAction(
+            self.tr("Show Cached Files..."),
+            self.__showCacheList)
+        self.__cacheRemoveAct = self.__pipMenu.addAction(
+            self.tr("Remove Cached Files..."),
+            self.__removeCachedFiles)
+        self.__cachePurgeAct = self.__pipMenu.addAction(
+            self.tr("Purge Cache..."),
+            self.__purgeCache)
+        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.__installPipAct.setEnabled(not enablePip)
+        self.__installPipUserAct.setEnabled(not enablePip)
+        self.__repairPipAct.setEnabled(enablePip)
+        
+        self.__installPackagesAct.setEnabled(enablePip)
+        self.__installLocalPackageAct.setEnabled(enablePip)
+        self.__reinstallPackagesAct.setEnabled(enablePip)
+        
+        self.__installRequirementsAct.setEnabled(enablePip)
+        self.__uninstallRequirementsAct.setEnabled(enablePip)
+        self.__generateRequirementsAct.setEnabled(enablePip)
+        
+        self.__cacheInfoAct.setEnabled(enablePipCache)
+        self.__cacheShowListAct.setEnabled(enablePipCache)
+        self.__cacheRemoveAct.setEnabled(enablePipCache)
+        self.__cachePurgeAct.setEnabled(enablePipCache)
+        
+        self.__editVirtualenvConfigAct.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.
+        """
+        venvName = self.environmentsComboBox.currentText()
+        if venvName:
+            from .PipPackagesInputDialog import PipPackagesInputDialog
+            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.
+        """
+        venvName = self.environmentsComboBox.currentText()
+        if venvName:
+            from .PipFileSelectionDialog import PipFileSelectionDialog
+            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(0) 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.
+        """
+        venvName = self.environmentsComboBox.currentText()
+        if venvName:
+            from .PipFreezeDialog import PipFreezeDialog
+            self.__freezeDialog = PipFreezeDialog(self.__pip, 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 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:
+            E5MessageBox.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:
+            E5MessageBox.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):
+            E5MessageBox.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.
+        """
+        e5App().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)

eric ide

mercurial