Added functionality to upgrade single or all pipx managed packages and to manage the standalone interpreters.

Fri, 28 Jun 2024 16:25:21 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Fri, 28 Jun 2024 16:25:21 +0200
changeset 14
12413552ae0d
parent 13
e0359a1339fe
child 15
2fb8d19c38ae

Added functionality to upgrade single or all pipx managed packages and to manage the standalone interpreters.

PipxInterface/Pipx.py file | annotate | diff | comparison | revisions
PipxInterface/PipxInterpretersDialog.py file | annotate | diff | comparison | revisions
PipxInterface/PipxInterpretersDialog.ui file | annotate | diff | comparison | revisions
PipxInterface/PipxWidget.py file | annotate | diff | comparison | revisions
PipxInterface/PipxWidget.ui file | annotate | diff | comparison | revisions
PipxInterface/Ui_PipxInterpretersDialog.py file | annotate | diff | comparison | revisions
PipxInterface/Ui_PipxWidget.py file | annotate | diff | comparison | revisions
PluginPipxInterface.epj file | annotate | diff | comparison | revisions
--- a/PipxInterface/Pipx.py	Thu Jun 27 17:54:24 2024 +0200
+++ b/PipxInterface/Pipx.py	Fri Jun 28 16:25:21 2024 +0200
@@ -11,12 +11,12 @@
 import json
 import os
 import pathlib
-##import sys
 import sysconfig
 
 from PyQt6.QtCore import QObject, QProcess
 
 from eric7 import Preferences
+from eric7.EricWidgets import EricMessageBox
 from eric7.SystemUtilities import OSUtilities
 
 from .PipxExecDialog import PipxExecDialog
@@ -166,6 +166,43 @@
         return jsonDict
 
     ############################################################################
+    ## pipx interpreter list function (modified from original to work here)
+    ############################################################################
+
+    def getPipxInterpretersList(self):
+        """
+        Public method returning a list all standalone interpreters.
+
+        @return dictionary containing data of standalone interpreters
+        @rtype dict
+        """
+        from pipx.commands.interpreter import (
+            get_installed_standalone_interpreters,
+            get_venvs_using_standalone_interpreter,
+            get_interpreter_users,
+        )
+        from pipx.paths import ctx
+        from pipx.venv import VenvContainer
+
+        interpreters = get_installed_standalone_interpreters()
+        venvs = get_venvs_using_standalone_interpreter(VenvContainer(ctx.venvs))
+
+        interpretersDict = {
+            "path": str(ctx.standalone_python_cachedir),
+            "interpreters": {},
+        }
+        for interpreter in interpreters:
+            usedBy = get_interpreter_users(interpreter, venvs)
+            interpretersDict["interpreters"][interpreter.name] = {
+                "used": bool(usedBy),
+                "used_by": [
+                    (p.main_package.package, p.main_package.package_version)
+                    for p in usedBy
+                ],
+            }
+        return interpretersDict
+
+    ############################################################################
     ## Command methods
     ############################################################################
 
@@ -176,6 +213,8 @@
         @return list of dictionaries containing the installed packages and apps
         @rtype list of dict[str, str | list]
         """
+        from pipx.paths import ctx
+
         packages = []
 
         ok, output = self.runPipxProcess(["list", "--json"])
@@ -190,6 +229,13 @@
                             "version": metadata["main_package"]["package_version"],
                             "apps": [],
                             "python": metadata["python_version"],
+                            "is_standalone": (
+                                str(metadata["source_interpreter"]).startswith(
+                                    str(ctx.standalone_python_cachedir.resolve())
+                                )
+                                if metadata["source_interpreter"]
+                                else False
+                            ),
                         }
                         for appPath in metadata["main_package"]["app_paths"]:
                             package["apps"].append((appPath.name, str(appPath)))
@@ -203,7 +249,7 @@
         interpreterVersion="",
         fetchMissingInterpreter=False,
         forceVenvModification=False,
-        systemSitePackages=False
+        systemSitePackages=False,
     ):
         """
         Public method to install a list of packages with the given options.
@@ -247,7 +293,7 @@
         interpreterVersion="",
         fetchMissingInterpreter=False,
         forceVenvModification=False,
