本文系《pytest源码剖析》系列内容
正在连载,欢迎关注
3. 插件机制实例分析
在 pluggy 的文档中提供了一个完整的名为 “eggsample” 软件 + 插件实例 ,
接下来,我以这个实例进行分析,更加深入得体验 pluggy 插件系统的使用
01
编写软件
以一个名为 eggsample
的项目编写软件本体,
首先,创建第一个文件来定义 hookimpl,供内部或外部插件实现 hook 使用
# eggsample/eggsample/__init__.py
import pluggy
hookimpl = pluggy.HookimplMarker("eggsample")
接着,创建第二个文件来定义 hookspecs,申明本软件中一共有些什么样的 hook
# eggsample/eggsample/hookspecs.py
import pluggy
hookspec = pluggy.HookspecMarker("eggsample")
@hookspec
def eggsample_add_ingredients(ingredients: tuple):
"""本hook接收ingredients参数,并返回修改后的ingredients"""
@hookspec
def eggsample_prep_condiments(condiments: dict):
"""本hook接收condiments参数,并返回一个字符串作为插件评论"""
接下来,实现一个内部插件,供软件本体使用
# eggsample/eggsample/lib.py
# 此时lib.py就相当于一个插件了,只不过它是内部插件,不需要安装
@eggsample.hookimpl # 实现了hook,但是没有按照hook要求接收参数
def eggsample_add_ingredients():
spices = ["salt", "pepper"]
you_can_never_have_enough_eggs = ["egg", "egg"]
ingredients = spices + you_can_never_have_enough_eggs
return ingredients
@eggsample.hookimpl # 实现了hook,但是没有按照hook要求返回字符串
def eggsample_prep_condiments(condiments):
condiments["mint sauce"] = 1
最后,编写软件本体的代码
# eggsample/eggsample/host.py
import itertools
import random
import pluggy
from eggsample import hookspecs, lib
condiments_tray = {"pickled walnuts": 13, "steak sauce": 4, "mushy peas": 2}
def main():
pm = get_plugin_manager()
cook = EggsellentCook(pm.hook)
cook.add_ingredients()
cook.prepare_the_food()
cook.serve_the_food()
def get_plugin_manager():
pm = pluggy.PluginManager("eggsample")
pm.add_hookspecs(hookspecs)
pm.load_setuptools_entrypoints("eggsample")
pm.register(lib)
return pm
class EggsellentCook:
FAVORITE_INGREDIENTS = ("egg", "egg", "egg")
def __init__(self, hook):
self.hook = hook
self.ingredients = None
def add_ingredients(self):
results = self.hook.eggsample_add_ingredients(
ingredients=self.FAVORITE_INGREDIENTS
)
my_ingredients = list(self.FAVORITE_INGREDIENTS)
# Each hook returns a list - so we chain this list of lists
other_ingredients = list(itertools.chain(*results))
self.ingredients = my_ingredients + other_ingredients
def prepare_the_food(self):
random.shuffle(self.ingredients)
def serve_the_food(self):
condiment_comments = self.hook.eggsample_prep_condiments(
condiments=condiments_tray
)
print(f"Your food. Enjoy some {', '.join(self.ingredients)}")
print(f"Some condiments? We have {', '.join(condiments_tray.keys())}")
if any(condiment_comments):
print("\n".join(condiment_comments))
if __name__ == "__main__":
main()
在软件本体中,做了几件事情:
-
创建 pm(22 行)
-
加载 hook 申明(23 行)
-
通过 setuptools 加载第三方插件 (24 行)
-
通过 import 加载内置插件 (25 行)
-
调用 hook,完成软件功能
从这个例子来看,hook 的调用代码和 非 hook 调用代码混在一起的,
也就是说,软件会将插件(hook 的实现)作为自己的一部分进行使用,而不关心这些插件被定义在哪里
还没有结束,为了使用插件能够像 pytest 那样能够安装、运行,还需要为软件额外创建一个 setup.py
# eggsample/setup.py
setup(
name="eggsample",
install_requires="pluggy",
entry_points={"console_scripts": ["eggsample=eggsample.host:main"]},
packages=find_packages(),
)
全部就绪之后,就可以使用 pip 进行安装了
pip install -e eggsample
OK,一个 python 软件就开发完毕并且安装成功了!
运行一下看看效果,在命令执行 eggsample
它会将内部定义的数据 ("egg", "egg", "egg")
再加上插件返回的数据(["salt", "pepper","egg", "egg"]
)合并在一起,打乱顺序后输出
egg, egg, salt, egg, egg, pepper, egg
同时,也会将内部定义的数据 ({"pickled walnuts": 13, "steak sauce": 4, "mushy peas": 2}
) 传递给插件,在插件对数据进行修改后再进行输出
pickled walnuts, steak sauce, mushy peas, mint sauce
目前软件只使用了内部插件来完成部分功能,
接下来看一看外部插件如何创建,
以及安装外部插件后,软件的执行效果
首发于公众号:测试开发研习社
原创不易,喜欢请星标+点赞+在看