我有一个用Python编写的应用程序,它是由相当专业的观众(科学家)使用的。

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

I am looking for something extremely lightweight. Most scripts, or plugins, are not going to be developed and distributed by a third-party and installed, but are going to be something whipped up by a user in a few minutes to automate a repeating task, add support for a file format, etc. So plugins should have the absolute minimum boilerplate code, and require no 'installation' other than copying to a folder (so something like setuptools entry points, or the Zope plugin architecture seems like too much.)

是否已经有类似的系统存在,或者是否有项目实现了类似的方案,我应该看看想法/灵感?


当前回答

当我在Python中搜索插件框架时,我花了时间阅读这篇文章。我用过一些,但有缺点。这是我在2017年为你提供的一个无接口、松散耦合的插件管理系统:稍后再加载我。这里有关于如何使用它的教程。

其他回答

虽然这个问题很有趣,但我认为在没有更多细节的情况下很难回答。这是什么类型的应用程序?它有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)

另一个例子,灵感来自mercurial。在这里,扩展只向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 }

对于这两种方法,您可能需要对扩展使用通用的initialize和finalize。 您可以使用所有扩展都必须实现的公共接口(更适合第二种方法;Mercurial使用一个用于所有扩展的reposetup(ui, repo),或者使用一种带有钩子的钩子类型的方法。安装钩子。

但同样,如果你想要更多有用的答案,你必须缩小你的问题;)

setuptools有一个EntryPoint:

入口点是发行版“宣传”Python的一种简单方式 供其他发行版使用的对象(如函数或类)。 可扩展应用程序和框架可以搜索入口点 使用特定的名称或组,或者来自特定的分布 或者从sys。路径,然后检查或加载 可以随意发布对象。

如果你使用pip或virtualenv,这个包总是可用的。

您可以使用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"}'))

Marty Allchin的简单插件框架是我自己使用的基础。我真的建议看看它,我认为这是一个很好的开始,如果你想要一些简单和容易破解。你也可以在Django Snippets中找到它。

扩展@edomaur的回答,我建议看看simple_plugins(无耻的插件),这是一个简单的插件框架,灵感来自Marty Alchin的工作。

一个基于项目README的简短使用示例:

# 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>