やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

PyPIパッケージの作成から運用までのワークフロー

 めっちゃ大変やで。

ワークフロー

  1. PyPIパッケージの作成
  2. PyPIパッケージの更新

 大きく言えば上記2手順のみ。これらの手順を詳細に見ていくと膨大。

1. PyPIパッケージの作成

  1. Pythonパッケージの作成
  2. GitHubリポジトリの作成
  3. メタデータファイルの作成
  4. コミット
  5. PyPIパッケージの作成

1-1. Pythonパッケージの作成

  1. 要件定義
  2. API定義
  3. 実装
  4. 単体テスト

1-1-1. 要件定義

 曖昧。最低限、以下を一言で説明した文書くらいは欲しい。

  • 概要: そのソースコードは何をするものか
  • 文脈: どこで使うものか
  • inputは何か
  • outputは何か
項目
概要 指定した文字列を<>で囲った文字列を返す
文脈 Python,Console
IN 文字列
OUT 文字列(INを<>で囲う)

ファイル・コード例  実装例。(Python文脈)

encloser.py

def enclose(source):
    return '<{}>'.format(source)

 使用例。(Python文脈)

example.py

import encloser
res = encloser.enclose()

 実装例。(Shell文脈)

cmd.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys, encloser
if len(sys.argv) < 2: raise ValueError('Pass the string you want to enclose as the first argument.')
res = encloser.enclose(sys.argv[1])

 使用例。(Shell文脈)

cmd.py 'html'
<html>

 内部の細かいAPIや処理をすべて決めるのは難しい。よって最初はエンドユーザが使う最も外側の入出力だけに注目する。

1-1-2. API定義

 最も重要なのが名前の定義。次いで入出力。

  • 公開API
    • パッケージ名(層)
    • モジュール名
    • クラス名
    • 属性名
    • プロパティ名
    • 関数名
      • 例外
        • メッセージ
      • 戻り値(型)
      • 引数名(型)
  • スコープ
    • global
    • static
    • class
    • instance

Python言語の限界  Pythonは型安全でなく型危険である。型を制限できないため実行時エラーが頻発するリスクを常に抱えている。

 mypyなどがあるように型安全にしたい要望はあるはず。だが言語仕様的にムリ。それでも型は想定すべき。実際は特定の型でないと実行時エラーになるから。さりとてmypyのような外部パッケージに依存したくないし、コードも互換性のない状態にしたくない。どうするか。

 別ファイルに書き出してもいい。ただ、実際のコードと関連付ける手段がないため、二重管理になってしまう。変更し忘れの頻発でメンテできなくなることが想定される。

 SemVerでは公開APIの定義をすべきとある。バージョニングの観点からも公開API定義はしたい。テストの自動化にも繋がりそう。もっとも、型安全言語であればそのテストは不要でありコンパイルエラーで検出してくれるのだが。

 結局の所、言語仕様レベルで型がサポートされていないため難しい。別の言語を使う選択も考慮に入れるべき。

要件定義、設計、実装を行ったり来たりすることになる。  原因は多数ある。そもそも言語やAPI仕様を知らずどう書けばいいかわからなかったり、バージョン差異による挙動の違いが発覚したなど。書いてみなければわからないことが多数ある。Pythonは罠だらけ。私は無知・無能・無力・阿呆・記憶喪失。

 問題は最初のうちにできるだけ多く洗い出すべき。もし一向に進まないなら、もっと小規模の別案件に分けることを検討すべき。できるだけ最初のうちにこれを繰り返し、具体化しておけば大きな後戻りを避けられる。ただしそればかり続くと一向に進まずフラストレーションが溜まり、モチベーションが低下していく。そのときが細分化すべきとき。この工程をうまいこと定量化して成果にできればいずれ目標の成果物にたどりつくはず。

 仕事が大きいときはいくつものクラスや関数などに細分化し、粗結合にする。このとき、階層が深いと入出力関係や呼び出し方を正確に把握することが困難になっていく。インタフェースだけを実装したコードを書かねば設計すら困難なこともある。

 そもそもクラスや関数に分けること自体が難しい。意味のある処理の単位を関数とし、意味のあるメモリの集合をインスタンスとする。これらを設計せねば規模が大きいコードを書くことはできない。でも、その設計をするためには知識・経験・センスが必要。私には何もない。よって、ひたすら書いて失敗するのを繰り返しブラッシュアップしていく。

