コンテンツにスキップ

アーキテクチャ

リポジトリ構成

pytilpack/          # メインパッケージ
  cli/              # CLIサブコマンド群
tests/              # テストコード
docs/               # ドキュメント
  api/              # APIリファレンス(モジュールごとの.md)
  guide/            # 利用者向けガイド
  development/      # 開発者向けドキュメント
scripts/            # 開発用スクリプト

モジュールの構成と依存

pytilpackは主要Pythonライブラリ向けの軽量ユーティリティ集。 モジュール単位の個別importとextras単位の依存管理を採用する。 利用者は必要なモジュールだけを取り込むことができる。

コア依存とextras

コア依存([project.dependencies])は最小限に保つ。 サードパーティライブラリに依存するモジュールはextras([project.optional-dependencies])で管理する。 mcpの推移的依存は、MCPサーバーをpip install pytilpackだけで利用できる状態を保つため受け入れている。 新規依存の追加にはこの判断を流用せず、コア依存の最小化を適用する。

コア依存の版指定には原則上限を設けない。上流のメジャー更新を利用者が選べる状態を保ち、上限が放置されることを防ぐ。 上流の更新でimportが失敗した場合は、その時点で追随する。 依存更新の担当者が実機実行・公式移行資料・公式セキュリティ情報で、対象メジャー版に次のいずれかを確認した場合だけ上限を設ける。

  • import失敗や、それに起因する要求機能の不成立
  • 既存APIの非互換変更
  • 未修正の脆弱性など、採用を維持できない事由

上限には移行対応の完了・対象メジャー版でのテスト成功・脆弱性修正版の公開など、実行結果か公式資料で確認できる解除条件をコメントで添える。

対応情報の定義元

  • コア依存と各extraの直接依存はpyproject.tomlに定義する
  • extraで提供するモジュールはscripts/check_extras_imports.pyのEXTRA_MODULESに定義する
  • CLIの名前・実装・必要extraはpytilpack/cli/main.pyの_SUBCOMMANDSに定義する

利用者が読む対応表とモジュール分類は利用者向けガイドに置く。 分類はモジュールの主な用途で1つに決め、MkDocsのnavとllmstxtにも同じ所属を記載する。 allは他の全extraの直接依存の和集合を持ち、sqlalchemy[asyncio]のような依存先のextraも保持する。

依存の向きとimport

標準ライブラリやコア依存を使う基盤モジュールを、ライブラリ別・フレームワーク別の支援から利用する。 functoolsはhttp、secretsはfunctools、jsonはjsoncとio、sqlalchemyはasyncio・paginator・pythonを利用する。 同じ処理を公開窓口ごとに写さず、共通の内部実装へ委ねる。同期と非同期のセッション実行や初期化状態はそれぞれが保持する。

importは原則トップレベルに置く。ベースのモジュールが任意extraを要する機能を持つ場合は、その関数内で依存を読み込む。 たとえばasyncio.io_のYAML操作は、呼び出す場合だけpytilpack.yamlを読み込む。 Unix専用依存については後掲のプラットフォーム依存の規則を適用する。

任意extraを要するモジュールをトップレベルでimportする側は、そのextraの直接依存も自分のextraとallへ含める。

Python 3.15以降でimport時間を短縮するため、各モジュールはdocstringの直後に__lazy_modules__を置き、トップレベルでimportするモジュール名を列挙する。 3.15未満では無視されるため、lazy構文と異なり対応版の下限を変えずに使える。 列挙しないのは次のimportに限る。

  • 名前を参照しない副作用目的のimport、from __future__、*によるimport
  • CLIのサブコマンドがextras未導入の判定に使うimport(pytilpack.cli.babelのbabel、pytilpack.cli.wait_for_db_connectionのpytilpack.sqlalchemy)。pytilpack.cli.mainは登録時のImportErrorで未導入を判定するため、遅延すると--helpの「未インストール」表示が失われる
  • typing.TYPE_CHECKINGの内側に置く型チェック専用のimport。実行時には読み込まれないため遅延の対象にならない

__lazy_modules__へ列挙していないimportはRuffのTID254が報告する。pyproject.tomlでrequire-lazy = "all"を設定し、__lazy_modules__を宣言したモジュールで遅延化できるトップレベルのimportが列挙されていなければ失敗する。 前掲の例外のうちCLIのextras判定と型チェック専用のimportは、理由付きの# noqa: TID254で行単位に除外する。 宣言の無いファイル、関数内・クラス内・try配下のimport、from __future__、*によるimportはTID254の対象外のため、宣言自体の削除はTID254では検出できない。

モジュール定義時に評価する箇所(基底クラス、デコレーター、型エイリアスの代入など)で使う依存は列挙しても読み込み時に解決されるため、短縮は依存を関数内でだけ使うモジュールで得られる。 列挙したextraの依存が欠けている場合、3.15以降はImportErrorがimport時ではなく最初の利用時に発生する。

サブパッケージ共通モジュールの依存

pytilpack/_web_asserts.pyのように複数のフレームワーク向けサブパッケージから共通利用される 私的モジュールがある。 これらが特定のサードパーティに依存する場合、そのパッケージを依存元の各フレームワークextras (flask・quart・fastapi等)とall extrasの双方に含める。

