nikkie-ftnextの日記

イベントレポートや読書メモを発信

Sphinxで作るドキュメント中のPythonコード(実行例)をdoctestでテストしよう! 不安は退屈に変わるのです

はじめに

エクスペクト・パトローナム!1 nikkieです。

久しぶりのSphinxネタです。
Sphinxを使ってチュートリアルや発表資料を作る中で、Pythonの対話モードの実行例を書くことがあります。
内容を調整しているうちに、気づかず実行例を壊してしまうことがあったのですが、Python標準ライブラリのdoctestでテストできることを知ってから、実行例を壊してしまう不安から解放されました!

目次

この記事で扱うツール

Sphinxと実行例

ドキュメント変換ツールSphinx2
ルールに従って記述されたテキストファイルをHTMLやPDF(など)に変換できます。

"ルール"としてよく使われるのがreST(reStructuredText)3
テキストのままでも(=Sphinxで変換しなくても)読みやすいマークアップです。

reSTにはソースコードも表示できます4
私がよく使うのはcode-blockディレクティブ!

.. code-block:: python

    print("spam")

pythonのように言語を指定5することで、シンタックスハイライトされます!

Pythonの対話モードの実行例もこんな感じで書いています。

.. code-block:: python

    >>> print("spam")
    spam

doctest

標準ライブラリに含まれるモジュールの1つ6

doctest モジュールは、対話的 Python セッションのように見えるテキストを探し出し、セッションの内容を実行して、そこに書かれている通りに振舞うかを調べます。

「対話的 Python セッションのように見えるテキスト」とは、簡単に言えば、>>>で始まる行やそれに続く行のことです。
つまり、上でcode-blockディレクティブで書いた実行例はdoctestでテストできるんです!

コマンドは以下のようになります7

python -m doctest file [file ...]

伝えたいこと

nikkieはSphinxをヘビーユースしており、reSTを書いて

に変換しているのですが、doctestを組み合わせることで、これらすべてで実行例をテストできます。
うっかり壊していたときはdoctestが失敗するので、すぐ気付いて修正できます!
これが私にはすっごく便利なんです。

nikkieのSphinx × doctest使用例

PyCon JP 2022「Pythonとアスタリスク」のスライドから紹介します。

通常ユースケース

https://raw.githubusercontent.com/ftnext/2022_slides/f0c4cf1aec1e88ca48b58ca2cf69a3ab15a75b70/source/pyconjp/as_unpack_operator.rst.txt より

.. code-block:: python

    >>> [*(1, 2), 3]
    [1, 2, 3]

reSTのコメントを使って前処理(や後処理)を追加する

https://raw.githubusercontent.com/ftnext/2022_slides/f0c4cf1aec1e88ca48b58ca2cf69a3ab15a75b70/source/pyconjp/in_keyword_only_parameters.rst.txt より

.. doctestを通すための変数定義
    >>> weight_diff, time_diff = 0.5, 3

.. code-block:: python

    >>> def flow_rate(weight_diff, time_diff, period=1, units_per_kg=1):
    ...     ...

    >>> flow_rate(weight_diff, time_diff, 3600, 2.2)

reSTでは..で始まった行は空行が入るまでコメントアウトされます。
これを利用して、紙面に出したくはないけれどdoctestを通すために必要なコードを実行例として続けます。

上記のreSTに対してdoctestを実行すると、flow_rate関数が呼び出せることが確認できます。
変数weight_difftime_diffが指す値がなにかはここではあまり重要ではない8ので、スライドに出さないためにコメントとしています。

私がreSTのコメントを使って前処理・後処理を追加するのは以下のようなシーンです。

  • 実行例を通すのに必要な前処理や通った後の後処理
    • 例:前処理として空ファイルを作る。実行例はそれに書き込み。後処理でそのファイルを削除
  • 実行例として紹介する関数やクラスの呼び出しの前に、reSTのコメントを使って関数やクラスを定義する
    • 私はliteralincludeディレクティブ9も好んで使うため、実行例に含めたコードだけをコメントとしてあらかじめ定義しておくことが多い

