nikkie-ftnextの日記

イベントレポートや読書メモを発信

Sphinx拡張 pydata-sphinx-theme に見る、動的に生成したTranslatorクラスの指定

はじめに

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から呼ばれると仮定する
    • BootstrapHTML5TranslatorMixinHTML5Translatorとを継承した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年のベスト?)。
こういうコードを自分も書けるようになりたい!