创建插件

经过大量的试验和错误,我发现定义 API 的最简单方法是遵循以下步骤

  1. 使用 abc 模块 创建一个基础抽象类,以定义 API 插件所需的行为。开发者不必从基础类继承,但它提供了一种方便的方式来记录 API,并且使用抽象基类可以保证你的代码质量。

  2. 通过继承基础类并实现所需的方法来创建插件。

  3. 通过组合应用程序(或库)的名称和 API 的名称来为每个 API 定义一个唯一的命名空间。保持命名空间简洁。例如,“cliff.formatters”或“ceilometer.pollsters.compute”。

示例插件集

本教程中的示例程序将创建一个插件集,其中包含几个数据格式化器,就像命令行程序用来准备要打印到控制台的数据一样。每个格式化器将字典作为输入,字典的键为字符串,值为内置数据类型。它将返回一个迭代器作为输出,该迭代器根据所使用的特定格式化器的规则格式化数据结构。格式化器的构造函数允许调用者指定输出应具有的最大宽度。

插件基础类

上述步骤 1 是为需要由每个插件实现的 API 定义一个抽象基类。

# stevedore/example/base.py
# Copyright (C) 2020 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    https://apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import abc


class FormatterBase(metaclass=abc.ABCMeta):
    """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
# Copyright (C) 2020 Red Hat, Inc.
#
#  Licensed under the Apache License, Version 2.0 (the "License"); you may
#  not use this file except in compliance with the License. You may obtain
#  a copy of the License at
#
#       https://apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#  License for the specific language governing permissions and limitations
#  under the License.
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 入口点,必须使用 setuptools 打包你的应用程序或库。构建和打包过程会生成元数据,在安装后可以找到每个 python 分发提供的插件。

入口点必须声明属于特定的命名空间,因此我们需要在进一步操作之前选择一个。这些插件是 stevedore 示例中的格式化器,因此我将使用命名空间“stevedore.example.formatter”。现在可以提供打包说明中的所有必要信息

# stevedore/example/setup.py
# Copyright (C) 2020 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    https://apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from setuptools import find_packages
from setuptools import setup

setup(
    name='stevedore-examples',
    version='1.0',

    description='Demonstration package for stevedore',

    author='Doug Hellmann',
    author_email='doug@doughellmann.com',

    url='http://opendev.org/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.5',
                 '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,
)

重要的行在底部附近,entry_points 参数传递给 setup() 被设置。该值为一个字典,将插件的命名空间映射到它们的定义列表。列表中的每个项目都应该是一个字符串,格式为 name = module:importable,其中 name 是插件的用户可见名称,module 是模块的 Python 导入引用,importable 是模块内可以导入的内容的名称。

                 'Environment :: Console',
                 ],

    platforms=['Any'],

    scripts=[],

在这种情况下,注册了两个插件。上面定义的“simple”插件,以及一个“plain”插件,它只是“simple”插件的别名。

setuptools 元数据

在构建期间,setuptools 将入口点定义复制到软件包的“.egg-info”目录中的一个文件。例如,stevedore 的文件位于 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

importlib.metadata 使用所有已安装软件包的 entry_points.txt 文件来查找插件。你不应该修改这些文件,除非更改 setup.py 中的入口点列表。

在其他软件包中添加插件

使用入口点进行插件的一个吸引力在于,它们可以独立于应用程序分发。setuptools 用于查找插件的命名空间与 Python 源代码命名空间不同。通常使用以应用程序或库的名称为前缀的插件命名空间,以确保其唯一性,但该名称与插件代码应位于哪个 Python 包中无关。

例如,我们可以添加一个格式化器插件的替代实现,该插件生成一个 reStructuredText 字段列表

# stevedore/example2/fields.py
# Copyright (C) 2020 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    https://apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
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
# Copyright (C) 2020 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    https://apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from setuptools import find_packages
from setuptools import setup

setup(
    name='stevedore-examples2',
    version='1.0',

    description='Demonstration package for stevedore',

    author='Doug Hellmann',
    author_email='doug@doughellmann.com',

    url='http://opendev.org/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.5',
                 '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 包中。

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.

但是,插件注册为 stevedore.example.formatter 命名空间的一部分。

                 'Intended Audience :: Developers',
                 'Environment :: Console',
                 ],

    platforms=['Any'],

当扫描插件命名空间时,会检查当前 PYTHONPATH 上的所有软件包,并找到第二个软件包中的入口点,可以在不让应用程序知道插件实际安装位置的情况下加载该入口点。