Wed, 20 Feb 2019 19:44:13 +0100
PipInterface: continued with the pip interface widget.
--- a/PipInterface/Pip.py Tue Feb 19 19:56:24 2019 +0100 +++ b/PipInterface/Pip.py Wed Feb 20 19:44:13 2019 +0100 @@ -25,7 +25,6 @@ from E5Gui.E5Application import e5App from .PipDialog import PipDialog -from . import DefaultIndexUrlXml import Preferences import Globals @@ -37,6 +36,10 @@ """ Class implementing the pip GUI logic. """ + DefaultPyPiUrl = "https://pypi.org" + DefaultIndexUrlXml = DefaultPyPiUrl + "/pypi" + DefaultIndexUrlPip = DefaultPyPiUrl + "/simple" + def __init__(self, parent=None): """ Constructor @@ -45,12 +48,12 @@ @type QObject """ super(Pip, self).__init__(parent) - +## ## self.__virtualenvManager = e5App().getObject("VirtualEnvManager") ## self.__project = e5App().getObject("Project") - - self.__menus = {} # dictionary with references to menus - +## +## self.__menus = {} # dictionary with references to menus +## ## self.__plugin.currentEnvironmentChanged.connect( ## self.__handleTearOffMenu) @@ -60,21 +63,21 @@ """ self.actions = [] - self.selectEnvironmentAct = E5Action( - self.tr('Virtual Environment for pip'), - self.tr('&Virtual Environment for pip'), - 0, 0, - self, 'pip_select_environment') - self.selectEnvironmentAct.setStatusTip(self.tr( - 'Selects the virtual environment to be used for pip')) - self.selectEnvironmentAct.setWhatsThis(self.tr( - """<b>Virtual Environment for pip</b>""" - """<p>This selects the virtual environment to be used for pip.""" - """</p>""" - )) - self.selectEnvironmentAct.triggered.connect(self.__selectPipVirtualenv) - self.actions.append(self.selectEnvironmentAct) - +## self.selectEnvironmentAct = E5Action( +## self.tr('Virtual Environment for pip'), +## self.tr('&Virtual Environment for pip'), +## 0, 0, +## self, 'pip_select_environment') +## self.selectEnvironmentAct.setStatusTip(self.tr( +## 'Selects the virtual environment to be used for pip')) +## self.selectEnvironmentAct.setWhatsThis(self.tr( +## """<b>Virtual Environment for pip</b>""" +## """<p>This selects the virtual environment to be used for pip.""" +## """</p>""" +## )) +## self.selectEnvironmentAct.triggered.connect(self.__selectPipVirtualenv) +## self.actions.append(self.selectEnvironmentAct) +## ## ############################################## ## ## Actions for listing packages ## ############################################## @@ -284,25 +287,25 @@ self.__generateRequirements) self.actions.append(self.generateRequirementsAct) - ############################################## - ## Actions for generating requirements files - ############################################## - - self.searchPyPIAct = E5Action( - self.tr('Search PyPI'), - self.tr('&Search PyPI...'), - 0, 0, - self, 'pip_search_pypi') - self.searchPyPIAct.setStatusTip(self.tr( - 'Open a dialog to search the Python Package Index')) - self.searchPyPIAct.setWhatsThis(self.tr( - """<b>Search PyPI</b>""" - """<p>This opens a dialog to search the Python Package""" - """ Index.</p>""" - )) - self.searchPyPIAct.triggered.connect(self.__searchPyPI) - self.actions.append(self.searchPyPIAct) - +## ############################################## +## ## Actions for generating requirements files +## ############################################## +## +## self.searchPyPIAct = E5Action( +## self.tr('Search PyPI'), +## self.tr('&Search PyPI...'), +## 0, 0, +## self, 'pip_search_pypi') +## self.searchPyPIAct.setStatusTip(self.tr( +## 'Open a dialog to search the Python Package Index')) +## self.searchPyPIAct.setWhatsThis(self.tr( +## """<b>Search PyPI</b>""" +## """<p>This opens a dialog to search the Python Package""" +## """ Index.</p>""" +## )) +## self.searchPyPIAct.triggered.connect(self.__searchPyPI) +## self.actions.append(self.searchPyPIAct) +## ############################################## ## Actions for editing configuration files ############################################## @@ -361,14 +364,14 @@ @return the menu generated @rtype QMenu """ - self.__menus = {} # clear menus references - +## self.__menus = {} # clear menus references +## menu = QMenu() ## menu.setTearOffEnabled(True) ## menu.setIcon(UI.PixmapCache.getIcon("pypi.png")) - - menu.addAction(self.selectEnvironmentAct) - menu.addSeparator() +## +## menu.addAction(self.selectEnvironmentAct) +## menu.addSeparator() ## menu.addAction(self.listPackagesAct) ## menu.addAction(self.listUptodatePackagesAct) ## menu.addAction(self.listOutdatedPackagesAct) @@ -396,8 +399,8 @@ menu.addSeparator() menu.addAction(self.pipConfigAct) - self.__menus["main"] = menu - +## self.__menus["main"] = menu +## menu.aboutToShow.connect(self.__aboutToShowMenu) return menu @@ -414,44 +417,44 @@ self.editVirtualenvConfigAct, self.pipConfigAct]: act.setEnabled(enable) - - def getMenu(self, name): - """ - Public method to get a reference to the requested menu. - - @param name name of the menu - @type str - @return reference to the menu or None, if no - menu with the given name exists - @rtype QMenu or None - """ - if name in self.__menus: - return self.__menus[name] - else: - return None - - def getMenuNames(self): - """ - Public method to get the names of all menus. - - @return menu names - @rtype list of str - """ - return list(self.__menus.keys()) - - def __handleTearOffMenu(self, venvName): - """ - Private slot to handle a change of the selected virtual environment. - - @param venvName logical name of the virtual environment - @type str - """ - if self.__menus["main"].isTearOffMenuVisible(): - # determine, if torn off menu needs to be refreshed - enabled = self.listPackagesAct.isEnabled() - if ((bool(venvName) and not enabled) or - (not bool(venvName) and enabled)): - self.__menus["main"].hideTearOffMenu() +## +## def getMenu(self, name): +## """ +## Public method to get a reference to the requested menu. +## +## @param name name of the menu +## @type str +## @return reference to the menu or None, if no +## menu with the given name exists +## @rtype QMenu or None +## """ +## if name in self.__menus: +## return self.__menus[name] +## else: +## return None +## +## def getMenuNames(self): +## """ +## Public method to get the names of all menus. +## +## @return menu names +## @rtype list of str +## """ +## return list(self.__menus.keys()) +## +## def __handleTearOffMenu(self, venvName): +## """ +## Private slot to handle a change of the selected virtual environment. +## +## @param venvName logical name of the virtual environment +## @type str +## """ +## if self.__menus["main"].isTearOffMenuVisible(): +## # determine, if torn off menu needs to be refreshed +## enabled = self.listPackagesAct.isEnabled() +## if ((bool(venvName) and not enabled) or +## (not bool(venvName) and enabled)): +## self.__menus["main"].hideTearOffMenu() ########################################################################## ## Methods below implement some utility functions @@ -559,15 +562,15 @@ return config - def getDefaultEnvironmentString(self): - """ - Public method to get the string for the default environment. - - @return string for the default environment - @rtype str - """ - return self.tr("<standard>") - +## def getDefaultEnvironmentString(self): +## """ +## Public method to get the string for the default environment. +## +## @return string for the default environment +## @rtype str +## """ +## return self.tr("<standard>") +## def getProjectEnvironmentString(self): """ Public method to get the string for the project environment. @@ -589,14 +592,16 @@ @return interpreter path @rtype str """ - if venvName == self.getDefaultEnvironmentString(): - venvName = Preferences.getPip("CurrentEnvironment") - elif venvName == self.getProjectEnvironmentString(): +## if venvName == self.getDefaultEnvironmentString(): +## venvName = Preferences.getPip("CurrentEnvironment") + if venvName == self.getProjectEnvironmentString(): venvName = \ e5App().getObject("Project").getDebugProperty("VIRTUALENV") if not venvName: - # fall back to standard if not defined - venvName = Preferences.getPip("CurrentEnvironment") + # fall back to interpreter used to run eric6 + return sys.executable +## # fall back to standard if not defined +## venvName = Preferences.getPip("CurrentEnvironment") interpreter = \ e5App().getObject("VirtualEnvManager").getVirtualenvInterpreter( @@ -624,66 +629,65 @@ ## Methods below implement the individual menu entries ########################################################################## - def __selectPipVirtualenv(self): - """ - Private method to select the virtual environment to be used. - """ - environments = self.getVirtualenvNames() - if environments: - currentEnvironment = Preferences.getPip("CurrentEnvironment") - try: - index = environments.index(currentEnvironment) - except ValueError: - index = 0 - environment, ok = QInputDialog.getItem( - None, - self.tr("Virtual Environment for pip"), - self.tr("Select the virtual environment to be used:"), - environments, index, False) - - if ok and environment: - Preferences.getPip("CurrentEnvironment", environment) - else: - E5MessageBox.warning( - None, - self.tr("Virtual Environment for pip"), - self.tr("""No virtual environments have been configured yet.""" - """ Please use the Virtualenv Manager to do that.""")) - - # TODO: move these three to the widget - def __listPackages(self): - """ - Private slot to list all installed packages. - """ - from .PipListDialog import PipListDialog - self.__listDialog = PipListDialog( - self, "list", Preferences.getPip("PipSearchIndex"), - self.tr("Installed Packages")) - self.__listDialog.show() - self.__listDialog.start() - - def __listUptodatePackages(self): - """ - Private slot to list all installed, up-to-date packages. - """ - from .PipListDialog import PipListDialog - self.__listUptodateDialog = PipListDialog( - self, "uptodate", Preferences.getPip("PipSearchIndex"), - self.tr("Up-to-date Packages")) - self.__listUptodateDialog.show() - self.__listUptodateDialog.start() - - def __listOutdatedPackages(self): - """ - Private slot to list all installed, up-to-date packages. - """ - from .PipListDialog import PipListDialog - self.__listOutdatedDialog = PipListDialog( - self, "outdated", Preferences.getPip("PipSearchIndex"), - self.tr("Outdated Packages")) - self.__listOutdatedDialog.show() - self.__listOutdatedDialog.start() - +## def __selectPipVirtualenv(self): +## """ +## Private method to select the virtual environment to be used. +## """ +## environments = self.getVirtualenvNames() +## if environments: +## currentEnvironment = Preferences.getPip("CurrentEnvironment") +## try: +## index = environments.index(currentEnvironment) +## except ValueError: +## index = 0 +## environment, ok = QInputDialog.getItem( +## None, +## self.tr("Virtual Environment for pip"), +## self.tr("Select the virtual environment to be used:"), +## environments, index, False) +## +## if ok and environment: +## Preferences.getPip("CurrentEnvironment", environment) +## else: +## E5MessageBox.warning( +## None, +## self.tr("Virtual Environment for pip"), +## self.tr("""No virtual environments have been configured yet.""" +## """ Please use the Virtualenv Manager to do that.""")) +## +## def __listPackages(self): +## """ +## Private slot to list all installed packages. +## """ +## from .PipListDialog import PipListDialog +## self.__listDialog = PipListDialog( +## self, "list", Preferences.getPip("PipSearchIndex"), +## self.tr("Installed Packages")) +## self.__listDialog.show() +## self.__listDialog.start() +## +## def __listUptodatePackages(self): +## """ +## Private slot to list all installed, up-to-date packages. +## """ +## from .PipListDialog import PipListDialog +## self.__listUptodateDialog = PipListDialog( +## self, "uptodate", Preferences.getPip("PipSearchIndex"), +## self.tr("Up-to-date Packages")) +## self.__listUptodateDialog.show() +## self.__listUptodateDialog.start() +## +## def __listOutdatedPackages(self): +## """ +## Private slot to list all installed, up-to-date packages. +## """ +## from .PipListDialog import PipListDialog +## self.__listOutdatedDialog = PipListDialog( +## self, "outdated", Preferences.getPip("PipSearchIndex"), +## self.tr("Outdated Packages")) +## self.__listOutdatedDialog.show() +## self.__listOutdatedDialog.start() +## def __editUserConfiguration(self): """ Private slot to edit the user configuration. @@ -1120,19 +1124,47 @@ self.__freezeDialog.show() self.__freezeDialog.start() - def __searchPyPI(self): +## def __searchPyPI(self): +## """ +## Private slot to search the Python Package Index. +## """ +## from .PipSearchDialog import PipSearchDialog +## +## if Preferences.getPip("PipSearchIndex"): +## indexUrl = Preferences.getPip("PipSearchIndex") + "/pypi" +## else: +## indexUrl = DefaultIndexUrlXml +## +## self.__searchDialog = PipSearchDialog(self, indexUrl) +## self.__searchDialog.show() +## + def getIndexUrl(self): """ - Private slot to search the Python Package Index. + Public method to get the index URL for PyPI. + + @return index URL for PyPI + @rtype str """ - from .PipSearchDialog import PipSearchDialog + if Preferences.getPip("PipSearchIndex"): + indexUrl = Preferences.getPip("PipSearchIndex") + "/simple" + else: + indexUrl = Pip.DefaultIndexUrlPip + return indexUrl + + def getIndexUrlXml(self): + """ + Public method to get the index URL for XML RPC calls. + + @return index URL for XML RPC calls + @rtype str + """ if Preferences.getPip("PipSearchIndex"): indexUrl = Preferences.getPip("PipSearchIndex") + "/pypi" else: - indexUrl = DefaultIndexUrlXml + indexUrl = Pip.DefaultIndexUrlXml - self.__searchDialog = PipSearchDialog(self, indexUrl) - self.__searchDialog.show() + return indexUrl def getInstalledPackages(self, envName, localPackages=True, notRequired=False, usersite=False):
--- 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
--- a/PipInterface/__init__.py Tue Feb 19 19:56:24 2019 +0100 +++ b/PipInterface/__init__.py Wed Feb 20 19:44:13 2019 +0100 @@ -6,9 +6,3 @@ """ Package implementing the various pip dialogs and data. """ - -from __future__ import unicode_literals - -DefaultPyPiUrl = "https://pypi.org" -DefaultIndexUrlXml = DefaultPyPiUrl + "/pypi" -DefaultIndexUrlPip = DefaultPyPiUrl + "/simple"