Pythonパッケージの作成と公開（setup.py, PyPI）

- [PyPI](https://pypi.org/)
  - [Packaging Python Projects — Python Packaging User Guide](https://packaging.python.org/tutorials/packaging-projects/)

`pip install`でインストールできるPythonパッケージのホスティングハブ。読みはパイ ピーアイ。

この記事ではsetuptoolsと`setup.py`を使う。`pyproject.toml`やpoetryについては他の記事をあたってほしい。

## バージョンの設定
pypi.org、test.pypi.orgともに同じ名前（バージョン）のアップロードによる上書きはできない（インデックスは独立）。更新/修正したい場合はバージョンを上げること。

0.1.0-alpha < 0.1.0-alpha1 < 0.1.0-beta < 0.1.0-beta1 < 0.1.0 < 0.1.1

- [PEP 440 -- Version Identification and Dependency Specification | Python.org](https://www.python.org/dev/peps/pep-0440/)

## ディレクトリ構成（例）
- LIBRARY_NAME
  - `setup.py`
  - PACKAGE_NAME
     - `__init__.py`
     - `YOUR_SCRIPT.py`
  - `LICENSE`
  - `README.md`
  - `.gitignore`
  - `MANIFEST.in`（optional）

## ファイルの例

```python:__init__.py
__VERSION__ = '20210903.1'

# from .YOUR_SCRIPT import *
from .YOUR_SCRIPT import some_function
```

```gitignore:.gitignore
__pycache__/

/build
/dist
/*.egg-info
```

## install後の利用例

```python
import PACKAGE_NAME as PKG

PKG.some_function() # defined in YOUR_SCRIPT.py
```

## setup.pyの作成
設定ファイルとして`setup.cfg`を読み込む方法もあるが、ここでは1ファイルの作成で済む、`setup`関数に引数として設定を渡す方法を書いておく。

setup.py

```python
from setuptools import setup, find_packages
import sys
from typing import (
    List,
)

from PACKAGE_NAME import __VERSION__ as VERSION

install_requires: List[str] = [
    # dependencies like requirements.txt
    # 'numpy>=1.20.2', # https://pypi.org/project/numpy/
]

python_version = sys.version_info
if python_version < (3, 7):
    # backport for dataclasses, https://pypi.org/project/dataclasses/
    install_requires += [ 'dataclasses>=0.8' ] 

setup(
    name='LIBRARY_NAME',
    version=VERSION, # '0.1.0-alpha', # == 0.1.0-alpha0 == 0.1.0a0
    # license='MIT',

    # packages=[ 'PACKAGE_NAME', ],
    packages=find_packages(),
    include_package_data=True,

    # entry_points = {
    #     'console_scripts': [
    #         # create `main` function in PACKAGE_NAME/scripts/my_command_module.py
    #         'my_command_name = PACKAGE_NAME.scripts.my_command_module:main',
    #     ],
    # },

    install_requires=install_requires,

    author='YOUR_NAME',
    author_email='YOUR_EMAIL',

    url='YOUR_WEBSITE_OR_REPOSITORY_URL',
    description='SHORT_DESCRIPTION',

    long_description=open('README.md', 'r').read(),
    long_description_content_type='text/markdown',

    classifiers=[
        'Development Status :: 3 - Alpha',
        # 'License :: OSI Approved :: MIT License',
        'Programming Language :: Python',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.6',
        'Programming Language :: Python :: 3.7',
        'Programming Language :: Python :: 3.8',
        'Programming Language :: Python :: 3.9',
    ],
)

```

- `classifiers`の項は該当するものを[Classifiers · PyPI](https://pypi.org/classifiers/)からコピペする
- `entry_points > console_scripts`を追加することでターミナルから実行するコマンドを追加できる（例では`my_command_name`コマンド）
    - [Pythonでグローバルコマンドを含んだパッケージを作る - Qiita](https://qiita.com/fetaro/items/bb0eb8292127b5d1e9a8)

## MANIFEST.in

- 画像など追加のファイルを配布物に含めたい場合、`MANIFEST.in`を作成する
    - https://docs.python.org/ja/3/distutils/sourcedist.html#specifying-the-files-to-distribute
    - 追加したファイルをプログラムから参照するには`pkgutil.get_data`を使う（パッケージインストール後のみ）
        - https://docs.python.org/ja/3/library/pkgutil.html#pkgutil.get_data
        - Pillow画像：`Image.open(BytesIO(pkgutil.get_data('PACKAGE_NAME', 'myimagedir/image.png')))`
    - または、`importlib.resources`が利用できる（Python 3.9以降）
        - https://docs.python.org/ja/3/library/importlib.html#module-importlib.resources

```:MANIFEST.in
include README.md
include LICENSE

include *.txt
recursive-include path/to/dir *.txt *.png
```

## ローカルでビルドして公開する場合（または処理の流れ）

### 事前セットアップ

```sh
pip3 install wheel twine
```

- wheel: wheelパッケージング
- twine: アップロードツール

### パッケージング
```sh
python3 setup.py sdist
python3 setup.py sdist bdist_wheel
```

※ sdistはソースコード配布、bdist_wheelはバイナリ（wheel）配布。単純なPythonスクリプトのみの場合sdistのみでも問題なかった。ビルドが必要なパッケージで事前ビルド済みのバイナリを添付できる、ということと思われる（sdistのみの場合、wheelの生成はpip installしたときにされるみたい）。

### 実験用アップロード（インストールテスト）
pypi.orgと同じように動作する https://test.pypi.org/ を実験的なアップロードに使用できる。pypi.orgとアカウントは独立していて、[こちらの記事](https://qiita.com/shinichi-takii/items/e90dcf7550ef13b047b5#pypi%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E7%99%BB%E9%8C%B2)でおすすめされているようにアカウント名を変えると間違えなくてよさそう。

前述の通り一度アップロードすると同じPyPIリポジトリには（同じバージョンで）上書きできないので、バージョン設定に注意。

```sh
# 特定のsdistのみの場合
twine upload --repository testpypi dist/YOUR_PACKAGE.tar.gz

# dist以下全ファイル
twine upload --repository testpypi dist/*
```

### 本番用アップロード

```sh
# 特定のsdistのみの場合
twine upload --repository pypi dist/YOUR_PACKAGE.tar.gz

# dist以下全ファイル
twine upload --repository pypi dist/*
```


## GitHub Actionsを使う場合
[pypa/gh-action-pypi-publish: GitHub Action, for publishing distribution files to PyPI](https://github.com/pypa/gh-action-pypi-publish)

上のGitHub Actionを使うと便利。

例：[aoirint-miyadaiku-theme-blog/deploy_pypi.yml at master · aoirint/aoirint-miyadaiku-theme-blog](https://github.com/aoirint/aoirint-miyadaiku-theme-blog/blob/master/.github/workflows/deploy_pypi.yml)

### GitHub Release作成時にPyPIにアップロード

- [Add API token · PyPI](https://pypi.org/manage/account/token/)

```yaml
name: Publish a package to PyPI

on:
  release:
    types:
      - created

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Setup Python
        uses: actions/setup-python@v2
        with:
          python-version: 3.x

      - name: Install Dependencies
        run: |
            pip3 install -r requirements.txt
            pip3 install wheel

      - name: Build Package
        run: python3 setup.py sdist bdist_wheel

      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          user: __token__
          password: ${{ secrets.PYPI_API_TOKEN }}
```

## PyPIを経由しないパッケージのインストール
### gitリポジトリからのインストール
PyPIにpushしなくてもGitHubやGitLabのリポジトリ、ローカルのgitリポジトリからインストールすることができる。ディレクトリ構成はPyPIにpushする場合と同じ。

- https://pip.pypa.io/en/stable/reference/pip_install/#git

```sh
pip3 install git+https://github.com/USERNAME/REPOSITORY
pip3 install git+ssh://github.com/USERNAME/REPOSITORY
pip3 install git+file:///home/USER/REPOSITORY
```

ブランチやコミットハッシュを指定することもできるらしい（詳しくは上の公式ドキュメント参照）。

### ファイルシステムからのインストール
gitリポジトリ同様、ファイルシステムからsetup.pyのあるディレクトリを指定してインストールできる。

```sh
# カレントディレクトリからインストール
pip3 install .
```

## 参考
- [PyPIパッケージ公開手順 - Qiita](https://qiita.com/shinichi-takii/items/e90dcf7550ef13b047b5)
- - [How to upload your python package to PyPi - joelbarmettlerUZH - Medium](https://medium.com/@joel.barmettler/how-to-upload-your-python-package-to-pypi-65edc5fe9c56)
- [pythonのsetup.pyについてまとめる - Qiita](https://qiita.com/Tadahiro_Yamamura/items/2cbcd272a96bb3761cc8)
- [PyPIパッケージ定義ファイル作成方法 - __init__.py setup.py MANIFEST.in の書き方 - Qiita](https://qiita.com/shinichi-takii/items/6d1063e0aa3f79e599f0)