![](/style/images/good.png)
![](/style/images/bad.png)
Pytest 源码解读 [4] - pluggy "_HookCall" 调用链分析
source link: http://markshao.github.io/2019/10/05/hook-caller-logic/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
pluggy "_HookCall" 调用链分析
我们知道pm.hook.xxx(**kwargs)
,最后实际是调用了绑定的 _HookCaller
对象的__call__
方法,那么今天我们来看一下这个方法的逻辑是什么样的,废话少说,先上代码, 我们先看下_HookCaller
的构造函数
def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None):
self.name = name
self._wrappers = []
self._nonwrappers = []
self._hookexec = hook_execute
self.argnames = None
self.kwargnames = None
self.multicall = _multicall
self.spec = None
if specmodule_or_class is not None:
assert spec_opts is not None
self.set_specification(specmodule_or_class, spec_opts)
这里有两个List
是用来存放 hook 对应的 plugin 的,分为 wrapper 和 nonwrapper 两种类型,当我们通过调用_HookCaller
的方法_add_hookimpl
来注册plugin的时候,会根据HookimplMarker
装饰器的hookwrapper
属性来区分不同的 plugin list , 简单看几行_add_hookimpl
的实现就知道了
def _add_hookimpl(self, hookimpl):
"""Add an implementation to the callback chain.
"""
if hookimpl.hookwrapper:
methods = self._wrappers
else:
methods = self._nonwrappers
if hookimpl.trylast:
methods.insert(0, hookimpl)
elif hookimpl.tryfirst:
methods.append(hookimpl)
,我们再继续回来看_HookCaller
的构造函数,有两个属性看起来很类似,一个是self._hookexec
,一个是self.multicall
, _multicall
是_HookCaller
自己的 method , _hookexec
是通过构造函数传入的,我们看下PluginManager
在绑定_HookCaller
对象的时候的代码,hook = _HookCaller(name, self._hookexec)
,传入的是一个_hookexec
, 再继续网上爬代码,发现这个所谓的 self._hookexec ,实际就是multicall
函数的封装,
self._inner_hookexec = lambda hook, methods, kwargs: hook.multicall(
methods,
kwargs,
firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
)
看到这里我们知道了,原来这两个属性最后底层都是同一个_multicall
函数对象 。 前面铺垫了那么多,那我们来看一下核心调用函数__call__
到底再干嘛
def __call__(self, *args, **kwargs):
if args:
raise TypeError("hook calling supports only keyword arguments")
assert not self.is_historic()
if self.spec and self.spec.argnames:
notincall = (
set(self.spec.argnames) - set(["__multicall__"]) - set(kwargs.keys())
)
if notincall:
warnings.warn(
"Argument(s) {} which are declared in the hookspec "
"can not be found in this hook call".format(tuple(notincall)),
stacklevel=2,
)
return self._hookexec(self, self.get_hookimpls(), kwargs)
- 先判断下调用参数的类型,不支持非kwargs类型的参数
- 判断一下是否是 historical call 的,这里回头要研究一下为啥不允许historical call
- 判断下调用的参数类型是否符合 spec 定义的要求,如果不符合会给 warning ,这里有点困惑, 既然不符合spec 规定,为什么不报错停止,只是给一个 warning ,有了解的同学欢迎给我留言
- 最后是调用
self._hookexec
,把所有的注册的plugin 作为参数传入,根据上面的介绍,就是调用了_multicall
的函数,_multicall
的函数挺长的,我们分别看一下
def _multicall(hook_impls, caller_kwargs, firstresult=False):
"""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(
"hook call must provide argument %r" % (argname,)
)
- 原来这里还是会再检查调用的参数,如果spec定义的参数没有在kwargs中,这次是报错了
然后就是根据是否是hookwrapper来区分调用逻辑,我们先看下也应该正确的调用逻辑
- hookwrapper
- 先调用
hookwrapper
的plugin,先执行yield
之前的代码 - 然后执行其他 plugin
- 最后把其他的plugin的执行结果作为参数传回到
hookwrapper
的plugin的 yield point ,继续执行
- 先调用
- nonwrapper
- 直接调用 plugin ,并把结果返回到 result list中
- 如果有 firstresult,就直接返回
我们看看代码的实现
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
- 同上面的逻辑,
next(gen)
, 先执行yield
前的逻辑,把gen存放在teardown 里,用作后续的callback - nonwrapper, 就直接调用,把结果存在result里
最后finllay那一段逻辑
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()
- 所有的结果都要用
_Result
来封装返回 - 如果是 hookwrapper的,还要遍历之前存的
generator
, 把nonwrapper的结果回调回去gen.send(output)
,为了避免实现错误,例如在plugin里写了两个yield, 直接在外部调用逻辑里抛出异常终止逻辑_raise_wrapfail(gen, "has second yield")
- 最后是把outcome 返回给到外部调用方
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK