--- a/Plugins/UiExtensionPlugins/PipInterface/PipSearchDialog.py Sat Feb 23 13:05:18 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,465 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2015 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> -# - -""" -Module implementing a dialog to search PyPI. -""" - -from __future__ import unicode_literals - -import textwrap - -from PyQt5.QtCore import pyqtSlot, Qt, QEventLoop, QRegExp -from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QAbstractButton, \ - QApplication, QTreeWidgetItem, QHeaderView, QInputDialog - -from E5Gui import E5MessageBox -try: - from E5Network.E5XmlRpcClient import E5XmlRpcClient -except ImportError: - from .E5XmlRpcClient import E5XmlRpcClient - -from .Ui_PipSearchDialog import Ui_PipSearchDialog - - -class PipSearchDialog(QDialog, Ui_PipSearchDialog): - """ - Class implementing a dialog to search PyPI. - """ - VersionRole = Qt.UserRole + 1 - - Stopwords = { - "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", - } - - def __init__(self, pip, indexUrl, parent=None): - """ - Constructor - - @param pip reference to the master object - @type Pip - @param indexUrl URL of XML RPC interface to the pypi index - @type str - @param parent reference to the parent widget - @type QWidget - """ - super(PipSearchDialog, self).__init__(parent) - self.setupUi(self) - self.setWindowFlags(Qt.Window) - - self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) - - self.__installButton = self.buttonBox.addButton( - self.tr("&Install"), QDialogButtonBox.ActionRole) - self.__installButton.setEnabled(False) - - self.__installUserButton = self.buttonBox.addButton( - self.tr("Install to &User-Site"), QDialogButtonBox.ActionRole) - self.__installUserButton.setEnabled(False) - - self.__showDetailsButton = self.buttonBox.addButton( - self.tr("&Show Details..."), QDialogButtonBox.ActionRole) - self.__showDetailsButton.setEnabled(False) - - self.__pip = pip - self.__client = E5XmlRpcClient(indexUrl, self) - - self.venvComboBox.addItem(self.__pip.getDefaultEnvironmentString()) - projectVenv = self.__pip.getProjectEnvironmentString() - if projectVenv: - self.venvComboBox.addItem(projectVenv) - self.venvComboBox.addItems(self.__pip.getVirtualenvNames()) - - self.searchEdit.setFocus(Qt.OtherFocusReason) - - self.__canceled = False - self.__detailsData = {} - self.__query = [] - - self.__packageDetailsDialog = None - - def closeEvent(self, e): - """ - Protected slot implementing a close event handler. - - @param e close event - @type QCloseEvent - """ - QApplication.restoreOverrideCursor() - - if self.__packageDetailsDialog is not None: - self.__packageDetailsDialog.close() - - e.accept() - - @pyqtSlot(str) - def on_searchEdit_textChanged(self, txt): - """ - Private slot handling a change of the search term. - - @param txt search term - @type str - """ - self.searchButton.setEnabled(bool(txt)) - - @pyqtSlot() - def on_searchButton_clicked(self): - """ - Private slot handling a press of the search button. - """ - self.__search() - - @pyqtSlot() - def on_resultList_itemSelectionChanged(self): - """ - Private slot handling changes of the selection. - """ - self.__installButton.setEnabled( - len(self.resultList.selectedItems()) > 0) - self.__installUserButton.setEnabled( - len(self.resultList.selectedItems()) > 0) - self.__showDetailsButton.setEnabled( - len(self.resultList.selectedItems()) == 1) - - @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 - @type QAbstractButton - """ - if button == self.buttonBox.button(QDialogButtonBox.Close): - self.close() - elif button == self.buttonBox.button(QDialogButtonBox.Cancel): - self.__client.abort() - self.__canceled = True - elif button == self.__installButton: - self.__install() - elif button == self.__installUserButton: - self.__install(userSite=True) - elif button == self.__showDetailsButton: - self.__showDetails() - - def __search(self): - """ - Private method to perform the search. - """ - self.resultList.clear() - self.infoLabel.clear() - - self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) - self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) - self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) - self.searchButton.setEnabled(False) - QApplication.processEvents(QEventLoop.ExcludeUserInputEvents) - - QApplication.setOverrideCursor(Qt.WaitCursor) - QApplication.processEvents(QEventLoop.ExcludeUserInputEvents) - - self.__canceled = False - - self.__query = [term for term in self.searchEdit.text().strip().split() - if term not in PipSearchDialog.Stopwords] - 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.infoLabel.setText(self.tr("%n package(s) found.", "", - len(packages))) - wrapper = textwrap.TextWrapper(width=80) - count = 0 - total = 0 - for package in packages: - if self.__canceled: - self.infoLabel.setText( - self.tr("Canceled - only {0} out of %n package(s)" - " shown", "", len(packages)).format(total)) - break - itm = QTreeWidgetItem( - self.resultList, [ - package['name'].strip(), - "{0:4d}".format(package['score']), - "\n".join([ - wrapper.fill(line) for line in - package['summary'].strip().splitlines() - ]) - ]) - itm.setData(0, self.VersionRole, 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.infoLabel.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.infoLabel.setText( - self.tr("""<p>The package search did not return anything.""" - """</p>""")) - - header = self.resultList.header() - self.resultList.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.__finish() - - def __finish(self): - """ - Private slot performing the finishing actions. - """ - QApplication.restoreOverrideCursor() - self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) - self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) - self.searchButton.setEnabled(True) - self.searchButton.setDefault(True) - 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.infoLabel.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 - - def __install(self, userSite=False): - """ - Private slot to install the selected packages. - - @param userSite flag indicating to install to the user directory - @type bool - """ - venvName = self.venvComboBox.currentText() - - packages = [] - for itm in self.resultList.selectedItems(): - packages.append(itm.text(0).strip()) - if packages: - self.__pip.installPackages(packages, venvName=venvName, - userSite=userSite) - - def __showDetails(self): - """ - Private slot to show details about the selected package. - """ - self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) - self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True) - self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) - self.__showDetailsButton.setEnabled(False) - QApplication.setOverrideCursor(Qt.WaitCursor) - QApplication.processEvents(QEventLoop.ExcludeUserInputEvents) - - self.__detailsData = {} - - itm = self.resultList.selectedItems()[0] - packageVersions = itm.data(0, self.VersionRole) - if len(packageVersions) == 1: - packageVersion = packageVersions[0] - elif len(packageVersions) == 0: - packageVersion = "" - else: - packageVersion, ok = QInputDialog.getItem( - self, - self.tr("Show Package Details"), - self.tr("Select the package version:"), - packageVersions, - 0, False) - if not ok: - return - - packageName = itm.text(0) - self.__client.call( - "release_data", - (packageName, packageVersion), - lambda d: self.__getPackageDownloadsData(packageVersion, d), - self.__detailsError - ) - - def __getPackageDownloadsData(self, packageVersion, data): - """ - Private method to store the details data and get downloads - information. - - @param packageVersion version info - @type str - @param data result data with package details in the first - element - @type tuple - """ - if data and data[0]: - self.__detailsData = data[0] - itm = self.resultList.selectedItems()[0] - packageName = itm.text(0) - self.__client.call( - "release_urls", - (packageName, packageVersion), - self.__displayPackageDetails, - self.__detailsError - ) - else: - self.__finish() - E5MessageBox.warning( - self, - self.tr("Search PyPI"), - self.tr("""<p>No package details info available.</p>""")) - - def __displayPackageDetails(self, data): - """ - Private method to display the returned package details. - - @param data result data with downloads information in the first element - @type tuple - """ - from .PipPackageDetailsDialog import PipPackageDetailsDialog - - self.__finish() - self.__showDetailsButton.setEnabled(True) - - if self.__packageDetailsDialog is not None: - self.__packageDetailsDialog.close() - - self.__packageDetailsDialog = \ - PipPackageDetailsDialog(self.__detailsData, data[0], self) - self.__packageDetailsDialog.show() - - def __detailsError(self, errorCode, errorString): - """ - Private method handling a details error. - - @param errorCode code of the error - @type int - @param errorString error message - @type str - """ - self.__finish() - self.__showDetailsButton.setEnabled(True) - E5MessageBox.warning( - self, - self.tr("Search PyPI"), - self.tr("""<p>Package details info could not be retrieved.</p>""" - """<p>Reason: {0}</p>""") - .format(errorString)) - - @pyqtSlot(QTreeWidgetItem, int) - def on_resultList_itemActivated(self, item, column): - """ - Private slot reacting on an item activation. - - @param item reference to the activated item - @type QTreeWidgetItem - @param column activated column - @type int - """ - self.__showDetails()