(1) pluggy简介
pluggy是一个非常优秀的插件系统,它是理解pytest的核心,只有理解了pluggy的原理,才能更好的理解和使用pytest,否则见到了pytest的很多应用都会感觉很难理解
pluggy插件总共的代码量不足一千行,而实现的功能却是如此的强大和好用,这不由得让我们对pytest的源码实现充满了好奇,接下来一段时间就详细的由浅入深的来解读pluggy源码,这个过程中,同样会继续总结一些基础的或者高级的python的知识点。
当然随着对pluggy源码的深入,也会发现很多在网上书上博客上看不到的pluggy的高级应用,同样本系列都会使用实例演示和分析。
(2)pluggy的简单应用
import pluggy
hookspec = pluggy.HookspecMarker("myproject")
hookimpl = pluggy.HookimplMarker("myproject")
class MySpec:
@hookspec
def myhook(self, arg1, arg2):
pass
class Plugin_1:
@hookimpl
def myhook(self, arg1, arg2):
print("inside Plugin_1.myhook()")
return arg1 + arg2
class Plugin_2:
@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())
results = pm.hook.myhook(arg1=1, arg2=2)
print(results)
执行结果如下:
inside Plugin_2.myhook()
inside Plugin_1.myhook()
[-1, 3]
(3)pluggy的文件组织结构:
pluggy模块总共就有如下6个文件,总共代码行数不到1k行,这6个文件中,caller、hooks.py和manager.py是pluggy的最核心的部分
pluggy
|--------__init__.py
|--------_result.py
|--------_tracing.py
|--------caller.py
|--------hooks.py
|--------manager.py
(4)HookspecMarker类和HookimplMarker类分析
从上面的使用举例看,我们首先是看到了实例化了这两个类的实例,所以,我们就先从这两个类开始分析 下面看下HookspecMarker类的源码(在hooks.py文件中)
阅读源码有一个好处就是能发现一些比较高级的语法和比较好的用法,如果觉得读源码有难度,至少说明一点,我们对python的基础语法或者高级语法掌握的还不是很到位。比如下面 HookspecMarker类的定义,这里面涉及到python的两个相对高级一定的语法,一个是类中call魔法函数的作用,一个就是装饰器类,如果说对这两个知识点不清楚的话,那看到这个类就会有点头大了
class HookspecMarker:
""" Decorator helper class for marking functions as hook specifications.
You can instantiate it with a project_name to get a decorator.
Calling :py:meth:`.PluginManager.add_hookspecs` later will discover all marked functions
if the :py:class:`.PluginManager` uses the same project_name.
"""
def __init__(self, project_name):
self.project_name = project_name
def __call__(
self, function=None, firstresult=False, historic=False, warn_on_impl=None
):
""" if passed a function, directly sets attributes on the function
which will make it discoverable to :py:meth:`.PluginManager.add_hookspecs`.
If passed no function, returns a decorator which can be applied to a function
later using the attributes supplied.
If ``firstresult`` is ``True`` the 1:N hook call (N being the number of registered
hook implementation functions) will stop at I<=N when the I'th function
returns a non-``None`` result.
If ``historic`` is ``True`` calls to a hook will be memorized and replayed
on later registered plugins.
"""
def setattr_hookspec_opts(func):
if historic and firstresult:
raise ValueError("cannot have a historic firstresult hook")
setattr(
func,
self.project_name + "_spec",
dict(
firstresult=firstresult,
historic=historic,
warn_on_impl=warn_on_impl,
),
)
return func
if function is not None:
return setattr_hookspec_opts(function)
else:
return setattr_hookspec_opts
为了彻底理解上面这个类的定义,下面先搞定几个基础的语法知识:
- call魔法函数的作用就是类的实例可以继续像函数一样调用,像函数一样调用对象的时候就会走到类的call魔法函数了
下面用一个实例演示:
class Test(object):
def __init__(self,a,b=10):
self.a=a
self.b=b
def __call__(self,c,d=20):
print(self.a+c)
print(self.b+d)
test=Test(10,20)
test(100,200)
可以看到,test本来是Test类的一个实例,下面紧接着把test当函数一样调用,而且传入了参数,执行结果如下:
110
220
- 利用类中call模范函数的额特性,装饰器可以使用类实现,即装饰器类
下面本质是就是实现了一个装饰器类,装饰器可以传入参数a和b,被装饰器对象可以是函数,即func,也可以传入参数,代码如下:
class Test(object):
def __init__(self,a,b=10):
self.a=a
self.b=b
def __call__(self,func):
def wrapper(*args,**kwargs):
print("before func {func}".format(func=func.__name__))
results=func(*args,**kwargs)
print("after func {func}".format(func=func.__name__))
return results
return wrapper
@Test(10,100)
def print_sum(v1,v2):
print("in print_sum()...")
print(v1+v2)
print_sum(20,200)
执行结果如下:
before func print_sum
in print_sum()...
220
after func print_sum
setattr的作用 setattr的作用即给一个对象设置属性,下面用一个最简单的例子展示:
class Test(object):
pass
t=Test()
setattr(t,"a",10)
print(t.a)
结果会打印10,此时t这个实例就已经有了属性a,而且属性a的值为10 熟悉了上述python基础或高级语法后,再回过头看下HookspecMarker类的代码: 其实它的本质就是一个装饰器类,如果只把这个类实例化,即不用语法糖加在一个具体的函数上时, 即此时返回的是一个setattr_hookimpl_opts,它的参数是另外一个被装饰的函数func,作用就是给 被装饰的函数func设置一个属性,属性名就是初始化的时候传入的名称加上”_spec”,属性值时一个字典,字典 里面有三个字段,分别是firstresult,historic和warm_on_impl
如下是HookspecMarker类的两种使用方法
import pluggy
hookspec = pluggy.HookspecMarker("myproject")
@hookspec
def test1():
pass
@pluggy.HookspecMarker("myproject2")
def test2():
pass
print(test1.myproject_spec)
print(test2.myproject2_spec)
执行结果为:
{'firstresult': False, 'historic': False, 'warn_on_impl': None}
{'firstresult': False, 'historic': False, 'warn_on_impl': None}
所以HookspecMarker类的本质就是为了给被装饰的函数对象增加这么一个属性
同理HookimplMarker类的代码如下,也同样是一个装饰器,也是为了给函数增加一个属性,属性名称为HookimplMarker类初始化时给的project_name加上”_impl”,其值主要有5个参数,至于每个参数做什么用的,可以到后面分析manager文件的时候再回头看
class HookimplMarker:
""" Decorator helper class for marking functions as hook implementations.
You can instantiate with a ``project_name`` to get a decorator.
Calling :py:meth:`.PluginManager.register` later will discover all marked functions
if the :py:class:`.PluginManager` uses the same project_name.
"""
def __init__(self, project_name):
self.project_name = project_name
def __call__(
self,
function=None,
hookwrapper=False,
optionalhook=False,
tryfirst=False,
trylast=False,
specname=None,
):
""" if passed a function, directly sets attributes on the function
which will make it discoverable to :py:meth:`.PluginManager.register`.
If passed no function, returns a decorator which can be applied to a
function later using the attributes supplied.
If ``optionalhook`` is ``True`` a missing matching hook specification will not result
in an error (by default it is an error if no matching spec is found).
If ``tryfirst`` is ``True`` this hook implementation will run as early as possible
in the chain of N hook implementations for a specification.
If ``trylast`` is ``True`` this hook implementation will run as late as possible
in the chain of N hook implementations.
If ``hookwrapper`` is ``True`` the hook implementations needs to execute exactly
one ``yield``. The code before the ``yield`` is run early before any non-hookwrapper
function is run. The code after the ``yield`` is run after all non-hookwrapper
function have run. The ``yield`` receives a :py:class:`.callers._Result` object
representing the exception or result outcome of the inner calls (including other
hookwrapper calls).
If ``specname`` is provided, it will be used instead of the function name when
matching this hook implementation to a hook specification during registration.
"""
def setattr_hookimpl_opts(func):
setattr(
func,
self.project_name + "_impl",
dict(
hookwrapper=hookwrapper,
optionalhook=optionalhook,
tryfirst=tryfirst,
trylast=trylast,
specname=specname,
),
)
return func
if function is None:
return setattr_hookimpl_opts
else:
return setattr_hookimpl_opts(function)
下面用简单的代码演示HookimplMarker装饰器类给函数设置属性的结果
import pluggy
hookspec = pluggy.HookimplMarker("myproject")
@hookspec
def test1():
pass
@pluggy.HookimplMarker("myproject2")
def test2():
pass
print(test1.myproject_impl)
print(test2.myproject2_impl)
执行结果为:
{'hookwrapper': False, 'optionalhook': False, 'tryfirst': False, 'trylast': False, 'specname': None}
{'hookwrapper': False, 'optionalhook': False, 'tryfirst': False, 'trylast': False, 'specname': None}
至此 HookspecMarker类和HookimplMarker类的代码就分析完了
|