自分で作成したPythonのパッケージをPyPIで公開するまでの手順まとめ.記事が古いものが多かったり,環境 (anaconda, pip) によって変わったりするので,自分の環境に合うものをまとめた.

環境

% sw_vers
ProductName:	macOS
ProductVersion:	11.4
BuildVersion:	20F71

setup.py, __init__.py, MANIFEST.inを適切に作成

ファイル構成

自分が想定しているファイル構成は以下.

template
├── MANIFEST.in
├── setup.py
└── template
    └── __init__.py

具体的なファイルの書き方

公式のコピーをしても意味がないので,参考に自分の例を載せる.

setup.py

本番ではsetup.pyとして直下のディレクトリに配置.

# PyPI登録流れ参考: https://qiita.com/kinpira/items/0a4e7c78fc5dd28bd695

"""
This is a setup.py script generated by py2applet
Usage:
    python setup.py py2app
"""

from setuptools import setup, find_packages
from shutil import copyfile
import os
import sys
import re

# 再帰回数に引っかかるのでとりあえず大きい数に.
sys.setrecursionlimit(10 ** 9)

# ------------------------ ここを変更 --------------------------------
PACKAGE_NAME = 'template'    # フォルダの名前も統一
DESCRIPTION = ''

# py2app用の変数
SRC = ['main.py']
DATA_FILES = ['LICENSE']
PKGS = []
ICON = os.path.join('icon', '{}.icns'.format(PACKAGE_NAME))
# --------------------------------------------------------------------

VERSION_PYTHON = '{0}.{1}'.format(sys.version_info.major, sys.version_info.minor)

# __init__.pyから読み込む
with open(os.path.join(PACKAGE_NAME, '__init__.py')) as f:
    init_text = f.read()
    VERSION = re.search(r'__version__\s*=\s*[\'\"](.+?)[\'\"]', init_text).group(1)
    LICENSE = re.search(r'__license__\s*=\s*[\'\"](.+?)[\'\"]', init_text).group(1)
    AUTHOR = re.search(r'__author__\s*=\s*[\'\"](.+?)[\'\"]', init_text).group(1)
    EMAIL = re.search(r'__author_email__\s*=\s*[\'\"](.+?)[\'\"]', init_text).group(1)
    ID = re.search(r'__user_id__\s*=\s*[\'\"](.+?)[\'\"]', init_text).group(1)
    APP_NAME = re.search(r'__app_name__\s*=\s*[\'\"](.+?)[\'\"]', init_text).group(1)
    url = re.search(r'__url__\s*=\s*[\'\"](.+?)[\'\"]', init_text).group(1)

assert VERSION
assert LICENSE
assert AUTHOR
assert EMAIL
assert ID
assert APP_NAME
assert url

'''
メモ
/.pyenv/versions/anaconda3-2019.03/envs/pymat/lib/python3.7/site-packages/PyQt5/uic/port_v2/ascii_upper.pyの28行目を書き換えた.
少なくともpython3.7においてstringオブジェクトはmaketrans関数を持っておらず,正しくはstr.maketransである.
python3.8で以前やったときはこのエラーがでなかった.
* 3.8にアップデートしてやってみる.
    → 3.8だとsetupのほうがエラー起きるので3.6でやってみる.
    3.6は起動後に関数が呼び出せないと言われる
setup.pyに関する参考サイト
* https://packaging.python.org/guides/distributing-packages-using-setuptools/#packages
py2app 0.23や0.22では動作確認
'''

