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()
から始まる。まずは拡張ガイドを一読するところから。
ディレクティブ、ロール、イベント、ビルダー……
あなたは何をさせたいか?
「拡張の動機」は大事。