2009-05-31 151 views
158

我有一个用Python编写的应用程序,由相当技术的受众(科学家)使用。在Python中构建最小插件体系结构

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

我在寻找的东西极其轻便。大多数脚本或插件不会由第三方开发和分发,并且会在几分钟内被用户激活,以自动执行重复任务,添加对文件格式的支持,等等。所以插件应该有绝对最小的样板代码,除了复制到一个文件夹以外不需要“安装”(所以像setuptools入口点,或者Zope插件体系结构似乎太多了。)

有没有像这样的系统已经存在了,或者任何实施类似计划的项目,我应该看看想法/灵感?

回答

137

我的基本上是一个名为“插件”的目录,主应用程序可以轮询,然后使用imp.load_module来获取文件,可能会使用模块级配置参数寻找一个众所周知的入口点,然后从那里。我使用文件监控的东西来获得一定的活力,插件是活跃的,但这是一件很不错的事情。

当然,沿着说:“我不需要[大,复杂的事情] X;我只是想要一些轻量级”谈到任何要求运行的重新实施X一个发现需求在一个时间的风险。但是,这并不是说你不能有一些有趣反正做:)

+54

+1第二段。如此真实。 :-) – 2012-04-03 06:29:11

+22

非常感谢!我写了一个基于你的帖子的小教程:http://lkubuntu.wordpress.com/2012/10/02/writing-a-python-plugin-api/ – MiJyn 2012-10-03 18:06:10

+2

'imp`模块被弃用,赞成'importlib `从python 3.4 – b0fh 2017-08-30 08:56:57

22

虽然这个问题真的很有趣,但我认为这是相当难回答,没有更多的细节。这是什么样的应用程序?它有一个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。在这里,扩展只会将命令添加到命令行可执行文件中,从而扩展行为。

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)),或者使用钩子类方法一个hooks.setup钩子。

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

11

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

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

1)世界转向个人电脑和2)剧本的时间超过500行左右,当Tcl的iffy组织能力开始变得非常不便时。时间已过...

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

packageName.command(为arg0,ARG1,...,参数0,参数1,...)

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

我记得有人已经在Python中完成了lex和yacc的版本(尝试:http://www.dabeaz.com/ply/),所以如果仍然需要这些版本的话,他们就在身边。

这个散乱的点在于,我认为Python本身就是科学家们所期望的“轻量级”前端。我很想知道你为什么认为它不是,我的意思是认真的。


后来补充:应用程序的gedit预计添加插件和他们的网站拥有约我在环视了几分钟发现了一个简单的插件程序的最清晰的解释。尝试:

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

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

+2

+1开始,链接到http://live.gnome.org/Gedit/PythonPluginHowTo,很好的阅读。 – synthesizerpatel 2012-01-20 10:24:45

+2

链接修复:Gedit插件现在是 - https://wiki.gnome。org/Apps/Gedit/PythonPluginHowTo – ohhorob 2013-12-26 15:23:27

+1

这是一篇很美的文章,因为它清楚而简洁地表明了我们现代生物学家有多幸运。对于他/她来说,python是模块化脚本语言,用于为模块开发人员提供一些抽象,以便他们不需要解析主要的C代码。然而,现在一天,很少有生物学家会学习C,而是用Python做所有事情。在编写模块时,我们如何抽象出我们的主要Python程序的复杂性?从现在开始的10年内,也许程序将写入表情符号,模块将只是包含一系列咕噜声的音频文件。也许那很好。 – 2016-02-24 12:22:59

6

我喜欢在Pycon 2009上Andre Drberge博士给出的不同插件体系结构的精彩讨论。他从很简单的东西开始介绍了实现插件的不同方式。

它作为一个podcast(第二部分,解释猴子补丁),伴随着一系列six blog entries

我建议在做出决定之前快速聆听。

45

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模块t oo,虽然你可以用__import__os.listdir和一些字符串操作做很多事情。

2

setuptools has an EntryPoint

入口点是用于分布一种通过其它发行使用“广告”的Python 对象(如函数或类)的简单方法。 可扩展应用程序和框架可以从特定分布 或从sys.path上的所有活动分布中搜索具有特定名称或组的入口点 ,然后根据需要检查或加载 广告对象。

如果您使用pip或virtualenv,AFAIK此软件包始终可用。

3

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

9

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

作为彼此方法插件系统,您可以检查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库操作类的创建ocess通过元类,从而例如在上面的MyCoolClass创建新实例时,我们得到了新的类的实例,它是两者具有MyCoolClassExtensionMyCoolClass功能两者的子类,由于Python的multiple inheritance

在过去的阶级创造更好的控制有在此lib中定义的几元类:

  • ExtensibleType - 允许简单的可扩展通过继承

  • ExtensibleByHashType - 类似EXTEN sibleType,但能力巡航能力,以 打造一流的专业版本,允许基类的全球扩展 和

  • 的专门版本,扩展

该库在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这里是extesible对象。 RecordDateTime是扩展名。

要启用扩展,包含扩展类只是导入模块,和(如果上文)之后创建的所有Record对象将具有在基类扩展类,因此具有其所有功能。

这个库的主要优点是,运行可扩展对象的代码不需要知道扩展和扩展就可以改变可扩展对象中的所有内容。

4

其实setuptools的与 “plugins目录” 的作品,从项目的文档采取了以下例子: 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的是一个更安全的选择,因为它可以加载插件而不会发生冲突或缺少要求。

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

2

扩展@ edomaur的答案,我建议看看simple_plugins(无耻插件),这是一个简单的插件框架,灵感来自于work of 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> 
0

我花了很多时间试图找到小插件系统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 

所以,当你有根基地,元类制成,并且具有从它继承的插件的树,你可以自动获得一流的,这是我从最特定的插件nherits仅通过子类:

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

代码库是非常小(〜30行的纯码)和灵活,因为继承允许。

如果你有兴趣,涉足@https://github.com/thodnev/pluginlib