Wed, 06 Jun 2018 20:05:39 +0200
pip Interface: started to add support for the '--user' option.
# -*- coding: utf-8 -*- # Copyright (c) 2015 - 2018 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing a dialog to list installed packages. """ from __future__ import unicode_literals try: str = unicode # __IGNORE_EXCEPTION__ except NameError: pass import json from PyQt5.QtCore import pyqtSlot, Qt, QProcess, QTimer from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton, \ QApplication, QTreeWidgetItem, QHeaderView from E5Gui import E5MessageBox from .Ui_PipListDialog import Ui_PipListDialog import Preferences class PipListDialog(QDialog, Ui_PipListDialog): """ Class implementing a dialog to list installed packages. """ CommandArguments = { "list": ["list", "--format=json"], "uptodate": ["list", "--uptodate", "--format=json"], "outdated": ["list", "--outdated", "--format=json"], } ShowProcessGeneralMode = 0 ShowProcessClassifiersMode = 1 ShowProcessEntryPointsMode = 2 ShowProcessFilesListMode = 3 def __init__(self, pip, mode, plugin, title, parent=None): """ Constructor @param pip reference to the master object (Pip) @param mode list command mode (string; one of 'list', 'uptodate', 'outdated') @param plugin reference to the plugin object (ToolPipPlugin) @param title title of the dialog (string) @param parent reference to the parent widget (QWidget) """ assert mode in PipListDialog.CommandArguments super(PipListDialog, self).__init__(parent) self.setupUi(self) self.setWindowFlags(Qt.Window) self.setWindowTitle(title) self.__refreshButton = self.buttonBox.addButton( self.tr("&Refresh"), QDialogButtonBox.ActionRole) self.__refreshButton.setEnabled(False) if mode == "outdated": self.__upgradeButton = self.buttonBox.addButton( self.tr("Up&grade"), QDialogButtonBox.ActionRole) self.__upgradeButton.setEnabled(False) self.__upgradeAllButton = self.buttonBox.addButton( self.tr("Upgrade &All"), QDialogButtonBox.ActionRole) self.__upgradeAllButton.setEnabled(False) else: self.__upgradeButton = None self.__upgradeAllButton = None self.__uninstallButton = self.buttonBox.addButton( self.tr("&Uninstall"), QDialogButtonBox.ActionRole) self.__uninstallButton.setEnabled(False) self.__pip = pip self.__mode = mode self.__defaultCommand = plugin.getPreferences("CurrentPipExecutable") self.__ioEncoding = Preferences.getSystem("IOEncoding") self.__indexUrl = plugin.getPreferences("PipSearchIndex") self.__errors = "" self.__output = [] self.__nothingStrings = { "list": self.tr("Nothing to show"), "uptodate": self.tr("All packages outdated"), "outdated": self.tr("All packages up-to-date"), } self.__default = self.tr("<Default>") pipExecutables = sorted(plugin.getPreferences("PipExecutables")) self.pipComboBox.addItem(self.__default) self.pipComboBox.addItems(pipExecutables) if mode == "list": self.infoLabel.setText(self.tr("Installed Packages:")) self.packageList.setHeaderLabels([ self.tr("Package"), self.tr("Version"), ]) elif mode == "uptodate": self.infoLabel.setText(self.tr("Up-to-date Packages:")) self.packageList.setHeaderLabels([ self.tr("Package"), self.tr("Version"), ]) elif mode == "outdated": self.infoLabel.setText(self.tr("Outdated Packages:")) self.packageList.setHeaderLabels([ self.tr("Package"), self.tr("Current Version"), self.tr("Latest Version"), self.tr("Package Type"), ]) self.packageList.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"]) self.process = QProcess() self.process.finished.connect(self.__procFinished) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) self.show() QApplication.processEvents() def __stopProcess(self): """ Private slot to stop the running process. """ if self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) QApplication.restoreOverrideCursor() def closeEvent(self, e): """ Protected slot implementing a close event handler. @param e close event (QCloseEvent) """ self.__stopProcess() e.accept() def __finish(self): """ Private slot called when the process finished or the user pressed the cancel button. """ self.__stopProcess() self.__processOutput() self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.buttonBox.button(QDialogButtonBox.Close).setFocus( Qt.OtherFocusReason) self.__refreshButton.setEnabled(True) if self.packageList.topLevelItemCount() == 0: QTreeWidgetItem(self.packageList, [self.__nothingStrings[self.__mode]]) if self.__errors and not self.__errors.startswith("DEPRECATION"): E5MessageBox.critical( self, self.windowTitle(), self.tr("""<p>The pip command failed.</p>""" """<p>Reason: {0}</p>""").format( self.__errors.replace("\r\n", "<br/>") .replace("\n", "<br/>").replace("\r", "<br/>") .replace(" ", " "))) if self.__upgradeAllButton is not None: self.__upgradeAllButton.setEnabled(False) else: if self.__upgradeAllButton is not None: self.__upgradeAllButton.setEnabled(True) self.packageList.sortItems( 0, self.packageList.header().sortIndicatorOrder()) self.packageList.header().resizeSections( QHeaderView.ResizeToContents) self.packageList.header().setStretchLastSection(True) @pyqtSlot(QAbstractButton) def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Close): self.close() elif button == self.buttonBox.button(QDialogButtonBox.Cancel): self.__finish() elif button == self.__refreshButton: self.__refresh() elif button == self.__upgradeButton: self.__upgradePackages() elif button == self.__upgradeAllButton: self.__upgradeAllPackages() elif button == self.__uninstallButton: self.__uninstallPackages() def __procFinished(self, exitCode, exitStatus): """ Private slot connected to the finished signal. @param exitCode exit code of the process (integer) @param exitStatus exit status of the process (QProcess.ExitStatus) """ self.__finish() def __refresh(self): """ Private slot to refresh the displayed list. """ self.__stopProcess() self.start() def start(self): """ Public method to start the command. """ self.packageList.clear() self.__errors = "" self.__output = [] self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.__refreshButton.setEnabled(False) if self.__upgradeAllButton is not None: self.__upgradeAllButton.setEnabled(False) QApplication.processEvents() QApplication.setOverrideCursor(Qt.WaitCursor) QApplication.processEvents() command = self.pipComboBox.currentText() if command == self.__default: command = self.__defaultCommand args = PipListDialog.CommandArguments[self.__mode][:] if self.localCheckBox.isChecked(): args.append("--local") if self.notRequiredCheckBox.isChecked(): args.append("--not-required") if self.userCheckBox.isChecked(): args.append("--user") if self.__indexUrl: args.append("--index-url") args.append(self.__indexUrl + "/simple") self.process.start(command, args) procStarted = self.process.waitForStarted(5000) if not procStarted: self.buttonBox.setFocus() self.__stopProcess() E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started.' ).format(command)) self.__finish() def __processOutput(self): """ Private method to process the captured output. """ if self.__output: try: packageData = json.loads("\n".join(self.__output)) for package in packageData: data = [ package["name"], package["version"], ] if self.__mode == "outdated": data.extend([ package["latest_version"], package["latest_filetype"], ]) QTreeWidgetItem(self.packageList, data) except ValueError as err: self.__errors += str(err) + "\n" self.__errors += "received output:\n" self.__errors += "\n".join(self.__output) def __readStdout(self): """ Private slot to handle the readyReadStandardOutput signal. It reads the output of the process, formats it and inserts it into the contents pane. """ self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): line = str(self.process.readLine(), self.__ioEncoding, 'replace').strip() self.__output.append(line) def __readStderr(self): """ Private slot to handle the readyReadStandardError signal. It reads the error output of the process and inserts it into the error pane. """ self.__errors += str(self.process.readAllStandardError(), self.__ioEncoding, 'replace') @pyqtSlot(str) def on_pipComboBox_activated(self, txt): """ Private slot handling the selection of a pip executable. @param txt path of the pip executable (string) """ self.__refresh() @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.__refresh() @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.__refresh() @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.__refresh() @pyqtSlot() def on_packageList_itemSelectionChanged(self): """ Private slot handling the selection of a package. """ self.infoWidget.clear() if len(self.packageList.selectedItems()) == 1: itm = self.packageList.selectedItems()[0] command = self.pipComboBox.currentText() if command == self.__default: command = "" QApplication.setOverrideCursor(Qt.WaitCursor) args = ["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, cmd=command) if success and output: mode = PipListDialog.ShowProcessGeneralMode for line in output.splitlines(): line = line.rstrip() if line != "---": if mode != PipListDialog.ShowProcessGeneralMode: if line[0] == " ": QTreeWidgetItem( self.infoWidget, [" ", line.strip()]) else: mode = PipListDialog.ShowProcessGeneralMode if mode == PipListDialog.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 = PipListDialog.ShowProcessFilesListMode elif label == "classifiers": mode = PipListDialog.ShowProcessClassifiersMode elif label == "entry-points": mode = PipListDialog.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() enable = (len(self.packageList.selectedItems()) > 1 or (len(self.packageList.selectedItems()) == 1 and self.packageList.selectedItems()[0].text(0) not in self.__nothingStrings.values())) self.__upgradeButton and self.__upgradeButton.setEnabled(enable) self.__uninstallButton.setEnabled(enable) @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_packageList_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_packageList_itemSelectionChanged() def __upgradePackages(self): """ Private slot to upgrade the selected packages. """ packages = [] for itm in self.packageList.selectedItems(): packages.append(itm.text(0)) if packages: if "pip" in packages: self.__upgradePip() else: self.__executeUpgradePackages(packages) def __upgradeAllPackages(self): """ Private slot to upgrade all listed packages. """ packages = [] for index in range(self.packageList.topLevelItemCount()): itm = self.packageList.topLevelItem(index) packages.append(itm.text(0)) if packages: if "pip" in packages: self.__upgradePip() else: self.__executeUpgradePackages(packages) def __upgradePip(self): """ Private slot to upgrade pip itself. """ pip = self.pipComboBox.currentText() if pip == self.__default: pip = "" res = self.__pip.upgradePip( pip=pip, userSite=self.userCheckBox.isChecked()) if res: self.__refresh() 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 """ command = self.pipComboBox.currentText() if command == self.__default: command = "" res = self.__pip.upgradePackages( packages, cmd=command, userSite=self.userCheckBox.isChecked()) if res: self.__refresh() def __uninstallPackages(self): """ Private slot to uninstall the selected packages. """ packages = [] for itm in self.packageList.selectedItems(): packages.append(itm.text(0)) if packages: command = self.pipComboBox.currentText() if command == self.__default: command = "" res = self.__pip.uninstallPackages(packages, cmd=command) if res: self.__refresh()