My Fucking Note

Software Engineeringのメモ

PyPIにパッケージを公開する手順の整理

はじめに

この記事ではPyPIにパッケージを公開するための手順について理解が曖昧だった部分を中心に整理してまとめます。 内容はPython公式のチュートリアル Packaging Python Projects を参考にしています。

PyPIについて

PyPIPython Package Indexの略でPythonのパッケージを管理しているリポジトリです。 普段pipを使ってインストールするパッケージはPyPIに登録されています。 PyPIにパッケージを登録する場合はDjangorequestsのような他のパッケージと重複しない一意なパッケージ名が必要になります。

パッケージの公開方法

パッケージを公開するための大まかな流れは次のようになります。

  1. パッケージ公開に必要なファイルの準備
  2. パッケージのビルド
  3. パッケージのアップロード

ディレクトリ構成

チュートリアルでは次のようなディレクトリ構成を採用していて、これがほぼ最小構成になります。

packaging_tutorial/
├── LICENSE
├── pyproject.toml
├── README.md
├── setup.cfg
├── src/
│   └── example_package/
│       ├── __init__.py
│       └── example.py
└── tests/
  • LICENSE: パッケージのライセンス
  • pyproject.toml: パッケージをビルドするための情報を書く設定ファイル
  • README.md: パッケージの利用者向けのドキュメント
  • setup.cfg: パッケージ名やバージョンを含むパッケージ自体の情報を書く設定ファイル
  • src/: パッケージを配置するディレクト
  • src/example_package/: パッケージ本体
  • tests/: テストコードを配置するディレクト

設定ファイルについて

Pythonの著名なパッケージではsetup.cfgの他にsetup.pyというファイルを利用していたり、pyproject.tomlがなかったりして闇雲に参考にすると混乱します。 そこでここでは各ファイルの役割を整理して説明します。

setup.cfg

setup.cfgsetuptools用の静的なメタデータconfigparserの形式で記述します。 setup.pyが実行可能なPythonコードであるのに対してsetup.cfgは静的な設定ファイルなのでバグが入り込む余地が少ないです。 そのため、基本的にはsetup.cfgに必要な設定を書くことが推奨されています。 チュートリアルでは次のような設定を使っています。

[metadata]
name = example-pkg-YOUR-USERNAME-HERE
version = 0.0.1
author = Example Author
author_email = author@example.com
description = A small example package
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/pypa/sampleproject
project_urls =
    Bug Tracker = https://github.com/pypa/sampleproject/issues
classifiers =
    Programming Language :: Python :: 3
    License :: OSI Approved :: MIT License
    Operating System :: OS Independent

[options]
package_dir =
    = src
packages = find:
python_requires = >=3.6

[options.packages.find]
where = src

利用可能なメタデータsetuptoolsのドキュメントにまとまっています。

setup.py

setup.pysetup.cfg同様にsetuptools用のメタデータですが、実行可能なPythonファイルであり、内容もインストール時に動的に決定します。 setup.pyではパッケージに関する必要な情報を引数にsetuptools.setup()関数を実行します。 上記のsetup.cfgsetup.pyで書く場合は次のようになります。

import setuptools

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

setuptools.setup(
    name="example-pkg-YOUR-USERNAME-HERE",
    version="0.0.1",
    author="Example Author",
    author_email="author@example.com",
    description="A small example package",
    long_description=long_description,
    long_description_content_type="text/markdown",
    url="https://github.com/pypa/sampleproject",
    project_urls={
        "Bug Tracker": "https://github.com/pypa/sampleproject/issues",
    },
    classifiers=[
        "Programming Language :: Python :: 3",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
    ],
    package_dir={"": "src"},
    packages=setuptools.find_packages(where="src"),
    python_requires=">=3.6",
)

昔からあるパッケージや複雑な処理が必要な大規模なパッケージではsetup.pyをよく利用していますが、最初にパッケージを公開する段階ではsetup.cfgで十分でしょう。 ただし、後述するpipのeditableインストールのために次のようなsetuptools.setup()関数を実行するだけのsetup.pyをプロジェクトに含めておくと良いです。

