Python でパッケージを開発して配布する標準的な方法 2023 年編

[Python でパッケージを開発して配布する標準的な方法 - Qiita](https://qiita.com/propella/items/803923b2ff02482242cd) の更新版です。`setup.py` の代わりに `pyproject.toml` というファイルが使われるようになった所が大きな変更点です。

Python コマンドが入っている前提で始めます。ここでは開発中のプロジェクトをインストールできる editable installs という機能を使うので、この機能が入った pip 21.3 以降がインストールされている事をご確認ください。

```shell
$ python --version
Python 3.7.17
$ pip --version
pip 23.0.1 from .pyenv/versions/3.7.17/lib/python3.7/site-packages/pip (python 3.7)
```

## 仮想環境の作成

まず環境が混ざらないように [12. 仮想環境とパッケージ — Python 3.11.4 ドキュメント](https://docs.python.org/ja/3/tutorial/venv.html) を参考に仮想環境を作ります。

```shell
mkdir packaging_tutorial
cd packaging_tutorial
python -m venv .venv --prompt tutorial
source .venv/bin/activate
```

## パッケージを実装する

こんな感じのパッケージを作ります。

```
├── LICENSE
├── README.md
├── corona/
│   └── __init__.py
├── pyproject.toml
└── tests/
```

前と同じですが、一応 `__init__.py` を書きます。まず新型コロナ情報を取得する `__init__.py` の例。

```python
import requests
import sys

def get(country: str) -> str:
    url = f"https://corona-stats.online/{country}?minimal=true"
    response = requests.get(url, headers={'user-agent': 'curl'})
    return response.text

def main() -> None:
    country = sys.argv[1] if len(sys.argv) > 1 else ""
    print(get(country))
```

pyproject.toml はこんな感じです。

```toml
[project]
name = "corona-propella"
version = "0.0.1"
authors = [{ name="Propella", email="propella@example.com" }]
description = "A covoid-19 tracker"
readme = "README.md"
requires-python = ">=3.7"
classifiers = [
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
]

dependencies = [
    "requests ~= 2.23.0",
]

[project.optional-dependencies]
dev = [
    "build",
    "twine",
]

[project.scripts]
corona = "corona:main"

[project.urls]
"Homepage" = "https://github.com/propella/sampleproject"
"Bug Tracker" = "https://github.com/propella/sampleproject/issues"
```

[Declaring project metadata — Python Packaging User Guide](https://packaging.python.org/en/latest/specifications/declaring-project-metadata/) に各項目の詳しい説明があります。特に私が重要だと思うのは以下の項目です。

* `dependencies`: 必要なパッケージを書きます。ここでは `requests` を使っています。
* `project.optional-dependencies`: 開発時だけ使うパッケージを書きます。
* `project.scripts`: このパッケージがコマンドを提供している時に書きます。この例だと、`corona` コマンドを実行すると corona.main 関数が呼ばれます。`python -m corona` でも実行されるので、この機能があると `__main__.py` は不要みたいです。

おもむろに `pip install -e .` してみましょう。`corona` コマンドが使えるようになります。

```shell
(tutorial) $ pip install -e .
(tutorial) $ corona jp
Rank Country    Total Cases  New Cases ▲ Total Deaths New Deaths ▲ Recovered   Active     Critical Cases / 1M pop
1    Japan (JP)   33,803,572                   74,694                          33,728,878       83        269,169
     World       692,178,105         4 ▲    6,903,051              623,750,871 61,524,183    6,438      88,809.10
```

この `-e` オプションはローカルパスを受け取り編集可能 (editable) なままインストールします。ファイルを変更するとインストールされたパッケージも更新されるので開発に便利です。

## 配布パッケージの作成

作成したパッケージを配布するには、 build というツールで配布パッケージを作ります。すでに `pyproject.toml` の `project.optional-dependencies` に配布パッケージを作成する `build` が開発時の依存として指定されているので、開発時パッケージをインストールします。以下のようにパッケージ名(この場合はカレントディレクトリ)に続き `[]` の中にキーを指定します。

```shell
pip install -e .[dev] 
```

パッケージを作成します。

```shell
python -m build
```

すると `dist/` ディレクトリに二つのファイルができます。ソース配布の .tar.gz とビルド配布の .whl です。これらのファイルを受け取った人は pip コマンドでパッケージをインストールする事ができます。さらに次に説明する PyPI を使うと、プロジェクトの名前を指定するだけでインストールできるようになります。

## PyPI に公開する

作ったパッケージは [PyPI](https://pypi.org/) に公開しましょう。初めての方は、test.pypi.org にアカウントを作って練習すると良いです。以下 test.pypi.org の設定で説明します。

事前準備

* 自分のアカウントを登録する。
    * https://test.pypi.org/account/register/
* API token を作成する。
    * https://test.pypi.org/manage/account/#api-tokens
    * この際に scope に Entire account と指定します。
    * この画面を閉じると二度と token をみる事ができなくなるので注意。

作成した token を `~/.pypirc` に保存しておくと、`twine` コマンドが読み取ってくれるようになります。

```
[testpypi]
  username = __token__
  password = pypi-hogehoge
```

test.pypi.org ではなく本番は pypi.org を使うときは上の `[testpypi]` を `[pypi]` とします。
このように、`username` は常に `__token__` です。

PyPI にアップロードするには twine というツールを使います。ここではすでに `project.optional-dependencies` でインストール済みなので、以下でアップロードします。

```shell
python -m twine upload --repository testpypi dist/*
```

もしも `~/.pypirc` にパスワードを書いていない場合はここでユーザ名とパスワードを聞かれるので、ユーザ名に `__token__` パスワードに取得した token を回答します。

ここで https://test.pypi.org/manage/projects/ を参照すると、自動的に新規プロジェクトが作成されている事を確認できます。問題なければ実際にインストール可能か確認してみましょう。適当な venv を作って pip install します。ただ、この段階では test.pypi.org に必要な依存パッケージが無いので実際に動かすとエラーになると思います。

```shell
deactivate
mkdir -p ~/tmp/hoge
cd ~/tmp/hoge
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install --index-url https://test.pypi.org/simple/ --no-deps corona-propella
(色々テストする)
deactivate
```

ここで、`pip install` の引数には以下のような意味があります。

* `--index-url`: test.pypi.org を使う事を指示します。本番 pypi.org では不要です。
* `--no-deps`: もしもあなたのプロジェクトが他のパッケージに依存している場合、test.pypi.org からはインストールできないのでこのオプションを使って依存を無視する事を伝えます。これも本番 pypi.org では不要です。

これでうまく行けばいよいよ本番 pypi.org に公開してください。test.pypi.org との違いは以下です。

* 公開時の `twine upload` コマンドに `--repository` 引数は不要です。
* テスト時の `pip install` コマンドに `--index-url` や `--no-deps` 引数は不要です。

## 参考

* [Packaging Python Projects — Python Packaging User Guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/)
* [Declaring project metadata — Python Packaging User Guide](https://packaging.python.org/en/latest/specifications/declaring-project-metadata/)
* [pip changelog 21.3 (2021-10-11)](https://pip.pypa.io/en/stable/news/#v21-3)
* [Python パッケージングの標準を知ろう - Tech Blog - Recruit Engineer](https://blog.recruit.co.jp/rls/2019-12-25-python-packaging-specs/)