Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Sphinxを通して考える、「拡張」の仕方 / First approach for development sphinx extension

attakei
October 15, 2022

Sphinxを通して考える、「拡張」の仕方 / First approach for development sphinx extension

PyCon JP 2022のDay 1にて登壇した際の発表時点をPDFファイルにしたものです。

なお、発表資料のオリジナルはHTML版となっています。
HTML版は表記ミスの修正などにより更新される場合があります。
これを踏まえて、PDF版は「発表当時の資料を保全」「PDFで資料閲覧したい人への共有」を目的としており、原則として更新する予定はありません。

attakei

October 15, 2022
Tweet

More Decks by attakei

Other Decks in Programming

Transcript

  1. Sphinxを通して考える、「拡張」の仕方 author: Kazuya Takei / @attakei date: 2022/10/14 event: PyCon

    JP 2022 hashtags: , #pyconjp #pyconjp_1
  2. はじめに

  3. お前誰よ Kazuya Takei attakei (Twitter, GitHub, etc) 株式会社ニジボックス 趣味系Pythonista <=

    こっち ライブラリ・拡張系を作りがち Sphinxでプレゼンテーションしたがる人
  4. 株式会社ニジボックス ニジボックス は「Grow all」をミッションに、企業やサービスの成長に向き合 い続けるリクルートグループのデザイン会社。 お客様のビジネスの成長をUI UXのデザインプロセスから開発・運用・改善ま でワンストップでサポート。 興味が湧いた・問い合わせしたくなったら、 へ。

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

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

  7. Sphinx イントロダクション

  8. アンケート

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

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

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

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

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

    toctree:: overview installation
  14. Sphinxとは何か アウトプット形式様々 HTML PDF EPUB man page

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

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

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

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

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

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

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

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

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

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

    import sys sys.path.append(PATH_TO_LOCAL_MODULE) extensions = [ "my_extensoin", ]
  25. Sphinx拡張とは何か 最低限「体裁が整っている」コード conf.py に記述すると、「ビルド時に Hello World とコンソール出力 する」 def setup(app):

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

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

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

    ablog sphinx-revealjs sphinxcontrib-budoux
  29. Sphinx拡張 ショーケース この後に例示用に出てくるSphinx拡張の紹介

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

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

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

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

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

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

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

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

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

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

    他 イベントハンドラの登録 => app.connect ...これらは、いずれも「Sphinxのメイン処理開始までに完遂しないと困るこ と」
  42. setup を制するものはSphinxを制す? setup() の役割 Sphinx 本体 ディレクティブ イベント ハンドラ集 Sphinx

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

    拡張 メイン処理以降は、登録済みのものを扱うだけ
  44. setup関数/設定項目の宣言 拡張の「動作」を設定させるための項目を宣言。 Sphinx全体で重複しないように注意が必要。 例) sphinxcontrib-budoux / budoux_targets => BudouXに解析して欲しいタグのリスト def

    setup(app): app.add_config_value( "config_name", # 名前 [], # 初期値 )
  45. ディレクティブ等の登録 入力(出力)に関する拡張をしたいときに必要となるもの。 ディレクティブ ノード ロール ...他にもあれこれ

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

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

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

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

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

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

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

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

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

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

  59. イベントハンドラの中身を実装する 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)
  60. イベントハンドラの中身を実装する 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 をいじりたい
  61. イベントハンドラの中身を実装する 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 イベントで処理する
  62. イベントハンドラの中身を実装する 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 イベントで処理する 引数を調べて、実装する
  63. ここまで整理 setup関数が第一。ここで、もろもろをSphinx本体に登録できる。 文法を増やしたいなら、ディレクティブ・ノードなどの設計・登録する。 本体の処理に割り込みたいなら、イベントハンドラの設計・登録する。

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

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

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

  67. ロギング 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: 動きはするけど、今後サポートから外れます")
  68. エラーハンドリング 🤔

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

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

  71. ビルド想定の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"
  72. 公開する? 「自分以外にも使いそうじゃない?」と思ったらPyPIに公開してみる。 今回は公開手法については省略 単なるPythonパッケージでしか無いので、情報は出回ってる。 Search: PyPI デビュー

  73. Not実装の話

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

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

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

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

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

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

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

  81. まとめ

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

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