在Python中构建最小的插件架构


190

我有一个用Python编写的应用程序,供相当技术的读者(科学家)使用。

我正在寻找一种使用户可以扩展应用程序的好方法,即脚本/插件体系结构。

我正在寻找轻巧的东西。大多数脚本或插件都不会由第三方开发和分发并安装,而是会在几分钟内被用户搅动,以自动执行重复任务,添加对文件格式的支持,等等。因此,插件应该具有绝对的最小模板代码,并且除了复制到文件夹外,不需要“安装”(因此,诸如setuptools入口点之类的东西,或者Zope插件架构似乎太多了。)

是否已经有类似这样的系统,或者是否有实施类似方案的项目,我应该寻求灵感/启发?

Answers:


150

基本上,我的目录是一个名为“插件”的目录,主应用程序可以对其进行轮询,然后使用imp.load_module拾取文件,查找可能带有模块级配置参数的知名入口点,然后从那里进入。我使用文件监视功能进行一定程度的动态处理,使插件处于活动状态,但这很不错。

当然,任何伴随着“我不需要[大,复杂的东西] X;我只想要轻量级的东西”的需求都会冒着重新实现X一次发现的需求的风险。但这并不是说您无法获得任何乐趣:)


26
非常感谢!我根据您的帖子写了一个小教程:lkubuntu.wordpress.com/2012/10/02/writing-a-python-plugin-api
MiJyn 2012年

9
imp不建议使用此模块,而建议importlib从python 3.4
b0fh

1
在许多用例中,您可以使用importlib.import_module代替imp.load_module
克里斯·阿恩特

58

module_example.py

def plugin_main(*args, **kwargs):
    print args, kwargs

loader.py

def load_plugin(name):
    mod = __import__("module_%s" % name)
    return mod

def call_plugin(name, *args, **kwargs):
    plugin = load_plugin(name)
    plugin.plugin_main(*args, **kwargs)

call_plugin("example", 1234)

它肯定是“最小的”,它绝对没有错误检查,可能有无数的安全问题,不是很灵活-但它应该向您展示Python中的插件系统可以多么简单。

你可能要考虑的小鬼模块也一样,虽然你可以做很多事只是__import__os.listdir和一些字符串操作。


4
我认为您可能想要更改def call_plugin(name, *args)def call_plugin(name, *args, **kwargs),然后更改plugin.plugin_main(*args)plugin.plugin_main(*args, **kwargs)
Ron Klein

12
在Python 3,imp在赞成不赞成importlib
亚当巴克斯特


25

尽管这个问题确实很有趣,但我认为如果没有更多细节,很难回答。这是什么样的应用程序?它有GUI吗?它是命令行工具吗?一组脚本?具有唯一入口点的程序,等等。

鉴于我所掌握的信息很少,我将以非常通用的方式回答。

您必须添加插件是什么意思?

  • 您可能必须添加一个配置文件,该文件将列出要加载的路径/目录。
  • 另一种方式是说“将加载该插件/目录中的所有文件”,但这不方便要求用户在文件之间移动。
  • 最后一个中间选项是要求所有插件都在同一插件/文件夹中,然后使用配置文件中的相对路径激活/停用它们。

在纯代码/设计实践中,您必须明确确定要用户扩展的行为/特定操作。确定将始终被覆盖的公共入口点/一组功能,并确定这些操作中的组。完成此操作后,扩展应用程序应该很容易,

钩子的示例,该示例受MediaWiki(PHP,但语言真的很重要吗?)的启发:

import hooks

# In your core code, on key points, you allow user to run actions:
def compute(...):
    try:
        hooks.runHook(hooks.registered.beforeCompute)
    except hooks.hookException:
        print('Error while executing plugin')

    # [compute main code] ...

    try:
        hooks.runHook(hooks.registered.afterCompute)
    except hooks.hookException:
        print('Error while executing plugin')

# The idea is to insert possibilities for users to extend the behavior 
# where it matters.
# If you need to, pass context parameters to runHook. Remember that
# runHook can be defined as a runHook(*args, **kwargs) function, not
# requiring you to define a common interface for *all* hooks. Quite flexible :)

# --------------------

# And in the plugin code:
# [...] plugin magic
def doStuff():
    # ....
# and register the functionalities in hooks

# doStuff will be called at the end of each core.compute() call
hooks.registered.afterCompute.append(doStuff)

另一个例子,灵感来自水银。在这里,扩展仅将命令添加到hg命令行可执行文件,从而扩展了行为。

