本教程是一步一步地演示如何定义插件,然后在您的应用程序中使用装载和使用这些插件。
命名插件指南
Stevedore使用setuptools入口点来定义和加载插件。 entry point是指在Python模块或包中定义的命名对象的标准方法。 名称可以是对任何类、函数或实例的引用,只要它是在导入包含模块的时候创建的。 ( 即:它需要一个模块级的全局 )
名称和命名空间
entry point使用命名空间中的名称进行注册。
entry point名称通常被认为是用户可见的。 例如:们经常出现在配置文件中,这是启动驱动程序的地方。 因为它们是公开的,所以在保持描述性的同时,名称通常尽可能短。 如: 数据库驱动程序插件名可能是mysql、postgresql、sqlite等。
另一方面,名称空间是一个实现细节,虽然它们是开发人员知道的,但它们通常不会公开给用户。 名称空间的命名语法看起来很像Python的包语法(a.b.c),但是名称空间并不对应于Python包。 为entry point命名空间使用Python包名是一种简单的方法来确保唯一的名称,但这完全不是必需的。 entry point 的主要特征是可以跨包发现它们。 这意味着一个插件可以与应用程序完全分开开发和安装, 只要他们同意命名空间和API。
每个名称空间都由使用该插件的代码所拥有,并用于搜索 entry points 。 entry point的名称通常是由插件所拥有的,但是它们也可以通过命名钩子的代码来定义(看 HookManager
) 。 在给定的 distribution 中,entry point的名称必须是唯一的,但在名称空间中不一定是唯一的 。
创建插件
经过反复试验,我发现定义API的最简单方法是遵循以下步骤:
- 使用abc模块创建一个基本抽象类来定义API插件所需的行为。 开发人员不需要从基类中子类化,但是它提供了一种方便的方式来记录API,并且使用抽象基类使代码保持可靠。
- 通过子类化基类并实现所需的方法来创建插件。
- 通过结合应用程序的名称(或库)和API的名称,为每个API定义一个惟一的名称空间。 保持简单, 例如: “cliff.formatters” or “ceilometer.pollsters.compute” 。
示例插件集
本教程中的示例程序将创建一个带有几个数据格式化程序的插件集,比如命令行程序可以使用什么来准备将数据打印到控制台。 每个格式化程序都将作为输入,使用字符串键和内置的数据类型作为值。 它将返回作为输出的迭代器,该迭代器根据所使用的特定格式化程序的规则生成数据结构的数据结构。 formatter的构造函数允许调用者指定输出应该具有的最大宽度。
一个插件的基类
上面的步骤1是为需要由每个插件实现的API定义一个抽象基类。
# stevedore/example/base.py import abc import six @six.add_metaclass(abc.ABCMeta) class FormatterBase(object): """Base class for example plugin used in the tutorial. """ def __init__(self, max_width=60): self.max_width = max_width @abc.abstractmethod def format(self, data): """Format the data and return unicode text. :param data: A dictionary with string keys and simple types as values. :type data: dict(str:?) :returns: Iterable producing the formatted text. """
构造函数是一个具体的方法,因为子类不需要覆盖它,但是format()方法没有做任何有用的事情,因为没有可用的默认实现。
继承并实现插件基类
下一步是创建几个带有format()的具体实现的插件类。 一个简单的示例格式化程序在一行中使用每个变量名和值生成输出。
# stevedore/example/simple.py from stevedore.example import base class Simple(base.FormatterBase): """A very basic formatter. """ def format(self, data): """Format the data and return unicode text. :param data: A dictionary with string keys and simple types as values. :type data: dict(str:?) """ for name, value in sorted(data.items()): line = '{name} = {value}\n'.format( name=name, value=value, ) yield line
还有很多其他的格式化选项,但是这个例子将给我们提供足够的工作来演示注册和使用插件。
注册插件
要使用setuptools entry point,必须使用setuptools打包应用程序或库。 构建和打包过程生成元数据,这些元数据可以在安装之后找到,以找到每个python发行版提供的插件。
entry point必须被声明为属于特定的名称空间,因此我们需要在进行下一步之前选择一个。 这些插件是来自于 stevedore 的例子, 因此我将使用 “stevedore.example.formatter”名称空间。 现在可以在包装说明中提供所有必要的信息:
# stevedore/example/setup.py from setuptools import setup, find_packages setup( name='stevedore-examples', version='1.0', description='Demonstration package for stevedore', author='Doug Hellmann', author_email='doug@doughellmann.com', url='http://git.openstack.org/cgit/openstack/stevedore', classifiers=['Development Status :: 3 - Alpha', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Intended Audience :: Developers', 'Environment :: Console', ], platforms=['Any'], scripts=[], provides=['stevedore.examples', ], packages=find_packages(), include_package_data=True, entry_points={ 'stevedore.example.formatter': [ 'simple = stevedore.example.simple:Simple', 'plain = stevedore.example.simple:Simple', ], }, zip_safe=False, )
底部最重要的地方就是给setup()设置entry point。 该值是一个字典,将插件的名称空间映射到它们的定义列表。 列表中的每一项都应该是一个带有name=module:importable的形式,此名对用户是可见的, module 是模块的Python导入引用 , importable 是可以从模块内部导入的名称 ,如下:
'simple = stevedore.example.simple:Simple', 'plain = stevedore.example.simple:Simple', ], }, zip_safe=False, )
在本例中,有两个插件注册了。 上面定义的简单插件和一个普通的插件,它只是简单插件的别名。
setuptools元数据
在构建期间, setuptools 为软件包复制 entry point定义 到 “.egg-info” 目录的文件中。例如,stevedore例子的entry point 位于 stevedore.egg-info/entry_points.txt
,内容如下:
[stevedore.example.formatter] simple = stevedore.example.simple:Simple plain = stevedore.example.simple:Simple [stevedore.test.extension] t2 = stevedore.tests.test_extension:FauxExtension t1 = stevedore.tests.test_extension:FauxExtension
pkg_resources
使用 entry_points.txt
从已安装到环境中的所有软件中查找插件。 你不应该修改这些文件, 除了更改setup.py中的entry point列表。
在其他包中添加插件
插件的吸引力除了 entry points 之外 ,还有就是它们可以独立于应用程序进行分发。 setuptools 命名空间用来区分插件与Python源代码名称空间不同。 通常使用一个插件名称空间,前缀是加载插件的应用程序或库的名称,以确保它是惟一的,但是这个名称对于Python包的代码应该如何生存没有任何影响。
例如, 我们可以添加一个formatter插件的另一个实现,它可以生成一个 reStructuredText field list
# stevedore/example2/fields.py import textwrap from stevedore.example import base class FieldList(base.FormatterBase): """Format values as a reStructuredText field list. For example:: : name1 : value : name2 : value : name3 : a long value will be wrapped with a hanging indent """ def format(self, data): """Format the data and return unicode text. :param data: A dictionary with string keys and simple types as values. :type data: dict(str:?) """ for name, value in sorted(data.items()): full_text = ': {name} : {value}'.format( name=name, value=value, ) wrapped_text = textwrap.fill( full_text, initial_indent='', subsequent_indent=' ', width=self.max_width, ) yield wrapped_text + '\n'
新的插件可以使用 setup.py来打包
# stevedore/example2/setup.py from setuptools import setup, find_packages setup( name='stevedore-examples2', version='1.0', description='Demonstration package for stevedore', author='Doug Hellmann', author_email='doug@doughellmann.com', url='http://git.openstack.org/cgit/openstack/stevedore', classifiers=['Development Status :: 3 - Alpha', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Intended Audience :: Developers', 'Environment :: Console', ], platforms=['Any'], scripts=[], provides=['stevedore.examples2', ], packages=find_packages(), include_package_data=True, entry_points={ 'stevedore.example.formatter': [ 'field = stevedore.example2.fields:FieldList', ], }, zip_safe=False, )
新插件是在一个叫stevedore-examples2的独立包
setup( name='stevedore-examples2',
这个插件也是注册在stevedore.example.formatter命名空间里的
'stevedore.example.formatter': [ 'field = stevedore.example2.fields:FieldList', ], },
当插件名称空间被扫描时,所有在当前 PYTHONPATH
的软件包会被检测到, 来自第二个软件包的 entry point 能够被找到并加载,而应用程序并不需要知道插件具体安装在哪里。
加载插件
在使用插件时有几种不同的启用调用方式,依赖于你的需要。
加载驱动
最常用的方式是插件作用单独的驱动程序。这种情况,一般是有多个插件选择但是只有一个需要加载和调用。 DriverManager
能够支持这种方式。下面例子就是使用 DriverManager
方式来实现
# stevedore/example/load_as_driver.py from __future__ import print_function import argparse from stevedore import driver if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument( 'format', nargs='?', default='simple', help='the output format', ) parser.add_argument( '--width', default=60, type=int, help='maximum output width for text', ) parsed_args = parser.parse_args() data = { 'a': 'A', 'b': 'B', 'long': 'word ' * 80, } mgr = driver.DriverManager( namespace='stevedore.example.formatter', name=parsed_args.format, invoke_on_load=True, invoke_args=(parsed_args.width,), ) for chunk in mgr.driver.format(data): print(chunk, end='')
manager 接收插件的命名空间和名字作为参数,用他们来找插件。因为 invoke_on_load
调用true,它会调用对象加载。在这种情况对象是被注册为 formatter 的插件类。 invoke_args
是会被传进类构造器的可选参数, 用于设置最大宽度参数。
mgr = driver.DriverManager( namespace='stevedore.example.formatter', name=parsed_args.format, invoke_on_load=True, invoke_args=(parsed_args.width,), )
manager 被创建之后, 它通过调用注册为插件的代码返回一个对象。 该对象是实际的驱动程序,在本例中是来自插件的formatter类的实例。 可以通过管理器的驱动程序访问单个驱动程序,然后可以直接调用它的方法 。
for chunk in mgr.driver.format(data): print(chunk, end='')
运行示例程序会产生这个输出
$ python -m stevedore.example.load_as_driver a = A b = B long = word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word $ python -m stevedore.example.load_as_driver field : a : A : b : B : long : word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word $ python -m stevedore.example.load_as_driver field --width 30 : a : A : b : B : long : word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word
加载扩展
另一个常见的用例是一次加载多个扩展, 然后调用他们。 其他几个管理器类支持这种调用模式 ,包括: ExtensionManager
, NamedExtensionManager
, and EnabledExtensionManager
.
# stevedore/example/load_as_extension.py from __future__ import print_function import argparse from stevedore import extension if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument( '--width', default=60, type=int, help='maximum output width for text', ) parsed_args = parser.parse_args() data = { 'a': 'A', 'b': 'B', 'long': 'word ' * 80, } mgr = extension.ExtensionManager( namespace='stevedore.example.formatter', invoke_on_load=True, invoke_args=(parsed_args.width,), ) def format_data(ext, data): return (ext.name, ext.obj.format(data)) results = mgr.map(format_data, data) for name, result in results: print('Formatter: {0}'.format(name)) for chunk in result: print(chunk, end='') print('')
ExtensionManager与 DriverManager 的创建略有不同,因为它不需要提前知道要加载哪个插件。 它加载了它找到的所有插件。
mgr = extension.ExtensionManager( namespace='stevedore.example.formatter', invoke_on_load=True, invoke_args=(parsed_args.width,), )
调用插件,使用 map()
方法,传递一个回调方法来调用每个插件。 format_data()
接收两个参数。
def format_data(ext, data): return (ext.name, ext.obj.format(data)) results = mgr.map(format_data, data)
被传进 format_data()
的 Extension
参数由 stevedore 定义的一个类。它包含插件的名字,由 pkg_resources
返回的 EntryPoint
和插件自己。当 invoke_on_load
是true时, Extension
将会有个对象属性, 该属性包含在调用插件时返回的值。 map() 返回回调函数返回的值序列。 在这个案例上, format_data()
返回一个元组,此元组包含一个插件名和一个 iterable。 在处理结果时,每个插件的名称都被打印出来,然后是格式化的数据。
for name, result in results: print('Formatter: {0}'.format(name)) for chunk in result: print(chunk, end='') print('')
加载插件的顺序是未定义的,并且依赖于在导入路径上找到的订单包以及读取元数据文件的方式。 如果使用了订单扩展名 ,尝试用 NamedExtensionManager
。
$ python -m stevedore.example.load_as_extension --width 30 Formatter: simple a = A b = B long = word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word Formatter: field : a : A : b : B : long : word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word Formatter: plain a = A b = B long = word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word
为什么不直接调用插件
使用一个单独的可调用的参数 map()
,而不是直接调用插件,这将导致应用程序代码和插件之间的分离。 这种分离的好处体现在应用程序代码设计和插件API设计中。
如果map()直接调用插件,每个插件都必须是可调用的。 这将意味着一个单独的命名空间,它实际上只是一个插件的方法。 通过使用一个单独的可调用的参数,插件API不需要匹配应用程序中任何特定的用例。 这使您可以创建一个更细粒度的API,使用更多的方法可以以不同的方式调用以实现不同的目标。