1-1-3. 実装

 重要工程。というより本質。これによってAPI仕様を決めたり、API仕様バグが見つかることもある。

 やることは単純。要件定義を満たすコードを書いていく。わからないことがあればネットで調べる。以下のように動作確認する。問題ないなら本番コードを書くなり修正するなりする。

  • REPLで動作確認する
  • 同ファイルにif __name__ == '__main__':assert()を実行する
  • 別ファイルに最小コードを書いて実行する
  • 単体テストコードを書いて実行する

 得た必要知識は別途ブログなどの記事にする。

1-1-4. 単体テスト

 これをしないとコードを確定できない。

 Pythonの場合、コンパイルエラーを検出してくれない。よって、実行時にはあらゆるエラーが発生しうる。それに気づくためには全コードを1回は実行すべき。単体テストは重要どころではなく絶対に必要。

 Pythonは他のコンパイルする言語と比べて圧倒的にテストの労力がかかる。コンパイルの仕事を人間が肩代わりせねばならないレベル。これをやっていたらいつまでも終わらない。自動化したい。

1-2. GitHubリポジトリの作成

 重要なのがリポジトリ名。パッケージホスティングサービスは言語がPythonであると想定される。だがGitHubは言語を特定できない。同一ソフトウェアを別言語で書いたリポジトリを作ることがあるかもしれない。そんなときリポジトリ名をどうすべきか。PyPIパッケージ名と同一にすべきとは言えない。

  • {package_name}.{lang}
    • mypack.sh
    • mypack.py
    • mypack.rb
    • mypack.go
    • mypack.js
    • mypack.rs
    • mypack.cs
    • mypack.c
    • mypack.cpp

 これで重複を避けることができる。

 ローカルリポジトリは以下コマンドで作る。

git init

 リモートリポジトリはGitHubAPIで作成できる。

json='{"name":"'${REPO_NAME}'","description":"'${REPO_DESC}'","homepage":"'${REPO_HOME}'"}'it
#echo "$json" | curl -u "${username}:${password}" https://api.github.com/user/repos -d @-
echo "$json" | curl -H "Authorization: token ${token}" https://api.github.com/user/repos -d @-

 コミットするときは以下。

git add -n .
git add .
git commit -m 'メッセージ'
git push

 このときリリースするなら以下でタグをつける。

version='v0.0.1'
message='first commit'
git tag -a "$version" -m "$message"
git push origin "$version"
git tag | sort -Vr 

1-3. メタデータファイルの作成

 これをいちいち作っていたら大変。テンプレートにして一括作成するようにしたい。ただしPyPI上での名前重複チェックなど難しそうな処理もある。

 これを半自動化してワークフローを簡略化したい。

  • パッケージ名
  • パッケージ説明
  • ライセンスの選択
  • ホームページURLの選択
  • 検索キーワード

 以下は設定ファイルにまとめておきたい。

  • PyPIアカウント(ユーザ名、パスワード、AccessToken)
  • GitHubアカウント(ユーザ名、パスワード、AccessToken)
  • 著者名
  • 著者メアド
  • versionの初期値
  • classifiersの初期値
  • python_requiresの初期値

 これらのデータを元に以下を作成したい。

  • __init__.py
  • setup.py
  • コピーライト
  • LICENSE.txt
  • README.md
  • CHANGELOG.md
  • MANIFEST.in
  • .gitignore

コマンドもデータを元に作成したい。  すべてPyPIパッケージをカレントディレクトリとして実行すること。

 ローカルリポジトリ作成。

git init
git commit -m "$message"
git add -n .
git add .
git remote add origin ...
git push origin master
git clone ...

 リモートリポジトリ作成。

json='{"name":"'${REPO_NAME}'","description":"'${REPO_DESC}'","homepage":"'${REPO_HOME}'"}'it
echo "$json" | curl -u "${username}:${password}" https://api.github.com/user/repos -d @-
echo "$json" | curl -H "Authorization: token ${token}" https://api.github.com/user/repos -d @-

 タグ作成。(github release機能)

version='v0.0.1'
message='first commit'
git tag -a "$version" -m "$message"
git push origin "$version"
git tag | sort -Vr 

 PyPIパッケージ作成。

rm -rf build
rm -rf dist
rm -rf ${package_name}.egg-info
python setup.py sdist
python setup.py bdist_wheel

 PyPIアップロード。

UN=ユーザ名
PW=パスワード
twine upload -u $UN -p $PW --repository-url https://pypi.org/legacy/ dist/*
twine upload -u $UN -p $PW --repository-url https://test.pypi.org/legacy/ dist/*

ワークフローの半自動化について  メタデータは以下2つの時点において変更しうる。

  • PyPIパッケージ初回作成
  • PyPI更新

 どのとき、何の項目を、どの値にしうるか。その候補の設定・表示・選択UI・反映をさせることで一元管理したい。要設計。

