
本文中コード
github.com
flat layoutとsrc layoutについて
Pythonプロジェクトのディレクトリ構成について調べてたところ、flat layoutとsrc layoutという2種類のディレクトリ構成が存在することを知りました。
src レイアウト対フラットレイアウト - Python Packaging User Guide
flat layout
flat layoutはパッケージフォルダをプロジェクトのルート直下に配置するスタイルです。
flat layoutの有名なpythonプロジェクトだと、 pytorch, django, tensorflow があります。
.
├── README.md
├── pyproject.toml
└── my_package/
├── __init__.py
└── module.py
src layout
一方、src layoutはsrcサブディレクトリにパッケージフォルダを配置するスタイルです。
src layoutの有名なpythonプロジェクトだと、transfomers, flask, black があります。
├── README.md
├── pyproject.toml
└── src/
└── my_package/
├── __init__.py
└── module.py
Pythonパッケージを開発する上ではsrc layoutが推奨されている
pytestの公式ドキュメントでは、src layoutが推奨されています。
Good Integration Practices - pytest documentation
Generally, but especially if you use the default import mode prepend, it is strongly suggested to use a src layout. Here, your application root package resides in a sub-directory of your root, i.e. src/mypkg/ instead of mypkg.
PyPA(Python Packaging Authority)のPython Packaging User GuideのGitHubレポジトリでも、src layoutを好むユーザーが多いことが伺えます。
https://github.com/pypa/packaging.python.org/issues/320
Python Packaging User Guideからsrc layoutとflat layoutの特徴のポイントを抜粋すると、以下のように書かれています。
src レイアウト対フラットレイアウト - Python Packaging User Guide
- ソースコードを実行するために、src layoutはインストールステップが必要となるが、flat layoutはインストールステップが不要
- Pythonインタープリタがカレントワーキングディレクトリをインポートパスの先頭に含むため、flat layoutでは開発中のコードを使用してしまう危険があるが、src layoutではインストール済みパッケージを使用することが保証されている
自分はこれらの説明を読んだ時に、何となく雰囲気は分かるけど、自分事として理解できていないモヤモヤがありました。
実際にflat layoutとsrc layoutでパッケージ開発の流れを再現してみて、src layoutがパッケージテストの上で安全であることを理解してみようと思います。
flat layoutでパッケージ開発
パッケージ構成
code-for-blogpost/src_vs_flat_layout/flat_layout at main · nsakki55/code-for-blogpost · GitHub
. ├── mypkg_flat │ ├── __init__.py │ └── math.py ├── tests │ ├── __init__.py │ └── test_math.py ├── pyproject.toml ├── requirements-dev.txt └── tox.ini
mypkg_flat というディレクトリをプロジェクトルート直下に作成しました。
パッケージ内のモジュールであるmath.py には、足し算と引き算を行うadd, substract関数を用意します。
def add(a: float, b: float) -> float: return a + b def substract(a: float, b: float) -> float: return a - b
tests/test_math.py には mypkg_flat パッケージのテストを記述します。
from mypkg_flat.math import add, subtract def test_add(): assert add(2, 3) == 5 def test_subtract(): assert subtract(5, 3) == 2
pyproject.tomlを使用してパッケージビルドを行います。
pyproject.tomlによるパッケージビルドの方法はnikkieさんの記事を参考にしました。
Pythonで自作ライブラリを作るとき、setup.pyに代えてpyproject.tomlを使ってみませんか? - nikkie-ftnextの日記
以下の内容のpyproject.tomlに記述します。mypkg_flat ディレクトリをビルド対象としています。
[project]
name = "mypkg-flat"
version = "0.1.0"
description = "Example package using flat layout"
requires-python = ">=3.11"
dependencies = [
"pytest",
]
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
packages = ["mypkg_flat"]
mypkg_flat パッケージのビルドを行います。distフォルダ内にビルド済みのパッケージファイルが作成されます。
$ python -m build $ ls dist/ > mypkg_flat-0.1.0-py3-none-any.whl mypkg_flat-0.1.0.tar.gz
requirements-dev.txtに mypkg_flat を含めます。
pytest mypkg_flat
パッケージの作成ができたので、toxで作った仮想環境にビルド済みパッケージをインストールしてテストを実行します。
tox.iniに skipdist=true を設定することで、requirements-dev.txtのインストール時にビルドが走らないようにします。
Configuration - tox
[tox]
envlist = py312
skipsdist = true
[testenv]
install_command = pip install --find-links=dist {opts} {packages}
deps = -r requirements-dev.txt
commands =
pytest tests
tox -r コマンドでテストを実行します。 -r オプションをつけて仮想環境を作り直してます。
$ tox -r py312: remove tox env folder /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/flat_layout/.tox/py312 py312: install_deps> pip install --find-links=dist -r requirements-dev.txt py312: commands[0]> pytest tests ================================================================================ test session starts ================================================================================ platform darwin -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 cachedir: .tox/py312/.pytest_cache rootdir: /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/flat_layout configfile: pyproject.toml collected 2 items tests/test_math.py .. [100%] ================================================================================= 2 passed in 0.00s ================================================================================= py312: OK (1.07=setup[0.95]+cmd[0.12] seconds) congratulations :) (1.09 seconds)
開発中コードがパッケージ外から直接importされるのを確認する
パッケージに含まれない開発中のコードが意図せず使用される状況を再現してみます。
math.pyモジュールに掛け算を行うmultiple関数を開発中のコードとして追加します。
def add(a: float, b: float) -> float: return a + b def substract(a: float, b: float) -> float: return a - b def multiple(a: float, b: float) -> float: return a * b
パッケージをテストする tests/test_math.pyにmultiple関数のテストを追加します。
from mypkg_flat.math import add, substract, multiple def test_add(): assert add(2, 3) == 5 def test_subtract(): assert substract(5, 3) == 2 def test_multiple(): assert multiple(2, 5) == 10
パッケージのビルドを行っていない状態から、 mypkg_flat をインストールしてmultipleをimportしようとすると、パッケージに含まれていない関数を読み込もうとしてるのでエラーが発生します。
$ cd dist $ pip install mypkg_flat-0.1.0.tar.gz $ python >>> from mypkg_flat.math import multiple Traceback (most recent call last): File "<stdin>", line 1, in <module> ImportError: cannot import name 'multiple' from 'mypkg_flat.math'
この状態でテストを実行すると奇妙なことが起きます。
開発中コードを含めてビルドを実行してないにも関わらず、テストが通ってしまいます。
$ cd .. # プロジェクトのルートディクトリに移動 $ tox -r py312: remove tox env folder /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/flat_layout/.tox/py312 py312: install_deps> pip install --find-links=dist -r requirements-dev.txt py312: commands[0]> pytest tests ================================================================================ test session starts ================================================================================ platform darwin -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 cachedir: .tox/py312/.pytest_cache rootdir: /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/flat_layout configfile: pyproject.toml collected 3 items tests/test_math.py ... [100%] ================================================================================= 3 passed in 0.01s ================================================================================= py312: OK (1.32=setup[1.19]+cmd[0.13] seconds) congratulations :) (1.36 seconds)
何が起きてるか確認します。
toxで作成されたpython仮想環境に入って、 mypkg_flat パッケージの読み込み先を見てみると、ライブラリディレクトリ内ではなく、mypkg_flatディレクトリ中のコードを直接読みに行ってることがわかります。
$ source .tox/py312/bin/activate $ python >>> import mypkg_flat >>> mypkg_flat.__file__ '/Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/flat_layout/mypkg_flat/__init__.py'
pythonのモジュール読み込みパス一覧を取得すると、カレントディレクトリが先頭にあるのを確認できます。
>>> import sys >>> sys.path ['', '/Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/flat_layout/.tox/py312/lib/python3.12/site-packages']
ドキュメントに記載されているように、pythonではデフォルトのPYTHONPATH設定では、モジュールの読み込みはライブラリディレクトリより、カレントディレクトリが優先されます。
6. Modules — Python 3.13.0 documentation
The directory containing the script being run is placed at the beginning of the search path, ahead of the standard library path. This means that scripts in that directory will be loaded instead of modules of the same name in the library directory.
そのため、パッケージを読み込んでるつもりが、実は開発中のコードを直接読み込んでいる状態が発生してしまいます。
この問題をsrc layoutで解決できることを確認します。
src layoutでパッケージ開発
パッケージ構成
code-for-blogpost/src_vs_flat_layout/src_layout at main · nsakki55/code-for-blogpost · GitHub
. ├── src │ └── mypkg_src │ ├── __init__.py │ └── math.py ├── tests │ ├── __init__.py │ └── test_math.py ├── pyproject.toml ├── requirements-dev.txt └── tox.ini
mypkg_src というディレクトリをsrcサブディレクトリ以下に作成しました。
パッケージ内のコードとテストは mypkg_flat と同じにするので省略します。
pyproject.tomlは以下の設定としました。パッケージのビルド対象をsrcディレクトリにしています。
[project]
name = "mypkg-src"
version = "0.1.0"
description = "Example package using src layout"
requires-python = ">= 3.12"
dependencies = [
"pytest",
]
[build-system]
requires = ["setuptools >= 61.0"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
package-dir = {"" = "src"}
mypkg_src パッケージのビルドを行います。
$ python -m build $ ls dist/ mypkg_src-0.1.0-py3-none-any.whl mypkg_src-0.1.0.tar.gz
requirements-dev.txtに mypkg_src を含めます。
pytest mypkg_src
flat layoutの場合と同様の設定でtoxによるテストを実行します。
mypkg_src パッケージがインストールされ、pytestのコードから mypkg_src パッケージが読み込まれているを確認できます。
$ tox -r py312: remove tox env folder /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/src_layout/.tox/py312 py312: install_deps> pip install --find-links=dist -r requirements-dev.txt py312: commands[0]> pytest tests ================================================================================ test session starts ================================================================================ platform darwin -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 cachedir: .tox/py312/.pytest_cache rootdir: /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/src_layout configfile: pyproject.toml collected 2 items tests/test_math.py .. [100%] ================================================================================= 2 passed in 0.00s ================================================================================= py312: OK (1.62=setup[1.50]+cmd[0.12] seconds) congratulations :) (1.64 seconds)
開発中コードをimportできずテストが失敗することを確認する
flat layoutと同様に、開発中のコードであるmultiple関数を加えた場合の挙動を確認します。
multiple関数を含めたパッケージビルドを行う前に、テストを実行します。
パッケージに含まれていないmultiple関数の読み込みエラーがでて、開発中のコードが使われないことを確認できます。
$ tox -r py312: remove tox env folder /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/src_layout/.tox/py312 py312: install_deps> pip install --find-links=dist -r requirements-dev.txt py312: commands[0]> pytest tests ================================================================================ test session starts ================================================================================ platform darwin -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 cachedir: .tox/py312/.pytest_cache rootdir: /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/src_layout configfile: pyproject.toml collected 3 items tests/test_math.py ..F [100%] ===================================================================================== FAILURES ====================================================================================== ___________________________________________________________________________________ test_multiple ___________________________________________________________________________________ def test_multiple(): > assert multiple(2, 5) == 10 E NameError: name 'multiple' is not defined tests/test_math.py:10: NameError ============================================================================== short test summary info ============================================================================== FAILED tests/test_math.py::test_multiple - NameError: name 'multiple' is not defined ============================================================================ 1 failed, 2 passed in 0.01s ============================================================================ py312: exit 1 (0.13 seconds) /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/src_layout> pytest tests pid=17455 py312: FAIL code 1 (2.17=setup[2.04]+cmd[0.13] seconds) evaluation failed :( (2.20 seconds)
toxで作成された仮想環境に入って、 mypkg_src の読み取り先を見ると、ライブラリディレクトリ内からパッケージが読み込まれているのを確認できます。
$ source .tox/py312/bin/activate $ python >>> import mypkg_src >>> mypkg_src.__file__ '/Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/src_layout/.tox/py312/lib/python3.12/site-packages/mypkg_src/__init__.py'
パッケージをビルドし直してテストを実行すると、テストが成功します。
$ python -m build $ tox -r py312: remove tox env folder /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/flat_layout/.tox/py312 py312: install_deps> pip install --find-links=dist -r requirements-dev.txt py312: commands[0]> pytest tests ================================================================================ test session starts ================================================================================ platform darwin -- Python 3.12.5, pytest-8.3.3, pluggy-1.5.0 cachedir: .tox/py312/.pytest_cache rootdir: /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/flat_layout configfile: pyproject.toml collected 3 items tests/test_math.py ... [100%] ================================================================================= 3 passed in 0.00s ================================================================================= py312: OK (1.92=setup[1.54]+cmd[0.38] seconds) congratulations :) (1.96 seconds)
src layoutではパッケージコードを実行するためにインストールが必要となり、開発中のコードが意図せず実行されるのを防げることを確認できました。
pythonプロジェクトはsrc layoutにすべきなのか?
pythonのパッケージ開発のテスト観点からは、src layoutの方が安全であることは分かりました。
パッケージ管理ツールであるpoetryでプロジェクト作成すると、 デフォルトではflat layoutで作成される一方、src layoutで作成するオプションも提供されています。
Commands | Documentation | Poetry - Python dependency management and packaging made easy
同じくpythonのパッケージ管理ツールであるuvでプロジェクトを作成すると、アプリケーションの場合はflat layout、パッケージの場合は src layoutで作成されます。
Projects | uv
uvの思想に従うなら
- アプリケーションの場合 : flat layout
- パッケージの場合 : src layout
で使い分けるのが今のpython界隈のデファクトスタンダードと言えるのでしょうか?
GitHub Star数が上位のpytonプロジェクトを見てみると flat layoutの構成をとってるものも多い印象です。
ML界隈でよく使われるプロジェクトで探してみると、以下のプロジェクトはflat layoutとなっていました。
これらのプロジェクトがflat layoutを採用してる思想を自分はまだ分かってないです。