Twisted 插件系统

本指南的目的是描述编写可扩展 Twisted 应用程序的首选方法(因此,也描述了如何扩展以这种方式编写的应用程序)。这种可扩展性是通过定义一个或多个 API 和一种机制来实现的,该机制用于收集实现此 API 的代码插件以提供一些额外的功能。该系统的基础是 twisted.plugin 模块。

使用插件系统使应用程序可扩展比其他技术具有几个明显的优势

  • 它允许第三方开发者以松散耦合的方式轻松增强您的软件:只需要保持插件 API 的稳定性。

  • 它允许灵活地发现新插件。例如,插件可以在程序首次运行时加载和保存,或者在程序每次启动时重新发现,或者它们可以在运行时反复轮询(允许发现程序启动后安装的新插件)。

编写可扩展程序

利用 twisted.plugin 是一个两步过程

  1. 定义插件需要实现的接口。这是使用 zope.interface 包完成的,与为任何其他目的定义接口的方式相同。

    定义接口的约定是在名为 ProjectName/projectname/iprojectname.py 的文件中进行。本文档的其余部分将遵循该约定:假设以下接口定义位于 Matsim/matsim/imatsim.py 中,这是一个假设的材料模拟包的接口定义模块。

  2. 在您的程序中的一个或多个位置,调用 twisted.plugin.getPlugins() 并迭代其结果。

作为第一步的示例,请考虑以下物理建模系统的接口定义。

from zope.interface import Interface, Attribute

class IMaterial(Interface):
    """
    An object with specific physical properties
    """
    def yieldStress(temperature):
        """
        Returns the pressure this material can support without
        fracturing at the given temperature.

        @type temperature: C{float}
        @param temperature: Kelvins

        @rtype: C{float}
        @return: Pascals
        """

    dielectricConstant = Attribute("""
        @type dielectricConstant: C{complex}
        @ivar dielectricConstant: The relative permittivity, with the
        real part giving reflective surface properties and the
        imaginary part giving the radio absorption coefficient.
        """)

在另一个模块中,我们可能有一个对提供 IMaterial 接口的对象进行操作的函数

def displayMaterial(m):
    print('A material with yield stress %s at 500 K' % (m.yieldStress(500),))
    print('Also a dielectric constant of %s.' % (m.dielectricConstant,))

最后一段所需的代码是收集 IMaterial 提供者并将它们传递给 displayMaterial 函数的代码。

from twisted.plugin import getPlugins
from matsim import imatsim

def displayAllKnownMaterials():
    for material in getPlugins(imatsim.IMaterial):
        displayMaterial(material)

第三方开发者现在可以通过为 IMaterial 接口实现一个或多个插件来为该建模系统贡献不同的材料。

扩展现有程序

上面的代码演示了如何使用 Twisted 的插件系统编写可扩展程序。但是,我们如何为它编写插件呢?本质上,我们创建提供所需接口的对象,然后在特定位置使它们可用。请考虑以下示例。

from zope.interface import implementer
from twisted.plugin import IPlugin
from matsim import imatsim

@implementer(IPlugin, imatsim.IMaterial)
class SimpleMaterial(object):
    def __init__(self, yieldStressFactor, dielectricConstant):
        self._yieldStressFactor = yieldStressFactor
        self.dielectricConstant = dielectricConstant

    def yieldStress(self, temperature):
        return self._yieldStressFactor * temperature

steelPlate = SimpleMaterial(2.06842719e11, 2.7 + 0.2j)
brassPlate = SimpleMaterial(1.03421359e11, 1.4 + 0.5j)

steelPlatebrassPlate 现在都提供了 IPluginIMaterial 。剩下的就是使该模块在适当的位置可用。为此,有两个选项。第一个主要在开发期间有用:如果添加到 sys.path 的目录(通常通过将其添加到 PYTHONPATH 环境变量中)包含一个名为 twisted/plugins/目录,则该目录中的每个 .py 文件都将作为插件源加载。此目录不能是 Python 包:包含 __init__.py 将导致跳过该目录,并且不会从该目录加载任何插件。其次,Twisted 的 twisted.plugins 包的已安装版本的每个模块也将作为插件源加载。

一旦以这两种方式之一安装了此插件,就可以运行 displayAllKnownMaterials,我们将看到两对输出:一对用于钢板,一对用于黄铜板。

备用插件包

getPlugins 接受一个上面没有提到的额外参数。如果传入,则第二个参数应该是要代替 twisted.plugins 用作插件元包的模块或包。如果您正在为 Twisted 接口编写插件,则您永远不需要传递此参数。但是,如果您已经开发了自己的接口,您可能希望强制其插件安装在您自己的插件包中,而不是 Twisted 的插件包中。

您可能希望支持 yourproject/plugins/ 目录以方便开发。为此,您应该使 yourproject/plugins/__init__.py 至少包含以下几行。

from twisted.plugin import pluginPackagePaths
__path__.extend(pluginPackagePaths(__name__))
__all__ = []

这里的关键行为是接口本质上与特定的插件包配对。如果插件安装在与依赖于其提供的接口的代码不同的包中,则应用程序加载它们时将找不到这些插件。

插件缓存

在使用 Twisted 插件系统时,您可能会注意到在不同位置出现 dropin.cache 文件。这些文件用于缓存有关包含它们的目录中存在哪些插件的信息。有时,此缓存信息可能会过时。Twisted 使用插件系统中涉及的各种文件的 mtime 来确定何时此缓存可能已失效。Twisted 会尝试在每次尝试使用缓存但发现它已过时时重新写入缓存。

对于站点范围的安装,以普通用户身份运行的应用程序可能无法(实际上也不应该)重写缓存文件。虽然这些应用程序仍然可以运行并找到正确的插件信息,但它们的运行速度可能比缓存更新时的速度慢,并且如果某些插件已被删除但缓存仍然引用它们,它们也可能会报告异常。出于这些原因,在安装或删除提供 Twisted 插件的软件时,站点管理员应确保重新生成缓存。此类软件的良好行为的包管理器应承担此任务,因为它是可以轻松自动化的。重新生成缓存的规范方法是运行以下 Python 代码

from twisted.plugin import IPlugin, getPlugins
list(getPlugins(IPlugin))

如前所述,如果插件已被删除,则在此处引发异常是正常的。

进一步阅读