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