import setuptools


setuptools.setup()

pyproject.toml

pyproject.tomlはパッケージをビルドする手順を記載する設定ファイルになります。 ビルドに関する情報はbuild-systemテーブルに記載します。

[build-system]
requires = [
    "setuptools>=42",
    "wheel"
]
build-backend = "setuptools.build_meta"

build-system.requiresには公開したいパッケージをビルドするために必要なビルド用のパッケージを列挙します。 チュートリアルではsetuptoolswheelを採用しています。

build-system.build-backendにはパッケージをビルドするためのPythonオブジェクトを指定します。 チュートリアルではsetuptools.build_metaを採用しています。

setuptoolswheelが最もスタンダードなパッケージになりますが、他にも選択肢*1はあり、それらを採用する場合はbuild-systemを書き換えます。

pyproject.tomlがなくともパッケージのビルドは可能で、その場合は必要なパッケージを手動でインストールし、各パッケージのビルド用のコマンドを実行することになります。 例えばよく紹介されている次の手順は手動でsetuptoolswheelをインストールしてビルドする方法になります。

# ビルド用のパッケージをインストール
pip install --upgrade setuptools wheel

# ビルド用のコマンドを実行
python setup.py sdist bdist_wheel

pyproject.tomlはパッケージのビルドに関する情報以外も記載することができ、各パッケージはtool.パッケージ名テーブルの中を自由に使うことができます。*2例えばPythonのLinter/Formatterであるblackisortがオプションを設定する目的で使っています。

pipのeditableインストールについて

開発中のパッケージを開発環境にインストールしてインポートしたり実行可能なコマンドを利用可能にする場合にpipのeditable modeを有効にしてインストールすると、ソースコードを変更しても再インストール不要で変更が反映されます。 具体的には開発中のパッケージのプロジェクトルート(setup.cfgなどが置いてあるディレクトリ)で次のコマンドを実行します。

pip install -e .

pipのeditableインストールには以下のような変更の歴史があります。

  1. pip21.1より前のバージョンではeditableインストールをするにはsetup.pyが必須だった
  2. pip21.1の機能でsetup.py無しでsetup.cfgだけでもeditableインストールがサポートされた
  3. pip21.1.3setup.pyが無い場合はpyproject.tomlbuild-systemの指定が必須に変更された

そのため、editableインストールを利用する場合は次の3ファイルを揃えておくと古いバージョンのpipに対しても互換性が確保されて不具合が起きにくいでしょう。

  • パッケージのメタデータを記載したsetup.cfg
  • setuptools.setup()を実行するだけのsetup.py
  • build-systemを定義したpyproject.toml

ビルド方法

pyproject.tomlにビルド用のメタデータを記載している場合、buildパッケージだけでビルドができます。 buildパッケージを使う場合、ビルドに必要なsetuptoolsなどのパッケージはビルド用の一時的な仮想環境にインストールされるので、開発環境に影響を及ぼすこともありません。

# buildパッケージのインストール
pip install --upgrade build

# パッケージのビルド
python -m build

ビルドが完了するとdistディレクトリの中にビルド済みのファイルが作成されます。

アップロード方法

事前にアカウント登録が必要になります。

アップロードにはtwineというパッケージを使います。 最初は本番のPyPIではなくテスト用のTestPyPI にアップロードすることを推奨します。

twineのインストール

pip install --upgrade twine

パッケージのチェック

アップロード前に内容に問題が無いかチェックします。 何もエラーが出なければアップロードに進みます。

twine check dist/*

TestPyPI へのアップロード

アップロード

twine upload --repository testpypi dist/*

TestPyPIからのインストール

pipインストール時に--index-urlオプションを付けると本番のPyPI以外からもインストールできます。

pip install --index-url https://test.pypi.org/simple/ パッケージ名

本番のPyPIへのアップロード

twine upload dist/*

本番のPyPIからのインストール

通常のpipインストールと同様です。

pip install パッケージ名