Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/wrapt/importer.py: 22%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

127 statements  

1"""This module implements a post import hook mechanism styled after what is 

2described in PEP-369. Note that it doesn't cope with modules being reloaded. 

3 

4""" 

5 

6import importlib.metadata 

7import sys 

8import threading 

9from importlib.util import find_spec 

10from typing import Callable, Dict, List 

11 

12from .__wrapt__ import BaseObjectProxy 

13 

14# The dictionary registering any post import hooks to be triggered once 

15# the target module has been imported. Once a module has been imported 

16# and the hooks fired, the list of hooks recorded against the target 

17# module will be truncated but the list left in the dictionary. This 

18# acts as a flag to indicate that the module had already been imported. 

19 

20_post_import_hooks: Dict[str, List[Callable]] = {} 

21_post_import_hooks_init = False 

22_post_import_hooks_lock = threading.RLock() 

23 

24# Register a new post import hook for the target module name. This 

25# differs from the PEP-369 implementation in that it also allows the 

26# hook function to be specified as a string consisting of the name of 

27# the callback in the form 'module:function'. This will result in a 

28# proxy callback being registered which will defer loading of the 

29# specified module containing the callback function until required. 

30 

31 

32def _create_import_hook_from_string(name): 

33 def import_hook(module): 

34 module_name, function = name.split(":") 

35 attrs = function.split(".") 

36 __import__(module_name) 

37 callback = sys.modules[module_name] 

38 for attr in attrs: 

39 callback = getattr(callback, attr) 

40 return callback(module) 

41 

42 return import_hook 

43 

44 

45def register_post_import_hook(hook, name): 

46 """ 

47 Register a post import hook for the target module `name`. The `hook` 

48 function will be called once the module is imported and will be passed the 

49 module as argument. If the module is already imported, the `hook` will be 

50 called immediately. If you also want to defer loading of the module containing 

51 the `hook` function until required, you can specify the `hook` as a string in 

52 the form 'module:function'. This will result in a proxy hook function being 

53 registered which will defer loading of the specified module containing the 

54 callback function until required. 

55 """ 

56 

57 # Create a deferred import hook if hook is a string name rather than 

58 # a callable function. 

59 

60 if isinstance(hook, str): 

61 hook = _create_import_hook_from_string(hook) 

62 

63 with _post_import_hooks_lock: 

64 # Automatically install the import hook finder if it has not already 

65 # been installed. 

66 

67 global _post_import_hooks_init 

68 

69 if not _post_import_hooks_init: 

70 _post_import_hooks_init = True 

71 sys.meta_path.insert(0, ImportHookFinder()) 

72 

73 # Check if the module is already imported. If not, register the hook 

74 # to be called after import. 

75 

76 module = sys.modules.get(name, None) 

77 

78 if module is None: 

79 _post_import_hooks.setdefault(name, []).append(hook) 

80 

81 # If the module is already imported, we fire the hook right away. Note that 

82 # the hook is called outside of the lock to avoid deadlocks if code run as a 

83 # consequence of calling the module import hook in turn triggers a separate 

84 # thread which tries to register an import hook. 

85 

86 if module is not None: 

87 hook(module) 

88 

89 

90# Register post import hooks defined as package entry points. 

91 

92 

93def _create_import_hook_from_entrypoint(entrypoint): 

94 def import_hook(module): 

95 entrypoint_value = entrypoint.value.split(":") 

96 module_name = entrypoint_value[0] 

97 __import__(module_name) 

98 callback = sys.modules[module_name] 

99 

100 if len(entrypoint_value) > 1: 

101 attrs = entrypoint_value[1].split(".") 

102 for attr in attrs: 

103 callback = getattr(callback, attr) 

104 return callback(module) 

105 

106 return import_hook 

107 

108 

109def discover_post_import_hooks(group): 

110 """ 

111 Discover and register post import hooks defined as package entry points 

112 in the specified `group`. The group should be a string that matches the 

113 entry point group name used in the package metadata. 

114 """ 

115 

116 try: 

117 # Python 3.10+ style with select parameter 

118 entrypoints = importlib.metadata.entry_points(group=group) 

119 except TypeError: 

120 # Python 3.8-3.9 style that returns a dict 

121 entrypoints = importlib.metadata.entry_points().get(group, ()) 

122 

123 for entrypoint in entrypoints: 

124 callback = entrypoint.load() # Use the loaded callback directly 

125 register_post_import_hook(callback, entrypoint.name) 

126 

127 

128# Indicate that a module has been loaded. Any post import hooks which 

129# were registered against the target module will be invoked. If an 

130# exception is raised in any of the post import hooks, that will cause 

131# the import of the target module to fail. 

132 

133 

134def notify_module_loaded(module): 

135 """ 

136 Notify that a `module` has been loaded and invoke any post import hooks 

137 registered against the module. If the module is not registered, this 

138 function does nothing. 

139 """ 

140 

141 name = getattr(module, "__name__", None) 

142 

143 with _post_import_hooks_lock: 

144 hooks = _post_import_hooks.pop(name, ()) 

145 

146 # Note that the hook is called outside of the lock to avoid deadlocks if 

147 # code run as a consequence of calling the module import hook in turn 

148 # triggers a separate thread which tries to register an import hook. 

149 

150 for hook in hooks: 

151 hook(module) 

152 

153 

154# A custom module import finder. This intercepts attempts to import 

155# modules and watches out for attempts to import target modules of 

156# interest. When a module of interest is imported, then any post import 

157# hooks which are registered will be invoked. 

158 

159 

160class _ImportHookLoader: 

161 

162 def load_module(self, fullname): 

163 module = sys.modules[fullname] 

164 notify_module_loaded(module) 

165 

166 return module 

167 

