pythonでプラグイン管理のしくみを作ってみました。

先日作ったファイルコピーツールにプラグイン機能を追加する時にも使えるかな。。。と思いながら、簡易プラグイン管理クラスを作ってみました。

クラスの名前はPluginMgrです。(あまりにも安易な名前ですけどね。。)

このPluginMgrは以下のことを行います。

・指定されたディレクトリ中から、モジュールをインポートする。
・インポートしたモジュールがプラグインとしての条件(必要な関数が存在すること)を満たしている場合には
 モジュールごとに、関数名と関数オブジェクトの辞書を作成して保持。

ソースコードはこんな感じです。

# -*- coding: utf-8 -*-

import os
import sys
import inspect

#
#
class PluginMgr:
    '''
    プラグイン管理マネージャ。
    
    生成時に指定されたプラグインディレクトリ中から、プラグインプロトコル
    に適合するプラグインモジュールをロードする。
    ロードしたプラグインモジュールに関して、モジュール名、モジュール中の
    関数名と関数オブジェクトを辞書として保持する。
    '''
    def __init__(self, targetDir, pluginFuncs):
        '''
        初期化。
        targetDir:
            管理対象のプラグインディレクトリパス。
        requiredFuncs:
            プラグインプロトコルに適合するための関数情報のリスト。
            [(関数名, 必須フラグ), ....]
            必須フラグがTrueに指定されている関数はプロトコル適合のためには必須。
            Falseのものは、その関数が存在すれば呼び出されることを示す。
        '''

        # プラグインプロトコルチェック用リスト
        # 必須関数用リスト
        self.requiredFuncNames = 
        # その他の関数用リスト
        self.otherFuncNames = 

        # プロトコル関数情報の設定
        self.set_plugin_funcs(pluginFuncs)
                        
        # 管理ディレクトリパスを初期化
        self.pluginDirPath = None

        # プラグイン情報用の辞書を初期化
        # {'モジュール名': {関数名: 関数オブジェクト, ...},
        #  'モジュール名': {関数名: 関数オブジェクト, ...}}
        # という管理構造となっている。
        self.pluginDict = {}

        # プラグインディレクトリの登録
        self.set_plugin_dir(targetDir)

        # パスが実存するディレクトリの場合には、その中に存在するプラグイン
        # を走査し、自身の管理用辞書に登録する
        self.load_plugins()

    def set_plugin_funcs(self, funcs):
        '''
        プラグインプロトコルに適合する関数条件を設定する。
        funcs:
            プラグインプロトコルに適合するための関数情報のリスト。
            [(関数名, 必須フラグ), ....]
        Return:
            None
        '''        
        if funcs == None:
            return            

        # 現状の情報を破棄
        del self.requiredFuncNames[:]
        del self.otherFuncNames[:]

        # 必須関数名、その他の関数名を振り分けておく
        for name, flag in funcs:
            if flag:
                self.requiredFuncNames.append(name)
            else:
                self.otherFuncNames.append(name)

    def set_plugin_dir(self, path):
        '''
        パス文字列を正規化し、プラグインディレクトリとして登録。
        path:
            プラグインディレクトリのパス文字列。
        Return:
            None
        '''
        if path == None or len(path) == 0:
            return

        # プラグインディレクトリパスを正規化
        self.pluginDirPath = os.path.normpath(os.path.expanduser(path))

        # 管理対象のプラグインディレクトリがモジュールパスに含まれて
        # いない場合には追加する。
        if self.pluginDirPath not in sys.path:
            sys.path.append(self.pluginDirPath)

    def load_plugins(self):
        '''
        プラグインディレクトリ中のプラグインをロードし、
        プラグインプロトコルチェックを通過したもののみを
        自身の管理用辞書に追加する。
        Return:
            None
        '''
        if self.pluginDirPath == None:
            return
        if os.path.isdir(self.pluginDirPath) == False:
            return

        # プラグイン管理用辞書をクリア
        self.pluginDict.clear()

        # プラグインディレクトリ中のファイルを調べ、プラグインプロトコルに
        # 適合するモジュールの情報を構築する。
        for item in os.listdir(self.pluginDirPath):
            if os.path.isfile(item) != False:
                continue

            modName = inspect.getmodulename(item)
            
            funcInfoDict = self.import_plugin(modName)
            if funcInfoDict:
                self.pluginDict[modName] = funcInfoDict

    def import_plugin(self, modName):
        '''
        プラグインプロトコルに一致したクラスをモジュールからインポートする。
        modName:
            モジュール名文字列。
        Return:
            辞書。
            {関数名: 関数オブジェクト, ....}
            モジュール名が不正な場合やプラグインプロトコルに適合しなかった
            場合にはNoneが返される。
        '''
        if modName == None or len(modName) == 0:
            return None

        # モジュールをインポート
        try:
            mod = __import__(modName)
        except:
            print 'error on importing %s' % modName
            return None

        # モジュールからクラス属性をもつオブジェクトを取得する。
        funcList = inspect.getmembers(mod, inspect.isfunction)
        if funcList == None or len(funcList) == 0:
            return None

        # 必須の関数が存在しない場合にはそのモジュールは
        # プロトコルに対応していないと判定し、処理を中断。
        # (そのモジュールの関数情報は作成しない)
        funcNames = [item[0] for item in funcList]
        for fname in self.requiredFuncNames:
            if fname not in funcNames:
                return None

        # モジュール内の関数のうち、プロトコル適合条件として
        # 登録されている関数についてのみ情報を登録する。
        # (プロトコルに無関係な関数オブジェクトは無視する)
        funcDict = {}
        for name, funcObj in funcList:
            if (name not in self.requiredFuncNames and
                name not in self.otherFuncNames):
                continue

            funcDict[name] = funcObj

        return funcDict

    def get_plugin_count(self):
        '''
        登録されているプラグイン数を取得する。
        Return:
            整数。プラグインモジュール数。
        '''
        return len(self.pluginDict)
    
    def get_plugin_names(self):
        '''
        登録されているプラグイン名のリストを取得する。
        Return:
            リスト。
            [モジュール名, ...]
        '''
        return self.pluginDict.keys()

    def get_plugin_funcs(self, modName):
        '''
        プラグインモジュール名(文字列)を指定して、そのモジュール
        のプラグイン関数情報を取得する。
        modName:
            モジュール名文字列
        return:
            リスト。
            [(関数名, 関数オブジェクト), ...]
        '''
        if modName == None:
            return None
        if modName not in self.pluginDict:
            return None

        return self.pluginDict[modName]


