Sun, 29 Dec 2024 14:56:04 +0100
Prepared a new release.
# -*- coding: utf-8 -*- # Copyright (c) 2024 - 2025 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing the pipx management widget. """ import contextlib import os import psutil from PyQt6.QtCore import Qt, QTimer, pyqtSlot from PyQt6.QtGui import QIcon from PyQt6.QtWidgets import QDialog, QMenu, QTreeWidgetItem, QWidget from eric7.EricGui import EricPixmapCache from eric7.EricGui.EricOverrideCursor import EricOverrideCursor from eric7.EricWidgets import EricFileDialog, EricMessageBox from eric7.EricWidgets.EricApplication import ericApp from .Pipx import Pipx from .PipxAppStartDialog import PipxAppStartDialog from .PipxDependenciesDialog import PipxDependenciesDialog from .Ui_PipxWidget import Ui_PipxWidget class PipxWidget(QWidget, Ui_PipxWidget): """ Class implementing the pipx management widget. """ PackageColumn = 0 VersionColumn = 1 PythonVersionColumn = 2 AppPathRole = Qt.ItemDataRole.UserRole VersionRole = Qt.ItemDataRole.UserRole + 1 LatestVersionRole = Qt.ItemDataRole.UserRole + 2 OutdatedDependenciesRole = Qt.ItemDataRole.UserRole + 3 def __init__(self, plugin, fromEric=True, parent=None): """ Constructor @param plugin reference to the plug-in object @type PluginPipxInterface @param fromEric flag indicating the eric-ide mode (defaults to True) (True = eric-ide mode, False = application mode) @type bool (optional) @param parent reference to the parent widget (defaults to None) @type QWidget (optional) """ super().__init__(parent) self.setupUi(self) self.__plugin = plugin self.__pipx = Pipx(self) self.__pipx.outdatedPackage.connect(self.__handleOutdatedPackage) if fromEric: self.layout().setContentsMargins(0, 3, 0, 0) else: self.layout().setContentsMargins(0, 0, 0, 0) iconSuffix = "-dark" if ericApp().usesDarkPalette() else "-light" self.pipxMenuButton.setIcon(EricPixmapCache.getIcon("superMenu")) self.refreshButton.setIcon(EricPixmapCache.getIcon("reload")) self.installButton.setIcon(EricPixmapCache.getIcon("plus")) self.outdatedButton.setIcon(EricPixmapCache.getIcon("question")) self.upgradeButton.setIcon(EricPixmapCache.getIcon("upgrade")) self.uninstallButton.setIcon(EricPixmapCache.getIcon("minus")) self.showOutdatedDependenciesButton.setIcon( EricPixmapCache.getIcon( os.path.join( "PipxInterface", "icons", "dependency{0}".format(iconSuffix) ) ) ) self.repairDependenciesButton.setIcon( EricPixmapCache.getIcon( os.path.join("PipxInterface", "icons", "repair{0}".format(iconSuffix)) ) ) self.installButton.clicked.connect(self.__installPackages) self.outdatedButton.clicked.connect(self.__checkOutdatedPackages) self.upgradeButton.clicked.connect(self.__upgradePackage) self.uninstallButton.clicked.connect(self.__uninstallPackage) self.showOutdatedDependenciesButton.clicked.connect( self.__showOutdatedDependencies ) self.repairDependenciesButton.clicked.connect(self.__repairBrokenDependencies) self.pipxMenuButton.setShowMenuInside(True) self.packagesList.header().setSortIndicator( PipxWidget.PackageColumn, Qt.SortOrder.AscendingOrder ) self.packagesList.itemCollapsed.connect(self.__resizePackagesColumns) self.packagesList.itemExpanded.connect(self.__resizePackagesColumns) self.__initPipxMenu() self.__showPipxVersion() pipxPaths = self.__pipx.getPipxStrPaths() self.venvsPathEdit.setText(pipxPaths["venvsPath"]) self.applicationsPathEdit.setText(pipxPaths["appsPath"]) self.manPagesPathEdit.setText(pipxPaths["manPath"]) self.interpretersPathEdit.setText(pipxPaths["pythonPath"]) QTimer.singleShot(0, self.__populatePackages) self.__outdatedCheckTimer = QTimer(self) self.__outdatedCheckTimer.timeout.connect(self.__checkOutdatedPackages) self.__plugin.preferencesChanged.connect(self.__setOutdatedCheckTimer) QTimer.singleShot(10, self.__setOutdatedCheckTimer) @pyqtSlot() def shutdown(self): """ Public slot to perform shutdown actions. """ self.__pipx.shutdown() ####################################################################### ## Menu related methods below ####################################################################### def __initPipxMenu(self): """ Private method to create the super menu and attach it to the super menu button. """ ################################################################### ## Menu with install related actions ################################################################### self.__installSubmenu = QMenu(self.tr("Install")) self.__installPackagesAct = self.__installSubmenu.addAction( self.tr("Install Packages"), self.__installPackages ) self.__installAllPackagesAct = self.__installSubmenu.addAction( self.tr("Install All Packages"), self.__installAllPackages ) self.__installSubmenu.addSeparator() self.__reinstallPackagesAct = self.__installSubmenu.addAction( self.tr("Re-Install Selected Package"), self.__reinstallPackage ) self.__reinstallAllPackagesAct = self.__installSubmenu.addAction( self.tr("Re-Install All Packages"), self.__reinstallAllPackages ) self.__installSubmenu.addSeparator() self.__createSpecMetadataAct = self.__installSubmenu.addAction( self.tr("Create Spec Metadata File"), self.__createSpecMetadataFile ) ################################################################### ## Menu with upgrade related actions ################################################################### self.__upgradeSubmenu = QMenu(self.tr("Upgrade")) self.__checkOutdatedPackagesAct = self.__upgradeSubmenu.addAction( self.tr("Check Outdated Packages"), self.__checkOutdatedPackages ) self.__upgradeSubmenu.addSeparator() self.__upgradePackagesAct = self.__upgradeSubmenu.addAction( self.tr("Upgrade Selected Package"), self.__upgradePackage ) self.__upgradeAllPackagesAct = self.__upgradeSubmenu.addAction( self.tr("Upgrade All Packages"), self.__upgradeAllPackages ) self.__upgradeSubmenu.addSeparator() self.__upgradeSharedLibsAct = self.__upgradeSubmenu.addAction( self.tr("Upgrade Shared Libraries"), self.__upgradeSharedLibs ) ################################################################### ## Menu with uninstall related actions ################################################################### self.__uninstallSubmenu = QMenu(self.tr("Uninstall")) self.__uninstallPackagesAct = self.__uninstallSubmenu.addAction( self.tr("Uninstall Selected Package"), self.__uninstallPackage ) self.__uninstallAllPackagesAct = self.__uninstallSubmenu.addAction( self.tr("Uninstall All Packages"), self.__uninstallAllPackages ) ################################################################### ## Menu with dependencies related actions ################################################################### self.__dependenciseSubmenu = QMenu(self.tr("Dependencies")) self.__showDependenciesAct = self.__dependenciseSubmenu.addAction( self.tr("Show All Dependencies"), self.__showDependencies ) self.__showOutdatedDependenciesAct = self.__dependenciseSubmenu.addAction( self.tr("Show Outdated Dependencies"), self.__showOutdatedDependencies ) self.__showUptodateDependenciesAct = self.__dependenciseSubmenu.addAction( self.tr("Show Up-to-date Dependencies"), self.__showUptodateDependencies ) self.__dependenciseSubmenu.addSeparator() self.__upgradeDependenciesAct = self.__dependenciseSubmenu.addAction( self.tr("Upgrade All Dependencies"), self.__upgradeDependencies ) ################################################################### ## Main menu ################################################################### self.__pipxMenu = QMenu() self.__installSubmenuAct = self.__pipxMenu.addMenu(self.__installSubmenu) self.__pipxMenu.addSeparator() self.__upgradeSubmenuAct = self.__pipxMenu.addMenu(self.__upgradeSubmenu) self.__pipxMenu.addSeparator() self.__uninstallSubmenuAct = self.__pipxMenu.addMenu(self.__uninstallSubmenu) self.__pipxMenu.addSeparator() self.__dependenciseSubmenuAct = self.__pipxMenu.addMenu( self.__dependenciseSubmenu ) self.__pipxMenu.addSeparator() self.__pipxMenu.addAction( self.tr("Standalone Interpreters"), self.__showInterpreters ) self.__pipxMenu.addSeparator() self.__pipxMenu.addAction( self.tr("Ensure PATH Modifications"), self.__ensurePath ) self.__pipxMenu.addSeparator() self.__pipxMenu.addAction(self.tr("Configure..."), self.__pipxConfigure) self.__pipxMenu.aboutToShow.connect(self.__aboutToShowPipxMenu) self.pipxMenuButton.setMenu(self.__pipxMenu) @pyqtSlot() def __aboutToShowPipxMenu(self): """ Private slot to set the action enabled status. """ selectedPackageItems = self.__selectedPackageItems() self.__reinstallPackagesAct.setEnabled(len(selectedPackageItems) == 1) self.__upgradePackagesAct.setEnabled( len(selectedPackageItems) == 1 and bool(selectedPackageItems[0].data(0, PipxWidget.LatestVersionRole)) ) self.__uninstallPackagesAct.setEnabled(len(selectedPackageItems) == 1) self.__dependenciseSubmenuAct.setEnabled(len(selectedPackageItems) == 1) self.__showOutdatedDependenciesAct.setEnabled( len(selectedPackageItems) == 1 and bool( selectedPackageItems[0].data(0, PipxWidget.OutdatedDependenciesRole) ) ) self.__upgradeDependenciesAct.setEnabled( len(selectedPackageItems) == 1 and bool( selectedPackageItems[0].data(0, PipxWidget.OutdatedDependenciesRole) ) ) @pyqtSlot() def __installPackages(self): """ Private slot to install packages to be given by the user. """ from .PipxPackagesInputDialog import PipxPackagesInputDialog dlg = PipxPackagesInputDialog(self.tr("Install Packages"), parent=self) if dlg.exec() == QDialog.DialogCode.Accepted: packages, pyVersion, fetchMissing, force, systemSitePackages = dlg.getData() self.__pipx.installPackages( packages, interpreterVersion=pyVersion, fetchMissingInterpreter=fetchMissing, forceVenvModification=force, systemSitePackages=systemSitePackages, ) self.on_refreshButton_clicked() @pyqtSlot() def __installAllPackages(self): """ Private slot to install all packages listed in a specification file. """ from .PipxSpecInputDialog import PipxSpecInputDialog dlg = PipxSpecInputDialog(self.tr("Install All Packages"), parent=self) if dlg.exec() == QDialog.DialogCode.Accepted: specFile, pyVersion, fetchMissing, force, systemSitePackages = dlg.getData() self.__pipx.installAllPackages( specFile, interpreterVersion=pyVersion, fetchMissingInterpreter=fetchMissing, forceVenvModification=force, systemSitePackages=systemSitePackages, ) self.on_refreshButton_clicked() @pyqtSlot() def __createSpecMetadataFile(self): """ Private slot to create a spec metadata file needed by 'pipx install-all'. """ specFile, selectedFilter = EricFileDialog.getSaveFileNameAndFilter( self, self.tr("Create Spec Metadata File"), "", self.tr("JSON Files (*.json);;All Files (*)"), self.tr("JSON Files (*.json)"), EricFileDialog.DontConfirmOverwrite, ) if specFile: ext = os.path.splitext(specFile)[1] if not ext: ex = selectedFilter.split("(*")[1].split(")")[0] if ex: specFile += ex if os.path.exists(specFile): ok = EricMessageBox.yesNo( self, self.tr("Create Spec Metadata File"), self.tr( "<p>The file <b>{0}</b> exists already. Overwrite it?</p>" ).format(specFile), ) if not ok: return ok, message = self.__pipx.createSpecMetadataFile(specFile=specFile) if ok: EricMessageBox.information( self, self.tr("Create Spec Metadata File"), self.tr( "<p>The spec metadata file <b>{0}</b> was created" " successfully.</p>" ).format(specFile), ) else: EricMessageBox.critical( self, self.tr("Create Spec Metadata File"), self.tr( "<p>The spec metadata file <b>{0}</b> could not be created.</p>" "<p>Reason: {1}</p>" ).format(specFile, message), ) @pyqtSlot() def __reinstallPackage(self): """ Private slot to force a re-installation of the selected package. """ from .PipxReinstallDialog import PipxReinstallDialog package = self.__selectedPackages()[0] yes = EricMessageBox.yesNo( self, self.tr("Re-Install Package"), self.tr( "<p>Shall the package <b>{0}</b> really be reinstalled?</p>" ).format(package), ) if yes: dlg = PipxReinstallDialog(reinstallAll=False, parent=self) if dlg.exec() == QDialog.DialogCode.Accepted: pyVersion, fetchMissing, _ = dlg.getData() self.__pipx.reinstallPackage( package, interpreterVersion=pyVersion, fetchMissingInterpreter=fetchMissing, ) self.on_refreshButton_clicked() @pyqtSlot() def __reinstallAllPackages(self): """ Private slot to force a re-installation of all packages. """ from .PipxReinstallDialog import PipxReinstallDialog yes = EricMessageBox.yesNo( self, self.tr("Re-Install All Packages"), self.tr("""Do you really want to reinstall all packages?"""), ) if yes: dlg = PipxReinstallDialog(reinstallAll=True, parent=self) if dlg.exec() == QDialog.DialogCode.Accepted: pyVersion, fetchMissing, skipList = dlg.getData() self.__pipx.reinstallAllPackages( interpreterVersion=pyVersion, fetchMissingInterpreter=fetchMissing, skipPackages=skipList, ) self.on_refreshButton_clicked() def __checkPackageOutdated(self, itm): """ Private method to check, if a package item is outdated or has outdated dependencies. @param itm reference to the package item @type QTreeWidgetItem """ package = itm.text(PipxWidget.PackageColumn) self.__pipx.checkPackageOutdated(package) @pyqtSlot(str, str, bool) def __handleOutdatedPackage(self, package, latestVersion, outdatedDependencies): """ Private slot to handle the pipx client reporting an outdated package or a package with outdated dependencies. @param package name of the package @type str @param latestVersion latest available version in case outdated @type str @param outdatedDependencies flag indicating outdated dependencies @type bool """ itm = self.__getItemForPackage(package=package) if latestVersion: self.__markPackageOutdated(itm, latestVersion, outdatedDependencies) elif ( self.__plugin.getPreferences("IncludeOutdatedDependencies") and outdatedDependencies ): self.__markPackageDependenciesOutdated(itm) else: self.__markPackageClean(itm) self.__resizePackagesColumns() self.__plugin.setOutdatedIndicator(self.__hasOutdatedItems()) @pyqtSlot() def __checkOutdatedPackages(self): """ Private slot to check, if there are any outdated packages. """ for row in range(self.packagesList.topLevelItemCount()): itm = self.packagesList.topLevelItem(row) self.__checkPackageOutdated(itm) @pyqtSlot() def __setOutdatedCheckTimer(self): """ Private slot to configure the periodic outdated packages check. """ interval = self.__plugin.getPreferences("PeriodicOutdatedCheckInterval") # interval is in hours if interval: self.__outdatedCheckTimer.setInterval( interval * 3_600_000 # interval in ms ) self.__outdatedCheckTimer.start() else: self.__outdatedCheckTimer.stop() self.__plugin.setOutdatedIndicator(self.__hasOutdatedItems()) @pyqtSlot() def __upgradePackage(self): """ Private slot to upgrade the selected package. """ packageItem = self.__selectedPackageItems()[0] runningApps = self.__getRunningApps(self.__packageApps(packageItem)) if runningApps: EricMessageBox.warning( self, self.tr("Upgrade Selected Package"), self.tr( "<p>The selected package cannot be upgraded because some of its" " apps are running.</p><ul><li>{0}</li></ul><p>Stop these apps" " and try again.</p>" ).format("</li><li>".join(runningApps)), ) else: package = packageItem.text(PipxWidget.PackageColumn) self.__pipx.upgradePackage(package) self.__checkPackageVersion(packageItem) self.__checkPackageOutdated(packageItem) self.__resizePackagesColumns() self.__plugin.setOutdatedIndicator(self.__hasOutdatedItems()) @pyqtSlot() def __upgradeAllPackages(self): """ Private slot to upgrade all packages. """ runningApps = self.__getAllRunningApps() if runningApps: EricMessageBox.warning( self, self.tr("Upgrade All Packages"), self.tr( "<p>The packages cannot be upgraded because some of their apps are" " running.</p><ul><li>{0}</li></ul><p>Stop these apps and try" " again.</p>" ).format("</li><li>".join(runningApps)), ) else: self.__pipx.upgradeAllPackages() self.on_refreshButton_clicked() @pyqtSlot() def __upgradeSharedLibs(self): """ Private slot to upgrade the shared libraries. """ self.__pipx.upgradeSharedLibraries() @pyqtSlot() def __uninstallPackage(self): """ Private slot to uninstall the selected package. """ package = self.__selectedPackages()[0] yes = EricMessageBox.yesNo( self, self.tr("Uninstall Package"), self.tr( "<p>Shall the package <b>{0}</b> really be uninstalled?</p>" ).format(package), ) if yes: self.__pipx.uninstallPackage(package) self.on_refreshButton_clicked() @pyqtSlot() def __uninstallAllPackages(self): """ Private slot to uninstall all packages. """ yes = EricMessageBox.yesNo( self, self.tr("Uninstall All Packages"), self.tr("<p>Do you really want to uninstall <b>ALL</b> packages?</p>"), ) if yes: self.__pipx.uninstallAllPackages() self.on_refreshButton_clicked() @pyqtSlot() def __showInterpreters(self): """ Private slot to show a list of standalone Python interpreters. """ from .PipxInterpretersDialog import PipxInterpretersDialog dlg = PipxInterpretersDialog(self.__pipx, parent=self) dlg.exec() @pyqtSlot() def __pipxConfigure(self): """ Private slot to show the pipx configuration page. """ ericApp().getObject("UserInterface").showPreferences("pipxPage") @pyqtSlot() def __ensurePath(self): """ Private slot to ensure that the directory where pipx stores apps is in your PATH environment variable. """ self.__pipx.ensurePath() @pyqtSlot() def __showDependencies(self): """ Private slot to show a dialog with the dependencies of the selected package. """ with EricOverrideCursor(): package = self.__selectedPackages()[0] dependencies = self.__pipx.getAllPackageDependencies(package=package) dlg = PipxDependenciesDialog( package=package, dependencies=dependencies, mode=PipxDependenciesDialog.AllMode, parent=self, ) dlg.exec() @pyqtSlot() def __showOutdatedDependencies(self): """ Private slot to show a dialog with the outdated dependencies of the selected package. """ with EricOverrideCursor(): package = self.__selectedPackages()[0] dependencies = self.__pipx.getOutdatedPackageDependencies(package=package) dlg = PipxDependenciesDialog( package=package, dependencies=dependencies, mode=PipxDependenciesDialog.OutdatedMode, parent=self, ) result = dlg.exec() if result == PipxDependenciesDialog.UpgradeAllDependenciesAction: # 'Upgrade All Dependencies' clicked dependencies = dlg.getDependencies(selectedOnly=False) self.__upgradeDependencies(dependencies=dependencies) elif result == PipxDependenciesDialog.UpgradeSelectedDependenciesAction: # 'Upgrade Selected Dependencies' clicked dependencies = dlg.getDependencies(selectedOnly=True) self.__upgradeDependencies(dependencies=dependencies) @pyqtSlot() def __showUptodateDependencies(self): """ Private slot to show a dialog with the up-to-date dependencies of the selected package. """ with EricOverrideCursor(): package = self.__selectedPackages()[0] dependencies = self.__pipx.getUptodatePackageDependencies(package=package) dlg = PipxDependenciesDialog( package=package, dependencies=dependencies, mode=PipxDependenciesDialog.UptodateMode, parent=self, ) dlg.exec() @pyqtSlot() def __upgradeDependencies(self, dependencies=None): """ Private slot to upgrade the outdated dependencies of the selected package. @param dependencies list of dependencies to be upgraded or None to upgrade all outdated dependencies (defaults to None) @type list of str or None (optional) """ package = self.__selectedPackages()[0] self.__pipx.upgradePackageDependencies( package=package, dependencies=dependencies ) packageItem = self.__getItemForPackage(package) if packageItem: self.__checkPackageOutdated(packageItem) self.__resizePackagesColumns() self.__plugin.setOutdatedIndicator(self.__hasOutdatedItems()) @pyqtSlot() def __repairBrokenDependencies(self): """ Private slot to repair broken (unmet) dependencies. Note: This could be necessary after a dependencies upgrade. pip will tell you. """ package = self.__selectedPackages()[0] self.__pipx.repairBrokenDependencies(package) ####################################################################### ## Main widget related methods below ####################################################################### def __showPipxVersion(self): """ Private method to show the pipx version in the widget header. """ self.pipxVersionLabel.setText( self.tr("<b>pipx Version {0}</b>").format(self.__pipx.getPipxVersion()) ) @pyqtSlot() def __resizePackagesColumns(self): """ Private slot to resize the columns of the packages list. """ self.packagesList.header().setStretchLastSection(True) self.packagesList.resizeColumnToContents(PipxWidget.PackageColumn) self.packagesList.resizeColumnToContents(PipxWidget.VersionColumn) self.packagesList.resizeColumnToContents(PipxWidget.PythonVersionColumn) def __markPackageOutdated(self, item, latestVersion, outdatedDependencies): """ Private method to mark the given package item as outdated. @param item reference to the outdated package item @type QTreeWidgetItem @param latestVersion latest version of the package @type str @param outdatedDependencies flag indicating the existence of outdated dependencies @type bool """ version = item.data(0, PipxWidget.VersionRole) item.setData(0, PipxWidget.LatestVersionRole, latestVersion) item.setData(0, PipxWidget.OutdatedDependenciesRole, outdatedDependencies) item.setText( PipxWidget.VersionColumn, self.tr("{0} ({1})", "current version, latest version").format( version, latestVersion ), ) item.setIcon(PipxWidget.VersionColumn, EricPixmapCache.getIcon("upgrade")) if outdatedDependencies: item.setToolTip( PipxWidget.VersionColumn, self.tr("package and some dependencies outdated"), ) else: item.setToolTip(PipxWidget.VersionColumn, self.tr("package outdated")) def __markPackageDependenciesOutdated(self, item): """ Private method to mark the given package item as having outdated dependencies. @param item reference to the outdated package item @type QTreeWidgetItem """ version = item.data(0, PipxWidget.VersionRole) item.setData(0, PipxWidget.LatestVersionRole, "") item.setData(0, PipxWidget.OutdatedDependenciesRole, True) item.setText(PipxWidget.VersionColumn, version) item.setIcon(PipxWidget.VersionColumn, EricPixmapCache.getIcon("upgrade")) item.setToolTip(PipxWidget.VersionColumn, self.tr("some dependencies outdated")) def __markPackageClean(self, item): """ Private method to mark the given package item as clean (i.e. not outdated nor having outdated dependencies). @param item reference to the outdated package item @type QTreeWidgetItem """ version = item.data(0, PipxWidget.VersionRole) item.setData(0, PipxWidget.LatestVersionRole, "") item.setData(0, PipxWidget.OutdatedDependenciesRole, False) item.setText(PipxWidget.VersionColumn, version) item.setIcon(PipxWidget.VersionColumn, QIcon()) item.setToolTip(PipxWidget.VersionColumn, self.tr("everything up-to-date")) def __checkPackageVersion(self, itm): """ Private method to check the version of a package. @param itm reference to the package item @type QTreeWidgetItem """ package = itm.text(PipxWidget.PackageColumn) version = self.__pipx.getPackageVersion(package) itm.setText(PipxWidget.VersionColumn, version) itm.setData(0, PipxWidget.VersionRole, version) def __populatePackages(self): """ Private method to populate the packages list. """ self.packagesList.clear() packages = self.__pipx.getInstalledPackages() for package in packages: topItem = QTreeWidgetItem( self.packagesList, [ package["name"], package["version"], self.tr("{0}{1}", "Python version, standalone indicator").format( package["python"], self.tr(" (standalone)") if package["is_standalone"] else "", ), ], ) topItem.setData(0, PipxWidget.VersionRole, package["version"]) for app, appPath in package["apps"]: itm = QTreeWidgetItem(topItem, [app]) itm.setData(0, PipxWidget.AppPathRole, appPath) if self.__plugin.getPreferences("AutoCheckOutdated"): self.__checkOutdatedPackages() self.__resizePackagesColumns() self.on_packagesList_itemSelectionChanged() @pyqtSlot() def on_refreshButton_clicked(self): """ Private slot to refresh the packages list. """ self.__showPipxVersion() expandedPackages = [] outdatedPackages = {} for row in range(self.packagesList.topLevelItemCount()): itm = self.packagesList.topLevelItem(row) if itm.isExpanded(): expandedPackages.append(itm.text(PipxWidget.PackageColumn)) latestVersion = itm.data(0, PipxWidget.LatestVersionRole) if not self.__plugin.getPreferences("AutoCheckOutdated"): outdatedDependencies = itm.data(0, PipxWidget.OutdatedDependenciesRole) if latestVersion or outdatedDependencies: outdatedPackages[itm.text(PipxWidget.PackageColumn)] = ( latestVersion, outdatedDependencies, ) self.__populatePackages() for row in range(self.packagesList.topLevelItemCount()): itm = self.packagesList.topLevelItem(row) package = itm.text(PipxWidget.PackageColumn) if package in expandedPackages: itm.setExpanded(True) with contextlib.suppress(KeyError): latestVersion, outdatedDependencies = outdatedPackages[package] if ( latestVersion is not None and itm.data(0, PipxWidget.VersionRole) != latestVersion ): self.__markPackageOutdated(itm, latestVersion, outdatedDependencies) elif ( outdatedDependencies is not None and itm.data(0, PipxWidget.OutdatedDependenciesRole) != outdatedDependencies and outdatedDependencies ): self.__markPackageDependenciesOutdated(itm) self.__resizePackagesColumns() self.__plugin.setOutdatedIndicator(bool(outdatedPackages)) @pyqtSlot(QTreeWidgetItem, int) def on_packagesList_itemActivated(self, item, column): """ Private slot to start the activated item, if it is not a top level one. @param item reference to the activated item @type QTreeWidgetItem @param column column number of the activation @type int """ if item.parent() is not None: app = item.data(0, PipxWidget.AppPathRole) dlg = PipxAppStartDialog(app, self.__plugin, self) dlg.show() @pyqtSlot() def on_packagesList_itemSelectionChanged(self): """ Private slot to handle a change of selected packages and apps. """ selectedPackageItems = self.__selectedPackageItems() self.upgradeButton.setEnabled( len(selectedPackageItems) == 1 and bool(selectedPackageItems[0].data(0, PipxWidget.LatestVersionRole)) ) self.uninstallButton.setEnabled(len(selectedPackageItems) == 1) self.showOutdatedDependenciesButton.setEnabled( len(selectedPackageItems) == 1 and bool( selectedPackageItems[0].data(0, PipxWidget.OutdatedDependenciesRole) ) ) self.repairDependenciesButton.setEnabled(len(selectedPackageItems) == 1) def __selectedPackages(self): """ Private method to determine the list of selected packages. @return list of selected packages @rtype list of str """ packages = [] for row in range(self.packagesList.topLevelItemCount()): itm = self.packagesList.topLevelItem(row) if itm.isSelected(): packages.append(itm.text(PipxWidget.PackageColumn)) return packages def __selectedPackageItems(self): """ Private method to determine the list of selected package items. @return list of selected package items @rtype list of QTreeWidgetItem """ packageItems = [] for row in range(self.packagesList.topLevelItemCount()): itm = self.packagesList.topLevelItem(row) if itm.isSelected(): packageItems.append(itm) return packageItems def __packageApps(self, packageItem): """ Private method to determine the apps belonging to a package item. @param packageItem reference to the package item @type QTreeWidgetItem @return list of app names @rtype list of str """ apps = [] for row in range(packageItem.childCount()): apps.append(packageItem.child(row).text(0)) return apps def __getRunningApps(self, apps): """ Private method to determine, which app of the given list of apps is running. @param apps list of apps to check @type str @return set of running apps @rtype set of str """ runningApps = set() venvs = self.venvsPathEdit.text() for proc in psutil.process_iter(["name", "cmdline"]): if ( proc.info["name"] in apps and proc.info["cmdline"] and proc.info["cmdline"][0].startswith(venvs) ): runningApps.add(proc.info["name"]) return runningApps def __getAllRunningApps(self): """ Private method to determine all running pipx managed apps. @return set of running apps @rtype set of str """ allApps = [] for topRow in range(self.packagesList.topLevelItemCount()): topItm = self.packagesList.topLevelItem(topRow) allApps.extend(self.__packageApps(topItm)) return self.__getRunningApps(allApps) def __getItemForPackage(self, package): """ Private method to get a reference to the item of a given package. @param package package name @type str @return reference to the associated item @rtype QTreeWidgetItem """ for row in range(self.packagesList.topLevelItemCount()): itm = self.packagesList.topLevelItem(row) if itm.text(PipxWidget.PackageColumn) == package: return itm return None def __hasOutdatedItems(self): """ Private method to check, if the list of packages contains any entries which is outdated or has outdated dependencies. @return flag indicating outdated items @rtype bool """ for row in range(self.packagesList.topLevelItemCount()): itm = self.packagesList.topLevelItem(row) if itm.data(0, PipxWidget.LatestVersionRole) or itm.data( 0, PipxWidget.OutdatedDependenciesRole ): return True return False