if 'py2app' in sys.argv:
    alias = '-A' in sys.argv or '--alias' in sys.argv

    # 諸変数・定数の定義
    lib_path = os.path.join(os.environ['CONDA_PREFIX'], 'lib')
    fname_libpython = 'libpython{}.dylib'.format(VERSION_PYTHON)

    # libpython3.7.m.dylibだとエラーになるのであらかじめコピーしておく.
    path_original = os.path.join(lib_path, 'libpython{}m.dylib'.format(VERSION_PYTHON))
    path_converted = os.path.join(lib_path, fname_libpython)
    if os.path.exists(path_original) and not os.path.exists(path_converted):
        copyfile(path_original, path_converted)

    # 諸変数の準備
    dylib_files = [os.path.join(lib_path, f) for f in os.listdir(lib_path) if '.dylib' in f]
    contents_path = os.path.join('dist', '{}.app'.format(APP_NAME), 'Contents')
    frameworks_path = os.path.join(contents_path, 'Frameworks')

    OPTIONS = {
        'argv_emulation': False,
        'packages': PKGS,
        'iconfile': ICON,
        'plist':{
            'PyRuntimeLocations':[
                '@executable_path/../Frameworks/{}'.format(fname_libpython),
                os.path.join(lib_path, fname_libpython),
            ],
            'CFBundleName': APP_NAME,
            'CFBundleDisplayName': APP_NAME,
            'CFBundleGetInfoString': DESCRIPTION,
            'CFBundleIdentifier': "com.{0}.osx.{1}".format(ID, APP_NAME),
            'CFBundleVersion': VERSION,
            'CFBundleShortVersionString': VERSION,
            'NSHumanReadableCopyright': u"Copyright © 2021-, {}".format(AUTHOR)
        },
        # 'frameworks': dylib_files,
    }

    setup(
        name = APP_NAME,
        app = SRC,
        author = AUTHOR,
        author_email = EMAIL,
        version = VERSION,
        data_files = DATA_FILES,
        options = {'py2app': OPTIONS},
        setup_requires = ['py2app'],
        url = 'https://github.com/{0}/{1}'.format(ID, APP_NAME),
    )

    # aliasモードじゃないとき.
    # if not alias:
    #     {copyfile(f, os.path.join(frameworks_path, os.path.basename(f))) for f in dylib_files}
else:
    """
    参考: https://python-packaging-user-guide-ja.readthedocs.io/ja/latest/distributing.html#manifest-in
    """
    with open('requirements.txt') as requirements_file:
        install_requirements = requirements_file.read().splitlines()
    
    try:
        with open('README.md') as f:
            long_description = f.read()
    except IOError:
        long_description = ''

    setup(
        name = APP_NAME,
        version = VERSION,
        description = DESCRIPTION,
        long_description = long_description,
        long_description_content_type = 'text/markdown', # long_descriptionの形式を'text/plain', 'text/x-rst', 'text/markdown'のいずれかから指定.
        author = AUTHOR,
        author_email = EMAIL,
        maintainer = AUTHOR,
        maintainer_email = EMAIL,
        install_requires = install_requirements,
        url = url,
        keywords = '', # PyPIでの検索用キーワードをスペース区切りで指定.
        license = LICENSE,
        packages = find_packages(exclude=['example']),
        classifiers = [
            'License :: OSI Approved :: MIT License',
            'Programming Language :: Python :: 3',
            'Programming Language :: Python :: 3.6',
            'Programming Language :: Python :: 3.7',
            'Programming Language :: Python :: 3.8',
            'Programming Language :: Python :: 3.9',
        ],  # パッケージ(プロジェクト)の分類.https://pypi.org/classifiers/ に掲載されているものを指定可能.
    )

__init__.py

本番では__init__.pyとしてメインモジュールの入ったディレクトリと同じ階層に配置.

# 正規表現でtextファイルと同じやり方で読み込むのですべて手打ちする.

from .template import *

__version__      = '0.0.1'
__license__      = 'MIT'
__author__       = 'yu9824'
__copyright__    = 'Copyright © 2021 yu9824'
__author_email__ = 'email@address'
__user_id__ = 'yu9824'
__app_name__ = 'template'
__url__ = 'http://github.com/yu9824/template/'

__all__ = [
    'main',
    ]

MANIFEST.in

本番ではMANIFEST.inとして直下のディレクトリに配置.

何をパッケージとして含んで,何をパッケージとして含まないかを指定するファイル. ここでrequirements.txtを指定しないとそれが入らず,setup.pyが適切に動作しないなどの不具合が生じてしまう.

include LICENSE
include *.md
exclude README.md
include requirements.txt

【初回のみ】 PyPI testおよびPyPIにアカウント登録.

ここで登録したusernameとpasswordをそれぞれ覚えておく.間違えないようにusernameをあえて分けている人も多い.

【初回のみ】 ~/.pypircファイルを作成

テンプレートは以下.

[distutils]
index-servers =
  pypi
  pypitest

