Added the main pipx interface widget and the pipx commands interface (basic variant each).

Mon, 24 Jun 2024 17:13:07 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Mon, 24 Jun 2024 17:13:07 +0200
changeset 2
26430067aa09
parent 1
d83409a59365
child 3
e73fb9827b74

Added the main pipx interface widget and the pipx commands interface (basic variant each).

PipxInterface/Pipx.py file | annotate | diff | comparison | revisions
PipxInterface/PipxWidget.py file | annotate | diff | comparison | revisions
PipxInterface/PipxWidget.ui file | annotate | diff | comparison | revisions
PluginPipxInterface.epj file | annotate | diff | comparison | revisions
PluginPipxInterface.py file | annotate | diff | comparison | revisions
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PipxInterface/Pipx.py	Mon Jun 24 17:13:07 2024 +0200
@@ -0,0 +1,180 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the pipx GUI logic.
+"""
+
+import contextlib
+import json
+import os
+import sys
+import sysconfig
+
+from PyQt6.QtCore import QObject, QProcess
+
+from eric7 import Preferences
+from eric7.SystemUtilities import OSUtilities
+
+
+class Pipx(QObject):
+    """
+    Class implementing the pip GUI logic.
+    """
+
+    def __init__(self, parent=None):
+        """
+        Constructor
+
+        @param parent reference to the user interface object
+        @type QObject
+        """
+        super().__init__(parent)
+
+        self.__ui = parent
+
+    ############################################################################
+    ## Utility methods
+    ############################################################################
+
+    def getPipxVersion(self):
+        """
+        Public method to get the version of the installed pipx package.
+
+        @return string containing the pipx version number
+        @rtype str
+        """
+        from pipx.version import version
+
+        return version
+
+    def getPipxVersionTuple(self):
+        """
+        Public method to get the version tuple of the installed pipx package.
+
+        @return tuple containing the elements of the pipx version number
+        @rtype tuple of (int, int, int)
+        """
+        from pipx.version import version_tuple
+
+        return version_tuple
+
+    def getPipxPaths(self):
+        """
+        Public method to get the paths used by pipx.
+
+        @return dictionary containing the various pipx paths. The keys are
+            'venvsPath', 'appsPath' and 'manPath'.
+        @rtype dict[str, Path]
+        """
+        from pipx.paths import ctx
+
+        return {
+            "venvsPath": ctx.venvs,
+            "appsPath": ctx.bin_dir,
+            "manPath": ctx.man_dir,
+        }
+
+    def getPipxStrPaths(self):
+        """
+        Public method to get the paths used by pipx.
+
+        @return dictionary containing the various pipx paths. The keys are
+            'venvsPath', 'appsPath' and 'manPath'.
+        @rtype dict[str, str]
+        """
+        from pipx.paths import ctx
+
+        return {
+            "venvsPath": str(ctx.venvs),
+            "appsPath": str(ctx.bin_dir),
+            "manPath": str(ctx.man_dir),
+        }
+
+    def __getPipxExecutable(self):
+        """
+        Private method to get the path name of the pipx executable.
+
+        @return path name of the pipx executable
+        @rtype str
+        """
+        binDir = sysconfig.get_path("scripts")
+        pipx = os.path.join(binDir, "pipx")
+        if OSUtilities.isWindowsPlatform:
+            pipx += ".exe"
+
+        return pipx
+
+    def runProcess(self, args):
+        """
+        Public method to execute pipx with the given arguments.
+
+        @param args list of command line arguments for pipx
+        @type list of str
+        @return tuple containing a flag indicating success and the output
+            of the process
+        @rtype tuple of (bool, str)
+        """
+        ioEncoding = Preferences.getSystem("IOEncoding")
+
+        process = QProcess()
+        process.start(sys.executable, ["-m", "pipx"] + args)
+        procStarted = process.waitForStarted()
+        if procStarted:
+            finished = process.waitForFinished(30000)
+            if finished:
+                if process.exitCode() == 0:
+                    output = str(process.readAllStandardOutput(), ioEncoding, "replace")
+                    return True, output
+                else:
+                    error = str(process.readAllStandardError(), ioEncoding, "replace")
+                    msg = self.tr("<p>Message:{0}</p>").format(error) if error else ""
+                    return (
+                        False,
+                        self.tr("<p>pipx exited with an error ({0}).</p>{1}").format(
+                            process.exitCode(), msg
+                        ),
+                    )
+            else:
+                process.terminate()
+                process.waitForFinished(2000)
+                process.kill()
+                process.waitForFinished(3000)
+                return False, self.tr("pipx did not finish within 30 seconds.")
+
+        return False, self.tr("pipx could not be started.")
+
+    ############################################################################
+    ## Command methods
+    ############################################################################
+
+    def getInstalledPackages(self):
+        """
+        Public method to get the installed packages.
+
+        @return list of dictionaries containing the installed packages and apps
+        @rtype list of dict[str, str | list]
+        """
+        packages = []
+
+        ok, output = self.runProcess(["list", "--json"])
+        if ok:
+            if output:
+                with contextlib.suppress(json.JSONDecodeError):
+                    data = json.loads(output)
+                    for venvName in data["venvs"]:
+                        metadata = data["venvs"][venvName]["metadata"]
+                        package = {
+                            "name": venvName,
+                            "version": metadata["main_package"]["package_version"],
+                            "apps": [],
+                            "python": metadata["python_version"],
+                        }
+                        for appPath in metadata["main_package"]["app_paths"]:
+                            path = appPath["__Path__"]
+                            package["apps"].append((os.path.basename(path), path))
+                        packages.append(package)
+
+        return packages
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PipxInterface/PipxWidget.py	Mon Jun 24 17:13:07 2024 +0200
@@ -0,0 +1,122 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the pipx management widget.
+"""
+
+from PyQt6.QtCore import Qt, pyqtSlot
+from PyQt6.QtWidgets import QTreeWidgetItem, QWidget
+
+from eric7.EricGui import EricPixmapCache
+
+from .Pipx import Pipx
+from .Ui_PipxWidget import Ui_PipxWidget
+
+
+class PipxWidget(QWidget, Ui_PipxWidget):
+    """
+    Class documentation goes here.
+    """
+
+    PackageColumn = 0
+    VersionColumn = 1
+    PythonVersionColumn = 2
+
+    AppPathRole = Qt.ItemDataRole.UserRole
+
+    def __init__(self, plugin, fromEric=True, parent=None):
+        """
+        Constructor
+
+        @param plugin reference to the plug-in object
+        @type MqttMonitorPlugin
+        @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.__pipx = Pipx(self)
+
+        if fromEric:
+            self.layout().setContentsMargins(0, 3, 0, 0)
+        else:
+            self.layout().setContentsMargins(0, 0, 0, 0)
+
+        # TODO: set the various icons
+        self.pipxMenuButton.setIcon(EricPixmapCache.getIcon("superMenu"))
+        self.refreshButton.setIcon(EricPixmapCache.getIcon("reload"))
+
+        self.packagesList.header().setSortIndicator(
+            PipxWidget.PackageColumn, Qt.SortOrder.AscendingOrder
+        )
+
+        self.__showPipxVersion()
+
+        pipxPaths = self.__pipx.getPipxStrPaths()
+        self.venvsPathEdit.setText(pipxPaths["venvsPath"])
+        self.applicationsPathEdit.setText(pipxPaths["appsPath"])
+        self.manPagesPathEdit.setText(pipxPaths["manPath"])
+
+        self.__populatePackages()
+
+    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())
+        )
+
+    def __resizePackagesColumns(self):
+        """
+        Private method 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 __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"], package["python"]],
+            )
+            for app, appPath in package["apps"]:
+                itm = QTreeWidgetItem(topItem, [app])
+                itm.setData(0, PipxWidget.AppPathRole, appPath)
+        self.__resizePackagesColumns()
+
+    @pyqtSlot()
+    def on_refreshButton_clicked(self):
+        """
+        Private slot to refresh the packages list.
+        """
+        self.__showPipxVersion()
+
+        expandedPackages = []
+        for row in range(self.packagesList.topLevelItemCount()):
+            itm = self.packagesList.topLevelItem(row)
+            if itm.isExpanded():
+                expandedPackages.append(itm.text(PipxWidget.PackageColumn))
+
+        self.__populatePackages()
+
+        for row in range(self.packagesList.topLevelItemCount()):
+            itm = self.packagesList.topLevelItem(row)
+            if itm.text(PipxWidget.PackageColumn) in expandedPackages:
+                itm.setExpanded(True)
+        self.__resizePackagesColumns()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PipxInterface/PipxWidget.ui	Mon Jun 24 17:13:07 2024 +0200
@@ -0,0 +1,174 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PipxWidget</class>
+ <widget class="QWidget" name="PipxWidget">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>629</width>
+    <height>722</height>
+   </rect>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout">
+     <item>
+      <spacer name="horizontalSpacer_4">
+       <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="QLabel" name="pipxVersionLabel">
+       <property name="text">
+        <string/>
+       </property>
+      </widget>
+     </item>
+     <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="pipxMenuButton"/>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <widget class="QGroupBox" name="groupBox">
+     <property name="title">
+      <string>Paths Information</string>
+     </property>
+     <layout class="QGridLayout" name="gridLayout">
+      <item row="0" column="0">
+       <widget class="QLabel" name="label_2">
+        <property name="text">
+         <string>Virtual Environments:</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="1">
+       <widget class="QLineEdit" name="venvsPathEdit">
+        <property name="readOnly">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="0">
+       <widget class="QLabel" name="label_3">
+        <property name="text">
+         <string>Applications:</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="1">
+       <widget class="QLineEdit" name="applicationsPathEdit">
+        <property name="readOnly">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item row="2" column="0">
+       <widget class="QLabel" name="label_4">
+        <property name="text">
+         <string>Manual Pages:</string>
+        </property>
+       </widget>
+      </item>
+      <item row="2" column="1">
+       <widget class="QLineEdit" name="manPagesPathEdit">
+        <property name="readOnly">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <layout class="QHBoxLayout" name="horizontalLayout_2">
+     <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>
+     <item>
+      <widget class="QToolButton" name="refreshButton">
+       <property name="toolTip">
+        <string>Press to refresh the packages list.</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="horizontalSpacer_3">
+       <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="packagesList">
+     <property name="alternatingRowColors">
+      <bool>true</bool>
+     </property>
+     <property name="selectionMode">
+      <enum>QAbstractItemView::ExtendedSelection</enum>
+     </property>
+     <property name="sortingEnabled">
+      <bool>true</bool>
+     </property>
+     <column>
+      <property name="text">
+       <string>Package/Application</string>
+      </property>
+     </column>
+     <column>
+      <property name="text">
+       <string>Version</string>
+      </property>
+     </column>
+     <column>
+      <property name="text">
+       <string>Python Version</string>
+      </property>
+     </column>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
--- a/PluginPipxInterface.epj	Mon Jun 24 15:22:19 2024 +0200
+++ b/PluginPipxInterface.epj	Mon Jun 24 17:13:07 2024 +0200
@@ -38,7 +38,9 @@
       "README.*": "OTHERS",
       "makefile": "OTHERS"
     },
