【Python】PyPI登録のコツ

#～sphinxcontrib.xxxの場合～

``pypi`` を「ぱいぱい」と読んでしまうのは私だけでしょうか…

#１．はじめに

人生初めてのPyPi登録を果たしました。梃子摺った点を中心に、PyPi登録のコツを紹介します。

###登録内容の特徴

- site-pakcages直下ではなく、site-packages/sphinxcontrib以下に登録したい
- pythonコードのみ
- コマンドとして実行したいpython関数がある

#２．コツの一覧
###📝バージョン表記
我流のバージョン表記で登録しようとしたら早速注意されました。

PEP440ですが、こちらの記事がわかりやすいです。

https://zenn.dev/shinsa82/articles/pep-440-overview

これを押さえた上での私の流儀。

- ``x.y.z[.n]``
    - ``x`` とりあえず最初は0。何らかの峠を超えたら1に変更。
    - ``y`` 機能追加なりの仕様の変更
    - ``x`` 不具合の修正
    - ``n`` README.rstの修正など、プログラムコードに影響のない変更（オプション）
- ``0.16.9`` 「現在の版」の例として。（以降も参考例です）
- ``0.17.0.dev1`` 機能追加に着手 
- ``0.17.0a1`` 取り敢えず完成。テストはしていない。コンパイルは通る。
- ``0.17.0b1`` 正常系のテストは済んでいる状態
- ``0.17.0rc1`` 異常系のテストも済んでtestpypiサイトを使って確認
- ``0.17.0`` testpypiを使った確認も済んでpypiにアップ
- ``0.17.0.1rc1`` ドキュメントの更新の確認
- ``0.17.0.1`` ドキュメントの更新

常に全て使うわけではありません。私のやり方だと ``.postN`` は使わず、 ``.devN``, ``aN``, ``bN`` についてはTestPyPIにもアップされない感じです。``rcN`` はTestPyPIのみ。

記事を読んで異なる流儀にする方もいると思います。

###✒アップロード前にチェック
``README.rst`` の構文エラーは、 ``twine check {アップする予定のファイル}`` を実行することで分かりました。

現在、次の流れが確立されています。

1. ``python setup.py sdist``
1. ``twien check dist/the_package.tar.gz``
1. ``upload.sh testpypi dist/the_package.tar.gz``
1. 案内された test.pypi.org にアクセスして ``pipコマンド`` のコピペ
1. コマンド実行→気になっている箇所の確認→rcを外してアップ
1. ``upload.sh pypi dist/the_package.tar.gz``

###⚠``packages``, ``package_dir`` と ``namespace_packages``
ハマっていました。長らく…

namespace_packages が不要なのに、「必要」だと勘違いして拘っていたのが問題でした。お手本にしていたパッケージでは使われた形跡があり、試行錯誤しつつも ``setup.cfg`` に書くことにも拘っていたため、関連する要因を整理できていませんでした。

> 恐らく開発環境でも ``shinxcontrib.kana_text`` と同じディレクトリ構成にする場合には必要になるパラメータ（憶測）で、開発環境では ``src`` ディレクトリ以下に作るなら不要なパラメータです。

###⚠include_package_dataを忘れない
提供するパッケージに.pyファイル以外のファイルがある場合、このファイルをインストール環境に展開するために必要です。

MANIFEST.in指定するとパッケージに取り込んで、``include_package_data = True`` でインストール環境に展開するようです。たぶん…

###⚠README.rstは凝リ過ぎない
ハマってました…

``setup.cfg`` とのコンボとも言えますが、rst構文エラーがある場合はエラーを出して止まらずにREADME.rstを除外してパッケージを作りやがります。不備がある場合は ``twine check`` でわかります。

###⚠ ``sphinxcontrib/__init__.py`` を作る
ハマってました…

``sphinxcontrib/kana_text`` として登録するために、このツナギのようなこのファイルが必要です。資料としては見つからず、先達の状況を ``pip download`` したファイルを調べることでわかりました。

> 現在はそもそも実行環境に合わせたディレクトリ構成にはしておらず、 ``packages``, ``package_dir`` を適切に設定することで不要になっています。内容については「ファイルの紹介」で。

###⚠ ``python setup.py develop`` 
ハマってました…

初回ということもあり分かっていませんが、これはアップの時には必須ではありません。「開発環境でわざわざパッケージの検索パスを追加することなく、site-packages以下にあるかのように見せかける」ための仕組みのようです。これを、作成される構成ファイルの確認として使っていました。

これが残っていると、「バッケージがインストールされていないのに、インストールされているようかのようなメッセージを ``pip uninstall`` が返す」という現象が起こります。これの原因がわからず悩まされました。

用が済んだら、このディレクトリを削除しています。対処用のコマンドがあるのかもしれません。（知っていたら教えてください）

#２．ファイルの紹介

###📕全体像

