Implemented the profiling and code coverage interface of the dialogs to the eric-ide server. server

Thu, 15 Feb 2024 13:59:02 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Thu, 15 Feb 2024 13:59:02 +0100
branch
server
changeset 10574
622e59b51640
parent 10573
c4f503f40caf
child 10575
abde60847db6

Implemented the profiling and code coverage interface of the dialogs to the eric-ide server.

eric7.epj file | annotate | diff | comparison | revisions
src/eric7/DataViews/PyCoverageDialog.py file | annotate | diff | comparison | revisions
src/eric7/DataViews/PyProfileDialog.py file | annotate | diff | comparison | revisions
src/eric7/Plugins/PluginCodeStyleChecker.py file | annotate | diff | comparison | revisions
src/eric7/Plugins/PluginSyntaxChecker.py file | annotate | diff | comparison | revisions
src/eric7/QScintilla/Editor.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServer/EricRequestCategory.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServer/EricServer.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServer/EricServerCoverageRequestHandler.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServer/EricServerDebuggerRequestHandler.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServerInterface/EricServerCoverageInterface.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServerInterface/EricServerDebuggerInterface.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServerInterface/EricServerFileSystemInterface.py file | annotate | diff | comparison | revisions
src/eric7/RemoteServerInterface/EricServerInterface.py file | annotate | diff | comparison | revisions
src/eric7/Utilities/__init__.py file | annotate | diff | comparison | revisions
--- a/eric7.epj	Wed Feb 14 10:24:23 2024 +0100
+++ b/eric7.epj	Thu Feb 15 13:59:02 2024 +0100
@@ -2126,10 +2126,12 @@
       "src/eric7/QtHelpInterface/__init__.py",
       "src/eric7/RemoteServer/EricRequestCategory.py",
       "src/eric7/RemoteServer/EricServer.py",
+      "src/eric7/RemoteServer/EricServerCoverageRequestHandler.py",
       "src/eric7/RemoteServer/EricServerDebuggerRequestHandler.py",
       "src/eric7/RemoteServer/EricServerFileSystemRequestHandler.py",
       "src/eric7/RemoteServer/__init__.py",
       "src/eric7/RemoteServerInterface/EricServerConnectionDialog.py",
+      "src/eric7/RemoteServerInterface/EricServerCoverageInterface.py",
       "src/eric7/RemoteServerInterface/EricServerDebuggerInterface.py",
       "src/eric7/RemoteServerInterface/EricServerFileDialog.py",
       "src/eric7/RemoteServerInterface/EricServerFileSystemInterface.py",
--- a/src/eric7/DataViews/PyCoverageDialog.py	Wed Feb 14 10:24:23 2024 +0100
+++ b/src/eric7/DataViews/PyCoverageDialog.py	Thu Feb 15 13:59:02 2024 +0100
@@ -25,6 +25,9 @@
 
 from eric7.EricWidgets import EricMessageBox
 from eric7.EricWidgets.EricApplication import ericApp
+from eric7.RemoteServerInterface.EricServerCoverageInterface import (
+    EricServerCoverageError,
+)
 from eric7.SystemUtilities import FileSystemUtilities
 
 from .Ui_PyCoverageDialog import Ui_PyCoverageDialog
@@ -57,7 +60,6 @@
         self.resultList.headerItem().setText(self.resultList.columnCount(), "")
 
         self.cancelled = False
-        self.path = "."
         self.reload = False
 
         self.excludeList = ["# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]"]
@@ -77,6 +79,11 @@
         self.__menu.addAction(self.tr("Erase Coverage Info"), self.__erase)
         self.resultList.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
         self.resultList.customContextMenuRequested.connect(self.__showContextMenu)
+ 
+        # eric-ide server interface
+        self.__serverCoverageInterface = ericApp().getObject(
+            "EricServer"
+        ).getServiceInterface("Coverage")
 
     def __format_lines(self, lines):
         """
@@ -143,7 +150,7 @@
         @param coverage percent of coverage
         @type int
         @param excluded list of excluded lines
-        @type str
+        @type list of int
         @param missing list of lines without coverage
         @type str
         """
@@ -154,7 +161,7 @@
                 str(statements),
                 str(executed),
                 "{0:.0f}%".format(coverage),
