naritoブログ

このブログはDjangoとBootstrap4で作成されました
ソースコード

Pythonで、シンプルなドキュメント生成プログラム

プログラミング関連 inspectモジュール importlibモジュール Python 約131日前
2017年4月8日10:14
このような関数があったとします。非常に単純な関数です。
"""functions.py"""

def display(string):
    """文字列を出力する関数

    引数:
        string: 出力する文字列
    """
    
    print(string)


この関数のdocstringはソースコードを見れば一目でわかりますが、プログラムからアクセスしたいケースもいくつかあります。
例えばエディタなんかを作っていれば、この関数を呼び出すdisplay('spam')にカーソルを合わせると、自動でdocstringを表示したい、だとか
pylintのように指定した.pyファイルのdocstringが適切な書き方をされているかチェックをしたい、
Sphinxのようにdocstringを取得し、自動でhtmlにする、なんてこともできますね。


今回はシンプルなドキュメント生成プログラムを作ってみます。
main.pyを実行すると、カレントディレクトリのモジュール、モジュールのdocstring、関数、関数のdocstringをそれぞれ取得し、
doc.htmlというHTMLを作成します。

ディレクトリは、このような構成


main.pyを実行すると、以下のようなdoc.htmlが作成されます。


それではmain.pyです。
import importlib
import inspect
import os


# カレントの.pyファイルを格納していく。このファイルはのぞく
python_files = []
for file in os.listdir('.'):
    name, extension = os.path.splitext(file)
    if name != 'main' and extension == '.py':
        python_files.append(name)

# [(モジュール名, モジュールdoc, モジュール内の関数名・docリスト)....]
# のようなリスト
docstrings = []

for module_name in python_files:
    
    # モジュールのロード
    module = importlib.import_module(module_name)
    
    # モジュールのdocstring。Noneだったら空文字列に
    module_doc = inspect.getdoc(module) or ''


    # [(関数名・docstring), .....]のようなリスト
    functions_name_and_doc = []

    # これで、モジュール内の関数を取得できる
    functions = inspect.getmembers(module, inspect.isfunction)
    
    for func_name, func_object in functions:
        
        # 関数のdoc取得と、リストへの(関数名, docstring)形式での格納
        func_doc = inspect.getdoc(func_object) or ''
        func_name_and_doc = (func_name, func_doc)
        functions_name_and_doc.append(func_name_and_doc)
    
    result = (module_name, module_doc, functions_name_and_doc)
    docstrings.append(result)


# htmlの、メイン部分の作成
content = ''
for module_name, module_doc, functions_name_and_doc in docstrings:
    content += f'<h2>モジュール:{module_name}.py</h2>'
    content += f'<pre>{module_doc}</pre>'
    for func_name, func_doc in functions_name_and_doc:
        content += f'<h3>関数:{func_name}</h3>'
        content += f'<pre>{func_doc}</pre>'
    content += '<hr>'

# htmlへのcontent埋め込みと、html作成
html = f"""
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
</head>
<body>
{content}
</body>
</html>
"""

with open('doc.html', 'wb') as file:
    file.write(html.encode('utf-8'))



doc.htmlは、以下のようなソースになります。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
</head>
<body>
<h2>モジュール:functions.py</h2><pre>関数を集めたモジュールです</pre><h3>関数:display</h3><pre>文字列を出力する関数

引数:
    string: 出力する文字列</pre><h3>関数:print2</h3><pre>print(string)を行う</pre><h3>関数:show</h3><pre>displayとおなじ同じ</pre><hr><h2>モジュール:test.py</h2><pre>テスト用モジュール</pre><h3>関数:test</h3><pre>testを行います</pre><hr><h2>モジュール:__init__.py</h2><pre></pre><hr>
</body>
</html>



今回であれば、python_filesには test, function, __init__ という文字列が入ります。
os.path.splitextは、拡張子部分とそれ以外に分割します。 /var/www/html/a.txt なら、/var/www/html/a と.txtです。
# カレントの.pyファイルを格納していく。このファイルはのぞく
python_files = []
for file in os.listdir('.'):
    name, extension = os.path.splitext(file)
    if name != 'main' and extension == '.py':
        python_files.append(name)



