Added functionality to create a spec metadata file and to use it for the 'install-all' function.

Thu, 27 Jun 2024 15:42:50 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Thu, 27 Jun 2024 15:42:50 +0200
changeset 10
89e0e6e5ca7a
parent 9
2ab7d3ac8283
child 11
6af0704c8175

Added functionality to create a spec metadata file and to use it for the 'install-all' function.

PipxInterface/Pipx.py file | annotate | diff | comparison | revisions
PipxInterface/PipxSpecInputDialog.py file | annotate | diff | comparison | revisions
PipxInterface/PipxWidget.py file | annotate | diff | comparison | revisions
PipxInterface/PipxWidget.ui file | annotate | diff | comparison | revisions
PipxInterface/Ui_PipxWidget.py file | annotate | diff | comparison | revisions
--- a/PipxInterface/Pipx.py	Wed Jun 26 18:40:48 2024 +0200
+++ b/PipxInterface/Pipx.py	Thu Jun 27 15:42:50 2024 +0200
@@ -10,7 +10,8 @@
 import contextlib
 import json
 import os
-import sys
+import pathlib
+##import sys
 import sysconfig
 
 from PyQt6.QtCore import QObject, QProcess
@@ -111,7 +112,7 @@
 
         return pipx
 
-    def runProcess(self, args):
+    def runPipxProcess(self, args):
         """
         Public method to execute pipx with the given arguments.
 
@@ -124,7 +125,7 @@
         ioEncoding = Preferences.getSystem("IOEncoding")
 
         process = QProcess()
-        process.start(sys.executable, ["-m", "pipx"] + args)
+        process.start(self.__getPipxExecutable(), args)
         procStarted = process.waitForStarted()
         if procStarted:
             finished = process.waitForFinished(30000)
@@ -150,6 +151,20 @@
 
         return False, self.tr("pipx could not be started.")
 
+    def __metadataDecoderHook(self, jsonDict):
+        """
+        Private method to allow the JSON decoding of Path objects of a spec metadata
+        file as created by 'pipx list --json'.
+
+        @param jsonDict JSON dictionary to be decoded
+        @type dict
+        @return decoded Path object or the dictionary unaltered
+        @rtype dict or pathlib.Path
+        """
+        if jsonDict.get("__type__") == "Path" and "__Path__" in jsonDict:
+            return pathlib.Path(jsonDict["__Path__"])
+        return jsonDict
+
     ############################################################################
     ## Command methods
     ############################################################################
@@ -163,11 +178,11 @@
         """
         packages = []
 
-        ok, output = self.runProcess(["list", "--json"])
+        ok, output = self.runPipxProcess(["list", "--json"])
         if ok:
             if output:
                 with contextlib.suppress(json.JSONDecodeError):
-                    data = json.loads(output)
+                    data = json.loads(output, object_hook=self.__metadataDecoderHook)
                     for venvName in data["venvs"]:
                         metadata = data["venvs"][venvName]["metadata"]
                         package = {
@@ -177,8 +192,7 @@
                             "python": metadata["python_version"],
                         }
                         for appPath in metadata["main_package"]["app_paths"]:
-                            path = appPath["__Path__"]
-                            package["apps"].append((os.path.basename(path), path))
+                            package["apps"].append((appPath.name, str(appPath)))
                         packages.append(package)
 
         return packages
@@ -265,7 +279,7 @@
             args.append("--force")
         if systemSitePackages:
             args.append("--system-site-packages")
-        args += specFile
+        args.append(specFile)
         dia = PipxExecDialog(self.tr("Install All Packages"))
         res = dia.startProcess(self.__getPipxExecutable(), args)
         if res:
