src から rst 生成 - Python Advent Calender 2013 11日目

Python Advent Calendar 2013 11日目です。

今日はTakesxiSximadaが担当です。 非web縛りなのでなるべくwebに離れた内容にしたいと思います。 Pythonは仕事でもそれなりに使っているのですが大したネタは持っていません。 苦肉の策で自分が作っているツールで ソースコードからrstを生成するツール dqn の事を書きます。

dqn - ソースコード から rst 生成

dqnはソースコードからrstを生成するためのツールです。 PythonだけではなくJava, C/C++などのソースコードに対しても行えます。 ドキュメントの記述はrstで行う事ができ Sphinxを利用しているのでSphinxのthemeやextensionも使えます。 こういう話をすると 「sphinx-apidoc 使えばいいじゃん、Sphinxなんだし」という声が聞こえてきそうです。 はい。その通りです。 sphinx-apidocsphinx.ext.autodoc など非常に良い選択だと思います。 ただ dqn はドキュメントを作るという目的は同じでも ドキュメント生成のためのアプローチの仕方や ターゲットとしている言語が異なります。

dqnの使い方

準備

Pythonとかpipとかはもう既にあるとおもうので割愛します。 Python2.7で動作確認しています。 旧スタイルのprint文とか埋め込んでたりするのでPython3では動かないと思います。orz

  1. Doxygenをインストール

    ソースコード解析にDoxygenを使っているのでDoxygenを入れておいてください(PATH通しておいてね)。 DoxygenはXML出力させてるだけなのでGraphvizは不要です。

  2. dqnをインストール

    pipでインストールしてください。:

    $ pip install dqn
    

    一応必要なSphinxやらMercurialやらをインストールしようとします。

この時点で dqn というコマンドが使えるようになると思います。 実際の作業は dqn コマンドを通して行います。

最初の使い方

インストールが終わったら早速rstを生成してみます。

  1. 前準備

    ドキュメント生成用のディレクトリを作って カレントディレクトリをドキュメント生成用のディレクトリに移動しておきます。

    $ mkdir doc
    $ cd doc
    

    出力されるファイルはすべてカレントディレクトリ配下に生成されるからです。

    ヒント

    今後この挙動は -o とかで指定できるように直したいなー。

  2. 解析/rst生成

    dqn コマンドの第一引数にソースコードツリーの最上位ディレクトリを指定してください。 その配下にあるファイルを解析しにいきます。

    $ dqn /PATH/TO/SRC/DIRECTORY
    

    Sphinxのsphinx-quickstartを実行するのでドキュメント名とかバージョンとかの 質問事項を聞いてきますのでそれなりに答えて上げてください。 それなりに答えてあげると解析を実行してsourceディレクトリにrstが作成されます。

  3. htmlの生成

    ここはSphinxの機能です。普通に make html とか make dirhtml とかやればhtmlが生成されます。

    $ make html
    

2回目以降の使い方

2回目以降も基本的には初回と同じコマンドを実行します。

1回目と2回目の違い

1回目と2回目では次のような違いがあります。

  • ドキュメント用のディレクトリを作る必要がない

    既に作ってあるので...

  • dqn コマンド実行時に sphinx-quickstart が聞いてくる内容が少ない

    しかも基本すべてデフォルト値で問題なし(enterをひたすら押せばよい)

まあ基本的にはほとんど同じです。

$ cd doc # 既に先ほど作ったディレクトリにいるなら移動する必要なし
$ dqn /PATH/TO/SRC/DIRECTORY
$ make html

気をつけなければならないのはカレントディレクトリの位置です。 カレントディレクトリは常に初回と同じ場所でなければなりません。 実行すると必要な分だけがrstに反映されています。

dqnって一体何ができるの?

dqnでは次のことが可能です。

  1. ソースコードからrstが生成できる
  2. ソースコードはPythonの他, C/C++, Javaも可能 (しかも混ざっていても良い)
  3. ソースコードが更新されても現状のrstを捨てる事なく ソースコードの更新をrst側に反映できる

3番目が特に他のドキュメンテーションビルダーと アプローチの仕方が異なっています。 sphinx.ext.autodocjavadocDoxyge 等は ソースコード内にドキュメントを埋め込むことで ドキュメントをソースコードに追従させるというアプローチを取っています。 そのためドキュメントはソースコード内に書かなければなりません。 dqn では解析の方法を工夫する事で ドキュメントをソースコードに追従させるというアプローチを取っています。 ドキュメントはソースコード内に記述する必要はありません。

