この記事は正しい方法の案内ではありません。自己流の方法のメモです。
できるだけ (事実上の) 標準に沿ったやり方を採用していますが、一部個人的な好みによって標準的でない方法があります。
Python プロジェクト の始め方なので、Python パッケージ を作るだけなら多くの箇所をスキップ可能です。
ライブラリとして開発する (=他のプロジェクトから import
される) のかアプリとして開発する (=最終成果物で、 import
されることはない) のかによって初期設定を変えるのは面倒なので、どちらでもいけるように最大公約数的な設定方法を採っています。
以下、 myproject
をプロジェクト名のプレースホルダーとして使います。
仮想環境の準備
pyenv とそのプラグインである pyenv-virtualenv を使います1。
pyenv それ自体のインストールは pyenv-installer を使うと楽。
$ pyenv install 3.10.12
$ pyenv virtualenv 3.10.12 myproject
$ pyenv activate myproject
レポジトリ作成 & clone
Github を前提にしてます。
Github でレポジトリ作成し、python 向けの .gitignore
を自動生成しておく。
LICENSE も自動生成でいいなら生成。特に気にしないなら MIT が無難だと思います。
README.md もどうせあとで作るのでここで自動生成。
Github 側のレポジトリが出来たら、ローカルに clone:
git clone git@github.com:<your_account>/myproject.git
プロジェクトディレクトリ準備
以下の通りディレクトリ構成を作ります
myproject/
├── LICENSE # 適当に設定
├── Makefile # 後述
├── README.md # 適当に設定
├── myproject/ # 後述
│ ├── __init__.py
│ ├── _version.py
│ ├── cli.py
│ ├── module1.py
│ └── module2/
│ └── module2.py
├── myproject.code-workspace # 後述
├── pyproject.toml # 後述
├── requirements-dev.in # 後述
├── requirements.in # 後述
└── tests/ # 後述
├── test_module1.py
└── test_module2/
└── test_module2.py
pyproject.toml
-
[project]
以下は 公式の仕様 に則って設定 -
[tool.pysen]
以下は pysen を参照 - build backend は setuptools を利用
[project]
name = "myproject"
version = "0.0.0"
description = "1行説明"
readme = "README.md"
requires-python = ">=3.10"
license = { file = "LICENSE" }
authors = [
{ name = "Your Name", email="dummy@example.com" },
]
scripts = { myproject = "myproject.cli:cli" } # CLI がいらないなら不要
dynamic = [ "dependencies", "optional-dependencies" ]
[build-system]
requires = [ "setuptools>=60" ]
build-backend = "setuptools.build_meta"
[tool.setuptools.dynamic]
dependencies = { file = [ "requirements.in" ] } # アプリの場合は "requirements.txt" のほうを指定
optional-dependencies.dev = { file = [ "requirements-dev.txt" ] }
[tool.pysen]
version = "0.10"
[tool.pysen.lint]
enable_black = true
enable_flake8 = true
enable_isort = true
enable_mypy = true
mypy_preset = "strict"
line_length = 120
py_version = "py310"
isort_known_first_party = ["myproject"]
[[tool.pysen.lint.mypy_targets]]
paths = [
"myproject/",
"tests/",
]
Makefile
Makefile はスペースとタブを区別するので注意
SHELL=/usr/bin/env bash
.PHONY: install
install:
pip install pip-tools
pip-sync requirements.txt
pip install .
.PHONY: install-dev
install-dev:
pip install pip-tools
pip-sync requirements.txt requirements-dev.txt
pip install -e .
.PHONY: pin
pin:
pip install pip-tools
pip-compile requirements.in -o requirements.txt
pip-compile requirements-dev.in -o requirements-dev.txt
# black との兼ね合いで E501 が disable されるが、black だけだと lint ができなくて不便なので enable しておく
.PHONY: pysen-configure
pysen-configure:
pysen generate .
sed -i setup.cfg -e 's/ignore = E203,E231,E501,W503/ignore = E203,E231,W503/g'
# pysen run lint がうまく階層を掘ってくれないので、それぞれ個別に実行
.PHONY: lint
lint:
black --config pyproject.toml --diff --check tests/ myproject/
flake8 --config setup.cfg tests/ myproject/
isort --settings-path pyproject.toml --diff --check-only tests/ myproject/
mypy --show-absolute-path --pretty --config-file setup.cfg tests/ myproject/
# pysen run format がうまく階層を掘ってくれないので、それぞれ個別に実行
.PHONY: format
format:
black --config pyproject.toml tests/ myproject/
isort --settings-path pyproject.toml tests/ myproject/
.PHONY: test
test:
pytest tests/
pip-compile
や pip-sync
は、 pip-tools のサブコマンド。依存関係のロックを取ったり、そのロックから依存ライブラリを一括インストールする機能がある。
python のボイラープレート
各ファイルを次の通り初期化。CLI が不要なら cli.py も不要です:
from myproject._version import __version__ # NOQA
from importlib.metadata import PackageNotFoundError, version
try:
__version__ = version("myproject")
except PackageNotFoundError:
__version__ = "0.dev0"
import click
from myproject import module1
from myproject.module2 import module2
@click.command()
@click.option("--arg", type=str)
def cli(arg: str) -> None:
print(module2.that_function(module1.some_function(arg)))
def some_function(arg: str) -> str:
return "hello, " + arg
def that_function(arg: str) -> str:
return arg + "!!!"
from myproject import module1
class TestModule1:
def test_some_function(self) -> None:
assert module1.some_function("foo") == "hello, foo"
from myproject.module2 import module2
class TestModule2:
def test_that_function(self) -> None:
assert module2.that_function("foo") == "foo!!!"
click>=8.1
-c requirements.txt
black>=24.0
flake8>=7.0
isort>=5.0
mypy>=1.8
pip-tools>=7.3
pysen>=0.10
pytest>=8.0
開発版インストール
次の通りコマンドを実行していく:
$ make pin # requirements.in と requirements-dev.in のロックを取って requirements.txt と requirements-dev.txt を生成
$ make install-dev # editable mode でインストール
$ make lint # linter 実行
$ make format # formatter 実行
$ make test # テスト実行
$ myproject --arg world # cli 動作チェック
hello, world!!!
VSCode 設定
VSCode 上では extension 連携の都合から pysen を経由せずに flake8/mypy/isort/black を呼び出しているため、以下のコマンドで設定を生成しておく:
make pysen-configure # pyproject.toml や setup.cfg に flake8/mypy/isort/black 用の設定が追加される
Visual Studio Code の設定ファイルを以下の通り作成。
/Users/keisuke.nakata/.pyenv/versions/myproject
の部分はpyenv prefix
の出力結果に置き換えてください。-
{workspaceFolder}
の部分は、VSCode workspace に登録されているのが単一のフォルダであることを前提にしているので、複数フォルダ登録しているときは絶対パスで記載
{
"folders": [
{
"path": "."
}
],
"settings": {
"python.defaultInterpreterPath": "/Users/keisuke.nakata/.pyenv/versions/myproject/bin/python",
"flake8.args": [
"--config=${workspaceFolder}/setup.cfg"
],
"flake8.path": [
"/Users/keisuke.nakata/.pyenv/versions/myproject/bin/flake8"
],
"mypy-type-checker.args": [
"--follow-imports=silent",
"--ignore-missing-imports",
"--show-column-numbers",
"--no-pretty",
"--config-file=${workspaceFolder}/setup.cfg"
],
"mypy-type-checker.path": [
"/Users/keisuke.nakata/.pyenv/versions/myproject/bin/mypy"
],
"black-formatter.path": [
"/Users/keisuke.nakata/.pyenv/versions/myproject/bin/black"
],
"isort.path": [
"/Users/keisuke.nakata/.pyenv/versions/myproject/bin/isort"
]
}
}
Github actions の設定
ここまででボイラープレートが完成したので commit & push して、github actions を設定する。
Python Package のテンプレートを選んで、微修正。今回は次のようにすればよい:
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
name: Python lint & test
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install --upgrade pip
make install-dev
- name: Lint
run: |
make lint
- name: Test
run: |
make test
make install-dev
だとロックファイルからのインストールになってしまうため、外部パッケージの破壊的変更に CI で気づけない。
アプリとして作っている場合は運用環境が定まっているのでこれで問題ない。
ただし、ライブラリとして作っている場合でも、原理主義的にはあらゆる依存パッケージのバージョンの組み合わせを試さないといけないことになり、現実的ではないと思われる。
(全組み合わせとまではいかずとも、最新版での挙動を確認するくらいはやってもいいかも)
PyPI への publish
ライブラリとして開発している場合のみ。
github actions の画面から "Publish Python Package" という新しい workflow をテンプレートから選択し、微修正。今回は次のようにすれば良い:
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Upload Python Package
on:
release:
types: [published]
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: release
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: '3.x'
- name: Install dependencies
run: |
pip install --upgrade pip
make install-dev
- name: Build package
run: python -m build
- name: Publish package distributions
uses: pypa/gh-action-pypi-publish@release/v1
最初は、本番の PyPI ではなく TestPyPI でテストすると良い。
一番最後の pypa/gh-action-pypi-publish@release/v1
に with
項を足して、 repository-url: https://test.pypi.org/legacy/
を足せば TestPyPI 向けになる。
事前に PyPI のアカウント登録や publisher 登録が必要。大雑把に言うと:
- your account -> Publishing から、Add a new pending publisher に情報を登録し、Add.
- どうやら、PyPI Project Name と Github の repository name は同じでないとうまく認証が通らない。2
その後は、 pyproject.toml の project.version を書き換え、適当な git tag を打って GitHub 側の Release 機能 で新しい Release を作ると、この python-publish.yml という Action が発火し、PyPI へ新しいバージョンがアップロードされる。
参考資料
- The Basics of Python Packaging in Early 2023 - DrivenData Labs
- Publishing with a Trusted Publisher - PyPI Docs