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

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


我的是,基本上,一个名为“plugins”的目录,主应用程序可以轮询,然后使用imp.load_module来拾取文件,寻找一个已知的入口点,可能是模块级配置参数,然后从那里开始。我使用文件监控的东西来实现一定程度的动态,其中插件是活跃的,但这是一个很好的拥有。

当然,任何出现的要求都是“我不需要(大而复杂的东西)X;我只是想要一些轻量级的东西”会冒着一次重新实现一个已发现需求的风险。但这并不是说你不能从中获得乐趣。


看看这个对现有插件框架/库的概述,这是一个很好的起点。我很喜欢yapsy,但这取决于你的用例。


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

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


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?


我很喜欢Andre Roberge博士在Pycon 2009上关于不同插件架构的精彩讨论。他从一些非常简单的东西开始,很好地概述了实现插件的不同方法。

它可以作为一个播客(第二部分是对猴子补丁的解释),并附有一系列的六篇博客文章。

我建议你在做决定之前先快速听一遍。


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插件系统可以多么简单。

你可能也想看看imp模块,尽管你可以用__import__, os。Listdir和一些字符串操作。


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


setuptools有一个EntryPoint:

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

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


I arrived here looking for a minimal plugin architecture, and found a lot of things that all seemed like overkill to me. So, I've implemented Super Simple Python Plugins. To use it, you create one or more directories and drop a special __init__.py file in each one. Importing those directories will cause all other Python files to be loaded as submodules, and their name(s) will be placed in the __all__ list. Then it's up to you to validate/initialize/register those modules. There's an example in the README file.


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

作为插件系统的另一种方法,你可以检查Extend Me项目。

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

# 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的新实例时,我们得到了一个新类的实例,它是MyCoolClassExtension和MyCoolClass的子类,由于Python的多重继承,它具有两者的功能

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

ExtensibleType—通过子类化允许简单的扩展性 ExtensibleByHashType -类似ExtensibleType,但有能力 构建类的专门版本,允许全局扩展 基类和类的专门版本扩展的

这个库是在OpenERP代理项目中使用的,似乎工作得很好!

对于实际使用的例子,看看OpenERP代理的“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

这里记录的是可执行对象。RecordDateTime是扩展名。

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

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


实际上,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是一个更安全的选择,因为它可以加载插件而不会发生冲突或丢失需求。

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


扩展@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>

我花了很多时间试图找到适合我需求的小型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参与进来


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


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

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

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

这是文件。

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


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

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

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

在Pip软件包中: 在setup.py的帮助下,你可以添加插件的入口点,用插件管理器做一些事情(注册表,初始化,等等)。 https://setuptools.readthedocs.io/en/latest/setuptools.html#automatic-script-creation

在npm包中: 与pip类似,npm脚本中有钩子来处理安装。 https://docs.npmjs.com/misc/scripts

我们的usecase:

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

在你的情况下:也许你可以写一个插件,然后重复使用它来做一些事情。

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

只是分享一下我们是如何在我们的产品中实现的,希望能给大家一点启发。