Slide 1

Slide 1 text

Sphinxを通して考える、「拡張」の仕方 author: Kazuya Takei / @attakei date: 2022/10/14 event: PyCon JP 2022 hashtags: , #pyconjp #pyconjp_1

Slide 2

Slide 2 text

はじめに

Slide 3

Slide 3 text

お前誰よ Kazuya Takei attakei (Twitter, GitHub, etc) 株式会社ニジボックス 趣味系Pythonista <= こっち ライブラリ・拡張系を作りがち Sphinxでプレゼンテーションしたがる人

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

株式会社ニジボックス POSTD エンジニアに向けたキュレーションメディア 海外のテック記事を日本語に翻訳して配信 https://postd.cc

Slide 6

Slide 6 text

今日話す予定のこと Sphinxの概要 Sphinx拡張の概要 Sphinx拡張の実装アプローチ And more これらを、「発表者の体験を踏まえて」話します。

Slide 7

Slide 7 text

Sphinx イントロダクション

Slide 8

Slide 8 text

アンケート

Slide 9

Slide 9 text

アンケート Sphinx、知ってますか?

Slide 10

Slide 10 text

アンケート Sphinx、知ってますか? Sphinx、使ってますか?

Slide 11

Slide 11 text

Sphinxとは何か Python製のドキュメンテーションビルダー ソーステキストを束ねて「ドキュメント」として扱う 「ドキュメント」からHTML・PDFなどを生成する メインソースはreStructuredTextし、内部でdocutilsを利用

Slide 12

Slide 12 text

Sphinxとは何か Python製のドキュメンテーションビルダー .rst .rst .rst .rst HTML PDF

Slide 13

Slide 13 text

Sphinxとは何か reStructuredText 軽量マークアップ言語 「ディレクティブ」の概念(表現力・拡張力の基盤) Document title ============== 概要 ---- .. toctree:: overview installation

Slide 14

Slide 14 text

Sphinxとは何か アウトプット形式様々 HTML PDF EPUB man page

Slide 15

Slide 15 text

Sphinxで出来ているサイト Python関連 Python本体 Sphinx Ansible 様々なPythonパッケージ

Slide 16

Slide 16 text

Sphinxで出来ているサイト Python以外 (このスライド) Fortran phpMyAdmin Linux kernel

Slide 17

Slide 17 text

Sphinxで書かれた書籍 (書籍執筆のどこかの工程でSphinxを使っているもの) Go言語による並行処理 Pythonプロフェッショナルプログラミング第3版 独学プログラマー エキスパートPythonプログラミング改訂2版 仕事ではじめる機械学習

Slide 18

Slide 18 text

おさらい:Sphinx単体で出来ること reStructuredTextでドキュメントを管理できる HTMLを生成できる・テーマを切り替えられる PDFを生成できる(要LaTex)

Slide 19

Slide 19 text

おさらい:Sphinx単体で出来ること reStructuredTextでドキュメントを管理できる HTMLを生成できる・テーマを切り替えられる PDFを生成できる(要LaTex) ちょっと物足りない?

Slide 20

Slide 20 text

ありがちな「物足りなさ」 Markdownでドキュメント管理したい 動画やツイートなどを、なるべく楽に埋め込みたい HTMLでの折り返しが気に食わないので、いい感じに改行したい

Slide 21

Slide 21 text

ありがちな「物足りなさ」 Markdownでドキュメント管理したい 動画やツイートなどを、なるべく楽に埋め込みたい HTMLでの折り返しが気に食わないので、いい感じに改行したい Sphinxは「拡張」が出来るようになっている

Slide 22

Slide 22 text

Sphinx拡張 イントロダクション

Slide 23

Slide 23 text

Sphinx拡張とは何か 「Sphinxの機能を拡張」するためのPythonライブラリ。 モジュールでも良いし、パッケージでも良い ローカル管理でも平気(インポートさえできればOK) (ちょっと雑だけど) conf.py 上で実装してもいい

Slide 24

Slide 24 text

Sphinx拡張とは何か 拡張の使用方法。 extensions にライブラリ名を追加するだけ 必要に応じて sys.path を編集 体裁が整っているなら、よしなに呼ばれる # Optional import sys sys.path.append(PATH_TO_LOCAL_MODULE) extensions = [ "my_extensoin", ]

