Sat, 08 Feb 2020 17:02:40 +0100
Placed the module loader and patching logic into a separate module of the debug client.
--- 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