Placed the module loader and patching logic into a separate module of the debug client. multi_processing

Sat, 08 Feb 2020 17:02:40 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 08 Feb 2020 17:02:40 +0100
branch
multi_processing
changeset 7404
663f1c3d6f53
parent 7403
7446a7eacfc3
child 7405
bf6be3cff6cf

Placed the module loader and patching logic into a separate module of the debug client.

eric6.e4p file | annotate | diff | comparison | revisions
eric6/DebugClients/Python/DebugClient.py file | annotate | diff | comparison | revisions
eric6/DebugClients/Python/ModuleLoader.py file | annotate | diff | comparison | revisions
eric6/DebugClients/Python/ThreadExtension.py file | annotate | diff | comparison | revisions
--- a/eric6.e4p	Sat Feb 08 17:01:47 2020 +0100
+++ b/eric6.e4p	Sat Feb 08 17:02:40 2020 +0100
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE Project SYSTEM "Project-6.3.dtd">
 <!-- eric project file for project eric6 -->
-<!-- Copyright (C) 2019 Detlev Offenbach, detlev@die-offenbachs.de -->
+<!-- Copyright (C) 2020 Detlev Offenbach, detlev@die-offenbachs.de -->
 <Project version="6.3">
   <Language>en_US</Language>
   <ProjectWordList>Dictionaries/words.dic</ProjectWordList>
@@ -49,6 +49,7 @@
     <Source>eric6/DebugClients/Python/DebugUtilities.py</Source>
     <Source>eric6/DebugClients/Python/DebugVariables.py</Source>
     <Source>eric6/DebugClients/Python/FlexCompleter.py</Source>
+    <Source>eric6/DebugClients/Python/ModuleLoader.py</Source>
     <Source>eric6/DebugClients/Python/PyProfile.py</Source>
     <Source>eric6/DebugClients/Python/ThreadExtension.py</Source>
     <Source>eric6/DebugClients/Python/__init__.py</Source>
@@ -2043,9 +2044,6 @@
     <Other>eric6/APIs/MicroPython/circuitpython.api</Other>
     <Other>eric6/APIs/MicroPython/microbit.api</Other>
     <Other>eric6/APIs/MicroPython/micropython.api</Other>
-    <Other>eric6/APIs/Python/zope-2.10.7.api</Other>
-    <Other>eric6/APIs/Python/zope-2.11.2.api</Other>
-    <Other>eric6/APIs/Python/zope-3.3.1.api</Other>
     <Other>eric6/APIs/Python3/PyQt4.bas</Other>
     <Other>eric6/APIs/Python3/PyQt5.bas</Other>
     <Other>eric6/APIs/Python3/PyQtChart.bas</Other>
@@ -2053,6 +2051,9 @@
     <Other>eric6/APIs/Python3/QScintilla2.bas</Other>
     <Other>eric6/APIs/Python3/eric6.api</Other>
     <Other>eric6/APIs/Python3/eric6.bas</Other>
+    <Other>eric6/APIs/Python/zope-2.10.7.api</Other>
+    <Other>eric6/APIs/Python/zope-2.11.2.api</Other>
+    <Other>eric6/APIs/Python/zope-3.3.1.api</Other>
     <Other>eric6/APIs/QSS/qss.api</Other>
     <Other>eric6/APIs/Ruby/Ruby-1.8.7.api</Other>
     <Other>eric6/APIs/Ruby/Ruby-1.8.7.bas</Other>
--- a/eric6/DebugClients/Python/DebugClient.py	Sat Feb 08 17:01:47 2020 +0100
+++ b/eric6/DebugClients/Python/DebugClient.py	Sat Feb 08 17:02:40 2020 +0100
@@ -10,6 +10,7 @@
 from DebugBase import DebugBase
 from DebugClientBase import DebugClientBase
 from ThreadExtension import ThreadExtension
+from ModuleLoader import ModuleLoader
 
 
 class DebugClient(DebugClientBase, DebugBase, ThreadExtension):
@@ -29,6 +30,8 @@
         
         ThreadExtension.__init__(self)
         
+        self.__moduleLoader = ModuleLoader(self)
+        
         self.variant = 'Standard'
 
 # We are normally called by the debugger to execute directly.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/DebugClients/Python/ModuleLoader.py	Sat Feb 08 17:02:40 2020 +0100
