1"""

2 Internal hook annotation, representation and calling machinery.

3"""

4 import inspect

5 import sys

6 import warnings

7

8

9 class 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

57 class 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

124 def 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

135 def 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

177 class _HookRelay:

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

179 of registered plugins.

180

181 """

182

183

184 class _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

305 class 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

318 class 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")