``__pycache__`` と、 ``dist`` から最新版以外を除いています。
``~/.pypirc`` の実体は、他にpypiプロジェクトがあるため一つ上位のディレクトリに置いています。

```
kana_text/
├── AUTHORS
├── CHANGES.rst
├── LICENSE
├── MANIFEST.in
├── README.rst
├── dist
│   └── sphinxcontrib.kana_text-0.20.1.7.tar.gz
├── purge.sh（※"python setup.py develop"で作成されるファイルの確認）
├── setup.cfg
├── setup.py
├── sphinxcontrib.kana_text.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   ├── entry_points.txt
│   ├── requires.txt
│   └── top_level.txt
├── src
│   ├── __init__.py
│   ├── command.py
│   └── templates
│       └── genindex.html.sample
├── testall.sh（※doctest実行用）
└── tests
    ├── __init__.py
    ├── unittest_01.py
    └── unittest_02.py
```

次の記事に依ると、setuptoolsは30辺りから ``setup.cfg`` を使う流れになっているようです。これに倣って、やり方がわからない設定以外は ``setup.cfg`` 側で設定しています。

https://astropengu.in/posts/23/

``setup.py`` から ``setup.cfg`` を作る方法も紹介されています。

###📗 ``setup.py``

（注意）
　以下の「import src as kt」は間違いです。
　これを使用すると、このパッケージに依存する別のパッケージでハマります。
　ハマりました…

- ``version`` は ``setup.cfg`` 側で設定できますが、統一感があるので ``setup.py`` 側で設定してます。

```python
import sys
from setuptools import setup
import src as kt

sys.path.append('./src')
sys.path.append('./tests')

setup(
    version = kt.__version__,
    license = kt.__license__,
    author = kt.__author__,
    url = kt.__url__,
)
```

###📘 ``setup.cfg``

- ``file:`` がコツです。
- 他には ``attr:``, ``find:`` がありますが、使っていません。
- ``[options.packages.find]`` に設定するものはありませんが、この文字列を忘れないように残しています。

```ini
[metadata]
name = sphinxcontrib.kana_text
license_file = LICENSE
maintainer = KaKkou
keywords = sphinx, sphinxcontrib, japanese, ja, kana_text, glossary, index, kana
description = KanaText class extends the functionality of Text class, for someone who use index/glossary directives.
long_description = file: README.rst, CHANGES.rst
long_description_content_type = text/x-rst
platforms = any
classifiers =
        Development Status :: 4 - Beta
        Framework :: Sphinx :: Extension
        Intended Audience :: Developers
        Intended Audience :: Education
        License :: OSI Approved :: BSD License
        Natural Language :: Japanese
        Operating System :: OS Independent
        Programming Language :: Python :: 3
        Topic :: Documentation :: Sphinx
        Topic :: Utilities

[options]
include_package_data = True
packages = sphinxcontrib.kana_text
package_dir =
        sphinxcontrib.kana_text = src
test_suite = tests.suites
install_requires = Sphinx

[options.packages.find]

[options.entry_points]
console_scripts =
        sphinx-kana-geindex = sphinxcontrib.kana_text.command:genindex
```

動作の確認のためだったり、影響の大きい箇所の変更だったりで忘れたくない行について、見つけやすいように ``KaKkou`` という文字列を使っています。これをそのままメンテナーとして登録しました。

###📙 ``sphinxcontrib/__init__.py``
⚠本件では、``packages``, ``package_dir`` を ``setup.py`` に設定することで不要になりました。

複数のパッケージが共通の親パッケージ（※ただし空）を持つ場合の流儀のようです。

```python
 __import__('pkg_resources').declare_namespace(__name__)
```

次の記事はボリュームがありますが、丁寧に説明しています。

https://www.lifewithpython.com/2018/05/python-difference-between-package-and-module.html

###🐶 ``sphinxcontrib/kana_text/__init__.py``
``setup.py`` と関連する箇所の抜粋です。

```python
 225
 226 __copyright__ = 'Copyright (C) 2021 @koKkekoh'
 227 __license__ = 'BSD 2-Clause License'
 228 __author__  = '@koKekkoh'
 229 __version__ = '0.20.1.7'
 230 __url__     = 'https://qiita.com/tags/sphinxcotrib.kana_text'
 231
```

###🐴~/.pypirc

```ini
[distutils]
index-servers =
  prodpypi
  testpypi
  prodtkn
  testtkn

[prodpypi]
repository=https://upload.pypi.org/legacy/
username=USERNAME1
password=PASSWORD1

[testpypi]
repository=https://test.pypi.org/legacy/
username=USERNAME2
password=PASSWORD

[prodtkn]
repository=https://upload.pypi.org/legacy/
username=__token__
password={pypi-で始まる長い文字列}

[testtkn]
repository=https://test.pypi.org/legacy/
username=__token__
password={pypi-で始まる長い文字列}
```

#以上