diff -r 62bd225b489f -r d8946c2a22b5 eric7/PipInterface/PipPackagesWidget.py --- a/eric7/PipInterface/PipPackagesWidget.py Tue Mar 22 19:31:29 2022 +0100 +++ b/eric7/PipInterface/PipPackagesWidget.py Wed Mar 23 20:21:42 2022 +0100 @@ -12,6 +12,8 @@ import html.parser import contextlib +from packaging.specifiers import SpecifierSet + from PyQt6.QtCore import pyqtSlot, Qt, QUrl, QUrlQuery from PyQt6.QtGui import QIcon from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest @@ -157,6 +159,10 @@ AvailableVersionColumn = 2 VulnerabilityColumn = 3 + DepPackageColumn = 0 + DepInstalledVersionColumn = 1 + DepRequiredVersionColumn = 2 + def __init__(self, pip, parent=None): """ Constructor @@ -171,6 +177,8 @@ self.layout().setContentsMargins(0, 3, 0, 0) + self.viewToggleButton.setIcon(UI.PixmapCache.getIcon("viewListTree")) + self.pipMenuButton.setObjectName( "pip_supermenu_button") self.pipMenuButton.setIcon(UI.PixmapCache.getIcon("superMenu")) @@ -183,21 +191,38 @@ self.pipMenuButton.setAutoRaise(True) self.pipMenuButton.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.showPackageDetailsButton.setIcon(UI.PixmapCache.getIcon("info")) - self.searchToggleButton.setIcon(UI.PixmapCache.getIcon("find")) - self.searchButton.setIcon(UI.PixmapCache.getIcon("findNext")) - self.installButton.setIcon(UI.PixmapCache.getIcon("plus")) - self.installUserSiteButton.setIcon(UI.PixmapCache.getIcon("addUser")) - self.showDetailsButton.setIcon(UI.PixmapCache.getIcon("info")) + 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.showPackageDetailsButton.setIcon( + UI.PixmapCache.getIcon("info")) + self.searchToggleButton.setIcon( + UI.PixmapCache.getIcon("find")) + self.searchButton.setIcon( + UI.PixmapCache.getIcon("findNext")) + self.installButton.setIcon( + UI.PixmapCache.getIcon("plus")) + self.installUserSiteButton.setIcon( + UI.PixmapCache.getIcon("addUser")) + self.showDetailsButton.setIcon( + UI.PixmapCache.getIcon("info")) + + self.refreshDependenciesButton.setIcon( + UI.PixmapCache.getIcon("reload")) + self.showDepPackageDetailsButton.setIcon( + UI.PixmapCache.getIcon("info")) self.__pip = pip self.packagesList.header().setSortIndicator( PipPackagesWidget.PackageColumn, Qt.SortOrder.AscendingOrder) + self.dependenciesList.header().setSortIndicator( + PipPackagesWidget.DepPackageColumn, Qt.SortOrder.AscendingOrder) self.__infoLabels = { "name": self.tr("Name:"), @@ -216,6 +241,7 @@ "files": self.tr("Files:"), } self.infoWidget.setHeaderLabels(["Key", "Value"]) + self.dependencyInfoWidget.setHeaderLabels(["Key", "Value"]) venvManager = ericApp().getObject("VirtualEnvManager") venvManager.virtualEnvironmentAdded.connect( @@ -232,6 +258,7 @@ self.__initPipMenu() self.__populateEnvironments() self.__updateActionButtons() + self.__updateDepActionButtons() self.statusLabel.hide() self.searchWidget.hide() @@ -242,6 +269,8 @@ self.__replies = [] self.__packageDetailsDialog = None + + self.viewsStackWidget.setCurrentWidget(self.packagesPage) @pyqtSlot(bool) def __projectClosed(self, shutdown): @@ -372,7 +401,7 @@ def __refreshPackagesList(self): """ - Private method to referesh the packages list. + Private method to refresh the packages list. """ self.packagesList.clear() venvName = self.environmentsComboBox.currentText() @@ -444,7 +473,10 @@ @param index index of the selected Python environment @type int """ - self.__refreshPackagesList() + if self.viewToggleButton.isChecked(): + self.__refreshDependencyTree() + else: + self.__refreshPackagesList() @pyqtSlot() def on_localCheckBox_clicked(self): @@ -467,12 +499,14 @@ """ self.__refreshPackagesList() - def __showPackageInformation(self, packageName): + def __showPackageInformation(self, packageName, infoWidget): """ Private method to show information for a package. @param packageName name of the package @type str + @param infoWidget reference to the widget to contain the information + @type QTreeWidget """ environment = self.environmentsComboBox.currentText() interpreter = self.__pip.getVirtualenvInterpreter(environment) @@ -497,7 +531,7 @@ if mode != self.ShowProcessGeneralMode: if line[0] == " ": QTreeWidgetItem( - self.infoWidget, + infoWidget, [" ", line.strip()]) else: mode = self.ShowProcessGeneralMode @@ -510,7 +544,7 @@ label = label.lower() if label in self.__infoLabels: QTreeWidgetItem( - self.infoWidget, + infoWidget, [self.__infoLabels[label], info]) if label == "files": mode = self.ShowProcessFilesListMode @@ -518,9 +552,9 @@ mode = self.ShowProcessClassifiersMode elif label == "entry-points": mode = self.ShowProcessEntryPointsMode - self.infoWidget.scrollToTop() + infoWidget.scrollToTop() - header = self.infoWidget.header() + header = infoWidget.header() header.setStretchLastSection(False) header.resizeSections(QHeaderView.ResizeMode.ResizeToContents) if ( @@ -562,7 +596,8 @@ ) else: self.__showPackageInformation( - item.text(PipPackagesWidget.PackageColumn) + item.text(PipPackagesWidget.PackageColumn), + self.infoWidget ) self.__updateActionButtons() @@ -1488,3 +1523,204 @@ header = self.infoWidget.header() header.setStretchLastSection(True) + + ####################################################################### + ## Dependency tree related methods below + ####################################################################### + + @pyqtSlot(bool) + def on_viewToggleButton_toggled(self, checked): + """ + Private slot handling the view selection. + + @param checked state of the toggle button + @type bool + """ + if checked: + self.viewsStackWidget.setCurrentWidget( + self.dependenciesPage) + self.__refreshDependencyTree() + else: + self.viewsStackWidget.setCurrentWidget( + self.packagesPage) + self.__refreshPackagesList() + + @pyqtSlot(bool) + def on_requiresButton_toggled(self, checked): + """ + Private slot handling the selection of the view type. + + @param checked state of the radio button (unused) + @type bool + """ + self.__refreshDependencyTree() + + @pyqtSlot() + def on_localDepCheckBox_clicked(self): + """ + Private slot handling the switching of the local mode. + """ + self.__refreshDependencyTree() + + @pyqtSlot() + def on_userDepCheckBox_clicked(self): + """ + Private slot handling the switching of the 'user-site' mode. + """ + self.__refreshDependencyTree() + + def __refreshDependencyTree(self): + """ + Private method to refresh the dependency tree. + """ + self.dependenciesList.clear() + venvName = self.environmentsComboBox.currentText() + if venvName: + interpreter = self.__pip.getVirtualenvInterpreter(venvName) + if interpreter: + with EricOverrideCursor(): + dependencies = self.__pip.getDependecyTree( + venvName, + localPackages=self.localDepCheckBox.isChecked(), + usersite=self.userDepCheckBox.isChecked(), + reverse = self.requiredByButton.isChecked(), + ) + + self.dependenciesList.setUpdatesEnabled(False) + for dependency in dependencies: + self.__addDependency(dependency, self.dependenciesList) + + self.dependenciesList.sortItems( + PipPackagesWidget.DepPackageColumn, + Qt.SortOrder.AscendingOrder) + for col in range(self.dependenciesList.columnCount()): + self.dependenciesList.resizeColumnToContents(col) + self.dependenciesList.setUpdatesEnabled(True) + + self.__updateDepActionButtons() + + def __addDependency(self, dependency, parent): + """ + Private method to add a dependency branch to a given parent. + + @param dependency dependency to be added + @type dict + @param parent reference to the parent item + @type QTreeWidget or QTreeWidgetItem + """ + itm = QTreeWidgetItem(parent, [ + dependency["package_name"], + dependency["installed_version"], + dependency["required_version"], + ]) + itm.setExpanded(True) + + if dependency["required_version"].lower() != "any": + spec = ( + "=={0}".format(dependency["required_version"]) + if dependency["required_version"][0] in "0123456789" else + dependency["required_version"] + ) + specifierSet = SpecifierSet(specifiers=spec) + if not specifierSet.contains(dependency["installed_version"]): + itm.setIcon(PipPackagesWidget.DepRequiredVersionColumn, + UI.PixmapCache.getIcon("warning")) + + if dependency["required_version"].lower() == "any": + itm.setText(PipPackagesWidget.DepRequiredVersionColumn, + self.tr("any")) + + # recursively add sub-dependencies + for dep in dependency["dependencies"]: + self.__addDependency(dep, itm) + + @pyqtSlot(QTreeWidgetItem, int) + def on_dependenciesList_itemActivated(self, item, column): + """ + Private slot reacting on a package item of the dependency tree being + activated. + + @param item reference to the activated item + @type QTreeWidgetItem + @param column activated column + @type int + """ + packageName = item.text(PipPackagesWidget.DepPackageColumn) + packageVersion = item.text( + PipPackagesWidget.DepInstalledVersionColumn) + + self.__showPackageDetails(packageName, packageVersion) + + @pyqtSlot() + def on_dependenciesList_itemSelectionChanged(self): + """ + Private slot reacting on a change of selected items of the dependency + tree. + """ + if len(self.dependenciesList.selectedItems()) == 0: + self.dependencyInfoWidget.clear() + + @pyqtSlot(QTreeWidgetItem, int) + def on_dependenciesList_itemPressed(self, item, column): + """ + Private slot reacting on a package item of the dependency tree being + pressed. + + @param item reference to the pressed item + @type QTreeWidgetItem + @param column pressed column + @type int + """ + self.dependencyInfoWidget.clear() + + if item is not None: + self.__showPackageInformation( + item.text(PipPackagesWidget.DepPackageColumn), + self.dependencyInfoWidget + ) + + self.__updateDepActionButtons() + + @pyqtSlot() + def on_refreshDependenciesButton_clicked(self): + """ + Private slot to refresh the dependency tree. + """ + currentEnvironment = self.environmentsComboBox.currentText() + self.environmentsComboBox.clear() + self.dependenciesList.clear() + + with EricOverrideCursor(): + self.__populateEnvironments() + + index = self.environmentsComboBox.findText( + currentEnvironment, + Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive + ) + if index != -1: + self.environmentsComboBox.setCurrentIndex(index) + + self.__updateDepActionButtons() + + @pyqtSlot() + def on_showDepPackageDetailsButton_clicked(self): + """ + Private slot to show information for the selected package of the + dependency tree. + """ + item = self.dependenciesList.selectedItems()[0] + if item: + packageName = item.text(PipPackagesWidget.DepPackageColumn) + packageVersion = item.text( + PipPackagesWidget.DepInstalledVersionColumn) + + self.__showPackageDetails(packageName, packageVersion) + + def __updateDepActionButtons(self): + """ + Private method to set the state of the dependency page action buttons. + """ + self.showDepPackageDetailsButton.setEnabled( + len(self.dependenciesList.selectedItems()) == 1 and + self.__isPipAvailable() + )