Mon, 24 Jun 2024 17:13:07 +0200
Added the main pipx interface widget and the pipx commands interface (basic variant each).
--- /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.