1-4. コミット

git diff
git add -n .
git add .
git commit -m "$message"
git remote add origin ...
git push origin master

1-5. PyPIパッケージの作成

 PyPIに登録するデータを作成する。

rm -rf build
rm -rf dist
rm -rf mypack.egg-info
python setup.py sdist
python setup.py bdist_wheel

2. PyPIパッケージの更新

  1. ソースコードを変更する
  2. メタデータファイルを変更する
  3. 変更内容を確認する
  4. GitHubローカルリポジトリへコミットする
  5. GitHubリモートリポジトリへプッシュする
  6. Gitタグをつける
  7. GitHubリリースする
  8. PyPIパッケージを再作成する
  9. PyPIパッケージをアップロードする
  10. 確認する

2-1. ソースコードを更新する

 これが目的。テキストエディタなどで対象のソースコードを変更したいように変更する。

 もしファイル名を変更するなら以下。

git mv old_name new_name

 もしファイル名の変更や追加をしたら必要に応じて以下を編集すべきか判断する。

  • .gitignore
  • MANIFEST.in

 もし依存パッケージが追加されたらrequirements.txtファイルを追加・更新する

  • requirements.txt
pip freeze > requirements.txt

2-2. メタデータファイルを更新する

  • versionをインクリメントする SemVer
    • setup.pyclassifiersDevelopment Statusに変更があるか確認する

 もしリリースするなら。(リリースせずコミットだけなら以下は不要)

  • CHANGES.mdに追記する
    • 前回のバージョンから今回までの間にあったコミットメッセージすべて

 更新時に一元管理して、バージョン値だけ入力すれば各種ファイルが一発で変更されるようにしたい。できればバージョン値もコードの変化を読み取ってSemVerに従い自動インクリメントしてほしい。

2-3. 変更内容を確認する

git diff

 内容が正しいことを確認する。結局、人間が目視確認するしかない。絶対ミスる。自動化したいが不可能。

2-4. GitHubローカルリポジトリへコミットする

git add -n .
git add .
git commit -m 'メッセージ'

2-5. GitHubリモートリポジトリへプッシュする

git push origin master

2-6. Gitタグをつける

git tag -a "$version" -m "$message"
git tag | sort -Vr 

2-7. GitHubリリースする

git push origin "$version"

2-8. PyPIパッケージを再作成する

rm -rf build
rm -rf dist
rm -rf mypack.egg-info
python setup.py sdist
python setup.py bdist_wheel

2-9. PyPIパッケージをアップロードする

 まずはtest.pypiで行う。それに成功すれば本番のpypiで同じように実行する。

UN={USERNAME}
PW={PASSWORD}
twine upload -u $UN -p $PW --repository-url https://test.pypi.org/legacy/ dist/*
twine upload -u $UN -p $PW --repository-url https://upload.pypi.org/legacy/ dist/*

 以下は嘘だった。動かない。URLは歴史的に変動していたらしい。今もlegacyという不穏なキーワードを含んでいるので、将来は最新のURLを調べる必要が出てくるかもしれない。eggpipになるなどPython界隈は環境が破壊的に変化するのが常識……。

twine upload -u $UN -p $PW --repository-url https://pypi.org/legacy/ dist/*

2-10. 確認する

  1. PyPIパッケージのURLを確認する
  2. PyPIパッケージのインストール確認をする
  3. TestPyPIパッケージのインストール確認をする
  4. 実行確認をする

2-10-1. PyPIパッケージのURLを確認する

2-10-2. PyPIパッケージのインストール確認をする

 インストールできるか確認する。

pip install --user -i https://test.pypi.org/simple/ {package_name}
pip install --user -i https://pypi.org/simple/ {package_name}

 アンインストールは以下。

pip uninstall {package_name}

2-10-3. TestPyPIパッケージのインストール確認をする

 更新できるか確認する。

 まずは現在のバージョンを確認。

$ pip list | grep {package_name}
{package_name} {versionX}

 更新する。

pip install -U {package_name}

 バージョン値が新しい値になったか確認する。

$ pip list | grep {package_name}
{package_name} {versionY}

2-10-4. 実行確認をする

 たとえばimport確認する。

import {module}

 たとえば単体試験を実行する。

run_test.sh
python -m tests.tests_{module}.py

所感

 これをひとつのミスもなく遂行する。できるわけがない。必ずどこかでミスる。自動化したい。

対象環境

$ uname -a
Linux raspberrypi 4.19.75-v7l+ #1270 SMP Tue Sep 24 18:51:41 BST 2019 armv7l GNU/Linux