经过pluggy源码解读系列1-4的分析,已经完成插件定义、spec定义,插件注册等环节,下面就到了调用插件执行了,即hook钩子函数是如何被调用执行的,下面还是先把pluggy使用的代码放下面:
import pluggy
# HookspecMarker 和 HookimplMarker 实质上是一个装饰器带参数的装饰器类,作用是给函数增加额外的属性设置
hookspec = pluggy.HookspecMarker("myproject")
hookimpl = pluggy.HookimplMarker("myproject")
# 定义自己的Spec,这里可以理解为定义接口类
class MySpec:
# hookspec 是一个装饰类中的方法的装饰器,为此方法增额外的属性设置,这里myhook可以理解为定义了一个接口
@hookspec
def myhook(self, arg1, arg2):
pass
# 定义了一个插件
class Plugin_1:
# 插件中实现了上面定义的接口,同样这个实现接口的方法用 hookimpl装饰器装饰,功能是返回两个参数的和
@hookimpl
def myhook(self, arg1, arg2):
print("inside Plugin_1.myhook()")
return arg1 + arg2
# 定义第二个插件
class Plugin_2:
# 插件中实现了上面定义的接口,同样这个实现接口的方法用 hookimpl装饰器装饰,功能是返回两个参数的差
@hookimpl
def myhook(self, arg1, arg2):
print("inside Plugin_2.myhook()")
return arg1 - arg2
# 实例化一个插件管理的对象,注意这里的名称要与文件开头定义装饰器的时候的名称一致
pm = pluggy.PluginManager("myproject")
# 将自定义的接口类加到钩子定义中去
pm.add_hookspecs(MySpec)
# 注册定义的两个插件
pm.register(Plugin_1())
pm.register(Plugin_2())
# 通过插件管理对象的钩子调用方法,这时候两个插件中的这个方法都会执行,而且遵循后注册先执行即LIFO的原则,两个插件的结果讲义列表的形式返回
results = pm.hook.myhook(arg1=1, arg2=2)
print(results)
通过上面的例子,可以看出,最后一个步骤就是通过PluginManager实例的pm的一个hook属性调用myhook函数,而myhook即定义的接口函数,在这个例子中,这个接口函数在pluggin_1和pluggin_2两个插件中都有实现,则这里两个插件的myhook函数都会执行,执行的顺序也是后讲究的,那么这些流程的控制执行等都本节详细讲述
现在先回头再看一下,在分析add_hookspecs方法的时候讲到,首先hook是PluginManager类的一个实例,这个比较好理解,下面是add_hookspecs方法的源代码,这个在前面都已经详细的分析过了,这里放这里再简单回顾一下,通过下面的代码可以发现,就是在这个函数中给hook设置了接口函数myhook的属性,myhook的属性值是_HookCaller类的一个实例,那么这里一个实例为什么当做函数调用了呢,这就涉及到python的高级语法中call魔法函数的应用了
def add_hookspecs(self, module_or_class):
""" add new hook specifications defined in the given ``module_or_class``.
Functions are recognized if they have been decorated accordingly. """
names = []
for name in dir(module_or_class):
spec_opts = self.parse_hookspec_opts(module_or_class, name)
if spec_opts is not None:
hc = getattr(self.hook, name, None)
if hc is None:
hc = _HookCaller(name, self._hookexec, module_or_class, spec_opts)
setattr(self.hook, name, hc)
else:
# plugins registered this hook without knowing the spec
hc.set_specification(module_or_class, spec_opts)
for hookfunction in hc.get_hookimpls():
self._verify_hook(hc, hookfunction)
names.append(name)
if not names:
raise ValueError(
"did not find any %r hooks in %r" % (self.project_name, module_or_class)
)
前面也都分析过call的应用,所以这里就是应用了这个特点,即把_HookCaller类的一个实例当做函数调用,实质上就是调用了_HookCaller类的call魔法函数,这里把_HookCaller类的call方法的代码放到下面,前面层提过,这个方法是整个pluggy最最核心的一个函数(pluggy最最核心的类是PluginManager类,它是插件管理注册等等控制类,而pluggy最最核心的函数就是_HookCaller类的call函数了,它控制了整个插件系统的钩子函数的执行过程)
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)
下面就对这个函数做详细的分析
- 首先这个函数的前两行就限定了插件中定义的函数的参数必须是key-value键值对的形式,不支持可变参数的形式
- 然后就是对参数做分析,主要就是分析出firstresult的值是True还是False
- 下面就是调用self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)函数了这里,首先name就是接口函数的名字,比如这里就是myhook字符串
下面看下第二个参数,第二个参数是一个函数,这个函数的代码如下:这里可以看出,这里就是上一节分析的注册函数的列表,所以这个返回的是一个实现函数的列表,第三个参数是函数的参数,第四个参数就是firstresult值
def get_hookimpls(self):
# Order is important for _hookexec
return self._nonwrappers + self._wrappers
下面就到了最最核心的函数了,即hookexec函数,通过前面几节的分析,已经知道这个函数就是callers.py文件中的_multicall函数,代码如下:
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(
"hook call must provide argument %r" % (argname,)
)
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()
这个最核心的函数,其实也是比较容易看懂的,只要前几节的分析大概都还有个印象,那么这个函数还是比较容易理解的
首先定义个一个结果列表,用于存放每个插件的实现函数执行的结果
然后定义了个teardown的列表,用于存放执行teardown操作的操作对象
然后将hook_impls即插件中对接口函数的实现函数倒序遍历,这也是看很多文档博客会说pluggy插件执行的顺序是后注册先执行的原因,然后开始解析函数的参数
然后判断实现函数的hookwrapper属性值是否为True,如果为True表示此函数带有yield关键字,即首先执行yield之前的代码,然后会生成一个对象,即生成器,将生成的对象存放teardowns,用于所有插件之后再来执行这些操作,这也就是为什么网上很多博客等说的pluggy的插件实现函数中如果带有yield,则yield之后的代码会在所有的普通插件执行完成之后再去执行。else分支就是不带yield关键字的实现函数,则执行执行,并且将结果存放到results列表,同时如果判断firstresult结果为True,则结束循环,即执行得到一个结果即OK
当然如果firstresult为False,则所有的插件注册的函数都会执行的
在finnally代码块中可以看到,如果firstresult结果为True,则直接返回第一个结果,而如果firstresult为False,则会讲所有的结果以列表的形式返回
最后再去倒序遍历执行teardown列表中存放的操作,即当带有多个yield关键字插件的时候,后注册的yeild之后的代码先执行
最后将结果返回
ok,pluggy的钩子函数的执行过程的源码分析就到这里了
自动化测试相关教程推荐:
2023最新自动化测试自学教程新手小白26天入门最详细教程,目前已有300多人通过学习这套教程入职大厂!!_哔哩哔哩_bilibili
2023最新合集Python自动化测试开发框架【全栈/实战/教程】合集精华,学完年薪40W+_哔哩哔哩_bilibili
测试开发相关教程推荐
2023全网最牛,字节测试开发大佬现场教学,从零开始教你成为年薪百万的测试开发工程师_哔哩哔哩_bilibili
postman/jmeter/fiddler测试工具类教程推荐
讲的最详细JMeter接口测试/接口自动化测试项目实战合集教程,学jmeter接口测试一套教程就够了!!_哔哩哔哩_bilibili
2023自学fiddler抓包,请一定要看完【如何1天学会fiddler抓包】的全网最详细视频教程!!_哔哩哔哩_bilibili
2023全网封神,B站讲的最详细的Postman接口测试实战教学,小白都能学会_哔哩哔哩_bilibili
总结:
光学理论是没用的,要学会跟着一起敲,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。
如果对你有帮助的话,点个赞收个藏,给作者一个鼓励。也方便你下次能够快速查找。
如有不懂还要咨询下方小卡片,博主也希望和志同道合的测试人员一起学习进步
在适当的年龄,选择适当的岗位,尽量去发挥好自己的优势。
我的自动化测试开发之路,一路走来都离不每个阶段的计划,因为自己喜欢规划和总结,
测试开发视频教程、学习笔记领取传送门!!