はじめてPyPIにPythonライブラリを登録した話
(2022年11月25日追記)
私の本が株式会社インプレス R&Dさんより出版されました。この記事の内容は含まれていませんが、ライブラリは登場します。イラストは鍋料理さんの作品です。猫のモデルはなんとうちのコです!
感想を書いていただけるととても嬉しいです!
(2022年8月3日追記)この記事で登録したライブラリを使ったWebサービスの作り方が本になりました。ただしPyPIへの登録方法は本には載っていません。
はじめに
先日、生まれてはじめてPyPIにPythonライブラリを登録しました。PyPIというのはPython Package Indexの略で、ここにパッケージを登録するとpip installでインストールできるようになります。
経緯は以下の通りです。
- もともとIMAP4をJSON化するAPIをローカルで動かすメール受信システムがあった
 - AWS LambdaとAPI Gatewayの組み合わせでサーバーレス化することになった
 - Flaskで書かれたAPIサーバー部分とメール受信機能を実装した箇所が密結合していたので分離した
 - メール受信機能を
pip installでインストールできたら便利じゃない? 
PyPIへの登録は一度やってみたかったので、良い機会だと思い挑戦してみました。n番煎じのネタですが、ドキュメント化しておかないと忘れそうなので、未来の自分のために記事にしました。
PyPIに登録した結果はこちらです。
ソースコードはこちらです。
手順は主にこちらの記事を参考にしました。
PyPI登録手順
ディレクトリ構成
今回登録するパッケージはimap2dictという名前にしました。IMAP4で受信したメールデータをPython辞書形式に変換するという意味を込めています。ディレクトリ構成は以下の通りです。
.
├── .gitignore ... Gitから除外したいファイルを指定
├── LICENSE ... ライセンス定義ファイル
├── README.md ... READMEファイル
├── imap2dict ... モジュールを配置するディレクトリ
│   ├── __init__.py ... モジュール検索や名前検索の初期化等を司るファイル
│   └── mail_client.py ... 機能を提供するモジュール
├── requirements.txt ... 依存関係を記述するファイル
└── setup.py ... PyPIの設定ファイル
init.pyの作成
from .mail_client import MailClient
参考にした記事では__init__.pyにバージョン情報を記載していましたが、それをするとsetup.pyを実行したときに依存関係を解消できずにエラーが発生したのでやめました。
ライセンス定義ファイルの生成
ライセンス定義ファイルはリポジトリ作成時にGitHub上で作るのが楽です。今回はMITライセンスにしました。
README.mdの作成
最低限必要なものを書いておこうと思い、概要とインストール方法、使い方だけを記載しました。
メインモジュールの修正
ディレクトリ構成に合わせてメインモジュールのmail_client.pyを修正しました。Pythonのお作法がいまいちよくわかっていないので、ファイル名とクラス名を決めるのに悩みましたが、機能をベースとして考えて以下のような構成にしました。
class MailClient():
    '''
    メールクライアントクラス。
    '''
    host_name = ''
    user_id = ''
    password  = ''
    def __init__(self, host_name, user_id, password):
        self.host_name = host_name
        self.user_id = user_id
        self.password = password
requirements.txtの作成
requirements.txtはpipreqsで自動生成してから手で修正しました。
pip install pipreqs
pipreqs .
pytz==2020.1
pytz>=2020.1
setup.pyの作成
setup.pyには以下の情報を記載しました。
| setupメソッドの引数 | 内容 | 
|---|---|
| name | ライブラリ名 | 
| version | バージョン | 
| description | 短い説明文 | 
| long_description | 長い説明文(README.mdの内容をファイルから読み込むようにした) | 
| long_description_content_type | 長い説明文の形式(今回はマークダウンなのでtext/markdownと記載する) | 
| author | 作者名 | 
| author_email | 作者のメールアドレス | 
| maintaner | メンテナーの名前(今回は作者と同じ) | 
| maintaner_email | メンテナーのメールアドレス(今回は作者と同じ) | 
| url | ホームページのURL(今回はGitHubリポジトリのURL) | 
| download_url | ダウンロード用URL(今回はGitHubリポジトリのURL) | 
| packages | パッケージ構成 | 
| classifiers | 分類情報(ライセンス情報とプログラミング言語を記載) | 
| license | ライセンス情報 | 
| keywords | 検索でヒットさせたいキーワード | 
| install_requires | 依存するパッケージの情報 | 
実際のコードはこちらです。
# Author: TAKAHASHI Taro <takahashi.taro@takedasystem.com>
# Copyright (c) 2022- TAKAHASHI Taro
# Licence: MIT
from setuptools import setup
DESCRIPTION = 'imap2dict: Receiving and deleting email on an IMAP4 server.'
NAME = 'imap2dict'
AUTHOR = 'TAKAHASHI Taro'
AUTHOR_EMAIL = 'takahashi.taro@takedasystem.com'
URL = 'https://github.com/sikkimtemi/imap2dict'
LICENSE = 'MIT'
DOWNLOAD_URL = URL
VERSION = '0.1.0'
PYTHON_REQUIRES = '>=3.6'
INSTALL_REQUIRES = [
    'pytz>=2020.1'
]
PACKAGES = [
    'imap2dict'
]
KEYWORDS = 'imap imap4 json'
CLASSIFIERS=[
    'License :: OSI Approved :: MIT License',
    'Programming Language :: Python :: 3.6'
]
with open('README.md', 'r', encoding='utf-8') as fp:
    readme = fp.read()
