97
69

More than 1 year has passed since last update.

2022年版pyproject.tomlを使ったPythonパッケージの作り方

Posted at

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-learnimport 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-dependenciesdev という項目を作って、その時に必要なパッケージを記載しています。

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

97
69
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
97
69