モジュールのドキュメントを抜き出す(別バージョン)


前回作ったのは、xml.dom.minidomを使ってxmlを出力する処理を行っていましたが、
今回はstring.Templateを使用して同じようなことをやってみました。
今までにstring.Templateはあまり使っていなかったのですが、使ってみるとなかなか便利です。


ソースコードはこんな感じです。
メソッド get_doc_str()の処理にstring.Templateを使用している以外は、前回のものとほぼ同じです。
(実行結果もほぼ同じです。)
xml.dom.minidomなどは、とても便利なんですけど、出力されるxmlの書かれ方を細かく決められないようなので、
場合によってはこちらのやり方のほうかシックリくるかもしれません。

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

import os
import sys
import inspect
from string import Template
from xml.dom.minidom import Document


DEFAULT_METHOD_TEMPLATE ='''
<para class="synopsis">
<function>$method_name</function>
<synopsis>$method_synopsis</synopsis>
<para>
$method_desc
</para>
</para>
'''

DEFAULT_CLASS_TEMPLATE = '''
<sect2><title>$class_name</title>
<para class="class_desc">
$class_desc
</para>
$template_method
</sect2>
'''

DEFAULT_MODULE_TEMPLATE = '''
<sect1><title>$module_name</title>
$template_class
</sect1>
'''

DEFAULT_CHAPTER_TEMPLATE = '''
<?xml version="1.0" encoding="UTF-8"?>

<chapter id="$chapter_id"><title>$chapter_title</title>
$template_mod
</chapter>
'''

#
#
class ModuleDocGenerator:
    '''
    モジュールのドキュメント文字列を抽出するためのモジュール。
    生成時、コンストラクタで指定されたモジュールをパースし、内部情報を構築。

    get_doc_str()を呼び出すことでdocbook形式のxmlテキストを取得することができる。
    
    '''
    def __init__(self, modPath, excludes=[]):
        '''
        初期化。

        引数:
            modPath: 処理対象のモジュールファイルパス。
                     ディレクトリが指定された場合には、再帰的に.pyファイルを処理する。
            excludes: 処理対象外のファイルまたはディレクトリ。
        '''
        modPath = os.path.normpath(os.path.expanduser(modPath))
        self.excludes = excludes
        self.modDictList = []
        self._parse_module(modPath)

    def _parse_module(self, modPath):
        '''
        モジュールファイルからドキュメント文字列を取り出し、辞書を構築する。

        内部的に構築される辞書の形式:
            {'module': モジュール名,
             'doc': モジュールのドキュメント文字列
             'classes': ['cls': クラス名称,
                         'doc': クラスのドキュメント文字列,
                         'methods': [{'method': メソッド名称,
                                      'doc': メソッドのドキュメント文字列}]]}

        引数:
            modPath: 処理対象のモジュールファイルパス。
                     ディレクトリが指定された場合には、再帰的に.pyファイルを処理する。
        '''
        if modPath in self.excludes:
            return

        if os.path.isdir(modPath):
            if modPath not in sys.path:
                sys.path.append(modPath)
                
            for p in os.listdir(modPath):
                self._parse_module(os.path.join(modPath, p))
        elif os.path.isfile(modPath) == False:
            print 'No such module: %s' % modPath
            return

        if os.path.splitext(modPath)[1] != '.py':
            return

        modName = inspect.getmodulename(modPath)
        try:
            mod = __import__(modName)
        except Exception, err:
            print err
            return

        # モジュール情報を構築
        modDict = {'module': modName, 'doc': None, 'classes':[]}
        modDict['doc'] = inspect.getdoc(mod)        
        self.modDictList.append(modDict)

        # クラス情報を構築
        for name, cls in inspect.getmembers(mod, inspect.isclass):
            if mod != inspect.getmodule(cls):
                continue

            cDict = {'cls': name, 'doc': None, 'methods': []}
            modDict['classes'].append(cDict)
            
            doc = inspect.getdoc(cls)
            if doc:
                cDict['doc'] = doc.strip().decode('utf-8')

            # メソッド情報を構築
            for mName, mObj in inspect.getmembers(cls, inspect.ismethod):
                if mod != inspect.getmodule(mObj):
                    continue

                mDict = {'method': mName,
                         'args': inspect.getargspec(mObj)[0],
                         'doc': None}
                cDict['methods'].append(mDict)
            
                doc = inspect.getdoc(mObj)
                if doc == None:
                    continue

                mDict['doc'] = doc.strip().decode('utf-8')

    def _replace_special_char(self, text):
        '''
        渡されたテキスト中で、xml表現としての特殊文字("<", ">"など)
        が含まれる場合、それらを適切に変換する。

        引数:
            text: 文字列

        戻り値:
            変換後のテキスト
        '''
        if not text:
            return
        specials = [('<', '&lt;'), ('>', '&gt;')]
        for s, r in specials:
            if s in text:
                text = text.replace(s, r)
        return text

    def get_doc_str(self, chapterID, chapterTitle='class reference',
                    tChapterStr=DEFAULT_CHAPTER_TEMPLATE,
                    tModuleStr=DEFAULT_MODULE_TEMPLATE,
                    tClassStr=DEFAULT_CLASS_TEMPLATE,
                    tMethodStr=DEFAULT_METHOD_TEMPLATE):
        '''
        docbook形式のxmlテキストを取得する。
        このメソッドによって取得できる文字列すべての行が行頭から始まる。

        引数:
            chapterName: チャプター名文字列。

        戻り値:
            文字列。
        '''
        tChapter=Template(tChapterStr.strip().rstrip())
        tModule=Template(tModuleStr.strip().rstrip())
        tClass=Template(tClassStr.strip().rstrip())
        tMethod=Template(tMethodStr.strip().rstrip())
        
        modStr = ''
        for modItem in self.modDictList:
            clsList = modItem['classes']
            if len(clsList) == 0:
                continue

            clsStr = ''
            for clsItem in clsList:
                methodStr = ''
                for methodItem in clsItem['methods']:
                    argList = [s for s in methodItem['args'] if s != 'self']
                    argStr = ''
                    if len(argList) > 0:
                        argStr = reduce(lambda a, b: a + ', ' + b, argList)
                    funcStr = methodItem['method'] + '(' + argStr + ')'
                    descStr = self._replace_special_char(methodItem['doc'])
                    methodKW = {'method_name': methodItem['method'],
                                'method_synopsis': funcStr,
                                'method_desc': descStr}
                    methodStr += tMethod.safe_substitute(methodKW)

                clsKW = {'template_method': methodStr,
                         'class_name': clsItem['cls'],
                         'class_desc': clsItem['doc']}
                clsStr += tClass.safe_substitute(clsKW)

            modKW = {'module_name': modItem['module'],
                     'template_class': clsStr}
            modStr = tModule.safe_substitute(modKW)

        chapKW = {'chapter_id': chapterID,
                  'chapter_title': chapterTitle,
                  'template_mod': modStr}

        text = tChapter.safe_substitute(chapKW)
        return text.encode('utf-8')

#
#
if __name__ == '__main__':
    '''
    テスト
    '''
    if len(sys.argv) < 2:
        print 'argument error'
        sys.exit(0)
        
    gen = ModuleDocGenerator(sys.argv[1])    
    xmlDoc = gen.get_doc_str('class_ref')
    try:
        f = file('class_ref.xml', 'w')
        f.write(xmlDoc)
        f.close()
    except IOError, err:
        print err