Technical Document for make_mini_tklib.py

プログラムの動作

make_mini_tklib.py は、Pythonプログラムのエントリーポイントとなるスクリプトから静的にインポートされるモジュールを解析し、特定のPythonパッケージのサブセットを生成することを目的としたツールです。具体的には、指定されたエントリースクリプトがインポートするPythonファイルのうち、指定されたパッケージ (pkg_name) のルートディレクトリ (pkg_root) 配下にあるものを再帰的に収集し、出力ディレクトリ (out) へ元の相対パス構造を保ってコピーします。

このプログラムの主な機能は以下の通りです。

  • 静的依存解析: ast モジュールを用いてPythonソースコードを解析し、import および from ... import ... ステートメントを検出します。

  • パッケージサブセット生成: 解析で検出されたインポートのうち、pkg_root 配下に解決できるファイルのみを選別し、出力ディレクトリへ複製します。これにより、特定のアプリケーションに必要な最小限のパッケージ構造を構築できます。

  • __init__.py ファイルの自動追加: コピーされるPythonファイルを含むディレクトリ階層において、__init__.py ファイルが存在すれば、それらも自動的にコピー対象に含めます。

  • オプション機能:

    • --report-unused: pkg_root 配下に存在するものの、依存解析によって選択されなかったPythonファイルをリストアップし、レポートとして出力します。

    • --dot-out: 収集された依存関係をGraphvizのDOT形式で出力し、視覚化を可能にします。

    • --ignore-if-imports: if ブロック内に記述されたインポート文を無視することで、より厳密な最小サブセットを生成できます。

  • ドライランモード: --dry-run オプションを使用すると、実際にファイルをコピーせずに、どのファイルがコピーされるかを事前に確認できます。

このツールは、大規模なPythonパッケージの中から、特定の用途に合わせた最小限の依存関係を持つサブパッケージを作成したい場合に有用です。これにより、配布物のサイズを削減し、デプロイメントを簡素化できます。

原理

make_mini_tklib.py は、Pythonの標準ライブラリである ast モジュールを利用した静的解析に基づいて動作します。

  1. エントリーポイントからの探索: プログラムは、指定されたエントリースクリプト (--entry) を起点として、依存関係グラフを幅優先探索 (BFS) に近い形で構築します。

  2. インポートの抽出: 各Pythonファイルは ast.parse() によって抽象構文木 (AST) に変換されます。その後、カスタムの ast.NodeVisitor がこのASTを走査し、ast.Importast.ImportFrom ノードを抽出します。

    • --ignore-if-imports オプションが有効な場合、ast.If ノードの内部にあるインポートは無視されます。

  3. モジュールの解決: 抽出されたインポート文は ImportRef オブジェクトとして表現されます。これには、モジュール名、インポートされる名前(from X import a, ba, b)、および相対インポートのレベルが含まれます。

    • 絶対インポートの解決: import foo.barfrom foo.bar import baz のような絶対インポートは、pkg_namepkg_root を基準にしてファイルパスに解決されます。例えば、pkg_name="tklib"pkg_root=/path/to/tklibmodule="tklib.sub.mod" の場合、候補パスとして /path/to/tklib/sub/mod.py/path/to/tklib/sub/mod/__init__.py が生成されます。

    • 相対インポートの解決: from . import modfrom ..sub import mod のような相対インポートは、現在のファイル (current_file) のパスとインポートレベル (level) を基に、相対的なディレクトリを特定し、そこからモジュールパスを解決します。

  4. pkg_root 内のフィルタリング: 解決されたファイルパスのうち、pkg_root ディレクトリの配下に存在するファイルのみが「選択されたファイル」としてキューに追加され、さらなる解析対象となります。is_within() 関数がこの判定に使用されます。

  5. __init__.py の追加: 任意のPythonファイルが選択された場合、そのファイルの親ディレクトリから pkg_root までの間の全てのディレクトリに __init__.py が存在すれば、それらも自動的に選択済みファイルセットに追加されます。これは、Pythonパッケージとして機能するために必要な構造を維持するためです。

  6. コピー: すべての依存関係が解決され、必要なファイルが selected セットに収集された後、これらのファイルは pkg_root からの相対パス構造を保ったまま out_root ディレクトリへコピーされます。

