本長文不適合手機端閱讀,請酌情退出python
公司架構組基於pytest自研了一套測試框架sstest,目的是爲了讓業務組(也就是我在的組)更好的寫單元測試,從而提升代碼質量,單元測試的目的是爲了迴歸校驗,避免新提交的代碼影響到項目中舊的功能。架構
我是組裏第一個接入sstest的同窗,踩了不少坑...從而對pytest的源碼產生了興趣,在閱讀pytest源碼的過程當中,發現pluggy插件系統實際上是pytest的核心,能夠說pytest只是將多個插件利用pluggy構建出來的項目,因此先分析pluggy。app
老規矩,在開始分析前,但願本身搞清楚的幾個問題:框架
隨着分析的進行會有新的問題拋出,問題能夠幫助咱們理清目的,避免迷失在源碼中。單元測試
pluggy插件系統與我此前研究的python插件系統不一樣,pluggy不能夠動態插入,即沒法在程序運行的過程當中利用插件添加新的功能。測試
pluggy主要有3個概念:spa
簡單使用一下,代碼以下。插件
import pluggy
# 建立插件規範類裝飾器
hookspac = pluggy.HookspecMarker('example')
# 建立插件類裝飾器
hookimpl = pluggy.HookimplMarker('example')
class MySpec(object):
# 建立插件規範
@hookspac
def myhook(self, a, b):
pass
class Plugin_1(object):
# 定義插件
@hookimpl
def myhook(self, a, b):
return a + b
class Plugin_2(object):
@hookimpl
def myhook(self, a, b):
return a - b
# 建立manger和添加hook規範
pm = pluggy.PluginManager('example')
pm.add_hookspecs(MySpec)
# 註冊插件
pm.register(Plugin_1())
pm.register(Plugin_2())
# 調用插件中的myhook方法
results = pm.hook.myhook(a=10, b=20)
print(results)
複製代碼
整段代碼簡單而言就是建立相應的類裝飾器裝飾類中的方法,經過這些類裝飾器構建出了插件規範與插件自己。code
首先,實例化PluginManager類,實例化時須要傳入全局惟一的project name,HookspecMarker類與HookimplMarker類的實例化也須要使用相同的project name。orm
建立完插件管理器後,經過add_hookspecs方法添加插件規範、經過register方法添加插件自己則可。
添加完插件調用規範與插件自己後,就能夠經過插件管理器的hook屬性直接調用插件了。
閱讀到這裏,關於問題「1,2,3」便有了答案。
pluggy使用的過程能夠分爲4步:
經過類裝飾器與PluginManager.add_hookspecs、PluginManager.register方法的配合,輕鬆實現插件的可插拔操做,其背後原理其實就是被類裝飾器裝飾後的方法會被動態添加上新的屬性信息,而對應的add_hookspecs與register等方法會根據這些屬性信息來判斷是否爲插件規範或插件自己。
想要在外部系統中使用插件,只須要調用pm.hook.any_hook_function方法則可,任意註冊了插件均可以被輕鬆調用。
但這裏引出了新的問題:
這三個問題關注的是實現細節,下面進一步步進行分析。
代碼中,使用了hookspac類裝飾器定義出插件調用規範,使用了hookimpl類裝飾器定義出插件自己,二者的做用其實就是「爲被裝飾的方法添加新的屬性」。由於二者邏輯類似,因此這裏就只分析hookspac類裝飾器代碼,代碼以下:
class HookspecMarker(object):
def __init__(self, project_name):
self.project_name = project_name
def __call__( self, function=None, firstresult=False, historic=False, warn_on_impl=None ):
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__
方法最核心的邏輯即是使用setattr方法爲被裝飾的func方法添加新的屬性,屬性名爲當前project name加上_spec後綴,而屬性的值爲一個字典對象。
在調用PluginManager.add_hookspecs方法時會利用hookspac類裝飾器添加的信息
HookimplMarker類相似,只是添加的屬性有所不一樣,核心代碼以下。
setattr(
func,
self.project_name + "_impl",
dict(
hookwrapper=hookwrapper,
optionalhook=optionalhook,
tryfirst=tryfirst,
trylast=trylast,
specname=specname,
),
)
複製代碼
因此關於「4.類裝飾器是如何將某個類中的方法設置成插件的?」,其實就是利用setattr方法爲當前方法設置新的屬性,這些屬性至關於提供了一種信息,PluginManager會根據這些信息判斷該方法是否是插件,跟下面例子本質相同。
In [1]: def fuc1():
...: print('hh')
...:
In [2]: setattr(fuc1, 'fuc1' + '_impl', dict(a=1, b=2))
In [3]: fuc1.fuc1_impl
Out[3]: {'a': 1, 'b': 2}
複製代碼
實例化pluggy.PluginManager類後即可以經過add_hookspecs方法添加插件規範與register方法註冊插件。
要搞清楚「pluggy是如何關聯插件規範與插件自己的?」,就須要深刻它們的源碼。
實例化PluginManager類,其實就是調用它的__init__
方法。
def __init__(self, project_name, implprefix=None):
"""If ``implprefix`` is given implementation functions will be recognized if their name matches the ``implprefix``. """
self.project_name = project_name
# ...省略部分...
# 關鍵
self.hook = _HookRelay()
self._inner_hookexec = lambda hook, methods, kwargs: hook.multicall(
methods,
kwargs,
firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
)
複製代碼
關鍵在於定義了self.hook屬性與self._inner_hookexec屬性,它是一個匿名方法,會接收hook、methods、kwargs三個參數並將這些參數傳遞給hook.multicall方法。
隨後調用add_hookspecs方法添加插件規範,其代碼以下。
class PluginManager(object):
# 獲取被裝飾方法中對應屬性的信息(HookspecMarker裝飾器添加的信息)
def parse_hookspec_opts(self, module_or_class, name):
method = getattr(module_or_class, name)
return getattr(method, self.project_name + "_spec", None)
def add_hookspecs(self, module_or_class):
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)
# ...省略部分代碼...
複製代碼
上述代碼中經過parse_hookspec_opts方法獲取方法中相應屬性的參數,若是參數不爲None那麼則獲取_HookRelay類中的被裝飾方法的信息(該方法就是MySpec類的myhook),從源碼中能夠發現_HookRelay類實際上是空類,它存在的意義其實就是接收新的屬性,分析到後面你就會發現_HookRelay類其實就是用於鏈接插件規範與插件自己的類。
若是_HookRelay類中沒有myhook屬性信息,則實例化_HookCaller類並將其做爲self.hook的屬性,具體而言,就是將_HookCaller類的實例做爲_HookRelay類中myhook屬性的值。
_HookCaller類很重要,其部分代碼以下。
class _HookCaller(object):
def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None):
self.name = name
# ...省略...
self._hookexec = hook_execute
self.spec = None
if specmodule_or_class is not None:
assert spec_opts is not None
self.set_specification(specmodule_or_class, spec_opts)
def has_spec(self):
return self.spec is not None
def set_specification(self, specmodule_or_class, spec_opts):
assert not self.has_spec()
self.spec = HookSpec(specmodule_or_class, self.name, spec_opts)
if spec_opts.get("historic"):
self._call_history = []
複製代碼
關鍵在於set_specification方法,該方法會實例化HookSpec類並將其複製給self.spec。
至此,插件規範就添加完了,緊接着經過register方法註冊插件自己,其核心代碼以下。
def register(self, plugin, name=None):
# 省略部分代碼
for name in dir(plugin):
hookimpl_opts = self.parse_hookimpl_opts(plugin, name)
if hookimpl_opts is not None:
normalize_hookimpl_opts(hookimpl_opts)
method = getattr(plugin, name)
# 實例化插件
hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts)
name = hookimpl_opts.get("specname") or name
hook = getattr(self.hook, name, None) # 獲取 hookspec 插件規範
if hook is None:
hook = _HookCaller(name, self._hookexec)
setattr(self.hook, name, hook)
elif hook.has_spec():
# 檢查插件方法的方法名與參數是否與插件規範相同
self._verify_hook(hook, hookimpl)
hook._maybe_apply_history(hookimpl)
# 添加到插件規範中,完成插件與插件規範的綁定
hook._add_hookimpl(hookimpl)
hookcallers.append(hook)
複製代碼
首先經過self.parse_hookimpl_opts方法獲取被hookimpl裝飾器添加的信息,隨後經過getattr(plugin, name)方法獲取方法名,其實就是myhook,最後初始化HookImpl類,其實就是插件自己,並將其與對應的插件規範綁定,經過_add_hookimpl方法實現這一目的。
_add_hookimpl方法會根據hookimpl實例中的屬性判斷其插入的位置,不一樣位置,調用順序不一樣,代碼以下。
def _add_hookimpl(self, hookimpl):
"""Add an implementation to the callback chain. """
# 是否有 包裝器 (即插件邏輯中使用了yield關鍵字)
if hookimpl.hookwrapper:
methods = self._wrappers
else:
methods = self._nonwrappers
# 先調用仍是後代碼
if hookimpl.trylast:
methods.insert(0, hookimpl)
elif hookimpl.tryfirst:
methods.append(hookimpl)
else:
# find last non-tryfirst method
i = len(methods) - 1
while i >= 0 and methods[i].tryfirst:
i -= 1
methods.insert(i + 1, hookimpl)
複製代碼
至此「5.pluggy是如何關聯插件規範與插件自己的?」的問題也是明白了,簡單而言,插件規範與插件自己都被裝飾器添加了特殊信息,經過這些特殊信息將二者找到並分佈利用這些屬性的值初始化_HookCaller類(插件規範)與HookImpl類(插件自己),最後經過_add_hookimpl方法完成綁定。
從一開始的示例代碼中,能夠發現,調用myhook插件方法經過pm.hook.myhook(a=10, b=20)
方法則可。
背後是什麼?
PluginManager.hook其實就是_HookRelay類,而_HookRelay類模式是一個空類,經過add_hookspecs方法與register方法的操做,_HookRelay類中多了名爲myhook的屬性,該屬性對應着_HookCaller類實例。
pm.hook.myhook(a=10, b=20)其實就是調用_HookCaller.__call__
,代碼以下。
def __call__(self, *args, **kwargs):
# 省略部分代碼
if self.spec and self.spec.argnames:
# 計算插件規範中能夠接收的參數與插件自己能夠接收的參數是否相同
notincall = (
set(self.spec.argnames) - set(["__multicall__"]) - set(kwargs.keys())
)
if notincall:
# 省略代碼
# 調用方法
return self._hookexec(self, self.get_hookimpls(), kwargs)
複製代碼
__call__
方法的主要就是判斷插件規範與插件自己是否匹配,而後經過self._hookexec方法去真正的執行。
經過分析,完整的調用鏈條爲:_HookCaller._hookexec
-> PluginManager._inner_hookexec
-> _HookCaller.multicall
-> callers文件的中的_multicall方法
_multicall方法中最關鍵的代碼片斷以下。
def _multicall(hook_impls, caller_kwargs, firstresult=False):
for hook_impl in reversed(hook_impls):
try:
# 調用myhook方法
args = [caller_kwargs[argname] for argname in hook_impl.argnames]
# 省略代碼
# 若是插件中使用了yeild,則經過這種方式調用
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")
複製代碼
至此,pluggy的核心邏輯就擼完了。
若是你看完了,恭喜呀,但這只是pluggy最簡單的模式,它還有一些比較重要的方法,由於篇幅緣由就沒往上貼了,各位感興趣能夠自行研究或跟我探討。
後面抽空出篇水本談談看源碼的一些建議。