首先看一下pluggy应用代码中执行hook函数调用的代码,如下所示,在分析addhookspecs方法的时候,我们曾经分析过,pm有一个属性时hook,而hook实质上是HookRelay类的实例对象,但是_HookRelay类是一个空类,在add_hookspecs中对hook这个对象设置了一个属性,属性名是myhook,而此属性的值是hc,而hc实质上是_HookCaller类的一个实例,换一句话说pm.hook.myhook就是_HookCaller类的一个实例,那么这里将实例当做函数调用,很显然,在python中这种用法实质上是在调用_HookCaller类中的__call魔法函数。
results = pm.hook.myhook(arg1=1, arg2=2)
进入HookCaller类中,确实可以找到_call魔法函数,代码试下如下,这里可以看到首先是获取firstresult是否设置为True,如果没有设置,则直接将firstresult设置为False,然后就调用_hookexec方法,而在_HookCaller类的初始化函数中,可以看出_hookexec方法就是传递进来的PluginManager类的_inner_hookexec属性,亦即_multicall函数。
def __call__(self, *args, **kwargs):
if args:
raise TypeError("hook calling supports only keyword arguments")
assert not self.is_historic()
# This is written to avoid expensive operations when not needed.
if self.spec:
for argname in self.spec.argnames:
if argname not in kwargs:
notincall = tuple(set(self.spec.argnames) - kwargs.keys())
warnings.warn(
"Argument(s) {} which are declared in the hookspec "
"can not be found in this hook call".format(notincall),
stacklevel=2,
)
break
firstresult = self.spec.opts.get("firstresult")
else:
firstresult = False
return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
因此pm.hook.myhook实质上就是调用了_multicall函数,在前面也曾经说过,_multicall函数是整个pluggy模块的调用执行的核心,这里就将详细的介绍此函数的实现,_multicall的函数代码如下,这里首先可以看到在for循环的时候,是将hook_impls使用reverse进行了反转,这就与我们前面分析到的在添加执行函数的时候好像使用了先进后出队列,那么之类可以看到,并没有使用队列的数据结构,而是使用了列表,只是在这里对列表进行了反转,在每个循环中判断hookwrapper的值是否为True,因为如果hookwrapper为True,则表示方法中有yield,此时就需要将yield之后的调用提前存入这里teardowns列表,同时执行所有的函数,此外这里同时可以看到,在判断firstresult,如果firstresult为True,当有一个结果时就会停止执行了,在finally部分可以看到,这里又将teardowns进行反转然后再依次执行,这就做到了当有多个插件类的方法中使用yield时,先注册的插件类中的yield之后的代码是后执行的,而这个功能对于pytest中的teardown就很有用处。
def _multicall(hook_name, hook_impls, caller_kwargs, firstresult):
"""Execute a call into multiple python functions/methods and return the
result(s).
``caller_kwargs`` comes from _HookCaller.__call__().
"""
__tracebackhide__ = True
results = []
excinfo = None
try: # run impl and wrapper setup functions in a loop
teardowns = []
try:
for hook_impl in reversed(hook_impls):
try:
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
except KeyError:
for argname in hook_impl.argnames:
if argname not in caller_kwargs:
raise HookCallError(
f"hook call must provide argument {argname!r}"
)
if hook_impl.hookwrapper:
try:
gen = hook_impl.function(*args)
next(gen) # first yield
teardowns.append(gen)
except StopIteration:
_raise_wrapfail(gen, "did not yield")
else:
res = hook_impl.function(*args)
if res is not None:
results.append(res)
if firstresult: # halt further impl calls
break
except BaseException:
excinfo = sys.exc_info()
finally:
if firstresult: # first result hooks return a single value
outcome = _Result(results[0] if results else None, excinfo)
else:
outcome = _Result(results, excinfo)
# run all wrapper post-yield blocks
for gen in reversed(teardowns):
try:
gen.send(outcome)
_raise_wrapfail(gen, "has second yield")
except StopIteration:
pass
return outcome.get_result()
至此,hook函数调用执行的源码就解析完成了。