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

时间:2009-05-31 13:46:42

标签: python architecture plugins

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

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

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

是否有任何这样的系统,或者任何实施类似方案的项目,我应该考虑一下这些方案的想法/灵感?

18 个答案:

答案 0 :(得分:144)

我的基本上是一个名为“plugins”的目录,主应用程序可以轮询该目录,然后使用imp.load_module来获取文件,查找可能带有模块级配置参数的众所周知的入口点,以及从那里开始我使用文件监控的东西来获得一些活跃的动态,但这是一个很好的选择。

当然,任何要求都说:“我不需要[大而复杂的东西] X;我只想要一些轻量级的东西”冒着重复实施X一次发现的要求的风险。但这并不是说你无论如何也无法做到这一点:)

答案 1 :(得分:48)

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和一些字符串操作做很多事情。

答案 2 :(得分:30)

看看at this overview over existing plugin frameworks / libraries,这是一个很好的起点。我非常喜欢yapsy,但这取决于你的用例。

答案 3 :(得分:25)

虽然这个问题非常有趣,但我认为如果没有更多细节,就很难回答。这是什么类型的应用程序?它有GUI吗?它是一个命令行工具吗?一组脚本?具有唯一入口点等的程序......

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

你有什么方法可以添加插件?

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

在纯粹的代码/设计实践中,您必须清楚地确定您希望用户扩展的行为/特定操作。确定将始终被覆盖的公共入口点/一组功能,并确定这些操作中的组。完成后,应该可以轻松扩展您的应用程序,

使用 hooks 的例子,灵感来自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 }

对于这两种方法,您可能需要为扩展程序进行常见的初始化 finalize 。 您可以使用所有扩展必须实现的通用接口(更适合第二种方法; mercurial使用为所有扩展调用的reposetup(ui,repo)),或者使用钩子方法,使用hooks.setup hook。

但是,如果你想要更多有用的答案,你将不得不缩小你的问题;)

答案 4 :(得分:11)

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

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

1)当Tcl的组织能力开始变得非常不便时,世界转向PC和2)脚本的时间长于500行。时间过去了......

我退休了,Python被发明了,它看起来像是Tcl的完美继承者。现在,我从来没有做过端口,因为我从来没有遇到过在PC上编译(相当大的)C程序,用C包扩展Python,用Python / Gt?/ Tk?/?做GUI的挑战? ?。但是,拥有可编辑模板脚本的旧想法似乎仍然可行。此外,以本机Python形式输入包命令不应该是太大的负担,例如:

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

一些额外的点,parens和逗号,但那些不是showstoppers。

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

这种漫无边际的观点是,在我看来,Python本身就是科学家可以使用的理想的“轻量级”前端。我很想知道为什么你认为它不是,我的意思是认真的。


稍后添加:应用程序 gedit 预计会添加插件,他们的网站有一个简单的插件程序的最清楚的解释,我在几分钟的环顾四周找到了。试试:

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

我仍然希望更好地理解你的问题。我不清楚您是否1)希望科学家能够以各种方式简单地使用您的(Python)应用程序,或2)希望科学家能够为您的应用程序添加新功能。选择#1是我们面对图像的情况,这导致我们使用我们修改的通用脚本以满足当下的需要。是选择#2引导您了解插件的概念,还是应用程序的某些方面使得发布命令变得不可行?

答案 5 :(得分:11)

Marty Allchin's simple plugin framework是我根据自己的需要使用的基础。我真的建议你去看看它,如果你想要一些简单易用的东西,我认为这是一个很好的开始。你也可以找到as a Django Snippets

答案 6 :(得分:10)

当我搜索Python装饰器时,找到了一个简单但有用的代码片段。它可能不符合您的需求,但非常鼓舞人心。

Scipy Advanced Python#Plugin Registration System

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**")

答案 7 :(得分:7)

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

podcast为准(第二部分是对猴子修补的解释),附有一系列six blog entries

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

答案 8 :(得分:4)

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

答案 9 :(得分: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 是一个更安全的选择,因为它可以加载插件而不会发生冲突或缺少要求。

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

答案 10 :(得分:3)

setuptools has an EntryPoint

  

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

如果您使用pip或virtualenv,AFAIK此套餐始终可用。

答案 11 :(得分: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的新实例时,我们获得了作为MyCoolClassExtension的子类的新类的实例感谢Python的multiple inheritance

MyCoolClass具有这两种功能

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

  • ExtensibleType - 通过子类化

  • 允许简单的可扩展性
  • ExtensibleByHashType - 类似于ExtensibleType,但具有异能  构建专用版本的类,允许全局扩展  基类和类的专用版本的扩展

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

有关实际使用示例,请查看OpenERP Proxy 'field_datetime' extension

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个对象,从而具有其所有功能。

这个库的主要优点是,操作可扩展对象的代码不需要知道扩展和扩展可能会改变可扩展对象中的所有内容。

答案 12 :(得分:2)

扩展@ edomaur的答案我建议你看一下simple_plugins(无耻插件),这是一个受work of 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>

答案 13 :(得分:2)

我花时间阅读这个帖子,而我偶尔会在Python中搜索插件框架。我跟他们used some but there were shortcomings。以下是我在2017年提出的审查,一个免费的,松散耦合的插件管理系统:Load me later。以下是关于如何使用它的tutorials

答案 14 :(得分:1)

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

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

但这可以通过元类来解决,元类跟踪基类的继承,并且可能构建类,它继承自大多数特定的插件(&#39; Root扩展&#39;在下图中)

enter image description here

所以我通过编写这样一个元类来提供解决方案:

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

答案 15 :(得分:1)

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

答案 16 :(得分: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进行声明。

这里是docs

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

答案 17 :(得分:0)

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

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

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

在点子包装中: 借助setup.py,您可以添加插件的入口点,以使用插件管理器执行某些操作(注册表,初始化等)。 https://setuptools.readthedocs.io/en/latest/setuptools.html#automatic-script-creation

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

我们的用例:

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

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

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

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