プログラムは、以下のようなパス解決ヘルパー関数群とデータ構造を使用します。

  • norm(p: Path) -> Path: パスを正規化し、絶対パスに解決します。

  • is_within(child: Path, parent: Path) -> bool: child パスが parent パスの配下にあるかを判定します。

  • find_pkg_root_upward(entry: Path, pkg_name: str) -> Optional[Path]: エントリースクリプトの親ディレクトリを遡って pkg_name ディレクトリを探します。

  • default_pkg_root_from_env_or_fallback() -> Optional[Path]: 環境変数 tkProg_Root からデフォルトの pkg_root を探し、見つからなければハードコードされたパスを試します。

  • ImportRef: インポート文の情報を保持するdataclassです。

  • extract_imports(): ファイルから ImportRef リストを抽出します。

  • module_to_candidate_paths_under_root(): モジュール名から pkg_root 配下の候補ファイルパスを生成します。

  • resolve_import_ref_to_files(): ImportRef を具体的なファイルパスに解決します。

必要な非標準ライブラリとインストール方法

make_mini_tklib.py は、Pythonの標準ライブラリのみを使用しており、追加の非標準ライブラリのインストールは不要です。

ただし、--dot-out オプションで生成されるGraphviz DOTファイルを画像としてレンダリングするには、別途Graphvizツールキットをインストールする必要があります。GraphvizはPythonライブラリではなく、システムにインストールするツールです。

Graphvizツールのインストール方法(例:Ubuntu/Debian系Linux):

sudo apt update
sudo apt install graphviz

Graphvizツールのインストール方法(例:macOS):

brew install graphviz

Graphvizツールのインストール方法(例:Windows):

Graphvizの公式ウェブサイトからインストーラーをダウンロードし、指示に従ってインストールしてください。 Graphviz Download

DOTファイルをレンダリングするコマンドの例:

dot -Tpng deps.dot -o deps.png

必要な入力ファイル

  1. エントリーPythonスクリプト:

    • --entry オプションでパスを指定します。

    • プログラムは、このスクリプトから始まる依存関係を解析します。

    • 例: Ne-T_fit.py

  2. ターゲットパッケージのPythonファイル群:

    • --pkg-root オプションで指定されたディレクトリ、または自動検出されたディレクトリ配下にある、Pythonソースファイル (.py) とパッケージ初期化ファイル (__init__.py) です。

    • これらのファイルは、エントリースクリプトからインポートされる可能性があります。

  3. パッケージ名:

    • --pkg-name オプションで指定します(デフォルトは tklib)。

    • この名前は、絶対インポートの解決や、pkg_root の検出に使用されます。

生成される出力ファイル

  1. ミニパッケージのファイル群:

    • --out オプションで指定されたディレクトリ(デフォルトは tklib_sub)の直下に、pkg_root からの相対パスを保ったPythonファイルがコピーされます。

    • これには、__init__.py ファイルも含まれる場合があります。

  2. 未使用ファイルレポート (オプション):

    • --report-unused オプションが指定された場合、出力ディレクトリの直下に unused_files.txt というファイルが生成されます。

    • このファイルには、pkg_root 配下に存在するものの、エントリースクリプトから直接的または間接的にインポートされなかったすべてのPythonファイルのリストが、pkg_root からの相対パスで記述されます。

  3. 依存関係グラフのDOTファイル (オプション):

    • --dot-out <FILE> オプションが指定された場合、指定されたファイルパスにGraphviz DOT形式のテキストファイルが生成されます。

    • このファイルは、make_mini_tklib.py が解析しコピーしたPythonファイル間のインポート依存関係を記述したもので、Graphvizツールを使用して視覚化できます。ノードはファイルパス(pkg_root からの相対パス)、エッジはインポート関係(インポート元 -> インポート先)を示します。

コマンドラインでの使用例 (Usage)

make_mini_tklib.py の基本的な使用法は以下の通りです。

python make_mini_tklib.py --entry <entry_script_path> [OPTIONS]

必須引数:

  • --entry ENTRY: 解析を開始するPythonスクリプトへのパス(例: Ne-T_fit.py)。

オプション引数:

  • --pkg-name PKG_NAME: ターゲットとなるパッケージの名前(デフォルト: "tklib")。

  • --pkg-root PKG_ROOT: パッケージのルートディレクトリへのパス。省略した場合、環境変数 tkProg_Root、ハードコードされたパス、またはエントリースクリプトからの上位ディレクトリ探索によって自動検出されます。

  • --out OUT: 生成されるミニパッケージが出力されるフォルダ(デフォルト: tklib_sub)。

  • --dry-run: 実際にファイルを書き込まずに、コピーされるファイルを表示します。

  • --verbose: スキャン中の詳細なログを出力します。

最小化オプション:

  • --ignore-if-imports: if ブロック内にあるインポート文を無視し、より最小限のサブセットを作成します。

レポートオプション:

  • --report-unused: pkg_root 配下で選択されなかったPythonファイルを unused_files.txt として --out ディレクトリに出力します。

  • --dot-out FILE: 選択されたファイル間の依存関係をGraphviz DOT形式で指定されたファイルに出力します(例: deps.dot)。

コマンドラインでの具体的な使用例