-        systemSitePackages=False
+        systemSitePackages=False,
     ):
         """
         Public method to install all packages define by a given spec metadata file
@@ -391,3 +437,100 @@
         res = dia.startProcess(self.__getPipxExecutable(), args)
         if res:
             dia.exec()
+
+    def checkPackageOutdated(self, package):
+        """
+        Public method to check, if a given package is outdated.
+
+        @param package name of the package
+        @type str
+        @return latest version in case the package is outdated and None otherwise
+        @rtype str or None
+        """
+        args = ["runpip", package, "list", "--outdated", "--format", "json"]
+        if Preferences.getPip("PipSearchIndex"):
+            indexUrl = Preferences.getPip("PipSearchIndex") + "/simple"
+            args += ["--index-url", indexUrl]
+        ok, output = self.runPipxProcess(args)
+        if not ok:
+            EricMessageBox.information(
+                None,
+                self.tr("Check Outdated Package"),
+                self.tr(
+                    "<p>The status of package <b>{0}</b> could not be determined.</p>"
+                    "<p>Reason: {1}</p>"
+                ).format(package, output),
+            )
+            return None
+
+        outdatedList = json.loads(output)
+        # check if the main package is in the list
+        for outdatedPackage in outdatedList:
+            if outdatedPackage["name"] == package:
+                return outdatedPackage["latest_version"]
+
+        return None
+
+    def upgradePackage(self, package):
+        """
+        Public method to upgrade the given package.
+
+        @param package name of the package
+        @type str
+        """
+        args = ["upgrade"]
+        if Preferences.getPip("PipSearchIndex"):
+            indexUrl = Preferences.getPip("PipSearchIndex") + "/simple"
+            args += ["--index-url", indexUrl]
+        args.append(package)
+        dia = PipxExecDialog(self.tr("Upgrade Package"))
+        res = dia.startProcess(self.__getPipxExecutable(), args)
+        if res:
+            dia.exec()
+
+    def upgradeAllPackages(self):
+        """
+        Public method to upgrade all package.
+        """
+        args = ["upgrade-all"]
+        dia = PipxExecDialog(self.tr("Upgrade All Packages"))
+        res = dia.startProcess(self.__getPipxExecutable(), args)
+        if res:
+            dia.exec()
+
+    def upgradeSharedLibraries(self):
+        """
+        Public method to upgrade shared libraries.
+        """
+        args = ["upgrade-shared"]
+        dia = PipxExecDialog(self.tr("Upgrade Shared Libraries"))
+        res = dia.startProcess(self.__getPipxExecutable(), args)
+        if res:
+            dia.exec()
+
+    def upgradeInterpreters(self, dialogParent=None):
+        """
+        Public method to upgrade the installed interpreters to the latest available
+        micro/patch version
+        
+        @param dialogParent parent widget of the execution dialog
+        @type QWidget
+        """
+        args = ["interpreter", "upgrade"]
+        dia = PipxExecDialog(self.tr("Upgrade Interpreters"), parent=dialogParent)
+        res = dia.startProcess(self.__getPipxExecutable(), args)
+        if res:
+            dia.exec()
+
+    def pruneInterpreters(self, dialogParent=None):
+        """
+        Public method to prune unused interpreters.
+        
+        @param dialogParent parent widget of the execution dialog
+        @type QWidget
+        """
+        args = ["interpreter", "prune"]
+        dia = PipxExecDialog(self.tr("Prune Unused Interpreters"), parent=dialogParent)
+        res = dia.startProcess(self.__getPipxExecutable(), args)
+        if res:
+            dia.exec()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PipxInterface/PipxInterpretersDialog.py	Fri Jun 28 16:25:21 2024 +0200
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to show the available standalone Python interpreters.
+"""
+
+from PyQt6.QtCore import pyqtSlot
+from PyQt6.QtWidgets import QDialog, QTreeWidgetItem
+
+from eric7.EricGui import EricPixmapCache
+
+from .Ui_PipxInterpretersDialog import Ui_PipxInterpretersDialog
+
+
+class PipxInterpretersDialog(QDialog, Ui_PipxInterpretersDialog):
+    """
+    Class implementing a dialog to show the available standalone Python interpreters.
+    """
+
+    def __init__(self, pipx, parent=None):
+        """
+        Constructor
+
+        @param pipx reference to the pipx interface
+        @type Pipx
+        @param parent reference to the parent widget (defaults to None)
+        @type QWidget (optional)
+        """
+        super().__init__(parent)
+        self.setupUi(self)
+
+        self.refreshButton.setIcon(EricPixmapCache.getIcon("reload"))
+        self.upgradeButton.setIcon(EricPixmapCache.getIcon("upgrade"))
+        self.pruneButton.setIcon(EricPixmapCache.getIcon("clear"))
+
+        self.__pipx = pipx
+
+        self.refreshButton.clicked.connect(self.__populateInterpretersList)
+
+        self.__populateInterpretersList()
+
+    @pyqtSlot()
+    def __populateInterpretersList(self):
+        """
+        Private slot to populate the list of standalone Python interpreters.
+        """
+        self.interpretersList.clear()
+
+        interpreters = self.__pipx.getPipxInterpretersList()["interpreters"]
+        for interpreter in interpreters:
+            pyItem = QTreeWidgetItem(
+                self.interpretersList,
+                [
+                    self.tr("Python {0}{1}").format(
+                        interpreter,
+                        ""
+                        if interpreters[interpreter]["used"]
+                        else self.tr(" (unused)"),
+                    )
+                ]
+            )
+            for package, packageVersion in interpreters[interpreter]["used_by"]:
+                QTreeWidgetItem(
+                    pyItem,
+                    [
+                        self.tr("{0} {1}", "package, version")
+                        .format(package, packageVersion)
+                    ]
+                )
+                pyItem.setExpanded(True)
+
+    @pyqtSlot()
+    def on_upgradeButton_clicked(self):
+        """
+        Private slot to upgrade all pipx managed interpreters to the latest available
+        micro/patch version.
+        """
+        self.__pipx.upgradeInterpreters(dialogParent=self)
+        self.__populateInterpretersList()
+
+    @pyqtSlot()
+    def on_pruneButton_clicked(self):
+        """
+        Private slot to prune unused interpreters.
+        """
+        self.__pipx.pruneInterpreters(dialogParent=self)
+        self.__populateInterpretersList()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PipxInterface/PipxInterpretersDialog.ui	Fri Jun 28 16:25:21 2024 +0200
@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PipxInterpretersDialog</class>
+ <widget class="QDialog" name="PipxInterpretersDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>450</width>
+    <height>500</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Standalone Python Interpreters</string>
+  </property>
+  <property name="sizeGripEnabled">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <spacer name="horizontalSpacer">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QToolButton" name="refreshButton">
+       <property name="toolTip">
+        <string>Press to refresh the list of interpreters.</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QToolButton" name="upgradeButton">
+       <property name="toolTip">
+        <string>Press to upgrade installed interpreters to the latest micro version.</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QToolButton" name="pruneButton">
+       <property name="toolTip">
+        <string>Press to prune all unused interpreters.</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer_2">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QTreeWidget" name="interpretersList">
+     <property name="editTriggers">
+      <set>QAbstractItemView::NoEditTriggers</set>
+     </property>
+     <property name="alternatingRowColors">
+      <bool>true</bool>
+     </property>
+     <property name="selectionMode">
+      <enum>QAbstractItemView::NoSelection</enum>
+     </property>
+     <property name="sortingEnabled">
+      <bool>true</bool>
+     </property>
+     <column>
+      <property name="text">
+       <string>Interpreter/Package</string>
+      </property>
+     </column>
+    </widget>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Close</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <tabstops>
+  <tabstop>refreshButton</tabstop>
+  <tabstop>upgradeButton</tabstop>
+  <tabstop>pruneButton</tabstop>
+  <tabstop>interpretersList</tabstop>
+ </tabstops>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>PipxInterpretersDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>PipxInterpretersDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
--- a/PipxInterface/PipxWidget.py	Thu Jun 27 17:54:24 2024 +0200
+++ b/PipxInterface/PipxWidget.py	Fri Jun 28 16:25:21 2024 +0200
@@ -13,6 +13,7 @@
 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 .Pipx import Pipx
