pyPlugger行数カウントプラグイン ひと段落

今日は行数カウントプラグインの続きを作りました。
下記のソース(ちょっと長めなので、載せるかどうか迷ったんですが、のせちまいました。。)
今日書いたのは、

pyLineCounter()
countTargetLines()

という関です。これらの関数でスクリプトのコメント行数、空白行数、それ以外の行数を
求めるようにしました。

以下の処理を実行させた結果を表示させてみると、こんな感じになりました。


# -*- coding: utf-8 -*-
'''
Pythonスクリプトの行数をカウントするプラグイン。
コメントの行数はカウントしない。
'''
import sys
import os
import pygtk
if sys.platform != 'win32':
    pygtk.require('2.0')
import gtk
import gobject


#
# グローバル変数
g_treeModel = None
g_dispModel = None
g_Tree = None
g_result = []
    

# 行数カウント関数
#
def pyLineCounter(pyText):
    '''
    Pythonスクリプトの行数をカウントする。

    pyText:
        処理対象のテキスト

    RETURN:
        行数カウントの処理結果(タプル)
        タプルの情報は
        (ファイルパス文字列, (総行数, スクリプト行数, コメント行数, 空行数))
        総行数 --> スクリプト行数 + コメント行数 + 空行数
        スクリプト行数 --> コメント行数、空行数を含まない行数
    '''
    emptys = 0
    comments = 0
    
    lines = pyText.splitlines()
    docStrBegin = False
    for ln in lines:        
        lnStr = ln.strip()
        # 空行
        if len(lnStr) == 0:
            emptys += 1
            continue
        # コメント
        if lnStr[0] == '#':
            comments += 1
            continue
        # ドキュメント文字列(開始)
        s = lnStr[:3]
        if s == "'''" or s == '"""':
            docStrBegin = not docStrBegin
            comments += 1
            continue
        # ドキュメント文字列(終了)
        if docStrBegin:
            comments += 1
            if "'''" in lnStr or '"""' in lnStr:
                docStrBegin = not docStrBegin

    return len(lines), len(lines) - emptys - comments, comments, emptys

def countTargetLines(path, acceptExts):
    '''
    指定されたパスの行数をカウントする。
    パスがファイルの場合には、そのファイルの行数をカウント。
    パスがディレクトリの場合には、そのディレクトリ中のファイルを対象に
    再帰的に処理を行う。
    acceptExtsは、ディレクトリ中で処理対象とするファイルの拡張子。

    RETURN:
        タプルのリスト。
        それぞれのタプルの情報は、
        (ファイルパス文字列, (総行数, スクリプト行数, コメント行数, 空行数))
        総行数 --> スクリプト行数 + コメント行数 + 空行数
        スクリプト行数 --> コメント行数、空行数を含まない行数
    '''
    result = []
    if path == None or len(path) == 0:
        return result

    # 渡されたパスがファイルパスの場合
    if os.path.isfile(path):
        print '-- file path --'
        try:
            f = file(os.path.normpath(path), 'r')
            txt = f.read()
            f.close()
            count = pyLineCounter(txt)
            result.append( ( path, count ) )
        except:
            pass
    # 渡されたパスがディレクトリの場合
    elif os.path.isdir(path):
        print '-- directory path --'
        try:
            for p in os.listdir(path):
                ext = os.path.splitext(p)[1]
                if ext not in acceptExts:
                    continue
                result += countTargetLines(os.path.join(path, p), acceptExts)
        except:
            pass
    else:
        pass

    return result

#
# ツリービューモデル
#
class TargetListModel:
    '''
    処理ターゲットリストのデータモデル
    '''
    def __init__(self, targets=[]):
        '''
        初期化
        '''
        self.store = gtk.ListStore(gobject.TYPE_BOOLEAN,
                                   gobject.TYPE_STRING,
                                   gobject.TYPE_STRING)
        self.set_targets(targets)

    def get_model(self):
        '''
        データモデルを取得
        '''
        return self.store

    def set_targets(self, targets):
        '''
        処理対象を追加。
        保持しているデータを破棄したあとで追加される。
        targets : リスト
        '''
        self.clear_targets()
        self.append_targets(targets)

    def append_targets(self, targets):
        '''
        処理対象を追加。
        保持しているデータはそのまま。
        '''
        for t in targets:
            self.store.append( (False, t, '0') )

    def set_target_value(self, row, col, val):
        '''
        ターゲットの値を設定する
        row : 行インデックス(0オリジン)
        col : カラムインデックス(0オリジン)
        val : 値
        '''
        aIter = self.store.get_iter( (row,) )
        if aIter == None:
            return
        self.store.set_value(aIter, col, val)

    def get_targets(self):
        '''
        全てのデータをリストとして取得。
        データは [(ターゲット名, 行数), ...]        
        '''
        targets =[]
        
        aIter = self.store.get_iter_first()
        while aIter != None:
            targets.append((self.store.get_value(aIter, 1),
                            self.store.get_value(aIter, 2)))
            aIter = self.store.iter_next(aIter)
            
        return targets
    
    def remove_targets(self, rows):
        '''
        行指定でデータを削除する。
        rows : 行インデックス(0オリジン)
        '''
        for aIter in [self.store.get_iter(r) for r in rows]:
            self.store.remove(aIter)

    def clear_targets(self):
        '''
        全てのデータをクリアする。
        '''
        self.store.clear()
    
    
