Sphinxを 通して 考える、 「拡張」の 仕方
- author:
Kazuya Takei / @attakei
- date:
2022/10/14
- event:
PyCon JP 2022
- hashtags:
は じめに
お前誰よ
Kazuya Takei
attakei (Twitter, GitHub, etc)
株式会社ニジボックス
趣味系Pythonista <= こっち
ライブラリ・拡張系を作りがち
Sphinxでプレゼンテーションしたがる人

株式会社ニジボックス

ニジボックス は「Grow all」をミッションに、企業やサービスの成長に向き合い続けるリクルートグループのデザイン会社。
お客様のビジネスの成長をUI UXのデザインプロセスから開発・運用・改善までワンストップでサポート。
興味が湧いた・問い合わせしたくなったら、 https://www.nijibox.jp へ。
株式会社ニジボックス
POSTD
エンジニアに向けたキュレーションメディア
海外のテック記事を日本語に翻訳して配信

今日話す予定の こと
Sphinxの概要
Sphinx拡張の概要
Sphinx拡張の実装アプローチ
And more
これらを、「発表者の体験を踏まえて」話します。
Sphinx イントロダクション
アンケート
Sphinx、知ってますか?
Sphinx、使ってますか?
Sphinxとは 何か
Python製のドキュメンテーションビルダー
ソーステキストを束ねて「ドキュメント」として扱う
「ドキュメント」からHTML・PDFなどを生成する
メインソースはreStructuredTextし、内部でdocutilsを利用
Sphinxとは 何か
Python製のドキュメンテーションビルダー
Sphinxとは 何か
reStructuredText
軽量マークアップ言語
「ディレクティブ」の概念(表現力・拡張力の基盤)
Document title
==============
概要
----
.. toctree::
overview
installation
Sphinxとは 何か
アウトプット形式様々
HTML
PDF
EPUB
man page
Sphinxで 出来ている サイト
Python関連
Python本体
Sphinx
Ansible
様々なPythonパッケージ
Sphinxで 出来ている サイト
Python以外
(このスライド)
Sphinxで 書かれた 書籍
(書籍執筆のどこかの工程でSphinxを使っているもの)
おさらい :Sphinx単体で 出来る こと
reStructuredTextでドキュメントを管理できる
HTMLを生成できる・テーマを切り替えられる
PDFを生成できる(要LaTex)
ちょっと物足りない?
ありがちな 「物足りなさ」
Markdownでドキュメント管理したい
動画やツイートなどを、なるべく楽に埋め込みたい
HTMLでの折り返しが気に食わないので、いい感じに改行したい
Sphinxは「拡張」が出来るようになっている
Sphinx拡張 イントロダクション
Sphinx拡張とは 何か
「Sphinxの機能を拡張」するためのPythonライブラリ。
モジュールでも良いし、パッケージでも良い
ローカル管理でも平気(インポートさえできればOK)
(ちょっと雑だけど)
conf.py
上で実装してもいい
Sphinx拡張とは 何か
拡張の使用方法。
# Optional
import sys
sys.path.append(PATH_TO_LOCAL_MODULE)
extensions = [
"my_extensoin",
]
extensions
にライブラリ名を追加するだけ必要に応じて
sys.path
を編集体裁が整っているなら、よしなに呼ばれる
Sphinx拡張とは 何か
最低限「体裁が整っている」コード
def setup(app):
print("Hello world")
conf.py
に記述すると、「ビルド時にHello World
とコンソール出力する」
Sphinx拡張で 実現可能な こと
ディレクティブの登録 /既存ディレクティブへの処理を変更 /読み取り可能なフォーマットを追加 /新しい出力形式(ビルダー)の追加 /出力処理時にデータ加工 /その他・Sphinxのビルド処理時の各種処理追加
Sphinx拡張で 実現可能な こと
ざっくりとした分類
- 「入力」を拡張する(フォーマットを増やす、文法を増やす)
- 「出力」を拡張する(フォーマットを増やす、自作テーマ)
「内部」で何かする
(複合系)
拡張の 例
- Sphinx本体にバンドルされているもの
- サードパーティ製
Sphinx拡張 ショーケース
この後に例示用に出てくるSphinx拡張の紹介
sphinxcontrib-oembed
oEmbedを使ったコンテンツ埋め込みをサポート。
URLの指定だけでツイートや動画の埋め込みが可能になる。
sphinx-revealjs v2.2.0 is released.
— kAZUYA tAKEI (@attakei) September 30, 2022
Thank you for usings, feedbacks and collaborations!
See PyPI: https://t.co/TCCYhLYHWl
See GitHub: https://t.co/F59O49dwnf
sphinxcontrib-oembed
.. raw:: html
<blockquote class="twitter-tweet">
<p lang="en" dir="ltr">
sphinx-revealjs v2.2.0 is released.<br>Thank you for usings, feedbacks and collaborations!<br>See PyPI: <a href="https://t.co/TCCYhLYHWl">https://t.co/TCCYhLYHWl</a><br>See GitHub: <a href="https://t.co/F59O49dwnf">https://t.co/F59O49dwnf</a>
</p>
— kAZUYA tAKEI (@attakei)<a href="https://twitter.com/attakei/status/1575887211962290176?ref_src=twsrc%5Etfw">September 30, 2022</a>
</blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
↓
.. oembed:: https://twitter.com/attakei/status/1575887211962290176
sphinxcontrib-oembed
分類:入力と出力に関する拡張
- ディレクティブ(とノード)の新規作成が必要
- ソース読み込み時にHTTP通信する=> ディレクティブの実装がちょっと複雑
とはいえ、 ディレクティブとノードで完結する
sphinxcontrib-budoux
BudouXを使って、日本語文の改行をいい感じに出来るようにする拡張。