@@ -31,6 +32,8 @@
     PythonVersionColumn = 2
 
     AppPathRole = Qt.ItemDataRole.UserRole
+    VersionRole = Qt.ItemDataRole.UserRole + 1
+    LatestVersionRole = Qt.ItemDataRole.UserRole + 2
 
     def __init__(self, plugin, fromEric=True, parent=None):
         """
@@ -59,10 +62,12 @@
         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.installButton.clicked.connect(self.__installPackages)
+        self.outdatedButton.clicked.connect(self.__checkOutdatedPackages)
         self.upgradeButton.clicked.connect(self.__upgradePackage)
         self.uninstallButton.clicked.connect(self.__uninstallPackage)
 
@@ -122,6 +127,10 @@
         ###################################################################
 
         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
         )
@@ -155,6 +164,10 @@
         self.__upgradeSubmenuAct = self.__pipxMenu.addMenu(self.__upgradeSubmenu)
         self.__pipxMenu.addSeparator()
         self.__uninstallSubmenuAct = self.__pipxMenu.addMenu(self.__uninstallSubmenu)
+        self.__pipxMenu.addSeparator()
+        self.__pipxMenu.addAction(
+            self.tr("Standalone Interpreters"), self.__showInterpreters
+        )
 
         self.__pipxMenu.aboutToShow.connect(self.__aboutToShowPipxMenu)
 
@@ -310,28 +323,42 @@
             self.on_refreshButton_clicked()
 
     @pyqtSlot()
+    def __checkOutdatedPackages(self):
+        """
+        Private slot to check, if there are any outdated packages.
+        """
+        with EricOverrideCursor():
+            for row in range(self.packagesList.topLevelItemCount()):
+                itm = self.packagesList.topLevelItem(row)
+                package = itm.text(PipxWidget.PackageColumn)
+                latestVersion = self.__pipx.checkPackageOutdated(package)
+                if latestVersion is not None:
+                    self.__markPackageOutdated(itm, latestVersion)
+        self.__resizePackagesColumns()
+
+    @pyqtSlot()
     def __upgradePackage(self):
         """
         Private slot to upgrade the selected package.
         """
-        # TODO: not implemented yet
-        pass
+        package = self.__selectedPackages()[0]
+        self.__pipx.upgradePackage(package)
+        self.on_refreshButton_clicked()
 
     @pyqtSlot()
     def __upgradeAllPackages(self):
         """
         Private slot to upgrade all packages.
         """
-        # TODO: not implemented yet
-        pass
+        self.__pipx.upgradeAllPackages()
+        self.on_refreshButton_clicked()
 
     @pyqtSlot()
     def __upgradeSharedLibs(self):
         """
         Private slot to upgrade the shared libraries.
         """
-        # TODO: not implemented yet
-        pass
+        self.__pipx.upgradeSharedLibraries()
 
     @pyqtSlot()
     def __uninstallPackage(self):
@@ -355,7 +382,6 @@
         """
         Private slot to uninstall all packages.
         """
-        # TODO: not implemented yet
         yes = EricMessageBox.yesNo(
             self,
             self.tr("Uninstall All Packages"),
@@ -367,6 +393,16 @@
             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, self)
+        dlg.exec()
+
     #######################################################################
     ## Main widget related methods below
     #######################################################################
@@ -388,6 +424,23 @@
         self.packagesList.resizeColumnToContents(PipxWidget.VersionColumn)
         self.packagesList.resizeColumnToContents(PipxWidget.PythonVersionColumn)
 
+    def __markPackageOutdated(self, item, latestVersion):
+        """
+        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
+        """
+        version = item.data(0, PipxWidget.VersionRole)
+        item.setData(0, PipxWidget.LatestVersionRole, latestVersion)
+        item.setText(
+            PipxWidget.VersionColumn,
+            self.tr("{0} ({1})", "current version, latest version")
+            .format(version, latestVersion),
+        )
+
     def __populatePackages(self):
         """
         Private method to populate the packages list.