Slide 25

Slide 25 text

Sphinx拡張とは何か 最低限「体裁が整っている」コード conf.py に記述すると、「ビルド時に Hello World とコンソール出力 する」 def setup(app): print("Hello world")

Slide 26

Slide 26 text

Sphinx拡張で実現可能なこと ディレクティブの登録 /既存ディレクティブへの処理を変更 /読み取り可能なフォーマットを 追加 /新しい出力形式(ビルダー)の追加 /出力処理時にデータ加工 /その他・Sphinxのビ ルド処理時の各種処理追加

Slide 27

Slide 27 text

Sphinx拡張で実現可能なこと ざっくりとした分類 「入力」を拡張する (フォーマットを増やす、文法を増やす) 「出力」を拡張する (フォーマットを増やす、自作テーマ) 「内部」で何かする (複合系)

Slide 28

Slide 28 text

拡張の例 Sphinx本体にバンドルされているもの , サードパーティ製 , 自作のもの , sphix.ext.autodoc sphinx.ext.todo myst-parser ablog sphinx-revealjs sphinxcontrib-budoux

Slide 29

Slide 29 text

Sphinx拡張 ショーケース この後に例示用に出てくるSphinx拡張の紹介

Slide 30

Slide 30 text

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 返信 リンクをコピー Twitter でもっと読む

Slide 31

Slide 31 text

sphinxcontrib-oembed ↓ .. raw:: html

Slide 32

Slide 32 text

sphinxcontrib-oembed 分類:入力と出力に関する拡張 ディレクティブ(とノード)の新規作成が必要 ソース読み込み時にHTTP通信する => ディレクティブの実装がちょっと複雑 とはいえ、 ディレクティブとノードで完結する

Slide 33

Slide 33 text

sphinxcontrib-budoux BudouXを使って、日本語文の改行をいい感じに出来るようにする拡張。

Slide 34

Slide 34 text

sphinxcontrib-budoux BudouXを使って、日本語文の改行をいい感じに出来るようにする拡張。

Slide 35

Slide 35 text

sphinxcontrib-budoux BudouXを使って、日本語文の改行をいい感じに出来るようにする拡張。

Slide 36

Slide 36 text

sphinxcontrib-budoux 分類:出力に関する拡張 Sphinxメイン処理内で行われた「ソースをもとにしたHTML」を再加工す る 出力に 割り込んで の加工が必要

Slide 37

Slide 37 text

Sphinx拡張の実装アプローチ 基本実装〜編

Slide 38

Slide 38 text

再掲:最低限「体裁が整っている」コード def setup(app): print("Hello world")

Slide 39

Slide 39 text

Sphinx拡張の実態 大雑把に書くと、以下のような要素たちの集まり 追加の入出力を定義するための関数/クラス群 イベントハンドラ用の関数群 ★Sphinx本体から呼び出される setup 関数 補助処理

Slide 40

Slide 40 text

setup を制するものはSphinxを制す? setup() の役割 設定項目の宣言 => app.add_config_value ディレクティブ/ビルダー等の登録 => app.add_builder 他 イベントハンドラの登録 => app.connect

Slide 41

Slide 41 text

setup を制するものはSphinxを制す? setup() の役割 設定項目の宣言 => app.add_config_value ディレクティブ/ビルダー等の登録 => app.add_builder 他 イベントハンドラの登録 => app.connect ...これらは、いずれも「Sphinxのメイン処理開始までに完遂しないと困るこ と」

Slide 42

Slide 42 text

setup を制するものはSphinxを制す? setup() の役割 Sphinx 本体 ディレクティブ イベント ハンドラ集 Sphinx 拡張 setup() ディレク ティブ イベント ハンドラ Sphinxから呼ばれるのはsetupのみ

Slide 43

Slide 43 text

setup を制するものはSphinxを制す? setup() の役割 Sphinx 本体 ディレクティブ イベント ハンドラ集 Sphinx 拡張 メイン処理以降は、登録済みのものを扱うだけ

Slide 44

Slide 44 text