#
# 動作テスト
#
def test_plugin_mgr(dirpath, funcs):
    # プラグインマネージャを生成
    mgr = PluginMgr(dirpath, funcs)

    #
    # 生成したプラグインマネージャの情報を確認
    #
    print '------------------------'
    print 'plugin module count : %d' % mgr.get_plugin_count()

    for name in mgr.get_plugin_names():
        print 'name: %s' % name
        funcDict = mgr.get_plugin_funcs(name)
        for funcName, funcObj in funcDict.iteritems():
            print '\t%s(...): %s' % (funcName, funcObj)
    
if __name__ == '__main__':
    # プラグインプロトコルに適合する関数の条件を準備
    funcs = [('initialize', False),
             ('terminate', False),
             ('get_name', True),
             ('get_description', False),
             ('get_version', True),
             ('execute', True)]

    # 正常系
    test_plugin_mgr('./plugins', funcs)

    # ディレクトリパスが不正
    test_plugin_mgr('./aaa', funcs)

    # プラグインプロトコル関数リストが不正
    test_plugin_mgr('./plugins', None)
    test_plugin_mgr('./plugins', [])

上記のソースコードを実行してみると、次のようになりました。

今回は、初めてinspectというモジュールを使いました。
このモジュールは、pythonスクリプトモジュール中からクラス、関数などを任意に取り出すことができます。
inspectを使えば、かなりコードを減らせるので便利だなと思いました。

さてさて、PluginMgrを作ったのはいいですが、これをどう使っていくか。。。まだ考えてません(笑
単なる実験で終わらなければいいけどな。。。と思ってます。(笑