Python pkg_resources模块动态加载插件实例分析
使用标准库importlib
的import_module()
函数、django的import_string(),它们都可以动态加载指定的 Python 模块。
举两个动态加载例子:
举例一:
在你项目中有个test函数,位于your_project/demo/test.py中,那么你可以使用import_module来动态加载并调用这个函数而不需要在使用的地方通过import导入。
module_path = 'your_project/demo' module = import_module(module_path) module.test()
举例二:
django的中间件都用过吧,只需要在setting中配置好django就能自动被调用,这也是利用import_string动态加载的。
#settings.py MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', ... ] # 动态加载调用处代码 for middleware_path in reversed(settings.MIDDLEWARE): middleware = import_string(middleware_path) ...
以上方式会有一些缺点:
- 所引用模块不存在时,在项目启动时不会及时抛出错误,只有在真正调用时才能被发现
- 所引用模块要事先写好并放到指定位置,不能删
- 半自动的可插拔性,想禁用某个插件功能需要手动修改代码。如想去掉django的SessionMiddleware功能,那需要手动去修改settings配置文件
pkg_resources实现动态加载插件
下面介绍另外一种动态 载插件的方法,与安装库setuptools一并安装的软件库pkg_resources,它基本解决了上述的问题,并且事实上成为了流行的插件实现方式。 pkg_resources操作的主要单位就是 Distribution(包分发),关于Distribution可以参考这里。Python 脚本启动时,pkg_resources识别出搜索路径中的所有 Distribution 的命名空间包,因此,我们会发现sys.path包含了很多pip安装的软件包的路径,并且可以正确执行import操作。
pkg_resources自带一个全局的WorkingSet对象,代表默认的搜索路径的工作集,也就是我们常用的sys.path工作集。有了这个工作集,那就能轻松实现动态导入任何模块了。
下面上案例:
这个案例是ansible-runner的一个事件处理插件,项目地址GitHub - ansible/ansible-runner-http。只需要把这个包安装到你的虚拟环境,ansible-runner就会自动识别并调用status_handler、event_handler两个事件处理函数。当卸载这个包后,ansible-runner就会使用默认的方式处理事件。相比前面介绍的import_module方式,这种动态加载方式好在对源代码侵入性少,实现真正的即插即用。下面分析它是怎么利用pkg_resources做到的。
ansible-runner-http项目源码目录结构:
├── README.md
├── ansible_runner_http
│├── __init__.py
│└── events.py
└── setup.py
event.py:
... def status_handler(runner_config, data): plugin_config = get_configuration(runner_config) if plugin_config['runner_url'] is not None: status = send_request(plugin_config['runner_url'], data=data, headers=plugin_config['runner_headers'], urlpath=plugin_config['runner_path']) logger.debug("POST Response {}".format(status)) else: logger.info("HTTP Plugin Skipped") def event_handler(runner_config, data): status_handler(runner_config, data)
__init__.py:
from .events import status_handler, event_handler # noqa
setup.py:
from setuptools import setup, find_packages with open('README.md', 'r') as f: long_description = f.read() setup( name="ansible-runner-http", version="1.0.0", author="Red Hat Ansible", url="https://github.com/ansible/ansible-runner-http", license='Apache', packages=find_packages(), long_description=long_description, long_description_content_type='text/markdown', install_requires=[ 'requests', 'requests-unixsocket', ], #方式一:具体到某个module entry_points={'ansible_runner.plugins': ['http = ansible_runner_http']}, #方式二:具体到某个module下的函数 #entry_points={'ansible_runner.plugins': [ # 'status_handler = ansible_runner_http:status_handler', # 'event_handler = ansible_runner_http:event_handler', # ] #}, zip_safe=False, )
重点在setup中的entry_points选项:
- 组名,以点号分隔便于组织层次,但与 Package 没有关联,如
ansible_runner.plugin
- 名字,如 http
- Distribution 中的位置,可以指向一个module如ansible_runner_http。也可以指向module下某个函数如ansible_runner_http:status_handler,前面是 Module,后面是模块内的函数
这样一来一旦这个包被安装后,pkg_resources就可以动态识别这个插件了。
下面看调用方:
... plugins = { #调用load方法,获取指向python的对象 entry_point.name: entry_point.load() for entry_point #调用WorkingSet.iter_entry_points方法遍历所有EntryPoint,参数为组名 in pkg_resources.iter_entry_points('ansible_runner.plugins') } ... def event_callback(self, event_data): ''' Invoked for every Ansible event to collect stdout with the event data and store it for later use ''' for plugin in plugins: plugins[plugin].event_handler(self.config, event_data) ...
方式一写法得到的plugins:
方式二写法得到的plugins:
到此这篇关于Python pkg_resources模块动态加载插件实例分析的文章就介绍到这了,更多相关Python 动态加载插件内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!