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

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

110 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 collections.abc import Callable 

10from importlib.util import find_spec 

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 discover_post_import_hooks(group): 

94 """ 

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

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

97 entry point group name used in the package metadata. 

98 """ 

99 

100 try: 

101 # Python 3.10+ style with select parameter 

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

103 except TypeError: 

104 # Python 3.8-3.9 style that returns a dict 

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

106 

107 for entrypoint in entrypoints: 

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

109 register_post_import_hook(callback, entrypoint.name) 

110 

111 

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

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

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

115# the import of the target module to fail. 

116 

117 

118def notify_module_loaded(module): 

119 """ 

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

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

122 function does nothing. 

123 """ 

124 

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

126 

127 with _post_import_hooks_lock: 

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

129 

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

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

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

133 

134 for hook in hooks: 

135 hook(module) 

136 

137 

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

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

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

141# hooks which are registered will be invoked. 

142 

143 

144class _ImportHookChainedLoader(BaseObjectProxy): 

145 

146 def __init__(self, loader): 

147 # Explicit class in super() is used because the proxy overrides 

148 # __class__ and MRO-related methods to delegate to the wrapped 

149 # object, which can interfere with bare super(). 

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

151 

152 if hasattr(loader, "load_module"): 

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

154 if hasattr(loader, "create_module"): 

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

156 if hasattr(loader, "exec_module"): 

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

158 

159 def _self_set_loader(self, module): 

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

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

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

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

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

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

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

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

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

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

170 

171 class UNDEFINED: 

172 pass 

173 

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

175 try: 

176 module.__loader__ = self.__wrapped__ 

177 except AttributeError: 

178 pass 

179 

180 if ( 

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

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

183 ): 

184 module.__spec__.loader = self.__wrapped__ 

185 

186 def _self_load_module(self, fullname): 

187 module = self.__wrapped__.load_module(fullname) 

188 self._self_set_loader(module) 

189 notify_module_loaded(module) 

190 

191 return module 

192 

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

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

195 

196 def _self_create_module(self, spec): 

197 return self.__wrapped__.create_module(spec) 

198 

199 def _self_exec_module(self, module): 

200 self._self_set_loader(module) 

201 self.__wrapped__.exec_module(module) 

202 notify_module_loaded(module) 

203 

204 

205class ImportHookFinder: 

206 

207 def __init__(self): 

208 self.in_progress = {} 

209 

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

211 with _post_import_hooks_lock: 

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

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

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

215 

216 if fullname not in _post_import_hooks: 

217 return None 

218 

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

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

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

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

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

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

225 

226 if fullname in self.in_progress: 

227 return None 

228 

229 self.in_progress[fullname] = True 

230 

231 # Now call back into the import system again. 

232 

233 try: 

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

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

236 # import the target module and only finds the 

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

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

239 # real loader to import the module and invoke the 

240 # post import hooks. 

241 

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

243 

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

245 return _ImportHookChainedLoader(loader) 

246 

247 finally: 

248 del self.in_progress[fullname] 

249 

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

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

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

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

254 

255 with _post_import_hooks_lock: 

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

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

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

259 

260 if fullname not in _post_import_hooks: 

261 return None 

262 

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

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

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

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

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

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

269 

270 if fullname in self.in_progress: 

271 return None 

272 

273 self.in_progress[fullname] = True 

274 

275 # Now call back into the import system again. 

276 

277 try: 

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

279 # exist so don't need to check. 

280 

281 spec = find_spec(fullname) 

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

283 

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

285 spec.loader = _ImportHookChainedLoader(loader) 

286 

287 return spec 

288 

289 finally: 

290 del self.in_progress[fullname] 

291 

292 

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

294# import hook when the target module is imported. 

295 

296 

297def when_imported(name): 

298 """ 

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

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

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

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

303 be called immediately. 

304 """ 

305 

306 def register(hook): 

307 register_post_import_hook(hook, name) 

308 return hook 

309 

310 return register