LONG_DESCRIPTION = readme
LONG_DESCRIPTION_CONTENT_TYPE = 'text/markdown'
setup(
    name=NAME,
    version=VERSION,
    description=DESCRIPTION,
    long_description=LONG_DESCRIPTION,
    long_description_content_type=LONG_DESCRIPTION_CONTENT_TYPE,
    author=AUTHOR,
    author_email=AUTHOR_EMAIL,
    maintainer=AUTHOR,
    maintainer_email=AUTHOR_EMAIL,
    url=URL,
    download_url=URL,
    packages=PACKAGES,
    classifiers=CLASSIFIERS,
    license=LICENSE,
    keywords=KEYWORDS,
    install_requires=INSTALL_REQUIRES
)
ライブラリのビルド
事前にWheelをインストールします。
pip install wheel
ソースコード配布物をビルドします。
python setup.py sdist
distディレクトリとimap2dict.egg-infoディレクトリが生成します。
Wheelパッケージをビルドします。
python setup.py bdist_wheel
buildディレクトリが生成します。
最終的に以下のようなディレクトリ構成になります。
.
├── LICENSE
├── README.md
├── build
│   ├── bdist.macosx-12-x86_64
│   └── lib
│       └── imap2dict
│           ├── __init__.py
│           └── mail_client.py
├── dist
│   ├── imap2dict-0.1.0-py3-none-any.whl
│   └── imap2dict-0.1.0.tar.gz
├── imap2dict
│   ├── __init__.py
│   └── mail_client.py
├── imap2dict.egg-info
│   ├── PKG-INFO
│   ├── SOURCES.txt
│   ├── dependency_links.txt
│   ├── requires.txt
│   └── top_level.txt
├── requirements.txt
└── setup.py
.gitignoreの作成
ビルドによって生成したファイルやディレクトリをGitで管理しないように.gitignoreを作成します。私はこちらをもとに必要に応じて修正して使うことが多いです。
PyPIのユーザー登録
PyPIには本番環境とテスト環境があります。どちらにもユーザー登録しましょう。
本番環境はこちら。
テスト環境はこちら。
ユーザー登録が完了したらホームディレクトリに.pypircを作成します。
[distutils]
index-servers =
  pypi
  testpypi
[pypi]
repository: https://upload.pypi.org/legacy/
username: 本番環境のPyPIユーザ名
password: 本番環境のPyPIパスワード
[testpypi]
repository: https://test.pypi.org/legacy/
username: テスト環境のPyPIユーザ名
password: テスト環境のPyPIパスワード
TwineでPyPIに登録
PyPIにライブラリを登録するツールのひとつであるTwineをインストールします。
pip install twine
まずテスト環境にアップロードしてみましょう。
twine upload --repository testpypi dist/*
成功するとURLが返ってきます。
URLにアクセスするとpipによるテストコマンドが記載されているので、テストしてみます。
pip install -i https://test.pypi.org/simple/ imap2dict
PyPIのテスト環境に存在するpytzモジュールのバージョンが古いので依存関係でエラーになりましたが、事前にpytzをインストールした状態でコマンドを打つとインストールに成功しました。
ではいよいよ本番環境にアップロードします。
twine upload --repository pypi dist/*
URLが返ってきたら表示を確認します。
pipでインストールできることを確認します。
pip install imap2dict
GitHubにタグをつける
最後に下図の手順でGitHubにバージョンのタグをつけます。


これで一連の作業は完了です。
Discussion