先日作ったPluginMgrを使ったアプリを作りました。

なんだか、最近、同じような小さなツールばかり作っていたので、なんだかうまい方法はないかなぁ。。
と考えていたのですが、小さいツールはプラグインとして作るようにしよう!!そういえばこのまえPluginMgr(プラグインマネージャ)作ったし!!と思いついたので、早速作ってみました。

今回作ったツールはpyPluggerというツールです。

・起動時に、ツール本体と同じディレクトリにある"plugins"というなまえのディレクトリ中にあるプラグインを読み込みます。
・読み込んだプラグインのファイル名がコンボボックスに表示されます。
・コンボボックスでプラグインを選択すると、選択したプラグインGUIが表示されます。

といった機能を持っています。

画面はこんな感じです。
(1)起動画面

(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

きょうも楽しく遊べました。(笑