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


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 はこんな感じ](https://github.com/ieiriyuki/mypackage/blob/flatlayout/pyproject.toml) になります。
(上記とファイル名などが異なります。)

```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 のタグから自動でバージョンを決めてくれます。
いちいちファイルを修正する必要がないので便利です。


```toml
# 動的生成の利用
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` でその値を参照するようにしています。

```python
# 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は以下の通り](https://github.com/ieiriyuki/mypackage/blob/srclayout/pyproject.toml) です。

`package-dir` のセクションで、`src` 以下をパッケージの対象として探索するように指定しています。
また、`MANIFEST.in` で強引に `src` 外のファイルをパッケージに追加できます。

```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.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 はパッケージを [リモートリポジトリからインストール](https://pip.pypa.io/en/stable/topics/vcs-support/) することができます。

リモートリポジトリがプライベートの場合は、[アクセストークンを使ってインストールする](https://qiita.com/shikumiya_hata/items/fa89126e13c57a997d59) など、適切な認証設定があればインストールすることができます。
アクセストークンは、Dockerfile 内でユーザーや ssh の設定をせずにインストールできるため便利です。

また、今まで知らなかったのですが、[パッケージの設定がリポジトリ直下でなくてもインストールできる](https://pip.pypa.io/en/stable/topics/vcs-support/#url-fragments) んですね。

# 終わり

いかがだったでしょうか。
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