PipxInterface/Pipx.py

changeset 78
5efcdee9c170
parent 59
a5e7feb2310c
child 80
f59f1bcc4c6f
--- a/PipxInterface/Pipx.py	Sat Sep 07 19:29:57 2024 +0200
+++ b/PipxInterface/Pipx.py	Sun Sep 15 11:57:39 2024 +0200
@@ -8,25 +8,36 @@
 """
 
 import contextlib
+import functools
 import json
 import os
 import pathlib
 import sysconfig
 
-from PyQt6.QtCore import QObject, QProcess
+from PyQt6.QtCore import QObject, QProcess, pyqtSignal
 
 from eric7 import Preferences
 from eric7.EricWidgets import EricMessageBox
 from eric7.SystemUtilities import OSUtilities
 
+try:
+    from eric7.EricCore.EricProcess import EricProcess
+except ImportError:
+    from .PipxProcess import PipxProcess as EricProcess
+
 from .PipxExecDialog import PipxExecDialog
 
 
 class Pipx(QObject):
     """
-    Class implementing the pip GUI logic.
+    Class implementing the pipx interface.
+
+    @signal outdatedPackage(package:str, latestVer:str, oudatedDeps:bool) emitted with
+    the result of a check for outdated status of a package
     """
 
+    outdatedPackage = pyqtSignal(str, str, bool)
+
     def __init__(self, parent=None):
         """
         Constructor
@@ -38,6 +49,8 @@
 
         self.__ui = parent
 
+        self.__pipxProcesses = []
+
     ############################################################################
     ## Utility methods
     ############################################################################
@@ -49,7 +62,7 @@
         @return string containing the pipx version number
         @rtype str
         """
-        ok, output = self.runPipxProcess(["--version"])
+        ok, output = self.__runPipxProcess(["--version"])
         if ok:
             return output.strip()
         else:
@@ -114,9 +127,9 @@
 
         return pipx
 
-    def runPipxProcess(self, args):
+    def __runPipxProcess(self, args):
         """
-        Public method to execute pipx with the given arguments.
+        Private method to execute pipx with the given arguments.
 
         @param args list of command line arguments for pipx
         @type list of str
@@ -167,6 +180,106 @@
             return pathlib.Path(jsonDict["__Path__"])
         return jsonDict
 
+    def __runPipxAsyncProcess(self, args, callback=None, timeout=30000):
+        """
+        Private method to execute pipx with the given arguments asynchronously.
+
+        @param args list of command line arguments for pipx
+        @type list of str
+        @param callback reference to the function to be called a success flag and the
+            process output or error message (defaults to None)
+        @type function (optional)
+        @param timeout timeout for the process in milliseconds (defaults to 30000)
+        @type int (optional)
+        @return reference to the generated process object
+        @rtype QProcess
+        """
+        process = EricProcess(timeout=timeout)
+        process.finished.connect(
+            functools.partial(self.__asyncProcessFinished, callback, process)
+        )
+        process.errorOccurred.connect(
+            functools.partial(self.__asyncProcessError, process)
+        )
+        self.__pipxProcesses.append(process)
+        process.start(self.__getPipxExecutable(), args)
+        return process
+
+    def __asyncProcessError(self, process, error):
+        """
+        Private method to handle a process error signal.
+
+        @param process reference to the process
+        @type QProcess
+        @param error error that occurred
+        @type QProcess.ProcessError
+        """
+        if error == QProcess.ProcessError.FailedToStart:
+            with contextlib.suppress(ValueError):
+                self.__pipxProcesses.remove(process)
+            EricMessageBox.critical(
+                None, self.tr("pipx Start Error"), self.tr("pipx could not be started.")
+            )
+        else:
+            EricMessageBox.critical(
+                None,
+                self.tr("pipx Runtime Error"),
+                self.tr(
+                    "<p>The pipx process reported an error.</p><p>Error: {0}</p>"
+                ).format(process.errorString()),
+            )
+
+    def __asyncProcessFinished(self, callback, process, _exitCode, exitStatus):
+        """
+        Private method to handle the process finished signal.
+
+        @param callback reference to the function to be called a success flag and the
+            process output or error message
+        @type function
+        @param process reference to the process
+        @type QProcess
+        @param _exitCode exit code of the process
+        @type int
+        @param exitStatus exit status of the process
+        @type QProcess.ExitStatus
+        """
+        if process.timedOut():
+            msg = self.tr("pipx did not finish within {0} seconds.").format(
+                process.timeoutInterval() // 1_000
+            )
+            if callback:
+                callback(False, msg)
+            else:
+                EricMessageBox.critical(None, self.tr("pipx Timeout Error"), msg)
+
+        elif exitStatus == QProcess.ExitStatus.NormalExit:
+            ioEncoding = Preferences.getSystem("IOEncoding")
+            if process.exitCode() == 0:
+                output = str(process.readAllStandardOutput(), ioEncoding, "replace")
+                if callback:
+                    callback(True, output)
+            else:
+                error = str(process.readAllStandardError(), ioEncoding, "replace")
+                msg = self.tr("<p>Message: {0}</p>").format(error) if error else ""
+                if callback:
+                    callback(
+                        False,
+                        self.tr("<p>pipx exited with an error ({0}).</p>{1}").format(
+                            process.exitCode(), msg
+                        ),
+                    )
+                else:
+                    EricMessageBox.critical(
+                        None,
+                        self.tr("pipx Execution Error"),
+                        self.tr("<p>pipx exited with an error ({0}).</p>{1}").format(
+                            process.exitCode(), msg
+                        ),
+                    )
+
+        with contextlib.suppress(ValueError):
+            self.__pipxProcesses.remove(process)
+
     ############################################################################
     ## pipx interpreter list function (modified from original to work here)
     ############################################################################