@@ -0,0 +1,179 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing an import hook patching modules to support debugging.
+"""
+
+import sys
+import importlib
+
+
+class ModuleLoader(object):
+    """
+    Class implementing an import hook patching modules to support debugging.
+    """
+    def __init__(self, debugClient):
+        """
+        Constructor
+        
+        @param debugClient reference to the debug client object
+        @type DebugClient
+        """
+        self.__dbgClient = debugClient
+        
+        self.__enableImportHooks = True
+        
+        # TODO: check if needed
+        if sys.version_info[0] == 2:
+            self.threadModName = 'thread'
+        else:
+            self.threadModName = '_thread'
+        
+        # reset already imported thread module to apply hooks at next import
+        for moduleName in ("thread", "_thread", "threading"):
+            if moduleName in sys.modules:
+                del sys.modules[moduleName]
+        
+        self.__modulesToPatch = (
+                'thread', '_thread', 'threading',
+                'greenlet',
+                'PyQt4.QtCore', 'PyQt5.QtCore',
+                'PySide.QtCore', 'PySide2.QtCore',
+        )
+        
+        sys.meta_path.insert(0, self)
+    
+    def __loadModule(self, fullname):
+        """
+        Public method to load a module.
+        
+        @param fullname name of the module to be loaded
+        @type str
+        @return reference to the loaded module
+        @rtype module
+        """
+        module = importlib.import_module(fullname)
+        sys.modules[fullname] = module
+        
+        ## Add hook for _thread.start_new_thread
+        if (
+            fullname in ('thread', '_thread') and
+            not hasattr(module, 'eric6_patched')
+        ):
+            module.eric6_patched = True
+            self.__dbgClient.patchPyThread(module)
+        
+        ## Add hook for threading.run()
+        elif (
+            fullname == "threading" and
+            not hasattr(module, 'eric6_patched')
+        ):
+            module.eric6_patched = True
+            self.__dbgClient.patchPyThreading(module)
+        
+        ## greenlet support
+        elif (
+            fullname == 'greenlet' and
+            not hasattr(module, 'eric6_patched')
+        ):
+            if self.__dbgClient.patchGreenlet(module):
+                module.eric6_patched = True
+        
+        ## Add hook for *.QThread
+        elif (
+            fullname in ('PyQt4.QtCore', 'PyQt5.QtCore',
+                         'PySide.QtCore', 'PySide2.QtCore') and
+            not hasattr(module, 'eric6_patched')
+        ):
+            module.eric6_patched = True
+            self.__dbgClient.patchQThread(module)
+        
+        self.__enableImportHooks = True
+        return module
+    
+    if sys.version_info >= (3, 4):
+        def find_spec(self, fullname, path, target=None):
+            """
+            Public method returning the module spec.
+            
+            @param fullname name of the module to be loaded
+            @type str
+            @param path path to resolve the module name
+            @type str
+            @param target module object to use for a more educated guess
+                about what spec to return
+            @type module
+            @return module spec object pointing to the module loader
+            @type ModuleSpec
+            """
+            if fullname in sys.modules or not self.__dbgClient.debugging:
+                return None
+            
+            if (
+                fullname in self.__modulesToPatch and
+                self.__enableImportHooks
+            ):
+                # Disable hook to be able to import original module
+                self.__enableImportHooks = False
+                return importlib.machinery.ModuleSpec(fullname, self)
+            
+            return None
+        
+        def create_module(self, spec):
+            """
+            Public method to create a module based on the passed in spec.
+            
+            @param spec module spec object for loading the module
+            @type ModuleSpec
+            @return created and patched module
+            @rtype module
+            """
+            return self.__loadModule(spec.name)
+        
+        def exec_module(self, module):
+            """
+            Public method to execute the created module
+            
+            @param module module to be executed
+            @type module
+            """
+            pass
+    
+    else:
+        def find_module(self, fullname, path=None):
+            """
+            Public method returning the module loader.
+            
+            @param fullname name of the module to be loaded
+            @type str
+            @param path path to resolve the module name
+            @type str
+            @return module loader object
+            @rtype object
+            """
+            if fullname in sys.modules or not self.__dbgClient.debugging:
+                return None
+            
+            if (
+                fullname in self.__modulesToPatch and
+                self.__enableImportHooks
+            ):
+                # Disable hook to be able to import original module
+                self.__enableImportHooks = False
+                return self
+            
+            return None
+        
+        def load_module(self, fullname):
+            """
+            Public method to load a module.
+            
+            @param fullname name of the module to be loaded
+            @type str
+            @return reference to the loaded module
+            @rtype module
+            """
+            return self.__loadModule(fullname)
--- a/eric6/DebugClients/Python/ThreadExtension.py	Sat Feb 08 17:01:47 2020 +0100
+++ b/eric6/DebugClients/Python/ThreadExtension.py	Sat Feb 08 17:02:40 2020 +0100
@@ -9,7 +9,6 @@
 
 import os
 import sys
-import importlib
 
 if sys.version_info[0] == 2:
     import thread as _thread
@@ -35,11 +34,7 @@
         Constructor
         """
         self.threadNumber = 1