dqnにはこんなことをしてもらいたかった

dqnは次のような(自分の)要求から作り始めました。

  1. Sphinxでソースコードドキュメントが書ける事

    Sphinxが好きだからSphinxで書たいんです。

  2. ソースコードにドキュメントを埋め込みたくない

    ソースコード内に日本語入れたくありませんでした。 かといってドキュメントを英語で書きつづけるのは結構しんどかったです。 autodoc は確かにいいツールなんですが自分のニーズには合いませんでした。

  3. ソースコードが更新しても今まで蓄えたrstを捨てたりマージしたりする必要がない事

    ただのrst生成ツールでは ソースコードが更新された場合に新しいrstを生成して 今までのrstをマージするということをしなくてはなりません。 しかしそんなの絶対にやりたくありませんでした。

vs dqn - 競合ツール

dqnは分類分けするとドキュメンテーションビルダーです。 そういう分類で行くと競合っていっぱいいるんですよね。

  • sphinx-apidoc
  • autodoc (Sphinx extension)
  • javadoc
  • Doxygen

どれもすごくよいものばかりです。 別に他のツールを否定するつもりもありません。 選択肢の一つとして dqn が挙るといいなと思っています。

dqnが普段やっている事

dqnはそれ自身はたいしたことはしていません。 ソースコードの解析やhtmlの生成など 主要なところはほとんど外部ツールが行ってくれています。

dqnが利用している環境はこのような環境です。

  • Doxygen

    ソースコードの解析のために利用しています。

  • Sphinx

    rstからhtmlへの生成のために利用しています。

  • Mercurial

    バージョン管理のために利用しています。

データの流れ

データの流れとしてはこんな感じです。

(source code files) -> (Doxygen)
(Doxygen) -> (dqn): XML
(dqn) -> (Sphinx): rst
(Sphinx) -> (HTML files)

dqnはDoxygenが生成したXMLからSphinx用rstとの取り次ぎをやっている感じです。 dqn自身もソースコードや生成済みrstを読み込んでいます。 更新チェックなどのためにソースコードやrstの解析を 行う必要があるからです。

解析時にやってる事

dqn コマンドを実行すると次のような順序で処理が行われます。

  1. hg init
  2. sphinx-quickstart
  3. conf.py書き換え
  4. Doxyfileの生成
  5. Doxyfileの書き換え
  6. Doxygenでソースコードを解析
  7. Doxygenの解析結果からソースコードファイルとrstの生成ファイルの対応を作成
  8. 既にrstがある場合はrstを解析
  9. 追加/更新されているかのチェック(シンボル毎)
  10. rstに書き出し (ディレクトリはなければ作る)
  11. 各ディレクトリに index.rst を作成

rstのtitleの設定やtoctreeの記述なども処理の中でおこなっているため dqn コマンド終了後には make html をやるだけです。

ハマったところとか

更新チェックはhashlib.md5でチェックサムを計算

Doxygenの解析結果には関数やクラスの定義ファイルと 定義位置(定義の先頭と最後の行数)を出力してくれます。

こんな感じ:

<memberdef>
    <location file="FUNC_LOCATIONFILE" line="3"
              bodyfile="FUNC_BODYFILE" bodystart="3" bodyend="6"/>
</memberdef>
file
ファイルパスが入ります
bodyfile
定義が書いてあるファイルパスが入ります
bodystart
定義の先頭行数
bodyend
定義の最後の行数(1行の場合は-1が入っている)

それをこんな感じで取ってきてシンボル単位のmd5のチェックサムを計算しています。 (抜粋):

import hashlib
class Location(object):
    """The source location data.
    """

    def __init__(self, path=None, line=None,
                 body=None, bodystart=None, bodyend=None,
                 ):
        self.path = None # file attribute
        self._line = None
        self.body = None # bodyfile attribute
        self._bodystart = None
        self._bodyend = None
        self.digest = None

        path = self.path
        start = self.start
        end = self.end

    def calc(self):
        path = self.path
        start = self.start
        end = self.end

        with open(path, 'rb') as fp:
            lines = fp.readlines()
            lines = lines[start:end]
            buf = ''.join(lines)
            chksum = hashlib.md5(buf)
            self.digest = chksum.hexdigest() # <- ココ
        return self.digest