sphinxcontrib-budoux
分類:出力に関する拡張
Sphinxメイン処理内で行われた「ソースをもとにしたHTML」を再加工する
出力に 割り込んで の加工が必要
Sphinx拡張の 実装アプローチ
基本実装〜編
再掲: 最低限 「体裁が 整っている」コード
def setup(app):
print("Hello world")
Sphinx拡張の 実態
大雑把に書くと、以下のような要素たちの集まり
追加の入出力を定義するための関数/クラス群
イベントハンドラ用の関数群
★Sphinx本体から呼び出される
setup
関数補助処理
setup を 制する ものは Sphinxを 制す?
setup()
の役割
- 設定項目の宣言=>
app.add_config_value
- ディレクティブ/ビルダー等の登録=>
app.add_builder
他 - イベントハンドラの登録=>
app.connect
...これらは、いずれも「Sphinxのメイン処理開始までに完遂しないと困ること」
setup を 制する ものは Sphinxを 制す?
setup()
の役割
Sphinxから呼ばれるのはsetupのみ
setup を 制する ものは Sphinxを 制す?
setup()
の役割
メイン処理以降は、登録済みのものを扱うだけ
setup関数/設定項目の 宣言
def setup(app):
app.add_config_value(
"config_name", # 名前
[], # 初期値
)
拡張の「動作」を設定させるための項目を宣言。
Sphinx全体で重複しないように注意が必要。
例)
sphinxcontrib-budoux / budoux_targets
ディレクティブ等の 登録
入力(出力)に関する拡張をしたいときに必要となるもの。
ディレクティブ
ノード
ロール
...他にもあれこれ
ディレクティブ等の 登録
.. oembed:: https://twitter.com/attakei/status/1575887211962290176
.. oembed:: https://www.youtube.com/watch?v=Jn2zvfDhU0w
Sphinx本体には無いディレクティブなので、自作&登録が必要。
ディレクティブ等の 登録
from sphinx.directives import SphinxDirective
class OembedDirective(SphinxDirective):
...
def run(self):
# 略
node = oembed()
...
# 略 - nodeの属性に各種データを引き渡す
...
return [node] # docutilsのノードを持つリストを返す
def setup(app):
app.add_directive("oembed", OembedDirective)
ディレクティブ等の 登録
ディレクティブを用意するなら、まずノードも必要。
ノードは出力にも関わるので、出力の実装もセット。
ディレクティブ等の 登録
from docutils import nodes
class oembed(nodes.General):
# 大抵の場合は、ディレクティブ側で処理をするので
# 何もしないことが多い
pass
class visit_oembed_node(self, node):
if "content" in node and "html" in node["content"]:
self.body.append(node["content"]["html"])
class depart_oembed_node(self, node):
pass
ディレクティブ等の 登録
def setup(app):
app.add_node(
oembed,
# ビルダー種別ごとに、どんな処理をさせたいか指定する
html=(visit_oembed_node, depart_oembed_node)
)
ビルダー(概要の み)
「既存のビルダーの枠組みではどうにもならない出力」をしたいときに、 頑張って用意する存在。
例: sphinx-revealjs 内の revealjs
ビルダー
Sphinxコアイベントと ハンドラ
イベントハンドラ関数を登録して、適宜実行させられる
処理直後のデータが引数で渡され、その場での加工などが役割
ドキュメントにあるだけで18箇所
自分でイベントを足せる
Sphinxコアイベントと ハンドラ
def some_func(app, config):
...
def some_func2(app):
...
def setup(app):
# 本体のイベントに接続
app.connect("config-inited", some_func)
# イベントを独自定義した上で、接続
app.add_event("event-for-my-extension")
app.connect("event-for-my-extension", some_func2)
Sphinx拡張からは、 app.connect()
で関数を登録するだけで良い。
Sphinxコアイベントと ハンドラ
公開されているイベント(見切れてますし、増やせます)
builder-inited
config-inited
env-get-outdated
env-purge-doc
env-before-read-docs
source-read
object-description-transform
doctree-read
missing-reference
warn-missing-reference
doctree-resolved
env-merge-info
env-updated
env-check-consistency
html-collect-pages
html-page-context
linkcheck-process-uri
build-finished
Sphinxコアイベントと ハンドラ
イベントタイミングの目安(参考)
Sphinxコアイベントと ハンドラ
使いがちなコアイベント
html-page-context
ドキュメントごとのHTMLファイルを生成するタイミングのイベント
生成時のテンプレート自体を切り替えたり、テンプレートに渡す値を加工したりと大活躍
あくまで「出力直前」であることに注意
Sphinxコアイベントと ハンドラ
使いがちなコアイベント
config-inited
conf.py
からConfigオブジェクトを生成した直後のイベントコアイベントとしては、一番最初のタイミング
「拡張の都合でビルダーを生成するより前にしておきたいこと」のために必要
イベントハンドラの 中身を 実装する
「その拡張が何をしたいか」を踏まえた上で、 「どのタイミングで」「どんな処理をすべきか」を整理する。
その上で、必要な実装をする。
イベントハンドラの 中身を 実装する
sphinxcontrib-budoux
の場合。
def apply_budoux(app, page_name, template_name, context, doctree):
# body ... ドキュメントHTMLの中身
# update_body内で加工する
context["body"] = update_body(context["body"])
def setup(app):
app.ocnnect("html-page-context", apply_budoux)
- ページごとの出力HTMLを加工したい=
body
をいじりたい html-page-context
イベントで処理する引数を調べて、実装する
ここまで 整理
setup関数が第一。ここで、もろもろをSphinx本体に登録できる。
文法を増やしたいなら、ディレクティブ・ノードなどの設計・登録する。
本体の処理に割り込みたいなら、イベントハンドラの設計・登録する。
Sphinx拡張を 実装アプローチ+
品質向上〜編
ローカル利用の 場合
トライ&エラーで十分。
困るのは自分だけで済む。
実装に失敗していれば、Pythonのスタックトレースが出る。
即時対応が難しいけど無視は可能なら、「仕様」と言い張る。
とは いえ…
(特に公開する場合は)考えておいたほうがいい箇所。
ロギング
エラーハンドリング
関数単位でのテスト
ビルド想定のe2eテスト
ロギング
sphinx.util.logging
を利用することで、
Sphinxのビルド時に本体の出力と統一感があるロギングが出来る。
import sys
from sphinx.util import logging
logger = logging.getLogger(__name__)
def setup(app):
if sys.version_info.minor < 7:
logger.info("NOTICE: 動きはするけど、今後サポートから外れます")
エラーハンドリング
🤔
エラーハンドリング
実はあまり気にしていない。
基本的に「拡張利用時の不足のエラー時には速やかにビルド失敗」させるスタンス。
素のエラーだと分かりづらそうなら、適宜解説を差し込む。
どちらかというと、エラーを必要に応じて握りつぶす傾向。
関数単位での テスト
こちらも「やるに越したことはない」が、複雑そうなときのみ実施。
基本的には単なるPythonモジュールでしかない。
適切な機能分離をして、必要に応じたテストを用意しておけばよい。
なお、後述の理由からpytestの利用を推奨。
ビルド想定の e2eテスト
sphinx.testing
を利用できる。
import pytest
from bs4 import BeautifulSoup
from bs4.element import NavigableString, Tag
from sphinx.testing.util import SphinxTestApp
@pytest.mark.sphinx("html")
def test_default(app: SphinxTestApp, status: StringIO, warning: StringIO):
app.build()
out_html = app.outdir / "index.html"
soup = BeautifulSoup(out_html.read_text(), "html.parser")
contents = list(soup.h1.children)
assert len(contents) > 1
assert isinstance(contents[0], NavigableString)
assert isinstance(contents[1], Tag)
assert contents[1].name == "wbr"
公開する?
「自分以外にも使いそうじゃない?」と思ったらPyPIに公開してみる。
今回は公開手法については省略
単なるPythonパッケージでしか無いので、情報は出回ってる。
Search:
PyPI デビュー
Not実装の 話
Sphinx拡張を 実装する ためには その 0
その拡張に「何をさせたいか」をイメージする。
「させたいこと」の 5W1H を整理し、分割する。
5W1H にある拡張ポイントを抑える。
実装する。(前述)
Sphinx拡張を 実装する ためには その 0

