パッケージ開発と動作確認の手順 (PyPI 登録の準備編)

# 前置き

[「最低限の構成編」](https://qiita.com/satamame/items/09780007ad327ed9f24f)では、他のパッケージに依存しないパッケージの作成と、簡単な配布の方法について書きました。
本記事では [PyPI](https://pypi.org) に登録することを前提に、他のパッケージに依存するパッケージの配布についてまとめたいと思います。
情報があちこちに散らばっているので、情報ソースを示しつつ押さえるべきところを押さえていきます。

setuptools について詳しく知りたい方は、[公式のユーザガイド](https://setuptools.readthedocs.io/en/latest/userguide/index.html)を御覧ください。

## 動作確認した環境

Python 3.8.1
pip 20.3.1
setuptools 51.0.0

## パッケージの内容

今回は「最低限の構成編」で扱ったパッケージにユーティリティ関数を1個追加します。
その関数の中で他のパッケージを使うことで、他のパッケージに依存する例として扱っていきます。

かんたんな関数なので "\_\_init\_\_.py" の中に書いてしまいます。NumPy に依存しています。

```python:__init__.py
import numpy as np
from .character import Character

def generate_extras(min_age, max_age, count):
    """Character をたくさん作るためのユーティリティ関数"""
    ages = np.random.randint(min_age, max_age + 1, count)
    for age in ages:
        yield Character(age=age)
```

"\_\_init\_\_.py" 以外のモジュールについては、[GitHub のリポジトリ](https://github.com/satamame/my_pkg)を御覧ください。

## ステップ

1. ディレクトリを構成する
1. インストール可能な状態にする
1. テストする
1. インストールする側を作る (動作確認用)

# ディレクトリを構成する

「最低限の構成編」と違って、パッケージになる部分を "src" フォルダに入れます。
理由は、リポジトリ・ルートでテストを実行する時に、ソースそのものをインポートされたくないからです。

テストの対象はどうするかというと、仮想環境にインストールしたものを使います。
ややこしいですが、**パッケージのリポジトリの中に仮想環境があって、そこにパッケージ自身 (+依存するパッケージ) をインストールする**のです。

他にも PyPI に登録することを考えて、いくつかファイルを追加します。
仮想環境用のフォルダは、あとで `venv` で作るので、今はありません。

```
my_pkg/ (パッケージのリポジトリ)
 ├─ .git/
 ├─ src/
 │   └─ my_pkg/
 │        ├─ __init__.py
 │        ├─ character.py
 │        └─ main_character.py
 ├─ tests/
 │   ├─ __init__.py
 │   └─ test_init_char.py
 ├─ .gitignore
 ├─ LICENSE
 ├─ pyproject.toml
 ├─ README.md
 ├─ requirements_dev.txt
 ├─ setup.cfg
 └─ setup.py
```

## 追加したファイル/ディレクトリ

- **src/**  
    パッケージになる部分のソースを入れるフォルダ。
- **LICENSE**  
    このパッケージのライセンスについて書くファイル。
- **requirements_dev.txt**  
    テスト用の仮想環境を構築するためのファイル。

## "setup.py" の必要性

前回[「"setup.py" を書かなくても良い」と書きました](https://qiita.com/satamame/items/09780007ad327ed9f24f#setuppy-%E3%82%92%E6%9B%B8%E3%81%8F%E3%81%AE%E3%81%AF%E5%8F%A4%E3%81%84%E3%82%84%E3%82%8A%E6%96%B9)が、今回も "setup.py" を残してあります。
理由は、"setup.py" がないと、[編集可能モードでインストール](https://qiita.com/satamame/items/09780007ad327ed9f24f#%E7%B7%A8%E9%9B%86%E5%8F%AF%E8%83%BD%E3%83%A2%E3%83%BC%E3%83%89%E3%81%A7%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8B)することが出来ないからです。
PyPI に登録する等、配布のためのセットでは省略可能です。

### 省略できる条件

setuptools のドキュメントの、[setup.cfg-only projects](https://setuptools.readthedocs.io/en/latest/setuptools.html#setup-cfg-only-projects) というセクションを読むと、以下の条件で省略できるようです (間違っていたらご指摘ください)。

- [PEP 517](https://www.python.org/dev/peps/pep-0517/) を使って配布前にビルドしておけば、インストール時に "setup.py" は必要ありません。
- PEP 517 でビルドする時も、"setup.py" で `setup()` しかしていないなら、省略可能です。
- インストール時にビルドする場合も、"setup.py" で `setup()` しかしていないなら、以下の条件の下で省略可能です。
    1. setuptools のバージョンが 40.9.0 以上である。
    1. かつ、pip のバージョンが 19 以上である。
    1. かつ、"setup.cfg" で `build-backend = "setuptools.build_meta"` を指定している。

ざっくり言うと、特に古いツールにこだわらないなら、編集可能モードでインストールする場合以外は省略可能と言えます。
ただし上記の setup.cfg-only projects セクションの注意書きには、「念のため "setup.py" もあった方が良い」と書かれています。

# インストール可能な状態にする

## pyproject.toml

[前回の pyproject.toml](https://qiita.com/satamame/items/09780007ad327ed9f24f#pyprojecttoml) から、少し変更があります。

```plaintext:pyproject.toml
[build-system]
requires = [
    "setuptools >= 40.9.0",
    "wheel",
]
build-backend = "setuptools.build_meta"
```

違いは setuptools のバージョンを指定していることだけです。
これは上記の「"setup.py" を省略できる条件」を満たすためなので、必須という訳ではありません。

## requirements_dev.txt

テストコードを実行するための仮想環境に、テストの対象となるパッケージをインストールするのに使います。

```plaintext:requirements_dev.txt
-e .
```

`-e` は編集可能モードでインストールするためのオプション、`.` が対象パッケージのパス (=このファイルがあるディレクトリ) です。

||
|:--|
|実は、ちゃんとテストするなら編集可能モードは使わない方が良いです。ただ、テストコードを書きながらコーディングするという作業においては、編集可能モードが楽です (毎回インストールし直さなくて済むので)。このジレンマを解決するのに、tox というツールがあります (後ほど紹介だけします)。|

ちなみにファイル名ですが、なぜ "requirements.txt" にしないかと言うと、依存パッケージを "requirements.txt" に書く (そして "setup.py" で読み込む) という人もいて紛らわしいからです。

### 編集可能モードの落とし穴

テストのための環境にインストールする時、編集可能モードにした方が「ちょっと直してはテストして…」という作業をするには楽です。
ただし、変更が即座に反映されるということは、ソースを直に見ているという事ですので、ここにちょっとした落とし穴があります。

それは、**"setup.cfg" で指定していないサブパッケージも見えてしまう**、という事です。

"setup.cfg" には、どのサブパッケージを配布物に含めるかの指定 (`packages`) があります。
そこで指定していないサブパッケージをテストで使っていると、**「テストは通るが配布物は動かない」**という事があります。

## setup.cfg

setuptools が参照する、パッケージ情報です。

```plaintext:setup.cfg
[metadata]
name = my_pkg
version = 0.1
url = https://github.com/satamame/my_pkg
author = satamame
author-email = satamame@gmail.com
license = MIT
license_file = LICENSE
description = my_pkg: A sample package
long_description = file: README.md
keywords = sample, package, development
classifiers =
    Development Status :: 3 - Alpha
    License :: OSI Approved :: MIT License
    Programming Language :: Python :: 3

[options]
package_dir =
    =src
packages = my_pkg
install_requires =
    numpy==1.19.3
python_requires = ~=3.8
```

[こちら](https://setuptools.readthedocs.io/en/latest/userguide/declarative_config.html)に公式のドキュメントがありますが、これを読んだだけだと (私は) よく分かりません。
"setup.py" と互換性があるので、["setup.py" のドキュメント](https://docs.python.org/ja/3/distutils/setupscript.html) や [PyPA のガイド](https://packaging.python.org/guides/distributing-packages-using-setuptools/) を読むと手がかりになります。
以下、私が把握している範囲で簡単にご説明します。

### metadata

- **url**  
    パッケージ情報の中で、Home-page として表示されます。
- **author**  
    パッケージ情報の中で、Author として表示されます。
- **author-email**  
    パッケージ情報の中で、Author-email として表示されます。
- **license**  
    パッケージ情報の中で、License として表示されます。
- **license_file**  
    ライセンスを記述しているファイルのファイル名。  
    `license = MIT` などと指定している場合は省略しても良いかも知れません。
- **description**  
    パッケージ情報の中で、Summary として表示されます。
- **long_description**  
    PyPI に登録した時、そのプロジェクトのページで「詳しい説明」として表示されます。  
    `file: filename` と書くことでファイルを参照することもできます。ファイル参照にした場合、PyPI に登録する際にファイルのフォーマットに注意が必要です (この記事の最後の方で説明します)。
- **keywords**  
    PyPI で[検索時](https://pypi.org/search/)にキーワードとして参照されます。
- **classifiers**  
    PyPI で検索時にフィルタとして参照されます。

### options

- **package_dir**  
    `パッケージ名=ディレクトリ名` という key=value 式の書き方で、パッケージの配置を列挙します。  
    上記の `=src` というエントリは、パッケージ名 (key 部分) が空なので、"src" ディレクトリがパッケージのルートであることを示します。
- **packages**  
    ビルド/インストールの対象となるパッケージのリストです。
    1. パッケージ名が `package_dir` のいずれかのエントリの key になっていれば、そのディレクトリがパッケージとして参照されます。  
    1. なければ (`=src` のような) key が空のエントリで示されるディレクトリをルートとし、パッケージ名でディレクトリを参照します。  
    1. それもなければ、リポジトリ・ルート ("setup.cfg" があるディレクトリ) の直下をパッケージ名で参照します。  

一般的には `packages = find:` として全てのサブパッケージを含むようにしますので、上記の難しいことを考える必要はありません。`find:` にすれば、前述の「編集可能モードの落とし穴」も考えなくて良くなります。ただし、`find:` を使うには [[options.packages.find]](https://setuptools.readthedocs.io/en/latest/userguide/package_discovery.html#using-find-or-find-packages) という設定を追加せねばなりません。

- **install_requires**  
    本パッケージが依存しているパッケージのリストです。  
    ここに書いたパッケージが (可能なら) 一緒にインストールされるようになります。  
    `importlib; python_version == "2.6"` 等と書くことで、Python が特定のバージョンの時だけインストールしたり出来るようです。
- **python_requires**  
    本パッケージがサポートする Python のバージョンです。  
    [こちら](https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires) に "setup.py" に書く時の書き方があるので、参考になると思います。

### リスト等で指定する値について

`package_dir` は "dict"、`install_requires` は "list-semi" というように、記述の仕方が決まっています。
詳しくは[こちら](https://setuptools.readthedocs.io/en/latest/userguide/declarative_config.html#specifying-values)をご覧ください。
たとえば "list-semi" の場合は "セミコロン区切り" またはそれを "字下げした改行区切り" にして記述します。

セミコロン区切り

```
install_requires = importlib; python_version == "2.6"
```

複数ある場合は、字下げした改行区切りにする

```
install_requires =
    numpy
    importlib; python_version == "2.6"
```

# テストする

テストには [tox](https://tox.readthedocs.io/en/latest/) というパッケージがよく推奨されているのですが、今回は使わずに済まそうと思います。
ただ、いずれは使うようにした方が良いと思っています。

### tox を使った方が良いケース

以下のケースでは、tox を使った方が良いです。

- Python のバージョンを変えながらテストしたい
- 毎回テスト用の仮想環境を作るところから、コマンドひとつで実行 (自動化) したい
    - これにより、編集可能モードではない正しいテストでも楽をしたい

tox について詳しくは[こちら](https://tox.readthedocs.io/en/latest/)を御覧ください。
以下は tox を使わず手動でテストする場合です。

## テストの準備

1. 仮想環境 (.venv) を作る

    ```
    > python -m venv .venv
    ```

2. 仮想環境に入る (下のコマンドは Windows の場合)

    ```
    > .venv/Scripts/activate
    ```

3. パッケージをインストールする。

    ```
    (.venv) > pip install -r requirements_dev.txt
    ```

"requirements_dev.txt" の中で**編集可能モード**を指定しているので、以上の準備は最初のテストをする前に一度やっておけば大丈夫です。
これで、テストコード内の `import my_pkg` は、この仮想環境からのインポートになります。
また、依存関係もインストールされるので、仮想環境には NumPy も入ります。

## テストの実行

```
(.venv) > python -m unittest -v
```

# インストールする側を作る (動作確認用)

前回はいろんなパターンをやりましたが、今回は `pip` で requirements ファイルを使ってインストールするパターンのみにします。
フォルダ構成は以下のようになります。
すでに素の仮想環境 (.venv) があるものとします。

```
.
 ├─ my_pkg/ (パッケージのリポジトリ)
 │   └─ (略)
 └─ install_check/
     ├─ .venv/
     ├─ make_chars.py
     └─ requirements.txt
```

requirements.txt も前回と同様です (パッケージ名が違うだけ)。

```plaintext:requirements.txt
-e ../my_pkg
```

ただしここもテスト環境と同じで、ちゃんと動作確認するなら `-e` をやめて変更のたびにインストールした方が良いです。

## パッケージのインストール

ではインストールしてみましょう。やり方は前回と同じです。

```
(.venv) > pip install -r requirements.txt
```

インストールできたら、パッケージの情報を表示してみましょう。
パッケージ名のアンダースコアが勝手にハイフンに変わりますが、アンダースコアのまま `pip show my_pkg` としても大丈夫です。

```
(.venv) > pip show my-pkg

Name: my-pkg
Version: 0.1
Summary: my_pkg: A sample package
Home-page: https://github.com/satamame/my_pkg
Author: satamame
Author-email: satamame@gmail.com
License: MIT
Location: f:\dev\python\pkg_bp_investigation\my_pkg\src
Requires: numpy
Required-by:
```

## 動作確認

"make_chars.py" は、今回作ったユーティリティ関数 (`generate_extras`) を使うものに変えました。
`MainCharacter` の名前と年齢を入力すると、近い年齢の適当な `Character` を100個作るというスクリプトです。

```python:make_chars.py
from my_pkg.main_character import MainCharacter
from my_pkg import generate_extras

def main():
    print('Making a main character')
    name = input('Name? ')
    age = input('Age? ')

    kwargs = {}
    if name:
        kwargs['name'] = name
    if age.isdigit():
        kwargs['age'] = int(age)
    main_character = MainCharacter(**kwargs)

    extras = generate_extras(
        (age := main_character.age) // 2, age + age // 2, 100)

    print('You made a {}-year-old character named "{}",'.format(
        main_character.age, main_character.name))

    print('and extras with following ages: ', end='')
    print(', '.join([str(e.age) for e in extras]))

if __name__ == '__main__':
    main()
```

実行結果は以下のとおりです。
最初に作った、NumPy に依存するユーティリティ関数が動くことが確認できました。

```
(.venv) > python make_chars.py
Making a main character
Name? ピーター
Age? 18

You made a 18-year-old character named "ピーター",
and extras with following ages: 22, 9, 11, 14, 16, 12, 10, 13, 24, 18, 21, 25, 27, 10, 20, 14, 11, 27, 25, 20, 17, 17, 21, 11, 18, 27, 20, 20, 17, 17, 22, 22, 16, 9, 23, 16, 19, 14, 9, 10, 11, 24, 10, 17, 23, 15, 23, 16, 22, 13, 19, 26, 12, 10, 16, 21, 26, 12, 25, 12, 9, 13, 25, 12, 19, 11, 21, 21, 9, 16, 22, 27, 11, 12, 15, 17, 17, 25, 24, 26, 25, 10, 24, 15, 16, 16, 25, 27, 13, 18, 12, 23, 17, 18, 21, 21, 26, 20, 23, 10
```

# PyPI に登録する前に知っておくべきこと

ここからはおまけです。
本記事はあくまで「PyPI 登録の準備編」ですが、登録そのものについても少し触れておいた方が良いかなと思いました。

## 同じ名前のプロジェクトは二度と作れない

いちばん大事なことは、PyPI では **同じ名前のプロジェクトは二度と作れない** という事です。
練習用の [TestPyPI](https://test.pypi.org/) というサイトもあるのですが、こちらも同じです。
好きな名前のプロジェクトを登録することが出来たら、**決して削除してはいけません**。

### 勘違いしないよう注意すべきこと

1. バージョン番号を変えずに再登録しようとすると「同じ名前のファイルがある」と言われます。
1. そこで PyPI 上のファイル (同じバージョン番号のリリース) を削除すれば良いのかと思うかも知れませんが、**削除しようが何しようが再登録できるようにはなりません**。
1. リリースを削除しても「同じ名前のファイルがある」と言われるので、じゃあプロジェクト自体をいちど削除すれば良いのかと思うかも知れませんが、それも違います。**削除すると、そのプロジェクト名が永久に使えなくなります**。

リリースは削除しても新しいバージョンを出せば済むことですが、プロジェクト自体を削除したら、もう名前を変えるしかありません。

※プロジェクト名を変えて同じ名前のパッケージを配布することは出来ると思います。

## 登録の作業中に気をつけること

PyPI への登録の仕方は、ググればいろいろ見つかります。
[setuptools の Quickstart](https://setuptools.readthedocs.io/en/latest/userguide/quickstart.html) では、[PEP 517](https://www.python.org/dev/peps/pep-0517/) でビルドして [twine](https://pypi.org/project/twine/) でアップロードする方法が紹介されています。
PEP 517 も twine も、グローバル環境に `pip install` して使うのが良いと思います。

### "dist" フォルダの中身を気にかける

twine の解説をしているウェブページでは、だいたい以下のコマンドを紹介しています。

```
> twine upload --repository pypi dist/*
```

これは、"dist" フォルダの中身をすべて PyPI にアップロードするということです。
"dist" フォルダはビルド時に作られるものなので、これで何も間違っていないのですが、**過去にビルドしたものが "dist" フォルダの中に残っていると、それもアップロードされます**。
書かれていることを機械的にやるのでなく、コマンドの意味を考えて、フォルダの中身を気にかけるようにしましょう。

### long_description のフォーマット

"setup.cfg" の中の `long_description` をファイル参照にした場合、ビルド時には言われないのですが、PyPI に登録する時になって「フォーマットがおかしい」と言われることがあります。
エラーメッセージは英語ですが、以下のヘルプを参照せよと言われるので、そのとおりにすれば OK です。

[How can I upload a project description in a different format?](https://pypi.org/help/#description-content-type)

たとえば Markdown で "README.md" というファイルを書いた場合は、"setup.cfg" に以下のように追加します。

```plaintext:setup.cfg
...
long_description = file: README.md
long_description_content_type = text/markdown  # これを追加
...
```

# 参考文献

※リンク先の情報を保証するものではありません。

- [setuptools Quickstart — setuptools 53.0.0 documentation](https://setuptools.readthedocs.io/en/latest/userguide/quickstart.html)
- [2. setup スクリプトを書く — Python 3.9.1 ドキュメント](https://docs.python.org/ja/3/distutils/setupscript.html)
- [Packaging and distributing projects — Python Packaging User Guide](https://packaging.python.org/guides/distributing-packages-using-setuptools/)
- [setuptools: Pythonパッケージ作成 - Heavy Watal](https://heavywatal.github.io/python/setuptools.html)
- [Python: Twine を使って PyPI にパッケージをアップロードする - CUBE SUGAR CONTAINER](https://blog.amedama.jp/entry/2017/12/31/175036)