def doStuff(ui, repo, *args, **kwargs):
    # when called, a extension function always receives:
    # * an ui object (user interface, prints, warnings, etc)
    # * a repository object (main object from which most operations are doable)
    # * command-line arguments that were not used by the core program

    doMoreMagicStuff()
    obj = maybeCreateSomeObjects()

# each extension defines a commands dictionary in the main extension file
commands = { 'newcommand': doStuff }

对于这两种方法,您可能需要对扩展进行通用的初始化终结处理。您可以使用所有扩展程序都必须实现的通用接口(更适合第二种方法; Mercurial使用所有扩展程序都需要使用的reposetup(ui,repo)),也可以使用带有某种挂钩的方法hooks.setup钩子。

但是同样,如果您想要更有用的答案,则必须缩小问题的范围;)



11

我是一位退休的生物学家,负责处理数字微图,发现自己必须编写图像处理和分析程序包(从技术上来说不是库),才能在SGi机器上运行。我用C语言编写了代码,并使用Tcl作为脚本语言。这样的GUI就是使用Tk完成的。Tcl中出现的命令的格式为“ extensionName commandName arg0 arg1 ... param0 param1 ...”,即,用空格分隔的简单单词和数字。当Tcl看到“ extensionName”子字符串时,控制权传递给C包。依次通过lexer / parser(在lex / yacc中完成)运行命令,然后根据需要调用C例程。

可以通过GUI中的窗口逐个运行用于操作软件包的命令,但是批处理作业是通过编辑文本文件完成的,这些文本文件是有效的Tcl脚本;您将选择执行所需的文件级操作的模板,然后编辑一个副本以包含实际的目录和文件名以及package命令。它就像一种魅力。直到 ...

1)世界转向PC,2)当Tcl顽强的组织能力开始成为真正的不便时,脚本的长度超过了500行。时间飞逝 ...

我退休了,Python被发明了,它看起来像是Tcl的完美继承者。现在,我从未做过移植工作,因为我从未面临过在PC上编译(相当大的)C程序,用C包扩展Python以及在Python / Gt?/ Tk?/?中进行GUI的挑战。 ?但是,拥有可编辑模板脚本的旧想法似乎仍然可行。同样,以本机Python形式输入包命令也不会带来太大负担,例如:

packageName.command(arg0,arg1,...,param0,param1,...)

一些额外的圆点,括号和逗号,但这些不是最常用的。

我记得曾经看到有人在Python中完成了lex和yacc的版本(尝试:http : //www.dabeaz.com/ply/),所以如果仍然需要它们,那就可以了。

这种杂乱无章的要点是,在我看来,Python本身是科学家可以使用的理想“轻量级”前端。我很想知道您为什么不这样认为,我是认真的意思。


稍后添加:应用程序gedit预计会添加插件,并且它们的站点中有关于我在几分钟内发现的简单插件过程的最清晰的解释。尝试:

https://wiki.gnome.org/Apps/Gedit/PythonPluginHowToOld

我仍然想更好地了解您的问题。我不清楚您是1)希望科学家能够以各种方式简单地使用您的(Python)应用程序,还是2)是否允许科学家为您的应用程序添加新功能。选择#1是我们面对图像的情况,这导致我们使用通用脚本,我们对该脚本进行了修改以满足当前需求。是选择2使您想到插件的想法,还是应用程序的某些方面使向其发出命令不可行?


2
修复链接腐烂:现在是Gedit
ohhorob 2013年

1
这是一篇美丽的文章,因为它清楚而简洁地显示了我们当今的生物学家多么幸运。对于他/她来说,python是用于向模块开发人员提供一些抽象的模块化脚本语言,这样他们就无需解析主要的C代码。但是,如今,很少有生物学家会学习C,而是使用Python来完成所有工作。编写模块时,我们如何抽象出我们的主要python程序的复杂性?从现在起的10年内,也许程序将用Emoji编写,而模块将只是包含一系列声音的音频文件。也许那就可以了。
JJ

10

当我搜索Python Decorators时,发现了一个简单但有用的代码段。它可能不适合您的需求,但很有启发性。

Scipy高级Python#插件注册系统

class TextProcessor(object):
    PLUGINS = []

    def process(self, text, plugins=()):
        if plugins is ():
            for plugin in self.PLUGINS:
                text = plugin().process(text)
        else:
            for plugin in plugins:
                text = plugin().process(text)
        return text

    @classmethod
    def plugin(cls, plugin):
        cls.PLUGINS.append(plugin)
        return plugin


@TextProcessor.plugin
class CleanMarkdownBolds(object):
    def process(self, text):
        return text.replace('**', '')

用法:

processor = TextProcessor()
processed = processor.process(text="**foo bar**", plugins=(CleanMarkdownBolds, ))
processed = processor.process(text="**foo bar**")

1
注意:在此示例中,WordProcessor.plugin不返回任何内容(None),因此CleanMdashesExtension稍后导入该类仅是import None。如果插件类本身有用,则使用.pluginclass方法return plugin
jkmacc

@jkmacc你是对的。您发表评论后13天,我已修改了该代码段。谢谢。
guneysus

7

我很享受Pycon 2009上Andre Roberge博士就不同的插件体系结构进行的精彩讨论。他从一个非常简单的东西开始,很好地概述了实现插件的不同方法。

它可以作为播客(后面是对猴子补丁的解释的第二部分),附带一系列六个博客条目

我建议您在做出决定之前先快速听一下。


4

我来到这里寻找的是最小的插件体系结构,发现很多东西对我来说似乎太过分了。因此,我已经实现了Super Simple Python Plugins。要使用它,您需要创建一个或多个目录,并__init__.py在每个目录中放置一个特殊文件。导入这些目录将导致所有其他Python文件作为子模块加载,并且它们的名称将放置在__all__列表中。然后由您来验证/初始化/注册这些模块。自述文件中有一个示例。


4

实际上,setuptools可与“插件目录”一起使用,如以下示例摘自项目文档:http : //peak.telecommunity.com/DevCenter/PkgResources#locating-plugins

用法示例:

plugin_dirs = ['foo/plugins'] + sys.path
env = Environment(plugin_dirs)
distributions, errors = working_set.find_plugins(env)
map(working_set.add, distributions)  # add plugins+libs to sys.path
print("Couldn't load plugins due to: %s" % errors)

从长远来看,setuptools是一个更安全的选择,因为它可以加载插件而不会发生冲突或缺少需求。

另一个好处是,插件本身可以使用相同的机制进行扩展,而原始应用程序不必关心它。


3

作为插件系统的另一种方法,您可以检查Extend Me project

例如,让我们定义简单的类及其扩展

# Define base class for extensions (mount point)
class MyCoolClass(Extensible):
    my_attr_1 = 25
    def my_method1(self, arg1):
        print('Hello, %s' % arg1)

# Define extension, which implements some aditional logic
# or modifies existing logic of base class (MyCoolClass)
# Also any extension class maby be placed in any module You like,
# It just needs to be imported at start of app
class MyCoolClassExtension1(MyCoolClass):
    def my_method1(self, arg1):
        super(MyCoolClassExtension1, self).my_method1(arg1.upper())

    def my_method2(self, arg1):
        print("Good by, %s" % arg1)

并尝试使用它:

>>> my_cool_obj = MyCoolClass()
>>> print(my_cool_obj.my_attr_1)
25
>>> my_cool_obj.my_method1('World')
Hello, WORLD
>>> my_cool_obj.my_method2('World')
Good by, World

并显示隐藏在幕后的内容:

>>> my_cool_obj.__class__.__bases__
[MyCoolClassExtension1, MyCoolClass]

extend_me库通过元类操纵类的创建过程,因此在上面的示例中,当MyCoolClass我们创建新实例时,由于Python的多重继承,我们得到了新类的实例,该实例是这两者的子类MyCoolClassExtension并且MyCoolClass具有两者的功能

为了更好地控制类的创建,此库中定义了一些元类:

  • ExtensibleType -通过子类实现简单的可扩展性

  • ExtensibleByHashType -与ExtensibleType相似,但具有构建类的专用版本的能力,从而允许对基类进行全局扩展和对类的专用版本进行扩展

这个库在OpenERP Proxy Project中使用,似乎工作得很好!

有关用法的真实示例,请查看OpenERP Proxy'field_datetime'扩展名

from ..orm.record import Record
import datetime

class RecordDateTime(Record):
    """ Provides auto conversion of datetime fields from
        string got from server to comparable datetime objects
    """

    def _get_field(self, ftype, name):
        res = super(RecordDateTime, self)._get_field(ftype, name)
        if res and ftype == 'date':
            return datetime.datetime.strptime(res, '%Y-%m-%d').date()
        elif res and ftype == 'datetime':
            return datetime.datetime.strptime(res, '%Y-%m-%d %H:%M:%S')
        return res

