用Python构建最小的插件架构

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

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

我正在找一些非常轻量级的。大多数脚本或插件都不是由第三方开发和分发并安装的,而是由用户在几分钟内完成的,以自动执行重复任务,增加对文件格式的支持等。所以插件应该有绝对最少的样板代码,并且除了复制到一个文件夹外不需要“安装”(所以像setuptools入口点或Zope插件架构似乎太多了)。

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

106768 次浏览

我的是,基本上,一个名为“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 }
对于这两种方法,你的扩展可能需要公共初始化完成。 您可以使用所有扩展都必须实现的公共接口(更适合第二种方法;Mercurial使用一个用于所有扩展的reposetup(ui, repo),或者使用一种带有钩子的钩子类型的方法。安装钩子。< / p >

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

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

操作包的命令可以通过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本身就是科学家所期望的“轻量级”前端。我很好奇为什么你不这么认为,我是认真的。


稍后添加:应用程序中用户预测插件被添加,他们的网站对一个简单的插件过程有我在几分钟内找到的最清晰的解释。试一试:

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

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

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

它可以作为播客(第二部分将解释monkey-patch),并伴随着一系列6篇博客

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

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

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

setuptools有一个入口点:

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

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

我来到这里寻找一个最小的插件架构,发现很多东西对我来说都是多余的。因此,我实现了超级简单的Python插件。要使用它,你创建一个或多个目录,并在每个目录中放置一个特殊的__init__.py文件。导入这些目录将导致所有其他Python文件作为子模块加载,它们的名称将被放置在__all__列表中。然后由您来验证/初始化/注册这些模块。在README文件中有一个例子。

当我搜索Python Decorators时,发现了一个简单但有用的代码片段。它可能不符合你的需求,但非常鼓舞人心。

< a href = " http://scipy-lectures.github。io/advanced/advanced_python/#a- Plugin - Registration - System " rel="nofollow noreferrer">Scipy advanced Python#Plugin Registration - System . exe

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的新实例时,我们得到了一个新类的实例,它是MyCoolClassExtensionMyCoolClass的子类,由于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

Record在这里是一个可伸缩对象。RecordDateTime是extension。

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

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

实际上setuptools工作于“插件目录”,如下例取自项目文档: http://peak.telecommunity.com/DevCenter/PkgResources#locating-plugins < / p >

使用示例:

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(无耻的插头),这是一个简单的插件框架,灵感来自马蒂·阿尔钦的作品

一个基于项目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插件系统。但后来我就想,既然已经有了一种遗传,它是自然的、灵活的,为什么不利用它呢?

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

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

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

所以当你有根基础,用元类,并有一棵从它继承的插件树时,你可以自动获得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

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

foundations通过编程方式将其绑定到应用程序(如上所示)或通过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 < / p >

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

我们的usecase:

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

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

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

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