setup関数/設定項目の宣言 拡張の「動作」を設定させるための項目を宣言。 Sphinx全体で重複しないように注意が必要。 例) sphinxcontrib-budoux / budoux_targets => BudouXに解析して欲しいタグのリスト def setup(app): app.add_config_value( "config_name", # 名前 [], # 初期値 )

Slide 45

Slide 45 text

ディレクティブ等の登録 入力(出力)に関する拡張をしたいときに必要となるもの。 ディレクティブ ノード ロール ...他にもあれこれ

Slide 46

Slide 46 text

ディレクティブ等の登録 Sphinx本体には無いディレクティブなので、自作&登録が必要。 .. oembed:: https://twitter.com/attakei/status/1575887211962290176 .. oembed:: https://www.youtube.com/watch?v=Jn2zvfDhU0w

Slide 47

Slide 47 text

ディレクティブ等の登録 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)

Slide 48

Slide 48 text

ディレクティブ等の登録 ディレクティブを用意するなら、まずノードも必要。 ノードは出力にも関わるので、出力の実装もセット。 .rst .. oembed:: ~~~ document oembed ディレクティブ ノード

Slide 49

Slide 49 text

ディレクティブ等の登録 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

Slide 50

Slide 50 text

ディレクティブ等の登録 def setup(app): app.add_node( oembed, # ビルダー種別ごとに、どんな処理をさせたいか指定する html=(visit_oembed_node, depart_oembed_node) )

Slide 51

Slide 51 text

ビルダー(概要のみ) 「既存のビルダーの枠組みではどうにもならない出力」をしたいときに、 頑張 って用意する存在。 例: 内の revealjs ビルダー sphinx-revealjs

Slide 52

Slide 52 text

Sphinxコアイベントとハンドラ コアイベント:  ビルド処理内に用意された、いくつかの追加処理向けタイミング イベントハンドラ関数を登録して、適宜実行させられる 処理直後のデータが引数で渡され、その場での加工などが役割 ドキュメントにあるだけで18箇所 自分でイベントを足せる

Slide 53

Slide 53 text

Sphinxコアイベントとハンドラ Sphinx拡張からは、 app.connect() で関数を登録するだけで良い。 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)

Slide 54

Slide 54 text

Sphinxコアイベントとハンドラ 公開されているイベント(見切れてますし、増やせます) builder-inited config-inited env-get-outdated env-purge-doc env-before-read-docs source-read object-description-transform

Slide 55

Slide 55 text

Sphinxコアイベントとハンドラ イベントタイミングの目安(参考) 初期処理 ソース 読み込み 中間処理 ファイル 出⼒ 終了処理 初期処理 ソース 読み込み 中間処理 ファイル 出⼒

Slide 56

Slide 56 text

Sphinxコアイベントとハンドラ 使いがちなコアイベント html-page-context ドキュメントごとのHTMLファイルを生成するタイミングのイベント 生成時のテンプレート自体を切り替えたり、テンプレートに渡す値を加工 したりと大活躍 あくまで「出力直前」であることに注意

Slide 57

Slide 57 text

Sphinxコアイベントとハンドラ 使いがちなコアイベント config-inited conf.py からConfigオブジェクトを生成した直後のイベント コアイベントとしては、一番最初のタイミング 「拡張の都合でビルダーを生成するより前にしておきたいこと」のために 必要

Slide 58

Slide 58 text

イベントハンドラの中身を実装する 「その拡張が何をしたいか」を踏まえた上で、 「どのタイミングで」「どんな 処理をすべきか」を整理する。 その上で、必要な実装をする。

Slide 59

Slide 59 text

イベントハンドラの中身を実装する 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)

Slide 60

Slide 60 text

イベントハンドラの中身を実装する 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 をいじりたい

Slide 61

Slide 61 text

イベントハンドラの中身を実装する 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 イベントで処理する

Slide 62

Slide 62 text

イベントハンドラの中身を実装する 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 イベントで処理する 引数を調べて、実装する

Slide 63

Slide 63 text

ここまで整理 setup関数が第一。ここで、もろもろをSphinx本体に登録できる。 文法を増やしたいなら、ディレクティブ・ノードなどの設計・登録する。 本体の処理に割り込みたいなら、イベントハンドラの設計・登録する。

Slide 64