@@ -219,7 +332,7 @@
 
         packages = []
 
-        ok, output = self.runPipxProcess(["list", "--json"])
+        ok, output = self.__runPipxProcess(["list", "--json"])
         if ok and output:
             with contextlib.suppress(json.JSONDecodeError):
                 data = json.loads(output, object_hook=self.__metadataDecoderHook)
@@ -343,7 +456,7 @@
             of failure
         @rtype tuple of (bool, str)
         """
-        ok, output = self.runPipxProcess(["list", "--json"])
+        ok, output = self.__runPipxProcess(["list", "--json"])
         if ok:
             try:
                 with open(specFile, "w") as f:
@@ -522,15 +635,27 @@
 
         @param package name of the package
         @type str
-        @return tuple containing the latest version in case the package is outdated
-            or None otherwise and a flag indicating any outdated dependencies
-        @rtype tuple of (str or None, bool)
         """
         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)
+        self.__runPipxAsyncProcess(
+            args, callback=functools.partial(self.__checkPackageOutdatedCb, package)
+        )
+
+    def __checkPackageOutdatedCb(self, package, ok, output):
+        """
+        Private method handling the pipx process output of a check for an outdated
+        package.
+
+        @param package name of the package
+        @type str
+        @param ok flag indicating the process ended successfully
+        @type bool
+        @param output output of the pipx process or an error message
+        @type str
+        """
         if not ok:
             EricMessageBox.information(
                 None,
@@ -540,15 +665,35 @@
                     "<p>Reason: {1}</p>"
                 ).format(package, output),
             )
-            return None, False
+            self.outdatedPackage.emit(package, "", False)
+            return
 
         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"], len(outdatedList) > 1
+                self.outdatedPackage.emit(
+                    package, outdatedPackage["latest_version"], len(outdatedList) > 1
+                )
+                return
+
+        self.outdatedPackage.emit(package, "", bool(outdatedList))
+
+    def getPackageVersion(self, package):
+        """
+        Public method to get the version of a package.
 
-        return None, bool(outdatedList)
+        @param package package name
+        @type str
+        @return package version
+        @rtype str
+        """
+        packagesList = self.__getPackageDependencies(package=package)
+        for pack in packagesList:
+            if pack["name"] == package:
+                return pack["version"]
+        else:
+            return ""
 
     def __getPackageDependencies(self, package, uptodate=False, outdated=False):
         """
@@ -572,7 +717,7 @@
         if Preferences.getPip("PipSearchIndex"):
             indexUrl = Preferences.getPip("PipSearchIndex") + "/simple"
             args += ["--index-url", indexUrl]
-        ok, output = self.runPipxProcess(args)
+        ok, output = self.__runPipxProcess(args)
         if not ok:
             EricMessageBox.information(
                 None,

eric ide

mercurial