PipxInterface/PipxWidget.py

Sun, 29 Dec 2024 14:56:04 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sun, 29 Dec 2024 14:56:04 +0100
changeset 121
8deb7d8d9b86
parent 116
0f49bfab0768
permissions
-rw-r--r--

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

eric ide

mercurial