class TargetDispModel:
    '''
    処理ターゲットの表示モデル
    '''
    def construct_view(self, model):
        '''
        処理ターゲット表示用のツリービューを生成して返す。
        '''
        # ツリービューの生成
        tree = gtk.TreeView(model)
        tree.set_rules_hint(True)
        tree.set_enable_tree_lines(True)
        tree.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_VERTICAL)

        sel = tree.get_selection()
        sel.set_mode(gtk.SELECTION_MULTIPLE)
        
        # カラム0 (処理状況表示)
        render = gtk.CellRendererToggle()        
        render.set_property('activatable', False)
        col = gtk.TreeViewColumn('Complete', render)        
        col.add_attribute(render, 'active', 0)
        tree.append_column(col)
        
        # カラム1 (ターゲットファイルパス表示)
        render = gtk.CellRendererText()
        render.set_property('editable', False)
        col = gtk.TreeViewColumn('Target', render, text=1)
        col.set_resizable(True)
        col.set_min_width(220)
        tree.append_column(col)

        # カラム2(行数表示)
        render = gtk.CellRendererText()
        render.set_property('editable', False)
        col = gtk.TreeViewColumn('Lines', render, text=2)
        tree.append_column(col)

        return tree

#
#
class DetailDialog:
    '''
    詳細表示ダイアログの表示
    '''
    def __init__(self, infos):
        '''
        初期化

        infos:
             行数カウントの処理結果(タプルのリスト)
             それぞれのタプルの情報は
             (パス文字列, (総行数, スクリプト行数, コメント行数, 空行数))
             総行数 --> スクリプト行数 + コメント行数 + 空行数
             スクリプト行数 --> コメント行数、空行数を含まない行数
        '''
        # ダイアログを生成
        self.dlog = gtk.Dialog('Detail',
                               None,
                               gtk.DIALOG_MODAL,
                               (gtk.STOCK_OK, gtk.RESPONSE_OK))
        self.dlog.set_size_request(400, 300)
        self.dlog.set_position(gtk.WIN_POS_CENTER)

        scroll = gtk.ScrolledWindow()
        textView = gtk.TextView()
        scroll.add(textView)

        detailText = ''
        tt = 0
        ts = 0
        tc = 0
        te = 0
        for path, (total, lines, comments, emptys) in infos:
            dname, fname = os.path.split(path)
            detailText += '- %s  ( %s )\n' % (fname, dname)
            detailText += '\t-> Total  : %d\n' % total
            detailText += '\t-> Source : %d\n' % lines
            detailText += '\t-> Comment: %d\n' % comments
            detailText += '\t-> Empty  : %d\n' % emptys
            tt += total
            ts += lines
            tc += comments
            te += emptys

        detailText += '----------\n'
        detailText += '[Total] %d, [Source] %d, [Comment] %d, [Empty] %d\n' % (tt, ts, tc, te)
        textBuff = textView.get_buffer()
        textBuff.set_text(detailText)

        self.dlog.vbox.pack_start(scroll)
        
        # ダイアログの表示
        self.dlog.show_all()

    def run(self):
        '''
        ダイアログのイベントループ開始
        '''
        return self.dlog.run()

    def destroy(self):
        '''
        ダイアログ破棄
        '''
        self.dlog.destroy()

#
# イベントハンドラ
#
def on_add_target(widget, tree, model):
    '''
    処理対象(ファイル)追加ボタンが押された場合の処理。
    ファイル選択ダイアログを開いて、ファイルを選択させる。
    現状では、拡張子が.pyのファイルのみ選択可能。
    複数ファイル選択可能。
    '''
    if tree == None:
        return
    
    btList = ((gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
               gtk.STOCK_OK, gtk.RESPONSE_OK))
    
    chooser = gtk.FileChooserDialog(title='Select files',
                                    parent=None,
                                    action=gtk.FILE_CHOOSER_ACTION_OPEN,
                                    buttons=btList)
    chooser.set_position(gtk.WIN_POS_CENTER)
    chooser.set_select_multiple(True)

    ff = gtk.FileFilter()
    ff.set_name('python srouce')
    ff.add_pattern('*.py')
    chooser.add_filter(ff)
    
    response = chooser.run()
    if response == gtk.RESPONSE_OK:
        model.append_targets(chooser.get_filenames())
        
    chooser.destroy()

