2009-05-31 35 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