uv workspacesとpluggyで作る,プラッガブルなPythonエコシステム
uv workspacesでスッキリ作るPythonモノレポでは,uv workspacesを使ったモノレポ構成について書いた。この記事はその続編で,モノレポにpluggyを組み合わせて,プラッガブルなPythonエコシステムを作っていく。
この記事で実現したいこと
uv workspacesのcommon use caseとして,「プラグインシステム」が挙げられている。
A library with a plugin system, where each plugin is a separate workspace package with a dependency on the root.
https://docs.astral.sh/uv/concepts/projects/workspaces/#when-not-to-use-workspaces
プラグインシステムとpluggy
前回の記事では,コアライブラリ(eggdishes-core
)と拡張ライブラリ(eggdishes-*
)を別パッケージに分離することで,インタフェースと実装部分を分けて開発することができるようになった。ただし,アプリケーションであるCLIツール(eggdishes-main
)のほうは,コアライブラリ(インタフェース)に加えてすべての拡張ライブラリ(実装クラス)の詳細を知らないといけない。拡張を追加するたびにアプリケーション側を変更しないといけないのは,拡張性という観点では自由度が低い。
pluggyは,pytestで使われているプラグイン管理ツールで,インタフェースとその拡張ないし実装(プラグイン)を仲介してくれる。拡張を使うアプリケーション(ホストプログラム)は,インタフェースとプラグインマネージャー(後述)だけ知っていれば良い。アプリケーション側を変更せずとも,拡張ライブラリをホストプログラムと同じ環境にpip install
するだけで呼べるようになる。
この記事では,前回作ったモノレポにpluggyを導入して,CLIアプリケーションから拡張ライブラリへの依存を剥がし,ドロップイン方式で拡張ライブラリが実行できるようにしていく。
toy projectのサンプルコード
https://github.com/mocobeta/uv-workspaces-eggdishes
pluggy入門
pluggyの使い方は少し込み入っていて,公式リファレンスを読んでもとっつきづらいため,実際に自分のプロジェクトに導入しながら手を動かすほうが理解がすすむ。
ということで早速やっていく。
pluggyのインストール
EggDish
インタフェースの定義があるコアライブラリにpluggyをインストールする(他のパッケージは,コアライブラリ経由でpluggyの機能が使えるため,インストール不要)。
スペック定義と実装マーカーの公開
プラグインのスペック(仕様)と,その実装を示すマーカーをコアライブラリのhookspecs.py
(ファイル名はなんでも良い)に定義する。
# eggdishes-core/src/eggdishes_core/hookspecs.py
# プラグイン仕様を示すマーカー
=
# プラグイン実装を示すマーカー
=
# プラグインが実装するべきメソッドインタフェース
"""Register egg dish recipe."""
pass
__init__.py
で実装マーカーを公開しておくと,プラグイン側から使いやすい。
# eggdishes-core/src/eggdishes_core/__init__.py
プラグインマネージャーを書く
プラグインのスペックと実装をつなぐPluginManagerのコードを,同じくコアライブラリに追加する。pluggyを使う上で,たぶんここが一番ややこしいので,細かくコメントをつけてみた。
# eggdishes-core/src/eggdishes_core/plugins.py
# EggDish のレジストリ
# プラグイン名と,対応する実装クラスのファクトリメソッドを管理する
=
# プラグインマネージャーを取得
=
# hookspec(プラグインのインターフェース)を登録
# 環境内のプラグインをロード
# このentry point名をプラグイン側のpyproject.tomlのentry pointと一致させることで,プラグインとして認識される
# fresh_eggの実装はeggdishes_core自身で定義されているので,明示的に登録する
return
"""Register available recipes to the registry."""
=
# 環境内のすべてのプラグインフックを呼び出す
=
"""Return a list of available egg dish recipes."""
return
"""Return the recipe for the given egg dish name."""
=
return
return None
プラグインを書く
コアライブラリ内で定義したデフォルト実装FreshEgg
をプラグインとして登録する。
# eggdishes-core/src/eggdishes_core/fresh_egg.py
=
return
# プラグイン実装
return
=
ここまででpluggyの使い方の説明がだいたい終わったのだけれど,おわかりいただけただろうか...。初見でぱっとわかるかというと正直厳しいと思う(私はわかったと言えるまでに数時間はかかった)が,スペック定義,プラグインマネージャー,プラグイン実装のコードを突き合わせて追いかけると,頭が整理できてくると思う。
アプリケーション(ホストプログラム)の変更
CLIツール(eggdishes-main
)のほうにも手を入れて,拡張クラスへの依存をすべて剥がし,コアライブラリが提供するプラグインシステムを使うように変更する。また,環境内で利用可能なプラグインをリストするlist
コマンドを追加する。
# eggdishes-main/src/eggdishes_main/cli.py
pass
=
=
前の記事での完成形だったバージョン0.2.0
の eggdishes-main/pyproject.toml
, eggdishes-main/src/eggdishes_main/cli.py
と比較すると違いがわかりやすいと思う。
dependency graphも確認しておく。
)
)
)
)
)
)
動作確認
ここまでで,動作を一度確認する。プラグインはコアに含まれるFreshEgg
しか登録されていない。
pluggy実践編(プラグイン追加)
ここからは,拡張ライブラリ(eggdishes-*
)を全部プラグイン化していく。デフォルト実装FreshEgg
をプラグイン化したのと同じように,拡張コード内にhookimpl
マーカーをつけた実装を追加するだけ。たとえばBoiledEgg
の場合:
=
# snip
return
=
また,このパッケージがプラグインであることをプラグインマネージャーに通知するために,pyproject.toml
に以下2行を追加する。
# プラグインのentry pointを登録
プラグインパッケージ側の変更は以上。
動作確認(プラグイン全部入り版)
すべての拡張ライブラリパッケージを同じ要領でプラグイン化したら,パッケージをビルドして動作確認する。
ドロップインでプラグインが動作するかは実際にpip install
してみないと確認できないので,適当な環境を作ってwheelをインストールする。
こんな感じで,インストールしたプラグインが全て認識されるはず。
ここまでのコードは0.4.0としてタグを打っているので,全体感はこのタグのソースツリーを参照してほしい。
まとめ
uv workspacesとpluggyを使うと,
- 関連する複数パッケージをモノレポで管理する
- 粗結合・プラッガブルなアプリケーションフレームワークを作る
ことができた。
プラグインのコードはもちろん,モノレポ内で管理されていなくても良い。「フレームワークとデフォルトプラグインをコア開発者が提供して,他の開発者が自作プラグインを作って公開できる」というようなエコシステムまで視野に入れると,このスキームの威力が発揮される。