Pythonでの開発・CI/CDの私的ベストプラクティス2022
はじめに
2021年、Pythonで複数の暗号系ライブラリを開発してPyPIで公開してきました。その過程で、setuptools、flit、poetryと、幾つかのパッケージ管理をわたり歩き、GitHub上でのCI/CDも色々試す中で私的なべスプラが定まってきたので、2022年初に備忘録としてまとめておきます。
具体的には、pyenv、poetry、pre-commit、tox、GitHub Actions を活用し、低コストで(=なるべく自動で)、高品質のプロダクトをPyPIにデプロイする方法・設定を共有します。個別のツールの記事はよく目にするのですが、開発ライフサイクル全体をカバーする記事がなかなか無かったので。
- 開発環境の整備 - pyenvで複数のPythonバージョンでの開発環境を整備
- パッケージ管理 - poetry/pyproject.tomlでの一元的なパッケージ管理
-
静的解析、自動整形、テスト
- flake8、black、isort、mypyなどの適用をpre-commitに集約
- pre-commit、ドキュメント生成、複数Pythonバージョンでのテスト・ビルドをtoxに集約
-
GitHub上でのCI/CD
- GitHub Actions による複数OS/Pythonバージョンでのテスト・ビルド・インストール
- GitHub Actions による PyPI への自動デプロイ
- 外部サービス活用(CodeCov - カバレッジの可視化、Read The Docs)
- botによる依存パッケージの自動メンテナンス
ここにまとめた内容は以下のリポジトリに反映されていますのでご参考まで。
- PySETO:JWTより安全という触れ込みのPASETO(Platform-Agnostic Security Tokens)実装。全プロトコルバージョン(v1-v4)に対応。全バージョンコンプは何気に本家のリファレンス実装よりも早かったんです。
- Python CWT:JWT/JOSEのバイナリ版、CWT/COSE実装。CWT/COSEは、主にIoT用途を想定した規格ですが、EUDCCやWebAuthnにも採用されています。
再利用歓迎です。設定ファイルの一覧は、末尾にまとめています。
前提
- 私の開発環境: Ubuntu 20.04.2 LTS
- リポジトリの場所:GitHub。CI/CDはGitHub 前提です。
- PySETO を例として使います。
- 各ツールのインストール方法は説明しません。どう使っているかだけ記します。
開発環境の整備 - pyenv
PyPIで公開するプロダクトを作る場合、複数のPythonバージョンでの動作検証(動作保証範囲の明確化)が不可欠です。このために、Pyenvで複数バージョンをインストールして、複数バージョンで動作確認・テストを行えるようにします。
pyenvのインストールと設定は公式のREADMEを参照。ちゃんと公式を見ましょう。
後述するPoetryで依存パッケージを組み込む過程で、自ずとサポートできる Python バージョンの下限が確定します。PySETOの場合は ^3.6.2
となったので、3.6(3.6.2以上)、3.7、3.8、3.9、3.10をインストールします。
以下、Python 3.9をインストールして、プロジェクト限定で利用する手順:
# Python 3.9 の一覧を確認
$ pyenv install --list | grep 3.9
# リストの中から最新バージョンをインストール
# ※ bz2, readline, sqlite3が無い云々と WARNINGが表示される場合:
# $ sudo apt install libbz2-dev libreadline-dev libsqlite3-dev
$ pyenv install 3.9.9
# プロジェクトで3.9.9を利用
$ cd pyseto
$ pyenv local 3.9.9
$ cat .python-version
3.9.9
$ python -V
Python 3.9.9
全てのPythonバージョンをインストールした後、同時に全バージョンを有効化する手順:
# インストール済みのバージョン確認
$ cd pyseto
$ pyenv versions
system
3.10.1
3.6.15
3.7.12
3.8.12
* 3.9.9 (set by /home/dajiaji/works/pyseto/.python-version)
# 全バージョンを同時に有効化するには、プロジェクト直下の.python-versionに列挙しておく
# ↓ こんな感じに
$ cat .python-version
3.6.15
3.7.12
3.8.12
3.9.9
3.10.1
$ pyenv versions
system
* 3.10.1 (set by /home/dajiaji/works/pyseto/.python-version)
* 3.6.15 (set by /home/dajiaji/works/pyseto/.python-version)
* 3.7.12 (set by /home/dajiaji/works/pyseto/.python-version)
* 3.8.12 (set by /home/dajiaji/works/pyseto/.python-version)
* 3.9.9 (set by /home/dajiaji/works/pyseto/.python-version)
パッケージ管理 - poetry
パッケージ管理にはpoetryを使います。poetryを選んだ理由は、プロジェクトの作成からパッケージ管理・ビルド、PyPIへのデプロイまで、全てが poetry(及び設定ファイルとしてのpyproject.toml - PEP518)で完結することが大きいです。flitも試したのですが、色々エコシステムとして足らないところがあり、現時点(2021-2022年)での採用は見送りました。
poetryのインストール・設定は、公式Webサイトを参照。
まずは、pyseto/pyproject.tomlを晒します。[build-system]と[tool.poetry]のlicenseまでは、poetry init
で対話形式で生成されます。設定内容はpoetryによるパッケージ管理に絞っています。flake8やmypyなどの設定をpyproject.tomlに集約することもできるのですが、私は後述するpre-commit側に寄せています。
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "pyseto"
version = "1.6.3"
description = "A Python implementation of PASETO/PASERK."
authors = ["Ajitomi Daisuke <dajiaji@gmail.com>"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/dajiaji/pyseto"
include = [
"CHANGES.rst",
"docs",
"poetry.lock",
"tests",
"tox.ini",
]
exclude = [
"docs/_build",
]
[tool.poetry.dependencies]
python = "^3.6.2"
cryptography = "^36.0.0"
pycryptodomex = "^3.12.0"
passlib = {extras = ["argon2"], version = "^1.7.4"}
iso8601 = "^1.0.2"
Sphinx = {version = "^4.3.2", optional = true, extras = ["docs"]}
sphinx-autodoc-typehints = {version = "1.12.0", optional = true, extras = ["docs"]}
sphinx-rtd-theme = {version = "^1.0.0", optional = true, extras = ["docs"]}
[tool.poetry.extras]
docs = [
"Sphinx",
"sphinx-rtd-theme",
"sphinx-autodoc-typehints",
]
[tool.poetry.dev-dependencies]
pytest = "^6.2"
pytest-cov = "^3.0.0"
tox = "^3.24.4"
pre-commit = "^2.15.0"
freezegun = "^1.1.0"
include/exclude
[tool.poetry]のinclude
, exclude
を使えば、MANIFEST.in を置き換えられます。無設定でpoetry build
すると、PySETOリポジトリ内の以下のファイルがパッケージに含まれていました。
- パッケージ本体の *.py ファイル(pysetoディレクトリ配下の.pyファイル)
- パッケージ本体の py.typed (pyseto/py.typed)
- pyproject.toml
- LICENSE
- README
私の場合は、上記の通り、docsやtests、CHANGES.rst、tox.ini、poetry.lockを追加でパッケージに入れています。"docs/_build"を自動で除外してくれなかったので、.gitignoreを見てくれているわけではなさそうです。
dependencies
[tool.poetry.dependencies]と[tool.poetry.dev-dependencies]は、poetry add
で追加していきます。ドキュメント生成用の依存パッケージは、後述する Read the Docs への自動デプロイのために、-E docs
をつけて poetry add
し、pip install .[docs]
でインストールできるようにします。
# PySETOライブラリ本体で利用する依存パッケージの追加
$ poetry add cryptography
# ...
# ドキュメント生成に利用する依存パッケージの追加
$ poetry add sphinx --optional -E docs
# ...
# テスト等、開発で利用する依存パッケージの追加
$ poetry add pytest -D
# ...
静的解析、自動整形、テスト
上記のpyproject.tomlを見て、flake8やblackを使わないのか?と思われた方もいるかもしれませんが、私的べスプラとして、静的解析や自動整形は pre-commit に、テストは tox に集約しています。以下の各設定ファイルをベースに説明していきます。
pre-commit
pre-commitを使い、git commit
時に静的解析・自動整形を実行するようにします。例のごとく、pre-commitのインストール方法は説明しません。公式ドキュメントを参照してください。
pyseto/.pre-commit-config.yamlは以下のとおりです。
repos:
- repo: https://github.com/psf/black
rev: 21.12b0
hooks:
- id: black
args: [--line-length, "128"]
- repo: https://github.com/asottile/blacken-docs
rev: v1.12.0
hooks:
- id: blacken-docs
- repo: https://github.com/PyCQA/flake8
rev: 4.0.1
hooks:
- id: flake8
args: [--ignore, "E203,E501,W503"]
additional_dependencies: [flake8-bugbear]
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
hooks:
- id: isort
args: [--profile, "black"]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.930
hooks:
- id: mypy
args: [--ignore-missing-imports]
additional_dependencies: [types-freezegun]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0
hooks:
- id: check-json
- id: check-toml
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
- id: fix-byte-order-marker
- id: trailing-whitespace
私は頻繁にコミットしていく(git commit --amend
をガンガンしていく)開発スタイルなので、コミットに連動させてしまうのが楽で性に合っています。適用しているツールは以下のとおり。
- black:コードの自動整形
- blacken-docs:ドキュメント内のコードサンプルの自動整形
- flake8:コーディングスタイル規約(PEP8)の準拠チェック
- isort:importの整列
- mypy: 型ヒントに基づく型チェック
- pre-commit-hooks:コード以外の設定ファイル(json, toml, yaml)のチェックや自動整形を噛ませています。
flake8など各ツールの設定ファイルを分離することも可能ですが、私の場合は、この.pre-commit-config.yaml内のargsで指定して設定を集約させています。
各ツールのargsについては、お好みで設定してください。私のはクセが強いと思います(--line-length=128
としつつ、E501
を設定しているとか・・)。
tox
toxを使って複数Pythonバージョンでのテストに加え、上記のpre-commitの実行、ドキュメント生成、パッケージビルドまで行います。GitHubへのプッシュ時に、GitHub Actionsと連携するための設定も含めておきます。
toxのインストール方法は説明しません。公式ドキュメントを参照してください。
pyseto/tox.ini は以下のとおりです。
[tox]
envlist =
check
build
build_docs
py{36,37,38,39,310}
isolated_build = True
skip_missing_interpreters = True
[gh-actions]
python =
3.6: py36
3.7: py37
3.8: check, build, build_docs, py38
3.9: py39
3.10: py310
[testenv:check]
whitelist_externals = poetry
skip_install = true
commands =
poetry install --no-root
poetry run pre-commit run --all-files
[testenv:build]
whitelist_externals = poetry
skip_install = true
commands =
poetry build
[testenv:build_docs]
whitelist_externals = poetry
skip_install = true
commands =
poetry install -E docs
poetry run sphinx-build -n -T -W -b html -d {envtmpdir}/doctrees docs docs/_build/html
[testenv]
whitelist_externals = poetry
skip_install = true
commands =
poetry install
poetry run pytest -ra --cov=pyseto --cov-report=term --cov-report=xml tests
[testenv:py310]
whitelist_externals = poetry
skip_install = true
commands =
pip install cleo tomlkit poetry-core requests cachecontrol cachy html5lib pkginfo virtualenv lockfile
poetry install
poetry run pytest -ra --cov=pyseto --cov-report=term --cov-report=xml tests
各設定ブロックのポイントをざっと解説します。
[tox]
tox -e
で実行できる環境リスト(envlist)は以下のとおり。
-
check: 上記のpre-commit実行。
poetry run pre-commit run --all-files
-
build: パッケージのビルド。
poetry build
-
build_doc: ドキュメントのビルド。
poetry run sphinx-build ...
-
py{36,37,38,39,310}: Python3.6 - 3.10環境でのテスト。
poetry run pytest ...
私が有効化しているオプションは、isolated_build
、skip_missing_interpreters
の2つ。toxで独立した仮想環境を使い、もし特定のPythonバージョンがインストール出来ていなくてもエラーにならないようにしています。
[gh-actions]
GitHub Actionsにて、Pythonバージョンに対応させて実行する環境のマッピング定義。
特定のPythonバージョン(3.8)ですべての環境(check, build, build_docs, py38)を実行し、残りはテストのみを実行するようにしています。
[testenv:*]
poetryは、toxの外側でインストール出来ている前提をとっています(whitelist_externals = poetry
)。その上で、各環境での実行内容は、1) poetry install
による依存パッケージのインストール 2) poetryを介したビルドやテスト実行(poetry run pytest ...
)で構成しています。なお現時点では、testenv:py310
は特別扱いしています。いろいろと事前にインストールしないと poetry install
が通らないのです。
ちなみにテストカバレッジには pytest-cov
を使います。標準出力で結果確認できるようにしつつ、同時にcodecovとの連携のため、--cov-report=term
と --cov-report=xml
の両方を指定しています。
GitHub上でのCI/CD
GitHub Actionsや各種Bot、外部サービス活用も含めたGitHub上でのCI/CDについて。
- GitHub Actions
- 外部サービス活用 - CodeCov、Read the Docs
- Bot連携 - depandabot、pre-commit-bot
- Badge設定
GitHub Actions
GitHubへのPull Request時、master(私の場合main)へのPush時にGitHub Actionsを使ってCIを回します。また、パッケージリリース時も、同様にGitHub Actionsを使います。
以下、例のごとく設定ファイルベースで説明していきます。私の場合、以下の3つのGitHub Actionsを使っています。
- pyseto/.github/workflows/python-package.yml
- pyseto/.github/workflows/python-publish.yml
- pyseto/.github/workflows/codeql-analysis.yml
python-package.yml
mainブランチへのPull Request及びPushをトリガとしたGitHub Actionsの設定です。
name: Python CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
name: Python ${{ matrix.python-version }} on ${{ matrix.platform }}
runs-on: ${{ matrix.platform }}
env:
USING_COVERAGE: "3.8"
strategy:
matrix:
platform: [ubuntu-latest, windows-latest]
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: python -m pip install tox-gh-actions poetry
- name: Run tox
run: poetry run tox
- name: Upload coverage to Codecov
if: contains(env.USING_COVERAGE, matrix.python-version) && matrix.platform == 'ubuntu-latest'
uses: codecov/codecov-action@v1
with:
fail_ci_if_error: true
package:
name: Build package
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: "3.8"
- name: Install poetry
run: python -m pip install poetry
- name: Build package
run: poetry build
- name: Show result
run: ls -l dist
- name: Install package
run: python -m pip install .
- name: Import package
run: python -c "import pyseto; print(pyseto.__version__)"
大きくは、test
とpackage
という2つのJobがあります。
前者(test
)は、Linux(Ubuntu)、Windows それぞれで、Python3.6, 3.7, 3.8, 3.9, 3.10 の tox を回し、カバレッジレポートを CodeCov に送信しています。なお、CodeCovとの連携には、トークンを取得し、GitHubリポジトリのSettings > Secrets で CODECOV_TOKEN
として設定しておく必要があります。その他のポイントとしては、tox-gh-actions を使って、GitHub Actionsでのtox実行を並列化している点ですね。
後者(package
)は、Linux(Ubuntu)、WindowsにMacOSを加え、パッケージのビルド、インストール、インポート確認を行っています。ビルドまでは test
ジョブにてPython3.6 ~ 3.10で動作確認を行っているので、ここではバージョンを3.8に固定しています。
python-publish.yml
続いてパッケージリリース時のGitHub Actionsの設定です。
name: Upload Python Package
on:
release:
types: [created]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: "3.8"
- name: Install dependencies
run: python -m pip install poetry
- name: Build and publish
env:
POETRY_USERNAME: ${{ secrets.PYPI_USERNAME }}
POETRY_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
run: |
poetry build
poetry publish -u $POETRY_USERNAME -p $POETRY_PASSWORD
poetry publish
で、PyPIへアップロードします。ここは、Python3.8、ubuntu-latest 固定にしています。
GitHubリポジトリのSettings > Secrets で、PYPI_USERNAME
とPYPI_PASSWORD
を忘れずに設定しておきましょう。
codeql-analysis.yml
CodeQL Analysisは、コードのセキュリティ解析などを行ってくれるものですが、私は特にひねらずデフォルト設定のまま使っています。有効化するには、リポジトリのSecurity > Code scanning alerts から、CodeQL Analysis の Set up this workflow
を選択し、デフォルトで用意されたこのyamlファイルを取り込むだけです。
外部サービス活用
ここまでで折々触れてきましたが、私はコードカバレッジ管理に CodeCov を、ドキュメントの公開に Read the Docs を使っています。完全にパブリックなリポジトリであれば無料で使えるので、こういうのは積極活用したほうが得だし楽ですね。
CodeCov
CodeCovは2021年に脆弱性が問題になりましたが、私としては解消されたと判断して使っています(そもそもパブリックなプロジェクトでしか使っていませんが)。PythonプロジェクトでCodeCovを使う公式チュートリアルがありますので、ご参照下さい。
前述した python-package.ymlから抜粋すると、以下の設定で CodeCovへのレポートアップロードを行っています。
- name: Upload coverage to Codecov
if: contains(env.USING_COVERAGE, matrix.python-version) && matrix.platform == 'ubuntu-latest'
uses: codecov/codecov-action@v1
with:
fail_ci_if_error: true
Read the Docs
Read the Docsでドキュメントを公開する方法は、公式のチュートリアル をご参照ください。
Read the Docsサービス側の設定は割愛し、リポジトリ側の設定を書いてしまうと、ルートディレクトリに以下の .readthedocs.yml を置いておけばよいです。以下の設定によって、readthedocs.org 上で pip install .[docs]
が実行され、ドキュメントが公開されます(前述した pyseto/pyproject.toml 参照)。
version: 2
python:
version: "3.8"
install:
- method: pip
path: .
extra_requirements:
- docs
デプロイ結果は、https://pyseto.readthedocs.io をご覧ください。
Bot連携
最後の話題ですが、依存パッケージのアップデートに、私は dependabotとpre-commit bot を使っています。依存パッケージのメンテナンスが完全にアウトソースできるのでおススメです。
- pre-commit[bot] - GitHub Actionsでのpre-commit実行に加え、.pre-commit-config.yamlに定義されている依存パッケージのアップデートを検出し、自動でアップデートのためのPull Requestを送ってくれます。
-
dependabot[bot] - pyproject.tomlに定義されている依存パッケージのセキュリティアップデートを検出し、自動でアップデートのためのPull Requestを送ってくれます。GitHubリポジトリの Insights > Dependency graph > Dependabot から簡単に有効化できます。設定ファイルは、
.github/dependabot.yml
に吐かれます。インターバルなどの調整が可能です。
Badge設定
せっかくCI/CDを整えたのでバッジも設定しておきましょう。設定方法は、pyseto/README.mdのRawデータを参照してください。
まとめ - 設定ファイル一覧
最後に、本記事で言及した(PySETOの)パッケージ管理・CI/CDのための設定ファイル一覧を貼っておきます。
- pyproject.toml
- .pre-commit-config.yaml
- tox.ini
- .readthedocs.yml
- .github/workflows/python-package.yml
- .github/workflows/python-publish.yml
- .github/workflows/codeql-analysis.yml
- .github/dependabot.yml
- .python-version: ローカルでのPythonバージョン管理用
おわりに
さすがにbotだけという状況も寂しい・・。
Discussion