XML解析で認識できない文字コードは無視する

source code解析をさせてみるとドイツ系の文字とかが結果のXMLに含まれてしまって xml.etree.ElementTree で解析できませんでした。 これは lxml で回避しました。

_parser = lxml.etree.XMLParser(recover=True)

recover=True にすると問題になりそうな文字を無視してくれます。

必要な分だけrstをtoctreeにする

dqnでは1つのソースファイルに対して1つのrstファイルを生成します。 それらをtoctreeでツリー上にしたいのですが 全部を入れてしまうと多いので必要最低限にしぼる必要がありました。 つまりやりたい事は次のような感じです。

  • index.rstには必ずtoctreeを書く
  • toctreeディレクティブが書かれたファイルと同じディレクトリにあるファイルは toctreeに取り込む
  • toctreeディレクティブが書かれたファイルと同じディレクトリにあるディレクトリは そのディレクトリの直下にあるindex.rstだけを取り込む
  • index.rstに書くtoctreeの記述は1つにしたい(そっちの方がわかりやすいから)

こんな感じにしたかった:

- package/
  - index.rst # <- ココにtoctreeが記述されているとすると...
  - a.rst            # 取り込まれる
  - b.rst            # 取り込まれる
  - src/
    - index.rst      # 取り込まれる
    - main.py.rst    # 取り込まれない
    - sub/
      - index.rst    # 取り込まれない
  - tests/
    - index.rst      # 取り込まれる
    - test_it.py.rst # 取り込まれない

こうすると index.rst がそのディレクトリの説明用のrstとして使う事ができ、 目次代わりにもなります。 しかし上記の要求をtoctreeディレクティブで行う方法が結局見つかりませんでした。 (配下のディレクトリを全部とかはできるんだけど

上記の要求をすべてみたすようにはどうしてもできなかった)

仕方がないので上記の用途用のディレクティブを作成しました。

.. dqn:tree::

結構無理矢理ですがこれでtoctreeのような挙動をさせています。

こんな感じになる:

- package/
  - index.rst # <- ココに.. dqn:treeが記述されているとすると...
  - a.rst            # 取り込まれる
  - b.rst            # 取り込まれる
  - src/
    - index.rst      # 取り込まれる
    - main.py.rst    # 取り込まれない
    - sub/
      - index.rst    # 取り込まれない
  - tests/
    - index.rst      # 取り込まれる
    - test_it.py.rst # 取り込まれない

正しくはどうやるべきだったんだろう...

まだまだできない事たくさん

細々と作り続けていますが不具合あり未作成機能ありで かなり中途半端な状態になってしまっています。 以下は今のところ課題になっている主な問題です。

  • Pythonのdecoratorをきちんと解析できない

    @propertyと@*.setterの組み合わせで定義しているものとかが特にそうなのですが メソッド名が同じになってしまうため 正しい解析結果が得られません。 2つのオブジェクトを同一と見なしてしまうためです。

  • overloadされたものを区別できない

    ファイル名とシンボル名で区別しているため overloadされたメソッドなどは同一として判断してしまうため 正しい解析結果が得られません。 pythonの @property, @*.setter decoratorと同じ事です。

  • deplicated ディレクティブを差し込めない

    削除されたものは duplicated ディレクティブでマークをつけたい。

  • サポート仕切れていないキーワードがあったりする

    特に Objective-C や Java などで 定義用の キーワード と rstのディレクティブの対応が 不十分なところがあります。

  • ソースコードが多いと解析にやたら時間がかかる

    ソースの量が多いとそれなりに時間がかかります。 性能改善とかもやりたいなー。

この他にもいろいろと問題もあるのですが 特にoverloadしているものとか悩ましくて 本当にどうしようかなーって感じです。

まとめ

まあこんな感じで細々とやってますよーってことです。

PyPIはこちら
https://pypi.python.org/pypi/dqn
repositoryはこちら
https://bitbucket.org/takesxi_sximada/dqn

明日は?

12日目の明日は r_rudi さんです。 @r_rudi さんにバトンタッチ!!

Python Advent Calendar 2013

inserted by FC2 system