|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing an import hook patching modules to support debugging. |
|
8 """ |
|
9 |
|
10 import sys |
|
11 import importlib |
|
12 |
|
13 |
|
14 class ModuleLoader(object): |
|
15 """ |
|
16 Class implementing an import hook patching modules to support debugging. |
|
17 """ |
|
18 def __init__(self, debugClient): |
|
19 """ |
|
20 Constructor |
|
21 |
|
22 @param debugClient reference to the debug client object |
|
23 @type DebugClient |
|
24 """ |
|
25 self.__dbgClient = debugClient |
|
26 |
|
27 self.__enableImportHooks = True |
|
28 |
|
29 # TODO: check if needed |
|
30 if sys.version_info[0] == 2: |
|
31 self.threadModName = 'thread' |
|
32 else: |
|
33 self.threadModName = '_thread' |
|
34 |
|
35 # reset already imported thread module to apply hooks at next import |
|
36 for moduleName in ("thread", "_thread", "threading"): |
|
37 if moduleName in sys.modules: |
|
38 del sys.modules[moduleName] |
|
39 |
|
40 self.__modulesToPatch = ( |
|
41 'thread', '_thread', 'threading', |
|
42 'greenlet', |
|
43 'PyQt4.QtCore', 'PyQt5.QtCore', |
|
44 'PySide.QtCore', 'PySide2.QtCore', |
|
45 ) |
|
46 |
|
47 sys.meta_path.insert(0, self) |
|
48 |
|
49 def __loadModule(self, fullname): |
|
50 """ |
|
51 Public method to load a module. |
|
52 |
|
53 @param fullname name of the module to be loaded |
|
54 @type str |
|
55 @return reference to the loaded module |
|
56 @rtype module |
|
57 """ |
|
58 module = importlib.import_module(fullname) |
|
59 sys.modules[fullname] = module |
|
60 |
|
61 ## Add hook for _thread.start_new_thread |
|
62 if ( |
|
63 fullname in ('thread', '_thread') and |
|
64 not hasattr(module, 'eric6_patched') |
|
65 ): |
|
66 module.eric6_patched = True |
|
67 self.__dbgClient.patchPyThread(module) |
|
68 |
|
69 ## Add hook for threading.run() |
|
70 elif ( |
|
71 fullname == "threading" and |
|
72 not hasattr(module, 'eric6_patched') |
|
73 ): |
|
74 module.eric6_patched = True |
|
75 self.__dbgClient.patchPyThreading(module) |
|
76 |
|
77 ## greenlet support |
|
78 elif ( |
|
79 fullname == 'greenlet' and |
|
80 not hasattr(module, 'eric6_patched') |
|
81 ): |
|
82 if self.__dbgClient.patchGreenlet(module): |
|
83 module.eric6_patched = True |
|
84 |
|
85 ## Add hook for *.QThread |
|
86 elif ( |
|
87 fullname in ('PyQt4.QtCore', 'PyQt5.QtCore', |
|
88 'PySide.QtCore', 'PySide2.QtCore') and |
|
89 not hasattr(module, 'eric6_patched') |
|
90 ): |
|
91 module.eric6_patched = True |
|
92 self.__dbgClient.patchQThread(module) |
|
93 |
|
94 self.__enableImportHooks = True |
|
95 return module |
|
96 |
|
97 if sys.version_info >= (3, 4): |
|
98 def find_spec(self, fullname, path, target=None): |
|
99 """ |
|
100 Public method returning the module spec. |
|
101 |
|
102 @param fullname name of the module to be loaded |
|
103 @type str |
|
104 @param path path to resolve the module name |
|
105 @type str |
|
106 @param target module object to use for a more educated guess |
|
107 about what spec to return |
|
108 @type module |
|
109 @return module spec object pointing to the module loader |
|
110 @type ModuleSpec |
|
111 """ |
|
112 if fullname in sys.modules or not self.__dbgClient.debugging: |
|
113 return None |
|
114 |
|
115 if ( |
|
116 fullname in self.__modulesToPatch and |
|
117 self.__enableImportHooks |
|
118 ): |
|
119 # Disable hook to be able to import original module |
|
120 self.__enableImportHooks = False |
|
121 return importlib.machinery.ModuleSpec(fullname, self) |
|
122 |
|
123 return None |
|
124 |
|
125 def create_module(self, spec): |
|
126 """ |
|
127 Public method to create a module based on the passed in spec. |
|
128 |
|
129 @param spec module spec object for loading the module |
|
130 @type ModuleSpec |
|
131 @return created and patched module |
|
132 @rtype module |
|
133 """ |
|
134 return self.__loadModule(spec.name) |
|
135 |
|
136 def exec_module(self, module): |
|
137 """ |
|
138 Public method to execute the created module |
|
139 |
|
140 @param module module to be executed |
|
141 @type module |
|
142 """ |
|
143 pass |
|
144 |
|
145 else: |
|
146 def find_module(self, fullname, path=None): |
|
147 """ |
|
148 Public method returning the module loader. |
|
149 |
|
150 @param fullname name of the module to be loaded |
|
151 @type str |
|
152 @param path path to resolve the module name |
|
153 @type str |
|
154 @return module loader object |
|
155 @rtype object |
|
156 """ |
|
157 if fullname in sys.modules or not self.__dbgClient.debugging: |
|
158 return None |
|
159 |
|
160 if ( |
|
161 fullname in self.__modulesToPatch and |
|
162 self.__enableImportHooks |
|
163 ): |
|
164 # Disable hook to be able to import original module |
|
165 self.__enableImportHooks = False |
|
166 return self |
|
167 |
|
168 return None |
|
169 |
|
170 def load_module(self, fullname): |
|
171 """ |
|
172 Public method to load a module. |
|
173 |
|
174 @param fullname name of the module to be loaded |
|
175 @type str |
|
176 @return reference to the loaded module |
|
177 @rtype module |
|
178 """ |
|
179 return self.__loadModule(fullname) |