#
#
def on_add_target_dir(widget, tree, model):
    '''
    処理対象(ディレクトリ)追加ボタンが押された場合の処理。
    ファイル選択ダイアログを開いて、ファイルを選択させる。
    複数ファイル選択可能。
    '''
    btList = ((gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
               gtk.STOCK_OK, gtk.RESPONSE_OK))
    
    chooser = gtk.FileChooserDialog(title='Select files',
                                    parent=None,
                                    action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
                                    buttons=btList)
    chooser.set_position(gtk.WIN_POS_CENTER)
    chooser.set_select_multiple(True)

    response = chooser.run()
    if response == gtk.RESPONSE_OK:        
        model.append_targets(chooser.get_filenames())
        
    chooser.destroy()

def on_delete_target(widget, tree, model):
    '''
    処理対象を削除。
    リストの選択されている行を削除する。
    '''
    if tree == None:
        return

    sel = tree.get_selection()
    if sel == None:
        return

    model.remove_targets(sel.get_selected_rows()[1])

def on_start(widget, tree, model):
    '''
    スタートボタンが押された場合の処理。
    リストに登録されているファイルの行数カウント処理を
    開始する。
    '''
    global g_result
    g_result = []

    if model == None:
        return

    targets = model.get_targets()
    for row, (path, flg) in enumerate(targets):
        result = countTargetLines(path, ('.py'))
        g_result += result
        
        model.set_target_value(row, 0, True)

        count = 0
        for path, (total, lines, comments, emptys) in result:
            count += lines
        model.set_target_value(row, 2, count)

def on_detail(widget, event=None):
    '''
    詳細ダイアログを表示
    '''
    global g_result

    if len(g_result) == 0:
        return

    dlg = DetailDialog(g_result)
    response = dlg.run()
    dlg.destroy()


#-------------------------
# プラグインプロトコル関数
#-------------------------
def get_author():
    '''
    プラグイン製作者名を返す
    pyPluggerのプラグインモジュールとしては必須ではないので省略可。
    '''
    return u'jkani4'

def get_name():
    '''
    プラグイン名称を取得
    pyPluggerのプラグインモジュールとしては必須の関数。
    '''
    return u'ScriptCounter'

def get_description():
    '''
    プラグインについての説明文を取得
    pyPluggerのプラグインモジュールとしては必須ではないので省略可。
    '''
    return u'Pythonスクリプトの行数をカウントするためのプラグイン。\nコメント行は含まれない。(ことを最終目標に..)'
    
def get_version():
    '''
    プラグインバージョン(文字列)の取得
    pyPluggerのプラグインモジュールとしては必須の関数。
    '''
    return u'0.5.0'

def get_widgets():
    '''
    プラグインがアクティブになった際に呼び出される関数。
    プラグインのGUIを生成して返す。
    作成するGUIはPyGTKのウィジェットであること。

    pyPluggerのプラグインモジュールとしては必須の関数。
    '''
    global g_treeModel
    global g_dispModel
    global g_tree
    
    base = gtk.VBox()
    base.set_border_width(5)

    g_treeModel = TargetListModel()
    g_dispModel = TargetDispModel()
    g_tree = g_dispModel.construct_view(g_treeModel.get_model())

    scroll = gtk.ScrolledWindow()
    scroll.set_size_request(-1, 200)
    scroll.add(g_tree)
    
    base.pack_start(scroll, False, False, 4)

    hbox = gtk.HBox()
    base.pack_start(hbox, False, False, 4)
    
    bt = gtk.Button('Files')
    bt.connect('clicked', on_add_target, g_tree, g_treeModel)
    hbox.pack_start(bt, True, True, 4)
    
    bt = gtk.Button('Directorys')
    bt.connect('clicked', on_add_target_dir, g_tree, g_treeModel)
    hbox.pack_start(bt, True, True, 4)
    
    bt = gtk.Button('Del')
    bt.connect('clicked', on_delete_target, g_tree, g_treeModel)
    hbox.pack_start(bt, True, True, 4)    

    bt = gtk.Button('Detail')
    bt.connect('clicked', on_detail)
    hbox.pack_start(bt, True, True, 4)
    
    bt = gtk.Button('Start')
    bt.connect('clicked', on_start, g_tree, g_treeModel)
    base.pack_start(bt, False, False, 8)
    
    return base

def get_settings():
    '''
    プラグインの設定を保存する際に呼び出される関数。
    プラグインのパラメータを辞書の形で返す。
    リターンするデータはpickleがサポートしているデータで
    ある必要がある。

    pyPluggerのプラグインモジュールとしては必須ではないので省略可。
    '''
    return {}

def set_settings(settings):
    '''
    プラグインの設定を復元する際に呼び出される関数。
    渡された辞書settingのデータをもとに、設定を復元する処理
    を記述する。
    pyPluggerのプラグインモジュールとしては必須ではないので省略可。

    setting:
        関数get_settingsで返したデータと同じものが渡される
    '''
    print 'set_settings', settings