我有一个用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.)

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


当前回答

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

其他回答

I am a retired biologist who dealt with digital micrograqphs and found himself having to write an image processing and analysis package (not technically a library) to run on an SGi machine. I wrote the code in C and used Tcl for the scripting language. The GUI, such as it was, was done using Tk. The commands that appeared in Tcl were of the form "extensionName commandName arg0 arg1 ... param0 param1 ...", that is, simple space-separated words and numbers. When Tcl saw the "extensionName" substring, control was passed to the C package. That in turn ran the command through a lexer/parser (done in lex/yacc) and then called C routines as necessary.

操作包的命令可以通过GUI中的窗口逐一运行,但批作业是通过编辑文本文件完成的,这些文本文件是有效的Tcl脚本;您可以选择执行您想要执行的文件级操作的模板,然后编辑一个副本,以包含实际的目录和文件名以及包命令。这招很管用。直到……

1)世界转向了pc, 2)脚本超过了500行,这时Tcl不稳定的组织能力开始成为真正的不便。时间流逝……

我退休了,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

I'd still like to understand your question better. I am unclear whether you 1) want scientists to be able to use your (Python) application quite simply in various ways or 2) want to allow the scientists to add new capabilities to your application. Choice #1 is the situation we faced with the images and that led us to use generic scripts which we modified to suit the need of the moment. Is it Choice #2 which leads you to the idea of plugins, or is it some aspect of your application that makes issuing commands to it impracticable?

当我搜索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**")

我花了很多时间试图找到适合我需求的小型Python插件系统。但后来我就想,既然已经有了一种遗传,它是自然的、灵活的,为什么不利用它呢?

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

但这可以用元类来解决,它可以跟踪基类的继承,也可以构建类,它继承自大多数特定的插件(下图中的“根扩展”)

所以我通过编写这样一个元类来解决这个问题:

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

所以当你有根基础,用元类,并有一棵从它继承的插件树时,你可以自动获得class,它通过子类化从最特定的插件继承:

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

代码库非常小(大约30行纯代码),并且继承时非常灵活。

如果你感兴趣,可以@ https://github.com/thodnev/pluginlib参与进来

你也可以看看“基础工作”。

其思想是围绕可重用组件(称为模式和插件)构建应用程序。插件是派生自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

还有更高级的模式来处理命令行接口、信号或共享对象。

foundation可以通过编程方式将插件绑定到应用程序中,如上面所示,也可以通过setuptools自动查找插件。包含插件的Python包必须使用一个特殊的入口点groundwork.plugin声明这些插件。

这是文件。

免责声明:我是《基础》的作者之一。

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

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