@@ -398,8 +451,17 @@
         for package in packages:
             topItem = QTreeWidgetItem(
                 self.packagesList,
-                [package["name"], package["version"], package["python"]],
+                [
+                    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)
@@ -413,17 +475,29 @@
         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 latestVersion is not None:
+                outdatedPackages[itm.text(PipxWidget.PackageColumn)] = latestVersion
 
         self.__populatePackages()
 
         for row in range(self.packagesList.topLevelItemCount()):
             itm = self.packagesList.topLevelItem(row)
-            if itm.text(PipxWidget.PackageColumn) in expandedPackages:
+            package = itm.text(PipxWidget.PackageColumn)
+            if package in expandedPackages:
                 itm.setExpanded(True)
+
+            if (
+                package in outdatedPackages
+                and itm.data(0, PipxWidget.VersionRole) != outdatedPackages[package]
+            ):
+                self.__markPackageOutdated(itm, outdatedPackages[package])
+
         self.__resizePackagesColumns()
 
     @pyqtSlot(QTreeWidgetItem, int)
--- a/PipxInterface/PipxWidget.ui	Thu Jun 27 17:54:24 2024 +0200
+++ b/PipxInterface/PipxWidget.ui	Fri Jun 28 16:25:21 2024 +0200
@@ -150,6 +150,13 @@
       </widget>
      </item>
      <item>
+      <widget class="QToolButton" name="outdatedButton">
+       <property name="toolTip">
+        <string>Press to check for outdated packages.</string>
+       </property>
+      </widget>
+     </item>
+     <item>
       <widget class="QToolButton" name="upgradeButton">
        <property name="toolTip">
         <string>Press to upgrade the selected package.</string>
@@ -219,6 +226,7 @@
   <tabstop>packagesList</tabstop>
   <tabstop>refreshButton</tabstop>
   <tabstop>installButton</tabstop>
+  <tabstop>outdatedButton</tabstop>
   <tabstop>upgradeButton</tabstop>
   <tabstop>uninstallButton</tabstop>
   <tabstop>pipxMenuButton</tabstop>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PipxInterface/Ui_PipxInterpretersDialog.py	Fri Jun 28 16:25:21 2024 +0200
@@ -0,0 +1,62 @@
+# Form implementation generated from reading ui file 'PipxInterface/PipxInterpretersDialog.ui'
+#
+# Created by: PyQt6 UI code generator 6.7.0
+#
+# WARNING: Any manual changes made to this file will be lost when pyuic6 is
+# run again.  Do not edit this file unless you know what you are doing.
+
+
+from PyQt6 import QtCore, QtGui, QtWidgets
+
+
+class Ui_PipxInterpretersDialog(object):
+    def setupUi(self, PipxInterpretersDialog):
+        PipxInterpretersDialog.setObjectName("PipxInterpretersDialog")
+        PipxInterpretersDialog.resize(450, 500)
+        PipxInterpretersDialog.setSizeGripEnabled(True)
+        self.verticalLayout = QtWidgets.QVBoxLayout(PipxInterpretersDialog)
+        self.verticalLayout.setObjectName("verticalLayout")
+        self.horizontalLayout = QtWidgets.QHBoxLayout()
+        self.horizontalLayout.setObjectName("horizontalLayout")
+        spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
+        self.horizontalLayout.addItem(spacerItem)
+        self.refreshButton = QtWidgets.QToolButton(parent=PipxInterpretersDialog)
+        self.refreshButton.setObjectName("refreshButton")
+        self.horizontalLayout.addWidget(self.refreshButton)
+        self.upgradeButton = QtWidgets.QToolButton(parent=PipxInterpretersDialog)
+        self.upgradeButton.setObjectName("upgradeButton")
+        self.horizontalLayout.addWidget(self.upgradeButton)
+        self.pruneButton = QtWidgets.QToolButton(parent=PipxInterpretersDialog)
+        self.pruneButton.setObjectName("pruneButton")
+        self.horizontalLayout.addWidget(self.pruneButton)
+        spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
+        self.horizontalLayout.addItem(spacerItem1)
+        self.verticalLayout.addLayout(self.horizontalLayout)
+        self.interpretersList = QtWidgets.QTreeWidget(parent=PipxInterpretersDialog)
+        self.interpretersList.setEditTriggers(QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers)
+        self.interpretersList.setAlternatingRowColors(True)
+        self.interpretersList.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.NoSelection)
+        self.interpretersList.setObjectName("interpretersList")
+        self.verticalLayout.addWidget(self.interpretersList)
+        self.buttonBox = QtWidgets.QDialogButtonBox(parent=PipxInterpretersDialog)
+        self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal)
+        self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Close)
+        self.buttonBox.setObjectName("buttonBox")
+        self.verticalLayout.addWidget(self.buttonBox)
+
+        self.retranslateUi(PipxInterpretersDialog)
+        self.buttonBox.accepted.connect(PipxInterpretersDialog.accept) # type: ignore
+        self.buttonBox.rejected.connect(PipxInterpretersDialog.reject) # type: ignore
+        QtCore.QMetaObject.connectSlotsByName(PipxInterpretersDialog)
+        PipxInterpretersDialog.setTabOrder(self.refreshButton, self.upgradeButton)
+        PipxInterpretersDialog.setTabOrder(self.upgradeButton, self.pruneButton)
+        PipxInterpretersDialog.setTabOrder(self.pruneButton, self.interpretersList)
+
+    def retranslateUi(self, PipxInterpretersDialog):
+        _translate = QtCore.QCoreApplication.translate
+        PipxInterpretersDialog.setWindowTitle(_translate("PipxInterpretersDialog", "Standalone Python Interpreters"))
+        self.refreshButton.setToolTip(_translate("PipxInterpretersDialog", "Press to refresh the list of interpreters."))
+        self.upgradeButton.setToolTip(_translate("PipxInterpretersDialog", "Press to upgrade installed interpreters to the latest micro version."))
+        self.pruneButton.setToolTip(_translate("PipxInterpretersDialog", "Press to prune all unused interpreters."))
+        self.interpretersList.setSortingEnabled(True)
+        self.interpretersList.headerItem().setText(0, _translate("PipxInterpretersDialog", "Interpreter/Package"))
--- a/PipxInterface/Ui_PipxWidget.py	Thu Jun 27 17:54:24 2024 +0200
+++ b/PipxInterface/Ui_PipxWidget.py	Fri Jun 28 16:25:21 2024 +0200
@@ -73,6 +73,9 @@
         self.installButton = QtWidgets.QToolButton(parent=PipxWidget)
         self.installButton.setObjectName("installButton")
         self.horizontalLayout_2.addWidget(self.installButton)