-                excluded,
+                excluded and self.__format_lines(excluded) or "",
                 missing,
             ],
         )
@@ -192,20 +199,16 @@
             else "{0}.coverage".format(os.path.splitext(cfn)[0])
         )
 
+        # TODO: adapt this to remote server
         if isinstance(fn, list):
             files = fn
-            self.path = os.path.dirname(cfn)
-        elif os.path.isdir(fn):
+        elif os.path.isdir(fn) and not FileSystemUtilities.isRemoteFileName(self.cfn):
+            # This case is not yet supported
             files = FileSystemUtilities.direntries(fn, True, "*.py", False)
-            self.path = fn
         else:
             files = [fn]
-            self.path = os.path.dirname(cfn)
         files.sort()
 
-        cover = Coverage(data_file=self.cfn)
-        cover.load()
-
         # set the exclude pattern
         self.excludeCombo.clear()
         self.excludeCombo.addItems(self.excludeList)
@@ -217,7 +220,23 @@
         total_executed = 0
         total_exceptions = 0
 
-        cover.exclude(self.excludeList[0])
+        if FileSystemUtilities.isRemoteFileName(self.cfn):
+            ok, error = self.__serverCoverageInterface.loadCoverageData(
+                self.cfn, self.excludeList[0]
+            )
+            if not ok:
+                EricMessageBox.critical(
+                    self,
+                    self.tr("Load Coverage Data"),
+                    self.tr(
+                        "<p>The coverage data could not be loaded from file"
+                        " <b>{0}</b>.</p><p>Reason: {1}</p>"
+                    ).format(self.cfn, error),
+                )
+        else:
+            cover = Coverage(data_file=self.cfn)
+            cover.load()
+            cover.exclude(self.excludeList[0])
 
         try:
             # disable updates of the list for speed
@@ -231,18 +250,24 @@
                     return
 
                 try:
-                    statements, excluded, missing, readable = cover.analysis2(file)[1:]
-                    readableEx = excluded and self.__format_lines(excluded) or ""
+                    if FileSystemUtilities.isRemoteFileName(self.cfn):
+                        file, statements, excluded, missing, readable = (
+                            self.__serverCoverageInterface.analyzeFile(file)
+                        )
+                    else:
+                        statements, excluded, missing, readable = (
+                            cover.analysis2(file)[1:]
+                        )
                     n = len(statements)
                     m = n - len(missing)
                     pc = 100.0 * m / n if n > 0 else 100.0
                     self.__createResultItem(
-                        file, str(n), str(m), pc, readableEx, readable
+                        file, str(n), str(m), pc, excluded, readable
                     )
 
                     total_statements += n
                     total_executed += m
-                except CoverageException:
+                except (CoverageException, EricServerCoverageError):
                     total_exceptions += 1
 
                 self.checkProgress.setValue(progress)
--- a/src/eric7/DataViews/PyProfileDialog.py	Wed Feb 14 10:24:23 2024 +0100
+++ b/src/eric7/DataViews/PyProfileDialog.py	Thu Feb 15 13:59:02 2024 +0100
@@ -22,7 +22,8 @@
 )
 
 from eric7.EricWidgets import EricMessageBox
-from eric7.SystemUtilities import PythonUtilities
+from eric7.EricWidgets.EricApplication import ericApp
+from eric7.SystemUtilities import FileSystemUtilities, PythonUtilities
 
 from .Ui_PyProfileDialog import Ui_PyProfileDialog
 
@@ -104,6 +105,11 @@
         self.summaryList.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
         self.summaryList.customContextMenuRequested.connect(self.__showContextMenu)
 
