はじめに
0ビットLLM見えましたね、nikkieです。
本日もSphinxネタ、めちゃ頭いいなと思った実装の紹介です。
目次
復習:Sphinx拡張でTranslatorを指定する
自作のSphinx拡張の実装について
def setup(app): app.set_translator("html", NewTabLinkHTMLTranslator)
- HTML用のBuilder(
"html"
)に - 出力フォーマット変換に
NewTabLinkHTMLTranslator
を使うように設定- このクラスが、別タブで開くaタグを持ったHTMLを出力
という実装をしている点を理解しました。
今回のエントリはset_translator
に渡すクラスを動的に定義する話です。
pydata-sphinx-theme
PyDataコミュニティから、BootstrapベースのSphinxテーマが公開されています。
執筆時点の最新版はv0.15.2。
このset_translator
メソッドが見事でした!👏
setup()
関数
setup()
関数を持つモジュールが、Sphinx拡張の実体です。
https://github.com/pydata/pydata-sphinx-theme/blob/v0.15.2/src/pydata_sphinx_theme/__init__.py#L265
def setup(app: Sphinx) -> Dict[str, str]: """Setup the Sphinx application.""" app.connect("builder-inited", translator.setup_translators)
さまざまなsetup処理がありますが、Translatorに関わるところを抜き出しました。
builder-initedはSphinxのイベントの1つです。
https://www.sphinx-doc.org/ja/master/extdev/appapi.html#event-builder-inited
ビルダーオブジェクトが作成された時に発行されます。
ビルダーオブジェクトが作成された時に、translatorモジュールにあるsetup_translators()
を実行します
見事な実装 translator モジュール
https://github.com/pydata/pydata-sphinx-theme/blob/v0.15.2/src/pydata_sphinx_theme/translator.py
BootstrapHTML5TranslatorMixin
https://github.com/pydata/pydata-sphinx-theme/blob/v0.15.2/src/pydata_sphinx_theme/translator.py#L14
Mixin HTML Translator for a Bootstrap-ified Sphinx layout.
class BootstrapHTML5TranslatorMixin: def starttag(self, *args, **kwargs): """Ensure an aria-level is set for any heading role.""" if kwargs.get("ROLE") == "heading" and "ARIA-LEVEL" not in kwargs: kwargs["ARIA-LEVEL"] = "2" return super().starttag(*args, **kwargs)
なるほどと思ったのは、このMixinはTranslatorを継承していない点。
Translatorを継承するクラスに多重継承させることで、Mixinのstarttag()
メソッドが実行されるようにするのです!
動的クラス生成を使った setup_translators()
https://github.com/pydata/pydata-sphinx-theme/blob/v0.15.2/src/pydata_sphinx_theme/translator.py#L63
docstringはその見事さを説明しています
This re-uses the pre-existing Sphinx translator and adds extra functionality defined in
BootstrapHTML5TranslatorMixin
.
This way we can retain the original translator's behavior and configuration, and only add the extra bootstrap rules.
意訳
setup_translators()
は、事前に存在しているSphinxのTranslatorを再利用し、BootstrapHTML5TranslatorMixin
に定義されている追加の機能を加える- 元のTranslatorの振る舞いと設定は保持し、追加のbootstrapの規則を加えるだけ
実装がマジですごいです。こんなことできるのか〜
def setup_translators(app: Sphinx): if not app.registry.translators.items(): translator = types.new_class( "BootstrapHTML5Translator", ( BootstrapHTML5TranslatorMixin, app.builder.default_translator_class, ), {}, ) app.set_translator(app.builder.name, translator, override=True)
- Translatorが設定されていない場合、
BootstrapHTML5Translator
クラスを動的に(=プログラム実行時に)定義make html
から呼ばれると仮定する- StandaloneHTMLBuilder
default_translator_class
はHTML5Translator
(ソース参照1)
BootstrapHTML5TranslatorMixin
とHTML5Translator
とを継承したBootstrapHTML5Translator
クラスが爆誕!!
- 動的に定義したBootstrapHTML5Translatorを
set_translator()
で設定
types.new_class()
なんてあるのか!!(動的な型生成)
https://docs.python.org/ja/3/library/types.html#types.new_class
Translatorが設定されている場合は詳細には述べませんが、上記と同様です。
HTMLのビルダーについて、TranslatorのクラスにBootstrapHTML5TranslatorMixin
も継承したクラスを動的に生成し、それをset_translator()
しています。
こうして、Translatorが設定されている場合であっても、Sphinx拡張として持たせたい機能を追加できるのです!
終わりに
pydata-sphinx-themeに見つけた見事な実装を紹介してきました。
Mixinを用意し、types.new_class()
でTranslatorのクラスと多重継承したクラスを動的に生み出す!
自作拡張はset_translator()
しているので、「別の拡張がset_translatorしていたらそれぞれ排他的なのでどちらかが動かないよな〜」とうっすらと感じていました。
Translatorクラスにはアクセスできそうなので、すでに設定されたTranslatorをプログラム実行中に拡張すれば回避できそうとぼんやり考えていましたが、今回 pydata-sphinx-theme にその回答を見ました。
過去の私はこの見事な解決策を書けなかったのでめちゃめちゃすごいと思うと同時に、アイデアはあったので自分の手で至りたかったという悔しさもあります。
ですが、今回は巨人の肩に乗せていただき、自作Sphinx拡張も pydata-sphinx-theme をパクって、更新するのみです!
実装を見て心が震えるのはごくまれにあるのですが、pydata-sphinx-theme はそれでした(もしや2024年のベスト?)。
こういうコードを自分も書けるようになりたい!