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