Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2Internal hook annotation, representation and calling machinery. 

3""" 

4import inspect 

5import sys 

6import warnings 

7from .callers import _legacymulticall, _multicall 

8 

9 

10class HookspecMarker(object): 

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

12 

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

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

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

16 """ 

17 

18 def __init__(self, project_name): 

19 self.project_name = project_name 

20 

21 def __call__( 

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

23 ): 

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

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

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

27 later using the attributes supplied. 

28 

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

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

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

32 

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

34 on later registered plugins. 

35 

36 """ 

37 

38 def setattr_hookspec_opts(func): 

39 if historic and firstresult: 

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

41 setattr( 

42 func, 

43 self.project_name + "_spec", 

44 dict( 

45 firstresult=firstresult, 

46 historic=historic, 

47 warn_on_impl=warn_on_impl, 

48 ), 

49 ) 

50 return func 

51 

52 if function is not None: 

53 return setattr_hookspec_opts(function) 

54 else: 

55 return setattr_hookspec_opts 

56 

57 

58class HookimplMarker(object): 

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

60 

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

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

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

64 """ 

65 

66 def __init__(self, project_name): 

67 self.project_name = project_name 

68 

69 def __call__( 

70 self, 

71 function=None, 

72 hookwrapper=False, 

73 optionalhook=False, 

74 tryfirst=False, 

75 trylast=False, 

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 """ 

100 

101 def setattr_hookimpl_opts(func): 

102 setattr( 

103 func, 

104 self.project_name + "_impl", 

105 dict( 

106 hookwrapper=hookwrapper, 

107 optionalhook=optionalhook, 

108 tryfirst=tryfirst, 

109 trylast=trylast, 

110 ), 

111 ) 

112 return func 

113 

114 if function is None: 

115 return setattr_hookimpl_opts 

116 else: 

117 return setattr_hookimpl_opts(function) 

118 

119 

120def normalize_hookimpl_opts(opts): 

121 opts.setdefault("tryfirst", False) 

122 opts.setdefault("trylast", False) 

123 opts.setdefault("hookwrapper", False) 

124 opts.setdefault("optionalhook", False) 

125 

126 

127if hasattr(inspect, "getfullargspec"): 

128 

129 def _getargspec(func): 

130 return inspect.getfullargspec(func) 

131 

132 

133else: 

134 

135 def _getargspec(func): 

136 return inspect.getargspec(func) 

137 

138 

139_PYPY3 = hasattr(sys, "pypy_version_info") and sys.version_info.major == 3 

140 

141 

142def varnames(func): 

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

144 method, class or callable. 

145 

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

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

148 """ 

149 cache = getattr(func, "__dict__", {}) 

150 try: 

151 return cache["_varnames"] 

152 except KeyError: 

153 pass 

154 

155 if inspect.isclass(func): 

156 try: 

157 func = func.__init__ 

158 except AttributeError: 

159 return (), () 

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

161 try: 

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

163 except Exception: 

164 return (), () 

165 

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

167 spec = _getargspec(func) 

168 except TypeError: 

169 return (), () 

170 

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

172 if defaults: 

173 index = -len(defaults) 

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

175 else: 

176 kwargs = () 

177 

178 # strip any implicit instance arg 

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

180 implicit_names = ("self",) if not _PYPY3 else ("self", "obj") 

181 if args: 

182 if inspect.ismethod(func) or ( 

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

184 ): 

185 args = args[1:] 

186 

187 try: 

188 cache["_varnames"] = args, kwargs 

189 except TypeError: 

190 pass 

191 return args, kwargs 

192 

193 

194class _HookRelay(object): 

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

196 of registered plugins. 

197 

198 """ 

199 

200 

201class _HookCaller(object): 

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

203 self.name = name 

204 self._wrappers = [] 

205 self._nonwrappers = [] 

206 self._hookexec = hook_execute 

207 self.argnames = None 

208 self.kwargnames = None 

209 self.multicall = _multicall 

210 self.spec = None 

211 if specmodule_or_class is not None: 

212 assert spec_opts is not None 

213 self.set_specification(specmodule_or_class, spec_opts) 

214 

215 def has_spec(self): 

216 return self.spec is not None 

217 

218 def set_specification(self, specmodule_or_class, spec_opts): 

219 assert not self.has_spec() 

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

221 if spec_opts.get("historic"): 

222 self._call_history = [] 

223 

224 def is_historic(self): 

225 return hasattr(self, "_call_history") 

226 

227 def _remove_plugin(self, plugin): 

228 def remove(wrappers): 

229 for i, method in enumerate(wrappers): 

230 if method.plugin == plugin: 

231 del wrappers[i] 

232 return True 

233 

234 if remove(self._wrappers) is None: 

235 if remove(self._nonwrappers) is None: 

236 raise ValueError("plugin %r not found" % (plugin,)) 

237 

238 def get_hookimpls(self): 

239 # Order is important for _hookexec 

240 return self._nonwrappers + self._wrappers 

241 

242 def _add_hookimpl(self, hookimpl): 

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

244 """ 

245 if hookimpl.hookwrapper: 

246 methods = self._wrappers 

247 else: 

248 methods = self._nonwrappers 

249 

250 if hookimpl.trylast: 

251 methods.insert(0, hookimpl) 

252 elif hookimpl.tryfirst: 

253 methods.append(hookimpl) 

254 else: 

255 # find last non-tryfirst method 

256 i = len(methods) - 1 

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

258 i -= 1 

259 methods.insert(i + 1, hookimpl) 

260 

261 if "__multicall__" in hookimpl.argnames: 

262 warnings.warn( 

263 "Support for __multicall__ is now deprecated and will be" 

264 "removed in an upcoming release.", 

265 DeprecationWarning, 

266 ) 

267 self.multicall = _legacymulticall 

268 

269 def __repr__(self): 

270 return "<_HookCaller %r>" % (self.name,) 

271 

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

273 if args: 

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

275 assert not self.is_historic() 

276 if self.spec and self.spec.argnames: 

277 notincall = ( 

278 set(self.spec.argnames) - set(["__multicall__"]) - set(kwargs.keys()) 

279 ) 

280 if notincall: 

281 warnings.warn( 

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

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

284 stacklevel=2, 

285 ) 

286 return self._hookexec(self, self.get_hookimpls(), kwargs) 

287 

288 def call_historic(self, result_callback=None, kwargs=None, proc=None): 

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

290 for all plugins which will be registered afterwards. 

291 

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

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

294 

295 .. note:: 

296 The ``proc`` argument is now deprecated. 

297 """ 

298 if proc is not None: 

299 warnings.warn( 

300 "Support for `proc` argument is now deprecated and will be" 

301 "removed in an upcoming release.", 

302 DeprecationWarning, 

303 ) 

304 result_callback = proc 

305 

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

307 # historizing hooks don't return results 

308 res = self._hookexec(self, self.get_hookimpls(), kwargs) 

309 if result_callback is None: 

310 return 

311 # XXX: remember firstresult isn't compat with historic 

312 for x in res or []: 

313 result_callback(x) 

314 

315 def call_extra(self, methods, kwargs): 

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

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

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

319 for method in methods: 

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

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

322 self._add_hookimpl(hookimpl) 

323 try: 

324 return self(**kwargs) 

325 finally: 

326 self._nonwrappers, self._wrappers = old 

327 

328 def _maybe_apply_history(self, method): 

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

330 """ 

331 if self.is_historic(): 

332 for kwargs, result_callback in self._call_history: 

333 res = self._hookexec(self, [method], kwargs) 

334 if res and result_callback is not None: 

335 result_callback(res[0]) 

336 

337 

338class HookImpl(object): 

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

340 self.function = function 

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

342 self.plugin = plugin 

343 self.opts = hook_impl_opts 

344 self.plugin_name = plugin_name 

345 self.__dict__.update(hook_impl_opts) 

346 

347 def __repr__(self): 

348 return "<HookImpl plugin_name=%r, plugin=%r>" % (self.plugin_name, self.plugin) 

349 

350 

351class HookSpec(object): 

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

353 self.namespace = namespace 

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

355 self.name = name 

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

357 self.opts = opts 

358 self.argnames = ["__multicall__"] + list(self.argnames) 

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