@@ -281,7 +295,7 @@
             of failure
         @rtype tuple of (bool, str)
         """
-        ok, output = self.runProcess(["list", "--json"])
+        ok, output = self.runPipxProcess(["list", "--json"])
         if ok:
             try:
                 with open(specFile, "w") as f:
--- a/PipxInterface/PipxSpecInputDialog.py	Wed Jun 26 18:40:48 2024 +0200
+++ b/PipxInterface/PipxSpecInputDialog.py	Thu Jun 27 15:42:50 2024 +0200
@@ -36,6 +36,7 @@
 
         self.setWindowTitle(title)
         self.specFilePicker.setMode(EricPathPickerModes.OPEN_FILE_MODE)
+        self.specFilePicker.setFilters(self.tr("JSON Files (*.json);;All Files (*)"))
 
         self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(False)
 
--- a/PipxInterface/PipxWidget.py	Wed Jun 26 18:40:48 2024 +0200
+++ b/PipxInterface/PipxWidget.py	Thu Jun 27 15:42:50 2024 +0200
@@ -7,10 +7,13 @@
 Module implementing the pipx management widget.
 """
 
+import os
+
 from PyQt6.QtCore import Qt, pyqtSlot
 from PyQt6.QtWidgets import QDialog, QMenu, QTreeWidgetItem, QWidget
 
 from eric7.EricGui import EricPixmapCache
+from eric7.EricWidgets import EricFileDialog, EricMessageBox
 
 from .Pipx import Pipx
 from .PipxAppStartDialog import PipxAppStartDialog
@@ -55,6 +58,13 @@
         # TODO: set the various icons
         self.pipxMenuButton.setIcon(EricPixmapCache.getIcon("superMenu"))
         self.refreshButton.setIcon(EricPixmapCache.getIcon("reload"))
+        self.installButton.setIcon(EricPixmapCache.getIcon("plus"))
+        self.upgradeButton.setIcon(EricPixmapCache.getIcon("upgrade"))
+        self.uninstallButton.setIcon(EricPixmapCache.getIcon("minus"))
+
+        self.installButton.clicked.connect(self.__installPackages)
+        self.upgradeButton.clicked.connect(self.__upgradePackages)
+        self.uninstallButton.clicked.connect(self.__uninstallPackages)
 
         self.pipxMenuButton.setAutoRaise(True)
         self.pipxMenuButton.setShowMenuInside(True)
@@ -73,6 +83,7 @@
         self.interpretersPathEdit.setText(pipxPaths["pythonPath"])
 
         self.__populatePackages()
+        self.on_packagesList_itemSelectionChanged()
 
     #######################################################################
     ## Menu related methods below
@@ -154,10 +165,11 @@
         """
         Private slot to set the action enabled status.
         """
-        hasPackagesSelected = bool(self.__selectedPackages())
-        self.__reinstallPackagesAct.setEnabled(hasPackagesSelected)
-        self.__upgradePackagesAct.setEnabled(hasPackagesSelected)
-        self.__uninstallPackagesAct.setEnabled(hasPackagesSelected)
+        selectedPackages = self.__selectedPackages()
+
+        self.__reinstallPackagesAct.setEnabled(len(selectedPackages) == 1)
+        self.__upgradePackagesAct.setEnabled(bool(selectedPackages))
+        self.__uninstallPackagesAct.setEnabled(len(selectedPackages) == 1)
 
     @pyqtSlot()
     def __installPackages(self):
@@ -183,13 +195,12 @@
         """
         Private slot to install all packages listed in a specification file.
         """
-        # TODO: not implemented yet
         from .PipxSpecInputDialog import PipxSpecInputDialog
 
         dlg = PipxSpecInputDialog(self.tr("Install All Packages"))
         if dlg.exec() == QDialog.DialogCode.Accepted:
             specFile, pyVersion, fetchMissing, force, systemSitePackages = dlg.getData()
