pyproject.toml のみを使った python パッケージの書き方について説明します。
setup.py や setup.cfg は不要です。
また poetry なども使いません。
(業務レベルでは使うほうが便利だと思います。)
背景
仕事で複数のリポジトリにわたる開発をしていますが、一部リポジトリはパッケージにしたほうが使いやすいなと思うことが多々ありました。
パッケージの作り方についてはたくさん記事がありますが、setup.py、setup.cfg、 pyproject.toml などのファイルをどう使い分けるのか、初心者にはわかりにくいです。
また近年は pyproject.toml に諸々の設定が集約され始めているため、 pyproject.toml で完結できると嬉しいですね。
今回、pyproject.toml だけで設定できるパッケージのサンプルを作ったので紹介します。
説明しないこと
wheelとか
モジュールのインポートの細かい話
pypiへの登録
flat-layout の場合
リポジトリ直下にインストールするパッケージディレクトリを置く構成を flat-layout といいます。
このとき、mypkg
がモジュールとして利用できるようになります。
project_root_directory
├── pyproject.toml
├── setup.cfg # or setup.py
├── ...
└── mypkg/
├── __init__.py
├── ...
├── module.py
└── subpkg1/
├── __init__.py
├── ...
└── module1.py
直感的にはこれで良いような気がしますが、others/some.py
などがある場合、それもパッケージの内容と判断してしまう場合があります。
この時 MANIFEST.in
などで除外対象を指定する必要があります。
(一応、tests/*.py
などはデフォルトで除外対象にはなっています。)
後述の src-layout のほうが作りやすい気がします。
この場合の pyproject.toml はこんな感じ になります。
(上記とファイル名などが異なります。)
[build-system]
requires = ["setuptools", "setuptools_scm"]
build-backend = "setuptools.build_meta"
[project]
name = "mypackage"
description = "My package description"
readme = "README.md"
license = {file = "LICENSE"}
classifiers = [
"Programming Language :: Python :: 3",
]
dynamic = ["version"]
[tool.setuptools.packages.find]
exclude = ["build", "tests"]
[tool.setuptools.dynamic]
version = {attr = "src.foo.version"}
[tool.setuptools_scm]
write_to = "src/foo/_version.py"
パッケージ名は mypackage なので、pip install mypackage
のようになります。
一方で、パッケージのトップディレクトリは src なので、コード内では import src
になります。
pip install scikit-learn
、import sklearn
みたいな感じです。
参考用にあえて名前を違うものにしていますが、基本同名のほうがいいと思います。
バージョンの自動生成
バージョンを自動で生成するために、setuptools_scm
を使っています。
git のタグから自動でバージョンを決めてくれます。
いちいちファイルを修正する必要がないので便利です。
# 動的生成の利用
dynamic = ["version"]
# src/foo/__init__.py の version を参照する
[tool.setuptools.dynamic]
version = {attr = "src.foo.version"}
# パッケージ作成時に指定のファイルにバージョン情報を書き込む
[tool.setuptools_scm]
write_to = "src/foo/_version.py"
上記のように、src/foo/_version.py
にバージョン情報を自動で書き込み、foo/__init__.py
でその値を参照するようにしています。
# src/foo/__init__.py
from ._version import version
src-layout の場合
numpy や pandas で使われている構成です。
パッケージ自体のコードと、その他のコードを分けやすいので、こちらのほうが使いやすい気がします。
project_root_directory
├── pyproject.toml
├── setup.cfg # or setup.py
├── ...
└── src/
└── mypkg/
├── __init__.py
├── ...
├── module.py
└── subpkg1/
├── __init__.py
├── ...
└── module1.py
この時の pyproject.tomlは以下の通り です。
package-dir
のセクションで、src
以下をパッケージの対象として探索するように指定しています。
また、MANIFEST.in
で強引に src
外のファイルをパッケージに追加できます。
[build-system]
requires = ["setuptools", "setuptools_scm"]
build-backend = "setuptools.build_meta"
[project]
name = "mypackage"
description = "My package description"
readme = "README.md"
license = {file = "LICENSE"}
classifiers = [
"Programming Language :: Python :: 3.10",
]
requires-python = "==3.10.*"
dependencies = [
"numpy~=1.21"
]
dynamic = ["version"]
[project.optional-dependencies]
dev = [
"pytest",
"flake8",
"mypy",
"black",
"isort"
]
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.dynamic]
version = {attr = "foo.version.version"}
[tool.setuptools_scm]
write_to = "src/foo/version.py"
version_scheme = "python-simplified-semver"
local_scheme = "no-local-version"
[tools.black]
line-length = 100
[tool.isort]
profile = "black"
[tools.flake8]
max-line-length = 100
python のバージョンは requires-python
で指定できます。
パッケージが依存するパッケージは dependencies
で指定できます。
requirements.txt
のようなファイルは直接指定できないようです。
dev パッケージ
pip 自体には、pipenv や poetry のように 開発用パッケージをインストールするコマンド引数はありません。
代わりに、 project.optional-dependencies
に dev
という項目を作って、その時に必要なパッケージを記載しています。
pip install mypackage[dev]
とすれば dev オプションに必要なパッケージもインストールできます。
(開発時は -e オプションでエディタブルモードのインストールをするほうがいいですが)
コンフィグ
black や mypy などは pyproject.toml でコンフィグ設定できます。
flake8 は現状できませんが、pyproject-flake8 というラッパーを使えば設定できるそうです。
VCS インストール
pip はパッケージを リモートリポジトリからインストール することができます。
リモートリポジトリがプライベートの場合は、アクセストークンを使ってインストールする など、適切な認証設定があればインストールすることができます。
アクセストークンは、Dockerfile 内でユーザーや ssh の設定をせずにインストールできるため便利です。
また、今まで知らなかったのですが、パッケージの設定がリポジトリ直下でなくてもインストールできる んですね。
終わり
いかがだったでしょうか。
setup.py も setup.cfg も必要ないので簡単ですね。
ただ pyproject.toml は比較的新しいフォーマットで、 setuptools のドキュメントでは、Experimental と記載されている箇所も多いです。
仕様や挙動が変わる可能性があるため、留意してもらえれば幸いです。
また、主要な python パッケージはなんだかんだ setup.py や setup.cfg も使っているので、上手に使い分けたほうがよいかもしれませんね。
本記事が参考になったら幸いです。
参考ドキュメント
https://setuptools.pypa.io/en/latest/userguide/package_discovery.html
https://setuptools.pypa.io/en/latest/userguide/dependency_management.html
https://packaging.python.org/en/latest/tutorials/packaging-projects/#creating-the-package-files
https://github.com/pypa/sampleproject
https://github.com/pypa/setuptools_scm