168 

169class _ImportHookChainedLoader(BaseObjectProxy): 

170 

171 def __init__(self, loader): 

172 super(_ImportHookChainedLoader, self).__init__(loader) 

173 

174 if hasattr(loader, "load_module"): 

175 self.__self_setattr__("load_module", self._self_load_module) 

176 if hasattr(loader, "create_module"): 

177 self.__self_setattr__("create_module", self._self_create_module) 

178 if hasattr(loader, "exec_module"): 

179 self.__self_setattr__("exec_module", self._self_exec_module) 

180 

181 def _self_set_loader(self, module): 

182 # Set module's loader to self.__wrapped__ unless it's already set to 

183 # something else. Import machinery will set it to spec.loader if it is 

184 # None, so handle None as well. The module may not support attribute 

185 # assignment, in which case we simply skip it. Note that we also deal 

186 # with __loader__ not existing at all. This is to future proof things 

187 # due to proposal to remove the attribute as described in the GitHub 

188 # issue at https://github.com/python/cpython/issues/77458. Also prior 

189 # to Python 3.3, the __loader__ attribute was only set if a custom 

190 # module loader was used. It isn't clear whether the attribute still 

191 # existed in that case or was set to None. 

192 

193 class UNDEFINED: 

194 pass 

195 

196 if getattr(module, "__loader__", UNDEFINED) in (None, self): 

197 try: 

198 module.__loader__ = self.__wrapped__ 

199 except AttributeError: 

200 pass 

201 

202 if ( 

203 getattr(module, "__spec__", None) is not None 

204 and getattr(module.__spec__, "loader", None) is self 

205 ): 

206 module.__spec__.loader = self.__wrapped__ 

207 

208 def _self_load_module(self, fullname): 

209 module = self.__wrapped__.load_module(fullname) 

210 self._self_set_loader(module) 

211 notify_module_loaded(module) 

212 

213 return module 

214 

215 # Python 3.4 introduced create_module() and exec_module() instead of 

216 # load_module() alone. Splitting the two steps. 

217 

218 def _self_create_module(self, spec): 

219 return self.__wrapped__.create_module(spec) 

220 

221 def _self_exec_module(self, module): 

222 self._self_set_loader(module) 

223 self.__wrapped__.exec_module(module) 

224 notify_module_loaded(module) 

225 

226 

227class ImportHookFinder: 

228 

229 def __init__(self): 

230 self.in_progress = {} 

231 

232 def find_module(self, fullname, path=None): 

233 # If the module being imported is not one we have registered 

234 # post import hooks for, we can return immediately. We will 

235 # take no further part in the importing of this module. 

236 

237 with _post_import_hooks_lock: 

238 if fullname not in _post_import_hooks: 

239 return None 

240 

241 # When we are interested in a specific module, we will call back 

242 # into the import system a second time to defer to the import 

243 # finder that is supposed to handle the importing of the module. 

244 # We set an in progress flag for the target module so that on 

245 # the second time through we don't trigger another call back 

246 # into the import system and cause a infinite loop. 

247 

248 if fullname in self.in_progress: 

249 return None 

250 

251 self.in_progress[fullname] = True 

252 

253 # Now call back into the import system again. 

254 

255 try: 

256 # For Python 3 we need to use find_spec().loader 

257 # from the importlib.util module. It doesn't actually 

258 # import the target module and only finds the 

259 # loader. If a loader is found, we need to return 

260 # our own loader which will then in turn call the 

261 # real loader to import the module and invoke the 

262 # post import hooks. 

263 

264 loader = getattr(find_spec(fullname), "loader", None) 

265 

266 if loader and not isinstance(loader, _ImportHookChainedLoader): 

267 return _ImportHookChainedLoader(loader) 

268 

269 finally: 

270 del self.in_progress[fullname] 

271 

272 def find_spec(self, fullname, path=None, target=None): 

273 # Since Python 3.4, you are meant to implement find_spec() method 

274 # instead of find_module() and since Python 3.10 you get deprecation 

275 # warnings if you don't define find_spec(). 

276 

277 # If the module being imported is not one we have registered 

278 # post import hooks for, we can return immediately. We will 

279 # take no further part in the importing of this module. 

280 

281 with _post_import_hooks_lock: 

282 if fullname not in _post_import_hooks: 

283 return None 

284 

285 # When we are interested in a specific module, we will call back 

286 # into the import system a second time to defer to the import 

287 # finder that is supposed to handle the importing of the module. 

288 # We set an in progress flag for the target module so that on 

289 # the second time through we don't trigger another call back 

290 # into the import system and cause a infinite loop. 

291 

292 if fullname in self.in_progress: 

293 return None 

294 

295 self.in_progress[fullname] = True 

296 

297 # Now call back into the import system again. 

298 

299 try: 

300 # This should only be Python 3 so find_spec() should always 

301 # exist so don't need to check. 

302 

303 spec = find_spec(fullname) 

304 loader = getattr(spec, "loader", None) 

305 

306 if loader and not isinstance(loader, _ImportHookChainedLoader): 

307 spec.loader = _ImportHookChainedLoader(loader) 

308 

309 return spec 

310 

311 finally: 

312 del self.in_progress[fullname] 

313 

314 

315# Decorator for marking that a function should be called as a post 

316# import hook when the target module is imported. 

317 

318 

319def when_imported(name): 

320 """ 

321 Returns a decorator that registers the decorated function as a post import 

322 hook for the module specified by `name`. The function will be called once 

323 the module with the specified name is imported, and will be passed the 

324 module as argument. If the module is already imported, the function will 

325 be called immediately. 

326 """ 

327 

328 def register(hook): 

329 register_post_import_hook(hook, name) 

330 return hook 

331 

332 return register