doctestを実行するときに、ある実行例をスキップも可能

実行例のうちテストで実行したくないものは # doctest: +SKIP というコメントでスキップできます。
https://raw.githubusercontent.com/ftnext/2022_slides/f0c4cf1aec1e88ca48b58ca2cf69a3ab15a75b70/source/pyconjp/as_binary_arithmetic_operator.rst.txt より

.. code-block:: python

    >>> CouplableStr("ぽむ")  # doctest: +SKIP
    'ぽむ'
    >>> CouplableStr("ゆう") * CouplableStr("ぽむ")  # doctest: +SKIP
    'ゆうぽむ'

code-blockディレクティブにlexerとしてpythonを指定すると、このコメントは紙面に出ません10

上のコードをスキップした理由としては、ソースコードのファイルのdocstringでもdoctestで検証できているので、「発表資料のreSTにわざわざクラス定義をコメントとして持ってくるまでもないかな」と判断してです。

このコメントの書式は https://docs.python.org/ja/3/library/doctest.html#directives で説明されています。
doctestの「ディレクティブ」という概念で、SKIPオプション11を(+で)onにしています。

doctestで不安は退屈に変わります!

ケント・ベックの『テスト駆動開発』には以下の言葉があります12

テストは不安を退屈に変える賢者の石だ。(Kindle の位置No.3198)

reSTに書いた実行例、doctestを使うまでは「気づかぬうちに壊しているんじゃないか」と不安でした。
doctestを使い始めると、その不安はなくなり、退屈に変わっています。
「コードはPythonこのバージョンで動作確認しています」という検証も兼ねるので便利です。
doctestのおかげで、私は本当に助かっています!

執筆中は頻繁に手動で実行しますが、CIでdoctestを実行するのもオススメです13

終わりに

reSTに書いた実行例をdoctestでテストする方法を紹介しました。
これは本当に便利で、執筆やプレゼン準備にreSTを使っている方は、実行例が含まれていたらぜひ一度試していただきたいです。
私のアウトプットの大部分はdoctestに支えてもらっています。

今回はcode-blockディレクティブに実行例を書くケースを紹介しましたが、Sphinxの拡張にもsphinx.ext.doctestがあるみたいです。
sphinx.ext.doctest -- ドキュメント内の簡易テスト — Sphinx documentation
これは今後の素振りトピックですね、もっと助けてもらえるかも(わくわく)



  1. エクスペクト・パトローナム|魔法ワールド|ワーナー・ブラザース
  2. https://pypi.org/project/Sphinx/
  3. https://www.sphinx-doc.org/ja/master/usage/restructuredtext/index.html
  4. https://www.sphinx-doc.org/ja/master/usage/restructuredtext/directives.html#showing-code-examples ここを参照したところcode-blockディレクティブ以外にも方法があると、執筆を機に知りました
  5. https://www.sphinx-doc.org/ja/master/usage/restructuredtext/directives.html#directive-code-block にPygmentsがサポートするlexerを指定するとあります。lexerの一覧:https://pygments.org/docs/lexers/
  6. Python実践レシピ』でも紹介されていますね(16.1)
  7. 詳しくは python -m doctest -h やドキュメントで確認してください
  8. period引数やunits_per_kg引数の話をしています
  9. https://www.sphinx-doc.org/ja/master/usage/restructuredtext/directives.html#directive-literalinclude
  10. 何によってコメントが除かれているのか仕組みはよく分かっていないのですが、大変重宝しています
  11. https://docs.python.org/ja/3/library/doctest.html#doctest.SKIPSKIP フラグは一時的に実行例を"コメントアウト"するのにも使えます。
  12. みんなのPython勉強会#88のやっとむさんによる「手軽なpytestでテストを活用しよう!」、テストコードに関係する知識が結び付き、刺激的でした #stapy - nikkie-ftnextの日記 でも紹介しました
  13. GitHub Actionsの例です:https://github.com/ftnext/2022_slides/blob/f0c4cf1aec1e88ca48b58ca2cf69a3ab15a75b70/.github/workflows/doctests.yml