PyPIパッケージの作成から運用までのワークフロー
めっちゃ大変やで。
ワークフロー
大きく言えば上記2手順のみ。これらの手順を詳細に見ていくと膨大。
1. PyPIパッケージの作成
1-1. Pythonパッケージの作成
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
mypyなどがあるように型安全にしたい要望はあるはず。だが言語仕様的にムリ。それでも型は想定すべき。実際は特定の型でないと実行時エラーになるから。さりとてmypyのような外部パッケージに依存したくないし、コードも互換性のない状態にしたくない。どうするか。
別ファイルに書き出してもいい。ただ、実際のコードと関連付ける手段がないため、二重管理になってしまう。変更し忘れの頻発でメンテできなくなることが想定される。
SemVerでは公開APIの定義をすべきとある。バージョニングの観点からも公開API定義はしたい。テストの自動化にも繋がりそう。もっとも、型安全言語であればそのテストは不要でありコンパイルエラーで検出してくれるのだが。
結局の所、言語仕様レベルで型がサポートされていないため難しい。別の言語を使う選択も考慮に入れるべき。
問題は最初のうちにできるだけ多く洗い出すべき。もし一向に進まないなら、もっと小規模の別案件に分けることを検討すべき。できるだけ最初のうちにこれを繰り返し、具体化しておけば大きな後戻りを避けられる。ただしそればかり続くと一向に進まずフラストレーションが溜まり、モチベーションが低下していく。そのときが細分化すべきとき。この工程をうまいこと定量化して成果にできればいずれ目標の成果物にたどりつくはず。
仕事が大きいときはいくつものクラスや関数などに細分化し、粗結合にする。このとき、階層が深いと入出力関係や呼び出し方を正確に把握することが困難になっていく。インタフェースだけを実装したコードを書かねば設計すら困難なこともある。
そもそもクラスや関数に分けること自体が難しい。意味のある処理の単位を関数とし、意味のあるメモリの集合をインスタンスとする。これらを設計せねば規模が大きいコードを書くことはできない。でも、その設計をするためには知識・経験・センスが必要。私には何もない。よって、ひたすら書いて失敗するのを繰り返しブラッシュアップしていく。
1-1-3. 実装
重要工程。というより本質。これによってAPI仕様を決めたり、API仕様バグが見つかることもある。
やることは単純。要件定義を満たすコードを書いていく。わからないことがあればネットで調べる。以下のように動作確認する。問題ないなら本番コードを書くなり修正するなりする。
得た必要知識は別途ブログなどの記事にする。
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で作成できる。
- https://developer.github.com/v3/repos/#create
- https://developer.github.com/v3/repos/#create-repository-using-a-repository-template
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
ローカルリポジトリ作成。
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つの時点において変更しうる。
どのとき、何の項目を、どの値にしうるか。その候補の設定・表示・選択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パッケージの更新
- ソースコードを変更する
- メタデータファイルを変更する
- 変更内容を確認する
- GitHubローカルリポジトリへコミットする
- GitHubリモートリポジトリへプッシュする
- Gitタグをつける
- GitHubリリースする
- PyPIパッケージを再作成する
- PyPIパッケージをアップロードする
- 確認する
2-1. ソースコードを更新する
これが目的。テキストエディタなどで対象のソースコードを変更したいように変更する。
もしファイル名を変更するなら以下。
git mv old_name new_name
もしファイル名の変更や追加をしたら必要に応じて以下を編集すべきか判断する。
.gitignore
MANIFEST.in
もし依存パッケージが追加されたらrequirements.txt
ファイルを追加・更新する
- requirements.txt
pip freeze > requirements.txt
2-2. メタデータファイルを更新する
- versionをインクリメントする SemVer
setup.py
のclassifiers
のDevelopment 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を調べる必要が出てくるかもしれない。egg
がpip
になるなどPython界隈は環境が破壊的に変化するのが常識……。
twine upload -u $UN -p $PW --repository-url https://pypi.org/legacy/ dist/*
2-10. 確認する
2-10-1. PyPIパッケージのURLを確認する
- https://test.pypi.org/project/{package_name}/
- https://pypi.org/project/{package_name}/
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
所感
これをひとつのミスもなく遂行する。できるわけがない。必ずどこかでミスる。自動化したい。
対象環境
- Raspbierry pi 4 Model B
- Raspbian buster 10.0 2019-09-26 ※
- bash 5.0.3(1)-release
$ uname -a Linux raspberrypi 4.19.75-v7l+ #1270 SMP Tue Sep 24 18:51:41 BST 2019 armv7l GNU/Linux