Sphinx拡張を 実装する ためには その 0
「拡張」に依存しないものは別立てすると良い。
例:oEmbedのHTML取得はSphinx拡張である必要はない。
最初は難しくとも、意識しておくだけで分割しやすくなる。
既存のSphinx拡張は、参考にする。
基本的な処理フローは、Sphinx拡張である分には同じ…はず。
バンドルされた拡張を読むところから。
Sphinx拡張を 実装する ためには その -1
「拡張する」ためには「拡張する動機」が必要。
自分が使ったときの不満(推奨)
他者が使ったときの不満
拡張には「拡張の仕方を知る」=「Sphinxを知る」必要がある。
Sphinxを使い、ドキュメントを読むことが大事。
「拡張かぶり」を意識しすぎない。
と ある OSSを 拡張する ためには
「拡張する」ためには「拡張する動機」が必要。
自分が使ったときの不満(推奨)
他者が使ったときの不満
拡張には「拡張の仕方を知る」=「とあるOSSを知る」必要がある。
とあるOSSを使い、ドキュメントを読むことが大事。
「拡張かぶり」を意識しすぎない。
「ただ使う」より、ほんの一歩先へ踏み込む。
もし、 OSSを 拡張可能に するなら
(拡張する何かしらの魅力を持たせる)
「データの拡張」をしやすくする。
「イベント」の設計して、割り込みやすくする。
拡張ガイドとなるドキュメントを用意する。
まと め
Sphinxを 「拡張」する
Sphinxの拡張は
setup()
から始まる。まずは拡張ガイドを一読するところから。
ディレクティブ、ロール、イベント、ビルダー……
あなたは何をさせたいか?
「拡張の動機」は大事。