先日作ったPluginMgrを使ったアプリを作りました。
なんだか、最近、同じような小さなツールばかり作っていたので、なんだかうまい方法はないかなぁ。。
と考えていたのですが、小さいツールはプラグインとして作るようにしよう!!そういえばこのまえPluginMgr(プラグインマネージャ)作ったし!!と思いついたので、早速作ってみました。
今回作ったツールはpyPluggerというツールです。
・起動時に、ツール本体と同じディレクトリにある"plugins"というなまえのディレクトリ中にあるプラグインを読み込みます。
・読み込んだプラグインのファイル名がコンボボックスに表示されます。
・コンボボックスでプラグインを選択すると、選択したプラグインのGUIが表示されます。
といった機能を持っています。
(2)プラグインの情報も見れます
(3)プラグインを選択すると
ためしに、昨日紹介したpydocツールをプラグイン化してみて、pluginsディレクトリに置いてみると、こんな感じです。
pyPlugger本体(PluginMgrを除いた部分)のソースはこんな感じです。
# -*- coding: utf-8 -*- import sys import pygtk if sys.platform != 'win32': pygtk.require('2.0') import gtk from pluginMgr import PluginMgr # GTKスレッド初期化 gtk.gdk.threads_init() # # class PlugWindow: ''' ''' def __init__(self, pluginMgr=None): ''' 初期化ウインドウ ''' self.pluginMgr = pluginMgr self.pluginContainer = None self.authorLabel = None self.versionLabel = None self.descLabel = None # ウインドウの生成 self.wind = gtk.Window(gtk.WINDOW_TOPLEVEL) self.wind.set_border_width(6) self.wind.set_size_request(300, -1) self.wind.set_position(gtk.WIN_POS_CENTER) self.wind.set_title('pyPlugger') self.wind.connect('delete-event', self.on_delete_event) # ウインドウ内容を構築 widgets = self.get_widgets() if widgets: self.wind.add(widgets) # ウインドウの内容を表示 self.wind.show_all() def get_widgets(self): ''' ウインドウに表示するウィジェットを構築する。 ''' vbox = gtk.VBox() hbox = gtk.HBox() vbox.pack_start(hbox, False, False, 5) label = gtk.Label('Choose Plugin') hbox.pack_start(label) self.pluginCombo = gtk.combo_box_new_text() self.pluginCombo.connect('changed', self.on_change_plugin_combo) hbox.pack_start(self.pluginCombo) self.update_plugin_combobox() self.pluginContainer = gtk.Frame() vbox.pack_start(self.pluginContainer, False, False, 4) expander = gtk.Expander('Plugin info') vbox.pack_start(expander, False, False, 4) expVBox = gtk.VBox(False) expander.add(expVBox) label = gtk.Label('Author:') label.set_alignment(0.0, 0.5) expVBox.pack_start(label, False, False, 2) self.authorLabel = gtk.Label('----') self.authorLabel.set_alignment(0.1, 0.5) expVBox.pack_start(self.authorLabel, False, False, 2) label = gtk.Label('Version:') label.set_alignment(0.0, 0.5) expVBox.pack_start(label, False, False, 2) self.versionLabel = gtk.Label('----') self.versionLabel.set_alignment(0.1, 0.5) expVBox.pack_start(self.versionLabel, False, False, 2) label = gtk.Label('Description:') label.set_alignment(0.0, 0.5) expVBox.pack_start(label, False, False, 2) self.descLabel = gtk.Label('----') self.descLabel.set_alignment(0.2, 0.5) expVBox.pack_start(self.descLabel, False, False, 2) return vbox def update_plugin_combobox(self): ''' プラグインコンボボックスの項目名を更新する。 ''' if self.pluginMgr == None: return if self.pluginCombo == None: return # コンボボックスにプラグインから取得した名称を # 追加する。 for name in self.pluginMgr.get_plugin_names(): self.pluginCombo.append_text(name) self.pluginCombo.set_active(0) def on_change_plugin_combo(self, combo, data=None): ''' コンボボックスの選択状態が変更された場合の処理 ''' if (combo == None or self.pluginMgr == None or self.pluginContainer == None): return # コンボボックスからプラグインモジュール名を取得し、その名前で # プラグインマネージャからプラグイン中の関数を取得 funcDict = self.pluginMgr.get_plugin_funcs(combo.get_active_text()) if funcDict == None: return # プラグインのGUI作成関数が存在するか調べる。 # 存在しなければ処理を中断 key = 'get_widgets' if key not in funcDict: return # 現在表示されているプラグインのGUIがあれば削除する。 child = self.pluginContainer.get_child() if child: self.pluginContainer.remove(child) # プラグインにGUIを作成させ、本体に貼り付ける。 widget = funcDict[key]() if widget: self.pluginContainer.add(widget) self.pluginContainer.show_all() # プラグインについての情報(作者、バージョン、説明など) # を更新する。 infoList = [('get_author', self.authorLabel), ('get_version', self.versionLabel), ('get_description', self.descLabel)] for funcName, field in infoList: if funcName not in funcDict: continue field.set_text(funcDict[funcName]()) def on_delete_event(self, widget, event=None): ''' ウインドウが閉じられた時に実行すべき処理 ''' print '-- quit --' gtk.main_quit() return False # # if __name__ == '__main__': # プラグインプロトコルに適合する関数の条件を準備 funcs = [('get_name', True), ('get_version', True), ('get_widgets', True), ('get_author', False), ('get_description', False), ('get_settings', False), ('set_settings', False)] # プラグインマネージャの生成 pluginMgr = PluginMgr('./plugins', funcs) # ウインドウを生成 wind = PlugWindow(pluginMgr) gtk.gdk.threads_enter() gtk.main() gtk.gdk.threads_leave()
上記のソースのこの部分でPluginMgr(プラグインマネージャ)を生成してます。
funcs = [('get_name', True), ('get_version', True), ('get_widgets', True), ('get_author', False), ('get_description', False), ('get_settings', False), ('set_settings', False)] # プラグインマネージャの生成 pluginMgr = PluginMgr('./plugins', funcs)
funcsの設定内容は、
プラグイン中に存在する関数名と、その関数が必須かどうかのフラグをタプルにし、それを関数の数だけリストにしています。
必須かどうかのフラグがTrueになっている関数が存在しないモジュールは、プラグインのプロトコルを満たしていないということで、プラグインとして登録されません。
次に、
ためしに作ってみたプラグイン(pydocUtilのプラグイン版)のソースはこんな感じです。
グローバル変数ってあまり使ったことないので、あまり自信がないのですが、一応動いてます。(笑
赤文字の部分が、プラグインのプロトコルとして指定されている関数です。それいがいの関数はプラグイン側の処理を行うために任意に追加された関数やクラスですので、Plugger本体側にとってはどうでもいい関数たちです。
# -*- coding: utf-8 -*- import sys import os import pkgutil import threading import pydoc import pygtk if sys.platform != 'win32': pygtk.require('2.0') import gtk # # class PydocThread(threading.Thread): ''' pydocを使って、モジュールのHTMLを出力するスレッド ''' def __init__(self, modList, cbStart, cbProceed, cbTerminate): ''' 初期化 ''' threading.Thread.__init__(self) self.modList = modList self.cbStart = cbStart self.cbProceed = cbProceed self.cbTerminate = cbTerminate def run(self): ''' スレッド処理 ''' cnt = len(self.modList) if self.cbStart: self.cbStart(cnt) i = 0 for mod in self.modList: # HTMLを作成して保存 pydoc.writedoc(mod) if self.cbProceed: self.cbProceed(i, cnt) i += 1 if self.cbTerminate: self.cbTerminate() # # globals # g_progressBar = None g_startButton = None g_modulePath = None g_outputPath = None g_saveCwd = None # # widget signal handlers # def show_dir_chooser(title, parent=None): ''' ディレクトリ選択ダイアログを表示 ''' chooser = gtk.FileChooserDialog(title=title, parent=parent, action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OK, gtk.RESPONSE_OK)) chooser.set_default_response(gtk.RESPONSE_OK) chooser.set_position(gtk.WIN_POS_CENTER) response = chooser.run() dirPath = None if response == gtk.RESPONSE_OK: dirPath = chooser.get_filename() chooser.destroy() return dirPath def update_start_button(): ''' モジュールディレクトリと出力ディレクトリの両方が 選択されていたら、スタートボタンを有効に。 ''' global g_modulePath global g_outputPath global g_startButton if (g_modulePath == None or g_outputPath == None or g_startButton == None): return flag = (len(g_modulePath.get_text()) > 0 and len(g_outputPath.get_text()) > 0) g_startButton.set_sensitive(flag) def on_button_modules_path(widget, event=None): ''' モジュールディレクトリ選択ボタンが押された場合の処理 ''' global g_modulePath if g_modulePath == None: return folder = show_dir_chooser('Select module directory') if folder: g_modulePath.set_text(folder) update_start_button() def on_button_output_path(widget, event=None): ''' 出力ディレクトリ選択ボタンが押された場合の処理 ''' global g_outputPath if g_outputPath == None: return folder = show_dir_chooser('Select output directory') if folder: g_outputPath.set_text(folder) update_start_button() def on_pydoc_begin(cnt): global g_saveCwd global g_outputPath global g_progressBar if (g_outputPath == None or g_progressBar == None): return # ワーキングディレクトリの退避 g_saveCwd = os.getcwd() # 出力ディレクトリをカレントワーキングディレクトリに。 os.chdir(g_outputPath.get_text()) gtk.gdk.threads_enter() g_progressBar.set_text('0/%d' % cnt) g_progressBar.set_fraction(0.0) gtk.gdk.threads_leave() def on_pydoc_proceed(i, cnt): ''' 1件処理が完了するたびにスレッドから呼び出される処理 ''' global g_progressBar if g_progressBar == None: return gtk.gdk.threads_enter() g_progressBar.set_text('%d/%d' % (i, cnt)) g_progressBar.set_fraction(float(i) / float(cnt)) gtk.gdk.threads_leave() def on_pydoc_end(): ''' スレッドの全ての処理が完了した際に呼び出される処理 ''' global g_progressBar if g_progressBar == None: return gtk.gdk.threads_enter() g_progressBar.set_text('-- Finished --') g_progressBar.set_fraction(0.0) gtk.gdk.threads_leave() # ワーキングディレクトリを復帰 os.chdir(g_saveCwd) def on_button_start(widget, event=None): ''' スタートボタンが押された場合の処理。 モジュールドキュメント作成を開始する。 ''' global g_modulePath # 処理対象のモジュールディレクトリが、Pythonパスに # 含まれていない場合には、追加 if g_modulePath == None: return modPath = g_modulePath.get_text() if modPath not in sys.path: sys.path.append(modPath) # モジュールディレクトリ中のモジュールを取得 modList = [name for loader, name, ispkg in pkgutil.walk_packages([modPath])] thr = PydocThread(modList, on_pydoc_begin, on_pydoc_proceed, on_pydoc_end) thr.start() def get_path_select_widget(frameLabel, handler): ''' パス選択用のウィジェットを生成して返す。 ''' frame = gtk.Frame(frameLabel) hbox = gtk.HBox() hbox.set_border_width(4) label = gtk.Label('') label.set_size_request(250, 18) hbox.pack_start(label, True, True, 4) bt = gtk.Button('...') if handler: bt.connect('clicked', handler) hbox.pack_end(bt, False, False, 4) frame.add(hbox) return frame, label # # plugin protocol functions # def get_author(): ''' プラグイン製作者名を返す ''' return u'jkani4' def get_name(): ''' プラグイン名称を取得 ''' return u'pydocUtil' def get_description(): ''' プラグインについての説明文を取得 ''' text = u'pydocモジュールを使用して、モジュールのドキュメントを\n指定されたディレクトリに出力します。' return text def get_version(): ''' プラグインバージョン(文字列)の取得 ''' return u'0.5.0' def get_widgets(): ''' プラグインがアクティブになった際に呼び出される関数。 プラグインのGUIを生成して返す。 ''' global g_modulePath global g_outputPath global g_progressBar global g_startButton vbox = gtk.VBox() # モジュールディレクトリ選択用ウィジェットの生成 frame, label = get_path_select_widget('Module directory', on_button_modules_path) g_modulePath = label vbox.pack_start(frame, False, False, 4) # ドキュメント出力ディレクトリ選択用ウィジェットの生成 frame, label = get_path_select_widget('Output directory', on_button_output_path) g_outputPath = label vbox.pack_start(frame, False, False, 4) # プログレスバー g_progressBar = gtk.ProgressBar() g_progressBar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT) g_progressBar.set_size_request(-1, 12) vbox.pack_start(g_progressBar, False, False, 4) # 開始ボタン g_startButton = gtk.Button('Start') g_startButton.connect('clicked', on_button_start) vbox.pack_start(g_startButton, False, False, 4) return vbox def get_settings(): ''' プラグインの設定を保存する際に呼び出される関数。 プラグインのパラメータを辞書の形で返す。 ''' return {} def set_settings(settings): ''' プラグインの設定を復元する際に呼び出される関数。 渡された辞書settingのデータをもとに、設定を復元する処理 を記述する。 ''' pass
きょうも楽しく遊べました。(笑