--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/CondaInterface/CondaPackagesWidget.py Sun Apr 14 15:09:21 2019 +0200 @@ -0,0 +1,721 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the conda packages management widget. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import pyqtSlot, Qt +from PyQt5.QtGui import QCursor +from PyQt5.QtWidgets import QWidget, QToolButton, QMenu, QTreeWidgetItem, \ + QApplication, QLineEdit, QDialog + +from E5Gui import E5FileDialog, E5MessageBox, E5TextInputDialog +from E5Gui.E5Application import e5App + +from .Ui_CondaPackagesWidget import Ui_CondaPackagesWidget + +import UI.PixmapCache + +import CondaInterface + + +class CondaPackagesWidget(QWidget, Ui_CondaPackagesWidget): + """ + Class implementing the conda packages management widget. + """ + # Role definition of packages list + PackageVersionRole = Qt.UserRole + 1 + PackageBuildRole = Qt.UserRole + 2 + + # Role definitions of search results list + PackageDetailedDataRole = Qt.UserRole + 1 + + def __init__(self, conda, parent=None): + """ + Constructor + + @param conda reference to the conda interface + @type Conda + @param parent reference to the parent widget + @type QWidget + """ + super(CondaPackagesWidget, self).__init__(parent) + self.setupUi(self) + + self.__conda = conda + + if not CondaInterface.isCondaAvailable(): + self.baseWidget.hide() + self.searchWidget.hide() + + else: + self.notAvailableWidget.hide() + + self.__initCondaInterface() + + def __initCondaInterface(self): + """ + Private method to initialize the conda interface elements. + """ + self.statusLabel.hide() + + self.condaMenuButton.setObjectName( + "navigation_supermenu_button") + self.condaMenuButton.setIcon(UI.PixmapCache.getIcon("superMenu")) + self.condaMenuButton.setToolTip(self.tr("Conda Menu")) + self.condaMenuButton.setPopupMode(QToolButton.InstantPopup) + self.condaMenuButton.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.condaMenuButton.setFocusPolicy(Qt.NoFocus) + self.condaMenuButton.setAutoRaise(True) + self.condaMenuButton.setShowMenuInside(True) + + self.refreshButton.setIcon(UI.PixmapCache.getIcon("reload")) + self.upgradeButton.setIcon(UI.PixmapCache.getIcon("1uparrow")) + self.upgradeAllButton.setIcon(UI.PixmapCache.getIcon("2uparrow")) + self.uninstallButton.setIcon(UI.PixmapCache.getIcon("minus")) + self.searchToggleButton.setIcon(UI.PixmapCache.getIcon("find")) + self.searchButton.setIcon(UI.PixmapCache.getIcon("findNext")) + self.installButton.setIcon(UI.PixmapCache.getIcon("plus")) + self.showDetailsButton.setIcon(UI.PixmapCache.getIcon("info")) + + if CondaInterface.condaVersion() >= (4, 4, 0): + self.searchOptionsWidget.hide() + else: + self.platformComboBox.addItems(sorted([ + "", "win-32", "win-64", "osx-64", "linux-32", "linux-64", + ])) + + self.__initCondaMenu() + self.__populateEnvironments() + self.__updateActionButtons() + + self.searchWidget.hide() + + self.__conda.condaEnvironmentCreated.connect( + self.on_refreshButton_clicked) + self.__conda.condaEnvironmentRemoved.connect( + self.on_refreshButton_clicked) + + def __populateEnvironments(self): + """ + Private method to get a list of environments and populate the selector. + """ + environments = [("", "")] + sorted( + self.__conda.getCondaEnvironmentsList()) + for environment in environments: + self.environmentsComboBox.addItem(environment[0], environment[1]) + + def __initCondaMenu(self): + """ + Private method to create the super menu and attach it to the super + menu button. + """ + self.__condaMenu = QMenu(self) + self.__envActs = [] + + self.__cleanMenu = QMenu(self.tr("Clean"), self) + self.__cleanMenu.addAction( + self.tr("All"), lambda: self.__conda.cleanConda("all")) + self.__cleanMenu.addAction( + self.tr("Cache"), lambda: self.__conda.cleanConda("index-cache")) + self.__cleanMenu.addAction( + self.tr("Lock Files"), + lambda: self.__conda.cleanConda("lock")) + self.__cleanMenu.addAction( + self.tr("Packages"), lambda: self.__conda.cleanConda("packages")) + self.__cleanMenu.addAction( + self.tr("Tarballs"), lambda: self.__conda.cleanConda("tarballs")) + + self.__condaMenu.addAction( + self.tr("About Conda..."), self.__aboutConda) + self.__condaMenu.addSeparator() + self.__condaMenu.addAction( + self.tr("Update Conda"), self.__conda.updateConda) + self.__condaMenu.addSeparator() + self.__envActs.append(self.__condaMenu.addAction( + self.tr("Install Packages"), self.__installPackages)) + self.__envActs.append(self.__condaMenu.addAction( + self.tr("Install Requirements"), self.__installRequirements)) + self.__condaMenu.addSeparator() + self.__envActs.append(self.__condaMenu.addAction( + self.tr("Generate Requirements"), self.__generateRequirements)) + self.__condaMenu.addSeparator() + self.__condaMenu.addAction( + self.tr("Create Environment from Requirements"), + self.__createEnvironment) + self.__envActs.append(self.__condaMenu.addAction( + self.tr("Clone Environment"), self.__cloneEnvironment)) + self.__deleteEnvAct = self.__condaMenu.addAction( + self.tr("Delete Environment"), self.__deleteEnvironment) + self.__condaMenu.addSeparator() + self.__condaMenu.addMenu(self.__cleanMenu) + self.__condaMenu.addSeparator() + self.__condaMenu.addAction( + self.tr("Edit User Configuration..."), + self.__editUserConfiguration) + self.__condaMenu.addSeparator() + self.__condaMenu.addAction( + self.tr("Configure..."), self.__condaConfigure) + + self.condaMenuButton.setMenu(self.__condaMenu) + + self.__condaMenu.aboutToShow.connect(self.__aboutToShowCondaMenu) + + 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. + """ + self.upgradeButton.setEnabled( + bool(self.__selectedUpdateableItems())) + self.uninstallButton.setEnabled( + bool(self.packagesList.selectedItems())) + self.upgradeAllButton.setEnabled( + bool(self.__allUpdateableItems())) + + @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.packagesList.clear() + prefix = self.environmentsComboBox.itemData(index) + if prefix: + 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.__conda.getInstalledPackages(prefix=prefix) + for package, version, build in installedPackages: + itm = QTreeWidgetItem(self.packagesList, [package, version]) + itm.setData(1, self.PackageVersionRole, version) + itm.setData(1, self.PackageBuildRole, build) + self.packagesList.setUpdatesEnabled(True) + self.statusLabel.setText(self.tr("Getting outdated packages...")) + QApplication.processEvents() + + # 2. update with update information + self.packagesList.setUpdatesEnabled(False) + updateablePackages = \ + self.__conda.getUpdateablePackages(prefix=prefix) + for package, version, build in updateablePackages: + items = self.packagesList.findItems( + package, Qt.MatchExactly | Qt.MatchCaseSensitive) + if items: + itm = items[0] + itm.setText(2, version) + itm.setData(2, self.PackageVersionRole, version) + itm.setData(2, self.PackageBuildRole, build) + if itm.data(1, self.PackageVersionRole) == version: + # build must be different, show in version display + itm.setText(1, self.tr("{0} (Build: {1})").format( + itm.data(1, self.PackageVersionRole), + itm.data(1, self.PackageBuildRole), + )) + itm.setText(2, self.tr("{0} (Build: {1})").format( + itm.data(2, self.PackageVersionRole), + itm.data(2, self.PackageBuildRole), + )) + + 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() + + @pyqtSlot() + def on_packagesList_itemSelectionChanged(self): + """ + Private slot to handle the selection of some items.. + """ + self.__updateActionButtons() + + @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: + prefix = self.environmentsComboBox.itemData( + self.environmentsComboBox.currentIndex()) + ok = self.__conda.updatePackages(packages, prefix=prefix) + if ok: + self.on_refreshButton_clicked() + + @pyqtSlot() + def on_upgradeAllButton_clicked(self): + """ + Private slot to upgrade all packages of the selected environment. + """ + prefix = self.environmentsComboBox.itemData( + self.environmentsComboBox.currentIndex()) + ok = self.__conda.updateAllPackages(prefix=prefix) + 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: + prefix = self.environmentsComboBox.itemData( + self.environmentsComboBox.currentIndex()) + ok = self.__conda.uninstallPackages(packages, prefix=prefix) + if ok: + self.on_refreshButton_clicked() + + ####################################################################### + ## Search widget related methods below + ####################################################################### + + def __updateSearchActionButtons(self): + """ + Private method to update the action button states of the search widget. + """ + enable = len(self.searchResultList.selectedItems()) == 1 + self.installButton.setEnabled( + enable and self.environmentsComboBox.currentIndex() > 0) + self.showDetailsButton.setEnabled( + enable and bool(self.searchResultList.selectedItems()[0].parent())) + + def __doSearch(self): + """ + Private method to search for packages. + """ + self.searchResultList.clear() + pattern = self.searchEdit.text() + if pattern: + QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) + QApplication.processEvents() + + if CondaInterface.condaVersion() >= (4, 4, 0): + prefix = "" + else: + prefix = self.environmentsComboBox.itemData( + self.environmentsComboBox.currentIndex()) + ok, result = self.__conda.searchPackages( + pattern, + fullNameOnly=self.fullNameButton.isChecked(), + packageSpec=self.packageSpecButton.isChecked(), + platform=self.platformComboBox.currentText(), + prefix=prefix, + ) + + if result: + if ok: + self.searchResultList.setUpdatesEnabled(False) + for package in result: + itm = QTreeWidgetItem(self.searchResultList, [package]) + itm.setExpanded(False) + for detail in result[package]: + version = detail["version"] + build = detail["build"] + if "subdir" in detail: + platform = detail["subdir"] + elif "platform" in detail: + platform = detail["platform"] + else: + platform = "" + citm = QTreeWidgetItem( + itm, ["", version, build, platform]) + citm.setData(0, self.PackageDetailedDataRole, + detail) + + self.searchResultList.sortItems(0, Qt.AscendingOrder) + self.searchResultList.resizeColumnToContents(0) + self.searchResultList.setUpdatesEnabled(True) + else: + QApplication.restoreOverrideCursor() + try: + message = result["message"] + except KeyError: + message = result["error"] + E5MessageBox.warning( + self, + self.tr("Conda Search Package Error"), + message) + QApplication.restoreOverrideCursor() + + def __showDetails(self, item): + """ + Private method to show a dialog with details about a package item. + + @param item reference to the package item + @type QTreeWidgetItem + """ + details = item.data(0, self.PackageDetailedDataRole) + if details: + from .CondaPackageDetailsWidget import CondaPackageDetailsDialog + dlg = CondaPackageDetailsDialog(details, self) + dlg.exec_() + + @pyqtSlot(str) + def on_searchEdit_textChanged(self, txt): + """ + Private slot handling changes of the entered search specification. + + @param txt current search entry + @type str + """ + self.searchButton.setEnabled(bool(txt)) + + @pyqtSlot() + def on_searchEdit_returnPressed(self): + """ + Private slot handling the user pressing the Return button in the + search edit. + """ + self.__doSearch() + + @pyqtSlot() + def on_searchButton_clicked(self): + """ + Private slot handling the press of the search button. + """ + self.__doSearch() + + @pyqtSlot() + def on_installButton_clicked(self): + """ + Private slot to install a selected package. + """ + if len(self.searchResultList.selectedItems()) == 1: + item = self.searchResultList.selectedItems()[0] + if item.parent() is None: + # it is just the package item + package = item.text(0) + else: + # item with version and build + package = "{0}={1}={2}".format( + item.parent().text(0), + item.text(1), + item.text(2), + ) + + prefix = self.environmentsComboBox.itemData( + self.environmentsComboBox.currentIndex()) + ok = self.__conda.installPackages([package], prefix=prefix) + if ok: + self.on_refreshButton_clicked() + + @pyqtSlot() + def on_showDetailsButton_clicked(self): + """ + Private slot handling the 'Show Details' button. + """ + item = self.searchResultList.selectedItems()[0] + self.__showDetails(item) + + @pyqtSlot() + def on_searchResultList_itemSelectionChanged(self): + """ + Private slot handling a change of selected search results. + """ + self.__updateSearchActionButtons() + + @pyqtSlot(QTreeWidgetItem) + def on_searchResultList_itemExpanded(self, item): + """ + Private slot handling the expansion of an item. + + @param item reference to the expanded item + @type QTreeWidgetItem + """ + for col in range(1, self.searchResultList.columnCount()): + self.searchResultList.resizeColumnToContents(col) + + @pyqtSlot(QTreeWidgetItem, int) + def on_searchResultList_itemDoubleClicked(self, item, column): + """ + Private slot handling a double click of an item. + + @param item reference to the item that was double clicked + @type QTreeWidgetItem + @param column column of the double click + @type int + """ + if item.parent() is not None: + self.__showDetails(item) + + @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() + + ####################################################################### + ## Menu related methods below + ####################################################################### + + @pyqtSlot() + def __aboutToShowCondaMenu(self): + """ + Private slot to handle the conda menu about to be shown. + """ + selectedEnvironment = self.environmentsComboBox.currentText() + enable = selectedEnvironment not in [""] + for act in self.__envActs: + act.setEnabled(enable) + + self.__deleteEnvAct.setEnabled( + selectedEnvironment not in ["", self.__conda.RootName]) + + @pyqtSlot() + def __aboutConda(self): + """ + Private slot to show some information about the conda installation. + """ + infoDict = self.__conda.getCondaInformation() + + from .CondaInfoDialog import CondaInfoDialog + dlg = CondaInfoDialog(infoDict, self) + dlg.exec_() + + @pyqtSlot() + def __installPackages(self): + """ + Private slot to install packages. + """ + prefix = self.environmentsComboBox.itemData( + self.environmentsComboBox.currentIndex()) + if prefix: + ok, packageSpecs = E5TextInputDialog.getText( + self, + self.tr("Install Packages"), + self.tr("Package Specifications (separated by whitespace):"), + QLineEdit.Normal, + minimumWidth=600) + if ok and packageSpecs.strip(): + packages = [p.strip() for p in packageSpecs.split()] + ok = self.__conda.installPackages(packages, prefix=prefix) + if ok: + self.on_refreshButton_clicked() + + @pyqtSlot() + def __installRequirements(self): + """ + Private slot to install packages from requirements files. + """ + prefix = self.environmentsComboBox.itemData( + self.environmentsComboBox.currentIndex()) + if prefix: + requirements = E5FileDialog.getOpenFileNames( + self, + self.tr("Install Packages"), + "", + self.tr("Text Files (*.txt);;All Files (*)")) + if requirements: + args = [] + for requirement in requirements: + args.extend(["--file", requirement]) + ok = self.__conda.installPackages(args, prefix=prefix) + if ok: + self.on_refreshButton_clicked() + + @pyqtSlot() + def __generateRequirements(self): + """ + Private slot to generate a requirements file. + """ + prefix = self.environmentsComboBox.itemData( + self.environmentsComboBox.currentIndex()) + if prefix: + env = self.environmentsComboBox.currentText() + + from .CondaExportDialog import CondaExportDialog + + self.__requirementsDialog = CondaExportDialog( + self.__conda, env, prefix) + self.__requirementsDialog.show() + QApplication.processEvents() + self.__requirementsDialog.start() + + @pyqtSlot() + def __cloneEnvironment(self): + """ + Private slot to clone a conda environment. + """ + from .CondaNewEnvironmentDataDialog import \ + CondaNewEnvironmentDataDialog + + prefix = self.environmentsComboBox.itemData( + self.environmentsComboBox.currentIndex()) + if prefix: + dlg = CondaNewEnvironmentDataDialog(self.tr("Clone Environment"), + False, self) + if dlg.exec_() == QDialog.Accepted: + virtEnvName, envName, _ = dlg.getData() + args = [ + "--name", + envName.strip(), + "--clone", + prefix, + ] + ok, prefix, interpreter = \ + self.__conda.createCondaEnvironment(args) + if ok: + e5App().getObject("VirtualEnvManager").addVirtualEnv( + virtEnvName, prefix, interpreter, isConda=True) + + @pyqtSlot() + def __createEnvironment(self): + """ + Private slot to create a conda environment from a requirements file. + """ + from .CondaNewEnvironmentDataDialog import \ + CondaNewEnvironmentDataDialog + + dlg = CondaNewEnvironmentDataDialog(self.tr("Create Environment"), + True, self) + if dlg.exec_() == QDialog.Accepted: + virtEnvName, envName, requirements = dlg.getData() + args = [ + "--name", + envName.strip(), + "--file", + requirements, + ] + ok, prefix, interpreter = \ + self.__conda.createCondaEnvironment(args) + if ok: + e5App().getObject("VirtualEnvManager").addVirtualEnv( + virtEnvName, prefix, interpreter, isConda=True) + + @pyqtSlot() + def __deleteEnvironment(self): + """ + Private slot to delete a conda environment. + """ + envName = self.environmentsComboBox.currentText() + ok = E5MessageBox.yesNo( + self, + self.tr("Delete Environment"), + self.tr("""<p>Shall the environment <b>{0}</b> really be""" + """ deleted?</p>""").format(envName) + ) + if ok: + self.__conda.removeCondaEnvironment(name=envName) + + @pyqtSlot() + def __editUserConfiguration(self): + """ + Private slot to edit the user configuration. + """ + from QScintilla.MiniEditor import MiniEditor + + cfgFile = CondaInterface.userConfiguration() + if not cfgFile: + return + + if not os.path.exists(cfgFile): + self.__conda.writeDefaultConfiguration() + + # check, if the destination is writeable + if not os.access(cfgFile, os.W_OK): + E5MessageBox.critical( + None, + self.tr("Edit Configuration"), + self.tr("""The configuration file "{0}" does not exist""" + """ or is not writable.""")) + return + + self.__editor = MiniEditor(cfgFile, "YAML") + self.__editor.show() + + @pyqtSlot() + def __condaConfigure(self): + """ + Private slot to open the configuration page. + """ + e5App().getObject("UserInterface").showPreferences("condaPage") + + @pyqtSlot() + def on_recheckButton_clicked(self): + """ + Private slot to re-check the availability of conda and adjust the + interface if it became available. + """ + if CondaInterface.isCondaAvailable(): + self.__initCondaInterface() + + self.notAvailableWidget.hide() + self.baseWidget.show()