Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pluggy/_hooks.py: 61%

156 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:35 +0000

1""" 

2Internal hook annotation, representation and calling machinery. 

3""" 

4import inspect 

5import sys 

6import warnings 

7 

8 

9class HookspecMarker: 

10 """Decorator helper class for marking functions as hook specifications. 

11 

12 You can instantiate it with a project_name to get a decorator. 

13 Calling :py:meth:`.PluginManager.add_hookspecs` later will discover all marked functions 

14 if the :py:class:`.PluginManager` uses the same project_name. 

15 """ 

16 

17 def __init__(self, project_name): 

18 self.project_name = project_name 

19 

20 def __call__( 

21 self, function=None, firstresult=False, historic=False, warn_on_impl=None 

22 ): 

23 """if passed a function, directly sets attributes on the function 

24 which will make it discoverable to :py:meth:`.PluginManager.add_hookspecs`. 

25 If passed no function, returns a decorator which can be applied to a function 

26 later using the attributes supplied. 

27 

28 If ``firstresult`` is ``True`` the 1:N hook call (N being the number of registered 

29 hook implementation functions) will stop at I<=N when the I'th function 

30 returns a non-``None`` result. 

31 

32 If ``historic`` is ``True`` calls to a hook will be memorized and replayed 

33 on later registered plugins. 

34 

35 """ 

36 

37 def setattr_hookspec_opts(func): 

38 if historic and firstresult: 

39 raise ValueError("cannot have a historic firstresult hook") 

40 setattr( 

41 func, 

42 self.project_name + "_spec", 

43 dict( 

44 firstresult=firstresult, 

45 historic=historic, 

46 warn_on_impl=warn_on_impl, 

47 ), 

48 ) 

49 return func 

50 

51 if function is not None: 

52 return setattr_hookspec_opts(function) 

53 else: 

54 return setattr_hookspec_opts 

55 

56 

57class HookimplMarker: 

58 """Decorator helper class for marking functions as hook implementations. 

59 

60 You can instantiate with a ``project_name`` to get a decorator. 

61 Calling :py:meth:`.PluginManager.register` later will discover all marked functions 

62 if the :py:class:`.PluginManager` uses the same project_name. 

63 """ 

64 

65 def __init__(self, project_name): 

66 self.project_name = project_name 

67 

68 def __call__( 

69 self, 

70 function=None, 

71 hookwrapper=False, 

72 optionalhook=False, 

73 tryfirst=False, 

74 trylast=False, 

75 specname=None, 

76 ): 

77 

78 """if passed a function, directly sets attributes on the function 

79 which will make it discoverable to :py:meth:`.PluginManager.register`. 

80 If passed no function, returns a decorator which can be applied to a 

81 function later using the attributes supplied. 

82 

83 If ``optionalhook`` is ``True`` a missing matching hook specification will not result 

84 in an error (by default it is an error if no matching spec is found). 

85 

86 If ``tryfirst`` is ``True`` this hook implementation will run as early as possible 

87 in the chain of N hook implementations for a specification. 

88 

89 If ``trylast`` is ``True`` this hook implementation will run as late as possible 

90 in the chain of N hook implementations. 

91 

92 If ``hookwrapper`` is ``True`` the hook implementations needs to execute exactly 

93 one ``yield``. The code before the ``yield`` is run early before any non-hookwrapper 

94 function is run. The code after the ``yield`` is run after all non-hookwrapper 

95 function have run. The ``yield`` receives a :py:class:`.callers._Result` object 

96 representing the exception or result outcome of the inner calls (including other 

97 hookwrapper calls). 

98 

99 If ``specname`` is provided, it will be used instead of the function name when 

100 matching this hook implementation to a hook specification during registration. 

101 

102 """ 

103 

104 def setattr_hookimpl_opts(func): 

105 setattr( 

106 func, 

107 self.project_name + "_impl", 

108 dict( 

109 hookwrapper=hookwrapper, 

110 optionalhook=optionalhook, 

111 tryfirst=tryfirst, 

112 trylast=trylast, 

113 specname=specname, 

114 ), 

115 ) 

116 return func 

117 

118 if function is None: 

119 return setattr_hookimpl_opts 

120 else: 

121 return setattr_hookimpl_opts(function) 

122 

123 

124def normalize_hookimpl_opts(opts): 

125 opts.setdefault("tryfirst", False) 

126 opts.setdefault("trylast", False) 

127 opts.setdefault("hookwrapper", False) 

128 opts.setdefault("optionalhook", False) 

129 opts.setdefault("specname", None) 

130 

131 

132_PYPY = hasattr(sys, "pypy_version_info") 

133 

134 

135def varnames(func): 

136 """Return tuple of positional and keywrord argument names for a function, 

137 method, class or callable. 

138 

139 In case of a class, its ``__init__`` method is considered. 

140 For methods the ``self`` parameter is not included. 

141 """ 

142 if inspect.isclass(func): 

143 try: 

144 func = func.__init__ 

145 except AttributeError: 

146 return (), () 

147 elif not inspect.isroutine(func): # callable object? 

148 try: 

149 func = getattr(func, "__call__", func) 

150 except Exception: 

151 return (), () 

152 

153 try: # func MUST be a function or method here or we won't parse any args 

154 spec = inspect.getfullargspec(func) 

155 except TypeError: 

156 return (), () 

157 

158 args, defaults = tuple(spec.args), spec.defaults 

159 if defaults: 

160 index = -len(defaults) 

161 args, kwargs = args[:index], tuple(args[index:]) 

162 else: 

163 kwargs = () 

164 

165 # strip any implicit instance arg 

166 # pypy3 uses "obj" instead of "self" for default dunder methods 

167 implicit_names = ("self",) if not _PYPY else ("self", "obj") 