Record这是易燃物品。RecordDateTime是扩展名。

要启用扩展,只需导入包含扩展类的模块,以及(在上述情况下)导入的所有Record对象,在其后创建的所有对象将在基类中具有扩展类,从而具有其所有功能。

该库的主要优点是,操作可扩展对象的代码不需要了解扩展,扩展可以更改可扩展对象中的所有内容。


我认为您的意思是从子类实例化,即my_cool_obj = MyCoolClassExtension1()而不是my_cool_obj = MyCoolClass()
pylang's

不,可扩展类具有重写的__new__方法,因此它会自动查找所有子类,并构建新类(即所有子类),并返回此创建的类的新实例。因此,原始应用程序不需要了解所有扩展名。在构建库时,此方法很有用,以允许最终用户轻松修改或扩展其行为。在上面的示例中,MyCoolClass可以在库中定义并由其使用,而MyCoolClassExtension可以由最终用户定义。
FireMage '16

加入一个例子来回答
FireMage

3

setuptools具有一个EntryPoint

入口点是发行版“公告” Python对象(例如函数或类)供其他发行版使用的简单方法。可扩展的应用程序和框架可以从特定发行版或sys.path上的所有活动发行版中搜索具有特定名称或组的入口点,然后随意检查或加载广告对象。

如果您使用pip或virtualenv,此软件包始终可用。


2

扩展@edomaur的答案,我建议您看一下simple_plugins(无耻插件),这是一个受Marty Alchin启发的简单插件框架。

一个基于项目自述文件的简短用法示例:

# All plugin info
>>> BaseHttpResponse.plugins.keys()
['valid_ids', 'instances_sorted_by_id', 'id_to_class', 'instances',
 'classes', 'class_to_id', 'id_to_instance']

# Plugin info can be accessed using either dict...
>>> BaseHttpResponse.plugins['valid_ids']
set([304, 400, 404, 200, 301])

# ... or object notation
>>> BaseHttpResponse.plugins.valid_ids
set([304, 400, 404, 200, 301])

>>> BaseHttpResponse.plugins.classes
set([<class '__main__.NotFound'>, <class '__main__.OK'>,
     <class '__main__.NotModified'>, <class '__main__.BadRequest'>,
     <class '__main__.MovedPermanently'>])

>>> BaseHttpResponse.plugins.id_to_class[200]
<class '__main__.OK'>

>>> BaseHttpResponse.plugins.id_to_instance[200]
<OK: 200>

>>> BaseHttpResponse.plugins.instances_sorted_by_id
[<OK: 200>, <MovedPermanently: 301>, <NotModified: 304>, <BadRequest: 400>, <NotFound: 404>]

# Coerce the passed value into the right instance
>>> BaseHttpResponse.coerce(200)
<OK: 200>


2

您可以使用pluginlib

插件易于创建,可以从其他软件包,文件路径或入口点加载。

创建一个插件父类,定义任何必需的方法:

import pluginlib

@pluginlib.Parent('parser')
class Parser(object):

    @pluginlib.abstractmethod
    def parse(self, string):
        pass

通过继承父类来创建插件:

import json

class JSON(Parser):
    _alias_ = 'json'

    def parse(self, string):
        return json.loads(string)

加载插件:

loader = pluginlib.PluginLoader(modules=['sample_plugins'])
plugins = loader.plugins
parser = plugins.parser.json()
print(parser.parse('{"json": "test"}'))

1
谢谢你的例子。我一直在努力解决1个问题。由于您提到可以从不同的软件包加载插件,所以您可能已经想到了。我想知道父类应该放在哪里。通常,建议将其放在应用程序的程序包中(大概是一个单独的源代码存储库),但是我们如何在插件的代码库中继承它呢?我们是否为此导入整个应用程序?还是有必要在第3个程序包(这将是第3个代码存储库)中包含诸如Parser类或类似抽象之类的接口代码?
JAponte

1
父类应与应用程序驻留在相同的代码库中,但可能位于其自己的模块中。因此,对于名为的包foo,您可能会有一个名为的模块foo.parents,您可以在其中定义父类。然后,您的插件将导入foo.parents。对于大多数用例来说,这很好用。因为'foo'本身也被导入,所以为了避免循环导入的可能性,许多项目将模块的根目录留空,并使用__main__.py文件或入口点启动应用程序。
aviso

