Sat, 09 Feb 2019 16:43:49 +0100
Conda: implemented the package search and 'Show Details' functionality.
--- a/CondaInterface/Conda.py Thu Feb 07 18:54:38 2019 +0100 +++ b/CondaInterface/Conda.py Sat Feb 09 16:43:49 2019 +0100 @@ -516,3 +516,70 @@ ok = False return ok + + def searchPackages(self, pattern, fullNameOnly=False, packageSpec=False, + platform="", name="", prefix=""): + """ + Public method to search for a package pattern of a conda environment. + + @param pattern package search pattern + @type str + @param fullNameOnly flag indicating to search for full names only + @type bool + @param packageSpec flag indicating to search a package specification + @type bool + @param platform type of platform to be searched for + @type str + @param name name of the environment + @type str + @param prefix prefix of the environment + @type str + @return flag indicating success and a dictionary with package name as + key and list of dictionaries containing detailed data for the found + packages as values + @rtype tuple of (bool, dict of list of dict) + @exception RuntimeError raised to indicate an error in parameters + + Note: only one of name or prefix must be given. + """ + if name and prefix: + raise RuntimeError("Only one of 'name' or 'prefix' must be given.") + + args = [ + "search", + "--json", + ] + if fullNameOnly: + args.append("--full-name") + if packageSpec: + args.append("--spec") + if platform: + args.extend(["--platform", platform]) + if name: + args.extend(["--name", name]) + elif prefix: + args.extend(["--prefix", prefix]) + args.append(pattern) + + exe = Preferences.getConda("CondaExecutable") + if not exe: + exe = "conda" + + packages = {} + ok = False + + proc = QProcess() + proc.start(exe, args) + if proc.waitForStarted(15000): + if proc.waitForFinished(30000): + output = str(proc.readAllStandardOutput(), + Preferences.getSystem("IOEncoding"), + 'replace').strip() + try: + packages = json.loads(output) + ok = "error" not in packages + except Exception: + # return values for errors is already set + pass + + return ok, packages
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/CondaInterface/CondaPackageDetailsWidget.py Sat Feb 09 16:43:49 2019 +0100 @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing a widget and a dialog to show package details. +""" + +from __future__ import unicode_literals + +from PyQt5.QtCore import Qt, QDateTime +from PyQt5.QtWidgets import QWidget, QDialog, QVBoxLayout, QDialogButtonBox + +from .Ui_CondaPackageDetailsWidget import Ui_CondaPackageDetailsWidget + +from Globals import dataString + + +class CondaPackageDetailsWidget(QWidget, Ui_CondaPackageDetailsWidget): + """ + Class implementing a widget to show package details. + """ + def __init__(self, details, parent=None): + """ + Constructor + + @param details dictionary containing the package details + @type dict + @param parent reference to the parent widget + @type QWidget + """ + super(CondaPackageDetailsWidget, self).__init__(parent) + self.setupUi(self) + + self.headerLabel.setText(self.tr("<b>{0} / {1} / {2}</b>").format( + details["name"], details["version"], details["build"])) + if "fn" in details: + self.filenameLabel.setText(details["fn"]) + if "size" in details: + self.sizeLabel.setText(dataString(details["size"])) + if "channel" in details: + self.channelLabel.setText(details["channel"]) + if "url" in details: + self.urlLabel.setText(details["url"]) + if "md5" in details: + self.md5Label.setText(details["md5"]) + if "license" in details: + self.licenseLabel.setText(details["license"]) + if "subdir" in details: + self.platformLabel.setText(details["subdir"]) + elif "platform" in details: + self.platformLabel.setText(details["platform"]) + else: + self.platformLabel.setText(self.tr("unknown")) + if "depends" in details: + self.dependenciesEdit.setPlainText("\n".join(details["depends"])) + + if "timestamp" in details: + dt = QDateTime.fromSecsSinceEpoch(details["timestamp"], Qt.UTC) + self.timestampLabel.setText(dt.toString("yyyy-MM-dd hh:mm:ss t")) + + self.resize(600, 500) + + +class CondaPackageDetailsDialog(QDialog): + """ + Class implementing a dialog to show package details. + """ + def __init__(self, details, parent=None): + """ + Constructor + + @param details dictionary containing the package details + @type dict + @param parent reference to the parent widget + @type QWidget + """ + super(CondaPackageDetailsDialog, self).__init__(parent) + self.setSizeGripEnabled(True) + + self.__layout = QVBoxLayout(self) + self.setLayout(self.__layout) + + self.__cw = CondaPackageDetailsWidget(details, self) + size = self.__cw.size() + self.__layout.addWidget(self.__cw) + self.__buttonBox = QDialogButtonBox(self) + self.__buttonBox.setStandardButtons(QDialogButtonBox.Close) + self.__layout.addWidget(self.__buttonBox) + + self.resize(size) + self.setWindowTitle(self.tr("Package Details")) + + self.__buttonBox.accepted.connect(self.accept) + self.__buttonBox.rejected.connect(self.reject)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/CondaInterface/CondaPackageDetailsWidget.ui Sat Feb 09 16:43:49 2019 +0100 @@ -0,0 +1,129 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CondaPackageDetailsWidget</class> + <widget class="QWidget" name="CondaPackageDetailsWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>612</width> + <height>492</height> + </rect> + </property> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0" colspan="2"> + <widget class="QLabel" name="headerLabel"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item row="1" column="0"> + <widget class="QLabel" name="label_2"> + <property name="text"> + <string>Filename:</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QLabel" name="filenameLabel"/> + </item> + <item row="2" column="0"> + <widget class="QLabel" name="label_3"> + <property name="text"> + <string>Size:</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QLabel" name="sizeLabel"/> + </item> + <item row="3" column="0"> + <widget class="QLabel" name="label_5"> + <property name="text"> + <string>Channel:</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QLabel" name="channelLabel"/> + </item> + <item row="4" column="0"> + <widget class="QLabel" name="label_4"> + <property name="text"> + <string>URL:</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QLabel" name="urlLabel"> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + <item row="5" column="0"> + <widget class="QLabel" name="label_6"> + <property name="text"> + <string>MD5:</string> + </property> + </widget> + </item> + <item row="5" column="1"> + <widget class="QLabel" name="md5Label"/> + </item> + <item row="6" column="0"> + <widget class="QLabel" name="label_7"> + <property name="text"> + <string>Timestamp:</string> + </property> + </widget> + </item> + <item row="6" column="1"> + <widget class="QLabel" name="timestampLabel"/> + </item> + <item row="7" column="0"> + <widget class="QLabel" name="label_8"> + <property name="text"> + <string>License:</string> + </property> + </widget> + </item> + <item row="7" column="1"> + <widget class="QLabel" name="licenseLabel"/> + </item> + <item row="8" column="0"> + <widget class="QLabel" name="label_9"> + <property name="text"> + <string>Platform:</string> + </property> + </widget> + </item> + <item row="8" column="1"> + <widget class="QLabel" name="platformLabel"/> + </item> + <item row="9" column="0"> + <widget class="QLabel" name="label_10"> + <property name="text"> + <string>Dependencies:</string> + </property> + <property name="alignment"> + <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> + </property> + </widget> + </item> + <item row="9" column="1"> + <widget class="QPlainTextEdit" name="dependenciesEdit"> + <property name="readOnly"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui>
--- a/CondaInterface/CondaPackagesWidget.py Thu Feb 07 18:54:38 2019 +0100 +++ b/CondaInterface/CondaPackagesWidget.py Sat Feb 09 16:43:49 2019 +0100 @@ -14,18 +14,26 @@ from PyQt5.QtWidgets import QWidget, QToolButton, QMenu, QTreeWidgetItem, \ QApplication +from E5Gui import E5MessageBox + 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 @@ -50,10 +58,21 @@ self.condaMenuButton.setAutoRaise(True) self.condaMenuButton.setShowMenuInside(True) + self.searchToggleButton.setIcon(UI.PixmapCache.getIcon("find.png")) + + 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( @@ -164,10 +183,12 @@ )) self.packagesList.sortItems(0, Qt.AscendingOrder) + self.packagesList.resizeColumnToContents(0) self.packagesList.setUpdatesEnabled(True) QApplication.restoreOverrideCursor() self.__updateActionButtons() + self.__updateSearchActionButtons() @pyqtSlot() def on_packagesList_itemSelectionChanged(self): @@ -234,3 +255,173 @@ 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() + + 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.show() + + @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): + """ + Slot documentation goes here. + """ + # TODO: not implemented yet + raise NotImplementedError + + @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()
--- a/CondaInterface/CondaPackagesWidget.ui Thu Feb 07 18:54:38 2019 +0100 +++ b/CondaInterface/CondaPackagesWidget.ui Sat Feb 09 16:43:49 2019 +0100 @@ -7,10 +7,10 @@ <x>0</x> <y>0</y> <width>639</width> - <height>443</height> + <height>573</height> </rect> </property> - <layout class="QVBoxLayout" name="verticalLayout"> + <layout class="QVBoxLayout" name="verticalLayout_3"> <item> <layout class="QHBoxLayout" name="horizontalLayout"> <item> @@ -123,8 +123,219 @@ </property> </spacer> </item> + <item> + <widget class="QToolButton" name="searchToggleButton"> + <property name="toolTip"> + <string>Toggle to show or hide the search window</string> + </property> + <property name="checkable"> + <bool>true</bool> + </property> + </widget> + </item> </layout> </item> + <item> + <widget class="QWidget" name="searchWidget" native="true"> + <layout class="QVBoxLayout" name="verticalLayout_2"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QLineEdit" name="searchEdit"> + <property name="placeholderText"> + <string>Enter search specification</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="searchButton"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="toolTip"> + <string>Press to start the search</string> + </property> + <property name="text"> + <string>&Search</string> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QWidget" name="searchOptionsWidget" native="true"> + <layout class="QVBoxLayout" name="verticalLayout"> + <property name="leftMargin"> + <number>0</number> + </property> + <property name="topMargin"> + <number>0</number> + </property> + <property name="rightMargin"> + <number>0</number> + </property> + <property name="bottomMargin"> + <number>0</number> + </property> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_5"> + <item> + <widget class="QRadioButton" name="patternButton"> + <property name="toolTip"> + <string>Search string is a pattern (default)</string> + </property> + <property name="text"> + <string>Search Pattern</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="fullNameButton"> + <property name="toolTip"> + <string>Search string is a full name</string> + </property> + <property name="text"> + <string>Full Name</string> + </property> + </widget> + </item> + <item> + <widget class="QRadioButton" name="packageSpecButton"> + <property name="toolTip"> + <string>Search string is a package specification</string> + </property> + <property name="text"> + <string>Package Specification</string> + </property> + </widget> + </item> + <item> + <widget class="Line" name="line"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Platform:</string> + </property> + </widget> + </item> + <item> + <widget class="QComboBox" name="platformComboBox"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="toolTip"> + <string>Select the platform</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QTreeWidget" name="searchResultList"> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <column> + <property name="text"> + <string>Package</string> + </property> + </column> + <column> + <property name="text"> + <string>Version</string> + </property> + </column> + <column> + <property name="text"> + <string>Build</string> + </property> + </column> + <column> + <property name="text"> + <string>Platform</string> + </property> + </column> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <spacer name="horizontalSpacer_3"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + <item> + <widget class="QPushButton" name="installButton"> + <property name="toolTip"> + <string>Press to install the selected package (by name or package specification)</string> + </property> + <property name="text"> + <string>&Install</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="showDetailsButton"> + <property name="toolTip"> + <string>Press to show details for the selected entry</string> + </property> + <property name="text"> + <string>Show Details...</string> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_4"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </item> + </layout> + </widget> + </item> </layout> </widget> <customwidgets> @@ -134,6 +345,25 @@ <header>E5Gui/E5ToolButton.h</header> </customwidget> </customwidgets> + <tabstops> + <tabstop>environmentsComboBox</tabstop> + <tabstop>condaMenuButton</tabstop> + <tabstop>packagesList</tabstop> + <tabstop>refreshButton</tabstop> + <tabstop>upgradeButton</tabstop> + <tabstop>upgradeAllButton</tabstop> + <tabstop>uninstallButton</tabstop> + <tabstop>searchToggleButton</tabstop> + <tabstop>searchEdit</tabstop> + <tabstop>searchButton</tabstop> + <tabstop>patternButton</tabstop> + <tabstop>fullNameButton</tabstop> + <tabstop>packageSpecButton</tabstop> + <tabstop>platformComboBox</tabstop> + <tabstop>searchResultList</tabstop> + <tabstop>installButton</tabstop> + <tabstop>showDetailsButton</tabstop> + </tabstops> <resources/> <connections/> </ui>
--- a/eric6.e4p Thu Feb 07 18:54:38 2019 +0100 +++ b/eric6.e4p Sat Feb 09 16:43:49 2019 +0100 @@ -18,6 +18,7 @@ <Sources> <Source>CondaInterface/Conda.py</Source> <Source>CondaInterface/CondaExecDialog.py</Source> + <Source>CondaInterface/CondaPackageDetailsWidget.py</Source> <Source>CondaInterface/CondaPackagesWidget.py</Source> <Source>CondaInterface/__init__.py</Source> <Source>Cooperation/ChatWidget.py</Source> @@ -1719,6 +1720,7 @@ </Sources> <Forms> <Form>CondaInterface/CondaExecDialog.ui</Form> + <Form>CondaInterface/CondaPackageDetailsWidget.ui</Form> <Form>CondaInterface/CondaPackagesWidget.ui</Form> <Form>Cooperation/ChatWidget.ui</Form> <Form>DataViews/CodeMetricsDialog.ui</Form>