Wed, 20 Feb 2019 19:44:13 +0100
PipInterface: continued with the pip interface widget.
# -*- coding: utf-8 -*- # Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the pip packages management widget. """ from __future__ import unicode_literals import textwrap from PyQt5.QtCore import pyqtSlot, Qt, QEventLoop, QRegExp from PyQt5.QtGui import QCursor from PyQt5.QtWidgets import QWidget, QToolButton, QApplication, QHeaderView, \ QTreeWidgetItem from E5Gui.E5Application import e5App from E5Gui import E5MessageBox from E5Network.E5XmlRpcClient import E5XmlRpcClient from .Ui_PipPackagesWidget import Ui_PipPackagesWidget import UI.PixmapCache from .Pip import Pip class PipPackagesWidget(QWidget, Ui_PipPackagesWidget): """ Class implementing the pip packages management widget. """ ShowProcessGeneralMode = 0 ShowProcessClassifiersMode = 1 ShowProcessEntryPointsMode = 2 ShowProcessFilesListMode = 3 SearchStopwords = { "a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", } SearchVersionRole = Qt.UserRole + 1 def __init__(self, parent=None): """ Constructor @param parent reference to the parent widget @type QWidget """ super(PipPackagesWidget, self).__init__(parent) self.setupUi(self) self.pipMenuButton.setObjectName( "navigation_supermenu_button") self.pipMenuButton.setIcon(UI.PixmapCache.getIcon("superMenu")) self.pipMenuButton.setToolTip(self.tr("pip Menu")) self.pipMenuButton.setPopupMode(QToolButton.InstantPopup) self.pipMenuButton.setToolButtonStyle(Qt.ToolButtonIconOnly) self.pipMenuButton.setFocusPolicy(Qt.NoFocus) self.pipMenuButton.setAutoRaise(True) self.pipMenuButton.setShowMenuInside(True) self.searchToggleButton.setIcon(UI.PixmapCache.getIcon("find")) self.__pip = Pip(self) self.__client = E5XmlRpcClient(self.__pip.getIndexUrlXml(), self) self.packagesList.header().setSortIndicator(0, Qt.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() 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()) 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.MatchExactly | Qt.MatchCaseSensitive)) == 1 return available ####################################################################### ## 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())) else: self.upgradeButton.setEnabled(False) self.uninstallButton.setEnabled(False) self.upgradeAllButton.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: QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) self.statusLabel.show() self.statusLabel.setText( self.tr("Getting installed packages...")) QApplication.processEvents() # 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.MatchExactly | Qt.MatchCaseSensitive) if items: itm = items[0] itm.setText(2, latest) self.packagesList.sortItems(0, Qt.AscendingOrder) for col in range(self.packagesList.columnCount()): self.packagesList.resizeColumnToContents(col) self.packagesList.setUpdatesEnabled(True) QApplication.restoreOverrideCursor() self.statusLabel.hide() self.__updateActionButtons() self.__updateSearchActionButtons() self.__updateSearchButton() @pyqtSlot(int) def on_environmentsComboBox_currentIndexChanged(self, index): """ Private slot handling the selection of a conda environment. @param index index of the selected conda 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 QApplication.setOverrideCursor(Qt.WaitCursor) args = ["-m", "pip", "show"] if self.verboseCheckBox.isChecked(): args.append("--verbose") if self.installedFilesCheckBox.isChecked(): args.append("--files") args.append(itm.text(0)) 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.ResizeToContents) if header.sectionSize(0) + header.sectionSize(1) < header.width(): header.setStretchLastSection(True) QApplication.restoreOverrideCursor() self.__updateActionButtons() @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() QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) QApplication.processEvents() self.__populateEnvironments() index = self.environmentsComboBox.findText( currentEnvironment, Qt.MatchExactly | Qt.MatchCaseSensitive) if index != -1: self.environmentsComboBox.setCurrentIndex(index) QApplication.restoreOverrideCursor() 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: ok = self.__executeUpgradePackages(packages) if ok: self.on_refreshButton_clicked() @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: ok = self.__executeUpgradePackages(packages) if ok: self.on_refreshButton_clicked() @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()] if packages: ok = self.__pip.uninstallPackages( packages, venvName=self.environmentsComboBox.currentText()) if ok: self.on_refreshButton_clicked() def __executeUpgradePackages(self, packages): """ Private method to execute the pip upgrade command. @param packages list of package names to be upgraded @type list of str @return flag indicating success @rtype bool """ ok = self.__pip.upgradePackages( packages, venvName=self.environmentsComboBox.currentText(), userSite=self.userCheckBox.isChecked()) return ok ####################################################################### ## 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.searchEdit.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.searchEdit.setFocus(Qt.OtherFocusReason) self.searchEdit.selectAll() self.__updateSearchActionButtons() self.__updateSearchButton() @pyqtSlot(str) def on_searchEdit_textChanged(self, txt): """ Private slot handling a change of the search term. @param txt search term @type str """ self.__updateSearchButton() @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. """ self.searchResultList.clear() self.searchInfoLabel.clear() self.searchButton.setEnabled(False) QApplication.setOverrideCursor(Qt.WaitCursor) QApplication.processEvents(QEventLoop.ExcludeUserInputEvents) self.__query = [term for term in self.searchEdit.text().strip().split() if term not in self.SearchStopwords] self.__client.call( "search", ({"name": self.__query, "summary": self.__query}, "or"), self.__processSearchResult, self.__searchError ) def __processSearchResult(self, data): """ Private method to process the search result data from PyPI. @param data result data with hits in the first element @type tuple """ if data: packages = self.__transformHits(data[0]) if packages: self.searchInfoLabel.setText( self.tr("%n package(s) found.", "", len(packages))) wrapper = textwrap.TextWrapper(width=80) count = 0 total = 0 for package in packages: itm = QTreeWidgetItem( self.searchResultList, [ package['name'].strip(), "{0:4d}".format(package['score']), "\n".join([ wrapper.fill(line) for line in package['summary'].strip().splitlines() ]) ]) itm.setData(0, self.SearchVersionRole, package['version']) count += 1 total += 1 if count == 100: count = 0 QApplication.processEvents() else: QApplication.restoreOverrideCursor() E5MessageBox.warning( self, self.tr("Search PyPI"), self.tr("""<p>The package search did not return""" """ anything.</p>""")) self.searchInfoLabel.setText( self.tr("""<p>The package search did not return""" """ anything.</p>""")) else: QApplication.restoreOverrideCursor() E5MessageBox.warning( self, self.tr("Search PyPI"), self.tr("""<p>The package search did not return anything.""" """</p>""")) self.searchInfoLabel.setText( self.tr("""<p>The package search did not return anything.""" """</p>""")) header = self.searchResultList.header() self.searchResultList.sortItems(1, Qt.DescendingOrder) header.setStretchLastSection(False) header.resizeSections(QHeaderView.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. """ QApplication.restoreOverrideCursor() self.__updateSearchActionButtons() self.__updateSearchButton() self.searchEdit.setFocus(Qt.OtherFocusReason) def __searchError(self, errorCode, errorString): """ Private method handling a search error. @param errorCode code of the error @type int @param errorString error message @type str """ self.__finish() E5MessageBox.warning( self, self.tr("Search PyPI"), self.tr("""<p>The package search failed.</p><p>Reason: {0}</p>""") .format(errorString)) self.searchInfoLabel.setText(self.tr("Error: {0}").format(errorString)) def __transformHits(self, hits): """ Private method to convert the list returned from pypi into a packages list. @param hits list returned from pypi @type list of dict @return list of packages @rtype list of dict """ # we only include the record with the highest score packages = {} for hit in hits: name = hit['name'].strip() summary = (hit['summary'] or "").strip() version = hit['version'].strip() score = self.__score(name, summary) # cleanup the summary if summary in ["UNKNOWN", "."]: summary = "" if name not in packages: packages[name] = { 'name': name, 'summary': summary, 'version': [version.strip()], 'score': score} else: if score > packages[name]['score']: packages[name]['score'] = score packages[name]['summary'] = summary packages[name]['version'].append(version.strip()) return list(packages.values()) def __score(self, name, summary): """ Private method to calculate some score for a search result. @param name name of the returned package @type str @param summary summary text for the package @type str @return score value @rtype int """ score = 0 for queryTerm in self.__query: if queryTerm.lower() in name.lower(): score += 4 if queryTerm.lower() == name.lower(): score += 4 if queryTerm.lower() in summary.lower(): if QRegExp(r'\b{0}\b'.format(QRegExp.escape(queryTerm)), Qt.CaseInsensitive).indexIn(summary) != -1: # word match gets even higher score score += 2 else: score += 1 return score ####################################################################### ## Menu related methods below ####################################################################### def __initPipMenu(self): """ Private method to create the super menu and attach it to the super menu button. """ self.__pip.initActions() self.__pipMenu = self.__pip.initMenu() self.pipMenuButton.setMenu(self.__pipMenu)