前言
現在我們開始分析,按照demo代碼順序先進行簡單分析。
個人拙見,有錯請各位指出。
如果的我的文章對您有幫助,不符動動您的金手指給個Star,予人玫瑰,手有餘香,不勝感激。 GitHub
pluggy代碼結構
按照前面demo中的代碼順序,在分析pluggy的核心邏輯之前,我們先來了解HookspecMarker
、HookspecMarker
的用處是什麼?
1.HookspecMarker
的實現邏輯是什麼?
我們來先來看它的代碼註釋
class HookspecMarker(object):
""" Decorator helper class for marking functions as hook specifications.
You can instantiate it with a project_name to get a decorator.
Calling PluginManager.add_hookspecs later will discover all marked functions
if the PluginManager uses the same project_name.
"""
def __init__(self, project_name):
self.project_name = project_name
- 我們可以傳入
project_name
實例化HookspecMarker
以獲得裝飾器,當我們調用PluginManager.add_hookspec
將會尋找所有與當前PluginManager
同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 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__
的邏輯代碼可以發現,主要功能是調用了一個setattr(object, name, value)
,給被裝飾的函數新增一個屬性project_nam + _spec
,並且該屬性的value爲裝飾器參數取值。
2.HookspecMarker
的實現邏輯是什麼?
HookimplMarker
的實現邏輯類似,區別在於被裝飾的函數新增的屬性爲project_name + _impl
,下面只顯示了部分代碼
def setattr_hookimpl_opts(func):
setattr(
func,
self.project_name + "_impl",
dict(
hookwrapper=hookwrapper,
optionalhook=optionalhook,
tryfirst=tryfirst,
trylast=trylast,
),
)
return func
if function is None:
return setattr_hookimpl_opts
else:
return setattr_hookimpl_opts(function)
pluggy核心設計
plugy的核心邏輯就是幾行代碼
pm = PluginManager("myPluggyDemo")
pm.add_hookspecs(HookSpec)
pm.register(HookImpl1())
pm.hook.calculate(a=2, b=3)
- 創建一個
PluginManager
對象,用於管理plugin - 調用
add_hookspecs
, 增加一個新的hook module object(標準對象) - 調用
register
,註冊一個新的plugin object - 通過
pm.hook
實現對與calculate
同名的所有plugin
的調用
按照上面的代碼邏輯來走,我們來分析三行代碼的實現,以幫助我們更好的理解
-
pm.add_hookspecs(HookSpec)
是怎麼實現的? -
pm.register(HookImpl1())
是怎麼實現的? -
pm.hook.calculate(a=2, b=3)
是怎麼實現的?
1.PluginManager.add_hookspecs()
是怎麼實現的?
Demo中的pm.add_hookspecs(HookSpec)
是怎麼實現的?
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): #1.遍歷傳入對象的所有屬性方法列表
spec_opts = self.parse_hookspec_opts(module_or_class, name) #2.拿到我們前面在HookspecMarker爲函數新增的那個屬性project_name + _spec
- 遍歷傳入對象的所有屬性方法列表
- 拿到每個屬性方法,若有特殊屬性
project_name + _spec
,則返回它,否則返回None
,下面是該方法的代碼展示
def parse_hookspec_opts(self, module_or_class, name): #2.1拿到該屬性的方法實現
method = getattr(module_or_class, name) #此處獲取到我們之前定義的hook方法
return getattr(method, self.project_name + "_spec", None) #此處獲取到爲該方法新增的屬性project_name + _spec
2.PluginManager.register()
是怎麼實現的?
Demo中的pm.register(HookImpl1())
是怎麼實現的?
pm.register
的作用是註冊一個pluggy的實現並將其與對應的hook關聯起來,我們來看主要代碼
# register matching hook implementations of the plugin
self._plugin2hookcallers[plugin] = hookcallers = []
for name in dir(plugin):
hookimpl_opts = self.parse_hookimpl_opts(plugin, name) #獲取pluggy的屬性或方法中的特殊attribute project_name + _impl
if hookimpl_opts is not None:
normalize_hookimpl_opts(hookimpl_opts)
method = getattr(plugin, name) #特殊attribute存在時獲取到plugin的對應方法
hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts)
hook = getattr(self.hook, name, None)
if hook is None:
hook = _HookCaller(name, self._hookexec) #爲hook添加一個_HookCaller對象
setattr(self.hook, name, hook)
elif hook.has_spec():
self._verify_hook(hook, hookimpl)
hook._maybe_apply_history(hookimpl)
hook._add_hookimpl(hookimpl) #將hookimpl添加到hook中
hookcallers.append(hook) #將遍歷找到的每一個plugin hook添加到hookcallers,以待調用
- 遍歷pluggy對象的所有屬性或方法(method),並獲取該pluggy method的特殊
attribute
project_name + _impl
- 將帶有project_name + _impl的method封裝成一個HookImpl中
- 再把一個
_HookCaller
的對象添加到hook
中,併爲self.hook
新增一個value爲hook
,name爲method
的屬性(比如前面的demo的calculate
) - 最後將遍歷找到的每一個
_HookCaller
添加到hookcallers,以待調用
3.PluginManager.hook.method()
是怎麼實現的?
pm.hook是什麼?實現調用pluggy的邏輯是什麼?
這裏就涉及到了上一步的_HookCaller
了,pm.hook.calculate
其實是相當於獲取了對應_HookCaller
,調用的是他的__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)
核心代碼在最後一行,我們再來看看self._hhokexec是什麼,發現它是在構造_HookCaller時傳入的一個參數,再找到它的定義
def _hookexec(self, hook, methods, kwargs):
# called from all hookcaller instances.
# enable_tracing will set its own wrapping function at self._inner_hookexec
return self._inner_hookexec(hook, methods, kwargs)
順着走到最後,發現核心其實是hook.multicall
self._inner_hookexec = lambda hook, methods, kwargs: hook.multicall(
methods,
kwargs,
firstresult=hook.spec.opts.get("firstresult") if hook.spec else False,
)
這是一個PluggyManager構建時的封裝函數_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,)
)
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()