概要
自分の知識が昔読んだ初版の『入門Python3』から更新されていないことに危機感を覚え、一念発起でモダンなツール群を調べた際の備忘録。以下のツール群1を駆使してゼロからパッケージを開発し、PyPiへの公開までやってみる。コードはGitHubに置いておくので、誰かの参考になれば。
題材
手の込んだパッケージを作るのが目的ではないので、今回はシンプルにNPS (Net Promoter Score)を計算するパッケージを作ってみる。利用イメージは以下。
import npspy
npspy.categorize(10) # "promoter"
npspy.calculate([0, 7, 9]) # 0
それでは実装に移ろう。
実装
下準備
コードを書き始める前に、まずはいい感じのディレクトリを作りたい。これはPoetryに任せる。以下のコマンドでインストールしよう2。
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python -
poetry new npspy
このようなディレクトリができているはず。
npspy
├── npspy
│ └── __init__.py
├── pyproject.toml
├── README.rst
└── tests
├── __init__.py
└── test_npspy.py
pyproject.toml
にはすでに依存パッケージ(pytest
)が指定されているので、ディレクトリに移動して以下のコマンドも実行しておく。
poetry install
※この時点のコミット
コーディング
続いて必要な機能を実装する。この記事の本題ではないのでさらっとコードを見せるだけ。
class InvalidAnswerError(Exception):
"""Class for invalid answer related errors."""
pass
def categorize(answer):
"""Categorize an answer for the NPS question.
Args:
answer: An answer for the NPS question.
Returns:
Answerer's category.
Raises:
InvalidAnswerError: If the answer is not between 0 and 10.
"""
if 0 <= answer <= 6:
return "detractor"
elif 7 <= answer <= 8:
return "passive"
elif 9 <= answer <= 10:
return "promoter"
else:
raise InvalidAnswerError(f"Invalid answer: {answer}")
def calculate(answers):
"""Calculates nps.
Args:
answers: Answers for the NPS question.
Returns:
Calculated nps.
"""
count = {}
for answer in answers:
category = categorize(answer)
count[category] = count.get(category, 0) + 1
nps = (
(count.get("promoter", 0) - count.get("detractor", 0))
/ sum(count.values())
* 100
)
return nps
from npspy.core import InvalidAnswerError, categorize, calculate
__version__ = '0.1.0'
※この時点のコミット
テスト
単純なコードではあるが、テストは書いておきたい。ここではpytestの枠組みに乗っかろう。すでにあるtests/test_npspy.py
をこんな感じで書き換える。
import pytest
from npspy import InvalidAnswerError, categorize, calculate
def test_categorize_negative():
with pytest.raises(InvalidAnswerError):
categorize(-1)
def test_categorize_0():
assert categorize(0) == "detractor"
def test_categorize_more_than_10():
with pytest.raises(InvalidAnswerError):
categorize(11)
def test_calculate_0():
assert calculate([0, 9]) == 0
def test_calculate_100():
assert calculate([9, 10]) == 100
関数名はtest
から始めること(詳細はこちら)。テストを書き終えたら以下のコマンドを実行。poetry run
が必要なのは、poetryが準備したvirtualenvで実行するため。
poetry run pytest
※この時点のコミット
型テスト
とりあえず意図通りに動くことはテストできたが、今後のために型テストも導入したい。まずはmypyのインストール。--dev
を指定するのは、開発環境でのみ必要であることを明示するため。
poetry add mypy --dev
先ほどのコードに型アノテーションを足すと、こんな感じ。
from typing import Dict, Iterable, Literal
Categories = Literal["detractor", "passive", "promoter"]
class InvalidAnswerError(Exception):
"""Class for invalid answer related errors."""
pass
def categorize(answer: int) -> Categories:
"""Categorize an answwer for the NPS question.
省略
"""
if 0 <= answer <= 6:
return "detractor"
elif 7 <= answer <= 8:
return "passive"
elif 9 <= answer <= 10:
return "promoter"
else:
raise InvalidAnswerError(f"Invalid answer: {answer}")
def calculate(answers: Iterable[int]) -> float:
"""Calculates nps.
省略
"""
count: Dict[Categories, int] = {}
for answer in answers:
category = categorize(answer)
count[category] = count.get(category, 0) + 1
nps = (
(count.get("promoter", 0) - count.get("detractor", 0))
/ sum(count.values())
* 100
)
return nps
npspy以下のファイルを対象に型テストを実行。--strict
フラグは付けておくのがおすすめ(詳細はこちら)。
poetry run mypy npspy/**.py --strict
※この時点のコミット
フォーマッタ
コーディングスタイルを統一するためにフォーマッタもほしい。Blackを導入しよう。
poetry add black --dev
以下のように実行する。
poetry run black .
不足していた空行などがいい感じに補われた。
※この時点のコミット
linter
コードの品質を担保するためにlinterも導入したい。ここではFlake8を導入する。
poetry add flake8 --dev
Blackと併用するために、設定ファイルを作っておく(詳細はこちら)。
[flake8]
max-line-length = 88
extend-ignore = E203
実行方法は以下の通り。だいぶ注意深くコーディングしていたつもりだが怒られた。
poetry run flake8
せっかくなのでnpspy/__init__.py
を修正。
from npspy.core import InvalidAnswerError, Categories, categorize, calculate
__all__ = [
"InvalidAnswerError",
"Categories",
"categorize",
"calculate",
]
__version__ = "0.1.0"
※この時点のコミット
公開
公開もPoetryを利用すれば難しくない。先に進む前に以下は済ませておく。
上記の準備が済んだら、テスト用のPyPiを使えるようにpoetry.toml
というファイルを作成しておく(本番用は特別な準備不要)。
[repositories]
[repositories.testpypi]
url = "https://test.pypi.org/legacy/"
では、いよいよ公開。コマンドは以下(UsernameとPasswordの入力が必要)。
# テスト用
poetry build
poetry publish -r testpypi
# 本番用
poetry build
poetry publish
無事に公開できたようだ。
※この時点のコミット
最後に
どうにか公開まで漕ぎつけられた。この記事が、いろいろなツールを試すチュートリアルとして誰かの役にたてば幸い。