-        self.enableImportHooks = True
         self._original_start_new_thread = None
-        self.threadingAttached = False
-        self.qtThreadAttached = False
-        self.greenlet = False
         
         self.clientLock = threading.RLock()
         
@@ -53,17 +48,6 @@
         
         # special objects representing the main scripts thread and frame
         self.mainThread = self
-        
-        if sys.version_info[0] == 2:
-            self.threadModName = 'thread'
-        else:
-            self.threadModName = '_thread'
-        
-        # reset already imported thread module to apply hooks at next import
-        del sys.modules[self.threadModName]
-        del sys.modules['threading']
-        
-        sys.meta_path.insert(0, self)
 
     def attachThread(self, target=None, args=None, kwargs=None,
                      mainThread=False):
@@ -249,192 +233,180 @@
         self.threads = {id_: thrd for id_, thrd in self.threads.items()
                         if id_ in frames}
     
-    def find_module(self, fullname, path=None):
+    #######################################################################
+    ## Methods below deal with patching various modules to support
+    ## debugging of threads.
+    #######################################################################
+    
+    def patchPyThread(self, module):
         """
-        Public method returning the module loader.
-        
-        @param fullname name of the module to be loaded
-        @type str
-        @param path path to resolve the module name
-        @type str
-        @return module loader object
-        @rtype object
-        """
-        if fullname in sys.modules or not self.debugging:
-            return None
+        Public method to patch Python _thread (Python3) and thread (Python2)
+        modules.
         
-        if fullname in [self.threadModName, 'PyQt4.QtCore', 'PyQt5.QtCore',
-                        'PySide.QtCore', 'PySide2.QtCore', 'greenlet',
-                        'threading'] and self.enableImportHooks:
-            # Disable hook to be able to import original module
-            self.enableImportHooks = False
-            return self
-        
-        return None
+        @param module reference to the imported module to be patched
+        @type module
+        """
+        # make thread hooks available to system
+        self._original_start_new_thread = module.start_new_thread
+        module.start_new_thread = self.attachThread
     
-    def load_module(self, fullname):
+    def patchGreenlet(self, module):
         """
-        Public method to load a module.
+        Public method to patch the 'greenlet' module.
         
-        @param fullname name of the module to be loaded
-        @type str
-        @return reference to the loaded module
-        @rtype module
+        @param module reference to the imported module to be patched
+        @type module
+        @return flag indicating that the module was processed
+        @rtype bool
+        """
+        # Check for greenlet.settrace
+        if hasattr(module, 'settrace'):
+            DebugBase.pollTimerEnabled = False
+            return True
+        return False
+    
+    def patchPyThreading(self, module):
+        """
+        Public method to patch the Python threading module.
+        
+        @param module reference to the imported module to be patched
+        @type module
         """
-        module = importlib.import_module(fullname)
-        sys.modules[fullname] = module
-        if (fullname == self.threadModName and
-                self._original_start_new_thread is None):
-            # make thread hooks available to system
-            self._original_start_new_thread = module.start_new_thread
-            module.start_new_thread = self.attachThread
-
-        elif (fullname == 'greenlet' and self.greenlet is False):
-            # Check for greenlet.settrace
-            if hasattr(module, 'settrace'):
-                self.greenlet = True
-                DebugBase.pollTimerEnabled = False
+        # _debugClient as a class attribute can't be accessed in following
+        # class. Therefore we need a global variable.
+        _debugClient = self
         
-        # Add hook for threading.run()
-        elif (fullname == "threading" and self.threadingAttached is False):
-            self.threadingAttached = True
+        def _bootstrap(self, run):
+            """
+            Bootstrap for threading, which reports exceptions correctly.
             
