340 Views
October 08, 24
スライド概要
PyCon JP 2022で発表したスライドのPDF版です。
オリジナルは https://slides.attakei.net/pyconjp-2022/ ですが、「アニメーション等なしでもいいからオフラインで読みたい」人向けとなっています。
Sphinx「で」プレゼンテーションをする人。 ※主にバックアップ用のスライドをアップロードします。
Sphinxを通して考える、「拡張」の仕方 author: Kazuya Takei / @attakei date: 2022/10/14 event: PyCon JP 2022 hashtags: #pyconjp , #pyconjp_1
はじめに
お前誰よ Kazuya Takei attakei (Twitter, GitHub, etc) 株式会社ニジボックス 趣味系Pythonista <= こっち ライブラリ・拡張系を作りがち Sphinxでプレゼンテーションしたがる人
株式会社ニジボックス ニジボックス は「Grow all」をミッションに、企業やサービスの成長に向き合 い続けるリクルートグループのデザイン会社。 お客様のビジネスの成長をUI UXのデザインプロセスから開発・運用・改善まで ワンストップでサポート。 興味が湧いた・問い合わせしたくなったら、 https://www.nijibox.jp へ。
株式会社ニジボックス POSTD https://postd.cc エンジニアに向けたキュレーションメディア 海外のテック記事を日本語に翻訳して配信
今日話す予定のこと Sphinxの概要 Sphinx拡張の概要 Sphinx拡張の実装アプローチ And more これらを、「発表者の体験を踏まえて」話します。
Sphinx イントロダクション
アンケート
アンケート Sphinx、知ってますか?
アンケート Sphinx、知ってますか? Sphinx、使ってますか?
Sphinxとは何か Python製のドキュメンテーションビルダー ソーステキストを束ねて「ドキュメント」として扱う 「ドキュメント」からHTML・PDFなどを生成する メインソースはreStructuredTextし、内部でdocutilsを利用
Sphinxとは何か Python製のドキュメンテーションビルダー HTML .rst .rst .rst .rst PDF
Sphinxとは何か reStructuredText 軽量マークアップ言語 「ディレクティブ」の概念(表現力・拡張力の基盤) Document title ============== 概要 ---.. toctree:: overview installation
Sphinxとは何か アウトプット形式様々 HTML PDF EPUB man page
Sphinxで出来ているサイト Python関連 Python本体 Sphinx Ansible 様々なPythonパッケージ
Sphinxで出来ているサイト Python以外 Fortran phpMyAdmin Linux kernel (このスライド)
Sphinxで書かれた書籍 (書籍執筆のどこかの工程でSphinxを使っているもの) Go言語による並行処理 Pythonプロフェッショナルプログラミング第3版 独学プログラマー エキスパートPythonプログラミング改訂2版 仕事ではじめる機械学習
おさらい:Sphinx単体で出来ること reStructuredTextでドキュメントを管理できる HTMLを生成できる・テーマを切り替えられる PDFを生成できる(要LaTex)
おさらい:Sphinx単体で出来ること reStructuredTextでドキュメントを管理できる HTMLを生成できる・テーマを切り替えられる PDFを生成できる(要LaTex) ちょっと物足りない?
ありがちな「物足りなさ」 Markdownでドキュメント管理したい 動画やツイートなどを、なるべく楽に埋め込みたい HTMLでの折り返しが気に食わないので、いい感じに改行したい
ありがちな「物足りなさ」 Markdownでドキュメント管理したい 動画やツイートなどを、なるべく楽に埋め込みたい HTMLでの折り返しが気に食わないので、いい感じに改行したい Sphinxは「拡張」が出来るようになっている
Sphinx拡張 イントロダクション
Sphinx拡張とは何か 「Sphinxの機能を拡張」するためのPythonライブラリ。 モジュールでも良いし、パッケージでも良い ローカル管理でも平気(インポートさえできればOK) (ちょっと雑だけど) conf.py 上で実装してもいい
Sphinx拡張とは何か 拡張の使用方法。 # Optional import sys sys.path.append(PATH_TO_LOCAL_MODULE) extensions = [ "my_extensoin", ] にライブラリ名を追加するだけ 必要に応じて sys.path を編集 体裁が整っているなら、よしなに呼ばれる extensions
Sphinx拡張とは何か 最低限「体裁が整っている」コード def setup(app): print("Hello world") conf.py する」 に記述すると、「ビルド時に Hello World とコンソール出力
Sphinx拡張で実現可能なこと ディレクティブの登録 /既存ディレクティブへの処理を変更 /読み取り可能なフォーマット を追加 /新しい出力形式(ビルダー)の追加 /出力処理時にデータ加工 /その他・Sphinxの ビルド処理時の各種処理追加
Sphinx拡張で実現可能なこと ざっくりとした分類 「入力」を拡張する (フォーマットを増やす、文法を増やす) 「出力」を拡張する (フォーマットを増やす、自作テーマ) 「内部」で何かする (複合系)
拡張の例 Sphinx本体にバンドルされているもの sphix.ext.autodoc, sphinx.ext.todo サードパーティ製 myst-parser, ablog 自作のもの sphinx-revealjs, sphinxcontrib-budoux
Sphinx拡張 ショーケース この後に例示用に出てくるSphinx拡張の紹介
sphinxcontrib-oembed oEmbedを使ったコンテンツ埋め 込みをサポート。 URLの指定だけでツイートや動画 の埋め込みが可能になる。 kAZUYA tAKEI @attakei · フォローする sphinx-revealjs v2.2.0 is released. Thank you for usings, feedbacks and collaborations! See PyPI: pypi.org/project/sphinx… See GitHub: github.com/attakei/sphinx… pypi.org sphinx-revealjs Sphinx extension with theme to generate Reveal.js presentation 午前1:36 · 2022年10月1日 1 返信 リンクをコピー Xでもっと読む
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!<b
</p>
— kAZUYA tAKEI (@attakei)<a href="https://twitter.com/attakei/status/1575887211962290176
</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 BudouXを使って、日本語文の改行をいい感じに出来るようにする拡張。
sphinxcontrib-budoux BudouXを使って、日本語文の改行をいい感じに出来るようにする拡張。
sphinxcontrib-budoux 分類:出力に関する拡張 Sphinxメイン処理内で行われた「ソースをもとにしたHTML」を再加工す る 出力に 割り込んで の加工が必要
Sphinx拡張の実装アプローチ 基本実装〜編
再掲:最低限「体裁が整っている」コード def setup(app): print("Hello world")
Sphinx拡張の実態 大雑把に書くと、以下のような要素たちの集まり 追加の入出力を定義するための関数/クラス群 イベントハンドラ用の関数群 ★Sphinx本体から呼び出される setup 関数 補助処理
setup を制するものはSphinxを制す? の役割 設定項目の宣言 => app.add_config_value ディレクティブ/ビルダー等の登録 => app.add_builder 他 イベントハンドラの登録 => app.connect setup()
setup を制するものはSphinxを制す? の役割 設定項目の宣言 => app.add_config_value ディレクティブ/ビルダー等の登録 => app.add_builder 他 イベントハンドラの登録 => app.connect ...これらは、いずれも「Sphinxのメイン処理開始までに完遂しないと困ること」 setup()
setup を制するものはSphinxを制す? setup() の役割 Sphinx本体 イベント ハンドラ集 Sphinx拡張 setup() ディレク ティブ ディレクティブ イベント ハンドラ Sphinxから呼ばれるのはsetupのみ
setup を制するものはSphinxを制す? setup() の役割 Sphinx本体 イベント ハンドラ集 ディレクティブ メイン処理以降は、登録済みのものを扱うだけ Sphinx拡張
setup関数/設定項目の宣言 def setup(app): app.add_config_value( "config_name", # 名前 [], # 初期値 ) 例) 拡張の「動作」を設定させるための項目を宣言。 Sphinx全体で重複しないように注意が必要。 sphinxcontrib-budoux / budoux_targets => BudouXに解析して欲しいタグのリスト
ディレクティブ等の登録 入力(出力)に関する拡張をしたいときに必要となるもの。 ディレクティブ ノード ロール ...他にもあれこれ
ディレクティブ等の登録 .. 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)
ディレクティブ等の登録 ディレクティブを用意するなら、まずノードも必要。 ノードは出力にも関わるので、出力の実装もセット。 .rst document ディレクティブ .. oembed:: ~~~ ノード oembed
ディレクティブ等の登録 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
Sphinxコアイベントとハンドラ イベントタイミングの目安(参考) 初期処理 ソース 読み込み 中間処理 ファイル 出力 終了処理
Sphinxコアイベントとハンドラ 使いがちなコアイベント html-page-context ドキュメントごとのHTMLファイルを生成するタイミングのイベント 生成時のテンプレート自体を切り替えたり、テンプレートに渡す値を加工 したりと大活躍 あくまで「出力直前」であることに注意
Sphinxコアイベントとハンドラ 使いがちなコアイベント config-inited からConfigオブジェクトを生成した直後のイベント コアイベントとしては、一番最初のタイミング 「拡張の都合でビルダーを生成するより前にしておきたいこと」のために 必要 conf.py
イベントハンドラの中身を実装する 「その拡張が何をしたいか」を踏まえた上で、 「どのタイミングで」「どんな 処理をすべきか」を整理する。 その上で、必要な実装をする。
イベントハンドラの中身を実装する 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)
イベントハンドラの中身を実装する 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 をいじりたい
イベントハンドラの中身を実装する 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 イベントで処理する
イベントハンドラの中身を実装する 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のビルド時に本体の出力 と統一感があるロギングが出来る。 sphinx.util.logging 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を拡張するためには 「拡張する」ためには「拡張する動機」が必要。 自分が使ったときの不満(推奨) 他者が使ったときの不満 拡張には「拡張の仕方を知る」=「とあるOSSを知る」必要がある。 とあるOSSを使い、ドキュメントを読むことが大事。 「拡張かぶり」を意識しすぎない。 「ただ使う」より、ほんの一歩先へ踏み込む。
もし、OSSを拡張可能にするなら (拡張する何かしらの魅力を持たせる) 「データの拡張」をしやすくする。 「イベント」の設計して、割り込みやすくする。 拡張ガイドとなるドキュメントを用意する。
まとめ
Sphinxを「拡張」する Sphinxの拡張は setup() から始まる。 まずは拡張ガイドを一読するところから。 ディレクティブ、ロール、イベント、ビルダー…… あなたは何をさせたいか? 「拡張の動機」は大事。
Thanks NIJIBOX Co., Ltd. Sphinx-Users.jp