SphinxでPythonプログラムをHTMLドキュメント化する

# 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```に以下の行を追加する。

```python: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からのドキュメントの取り込み](https://www.sphinx-doc.org/ja/master/usage/extensions/autodoc.html)

ちなみに`language = en`のままで使用した。`ja`にするとレイアウトがしっくりこなかったから。

---
# 7. reST表記

採用したdocstringのreST表記について示す。

---
## 7-1. パッケージの初期化ファイル (\_\_init\_\_.py)

パッケージの説明はここに記述する。

```python:__init__.py
"""パッケージタイトル

| 詳細情報はここに書く。
| 必ずパッケージタイトルから１行開けておくこと。
| ←ラインブロックを使うと、改行状態が維持される。

:author: このプログラムの担当者名
:copyright: 著作権表示
"""
```

上記の```:author:```や```:copyright:```のようにコロンで挟まれた文字列はフィールドと呼ばれ、独自に定義できる。HTML出力すると以下のように表示される。

![package_header.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110430/2d2a520b-9263-0415-91ce-a18fb4758e9e.png)

---
## 7-2. ファイルヘッダ

ファイルヘッダもパッケージと同じ。

```python:Foo.py
"""モジュールタイトル

| 詳細情報はここに書く。
| 必ずモジュールタイトルから１行開けておくこと。

:note: 注意事項があればここに記載する。

:author: このプログラムの担当者名
:copyright: 著作権表示
"""
```

上記では`:note:`フィールドを追加した例を示している。
`:note:`フィールドは、すべてのdocstringで注意事項の記載として必要な箇所で使用する。

---
## 7-3. クラスヘッダ

クラスヘッダの記述を示す。

```python
class Foo(object):
    """クラスの要約

    | クラスの詳細説明。
    | 必ずクラスの要約から１行開けておくこと。
    | ←ラインブロックも使用できる。
    """
```

---
## 7-4. メソッドヘッダ

メソッドヘッダの記述を示す。

```python
class Foo(object):

    def method(self, name: str, val: int) -> str:
        '''メソッドの要約

        | クラスの詳細説明。
        | 必ずクラスの要約から１行開けておくこと。
        | ←ラインブロックも使用できる。

        :param name: 名前   ← 型名を省略すると型ヒントを参照
        :param int val: 値  ← 直接、型名を書くことができる
        :returns: （戻り値の説明）
        :rtype str:         ← 戻り値の型、型ヒントがあれば省略可
        '''
        ...
```

型ヒントの書き方として、戻り値のないメソッドは```-> None```としておく。

2020/8/1：補足
たまたまSudachiPyのソースコード見てたら、型ヒントはpublicなメソッドしか使ってなかった。メンテが大変なら、そういう割り切りもありか。

---
## 7-5. 列挙クラスのメンバ

列挙クラスのメンバの説明は、以下のように記述する。

```python
class State(enum.Enum):
    """ステート
    """

    UNINIT = enum.auto()  #: 未初期化
    READY = enum.auto()   #: 待機
    BUSY = enum.auto()    #: 処理中
```

---
## 7-6. 行の折り返し

記述の行が長くなった場合、```\```あるいは```¥```で継続できる。

```python
"""モジュール要約

モジュールの説明。¥
この行は上の行から改行なしで表示される。
"""
```

---
# 8. 型ヒント

ドキュメント化が目的で型ヒントを導入したら、型ヒントで色々とはまることになった。

---
## 8-1. 自クラスの参照 (Forward Rreferences)

自クラスをそのまま型ヒントに使うと、エラーになる。

```python
class Foo(object):

    def method(self, obj: Foo) -> None:
        #                 ↑ FooはUndefinedになる
        pass
```

以下のように、文字列とすればエラーを回避できる。

```python
class Foo(object):

    def method(self, obj: 'Foo') -> None:
        #                  ↑ OK!
        pass
```

ちゃんとPEP-484にも書いてありました。

[PEP-484: Forward references](https://www.python.org/dev/peps/pep-0484/#forward-references)

---
## 8-2. ListとDict (Type Aliaces)

引数や戻り値の型で```list```や```dict```を指定した場合、その要素の型は記載できない。そこで、typingパッケージの```List```と```Dict```を使用して記載する。


```python
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](https://www.python.org/dev/peps/pep-0484/#forward-references)

---
## 8-3. 循環importの回避と妥協 (Circular import)

モジュール間でimportしあうと、相互import状態なので循環importでエラーとなり、`ImportError: Cannot import name ...`とエラーメッセージが表示される。
こういう場合は基本的に「構造を変えてください」のスタンスだけど、どうしても構造が変えられない場合は次のような妥協案をとった。

(1) 型ヒントを諦めて、docstringに型名を書く
(2) Forward refencesのように型名を文字列にする

後で(2)の対応ではflake8を通すと`F821 undefined name`のチェックにひっかかることが分かり、実際にはすべて(1)で対応した。

---
# Special Thanks

* [みんなのPython勉強会#58 Python製ドキュメント生成ツールSphinx丸わかり](https://startpython.connpass.com/event/175451/)
    * 小宮 健さん: 「[Sphinx を使って Python プログラムのドキュメントを書こう](https://docs.google.com/presentation/d/1mLw2ZHeyl_9jWAwxE3Hx7stY5msCd-jyl22PkKRCdqg/edit#slide=id.g885309dee8_0_166)」
* [みんなのPython勉強会#43](https://startpython.connpass.com/event/112788/)
    * cocodripsさん：「[チームメイトと未来の自分のために docstringを書こう](https://www.slideshare.net/cocodrips/docstring)」