-            # _debugClient as a class attribute can't be accessed in following
-            # class. Therefore we need a global variable.
-            _debugClient = self
-            
-            def _bootstrap(self, run):
-                """
-                Bootstrap for threading, which reports exceptions correctly.
-                
-                @param run the run method of threading.Thread
-                @type method pointer
+            @param run the run method of threading.Thread
+            @type method pointer
+            """
+            newThread = DebugBase(_debugClient)
+            _debugClient.threads[self.ident] = newThread
+            newThread.name = self.name
+            # see DebugBase.bootstrap
+            sys.settrace(newThread.trace_dispatch)
+            try:
+                run()
+            except Exception:
+                excinfo = sys.exc_info()
+                newThread.user_exception(excinfo, True)
+            finally:
+                sys.settrace(None)
+        
+        class ThreadWrapper(module.Thread):
+            """
+            Wrapper class for threading.Thread.
+            """
+            def __init__(self, *args, **kwargs):
                 """
-                newThread = DebugBase(_debugClient)
-                _debugClient.threads[self.ident] = newThread
-                newThread.name = self.name
-                # see DebugBase.bootstrap
-                sys.settrace(newThread.trace_dispatch)
-                try:
-                    run()
-                except Exception:
-                    excinfo = sys.exc_info()
-                    newThread.user_exception(excinfo, True)
-                finally:
-                    sys.settrace(None)
-            
-            class ThreadWrapper(module.Thread):
+                Constructor
                 """
-                Wrapper class for threading.Thread.
-                """
-                def __init__(self, *args, **kwargs):
-                    """
-                    Constructor
-                    """
-                    # Overwrite the provided run method with our own, to
-                    # intercept the thread creation by threading.Thread
-                    self.run = lambda s=self, run=self.run: _bootstrap(s, run)
-                    
-                    super(ThreadWrapper, self).__init__(*args, **kwargs)
-            
-            module.Thread = ThreadWrapper
+                # Overwrite the provided run method with our own, to
+                # intercept the thread creation by threading.Thread
+                self.run = lambda s=self, run=self.run: _bootstrap(s, run)
+                
+                super(ThreadWrapper, self).__init__(*args, **kwargs)
+        
+        module.Thread = ThreadWrapper
+        
+        # Special handling of threading.(_)Timer
+        if sys.version_info[0] == 2:
+            timer = module._Timer
+        else:
+            timer = module.Timer
             
-            # Special handling of threading.(_)Timer
-            if sys.version_info[0] == 2:
-                timer = module._Timer
-            else:
-                timer = module.Timer
-                
-            class TimerWrapper(timer, ThreadWrapper):
+        class TimerWrapper(timer, ThreadWrapper):
+            """
+            Wrapper class for threading.(_)Timer.
+            """
+            def __init__(self, interval, function, *args, **kwargs):
                 """
-                Wrapper class for threading.(_)Timer.
+                Constructor
                 """
-                def __init__(self, interval, function, *args, **kwargs):
-                    """
-                    Constructor
-                    """
-                    super(TimerWrapper, self).__init__(
-                        interval, function, *args, **kwargs)
-            
-            if sys.version_info[0] == 2:
-                module._Timer = TimerWrapper
-            else:
-                module.Timer = TimerWrapper
+                super(TimerWrapper, self).__init__(
+                    interval, function, *args, **kwargs)
+        
+        if sys.version_info[0] == 2:
+            module._Timer = TimerWrapper
+        else:
+            module.Timer = TimerWrapper
+    
+        # Special handling of threading._DummyThread
+        class DummyThreadWrapper(module._DummyThread, ThreadWrapper):
+            """
+            Wrapper class for threading._DummyThread.
+            """
+            def __init__(self, *args, **kwargs):
+                """
+                Constructor
+                """
+                super(DummyThreadWrapper, self).__init__(*args, **kwargs)
+        
+        module._DummyThread = DummyThreadWrapper
+    
+    def patchQThread(self, module):
+        """
+        Public method to patch the QtCore module's QThread.
         
