PipInterface/PipPackagesWidget.py

branch
pypi
changeset 6793
cca6a35f3ad2
parent 6792
9dd854f05c83
child 6795
6e2ed2aac325
diff -r 9dd854f05c83 -r cca6a35f3ad2 PipInterface/PipPackagesWidget.py
--- a/PipInterface/PipPackagesWidget.py	Tue Feb 19 19:56:24 2019 +0100
+++ b/PipInterface/PipPackagesWidget.py	Wed Feb 20 19:44:13 2019 +0100
@@ -9,12 +9,17 @@
 
 from __future__ import unicode_literals
 
-from PyQt5.QtCore import pyqtSlot, Qt
+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
 
@@ -32,6 +37,15 @@
     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
@@ -55,6 +69,7 @@
         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)
         
@@ -105,6 +120,23 @@
             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
     #######################################################################
@@ -140,12 +172,17 @@
         """
         Private method to set the state of the action buttons.
         """
-        self.upgradeButton.setEnabled(
-            bool(self.__selectedUpdateableItems()))
-        self.uninstallButton.setEnabled(
-            bool(self.packagesList.selectedItems()))
-        self.upgradeAllButton.setEnabled(
-            bool(self.__allUpdateableItems()))
+        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):
         """
@@ -201,6 +238,7 @@
         
         self.__updateActionButtons()
         self.__updateSearchActionButtons()
+        self.__updateSearchButton()
     
     @pyqtSlot(int)
     def on_environmentsComboBox_currentIndexChanged(self, index):
@@ -409,12 +447,27 @@
         """
         Private method to update the action button states of the search widget.
         """
-        # TODO: adjust this like search dialog
-        enable = len(self.searchResultList.selectedItems()) == 1
-        self.installButton.setEnabled(
-            enable and self.environmentsComboBox.currentIndex() > 0)
+        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(
-            enable and bool(self.searchResultList.selectedItems()[0].parent()))
+            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):
@@ -431,6 +484,206 @@
             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

eric ide

mercurial