はじめに
「お前」のルビは「おまい」です1、nikkieです。
以前、Sphinx関連ネタでreST(reStructuredText)に書いたPythonの対話モードの実行例をdoctestを使ってテストしていることをアウトプットしました。
こちらに関連して、すごーく小さい(だけど私にとってはめちゃくちゃ重要な)気づきをこの記事に綴ります。
目次
- はじめに
- 目次
- doctestの動きを制御するコメントはSphinxでビルドしたHTMLに表示されない
- ずっと分からなかった「なぜHTMLに表示されない?」
- 秘密はね、conf.pyで設定できる値にあったんだよ
- trim_doctest_flagsを使う実装を覗いてみる
- Pygmentsのlexer、pycon
- 終わりに
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_flags
はTrimDoctestFlagsTransform
クラスで使われていました。
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を積極的に指定していこうと思います!
- 新美南吉 ごん狐より「ごん、お前だったのか。いつも栗をくれたのは」↩
- reSTでのdoctestは2020年頃から使い出したので3年くらい、この小さな疑問を抱えていたことになります↩
- 今回実装やテストコードまで見たことで、lexerに何を指定したときにコメントが表示されなくなるか全体感が掴めました↩
-
「Pythonのインタラクティブセッション形式のコードブロック」には、
code-block
ディレクティブの他に、Sphinxには、シンタックスハイライトしたコードの表示の仕方が「4つ」ある - nikkie-ftnextの日記で紹介した「doctest block」や「literal block」も含むという理解です↩ - コアイベントのうちの「13. apply post-transforms (by priority)」という理解です。 ref: https://www.sphinx-doc.org/ja/master/extdev/appapi.html#sphinx-core-events↩
- https://pygments.org/languages/↩
- https://raw.githubusercontent.com/sphinx-doc/sphinx/v6.1.3/tests/roots/test-trim_doctest_flags/index.rst↩