-            self.__pipx.installPackages(
+            self.__pipx.installAllPackages(
                 specFile,
                 interpreterVersion=pyVersion,
                 fetchMissingInterpreter=fetchMissing,
@@ -203,8 +214,51 @@
         """
         Private slot to create a spec metadata file needed by 'pipx install-all'.
         """
-        # TODO: not implemented yet
-        pass
+        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 __reinstallPackages(self):
@@ -336,6 +390,16 @@
             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.
+        """
+        selectedPackages = self.__selectedPackages()
+
+        self.upgradeButton.setEnabled(bool(selectedPackages))
+        self.uninstallButton.setEnabled(len(selectedPackages) == 1)
+
     def __selectedPackages(self):
         """
         Private method to determine the list of selected packages.
--- a/PipxInterface/PipxWidget.ui	Wed Jun 26 18:40:48 2024 +0200
+++ b/PipxInterface/PipxWidget.ui	Thu Jun 27 15:42:50 2024 +0200
@@ -143,6 +143,27 @@
       </widget>
      </item>
      <item>
+      <widget class="QToolButton" name="installButton">
+       <property name="toolTip">
+        <string>Press to install packages.</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QToolButton" name="upgradeButton">
+       <property name="toolTip">
+        <string>Press to upgrade the selected packages.</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QToolButton" name="uninstallButton">
+       <property name="toolTip">
+        <string>Press to uninstall the selected packages.</string>
+       </property>
+      </widget>
+     </item>
+     <item>
       <spacer name="horizontalSpacer_3">
        <property name="orientation">
         <enum>Qt::Horizontal</enum>
@@ -197,6 +218,9 @@
  <tabstops>
   <tabstop>packagesList</tabstop>
   <tabstop>refreshButton</tabstop>
+  <tabstop>installButton</tabstop>
+  <tabstop>upgradeButton</tabstop>
+  <tabstop>uninstallButton</tabstop>
   <tabstop>pipxMenuButton</tabstop>
   <tabstop>venvsPathEdit</tabstop>
   <tabstop>applicationsPathEdit</tabstop>
--- a/PipxInterface/Ui_PipxWidget.py	Wed Jun 26 18:40:48 2024 +0200
+++ b/PipxInterface/Ui_PipxWidget.py	Thu Jun 27 15:42:50 2024 +0200
@@ -70,6 +70,15 @@
         self.refreshButton = QtWidgets.QToolButton(parent=PipxWidget)
         self.refreshButton.setObjectName("refreshButton")
         self.horizontalLayout_2.addWidget(self.refreshButton)
+        self.installButton = QtWidgets.QToolButton(parent=PipxWidget)
+        self.installButton.setObjectName("installButton")
+        self.horizontalLayout_2.addWidget(self.installButton)
+        self.upgradeButton = QtWidgets.QToolButton(parent=PipxWidget)
+        self.upgradeButton.setObjectName("upgradeButton")
+        self.horizontalLayout_2.addWidget(self.upgradeButton)
+        self.uninstallButton = QtWidgets.QToolButton(parent=PipxWidget)
+        self.uninstallButton.setObjectName("uninstallButton")
+        self.horizontalLayout_2.addWidget(self.uninstallButton)
         spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
         self.horizontalLayout_2.addItem(spacerItem3)
         self.verticalLayout.addLayout(self.horizontalLayout_2)
@@ -82,7 +91,10 @@
         self.retranslateUi(PipxWidget)
         QtCore.QMetaObject.connectSlotsByName(PipxWidget)
         PipxWidget.setTabOrder(self.packagesList, self.refreshButton)
-        PipxWidget.setTabOrder(self.refreshButton, self.pipxMenuButton)
+        PipxWidget.setTabOrder(self.refreshButton, self.installButton)
+        PipxWidget.setTabOrder(self.installButton, self.upgradeButton)
+        PipxWidget.setTabOrder(self.upgradeButton, self.uninstallButton)
+        PipxWidget.setTabOrder(self.uninstallButton, self.pipxMenuButton)
         PipxWidget.setTabOrder(self.pipxMenuButton, self.venvsPathEdit)
         PipxWidget.setTabOrder(self.venvsPathEdit, self.applicationsPathEdit)
         PipxWidget.setTabOrder(self.applicationsPathEdit, self.manPagesPathEdit)
@@ -96,6 +108,9 @@
         self.label_4.setText(_translate("PipxWidget", "Manual Pages:"))
         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.upgradeButton.setToolTip(_translate("PipxWidget", "Press to upgrade the selected packages."))
+        self.uninstallButton.setToolTip(_translate("PipxWidget", "Press to uninstall the selected packages."))
         self.packagesList.setSortingEnabled(True)
         self.packagesList.headerItem().setText(0, _translate("PipxWidget", "Package/Application"))
         self.packagesList.headerItem().setText(1, _translate("PipxWidget", "Version"))

eric ide

mercurial