1. はじめに
あるプロジェクトで開発したPythonプログラムを、新たな別のプロジェクトで保守しながら機能拡張を行うことになった。引き継ぎで、内部文書としてSphinxで生成したHTMLドキュメントを残すことにした。過去にDoxygenでHTML化した経験もあり、ソースコードには同程度の情報量をコメントとして埋め込んでいたので、それらをSphinxに対応すればいい。という背景。
ようは、詳細設計書を自動生成する。
Sphinxは初めて使用するので、その作業内容を記録しておく。
なお、docstringのフィールドについては独自のルールを採用している。
2. 使用環境
以下の通り。
- Windows 10 Pro
- Python 3.7.4
- Sphinx 3.1.2
3. docstringフォーマットの決定
Google形式、numpy形式とあるが、reStructureText(通称reST)を採用した。理由は以下の通り。
- 表記が短い
- 標準なので追加のパッケージが不要
- 型ヒントに対応している
- 型ヒントを採用した場合、型ヒントとdocstringで同じものを書くのを避けたい
- 逆に、型ヒントを書いておけばdocstringの型情報は書かなくていい
4. Sphinxのインストール
Windows環境の手順だけど、macOSもLinuxもそれほど大きく変わらないと思われる。
pip install Sphinx sphinx-autodoc-typehints
pip install sphinx_rtd_theme
sphinx_rtd_theme
は、HTMLドキュメントのテーマに使用する。
デフォルトだとちょっと見づらい。
5. HTMLドキュメントの生成
5-1. 対象となるファイル構成例
以下のファイル構成を想定する。
.
├── src/ # ソースコード一式はこの下
│ ├── __init__.py
│ ├── Foo.py
│ ├── Bar.py
│ └── Hoge/
│ ├── __init__.py
│ └── Hoge.py
│
└── doc/ # ここをSphinxの作業ディレクトリとする
- ソースコードは
src/
にすべて保存 - ソースコードはサブディレクトリ(サブパッケージ)を持つ
- Sphinxの生成ファイルは
doc/
に出力する
5-2. ドキュメント生成手順
ターミナルから以下のコマンドを実行する。
sphinx-apidoc -F -a -o .\doc .\src
cd doc
make html
.\_build\html\index.html
make html
でHTMLドキュメントを生成する。ソースコードのdocstringを変更した場合、このコマンドだけ再実行すれば良い。
最後のindex.htmlを指定したコマンドで、デフォルトブラウザが起動し、HTMLドキュメントが開く。
5-3. 注意すべきこと
Sphinxは対象となるPythonのコードを処理系に渡しているようで、対象コードにエラーがあるとmake html
でエラーとなり、ドキュメントが生成されない。あらかじめ動作確認は終えておくこと。
そのため、例えば対象コードがWindowsにしかないパッケージを使用していた場合、ダミーのパッケージを自作するような対応をしない限り、macOSやLinux環境でドキュメント化できない。
6. Sphinxの設定を変える
デフォルトのままだとちょっとよろしくないので、設定ファイルdoc/conf.py
に以下の行を追加する。
html_theme = 'sphinx_rtd_theme' # 表示テーマを指定
autodoc_typehints = 'description' # 型ヒントを有効
autoclass_content = 'both' # __init__()も出力
autodoc_default_options = {'private-members': True, # プライベートメソッドも出力
'show-inheritance': True} # 継承を表示
上記でやっていることは以下の通り。
- 表示テーマをデフォルトから指定テーマに変更
- 型ヒントを使用するように設定
- デフォルトでは__init__()が非表示なので、表示設定にする
- デフォルトではプライベートメソッドが非表示なので、表示設定にする
-
_method1()
とか__method2()
のように、先頭が_
で始まるメソッドが対象
-
- デフォルトでは継承情報が非表示なので、表示設定にする
設定を変えたらmake html
を実行する。
設定はこのページを参考にした。
sphinx.ext.autodoc -- docstringからのドキュメントの取り込み
ちなみにlanguage = en
のままで使用した。ja
にするとレイアウトがしっくりこなかったから。
7. reST表記
採用したdocstringのreST表記について示す。
7-1. パッケージの初期化ファイル (__init__.py)
パッケージの説明はここに記述する。
"""パッケージタイトル
| 詳細情報はここに書く。
| 必ずパッケージタイトルから1行開けておくこと。
| ←ラインブロックを使うと、改行状態が維持される。
:author: このプログラムの担当者名
:copyright: 著作権表示
"""
上記の:author:
や:copyright:
のようにコロンで挟まれた文字列はフィールドと呼ばれ、独自に定義できる。HTML出力すると以下のように表示される。
7-2. ファイルヘッダ
ファイルヘッダもパッケージと同じ。
"""モジュールタイトル
| 詳細情報はここに書く。
| 必ずモジュールタイトルから1行開けておくこと。
:note: 注意事項があればここに記載する。
:author: このプログラムの担当者名
:copyright: 著作権表示
"""
上記では:note:
フィールドを追加した例を示している。
:note:
フィールドは、すべてのdocstringで注意事項の記載として必要な箇所で使用する。
7-3. クラスヘッダ
クラスヘッダの記述を示す。
class Foo(object):
"""クラスの要約
| クラスの詳細説明。
| 必ずクラスの要約から1行開けておくこと。
| ←ラインブロックも使用できる。
"""
7-4. メソッドヘッダ
メソッドヘッダの記述を示す。
class Foo(object):
def method(self, name: str, val: int) -> str:
'''メソッドの要約
| クラスの詳細説明。
| 必ずクラスの要約から1行開けておくこと。
| ←ラインブロックも使用できる。
:param name: 名前 ← 型名を省略すると型ヒントを参照
:param int val: 値 ← 直接、型名を書くことができる
:returns: (戻り値の説明)
:rtype str: ← 戻り値の型、型ヒントがあれば省略可
'''
...
型ヒントの書き方として、戻り値のないメソッドは-> None
としておく。
2020/8/1:補足
たまたまSudachiPyのソースコード見てたら、型ヒントはpublicなメソッドしか使ってなかった。メンテが大変なら、そういう割り切りもありか。
7-5. 列挙クラスのメンバ
列挙クラスのメンバの説明は、以下のように記述する。
class State(enum.Enum):
"""ステート
"""
UNINIT = enum.auto() #: 未初期化
READY = enum.auto() #: 待機
BUSY = enum.auto() #: 処理中
7-6. 行の折り返し
記述の行が長くなった場合、\
あるいは¥
で継続できる。
"""モジュール要約
モジュールの説明。¥
この行は上の行から改行なしで表示される。
"""
8. 型ヒント
ドキュメント化が目的で型ヒントを導入したら、型ヒントで色々とはまることになった。
8-1. 自クラスの参照 (Forward Rreferences)
自クラスをそのまま型ヒントに使うと、エラーになる。
class Foo(object):
def method(self, obj: Foo) -> None:
# ↑ FooはUndefinedになる
pass
以下のように、文字列とすればエラーを回避できる。
class Foo(object):
def method(self, obj: 'Foo') -> None:
# ↑ OK!
pass
ちゃんとPEP-484にも書いてありました。
8-2. ListとDict (Type Aliaces)
引数や戻り値の型でlist
やdict
を指定した場合、その要素の型は記載できない。そこで、typingパッケージのList
とDict
を使用して記載する。
from typing import List
from typing import Dict
import Bar.Bar
class Foo(object):
def method(self, bars: List[Bar],
map: Dict[str, int]) -> List[str]:
...
Foo.method()の仕様は以下の通り。
- 引数
bars
はクラスBarのオブジェクトのリスト - 引数
map
はキーが文字列で値が整数の辞書 - 戻り値は文字列のリスト
詳細はこちら。
PEP-484: Type Aliases
8-3. 循環importの回避と妥協 (Circular import)
モジュール間でimportしあうと、相互import状態なので循環importでエラーとなり、ImportError: Cannot import name ...
とエラーメッセージが表示される。
こういう場合は基本的に「構造を変えてください」のスタンスだけど、どうしても構造が変えられない場合は次のような妥協案をとった。
(1) 型ヒントを諦めて、docstringに型名を書く
(2) Forward refencesのように型名を文字列にする
後で(2)の対応ではflake8を通すとF821 undefined name
のチェックにひっかかることが分かり、実際にはすべて(1)で対応した。