PipInterface/PipPackagesWidget.py

Wed, 20 Feb 2019 19:44:13 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Wed, 20 Feb 2019 19:44:13 +0100
branch
pypi
changeset 6793
cca6a35f3ad2
parent 6792
9dd854f05c83
child 6795
6e2ed2aac325
permissions
-rw-r--r--

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)

eric ide

mercurial