プラグインマネージャクラス(PluginMgr)に機能追加。

先日から紹介しているプラグインマネージャクラスに機能を追加しました。
追加した機能は

プラグインのごとに、動作中の状態を保存しておけるように。

となっています。

プラグインの中には、動作中に設定された内容を次回起動時にも覚えておきたいものも多いのでは。。と思ってこの機能を追加しました。もしかしたら、それはプラグインマネージャとは分離したほうがいい機能なのかもしれませんが。。(笑
ソースコード後半の赤文字の部分が今回追加した部分です。

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

import os
import sys
import inspect
import cPickle

#
#
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 check_config_args(self, confDir, modName, funcName):
        '''
        プラグイン設定・取得関数の引数をチェックする。
        '''
        if (confDir == None or
            modName == None or
            funcName == None):
            print 'illegal param'
            return False

        if os.path.isdir(confDir) == False:
            return False

        return True

    def load_all_configs(self, confDir, funcName):
        '''
        管理している全てのプラグインの設定を読み込む。
        
        confDir:
            設定ファイル出力先ディレクトリ
        funcName:
            プラグインの現在の設定を取得するための関数名
        return:
            なし
        '''
        for modName in self.get_plugin_names():
            self.load_config(confDir, modName, funcName)
    
    def load_config(self, confDir, modName, funcName):
        '''
        modNameで指定されたプラグインの設定(pickle化されて保存されている)を
        読み込み、プラグインに設定する。
        プラグインへの設定はfuncNameで指定された名前の関数を使用する。
        この関数はconfDir中に"モジュール名.pkl"が存在することを期待する。
        存在しない場合は、読み込みをスキップする。

        confDir:
            設定ファイル読み込みディレクトリ
        modName:
            設定を読み込むモジュール名
        funcName:
            プラグインの現在の設定を設定するための関数名
        return:
            True  成功
            False 失敗
        '''
        # 引数チェック
        if self.check_config_args(confDir, modName, funcName) == False:
            return False

        # プラグインの関数辞書を取得し、モジュールに指定された関数が
        # 存在するかチェック。
        funcDict = self.get_plugin_funcs(modName)
        if funcDict == None or funcName not in funcDict:
            print '%s does not have such function(%s).' % (modName, funcName)
            return False

        # 設定ファイルパスを作成して、設定ファイルの存在確認。
        # 設定ファイルが存在しない場合には処理を中断。
        fname = os.path.join(confDir, modName+'.pkl')
        if os.path.isfile(fname) == False:
            print 'config file(%s) is not found, skipped.' % fname
            return False
        
        try:
            # pickle化されている設定ファイルをunpickeして、
            # Pluginに設定
            f = file(fname, 'rb')
            confData = cPickle.load(f)
            f.close()
            if confData:
                funcDict[funcName](confData)
        except:
            print 'Can not load the plugin configuration.'
            return False

        return True

    def save_all_configs(self, confDir, funcName):
        '''
        管理中の全てのプラグイン設定を保存する。

        confDir:
            設定ファイル出力先ディレクトリ
        funcName:
            プラグインの現在の設定を取得するための関数名
        return:
            なし
        '''
        for modName in self.get_plugin_names():
            self.save_config(confDir, modName, funcName)
    
    def save_config(self, confDir, modName, funcName):
        '''        
        modNameで指定されたプラグイン設定をpickle化してファイルへ保存する。
        プラグインの設定は、funcNameで指定された名前の関数を使用して
        プラグインから取得する。
        保存される設定ファイルの名前は"モジュール名.pkl"となる。

        confDir:
            設定ファイル出力先ディレクトリ
        modName:
            設定を保存するモジュール名
        funcName:
            プラグインの現在の設定を取得するための関数名
        return:
            True  成功
            False 失敗
        '''
        # 引数チェック
        if self.check_config_args(confDir, modName, funcName) == False:
            return False

        # プラグインの関数辞書を取得し、モジュールに指定された関数が
        # 存在するかチェック。
        funcDict = self.get_plugin_funcs(modName)
        if funcDict == None or funcName not in funcDict:
            print '%s does not have such function(%s), skipped.' % (modName, funcName)
            return False

        # プラグインから設定データを取得する。
        # 設定データが空っぽの場合には、保存処理をスキップする。
        confData = funcDict[funcName]()
        if confData:
            try:
                f = file(os.path.join(confDir, modName+'.pkl'), 'wb')
                cPickle.dump(confData, f)
                f.close()
            except:
                print 'Can not save the plugin configuration.'
                return False
        else:
            print 'Config is None, skipped.'
            
        return True

以下の関数を、プラグインマネージャ利用者側から呼び出せば、プラグインごとの設定ファイルを保存、読み込みできます。現状ではcPickleモジュールを使用しています。

・load_all_configs()
・load_config()
・save_all_configs()
・save_config()

ソースコード中の冒頭コメントにもかきましたが、プラグインマネージャが自動で上記の4つの関数を呼び出すことはありません。いつ保存・読込みするか、どのようなプロトコルでアプリケーションとプラグインが情報のやりとりをするかはプラグインマネージャを利用する側にしかわからないためです。
いまのところ、この方針でいこうと思います。

一応、昨日紹介したpyPluggerには組み込んであります。が。。。まだ設定を保持するようなプラグインがないので、紹介できません。
明日にでも簡単なプラグインを作ってみようと思います。

いやはや、今日も楽しめました。