[pypi]
repository = https://upload.pypi.org/legacy/
username = <username>
password = <password>

[pypitest]
repository = https://test.pypi.org/legacy/
username = <username>
password = <password>

場所はHOMEディレクトリの直下.

たとえば,以下のコマンドでvimを起動して,作成する.

vim ~/.pypirc

テンプレートに対して,username, passwordを適切に指定すればOK.

もしくはその他のやり方でエディターとか使って作成してももちろんOK.

username, passwordを設定せずにコマンドにベタ打ちすることも可能.しかし,繰り返しやることを考えるとあまり使わないかも.

参考: PyPIへのアップロード

注意点

2016年くらい?にリンクが変更されたらしいので古い記事では変わっていないことが多い.今後ももちろん変わっていく可能性はあるが2021/07/23時点では.pypircにあるリンクでアップロードできることを確認した.

環境にtwineとwheelをインストール

現時点の自分はanacondaのconda-forgeチャンネルを使用しているので,

conda install twine wheel

でインストールした.しかし,多くのサイトでは

pip install twine
pip install wheel

でしていた.おそらくどちらでも大丈夫.

参考

  • wheel : パッケージ作成に必要
  • twine : PyPIへのアップロードに必要

すでに前のバージョンをアップロードしている場合

往々にして,distディレクトリに前回の内容が残っているのでコマンドラインからそれらを削除する.

rm -rf *.egg-info/* dist/*

アップロードする前にパッケージとして動作するか確認.

setup.py, __init__.py, MANIFEST.inが正しいかローカル環境で確認.

以下のコマンドにより,site-packagesにインストールすることができる.(平たく言うと他のパッケージと同様にimportして使えるようになる.)

これにより,パッケージとして作用しているかを確認する.

python3 setup.py develop

とすることで,pip installされたのと同様の動作をする.test.pyが動くかを確認したり,jupyter notebook上のexampleを動かしたりできる.


なんだかよくわからないけど,VSCode上だとこれができないことがある. 普通にターミナルからこれをやると動く.エラーコードを貼っておく.

running develop
error: can't create or remove files in install directory

The following error occurred while trying to add or remove files in the
installation directory:

    [Errno 2] No such file or directory: '/Library/Python/3.8/site-packages/test-easy-install-24382.write-test'

The installation directory you specified (via --install-dir, --prefix, or
the distutils default setting) was:

    /Library/Python/3.8/site-packages/

This directory does not currently exist.  Please create it and try again, or
choose a different installation directory (using the -d or --install-dir
option).

Pythonのパスのデフォルトがvscodeだと違うのかな?

VSCode上でできるレポジトリもあった.難しい…


PyPIにアップロードするファイルを生成

以下のコマンドを実行することでdistディレクトリ内にそれぞれ一つずつ(合計二つ)のファイルが生成される.

python3 setup.py sdist bdist_wheel

上記を別々に行うこともできる.

python3 setup.py sdist  # ソースコード配布物の作成
python3 setup.py bdist_wheel  # ライブラリのパッケージを作成

アップロード

以下のコマンドを実行.

テスト環境:

twine upload --repository pypitest dist/*

本番環境:

twine upload --repository pypi dist/*

pypitestpypiの部分は~/.pypirc内のindex-serversで自分がどのように登録したかに依存する.(リンクに対してサーバー名を自分でつけているイメージ)

コメント

その他のコマンドでアップロードしているサイトも多かったが非推奨らしい.(真偽未検証)
だが,上記の手順だけで少なくとも正常なアップロードができたのでそんなに問題はないかなと考えている.
実際regitsterなどの手順はふまず,上記の手順のみで登録できた.

今後はanacondaへのアップロードにも挑戦したい.

参考

その他

pip installのやり方

それぞれ成功したときに表示されるpypi.orgのサイトに大体のやり方は書いてある.

classifiers一覧

PyPI上のパッケージを見つけてもらうためのタグみたいなもの.対応のOSやバージョン,ライセンスなどの情報を付与できる.

一覧はこちらにあり,コピペすればOK.

コメント

大事なのはsetup.pyやその周辺の整備.あとはそんなに難しくはない.

全体の参考

PyPIパッケージ公開手順 - Qiita