src レイアウト対フラットレイアウト¶
「フラットレイアウト」とは、さまざまな設定ファイルや インポートパッケージ をすべてトップレベルのディレクトリに置くようなやり方で、プロジェクトのファイル群をひとつのフォルダまたはリポジトリに配置することです。
.
├── README.md
├── noxfile.py
├── pyproject.toml
├── setup.py
├── awesome_package/
│ ├── __init__.py
│ └── module.py
└── tools/
├── generate_awesomeness.py
└── decrease_world_suck.py
「src レイアウト」は、インポート可能 (すなわち import awesome_package 、別名 インポートパッケージ) にするつもりのソースコードをサブディレクトリに置く点でフラットレイアウトとは異なります。このサブディレクトリは、典型的には src/ と命名されるので、「src レイアウト」と呼ばれるのです。
.
├── README.md
├── noxfile.py
├── pyproject.toml
├── setup.py
├── src/
│ └── awesome_package/
│ ├── __init__.py
│ └── module.py
└── tools/
├── generate_awesomeness.py
└── decrease_world_suck.py
ここで、src レイアウトとフラットレイアウトの動作の違いで重要なものを掲出しておきましょう:
src レイアウトではそのソースコードを走らせるためにプロジェクトのインストールが要求されますが、フラットレイアウトではそのようなことはありません。
これが意味するところは、src レイアウトの場合にはプロジェクトの開発ワークフローに追加的なステップ (典型的には、開発には 編集可能なインストール を使い、テストには通常のインストールを用いる) が必要になるということです。
src レイアウトを採用することは、今まさに開発中のソースコードを使ってしまうという事故を防ぐことを助けます。
Python インタープリタはカレントワーキングディレクトリをインポートパスの先頭に含むので、これは妥当なことです。これが意味するところは、もしインストール済みのパッケージと同名のものがカレントワーキングディレクトリに存在するならば、カレントワーキングディレクトリにあるものが使われるであろうということです。これによって、配布物に一部ファイル群が含まれない結果に終わるという、プロジェクトのパッケージングツールの微妙な誤設定をもたらしかねません。
src レイアウトを使えば、パッケージ群をプロジェクトのルートディレクトリとは異なるディレクトリに置くので、インストール済みのパッケージの方を使用することが保証され、このような誤設定を避ける助けになります。
src レイアウトを使うことで、インポートしようと意図した 編集可能なインストール だけをインポートするように強制することを助けます。
これは、編集可能なインストール (のパッケージ) がインポートパスにそのディレクトリを追加するように動く パス設定ファイル を使って実装されている場合に、特に適切です。
フラットレイアウトでは、インポートパスに他のプロジェクトファイル群 (例えば
README.mdやtox.ini) や、パッケージング/ツール使用の設定ファイル (例えばsetup.pyやnoxfile.py) を追加します。こうすることによって、あるインポートが、通常のインストールではなくて編集可能なインストールの側を使うことを確実にするでしょう。
src レイアウト付きのソースコードからコマンドラインインターフェースを走らせる¶
最初に述べた src レイアウトの特殊性の故に、コマンドラインインタフェースは、 ソースコードツリー から直接に起動することができず、試験目的のために 開発モード開発モード でインストールされたパッケージを要求します。状況によってはこれは現実的ではなくなるので、 __main__.py ファイル経由で呼ばれた時にパッケージフォルダを Python の sys.path の先頭に追加することが回避策になるかもしれません。
import os
import sys
if not __package__:
# Make CLI runnable from source tree with
# python src/package
package_source_path = os.path.dirname(os.path.dirname(__file__))
sys.path.insert(0, package_source_path)