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

(この記事の情報は古いです。最近は pip が myproject.toml を理解できるようになりました。[Python でパッケージを開発して配布する標準的な方法 2023 年編](https://qiita.com/propella/items/5cd89caee6379920d889) に更新版を書きましたので参考にしてください。)

今更ながら Python でパッケージを開発したり配布する標準の方法を知らなかったので調べました。Poetry の方が簡単ですが、人の作ったプロジェクトで仕事するのに覚えておくと便利です。

## 仮想環境の作成

Python 3.7.7 等の最新の Python が入っていて python コマンドで実行出来るようになっている状態から始めます。これからパッケージを開発するために、他の Python プログラムと利用ライブラリが混ざらないように最初に仮想環境を作ります。仮想環境を作る標準の方法は venv モジュール https://docs.python.org/ja/3/tutorial/venv.html を使う事です。仮想環境の場所に決まりは無いですが、よく `.venv` フォルダが使われます。

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

最後の `source .venv/bin/activate` が環境に入るコマンドです。抜けるには単に `deactivate` と入力します。上記 `--prompt` オプションは無くても良いですが、あるとプロンプトにその文字が出るので、今自分が何を使っているかわかりやすいです。

## 開発に使うライブラリのインストール (無くても良いかも)

開発に使うライブラリは `requirements.txt` というファイルに書きます。ここでは requests というパッケージを使います。

    requests ~= 2.23.0

requirements.txt に書いたパッケージをインストールするには、以下のコマンドを使います。

    pip install -r requirements.txt 

こうすると、有効な仮想環境 `.venv` に必要なライブラリがインストールされます。すでに色々 `pip install` してしまって何を入れたか忘れていたら、

    pip freeze > requirements.txt

のように現在の環境から requirements.txt を作れます。

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

今回次のようなディレクトリ構成を使います。

```
packaging_tutorial/
  corona/
    __init__.py
    __main__.py
  tests/
  requirements.txt
  setup.py
  LICENSE
  README.md
```

`corona/__init__.py` の例を示します。最近流行りの新型コロナ情報を取得する `get()` 関数を実装してみました。コマンドラインから簡単に実行出来るよう `main()` も用意しておきます。

```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))
```

`pythonm -m` で `main()` を呼ぶために `corona/__main__.py` を書いておきます。

```python
from . import main
main()
```

試しに実行。

```shell
% python -m corona jp
Rank Country    Total Cases  New Cases ▲ Total Deaths New Deaths ▲ Recovered Active  Critical Cases / 1M pop
1    Japan (JP)        2,495                       62                    472   1,961       60             20
     World         1,015,466       523 ▲       53,190         24 ▲   212,229 750,047   37,696         130.29

```

## setup.py を書いてパッケージを完成させる。

setup.py というファイルを作って、パッケージの情報を書きます。細かい仕様は https://packaging.python.org/guides/distributing-packages-using-setuptools/ に書いてあります。ポイントとしては、

* `install_requires` にパッケージが依存するパッケージを書くと pip が自動インストールしてくれます。
* `entry_points` の `console_scripts` に `コマンド名=モジュール:関数` を書くと、このパッケージを使ったコマンドが生成されます。

```python
import setuptools

with open("README.md", "r") as fh:
    long_description = fh.read()

setuptools.setup(
    name="corona-propella", # Replace with your own username
    version="0.0.1",
    install_requires=[
        "requests",
    ],
    entry_points={
        'console_scripts': [
            'corona=corona:main',
        ],
    },
    author="Propella",
    author_email="propella@example.com",
    description="A covoid-19 tracker",
    long_description=long_description,
    long_description_content_type="text/markdown",
    url="https://github.com/pypa/sampleproject",
    packages=setuptools.find_packages(),
    classifiers=[
        "Programming Language :: Python :: 3",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
    ],
    python_requires='>=3.7',
)
```

これでパッケージの完成なので、この環境にインストールして実行してみます。

```shell
pip install -e .
rehash
corona
```

この `pip install -e` コマンドは開発モードでパッケージをインストールするので、ソースコードを変更するとすぐ反映されて便利です。

## 配布物を作る

作ったパッケージを配布出来るようまとめます。標準は wheel というフォーマットです。
必要なコマンドをインストールして dist に whl ファイルを作ります。

```
pip install --upgrade pip setuptools wheel
python setup.py bdist_wheel
```

`dist/corona_propella-0.0.1-py3-none-any.whl` のようなファイルが配布物です。これを pip でインストール出来ます。

## 配布物のテスト

別の仮想環境を作って配布物をテストしてみます。

```
deactivate
cd ..
mkdir packaging_test
cd packaging_test
python -m venv .venv
source .venv/bin/activate
pip install ../packaging_tutorial/dist/corona_propella-0.0.1-py3-none-any.whl
python -m corona jp
```

これで依存パッケージと共に先程作ったパッケージが仮想環境にインストールされます。スムーズ。。。

## Egg の作成

Wheel の代わりに昔使われていたらしい Egg というフォーマットを作る事も出来ます。

    python setup.py bdist_egg

Egg のインストールには pip ではなく easy_install を使います。

    easy_install ../packaging_tutorial/dist/corona_propella-0.0.1-py3.7.egg

## 様々な後片付け

setup.py が作ったファイルを消す

    python setup.py clean --all

仮想環境を消す

    rm -r .venv

## requirements.txt と setup.py の関係

ご紹介した方法では、必要なパッケージを requirements.txt と setup.py の install_requires の両方に書くという事になってしまい冗長です。https://caremad.io/posts/2013/07/setup-vs-requirement/ によると作法としては、次のような違いがあるとなっています。

* requirements.txt
    * Python アプリに必要な依存を具体的に書く。
    * ダウンロードする場所や、具体的なバージョンをはっきり書く。
    * 開発時と同じ環境をデプロイ時に構築出来るようにする。
* setup.py の install_requires
    * Python ライブラリに必要な依存を抽象的に書く。
    * 出来るだけ幅広い環境に対応出来るように多くのバージョンに対応させる。

ライブラリを作るときは `pip install -e .` で依存パッケージもインストールされるので、requirements.txt 不要だと思います。

## `extras_require`

たまに setup.py に `extras_require` が指定されている事があります。

```python
    extras_require={
        "dev": ["pytest", "mypy", "pylint", "boto3", "pandas"],
        "jupyter": ["jupyter", "pandas"],
    },
```

これは開発向けなど、必要な時だけインストールするパッケージを表しています。これをインストールするには、パッケージ名の後ろに `[]` を書いてオプション名を指定します。例えばローカルに "jupyter", "pandas" をインストールするには、

    pip install -e .[jupyter]

のようにします。

## 参考

* 仮想環境とパッケージ:
    * https://docs.python.org/ja/3/tutorial/venv.html
* Packaging Python Projects (setup.py を書いて配布物を作る方法)
    * https://packaging.python.org/tutorials/packaging-projects/
* Packaging and distributing projects (setup.py の仕様)
    * https://packaging.python.org/guides/distributing-packages-using-setuptools/
* Requirements file format (requirements.txt の仕様):
    * https://pip.pypa.io/en/stable/reference/pip_install/#requirements-file-format
* setup.py vs requirements.txt: (setup.py と requirements.txt の違い):
    * https://caremad.io/posts/2013/07/setup-vs-requirement/
* Building and Distributing Packages with Setuptools (setup.py で Wheel や Egg を作る):
    * https://setuptools.readthedocs.io/en/latest/setuptools.html
* ソースコード配布物を作成する (sdist の説明)
    * https://docs.python.org/ja/3/distutils/sourcedist.html
* extras_require の解説
    * https://setuptools.readthedocs.io/en/latest/userguide/dependency_management.html#optional-dependencies