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

    View full-size slide

  2. はじめに

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  8. アンケート

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    ==============



    概要

    ----



    .. toctree::



    overview

    installation

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    import sys

    sys.path.append(PATH_TO_LOCAL_MODULE)



    extensions = [

    "my_extensoin",

    ]

    View full-size slide

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

    print("Hello world")

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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
    でもっと読む

    View full-size slide

  31. sphinxcontrib-oembed

    .. raw:: html







    sphinx-revealjs v2.2.0 is released.
    Thank you for usings, feedbacks and collaborations!

    — kAZUYA tAKEI (@attakei)
    .. oembed:: https://twitter.com/attakei/status/1575887211962290176

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    出力に 割り込んで の加工が必要

    View full-size slide

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

    View full-size slide

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

    print("Hello world")

    View full-size slide

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

    View full-size slide

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

    イベントハンドラの登録
    => app.connect

    View full-size slide

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

    イベントハンドラの登録
    => app.connect
    ...これらは、いずれも「Sphinxのメイン処理開始までに完遂しないと困るこ
    と」

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    app.add_config_value(

    "config_name", #
    名前

    [], #
    初期値

    )

    View full-size slide

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

    View full-size slide

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



    .. oembed:: https://www.youtube.com/watch?v=Jn2zvfDhU0w

    View full-size slide

  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)

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  50. ディレクティブ等の登録
    def setup(app):

    app.add_node(

    oembed,

    #
    ビルダー種別ごとに、どんな処理をさせたいか指定する

    html=(visit_oembed_node, depart_oembed_node)

    )

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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)

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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)

    View full-size slide

  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
    をいじりたい

    View full-size slide

  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
    イベントで処理する

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  68. エラーハンドリング
    🤔

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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"

    View full-size slide

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

    View full-size slide

  73. Not実装の話

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide