
本文中コード
github.com
Pythonプロジェクトのディレクトリ構成について調べてたところ、flat layoutとsrc layoutという2種類のディレクトリ構成が存在することを知りました。
src レイアウト対フラットレイアウト - Python Packaging User Guide
flat layoutはパッケージフォルダをプロジェクトのルート直下に配置するスタイルです。
flat layoutの有名なpythonプロジェクトだと、pytorch,django,tensorflow があります。
.├── README.md├── pyproject.toml└── my_package/ ├── __init__.py └── module.py
一方、src layoutはsrcサブディレクトリにパッケージフォルダを配置するスタイルです。
src layoutの有名なpythonプロジェクトだと、transfomers,flask,black があります。
├── README.md├── pyproject.toml└── src/ └── my_package/ ├── __init__.py └── module.py
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
自分はこれらの説明を読んだ時に、何となく雰囲気は分かるけど、自分事として理解できていないモヤモヤがありました。
実際にflat layoutとsrc layoutでパッケージ開発の流れを再現してみて、src 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関数を用意します。
defadd(a:float, b:float) ->float:return a + bdefsubstract(a:float, b:float) ->float:return a - b
tests/test_math.py にはmypkg_flat パッケージのテストを記述します。
from mypkg_flat.mathimport add, subtractdeftest_add():assert add(2,3) ==5deftest_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 を含めます。
pytestmypkg_flat
パッケージの作成ができたので、toxで作った仮想環境にビルド済みパッケージをインストールしてテストを実行します。
tox.iniにskipdist=true を設定することで、requirements-dev.txtのインストール時にビルドが走らないようにします。
Configuration - tox
[tox]envlist = py312skipsdist = true[testenv]install_command = pip install --find-links=dist {opts} {packages}deps = -r requirements-dev.txtcommands = pytest teststox -r コマンドでテストを実行します。 -r オプションをつけて仮想環境を作り直してます。
$ tox -rpy312: remove tox env folder /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/flat_layout/.tox/py312py312: install_deps> pip install --find-links=dist -r requirements-dev.txtpy312: commands[0]> pytest tests================================================================================test session starts ================================================================================platform darwin -- Python3.12.5, pytest-8.3.3, pluggy-1.5.0cachedir: .tox/py312/.pytest_cacherootdir: /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/flat_layoutconfigfile: pyproject.tomlcollected2 itemstests/test_math.py .. [100%]=================================================================================2 passedin0.00s ================================================================================= py312: OK (1.07=setup[0.95]+cmd[0.12] seconds) congratulations :) (1.09 seconds)
パッケージに含まれない開発中のコードが意図せず使用される状況を再現してみます。
math.pyモジュールに掛け算を行うmultiple関数を開発中のコードとして追加します。
defadd(a:float, b:float) ->float:return a + bdefsubstract(a:float, b:float) ->float:return a - bdefmultiple(a:float, b:float) ->float:return a * b
パッケージをテストする tests/test_math.pyにmultiple関数のテストを追加します。
from mypkg_flat.mathimport add, substract, multipledeftest_add():assert add(2,3) ==5deftest_subtract():assert substract(5,3) ==2deftest_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 multipleTraceback (most recent call last): File"<stdin>", line1,in<module>ImportError: cannot import name'multiple' from'mypkg_flat.math'
この状態でテストを実行すると奇妙なことが起きます。
開発中コードを含めてビルドを実行してないにも関わらず、テストが通ってしまいます。
$cd .. # プロジェクトのルートディクトリに移動$ tox -rpy312: remove tox env folder /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/flat_layout/.tox/py312py312: install_deps> pip install --find-links=dist -r requirements-dev.txtpy312: commands[0]> pytest tests================================================================================test session starts ================================================================================platform darwin -- Python3.12.5, pytest-8.3.3, pluggy-1.5.0cachedir: .tox/py312/.pytest_cacherootdir: /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/flat_layoutconfigfile: pyproject.tomlcollected3 itemstests/test_math.py ... [100%]=================================================================================3 passedin0.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 thatscripts in that directory will be loaded instead of modules of the same name in the library directory.
そのため、パッケージを読み込んでるつもりが、実は開発中のコードを直接読み込んでいる状態が発生してしまいます。
この問題を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 を含めます。
pytestmypkg_src
flat layoutの場合と同様の設定でtoxによるテストを実行します。mypkg_src パッケージがインストールされ、pytestのコードからmypkg_src パッケージが読み込まれているを確認できます。
$ tox -rpy312: remove tox env folder /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/src_layout/.tox/py312py312: install_deps> pip install --find-links=dist -r requirements-dev.txtpy312: commands[0]> pytest tests================================================================================test session starts ================================================================================platform darwin -- Python3.12.5, pytest-8.3.3, pluggy-1.5.0cachedir: .tox/py312/.pytest_cacherootdir: /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/src_layoutconfigfile: pyproject.tomlcollected2 itemstests/test_math.py .. [100%]=================================================================================2 passedin0.00s ================================================================================= py312: OK (1.62=setup[1.50]+cmd[0.12] seconds) congratulations :) (1.64 seconds)
flat layoutと同様に、開発中のコードであるmultiple関数を加えた場合の挙動を確認します。
multiple関数を含めたパッケージビルドを行う前に、テストを実行します。
パッケージに含まれていないmultiple関数の読み込みエラーがでて、開発中のコードが使われないことを確認できます。
$ tox -rpy312: remove tox env folder /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/src_layout/.tox/py312py312: install_deps> pip install --find-links=dist -r requirements-dev.txtpy312: commands[0]> pytest tests================================================================================test session starts ================================================================================platform darwin -- Python3.12.5, pytest-8.3.3, pluggy-1.5.0cachedir: .tox/py312/.pytest_cacherootdir: /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/src_layoutconfigfile: pyproject.tomlcollected3 itemstests/test_math.py ..F [100%]===================================================================================== FAILURES ======================================================================================___________________________________________________________________________________ test_multiple ___________________________________________________________________________________ def test_multiple():> assert multiple(2,5) ==10E NameError: name'multiple' is not definedtests/test_math.py:10: NameError============================================================================== shorttest summary info ==============================================================================FAILED tests/test_math.py::test_multiple- NameError: name'multiple' is not defined============================================================================1 failed,2 passedin0.01s ============================================================================py312:exit1 (0.13 seconds) /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/src_layout> pytest tests pid=17455 py312: FAIL code1 (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 -rpy312: remove tox env folder /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/flat_layout/.tox/py312py312: install_deps> pip install --find-links=dist -r requirements-dev.txtpy312: commands[0]> pytest tests================================================================================test session starts ================================================================================platform darwin -- Python3.12.5, pytest-8.3.3, pluggy-1.5.0cachedir: .tox/py312/.pytest_cacherootdir: /Users/satsuki/github/code-for-blogpost/src_vs_flat_layout/flat_layoutconfigfile: pyproject.tomlcollected3 itemstests/test_math.py ... [100%]=================================================================================3 passedin0.00s ================================================================================= py312: OK (1.92=setup[1.54]+cmd[0.38] seconds) congratulations :) (1.96 seconds)
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の思想に従うなら
で使い分けるのが今のpython界隈のデファクトスタンダードと言えるのでしょうか?
GitHub Star数が上位のpytonプロジェクトを見てみるとflat layoutの構成をとってるものも多い印象です。
ML界隈でよく使われるプロジェクトで探してみると、以下のプロジェクトはflat layoutとなっていました。
これらのプロジェクトがflat layoutを採用してる思想を自分はまだ分かってないです。
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。