具体例: pytilpack/_web_asserts.pyのassert_xml_coreはXML外部実体攻撃を防ぐため defusedxmlを使用する。 このためflask・quart・fastapi・all extrasにdefusedxmlを含める。

公開API設計方針

  • 各モジュールは対象ライブラリ(pytilpack.fastapi・pytilpack.flask等)ごとに独立した名前空間を持つ
  • pytilpack.cli配下はコマンドラインインターフェースの実装で、パッケージのpublic APIには含めない

同期・非同期を包むデコレーターは、コルーチン関数かどうかを判別して両方に対応する。 デコレーター以外で非同期版を持つ関数は、同じモジュールにa接頭辞の名前で置く。 pytilpack.asyncioは標準ライブラリと他モジュールのI/Oの非同期版を置く。 既存のthreadingaは互換のため維持する。 各サブモジュールの__all__には意図して公開する関数・クラスを記載し、importした名前や内部処理を漏らさない。

非推奨と削除

関数全体の非推奨にはpytilpack.python.deprecatedを使い、警告文に現行の完全修飾名で移行先を示す。 特定の引数だけの非推奨は、その引数を使った場合に警告する。 公開インターフェースの削除は非互換変更としてメジャー版の更新で行う。後方互換用の空extraも同じ扱いとする。

プラットフォーム依存とファイル権限

fcntlなどUnix専用モジュールに依存するモジュールは、API文書でUnix専用であることを案内する。 他のモジュールから最上位で読み込まず、その機能を使う関数内でimportする。

umaskによらず厳密な権限が必要なファイルは、os.open(..., mode=0o600)などで作成時点から権限を確定する。 Path.openの後にchmodする方法は権限変更前に他プロセスが開ける時間窓を持つため使わない。 鍵を保存するpytilpack.secretsにも適用する。

テスト規約

  • pytilpack/xxx.py → tests/xxx_test.py
  • pytilpack/xxx/yyy.py → tests/xxx/yyy_test.py
  • xxxがPythonキーワード等と衝突する場合はxxx_.pyとなる。テストはxxx_test.py(末尾アンダースコアは除く)
  • 既存モジュールへ公開関数・公開クラスを追加した場合は、対応するtests/.../xxx_test.pyに単体テストも追加する

リポジトリの設定を検証するテストは、この配置規約の対象外とする。 たとえばtests/precommit_test.pyは.pre-commit-config.yamlの設定を検証する。

SQLAlchemyのfixture(厳守規定)

SyncMixinとAsyncMixinは各サブクラスのクラス変数で初期化状態を保持し、二重のinit()を拒否する。 AsyncMixin.term()はスレッド単位のengineをdispose()し、engineとsessionmakerの参照をNoneへ戻すが、クラスの初期化状態を戻さない。 このためinit()→term()→init()は成立しない。

同じサブクラスを複数回のfixture setupで使う場合は、scope="session"でワーカーごとに1回だけ初期化する。 moduleスコープではpytest-xdistの分配によってsetupが繰り返され、2回目のinit()が失敗する。 共有engineが残す行の影響を除くため、使用する各テストモジュールは次の2点も守る。

  • functionスコープのautouse fixtureで各テスト前に全テーブルの行を削除する
  • 検証をそのテスト自身が挿入した行へ限定し、他テストの残した行で変わる絶対値アサーションを避ける

テスト用ローカルポートの割り当て

tests/配下でローカルHTTPサーバーを起動するテストは、pytest-xdistの並列実行(-n 4)で ポート衝突によるハング・失敗を避けるため、port=0でOSに空きポートを割り当てさせる。 Flask・Quartのrunが返す実ポートへ接続し、コンテキストを抜けて停止・回収する。起動待ちは間隔と上限を持つ。

後始末と解放を検証するテスト

teardownでの状態の解除や資源の解放を検証するテストは、要求の完了時に後始末まで終える呼び出し方で対象を実行する。 Quartではapp.test_client()をasync withなしで使う。 async with app.test_client()はリクエスト文脈を保持するため、Quart 0.22.0では実リクエストのteardownが実行されないまま終わる。 解除後の状態は観測対象と同じ実行文脈のteardownフックで記録し、記録が空でないことと値の両方を確かめる。 解放の検証は、解放しなければ失敗する入力(上限を超える回数の逐次要求など)で行う。

ドキュメント構成と対応情報の確認

サイトの生成・配布とモジュール追加の手順は開発手順の「ドキュメントサイト運用」を参照する。

  • docs/api/<name>.md、各モジュールのAPI生成設定と必要extra・プラットフォームの注記
  • docs/guide/index.md、extras一覧と唯一のモジュール分類
  • docs/guide/cli.md、各CLIの用途・代表例・ヘルプへの案内
  • mkdocs.yml、navとllmstxt sectionsの同じ分類

scripts/check_docs_api.pyは公開モジュール一覧、extra・対象モジュール・直接依存、コア依存、 allの和集合、APIの必要extra注記、CLI節、8分類の所属が定義元と一致するか確かめる。 pre-commitとCIが同じスクリプトを実行し、不一致では所在を示して非0で終了する。 配布wheelの単独extraでのimportとWebアサーションはscripts/check_extras_imports.pyが隔離環境で検証する。