以下の例では、make_mini_tklib.py の様々な使用シナリオと、それぞれのコマンドライン引数がどのような結果をもたらすかを説明します。

前提:

  • Ne-T_fit.pyoptimize_mup.py は、ターゲットパッケージ tklib のモジュールをインポートするエントリースクリプトと仮定します。

  • D:\path\to\tklib または D:\git\tkProg\tklib\python\tklibtklib パッケージのルートディレクトリであると仮定します。


1. デフォルトの tklib パッケージを検索し、ミニパッケージを生成する

この例では、pkg_root を明示せずに、make_mini_tklib.py がデフォルトの検索ロジック(環境変数、ハードコードパス、上位ディレクトリ探索)を使って tklib パッケージのルートを特定します。

python make_mini_tklib.py --entry Ne-T_fit.py --out tklib_sub --verbose
  • 説明: Ne-T_fit.py をエントリーポイントとして、そのスクリプトが依存する tklib パッケージ内のモジュールを収集します。収集されたファイルは tklib_sub ディレクトリにコピーされ、詳細なスキャンログが表示されます。


2. tklib パッケージの場所を明示する場合

tklib パッケージのルートディレクトリが特定の場所にあることを明示的に指定します。

python make_mini_tklib.py --entry Ne-T_fit.py --pkg-root D:\path\to\tklib --out tklib_sub --verbose
  • 説明: D:\path\to\tklibtklib パッケージのルートとして使用し、Ne-T_fit.py からの依存関係を解析します。結果は tklib_sub に出力されます。これにより、pkg_root の自動検出ロジックに依存しない、確実なパス指定が可能です。


3. パッケージ名と場所を明示する場合

pkg_name (この例では tklib) と pkg_root の両方を明示的に指定します。エントリーポイントが optimize_mup.py の場合。

python make_mini_tklib.py --entry optimize_mup.py --pkg-name tklib --pkg-root D:\git\tkProg\tklib\python\tklib --out tklib_sub --verbose
  • 説明: optimize_mup.py をエントリーポイントとし、パッケージ名を tklib、ルートディレクトリを D:\git\tkProg\tklib\python\tklib として解析を行います。これは、複数のパッケージが存在する場合や、デフォルトのパッケージ名以外を使用したい場合に有効です。


4. 何がコピーされるかだけを確認する(ドライラン)

実際にファイルをコピーする前に、どのファイルが選択され、どこにコピーされるかを確認します。

python make_mini_tklib.py --entry optimize_mup.py --pkg-name tklib --pkg-root D:\git\tkProg\tklib\python\tklib --out tklib_sub --dry-run --verbose
  • 説明: --dry-run オプションにより、ファイルは実際にコピーされません。代わりに、[DRY] プレフィックス付きで、コピーされる予定のソースパスとターゲットパスが標準出力に表示されます。これにより、意図しないファイルがコピーされるのを防ぎ、結果を事前に検証できます。


5. if ブロック内のインポートをコピーしない

動的な条件に基づいてインポートされるモジュールを無視し、より厳密な最小サブセットを作成します。

python make_mini_tklib.py --entry optimize_mup.py --pkg-name tklib --pkg-root D:\git\tkProg\tklib\python\tklib --out tklib_sub --ignore-if-imports --verbose
  • 説明: --ignore-if-imports オプションが指定されているため、optimize_mup.py およびその依存関係のPythonファイル内で、if ステートメントのブロック内にあるインポート文は解析対象から除外されます。これにより、実行時に常に必要とされないモジュールをサブセットから除外できます。


6. 未使用ライブラリを報告する

pkg_root 配下のどのファイルがミニパッケージに含まれなかったかをレポートとして出力します。

python make_mini_tklib.py --entry Ne-T_fit.py --out tklib_sub --report-unused
  • 説明: Ne-T_fit.py からの依存関係解析後、tklib_sub ディレクトリに unused_files.txt が生成されます。このファイルには、pkg_root ディレクトリ(自動検出または明示的に指定されたもの)内に存在する全ての .py ファイルのうち、生成されたミニパッケージに含まれなかったもののリストが含まれます。


7. 依存関係を可視化するDOTファイルを出力する

解析された依存関係をGraphviz形式で出力し、後でグラフとしてレンダリングできるようにします。

python make_mini_tklib.py --entry optimize_mup.py --pkg-name tklib --pkg-root D:\git\tkProg\tklib\python\tklib --out tklib_sub --dot-out deps.dot
  • 説明: 実行後、deps.dot というファイルが生成されます。このファイルはGraphvizツールで処理できる形式で、optimize_mup.py とそこからインポートされた tklib 内のモジュール間の依存関係が記述されています。

    • この deps.dot ファイルをPNG画像に変換する例:

      dot -Tpng deps.dot -o deps.png