はじめに
エクスペクト・パトローナム!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を書いて
- 技術書(同人誌)
- チュートリアル
- 発表資料(
sphinx-revealjs
)
に変換しているのですが、doctestを組み合わせることで、これらすべてで実行例をテストできます。
うっかり壊していたときはdoctestが失敗するので、すぐ気付いて修正できます!
これが私にはすっごく便利なんです。
nikkieのSphinx × doctest使用例
PyCon JP 2022「Pythonとアスタリスク」のスライドから紹介します。
通常ユースケース
.. code-block:: python >>> [*(1, 2), 3] [1, 2, 3]
reSTのコメントを使って前処理(や後処理)を追加する
.. 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_diff
とtime_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
これは今後の素振りトピックですね、もっと助けてもらえるかも(わくわく)
- エクスペクト・パトローナム|魔法ワールド|ワーナー・ブラザース↩
- https://pypi.org/project/Sphinx/↩
- https://www.sphinx-doc.org/ja/master/usage/restructuredtext/index.html↩
-
https://www.sphinx-doc.org/ja/master/usage/restructuredtext/directives.html#showing-code-examples ここを参照したところ
code-block
ディレクティブ以外にも方法があると、執筆を機に知りました↩ - https://www.sphinx-doc.org/ja/master/usage/restructuredtext/directives.html#directive-code-block にPygmentsがサポートするlexerを指定するとあります。lexerの一覧:https://pygments.org/docs/lexers/↩
- 『Python実践レシピ』でも紹介されていますね(16.1)↩
-
詳しくは
python -m doctest -h
やドキュメントで確認してください↩ -
period
引数やunits_per_kg
引数の話をしています↩ - https://www.sphinx-doc.org/ja/master/usage/restructuredtext/directives.html#directive-literalinclude↩
- 何によってコメントが除かれているのか仕組みはよく分かっていないのですが、大変重宝しています↩
- https://docs.python.org/ja/3/library/doctest.html#doctest.SKIP 「SKIP フラグは一時的に実行例を"コメントアウト"するのにも使えます。」↩
- みんなのPython勉強会#88のやっとむさんによる「手軽なpytestでテストを活用しよう!」、テストコードに関係する知識が結び付き、刺激的でした #stapy - nikkie-ftnextの日記 でも紹介しました↩
- GitHub Actionsの例です:https://github.com/ftnext/2022_slides/blob/f0c4cf1aec1e88ca48b58ca2cf69a3ab15a75b70/.github/workflows/doctests.yml↩