docstringsは、例えば以下のような内容になります。ちょっと複雑ですね。
[
('functions',  # モジュール名
'関数を集めたモジュールです',  # モジュールdoc
[('display', '文字列を出力する関数\n\n引数:\n string: 出力する文字列'),  # 関数名1, 関数doc1
('print2', 'print(string)を行う'),  # 関数名2, 関数doc2
('show', 'displayとおなじ同じ')]),  # 関数名3, 関数doc3

('test',
'テスト用モジュール',
[('display', '文字列を出力する関数\n\n引数:\n string: 出力する文字列'),
('test', 'testを行います')]),

('__init__',
'', 
[])]



importlib.import_module('文字列でのモジュール名')
という形で、モジュールをロードできます。文字列で取得できるというのが便利ですね。
# モジュールのロード
module = importlib.import_module(module_name)



標準ライブラリのinspectモジュールを使っています。
https://docs.python.jp/3/library/inspect.html#module-inspect


inspect は、活動中のオブジェクト (モジュール、クラス、メソッド、関数、トレースバック、フレームオブジェクト、コードオブジェクトなど) から情報を取得する関数を定義しており、クラスの内容を調べたり、メソッドのソースコードを取得したり、関数の引数リストを取り出して整形したり、詳細なトレースバックを表示するのに必要な情報を取得したりするために利用できます。

このモジュールの機能は4種類に分類することができます。型チェック、ソースコードの情報取得、クラスや関数からの情報取得、インタープリタのスタック情報の調査です。



getdocは、__doc__でのアクセスを便利にしたものです。__doc__を取得し、人に見やすい形に整形もしてくれます。
# モジュールのdocstring。Noneだったら空文字列に
module_doc = inspect.getdoc(module) or ''



これは、オブジェクトの全メンバーを、(名前, 値) の組み合わせのリストで返します。
リストはメンバー名でソートされています。
inspect.isfunctionは、関数だったら返す、というオプションです。
# これで、モジュール内の関数を取得できる
functions = inspect.getmembers(module, inspect.isfunction)



これがhtmlのドキュメント部分の作成です。python3.6から、f-stringsで簡単に書けるようになりました。
content = ''
for module_name, module_doc, functions_name_and_doc in docstrings:
    content += f'<h2>モジュール:{module_name}.py</h2>'
    content += f'<pre>{module_doc}</pre>'
    for func_name, func_doc in functions_name_and_doc:
        content += f'<h3>関数:{func_name}</h3>'
        content += f'<pre>{func_doc}</pre>'
    content += '<hr>'



最後にhtml全体へのドキュメント部分埋め込みと、htmlファイルの作成ですね。
# htmlへのcontent埋め込みと、html作成
html = f"""
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
</head>
<body>
{content}
</body>
</html>
"""

with open('doc.html', 'wb') as file:
    file.write(html.encode('utf-8'))




現状では、モジュール内の関数が全て取得されてしまいます...これは、from a import b のようなimport文での、bも拾ってしまいます。
例えばですが、test.pyの中身を以下のようにします。
"""テスト用モジュール"""
from functions import display, show
from os.path import *

def test():
    """testを行います"""
    pass


結果は散々なものでした。あきらかに、os.path内の関数も拾っています。自分でつくったわけじゃないのに!



inspectモジュールは強力なので、対策方法はもちろんあります。
for func_name, func_object in functionsの部分を、以下のように書き換えてみます。
    for func_name, func_object in functions:
        
        py_file = inspect.getsourcefile(func_object)
        py_file = os.path.basename(py_file)
        
        # 関数が、そのモジュール内で定義しているか(importしたものじゃない)
        if f'{module_name}.py' == py_file:
            
            # 関数のdoc取得と、リストへの(関数名, docstring)形式での格納
            func_doc = inspect.getdoc(func_object) or ''
            func_name_and_doc = (func_name, func_doc)
            functions_name_and_doc.append(func_name_and_doc)
        
        else:
            print(f'{func_name}はimportしたもの - {py_file}')


このinspect.getsourcefileは、オブジェクトを定義している Python ソースファイルの名前を返します。
これが今のモジュールの名前と違ったら、importした関数とみなしているわけです。
inspect.getsourcefile(func_object)


実行した感じ、ちゃんと動いているようです。