【Sphinx】Pythonドキュメントをdocstringから良い感じに作成する

Pythonのドキュメント・リファレンスをdocstringの内容から良い感じに生成してくれる、Sphinxの簡単な使い方を紹介します!

動作環境

Python 3.7.4
Sphinx 1.7.6

完成イメージ

ドキュメントのデザインはこんな感じです。
画像では1ページしかありませんが、モジュールが複数あればモジュールごとにページが作成され、左のリンク集から辿ることができるようになります!

f:id:hesma2:20210403171837p:plain
https://qiita.com/futakuchi0117/items/4d3997c1ca1323259844 より

手順

1. Sphinxをインストール

pip install sphinx

2. プロジェクト作成

mkdir docs
sphinx-quickstart docs

対話形式でたくさんの質問が来るが、全てそのままEnterでOKです(あとで設定ファイルを編集します)
ただし、 Project nameAuthor name(s) は入力が必要になります。

> Project name: mi-restapi
> Author name(s): kizuki-engineer

3. 設定の編集

conf.py を編集
# 以下のコメントを外し、conf.pyから見たルートディレクトリへのパスを設定します(ここでは../)
- # import os
- # import sys
- # sys.path.insert(0, os.path.abspath('.'))
+ import os
+ import sys
+ sys.path.insert(0, os.path.abspath('../'))
conf.py拡張機能を追加
- extensions = []
+ extensions = [
+   "sphinx.ext.autodoc",
+   "sphinx.ext.napoleon",
+ ]

設定した拡張機能について

autodocの設定を拡張して、プライベートメソッドもドキュメントに含めるように設定します。

conf.py に以下を追加

+ autodoc_default_flags = [
+   'members',
+   'private-members'
+ ]

4. ドキュメント作成

pyファイルからrstファイルを生成

今回はモジュールごとにドキュメントを生成したいので、 -e を指定します。

-E, --no-headings Do not create headings for the modules/packages. This is useful, for example, when docstrings already contain headings.

sphinx-apidoc -e -f -o ./docs .

その他のオプションは以下のリンクを参照ください。

www.sphinx-doc.org

トップページの設定をするため、 index.rst を編集

今回は modules.rst に全てのモジュールがまとまっていたため、modulesを設定しました。

.. toctree::
  :maxdepth: 2
  :caption: Contents:

+  modules

ビルドを実行します。

sphinx-build ./docs/ ./docs/_build/

www.sphinx-doc.org

成功すると、./docs/_build/ の下にindex.htmlが生成されます!
質素なページなので、スタイルを変更していきます。

f:id:hesma2:20210403173720p:plain
https://qiita.com/futakuchi0117/items/4d3997c1ca1323259844 より

スタイルの変更

sphinx_rtd_themeをインストール、適用していきます。

pip install sphinx_rtd_theme

conf.py を編集します。

- html_theme = 'alabaster'
+ html_theme = 'sphinx_rtd_theme'

再度ビルドすると、最初の完成イメージのようなデザインのページが出来上がります!

sphinx-build ./docs/ ./docs/_build/

napoleonのエラー

docstringの書き方によっては、napleonのパース時にwarningが発生してしまいます。

例えば以下のような感じのエラーです。

WARNING: Definition list ends without a blank line; unexpected unindent.

以降では、実際に筆者がハマったエラーとその修正方法を紹介していきます。(例に挙げてるソースコードは雑な再現です)

また、筆者のチームではGoogleスタイルのDocstringを採用しています。

【エラーケース1】dictの書き方が不正

Before

def get_user_detail(user_id):
    """
    ユーザーの詳細情報を取得する

    Returns: {
        "ユーザーID": str,
        "ユーザー名": str,
        "電話番号": str,
        "メールアドレス": str,
        "住所": str,
    }
    """

After

def get_user_detail(user_id):
    """
    ユーザーの詳細情報を取得する

    Returns:
        dict: ユーザーの詳細情報::

            {
                "ユーザーID": str,
                "ユーザー名": str,
                "電話番号": str,
                "メールアドレス": str,
                "住所": str,
            }
    """

【エラーケース2】 * の使い方が不正

展開で使う * をdocstringで使ってしまうと、napoleonはそれを強調と認識してしまいます。
閉じる * がないのでwarningを出してしまう、というわけです。

before

def create_user_df(args):
    """
    hogehoge

    Args:
        user_df: [*args, "hogehoge"]
    """

after

def create_user_df(args):
    """
    hogehoge

    Args:
        user_df: 以下のカラムを持つdf::

            [args, "hogehoge"]
    """

【エラーケース3】不正なセクション

napoleonがサポートしてないセクションを設定してしまうとNG。

f:id:hesma2:20210403175632p:plain

before

def get_sales_data(store_id, start_date, end_date):
    """
    期間内の店舗売り上げを取得する

    columns:
        日付: date
        売上: int
        出費: int
        来客数: int
        平均単価: float

    """

after

def get_sales_data(store_id, start_date, end_date):
    """
    期間内の店舗売り上げを取得する

    Returns:
        dict: 店舗売り上げのデータ::

            日付: date
            売上: int
            出費: int
            来客数: int
            平均単価: float
    
    """

参考サイト

qiita.com

bonbonbe.hatenablog.com