+        self.outdatedButton = QtWidgets.QToolButton(parent=PipxWidget)
+        self.outdatedButton.setObjectName("outdatedButton")
+        self.horizontalLayout_2.addWidget(self.outdatedButton)
         self.upgradeButton = QtWidgets.QToolButton(parent=PipxWidget)
         self.upgradeButton.setObjectName("upgradeButton")
         self.horizontalLayout_2.addWidget(self.upgradeButton)
@@ -92,7 +95,8 @@
         QtCore.QMetaObject.connectSlotsByName(PipxWidget)
         PipxWidget.setTabOrder(self.packagesList, self.refreshButton)
         PipxWidget.setTabOrder(self.refreshButton, self.installButton)
-        PipxWidget.setTabOrder(self.installButton, self.upgradeButton)
+        PipxWidget.setTabOrder(self.installButton, self.outdatedButton)
+        PipxWidget.setTabOrder(self.outdatedButton, self.upgradeButton)
         PipxWidget.setTabOrder(self.upgradeButton, self.uninstallButton)
         PipxWidget.setTabOrder(self.uninstallButton, self.pipxMenuButton)
         PipxWidget.setTabOrder(self.pipxMenuButton, self.venvsPathEdit)
@@ -109,6 +113,7 @@
         self.label_5.setText(_translate("PipxWidget", "Standalone Interpreters:"))
         self.refreshButton.setToolTip(_translate("PipxWidget", "Press to refresh the packages list."))
         self.installButton.setToolTip(_translate("PipxWidget", "Press to install packages."))
