Fri, 25 Apr 2025 16:23:02 +0200
MicroPython
- Extended the handling of Access Point security/authmode settings to be more dynamic.
# -*- coding: utf-8 -*- # Copyright (c) 2019 - 2025 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the conda packages management widget. """ import os from PyQt6.QtCore import Qt, pyqtSlot from PyQt6.QtWidgets import ( QApplication, QDialog, QLineEdit, QMenu, QToolButton, QTreeWidgetItem, QWidget, ) from eric7 import CondaInterface from eric7.EricGui import EricPixmapCache from eric7.EricGui.EricOverrideCursor import EricOverrideCursor from eric7.EricWidgets import EricFileDialog, EricMessageBox, EricTextInputDialog from eric7.EricWidgets.EricApplication import ericApp from eric7.VirtualEnv.VirtualenvMeta import VirtualenvMetaData from .Conda import Conda from .Ui_CondaPackagesWidget import Ui_CondaPackagesWidget class CondaPackagesWidget(QWidget, Ui_CondaPackagesWidget): """ Class implementing the conda packages management widget. """ # Role definition of packages list PackageVersionRole = Qt.ItemDataRole.UserRole + 1 PackageBuildRole = Qt.ItemDataRole.UserRole + 2 # Role definitions of search results list PackageDetailedDataRole = Qt.ItemDataRole.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().__init__(parent) self.setupUi(self) self.layout().setContentsMargins(0, 3, 0, 0) self.__conda = conda if not CondaInterface.isCondaAvailable(): self.availableWidget.hide() else: self.notAvailableWidget.hide() self.__initCondaInterface() def __initCondaInterface(self): """ Private method to initialize the conda interface elements. """ self.statusLabel.hide() self.condaMenuButton.setObjectName("conda_supermenu_button") self.condaMenuButton.setIcon(EricPixmapCache.getIcon("superMenu")) self.condaMenuButton.setToolTip(self.tr("Conda Menu")) self.condaMenuButton.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) self.condaMenuButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) self.condaMenuButton.setFocusPolicy(Qt.FocusPolicy.NoFocus) self.condaMenuButton.setShowMenuInside(True) self.refreshButton.setIcon(EricPixmapCache.getIcon("reload")) self.upgradeButton.setIcon(EricPixmapCache.getIcon("1uparrow")) self.upgradeAllButton.setIcon(EricPixmapCache.getIcon("2uparrow")) self.uninstallButton.setIcon(EricPixmapCache.getIcon("minus")) self.searchToggleButton_1.setIcon(EricPixmapCache.getIcon("find")) self.searchToggleButton_2.setIcon(EricPixmapCache.getIcon("find")) self.searchButton.setIcon(EricPixmapCache.getIcon("findNext")) self.installButton.setIcon(EricPixmapCache.getIcon("plus")) self.showDetailsButton.setIcon(EricPixmapCache.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.baseWidget.show() 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: self.statusLabel.show() self.statusLabel.setText(self.tr("Getting installed packages...")) with EricOverrideCursor(): # 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.MatchFlag.MatchExactly | Qt.MatchFlag.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.SortOrder.AscendingOrder) for col in range(self.packagesList.columnCount()): self.packagesList.resizeColumnToContents(col) self.packagesList.setUpdatesEnabled(True) 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() with EricOverrideCursor(): self.__populateEnvironments() index = self.environmentsComboBox.findText( currentEnvironment, Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive, ) if index != -1: self.environmentsComboBox.setCurrentIndex(index) 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: with EricOverrideCursor(): prefix = ( "" if CondaInterface.condaVersion() >= (4, 4, 0) else 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 ok and result: 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"] platform = detail.get("subdir", detail.get("platform", "")) citm = QTreeWidgetItem(itm, ["", version, build, platform]) citm.setData(0, self.PackageDetailedDataRole, detail) self.searchResultList.sortItems(0, Qt.SortOrder.AscendingOrder) self.searchResultList.resizeColumnToContents(0) self.searchResultList.setUpdatesEnabled(True) if not ok: try: message = result["message"] except KeyError: message = result["error"] EricMessageBox.warning( self, self.tr("Conda Search Package Error"), message ) 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 """ from .CondaPackageDetailsWidget import CondaPackageDetailsDialog details = item.data(0, self.PackageDetailedDataRole) if details: dlg = CondaPackageDetailsDialog(details, parent=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 (unused) @type int """ if item.parent() is not None: self.__showDetails(item) @pyqtSlot(bool) def on_searchToggleButton_1_toggled(self, checked): """ Private slot to toggle the search widget. @param checked state of the search widget button @type bool """ self.searchWidget.setVisible(checked) self.searchToggleButton_2.setChecked(checked) if checked: self.searchEdit.setFocus(Qt.FocusReason.OtherFocusReason) self.searchEdit.selectAll() self.__updateSearchActionButtons() @pyqtSlot(bool) def on_searchToggleButton_2_toggled(self, checked): """ Private slot to toggle the search widget. @param checked state of the search widget button @type bool """ self.searchToggleButton_1.setChecked(checked) ####################################################################### ## 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. """ from .CondaInfoDialog import CondaInfoDialog infoDict = self.__conda.getCondaInformation() dlg = CondaInfoDialog(infoDict, parent=self) dlg.exec() @pyqtSlot() def __installPackages(self): """ Private slot to install packages. """ prefix = self.environmentsComboBox.itemData( self.environmentsComboBox.currentIndex() ) if prefix: ok, packageSpecs = EricTextInputDialog.getText( self, self.tr("Install Packages"), self.tr("Package Specifications (separated by whitespace):"), QLineEdit.EchoMode.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 = EricFileDialog.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. """ from .CondaExportDialog import CondaExportDialog prefix = self.environmentsComboBox.itemData( self.environmentsComboBox.currentIndex() ) if prefix: env = self.environmentsComboBox.currentText() 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, parent=self ) if dlg.exec() == QDialog.DialogCode.Accepted: virtEnvName, envName, _ = dlg.getData() args = [ "--name", envName.strip(), "--clone", prefix, ] ok, prefix, interpreter = self.__conda.createCondaEnvironment(args) if ok: metadata = VirtualenvMetaData( name=virtEnvName, path=prefix, interpreter=interpreter, environment_type=Conda.EnvironmentType, ) ericApp().getObject("VirtualEnvManager").addVirtualEnv(metadata) @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, parent=self ) if dlg.exec() == QDialog.DialogCode.Accepted: virtEnvName, envName, requirements = dlg.getData() args = [ "--name", envName.strip(), "--file", requirements, ] ok, prefix, interpreter = self.__conda.createCondaEnvironment(args) if ok: metadata = VirtualenvMetaData( name=virtEnvName, path=prefix, interpreter=interpreter, environment_type=Conda.EnvironmentType, ) ericApp().getObject("VirtualEnvManager").addVirtualEnv(metadata) @pyqtSlot() def __deleteEnvironment(self): """ Private slot to delete a conda environment. """ envName = self.environmentsComboBox.currentText() ok = EricMessageBox.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 eric7.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): EricMessageBox.critical( self, self.tr("Edit Configuration"), self.tr( """The configuration file "{0}" does not exist""" """ or is not writable.""" ).format(cfgFile), ) return self.__editor = MiniEditor(cfgFile, "YAML") self.__editor.show() @pyqtSlot() def __condaConfigure(self): """ Private slot to open the configuration page. """ ericApp().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.availableWidget.show()