pytest源码走读-pluggy的前身之_com.py
背景
pluggy仅有几千行代码,但是是实现pytest框架的一个核心组成。
1.0.06b版本的pytest中,pluggy框架还只是作为一个.py模块出现,没有被独立打包成一个插件,而这个模块就是_com.py。接下来主要读一读_com.py这段源码
钩子函数的实现
研究对象:pytest的_com.py
细化目标:Muticall、Registry、Hook,是一个递进关系
学习方式:实现一遍Hook
Muticall
将多个方法对象(python 万物即对象)存放在list中,在调用这些方法时,执行execute方法,实现多方法的一次性调用输出,输出结果以list结构输出
以下是Muticall的实现,
#coding=utf-8 #使用list实现muticall class Muticall: def __init__(self,methods,*args,**kwargs): self.methods = methods[:] #以list的数据格式入参 self.args = args self.kwargs = kwargs self.result = [] def execute(self): while self.methods: currentmethod = self.methods.pop() res = self.execute_method(currentmethod) self.result.append(res) if self.result: return self.result def execute_method(self,currentmethod): self.currentmethod = currentmethod varnames = currentmethod.func_code.co_varnames #varnames0 = currentmethod.__code__.co_varnames #调用标识位 needscall = varnames[:1] == ('__call__',) #print needscall #print "func_code属性:" #print varnames #print "__code__属性:" #print varnames0 return currentmethod(*self.args, **self.kwargs)
调用过程,
(1)首先初始化Muticall对象,将需要调用的方法,以list的格式赋值给mc的属性self.methods
(2)调用execute()方法,由该方法循环遍历对象,实现挨个执行调用方法的能力,
(3)将返回结果append到list中返回
调用方式(目的是熟悉,固定入参,*args 的tuple 可变长度入参,kwargs 可变长度的key-value 入参该如何入参,muticall 初始化时,如何打包固定参数和tuple入参,具体到调用函数时,又如何解包)
from muticall import Muticall class Test: def mytest(self): return "111" def mytest1(self): return "222" class Test1: def mytest(self,x,*args): print args return x+1 def mytest1(self,x,*args): print args return x+2 class Test2: def mytest(self,**kwargs): return kwargs class Test3: def mytest(self,x,*args,**kwargs): print args,kwargs return x+1 def mytest1(self,x,*args,**kwargs): print args,kwargs return x+2 class Tool: @staticmethod def call_execute(methods,*args,**kwargs): pass if __name__=='__main__': switch = sys.argv[1] if switch == 'None': test = Test() methods = [test.mytest,test.mytest1] mc = Muticall(methods) print mc.execute() elif switch == 'args': test = Test1() args = 1 methods = [test.mytest,test.mytest1] mc = Muticall(methods,3,args) print mc.execute() elif switch == 'kwargs': test = Test2() methods = [test.mytest] mc = Muticall(methods,kw='a') print mc.execute() elif switch == 'args_kwargs': test = Test3() args = 1 methods = [test.mytest,test.mytest1] mc = Muticall(methods,3,args,kw1=1,kw2=2) print mc.execute() else: pass
1、switch == 'None',以空入参方法,调用,返回 ['222', '111']
2、switch == 'args',有固定入参&有可选入参,调用,返回 [5, 4]
3、switch == 'kwargs',有可选key-value 的dict入参,调用,返回 [{'kw': 'a'}]
4、switch == 'args_kwargs',有可选key-value 的dict入参,调用,返回 [5, 4]
Registry
1、将类作为插件,存放在list中,通过操作list的append、remove、 in ,实现插件的注册、注销、判断插件是否注册;以及通过listattr获取指定的属性或者每个插件的指定属性。
2、支持批量注册
#coding=utf-8 #对插件做管理,包括注册、移除插件,遍历插件,检查插件是否注册 #注册对象:类(、方法、实例、属性) #数据结构:list import muticall as Muticall class Registry: Muticall = Muticall #无特殊意义和用途,只是用来做sys.argv[1]=='None'的测试之用 def __init__(self,plugins=None): if plugins is None: plugins = [] self.plugins = plugins def register(self,plugin): self.plugins.append(plugin) def unregister(self,plugin): self.plugins.remove(plugin) def isregister(self,plugin): return plugin in self.plugins def __iter__(self): return iter(self.plugins) def listattr(self,attrname,plugins=None,extra=(),isreverse=False): l = [] if plugins is None: plugins = self.plugins for plugin in list(plugins)+list(extra): try: l.append(getattr(plugin,attrname)) except AttributeError: continue if isreverse: l.reverse() return l
调用过程,
初始化的时候,获得一个注册机对象,包含注册插件的list容器plugins。
可通过register方法,一个一个的注册插件,通过unregister实现注销,isregister判断是否已经注册,listattr可以指定插件,获取插件属性,不指定的情况下,默认输出所有插件的该属性,最终以list格式返回;
可通过初始化注册机对象,实现批量注册
调用方式,
1、实例化一个空注册机,没有任何注册类
2、通过注册函数,注册单个插件
3、注销
4、判断是否注册
5、获取注册插件的指定属性
6、通过初始化,批量注册插件
#coding=utf-8 import sys from register import Registry class test_Registry: pass class Test: pass class Api1: x = 1 class Api2: x = 2 class Api3: x = 3 if __name__ == '__main__': test = test_Registry() test1 = Test() api1 = Api1() api2 = Api2() api3 = Api3() if sys.argv[1] == 'None': registry = Registry() if hasattr(registry,'Muticall'): print 'Muticall' if sys.argv[1] == 'register': registry = Registry() registry.register(test) print "注册test" flag = (list(registry) == [test]) print flag print list(registry) print [registry] if sys.argv[1] == 'unregister': registry = Registry() registry.register(test) registry.register(test1) print "注册[test,test1]" flag = (list(registry)==[test,test1]) print flag print list(registry) print [test,test1] print "注销test1" registry.unregister(test1) flag = (list(registry)==[test]) print flag print list(registry) print [test] if sys.argv[1] == 'isregister': registry = Registry() registry.register(test) print "注册test" flag = (list(registry)==[test]) print flag print list(registry) print [test] print "test已注册:", flag = registry.isregister(test) print flag print "test1已注册:", flag1 = registry.isregister(test1) print flag1 if sys.argv[1] == 'listattr': registry = Registry() registry.register(api1) registry.register(api2) registry.register(api3) print '指定属性&所有插件' print registry.listattr('x') print '指定属性&指定插件:' print registry.listattr('x',[api1]) if sys.argv[1] == 'mutiregister': registry = Registry([api1,api2,api3]) print "初始化时注册api1,api2,api3:" flag = (list(registry)==[api1,api2,api3]) print flag print list(registry) print [api1,api2,api3]
Hook
第一步需要实现注册,利用注册机对待钩的函数进行登记注册;
第二步需要实现调用,利用Muticall 对注册的钩子函数进行批量执行;
以下是对1.0.06b的钩子实现的解读:
class Hooks: def __init__(self, hookspecs, registry=None): self._hookspecs = hookspecs if registry is None: registry = py._com.comregistry self.registry = registry for name, method in vars(hookspecs).items():#是class Object的dict的key和value(key- name,value-method) #只注册方法 if name[:1] != "_":#找函数名(所以强烈建议甚至禁止,函数名开头使用下划线) #print method.__dict__ #空方法对象的是个空dict firstresult = getattr(method, 'firstresult', False) #print "注册一个方法需要的信息,注册插件,注册函数名,注册属性是否存在" #print registry.__dict__ #print name #print firstresult mm = HookCall(registry, name, firstresult=firstresult) #打包成一个钩子对象 print '\033[1;31m 打包的钩子对象:\033[0m' print mm.__dict__ setattr(self, name, mm) print('\033[0;31m hookspecs='+ str(self._hookspecs) + '\033[0m') print self.__dict__ #for key in self: #print(key+":"+self[key]) def __repr__(self): return "<Hooks %r %r>" %(self._hookspecs, self.registry) class HookCall: def __init__(self, registry, name, firstresult, extralookup=None): self.registry = registry self.name = name self.firstresult = firstresult self.extralookup = extralookup and [extralookup] or () def clone(self, extralookup): return HookCall(self.registry, self.name, self.firstresult, extralookup) def __repr__(self): mode = self.firstresult and "firstresult" or "each" return "<HookCall %r mode=%s %s>" %(self.name, mode, self.registry) def __call__(self, *args, **kwargs): if args: raise TypeError("only keyword arguments allowed " "for api call to %r" % self.name) attr = self.registry.listattr(self.name, extra=self.extralookup) mc = MultiCall(attr, **kwargs) # XXX this should be doable from a hook impl: if self.registry.logfile: self.registry.logfile.write("%s(**%s) # firstresult=%s\n" % (self.name, kwargs, self.firstresult)) self.registry.logfile.flush() return mc.execute(firstresult=self.firstresult) comregistry = Registry()
在使用的时候,除了第一个类需要初始化一次钩子对象,其他的具有相同方法的类,直接通过注册,即可实现钩子函数的身份登记
然后,直接使用实例化钩子对象,调用暴露出来的钩子函数,即可完成调用。调用过程使用了listattr()方法遍历该类该方法先的属性(参数值),还原该方法的入参格式,供Muticall 调用
class TestHooks: def test_happypath(self): registry = Registry() class Api: def hello(self, arg): return arg mcm = Hooks(hookspecs=Api, registry=registry) #registry.register(Api()) #r = mcm.hello(arg="222") #print "调用钩子函数:" #print r assert hasattr(mcm, 'hello') assert repr(mcm.hello).find("hello") != -1 class Plugin: def hello(self, arg): return arg+"1" registry.register(Plugin()) l = mcm.hello(arg="3") print l #assert l == [4] assert not hasattr(mcm, 'world') print mcm.__dict__ class Test: def hello(self,arg): return arg+"5" registry.register(Test()) #t = mcm.hello(arg='abc') t = mcm.hello(arg='a') print "调用钩子函数:" print t #assert t == "abc"
参考资料:http://markshao.github.io/categories/pytest-%E6%BA%90%E7%A0%81%E8%A7%A3%E8%AF%BB/(将pytest中hook和pluggy的关系,pytest中hook的作用指明)