-            # Special handling of threading._DummyThread
-            class DummyThreadWrapper(module._DummyThread, ThreadWrapper):
-                """
-                Wrapper class for threading._DummyThread.
-                """
-                def __init__(self, *args, **kwargs):
-                    """
-                    Constructor
-                    """
-                    super(DummyThreadWrapper, self).__init__(*args, **kwargs)
+        @param module reference to the imported module to be patched
+        @type module
+        """
+        # _debugClient as a class attribute can't be accessed in following
+        # class. Therefore we need a global variable.
+        _debugClient = self
+
+        def _bootstrapQThread(self, run):
+            """
+            Bootstrap for QThread, which reports exceptions correctly.
             
-            module._DummyThread = DummyThreadWrapper
+            @param run the run method of *.QThread
+            @type method pointer
+            """
+            global _qtThreadNumber
+            
+            newThread = DebugBase(_debugClient)
+            ident = _thread.get_ident()
+            name = 'QtThread-{0}'.format(_qtThreadNumber)
+            
+            _qtThreadNumber += 1
         
-        # Add hook for *.QThread
-        elif (fullname in ['PyQt4.QtCore', 'PyQt5.QtCore',
-                           'PySide.QtCore', 'PySide2.QtCore'] and
-                self.qtThreadAttached is False):
-            self.qtThreadAttached = True
-            # _debugClient as a class attribute can't be accessed in following
-            # class. Therefore we need a global variable.
-            _debugClient = self
-
-            def _bootstrapQThread(self, run):
+            newThread.id = ident
+            newThread.name = name
+            
+            _debugClient.threads[ident] = newThread
+            
+            # see DebugBase.bootstrap
+            sys.settrace(newThread.trace_dispatch)
+            try:
+                run()
+            except SystemExit:
+                # *.QThreads doesn't like SystemExit
+                pass
+            except Exception:
+                excinfo = sys.exc_info()
+                newThread.user_exception(excinfo, True)
+            finally:
+                sys.settrace(None)
+    
+        class QThreadWrapper(module.QThread):
+            """
+            Wrapper class for *.QThread.
+            """
+            def __init__(self, *args, **kwargs):
                 """
-                Bootstrap for QThread, which reports exceptions correctly.
+                Constructor
+                """
+                # Overwrite the provided run method with our own, to
+                # intercept the thread creation by Qt
+                self.run = lambda s=self, run=self.run: (
+                    _bootstrapQThread(s, run))
                 
-                @param run the run method of *.QThread
-                @type method pointer
-                """
-                global _qtThreadNumber
-                
-                newThread = DebugBase(_debugClient)
-                ident = _thread.get_ident()
-                name = 'QtThread-{0}'.format(_qtThreadNumber)
-                
-                _qtThreadNumber += 1
-            
-                newThread.id = ident
-                newThread.name = name
-                
-                _debugClient.threads[ident] = newThread
-                
-                # see DebugBase.bootstrap
-                sys.settrace(newThread.trace_dispatch)
-                try:
-                    run()
-                except SystemExit:
-                    # *.QThreads doesn't like SystemExit
-                    pass
-                except Exception:
-                    excinfo = sys.exc_info()
-                    newThread.user_exception(excinfo, True)
-                finally:
-                    sys.settrace(None)
+                super(QThreadWrapper, self).__init__(*args, **kwargs)
         
-            class QThreadWrapper(module.QThread):
-                """
-                Wrapper class for *.QThread.
-                """
-                def __init__(self, *args, **kwargs):
-                    """
-                    Constructor
-                    """
-                    # Overwrite the provided run method with our own, to
-                    # intercept the thread creation by Qt
-                    self.run = lambda s=self, run=self.run: (
-                        _bootstrapQThread(s, run))
-                    
-                    super(QThreadWrapper, self).__init__(*args, **kwargs)
-            
-            module.QThread = QThreadWrapper
-        
-        self.enableImportHooks = True
-        return module
+        module.QThread = QThreadWrapper
 
 #
 # eflag: noqa = M702

eric ide

mercurial