+        # eric-ide server interface
+        self.__serverFsInterface = ericApp().getObject(
+            "EricServer"
+        ).getServiceInterface("FileSystem")
+
     def __createResultItem(
         self,
         calls,
@@ -265,7 +271,16 @@
         self.basename = os.path.splitext(pfn)[0]
 
         fname = "{0}.profile".format(self.basename)
-        if not os.path.exists(fname):
+        if (
+            (
+                FileSystemUtilities.isRemoteFileName(fname)
+                and not self.__serverFsInterface.exists(fname)
+            )
+            or (
+                FileSystemUtilities.isPlainFileName(fname)
+                and not os.path.exists(fname)
+            )
+        ):
             EricMessageBox.warning(
                 self,
                 self.tr("Profile Results"),
@@ -277,8 +292,12 @@
             self.close()
             return
         try:
-            with open(fname, "rb") as f:
-                self.stats = pickle.load(f)  # secok
+            if FileSystemUtilities.isRemoteFileName(fname):
+                data = self.__serverFsInterface.readFile(fname)
+                self.stats = pickle.loads(data)
+            else:
+                with open(fname, "rb") as f:
+                    self.stats = pickle.load(f)  # secok
         except (EOFError, OSError, pickle.PickleError):
             EricMessageBox.critical(
                 self,
@@ -291,7 +310,10 @@
             self.close()
             return
 
-        self.file = fn
+        if FileSystemUtilities.isRemoteFileName(fname):
+            self.file = FileSystemUtilities.plainFileName(fn)
+        else:
+            self.file = fn
         self.__populateLists()
         self.__finish()
 
--- a/src/eric7/Plugins/PluginCodeStyleChecker.py	Wed Feb 14 10:24:23 2024 +0100
+++ b/src/eric7/Plugins/PluginCodeStyleChecker.py	Thu Feb 15 13:59:02 2024 +0100
@@ -476,6 +476,7 @@
         if menuName == "Checks":
             if self.__editorAct not in menu.actions():
                 menu.addAction(self.__editorAct)
+            # TODO: disable the action for eric-ide server files
             self.__editorAct.setEnabled(editor.isPyFile())
 
     def __editorCodeStyleCheck(self):
--- a/src/eric7/Plugins/PluginSyntaxChecker.py	Wed Feb 14 10:24:23 2024 +0100
+++ b/src/eric7/Plugins/PluginSyntaxChecker.py	Thu Feb 15 13:59:02 2024 +0100
@@ -408,6 +408,7 @@
         if menuName == "Checks":
             if self.__editorAct not in menu.actions():
                 menu.addAction(self.__editorAct)
+            # TODO: disable the action for eric-ide server files
             self.__editorAct.setEnabled(
                 editor.getLanguage() in self.syntaxCheckService.getLanguages()
             )
--- a/src/eric7/QScintilla/Editor.py	Wed Feb 14 10:24:23 2024 +0100
+++ b/src/eric7/QScintilla/Editor.py	Thu Feb 15 13:59:02 2024 +0100
@@ -1190,7 +1190,9 @@
         """
         menu = QMenu(self.tr("Show"))
 
-        menu.addAction(self.tr("Code metrics..."), self.__showCodeMetrics)
+        self.codeMetricsAct = menu.addAction(
+            self.tr("Code metrics..."), self.__showCodeMetrics
+        )
         self.coverageMenuAct = menu.addAction(
             self.tr("Code coverage..."), self.__showCodeCoverage
         )
@@ -6416,10 +6418,12 @@
                 self.menuActs["Show"].setEnabled(True)
             else:
                 self.menuActs["Show"].setEnabled(False)
+            # TODO: disable that for server files
             if self.fileName and (self.isPyFile() or self.isRubyFile()):
                 self.menuActs["Diagrams"].setEnabled(True)
             else:
                 self.menuActs["Diagrams"].setEnabled(False)
+            # TODO: disable 'Check' and 'Code Formatting' for server files
         if not self.miniMenu:
             if self.lexer_ is not None:
                 self.menuActs["Comment"].setEnabled(self.lexer_.canBlockComment())
@@ -6511,6 +6515,12 @@
         )
         self.coverageHideAnnotationMenuAct.setEnabled(len(self.notcoveredMarkers) > 0)
 
+        # disable actions not supporting eric-ide server
+        self.codeMetricsAct.setEnabled(
+            False if fn is None else FileSystemUtilities.isPlainFileName(fn)
+        )
+
+        # TODO: disable action in Radon plugin for server files
         self.showMenu.emit("Show", self.menuShow, self)
 
     @pyqtSlot()
--- a/src/eric7/RemoteServer/EricRequestCategory.py	Wed Feb 14 10:24:23 2024 +0100
+++ b/src/eric7/RemoteServer/EricRequestCategory.py	Thu Feb 15 13:59:02 2024 +0100
@@ -18,6 +18,7 @@
     FileSystem = 0
     Project = 1
     Debugger = 2
+    Coverage = 3
 
     Echo = 253
     Server = 254
--- a/src/eric7/RemoteServer/EricServer.py	Wed Feb 14 10:24:23 2024 +0100
+++ b/src/eric7/RemoteServer/EricServer.py	Thu Feb 15 13:59:02 2024 +0100
@@ -20,6 +20,7 @@
 from eric7.UI.Info import Version
 
 from .EricRequestCategory import EricRequestCategory
+from .EricServerCoverageRequestHandler import EricServerCoverageRequestHandler
 from .EricServerDebuggerRequestHandler import EricServerDebuggerRequestHandler
 from .EricServerFileSystemRequestHandler import EricServerFileSystemRequestHandler
 
@@ -65,6 +66,13 @@
             self.__fileSystemRequestHandler.handleRequest,
         )
 
+        # create and register the 'Coverage' request handler
+        self.__coverageRequestHandler = EricServerCoverageRequestHandler(self)
+        self.registerRequestHandler(
+            EricRequestCategory.Coverage,
+            self.__coverageRequestHandler.handleRequest,
+        )
+
         # TODO: 'Project' handler not implemented yet
         # TODO: implement an 'EditorConfig' handler (?)
 
@@ -331,6 +339,9 @@
     def __closeIdeConnection(self, shutdown=False):
         """
         Private method to close the connection to an eric-ide.
+        
+        @param shutdown flag indicating a shutdown process
+        @type bool
         """
         if self.__connection is not None:
             self.__selector.unregister(self.__connection)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServer/EricServerCoverageRequestHandler.py	Thu Feb 15 13:59:02 2024 +0100
@@ -0,0 +1,172 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the code coverage request handler of the eric-ide server.
+"""
+
+from coverage import Coverage
+from coverage.misc import CoverageException
+
+from eric7.SystemUtilities import FileSystemUtilities
+
+from .EricRequestCategory import EricRequestCategory
+
+
+class EricServerCoverageRequestHandler:
+    """
+    Class implementing the code coverage request handler of the eric-ide server.
+    """
+
+    def __init__(self, server):
+        """
+        Constructor
+
+        @param server reference to the eric-ide server object
+        @type EricServer
+        """
+        self.__server = server
+
+        self.__requestMethodMapping = {
+            "LoadData": self.__loadCoverageData,
+            "AnalyzeFile": self.__analyzeFile,
+            "AnalyzeFiles": self.__analyzeFiles,
+            "AnalyzeDirectory": self.__analyzeDirectory,
+        }
+
+        self.__cover = None
+
+    def handleRequest(self, request, params, reqestUuid):
+        """
+        Public method handling the received file system requests.
+
+        @param request request name
+        @type str
+        @param params dictionary containing the request parameters
+        @type dict
+        @param reqestUuid UUID of the associated request as sent by the eric IDE
+        @type str
+        """
+        try:
+            result = self.__requestMethodMapping[request](params)
+            self.__server.sendJson(
+                category=EricRequestCategory.Coverage,
+                reply=request,
+                params=result,
+                reqestUuid=reqestUuid,
+            )
+
+        except KeyError:
+            self.__server.sendJson(
+                category=EricRequestCategory.Coverage,
+                reply=request,
+                params={"Error": f"Request type '{request}' is not supported."},
+            )
+
+    def __loadCoverageData(self, params):
+        """
+        Private method to load the data collected by a code coverage run.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        if self.__cover is not None:
+            del self.__cover
+            self.__cover = None
+
+        try:
+            self.__cover = Coverage(data_file=params["data_file"])
+            self.__cover.load()
+            self.__cover.exclude(params["exclude"])
+            return {"ok": True}
+        except CoverageException as err:
+            return {
+                "ok": False,
+                "error": str(err),
+            }
+
+    def __analyzeFile(self, params):
+        """
+        Private method to analyze a single file.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        if self.__cover is None:
+            return {
+                "ok": False,
+                "error": "Coverage data has to be loaded first.",
+            }
+
+        try:
+            return {
+                "ok": True,
+                "result": self.__cover.analysis2(params["filename"]),
+            }
+        except CoverageException as err:
+            return {
+                "ok": False,
+                "error": str(err),
+            }
+
+    def __analyzeFiles(self, params):
+        """
+        Private method to analyze a list of files.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        # TODO: not implemented yet
+        if self.__cover is None:
+            return {
+                "ok": False,
+                "error": "Coverage data has to be loaded first.",
+            }
+
+        try:
+            return {
+                "ok": True,
+                "results": [self.__cover.analysis2(f) for f in params["filenames"]],
+            }
+        except CoverageException as err:
+            return {
+                "ok": False,
+                "error": str(err),
+            }
+
+    def __analyzeDirectory(self, params):
+        """
+        Private method to analyze files of a directory tree.
+
+        @param params dictionary containing the request data
+        @type dict
+        @return dictionary containing the reply data
+        @rtype dict
+        """
+        # TODO: not implemented yet
+        if self.__cover is None:
+            return {
+                "ok": False,
+                "error": "Coverage data has to be loaded first.",
+            }
+
+        files = FileSystemUtilities.direntries(params["directory"], True, "*.py", False)
+
+        try:
+            return {
+                "ok": True,
+                "results": [self.__cover.analysis2(f) for f in files],
+            }
+        except CoverageException as err:
+            return {
+                "ok": False,
+                "error": str(err),
+            }
--- a/src/eric7/RemoteServer/EricServerDebuggerRequestHandler.py	Wed Feb 14 10:24:23 2024 +0100
+++ b/src/eric7/RemoteServer/EricServerDebuggerRequestHandler.py	Thu Feb 15 13:59:02 2024 +0100
@@ -115,7 +115,9 @@
             address=address,
             handler=self.__serviceDbgClientConnection,
         )
-        self.__server.getSelector().register(connection, selectors.EVENT_READ, data=data)
+        self.__server.getSelector().register(
+            connection, selectors.EVENT_READ, data=data
+        )
 
     def __serviceDbgClientConnection(self, key):
         """
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/eric7/RemoteServerInterface/EricServerCoverageInterface.py	Thu Feb 15 13:59:02 2024 +0100
@@ -0,0 +1,228 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2024 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing the code coverage interface to the eric-ide server.
+"""
+
+import contextlib
+
+from PyQt6.QtCore import QEventLoop, QObject
+
+from eric7.RemoteServer.EricRequestCategory import EricRequestCategory
+from eric7.SystemUtilities import FileSystemUtilities
+
+
+class EricServerCoverageError(Exception):
+    """
+    Class defining a substitute exception for coverage errors of the server.
+    """
+
+    pass
+
+
+class EricServerCoverageInterface(QObject):
+    """
+    Class implementing the code coverage interface to the eric-ide server.
+    """
+
+    def __init__(self, serverInterface):
+        """
+        Constructor
+
+        @param serverInterface reference to the eric-ide server interface
+        @type EricServerInterface
+        """
+        super().__init__(parent=serverInterface)
+
+        self.__serverInterface = serverInterface
+
+    def loadCoverageData(self, dataFile, excludePattern):
+        """
+        Public method to tell the server to load the coverage data for a later analysis.
+
+        @param dataFile name of the data file to be loaded
+        @type str
+        @param excludePattern regular expression determining files to be excluded
+        @type str
+        @return tuple containing a success flag and an error message
+        @rtype tuple of (bool, str)
+        """
+        loop = QEventLoop()
+        ok = False
+        error = ""
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal ok, error
+
+            if reply == "LoadData":
+                ok = params["ok"]
+                with contextlib.suppress(KeyError):
+                    error = params["error"]
+                loop.quit()
+
+        self.__serverInterface.sendJson(
+            category=EricRequestCategory.Coverage,
+            request="LoadData",
+            params={
+                "data_file": FileSystemUtilities.plainFileName(dataFile),
+                "exclude": excludePattern,
+            },
+            callback=callback,
+        )
+
+        loop.exec()
+        return ok, error
+
+    def analyzeFile(self, filename):
+        """
+        Public method to analyze the code coverage of one file.
+
+        @param filename name of the file to be analyzed
+        @type str
+        @return list containing coverage result as reported by Coverage.analysis2()
+        @rtype list of [str, list of int, list of int, list of int, str]
+        @exception EricServerCoverageException raised to indicate a coverage exception
+        """
+        loop = QEventLoop()
+        ok = False
+        error = ""
+        result = None
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal ok, error, result
+
+            if reply == "AnalyzeFile":
+                ok = params["ok"]
+                if ok:
+                    result = params["result"]
+                else:
+                    error = params["error"]
+                loop.quit()
+
+        self.__serverInterface.sendJson(
+            category=EricRequestCategory.Coverage,
+            request="AnalyzeFile",
+            params={"filename": FileSystemUtilities.plainFileName(filename)},
+            callback=callback,
+        )
+
+        loop.exec()
+        if not ok:
+            raise EricServerCoverageError(error)
+
+        return result
+
+    def analyzeFiles(self, filenames):
+        """
+        Public method to analyze the code coverage of a list of files.
+
+        @param filenames list of file names to be analyzed
+        @type str
+        @return lists containing coverage results as reported by Coverage.analysis2()
+        @rtype list of [list of [str, list of int, list of int, list of int, str]]
+        @exception EricServerCoverageException raised to indicate a coverage exception
+        """
+        loop = QEventLoop()
+        ok = False
+        error = ""
+        result = None
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal ok, error, result
+
+            if reply == "AnalyzeFiles":
+                ok = params["ok"]
+                if ok:
+                    result = params["results"]
+                else:
+                    error = params["error"]
+                loop.quit()
+
+        self.__serverInterface.sendJson(
+            category=EricRequestCategory.Coverage,
+            request="AnalyzeFiles",
+            params={
+                "filenames": [FileSystemUtilities.plainFileName(f) for f in filenames]
+            },
+            callback=callback,
+        )
+
+        loop.exec()
+        if not ok:
+            raise EricServerCoverageError(error)
+
+        return result
+
+    def analyzeDirectory(self, directory):
+        """
+        Public method to analyze the code coverage of a directory.
+
+        @param directory directory name to be analyzed
+        @type str
+        @return lists containing coverage results as reported by Coverage.analysis2()
+        @rtype list of [list of [str, list of int, list of int, list of int, str]]
+        @exception EricServerCoverageException raised to indicate a coverage exception
+        """
+        loop = QEventLoop()
+        ok = False
+        error = ""
+        result = None
+
+        def callback(reply, params):
+            """
+            Function to handle the server reply
+
+            @param reply name of the server reply
+            @type str
+            @param params dictionary containing the reply data
+            @type dict
+            """
+            nonlocal ok, error, result
+
+            if reply == "AnalyzeDirectory":
+                ok = params["ok"]
+                if ok:
+                    result = params["results"]
+                else:
+                    error = params["error"]
+                loop.quit()
+
+        self.__serverInterface.sendJson(
+            category=EricRequestCategory.Coverage,
+            request="AnalyzeDirectory",
+            params={"directory": FileSystemUtilities.plainFileName(directory)},
+            callback=callback,
+        )
+
+        loop.exec()
+        if not ok:
+            raise EricServerCoverageError(error)
+
+        return result
--- a/src/eric7/RemoteServerInterface/EricServerDebuggerInterface.py	Wed Feb 14 10:24:23 2024 +0100
+++ b/src/eric7/RemoteServerInterface/EricServerDebuggerInterface.py	Thu Feb 15 13:59:02 2024 +0100
@@ -19,6 +19,13 @@
 class EricServerDebuggerInterface(QObject):
     """
     Class implementing the file system interface to the eric-ide server.
+
+    @signal debugClientResponse(response:str) emitted to relay a response of
+        the remote debug client
+    @signal debugClientDisconnected(debuggerId:str) emitted when a remote debug
+        client did disconnect from the eric-ide server
+    @signal lastClientExited() emitted to indicate that the last debug client of
+        the eric-ide server exited
     """
 
     debugClientResponse = pyqtSignal(str)
--- a/src/eric7/RemoteServerInterface/EricServerFileSystemInterface.py	Wed Feb 14 10:24:23 2024 +0100
+++ b/src/eric7/RemoteServerInterface/EricServerFileSystemInterface.py	Thu Feb 15 13:59:02 2024 +0100
@@ -13,6 +13,7 @@
 from PyQt6.QtCore import QEventLoop, QObject
 
 from eric7.RemoteServer.EricRequestCategory import EricRequestCategory
+from eric7.SystemUtilities import FileSystemUtilities
 
 
 # TODO: sanitize all file names with FileSystemUtilities.plainFileName()
@@ -100,7 +101,7 @@
         self.__serverInterface.sendJson(
             category=EricRequestCategory.FileSystem,
             request="Chdir",
-            params={"directory": directory},
+            params={"directory": FileSystemUtilities.plainFileName(directory)},
             callback=callback,
         )
 
@@ -155,7 +156,7 @@
         self.__serverInterface.sendJson(
             category=EricRequestCategory.FileSystem,
             request="Listdir",
-            params={"directory": directory},
+            params={"directory": FileSystemUtilities.plainFileName(directory)},
             callback=callback,
         )
 
@@ -204,7 +205,10 @@
         self.__serverInterface.sendJson(
             category=EricRequestCategory.FileSystem,
             request="Stat",
-            params={"filename": filename, "st_names": stNames},
+            params={
+                "filename": FileSystemUtilities.plainFileName(filename),
+                "st_names": stNames,
+            },
             callback=callback,
         )
 
@@ -244,7 +248,7 @@
         self.__serverInterface.sendJson(
             category=EricRequestCategory.FileSystem,
             request="Exists",
-            params={"name": name},
+            params={"name": FileSystemUtilities.plainFileName(name)},
             callback=callback,
         )
 
@@ -295,7 +299,7 @@
         self.__serverInterface.sendJson(
             category=EricRequestCategory.FileSystem,
             request="Access",
-            params={"name": name, "modes": modes},
+            params={"name": FileSystemUtilities.plainFileName(name), "modes": modes},
             callback=callback,
         )
 
@@ -335,7 +339,7 @@
         self.__serverInterface.sendJson(
             category=EricRequestCategory.FileSystem,
             request="Mkdir",
-            params={"directory": directory},
+            params={"directory": FileSystemUtilities.plainFileName(directory)},
             callback=callback,
         )
 
@@ -375,7 +379,7 @@
         self.__serverInterface.sendJson(
             category=EricRequestCategory.FileSystem,
             request="Rmdir",
-            params={"directory": directory},
+            params={"directory": FileSystemUtilities.plainFileName(directory)},
             callback=callback,
         )
 
@@ -417,7 +421,10 @@
         self.__serverInterface.sendJson(
             category=EricRequestCategory.FileSystem,
             request="Replace",
-            params={"old_name": oldName, "new_name": newName},
+            params={
+                "old_name": FileSystemUtilities.plainFileName(oldName),
+                "new_name": FileSystemUtilities.plainFileName(newName),
+            },
             callback=callback,
         )
 
@@ -457,7 +464,7 @@
         self.__serverInterface.sendJson(
             category=EricRequestCategory.FileSystem,
             request="Remove",
-            params={"filename": filename},
+            params={"filename": FileSystemUtilities.plainFileName(filename)},
             callback=callback,
         )
 
@@ -510,7 +517,10 @@
         self.__serverInterface.sendJson(
             category=EricRequestCategory.FileSystem,
             request="ReadFile",
-            params={"filename": filename, "create": create},
+            params={
+                "filename": FileSystemUtilities.plainFileName(filename),
+                "create": create,
+            },
             callback=callback,
         )
 
@@ -558,7 +568,7 @@
             category=EricRequestCategory.FileSystem,
             request="WriteFile",
             params={
-                "filename": filename,
+                "filename": FileSystemUtilities.plainFileName(filename),
                 "filedata": str(base64.b85encode(data), encoding="ascii"),
                 "with_backup": withBackup,
             },
--- a/src/eric7/RemoteServerInterface/EricServerInterface.py	Wed Feb 14 10:24:23 2024 +0100
+++ b/src/eric7/RemoteServerInterface/EricServerInterface.py	Thu Feb 15 13:59:02 2024 +0100
@@ -38,6 +38,8 @@
 
     @signal remoteReply(category:int, request:str, params:dict) emitted to deliver the
         reply of an unknown category
+    @signal remoteCoverageReply(request:str, params:dict) emitted to deliver the reply
+        of a remote server code coverage request
     @signal remoteDebuggerReply(request:str, params:dict) emitted to deliver the reply
         of a remote server debugger request
     @signal remoteEchoReply(request:str, params:dict) emitted to deliver the reply of
@@ -57,6 +59,7 @@
 
     remoteReply = pyqtSignal(int, str, dict)
 
+    remoteCoverageReply = pyqtSignal(str, dict)
     remoteDebuggerReply = pyqtSignal(str, dict)
     remoteEchoReply = pyqtSignal(str, dict)
     remoteFileSystemReply = pyqtSignal(str, dict)
@@ -75,6 +78,7 @@
         self.__ui = parent
 
         self.__categorySignalMapping = {
+            EricRequestCategory.Coverage: self.remoteCoverageReply,
             EricRequestCategory.Debugger: self.remoteDebuggerReply,
             EricRequestCategory.Echo: self.remoteEchoReply,
             EricRequestCategory.FileSystem: self.remoteFileSystemReply,
@@ -107,21 +111,27 @@
         try:
             return self.__serviceInterfaces[lname]
         except KeyError:
-            if lname not in ("debugger", "filesystem", "project"):
+            if lname not in ("coverage", "debugger", "filesystem", "project"):
                 raise ValueError(f"no such service supported ({name})")
             else:
                 # instantiate the service interface
                 if lname == "filesystem":
                     from .EricServerFileSystemInterface import (  # noqa: I101
-                        EricServerFileSystemInterface
+                        EricServerFileSystemInterface,
                     )
                     self.__serviceInterfaces[lname] = (
                         EricServerFileSystemInterface(self)
                     )
                 elif lname == "debugger":
-                    from .EricServerDebuggerInterface import EricServerDebuggerInterface
-                    # noqa: I101
+                    from .EricServerDebuggerInterface import (  # noqa: I101
+                        EricServerDebuggerInterface,
+                    )
                     self.__serviceInterfaces[lname] = EricServerDebuggerInterface(self)
+                elif lname == "coverage":
+                    from .EricServerCoverageInterface import (  # noqa: I101
+                        EricServerCoverageInterface,
+                    )
+                    self.__serviceInterfaces[lname] = EricServerCoverageInterface(self)
                 elif lname == "project":
                     # TODO: 'Project Interface' not implemented yet
                     pass
--- a/src/eric7/Utilities/__init__.py	Wed Feb 14 10:24:23 2024 +0100
+++ b/src/eric7/Utilities/__init__.py	Thu Feb 15 13:59:02 2024 +0100
@@ -35,7 +35,7 @@
 
 from eric7 import Preferences
 from eric7.EricWidgets.EricApplication import ericApp
-from eric7.SystemUtilities import DesktopUtilities, OSUtilities
+from eric7.SystemUtilities import DesktopUtilities, FileSystemUtilities, OSUtilities
 from eric7.UI.Info import Program, Version
 
 
@@ -903,6 +903,16 @@
     basename = os.path.splitext(fn)[0]
     filename = "{0}.coverage".format(basename)
     if mustExist:
+        if FileSystemUtilities.isRemoteFileName(fn):
+            ericServer = ericApp().getObject("EricServer")
+            if ericServer.isServerConnected() and ericServer.getServiceInterface(
+                "FileSystem"
+            ).exists(filename):
+                return filename
+            else:
+                return ""
+
+        # It is a local file.
         if os.path.isfile(filename):
             return filename
         else:
@@ -943,12 +953,22 @@
     basename = os.path.splitext(fn)[0]
     filename = "{0}.profile".format(basename)
     if mustExist:
+        if FileSystemUtilities.isRemoteFileName(fn):
+            ericServer = ericApp().getObject("EricServer")
+            if ericServer.isServerConnected() and ericServer.getServiceInterface(
+                "FileSystem"
+            ).exists(filename):
+                return filename
+            else:
+                return ""
+
+        # It is a local file.
         if os.path.isfile(filename):
             return filename
         else:
             return ""
-    else:
-        return filename
+
+    return filename
 
 
 def parseOptionString(s):

eric ide

mercurial