-    "FORMS": [],
+    "FORMS": [
+      "PipxInterface/PipxWidget.ui"
+    ],
     "HASH": "e670b8ea0fd5593abf0187483d113c50db352d90",
     "IDLPARAMS": {
       "DefinedNames": [],
@@ -60,9 +62,36 @@
     "MIXEDLANGUAGE": false,
     "OTHERS": [
       ".hgignore",
-      "PluginPipxInterface.epj"
+      "PipxInterface/icons/pipx22.svg",
+      "PipxInterface/icons/pipx48.svg",
+      "PipxInterface/icons/pipx96.svg",
+      "PluginPipxInterface.epj",
+      "changelog.md",
+      "pyproject.toml"
     ],
-    "OTHERTOOLSPARMS": {},
+    "OTHERTOOLSPARMS": {
+      "isort": {
+        "combine_as_imports": true,
+        "config_source": "pyproject",
+        "extend_skip_glob": [
+          "*/Ui_*.py"
+        ],
+        "known_first_party": [
+          "PipxInterface",
+          "eric7"
+        ],
+        "lines_between_types": 1,
+        "profile": "black",
+        "sort_order": "natural",
+        "supported_extensions": [
+          "py",
+          "pyi",
+          "pyx",
+          "pxd",
+          "pyw"
+        ]
+      }
+    },
     "PACKAGERSPARMS": {},
     "PROGLANGUAGE": "Python3",
     "PROJECTTYPE": "E7Plugin",
@@ -76,6 +105,9 @@
     },
     "RESOURCES": [],
     "SOURCES": [
+      "PipxInterface/Pipx.py",
+      "PipxInterface/PipxWidget.py",
+      "PipxInterface/Ui_PipxWidget.py",
       "PipxInterface/__init__.py",
       "PluginPipxInterface.py",
       "__init__.py"
--- a/PluginPipxInterface.py	Mon Jun 24 15:22:19 2024 +0200
+++ b/PluginPipxInterface.py	Mon Jun 24 17:13:07 2024 +0200
@@ -10,12 +10,25 @@
 import os
 import sysconfig
 
-from PyQt6.QtCore import QCoreApplication, QObject, QTranslator
+from PyQt6.QtCore import QCoreApplication, QObject, Qt, QTranslator
+from PyQt6.QtGui import QKeySequence
 
 from eric7 import Preferences
+from eric7.EricGui import EricPixmapCache
+from eric7.EricGui.EricAction import EricAction
 from eric7.EricWidgets.EricApplication import ericApp
 from eric7.SystemUtilities import OSUtilities
 
+try:
+    from eric7.UI.UserInterface import UserInterfaceSide
+
+    _Side = UserInterfaceSide.Right
+except ImportError:
+    # backward compatibility for eric < 24.2
+    from eric7.UI.UserInterface import UserInterface
+
+    _Side = UserInterface.RightSide
+
 # Start-Of-Header
 __header__ = {
     "name": "pipx Interface",
@@ -56,7 +69,7 @@
         "programEntry": True,
         "header": QCoreApplication.translate(
             "PluginPipxInterface", "PyPI Application Management"
-        ), 
+        ),
         "exe": pipx,
         "versionCommand": "--version",
         "versionStartsWith": "",
@@ -153,8 +166,44 @@
         @return tuple of None and activation status
         @rtype bool
         """
-        global error
+        global error, pipxInterfacePluginObject
         error = ""  # clear previous error
+        pipxInterfacePluginObject = self
+
+        from PipxInterface.PipxWidget import PipxWidget
+
+        self.__widget = PipxWidget(self, fromEric=True)
+        iconName = "pipx96" if self.__ui.getLayoutType() == "Sidebars" else "pipx22"
+        self.__ui.addSideWidget(
+            _Side,
+            self.__widget,
+            EricPixmapCache.getIcon(os.path.join("PipxInterface", "icons", iconName)),
+            self.tr("PyPI Application Management"),
+        )
+
+        self.__activateAct = EricAction(
+            self.tr("PyPI Application Management"),
+            self.tr("PyPI Application Management"),
+            QKeySequence(self.tr("Ctrl+Alt+Shift+A")),
+            0,
+            self,
+            "pipx_interface_activate",
+        )
+        self.__activateAct.setStatusTip(
+            self.tr("Switch the input focus to the PyPI Application Management window.")
+        )
+        self.__activateAct.setWhatsThis(
+            self.tr(
+                """<b>Activate PyPI Application Management</b>"""
+                """<p>This switches the input focus to the PyPI Application"""
+                """ Management window.</p>"""
+            )
+        )
+        self.__activateAct.triggered.connect(self.__activateWidget)
+
+        self.__ui.addEricActions([self.__activateAct], "ui")
+        menu = self.__ui.getMenu("subwindow")
+        menu.addAction(self.__activateAct)
 
         return None, True
 
@@ -162,7 +211,12 @@
         """
         Public method to deactivate this plug-in.
         """
-        pass
+        menu = self.__ui.getMenu("subwindow")
+        menu.removeAction(self.__activateAct)
+        self.__ui.removeEricActions([self.__activateAct], "ui")
+        self.__ui.removeSideWidget(self.__widget)
+
+        self.__initialize()
 
     def __loadTranslator(self):
         """
@@ -187,6 +241,22 @@
                     )
                     print("Using default.")
 
+    def __activateWidget(self):
+        """
+        Private slot to handle the activation of the MQTT Monitor.
+        """
+        uiLayoutType = self.__ui.getLayoutType()
+
+        if uiLayoutType == "Toolboxes":
+            self.__ui.rToolboxDock.show()
+            self.__ui.rToolbox.setCurrentWidget(self.__widget)
+        elif uiLayoutType == "Sidebars":
+            self.__ui.rightSidebar.show()
+            self.__ui.rightSidebar.setCurrentWidget(self.__widget)
+        else:
+            self.__widget.show()
+        self.__widget.setFocus(Qt.FocusReason.ActiveWindowFocusReason)
+
     def getPreferences(self, key):
         """
         Public method to retrieve the various settings values.

eric ide

mercurial