Slide 64 text

Sphinx拡張を実装アプローチ+ 品質向上〜編

Slide 65

Slide 65 text

ローカル利用の場合 トライ&エラーで十分。 困るのは自分だけで済む。 実装に失敗していれば、Pythonのスタックトレースが出る。 即時対応が難しいけど無視は可能なら、「仕様」と言い張る。

Slide 66

Slide 66 text

とはいえ… (特に公開する場合は)考えておいたほうがいい箇所。 ロギング エラーハンドリング 関数単位でのテスト ビルド想定のe2eテスト

Slide 67

Slide 67 text

ロギング 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: 動きはするけど、今後サポートから外れます")

Slide 68

Slide 68 text

エラーハンドリング 🤔

Slide 69

Slide 69 text

エラーハンドリング 実はあまり気にしていない。 基本的に「拡張利用時の不足のエラー時には速やかにビルド失敗」さ せるスタンス。 素のエラーだと分かりづらそうなら、適宜解説を差し込む。 どちらかというと、エラーを必要に応じて握りつぶす傾向。

Slide 70

Slide 70 text

関数単位でのテスト こちらも「やるに越したことはない」が、複雑そうなときのみ実施。 基本的には単なるPythonモジュールでしかない。 適切な機能分離をして、必要に応じたテストを用意しておけばよい。 なお、後述の理由からpytestの利用を推奨。

Slide 71

Slide 71 text

ビルド想定の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"

Slide 72

Slide 72 text

公開する? 「自分以外にも使いそうじゃない?」と思ったらPyPIに公開してみる。 今回は公開手法については省略 単なるPythonパッケージでしか無いので、情報は出回ってる。 Search: PyPI デビュー

Slide 73

Slide 73 text

Not実装の話

Slide 74

Slide 74 text

Sphinx拡張を実装するためには その0 その拡張に「何をさせたいか」をイメージする。 「させたいこと」の 5W1H を整理し、分割する。 5W1H にある拡張ポイントを抑える。 実装する。(前述)

Slide 75

Slide 75 text

Sphinx拡張を実装するためには その0 拡張のドキュメントを一読するとよい。

Slide 76

Slide 76 text

Sphinx拡張を実装するためには その0 「拡張」に依存しないものは別立てすると良い。 例:oEmbedのHTML取得はSphinx拡張である必要はない。 最初は難しくとも、意識しておくだけで分割しやすくなる。 既存のSphinx拡張は、参考にする。 基本的な処理フローは、Sphinx拡張である分には同じ…はず。 バンドルされた拡張を読むところから。

Slide 77

Slide 77 text

Sphinx拡張を実装するためには その-1 「拡張する」ためには「拡張する動機」が必要。 自分が使ったときの不満(推奨) 他者が使ったときの不満 拡張には「拡張の仕方を知る」=「Sphinxを知る」必要がある。 Sphinxを使い、ドキュメントを読むことが大事。 「拡張かぶり」を意識しすぎない。

Slide 78

Slide 78 text

とあるOSSを拡張するためには 「拡張する」ためには「拡張する動機」が必要。 自分が使ったときの不満(推奨) 他者が使ったときの不満 拡張には「拡張の仕方を知る」=「とあるOSSを知る」必要がある。 とあるOSSを使い、ドキュメントを読むことが大事。 「拡張かぶり」を意識しすぎない。

Slide 79

Slide 79 text

とあるOSSを拡張するためには 「拡張する」ためには「拡張する動機」が必要。 自分が使ったときの不満(推奨) 他者が使ったときの不満 拡張には「拡張の仕方を知る」=「とあるOSSを知る」必要がある。 とあるOSSを使い、ドキュメントを読むことが大事。 「拡張かぶり」を意識しすぎない。 「ただ使う」より、ほんの一歩先へ踏み込む。

Slide 80

Slide 80 text

もし、OSSを拡張可能にするなら (拡張する何かしらの魅力を持たせる) 「データの拡張」をしやすくする。 「イベント」の設計して、割り込みやすくする。 拡張ガイドとなるドキュメントを用意する。

Slide 81

Slide 81 text

まとめ

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

Thanks NIJIBOX Co., Ltd. Sphinx-Users.jp