nikkie-ftnextの日記

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

conf.py、お前だったのか。doctestの動きを制御するコメントがSphinxでビルドしたHTMLに表示されなかったのは

はじめに

「お前」のルビは「おまい」です1、nikkieです。

以前、Sphinx関連ネタでreST(reStructuredText)に書いたPythonの対話モードの実行例をdoctestを使ってテストしていることをアウトプットしました。

こちらに関連して、すごーく小さい(だけど私にとってはめちゃくちゃ重要な)気づきをこの記事に綴ります。

目次

doctestの動きを制御するコメントはSphinxでビルドしたHTMLに表示されない

先日の記事には以下のようにあります。

実行例のうちテストで実行したくないものは # doctest: +SKIP というコメントでスキップできます。

.. code-block:: python

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

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

このコメントはビルドしたHTMLには表示されません

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

ずっと分からなかった「なぜHTMLに表示されない?」

執筆時に必要なdoctestの実行だけが制御され、HTML(読者が見る部分)には表示されないというこの挙動は素晴らしいものです。
ですが、なぜHTMLにコメントが表示されないのかずっと分かりませんでした2

code-blockディレクティブのlexerにnoneを指定するとHTMLに表示され、pythonを指定すると表示されないという経験をした3から引っかかっているのかもしれません。
やりたいことができる設定は分かったのですが、仕組みを理解して使っている感覚がないぞ、と。

秘密はね、conf.pyで設定できる値にあったんだよ

Sphinxの「設定」のドキュメントに、このたび偶然設定値を見つけました。
その名もtrim_doctest_flags!!4
https://www.sphinx-doc.org/ja/master/usage/configuration.html#confval-trim_doctest_flags

Trueの場合、行末のdoctestフラグ ( # doctest: FLAG, ... のようなコメント) もしくは <BLANKLINE> マーカーがPythonインタラクティブセッション形式のコードブロック(例えば doctests など) で削除されます。デフォルトは True です。

trim_doctest_flagsはデフォルト値がTrue
つまりこの設定値の存在を知らなくても、デフォルトで# doctest: +SKIPのようなコメントをtrimしていたわけです!
なるほど!お前だったのか〜

trim_doctest_flagsを使う実装を覗いてみる

設定値trim_doctest_flagsTrimDoctestFlagsTransformクラスで使われていました。
https://github.com/sphinx-doc/sphinx/blob/v6.1.3/sphinx/transforms/post_transforms/code.py#L82-L125

SphinxのTransformという仕組みを使って、# doctest: +SKIPのようなコメント(以降では「doctestフラグ」)をtrimしているようです。
add_post_transformして、Transformをapplyするタイミングを設定しています5

「doctestフラグ」をtrimする実装は正規表現を使った置換re.sub)でした。
sphinx.ext.doctestに「doctestフラグ」を表す正規表現パターンが定義されています。
https://github.com/sphinx-doc/sphinx/blob/v6.1.3/sphinx/ext/doctest.py#L38

doctestopt_re = re.compile(r'#\s*doctest:.+$', re.MULTILINE)
  • まず'#'が来て
  • 空白文字が0個以上('\s*')続き
  • 'doctest:'
  • そして任意の文字1つ以上の繰り返しで終わる('.+$'

re.MULTILINEにより、行ごとに正規表現パターンとマッチするかを確認するので、例で挙げた>>> CouplableStr("ぽむ") # doctest: +SKIPという行は「'# doctest: +SKIP'」がマッチしますね。

TrimDoctestFlagsTransformでは、空文字列に置換してtrimを実現しています(strip_doctest_flagsメソッド)。
https://github.com/sphinx-doc/sphinx/blob/v6.1.3/sphinx/transforms/post_transforms/code.py#L104

        source = doctest.doctestopt_re.sub('', source)

実は残っている空白文字!

>>> CouplableStr("ぽむ") # doctest: +SKIPという行の「'# doctest: +SKIP'」を空文字列に置換すると、結果は>>> CouplableStr("ぽむ") 末尾に空白が残りますよね。
実はHTMLには(doctestフラグの前に入れた)空白はそのまま表示されているんです!(以下の画像ではドラッグして選択しました)

TrimDoctestFlagsTransformクラスのstrip_doctest_flagsメソッドにbreakpointを仕込んで確認してみました。

(Pdb) node
<literal_block: <#text: '>>> CouplableS ...'>>
(Pdb) node.rawsource
'>>> CouplableStr("ぽむ")  # doctest: +SKIP\n\'ぽむ\''
(Pdb) doctest.doctestopt_re.sub('', node.rawsource)  # 空白文字が残っている
'>>> CouplableStr("ぽむ")  \n\'ぽむ\''

空白文字込みで除くのは、経緯があったり、やってみたら分かる難しさがあったりするんですかね?
ひょっとするとプルリクチャンスかもしれません。

Pygmentsのlexer、pycon

Pythonインタラクティブセッション形式のコードブロック」のlexerを私はこれまでpythonと指定してきました。
今回trim_doctest_flagsを使う実装を見たことでpyconというlexerの存在を知りました。

Pygmentsのドキュメントには、Python console session向けのlexerの名がpyconと示されています6
また、TrimDoctestFlagsTransformクラスのテストに使うreSTファイル7でもpycon lexerが指定されていました。

📣Python Conferenceじゃないよ、Python Consoleだよ!

終わりに

reSTに書いたdoctestフラグが、SphinxでビルドしたHTMLになぜ表示されないか、その理由を見てきました。

  • 設定値trim_doctest_flagsがあり、デフォルト値がTrue
  • doctestフラグを空文字列に置換する実装
    • doctestフラグ=コメントの前に入れた空白文字は実は残っている
  • Pythonのconsoleのシンタックスハイライトにはpycon lexerを指定

trim_doctest_flagsだけでなくpyconと、これまで全然知らなかったことに気づく機会となりました。
ずっと引っかかっていた表示されない理由が分かって非常にスッキリしましたし、今後はpycon lexerを積極的に指定していこうと思います!


  1. 新美南吉 ごん狐より「ごん、お前だったのか。いつも栗をくれたのは
  2. reSTでのdoctestは2020年頃から使い出したので3年くらい、この小さな疑問を抱えていたことになります
  3. 今回実装やテストコードまで見たことで、lexerに何を指定したときにコメントが表示されなくなるか全体感が掴めました
  4. Pythonインタラクティブセッション形式のコードブロック」には、code-blockディレクティブの他に、Sphinxには、シンタックスハイライトしたコードの表示の仕方が「4つ」ある - nikkie-ftnextの日記で紹介した「doctest block」や「literal block」も含むという理解です
  5. コアイベントのうちの「13. apply post-transforms (by priority)」という理解です。 ref: https://www.sphinx-doc.org/ja/master/extdev/appapi.html#sphinx-core-events
  6. https://pygments.org/languages/
  7. https://raw.githubusercontent.com/sphinx-doc/sphinx/v6.1.3/tests/roots/test-trim_doctest_flags/index.rst