168 if args: 

169 if inspect.ismethod(func) or ( 

170 "." in getattr(func, "__qualname__", ()) and args[0] in implicit_names 

171 ): 

172 args = args[1:] 

173 

174 return args, kwargs 

175 

176 

177class _HookRelay: 

178 """hook holder object for performing 1:N hook calls where N is the number 

179 of registered plugins. 

180 

181 """ 

182 

183 

184class _HookCaller: 

185 def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None): 

186 self.name = name 

187 self._wrappers = [] 

188 self._nonwrappers = [] 

189 self._hookexec = hook_execute 

190 self._call_history = None 

191 self.spec = None 

192 if specmodule_or_class is not None: 

193 assert spec_opts is not None 

194 self.set_specification(specmodule_or_class, spec_opts) 

195 

196 def has_spec(self): 

197 return self.spec is not None 

198 

199 def set_specification(self, specmodule_or_class, spec_opts): 

200 assert not self.has_spec() 

201 self.spec = HookSpec(specmodule_or_class, self.name, spec_opts) 

202 if spec_opts.get("historic"): 

203 self._call_history = [] 

204 

205 def is_historic(self): 

206 return self._call_history is not None 

207 

208 def _remove_plugin(self, plugin): 

209 def remove(wrappers): 

210 for i, method in enumerate(wrappers): 

211 if method.plugin == plugin: 

212 del wrappers[i] 

213 return True 

214 

215 if remove(self._wrappers) is None: 

216 if remove(self._nonwrappers) is None: 

217 raise ValueError(f"plugin {plugin!r} not found") 

218 

219 def get_hookimpls(self): 

220 # Order is important for _hookexec 

221 return self._nonwrappers + self._wrappers 

222 

223 def _add_hookimpl(self, hookimpl): 

224 """Add an implementation to the callback chain.""" 

225 if hookimpl.hookwrapper: 

226 methods = self._wrappers 

227 else: 

228 methods = self._nonwrappers 

229 

230 if hookimpl.trylast: 

231 methods.insert(0, hookimpl) 

232 elif hookimpl.tryfirst: 

233 methods.append(hookimpl) 

234 else: 

235 # find last non-tryfirst method 

236 i = len(methods) - 1 

237 while i >= 0 and methods[i].tryfirst: 

238 i -= 1 

239 methods.insert(i + 1, hookimpl) 

240 

241 def __repr__(self): 

242 return f"<_HookCaller {self.name!r}>" 

243 

244 def __call__(self, *args, **kwargs): 

245 if args: 

246 raise TypeError("hook calling supports only keyword arguments") 

247 assert not self.is_historic() 

248 

249 # This is written to avoid expensive operations when not needed. 

250 if self.spec: 

251 for argname in self.spec.argnames: 

252 if argname not in kwargs: 

253 notincall = tuple(set(self.spec.argnames) - kwargs.keys()) 

254 warnings.warn( 

255 "Argument(s) {} which are declared in the hookspec " 

256 "can not be found in this hook call".format(notincall), 

257 stacklevel=2, 

258 ) 

259 break 

260 

261 firstresult = self.spec.opts.get("firstresult") 

262 else: 

263 firstresult = False 

264 

265 return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult) 

266 

267 def call_historic(self, result_callback=None, kwargs=None): 

268 """Call the hook with given ``kwargs`` for all registered plugins and 

269 for all plugins which will be registered afterwards. 

270 

271 If ``result_callback`` is not ``None`` it will be called for for each 

272 non-``None`` result obtained from a hook implementation. 

273 """ 

274 self._call_history.append((kwargs or {}, result_callback)) 

275 # Historizing hooks don't return results. 

276 # Remember firstresult isn't compatible with historic. 

277 res = self._hookexec(self.name, self.get_hookimpls(), kwargs, False) 

278 if result_callback is None: 

279 return 

280 for x in res or []: 

281 result_callback(x) 

282 

283 def call_extra(self, methods, kwargs): 

284 """Call the hook with some additional temporarily participating 

285 methods using the specified ``kwargs`` as call parameters.""" 

286 old = list(self._nonwrappers), list(self._wrappers) 

287 for method in methods: 

288 opts = dict(hookwrapper=False, trylast=False, tryfirst=False) 

289 hookimpl = HookImpl(None, "<temp>", method, opts) 

290 self._add_hookimpl(hookimpl) 

291 try: 

292 return self(**kwargs) 

293 finally: 

294 self._nonwrappers, self._wrappers = old 

295 

296 def _maybe_apply_history(self, method): 

297 """Apply call history to a new hookimpl if it is marked as historic.""" 

298 if self.is_historic(): 

299 for kwargs, result_callback in self._call_history: 

300 res = self._hookexec(self.name, [method], kwargs, False) 

301 if res and result_callback is not None: 

302 result_callback(res[0]) 

303 

304 

305class HookImpl: 

306 def __init__(self, plugin, plugin_name, function, hook_impl_opts): 

307 self.function = function 

308 self.argnames, self.kwargnames = varnames(self.function) 

309 self.plugin = plugin 

310 self.opts = hook_impl_opts 

311 self.plugin_name = plugin_name 

312 self.__dict__.update(hook_impl_opts) 

313 

314 def __repr__(self): 

315 return f"<HookImpl plugin_name={self.plugin_name!r}, plugin={self.plugin!r}>" 

316 

317 

318class HookSpec: 

319 def __init__(self, namespace, name, opts): 

320 self.namespace = namespace 

321 self.function = function = getattr(namespace, name) 

322 self.name = name 

323 self.argnames, self.kwargnames = varnames(function) 

324 self.opts = opts 

325 self.warn_on_impl = opts.get("warn_on_impl")