+        self.outdatedButton.setToolTip(_translate("PipxWidget", "Press to check for outdated packages."))
         self.upgradeButton.setToolTip(_translate("PipxWidget", "Press to upgrade the selected package."))
         self.uninstallButton.setToolTip(_translate("PipxWidget", "Press to uninstall the selected package."))
         self.packagesList.setSortingEnabled(True)
--- a/PluginPipxInterface.epj	Thu Jun 27 17:54:24 2024 +0200
+++ b/PluginPipxInterface.epj	Fri Jun 28 16:25:21 2024 +0200
@@ -41,6 +41,7 @@
     "FORMS": [
       "PipxInterface/PipxAppStartDialog.ui",
       "PipxInterface/PipxExecDialog.ui",
+      "PipxInterface/PipxInterpretersDialog.ui",
       "PipxInterface/PipxPackagesInputDialog.ui",
       "PipxInterface/PipxReinstallDialog.ui",
       "PipxInterface/PipxSpecInputDialog.ui",
@@ -130,12 +131,14 @@
       "PipxInterface/Pipx.py",
       "PipxInterface/PipxAppStartDialog.py",
       "PipxInterface/PipxExecDialog.py",
+      "PipxInterface/PipxInterpretersDialog.py",
       "PipxInterface/PipxPackagesInputDialog.py",
       "PipxInterface/PipxReinstallDialog.py",
       "PipxInterface/PipxSpecInputDialog.py",
       "PipxInterface/PipxWidget.py",
       "PipxInterface/Ui_PipxAppStartDialog.py",
       "PipxInterface/Ui_PipxExecDialog.py",
+      "PipxInterface/Ui_PipxInterpretersDialog.py",
       "PipxInterface/Ui_PipxPackagesInputDialog.py",
       "PipxInterface/Ui_PipxReinstallDialog.py",
       "PipxInterface/Ui_PipxSpecInputDialog.py",

eric ide

mercurial