Pythonプロジェクトを快適にするために導入したツールとその設定
はじめに
私は普段よくTypeScript/Node.jsを書くことが多く、ESLintやPrettierといった開発ツールのエコシステムが便利で、効率よくコーディングできた体験がありました。今回Pythonプロジェクトに参画するにあたり、同様のことが出来ないか検討し、設定した内容について共有します。
個人的にも結構癖のある設定をしている自覚はあり、あくまで設定の一例として記事を見て頂けたらと存じます。
対象読者
普段別言語がメインが、急遽Pythonプロジェクトを作る必要があり、下記の要件がある人向けです
- パッケージの依存関係を管理したい
- チーム開発するため、リンター、コードフォーマッター、テストツールを導入したい
- ただし、mypyで静的解析をがっつりと通せる自信がない
- 環境毎に挙動を変更できるようにしたい
- ソースコード保存時、コードフォーマッターを適用したい
- テストが書きたい
- 関数をモックしたい
- テスト実行を自動化したい
利用するツール
Python関連
Pythonは3.8以上を想定
ツール名 | 用途 |
---|---|
Poetry | パッケージ管理ツール |
isort | インポートの宣言順を整えてくれるコードフォーマッター |
black | 改行や、' と" の統一 末尾, の統一といった広義なコードフォーマッター |
flake8 | PEP8準拠かチェックするリンター |
pysen | 上記のリンター、コードフォーマッターの統合ツール |
pytest | テストツール プラグイン設定に関しては後述 |
python-dotenv | 環境毎にファイル用意し、環境変数を切り替える際に利用するツール |
その他
ツール名 | 用途 |
---|---|
GitHub Actions | テスト自動化 |
ツールのセットアップ
Pythonについては割愛。私はanyenv+pyenvを利用してPythonの管理をしています。
Poetry
python-poetry/poetryを参考に、poetryをインストール
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python
パスを通します
export PATH="$HOME/.poetry/bin:$PATH"
プロジェクトの作成手順
本項を実行するのが面倒な方向けに、GitHubにプロジェクトのテンプレートリポジトリshuntaka9576/python-project-templateを用意しました。クローンしてお使いください。
プロジェクトの初期設定作業
プロジェクト用のディレクトリを作成
mkdir python-project-template cd python-project-template
PoetryでPythonプロジェクトの初期化を行います
poetry init
実行時、対話型でPythonプロジェクトオプションを設定(コメントで設定内容を補足)
Package name [python-project-template]: # Enterでスキップ Version [0.1.0]: # Enterでスキップ Description []: # Enterでスキップ Author [xxxx <xxxxxx@gmail.com>, n to skip]: # Enterでスキップ License []: # Enterでスキップ Compatible Python versions [^3.7]: # Enterでスキップ Would you like to define your main dependencies interactively? (yes/no) [yes] no # no Would you like to define your development dependencies interactively? (yes/no) [yes] # no # 作成されるpyproject.tomlが標準出力に出力 Do you confirm generation? (yes/no) [yes] # Enterでスキップ # pyproject.tomlが作成される
プロジェクトルートの.venv
でパッケージを管理したいので、以下のコマンドを実行します
poetry config --local virtualenvs.in-project true # poetry.tomlが作成される
pysenの導入と設定
pysenを導入します
poetry add -D pysen==0.9.1 -E lint poetry add -D pytest
pyproject.toml
にpysen
の設定を追記
mypyは、導入するハードルが高いプロジェクトもあることから、今回falseとしています。(すいません)
[tool.pysen] version = "0.9" [tool.pysen.lint] enable_black = true enable_flake8 = true enable_isort = true enable_mypy = false mypy_preset = "strict" line_length = 88 py_version = "py37" [[tool.pysen.lint.mypy_targets]] paths = ["."]
pytestの導入と設定
pytestを導入します
poetry add -D pytest
pytest補助パッケージを追加
# モックパッケージ poetry add -D pytest-mock # テストの表示を見やすくするパッケージ poetry add -D pytest-sugar
dotenvの導入と設定
dotenvを導入します。下記のように追加するとCLI機能のみ追加できます。
poetry add "python-dotenv[cli]"
dotenv
は、以下のように実行されることを想定し、本番向けパッケージとして追加しています
# 開発 .venv/bin/dotenv --file .env.development run -- .venv/bin/python src/main.py # 本番 .venv/bin/dotenv --file .env run -- .venv/bin/python src/main.py
タスクランナーの設定
今回はMakefileを利用しますが、本設定は任意です。
コマンド | 説明 |
---|---|
make start {環境名} | {環境名}で実行 |
make lint | リンター実行 |
make lint-fix | フォマッター実行後、リンター実行 |
make install-dev | 開発ツールを含めた依存解決 |
make install | 開発ツールを除いた依存解決(本番環境向け) |
start: if [ -n "${ENV}" ]; then \ .venv/bin/dotenv --file ${ENV} run -- .venv/bin/python src/main.py; \ lint: poetry run pysen run lint lint-fix: poetry run pysen run format && \ poetry run pysen run lint test-unit: poetry run pytest install-dev: poetry install install: poetry install --no-dev
本番環境のみ入れたいパッケージがある場合
IoT開発等では、開発時はMacで本番はラズパイみたいなケースの場合、アーキテクチャ依存で追加できないパッケージがあったりします。
例えばRPi.GPIO
は、GPIOがないMacでは追加できません。その際はpyproject.toml
のextras
オプションを活用します。
[tool.poetry.extras] prd = ["RPi.GPIO"]
--extras prd
指定時のみ、前述の[]
の中のパッケージをインストールすることが可能です
本オプションを利用する場合、前述のMakefileは以下のように書き換える必要があります
install: - poetry install --no-dev + poetry install --no-dev --extras prd
Visual Studio Code設定
詳細はリポジトリをご参照ください
ポイントは下記です。
- エディタ側既にPython設定がある場合を考慮し、副作用を与えないため、推奨設定を使いたい人のみ
settings.default.json
をsettings.json
にリネームする運用 対象箇所 - 保存時にblackが適用されるようになっている 対象箇所
- エディタ側で実行される
flake8
とpysen
経由で実行されるflake8
の設定を同じにするように、VSCode側の設定を.config/flake8
に記述 対象箇所 - 外部パッケージをコード補完できるように、
.venv
のPythonを指定 対象箇所
GitHub Actions設定
依存関係の取得 -> 静的解析 -> テストの順で実行されます
name: ci python-project-template env: PROJECT_NAME: python-project-template on: [push] jobs: deploy: name: CI timeout-minutes: 5 runs-on: ubuntu-latest strategy: matrix: python-version: [3.7, 3.8] steps: - name: Checkout uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install Poetry run: | curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python echo "$HOME/.poetry/bin" >> $GITHUB_PATH - name: Install Packages run: | poetry install - name: Run Lint run: | poetry run pysen run lint - name: Run Test run: | poetry run pytest
プロジェクトの使い方
本項だけ試したい場合は、shuntaka9576/python-project-templateをクローンしてください。
依存関係の取得
開発
poetry install # or make install
本番
poetry install --no-dev # or make install-dev
パッケージの追加
開発向け
poetry add -D [package名]
本番向け
poetry add [package名]
静的解析/コードフォーマット
Visual Studio Code側にも自動でフォーマットするような設定をしていますが、基本pysenの実行結果を正として運用します。実装してコミットする前に下記のコマンドを実行します。CIでも実行します。
poetry run pysen run format && poetry run pysen run lint # 下記のコマンドが実行されます # or make lint-fix
テスト
ユーザーのIDを指定して、ブログ記事一覧を取ってくるという想定でコードを書きます
Pythonのアプリ側のコードを作成
from typing import List, Optional, TypedDict class DbArticle(TypedDict): title: str user_id: str extra_field1: str extra_field2: str class Article(TypedDict): title: str user_id: str def get_articles_from_db(userId: str) -> Optional[List[DbArticle]]: # TODO DB問い合わせ処理 return None def get_articles(user_id: str) -> List[Article]: db_article_list = get_articles_from_db(user_id) article_list: List[Article] = [] if db_article_list is None: return [] else: for db_article in db_article_list: article: Article = { "title": db_article["title"], "user_id": db_article["user_id"], } article_list.append(article) return article_list if __name__ == "__main__": user_id = "alice" article_list = get_articles(user_id) if len(article_list) == 0: print("Not found article") else: print(article_list)
Pythonのテストコードを作成。適宜init.py
を作る必要があるため、詳細はリポジトリをご参照ください
import src.main class TestClass(object): def test_get_articles(self, mocker) -> None: mock_get_articles_from_db = mocker.patch.object( src.main, "get_articles_from_db" ) mock_get_articles_from_db.return_value = [ { "title": "blog title", "user_id": "test_user_id", "extra_field1": "extra_field1_value", "extra_field2": "extra_field2_value", } ] article_list = src.main.get_articles("test_user_id") mock_get_articles_from_db.assert_called_once_with("test_user_id") assert article_list == [ { "title": "blog title", "user_id": "test_user_id", } ]
実行
poetry run pytest # or make test-unit
実行結果は、pytest-sugar
を利用しているため、デフォルトよりテスト結果をリッチに表示してくれるのでおすすめです。
$ make test-unit poetry run pytest Test session starts (platform: darwin, Python 3.8.9, pytest 6.2.4, pytest-sugar 0.9.4) rootdir: /Users/takahashi.shunichi/repos/github.com/shuntaka9576/python-prj-sample plugins: sugar-0.9.4, mock-3.6.1 collecting ... tests/unit/test_main.py ✓ 100% ██████████ Results (0.04s): 1 passed
最後に
本設定を作り込んだのが少し古く、一応VSCodeのLSPの設定など見直し更新をかけていますが不備がありましたら申し訳ございません。心残りとしては、mypyをもう少しがっつり組み込めたら、より効率よく開発が出来たなぁと感じております。 ご参考になれば幸いです。