1

我花了很多时间试图找到适合我的Python小型插件系统。但是然后我只是想,如果已经有了一个自然而灵活的继承,为什么不使用它。

对插件使用继承的唯一问题是您不知道最具体的(继承树上最低的)插件类是什么。

但这可以通过元类来解决,它可以跟踪基类的继承,并可以构建类,该类继承自大多数特定的插件(下图为“ Root extended”)

在此处输入图片说明

因此,我通过编码这样的元类提供了一个解决方案:

class PluginBaseMeta(type):
    def __new__(mcls, name, bases, namespace):
        cls = super(PluginBaseMeta, mcls).__new__(mcls, name, bases, namespace)
        if not hasattr(cls, '__pluginextensions__'):  # parent class
            cls.__pluginextensions__ = {cls}  # set reflects lowest plugins
            cls.__pluginroot__ = cls
            cls.__pluginiscachevalid__ = False
        else:  # subclass
            assert not set(namespace) & {'__pluginextensions__',
                                         '__pluginroot__'}     # only in parent
            exts = cls.__pluginextensions__
            exts.difference_update(set(bases))  # remove parents
            exts.add(cls)  # and add current
            cls.__pluginroot__.__pluginiscachevalid__ = False
        return cls

    @property
    def PluginExtended(cls):
        # After PluginExtended creation we'll have only 1 item in set
        # so this is used for caching, mainly not to create same PluginExtended
        if cls.__pluginroot__.__pluginiscachevalid__:
            return next(iter(cls.__pluginextensions__))  # only 1 item in set
        else:
            name = cls.__pluginroot__.__name__ + 'PluginExtended'
            extended = type(name, tuple(cls.__pluginextensions__), {})
            cls.__pluginroot__.__pluginiscachevalid__ = True
return extended

因此,当您具有由元类构成的Root基础并且具有从其继承的插件树时,您可以自动获取类,该类可以通过子类化而从最特定的插件继承:

class RootExtended(RootBase.PluginExtended):
    ... your code here ...

代码库很小(约30行纯代码),并且在继承允许的范围内具有足够的灵活性。

如果您有兴趣,请参与@ https://github.com/thodnev/pluginlib


1

您也可以看看Groundwork

这个想法是围绕可重用​​的组件(称为模式和插件)构建应用程序。插件是从派生的类GwBasePattern。这是一个基本示例:

from groundwork import App
from groundwork.patterns import GwBasePattern

class MyPlugin(GwBasePattern):
    def __init__(self, app, **kwargs):
        self.name = "My Plugin"
        super().__init__(app, **kwargs)

    def activate(self): 
        pass

    def deactivate(self):
        pass

my_app = App(plugins=[MyPlugin])       # register plugin
my_app.plugins.activate(["My Plugin"]) # activate it

还有更高级的模式可以处理例如命令行界面,信令或共享对象。

Groundwork可以通过以编程方式将其绑定到应用程序(如上所示)或通过来自动找到其插件setuptools。包含插件的Python包必须使用特殊的入口点来声明它们groundwork.plugin

这是文档

免责声明:我是Groundwork的作者之一。


0

在当前的医疗保健产品中,我们具有使用接口类实现的插件体系结构。我们的技术堆栈是在Python之上的Django(用于API)和Nuxtjs(在nodejs的在前端)。

我们为我们的产品编写了一个插件管理器应用程序,该应用程序基本上是pip和npm软件包,遵循Django和Nuxtjs。

对于新的插件开发(pip和npm),我们将插件管理器作为依赖项。

在Pip包中:在setup.py的帮助下,您可以添加插件的入口点,以使用插件管理器(注册表,启动等)进行某些操作 https://setuptools.readthedocs.io/zh/latest/setuptools .html#automatic-script-creation

在npm软件包中:与pip相似,npm脚本中有钩子可以处理安装。 https://docs.npmjs.com/misc/scripts

我们的用例:

插件开发团队现已与核心开发团队分离。插件开发的范围是用于与在产品的任何类别中定义的第三方应用程序集成。插件界面的分类例如:-传真,电话,电子邮件...等插件管理器可以增强到新的类别。

在您的情况下:也许您可以编写一个插件,然后将其重复用于做事。

如果插件开发人员需要使用重用核心对象,则可以通过在插件管理器中进行一定程度的抽象来使用该对象,以便任何插件都可以继承这些方法。

只是分享我们在产品中的实现方式,希望对您有所帮助。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.