みなさんこんにちは、 電通 総研コーポーレート本部システム推進部の佐藤太一です。 この記事では、 VS Code のDev Containerを使ってOSに依存しない Python の開発環境を構築する方法をステップ バイス テップで丁寧に説明します。 VS Code の利用経験があり、また Python によるアプリケーション開発に興味のある方を想定読者として記述しています。 Python の初心者から中級者向けを意識して書いていますので、意図して冗長な説明をしています。 すでに Python によるアプリケーション開発に十分に詳しい方は、まずはまとめだけ読んでみてください。私自身それほど Python のエコシステムに詳しいわけではありませんので、知識の抜け漏れは恐らくあるでしょう。そういった事に気が付いたら、Xなどの SNS でこの記事のURLを付けてコメントをしていただけると幸いです。 はじめに 事前の準備 最小限のDev Container Dev Containerの起動 devcontainer.jsonを編集する環境の構築 最小限のPython用VS Code拡張 uvの導入 Python仮想環境の構築 プロジェクト構成ファイルの作成 コンテナ作成時に動作するシェルスクリプトの追加 Pythonの仮想環境用ボリュームのマウント プロジェクトローカルに仮想環境を作る さぁ、Pythonを動かそう フォーマッタとLinterの導入 pytestの導入 テストコードの追加 テストをターミナルから動かす テストをGUIから動かす テストをデバッガから動かす テストカバレッジを計測する pytest-covの導入 テストカバレッジの微調整 実行時オプションの調整 カバレッジ除外の調整 VS Codeにカバレッジレポートを統合する VS Codeにおけるカバレッジレポートの確認方法 タスクランナーの導入 uvでのタスクランナー まとめ この記事で紹介している開発環境の構成ファイル .devcontainer/devcontainer.json .devcontainer/postCreateCommand.sh .gitignore pyproject.toml はじめに ソフトウェア開発をチームで行うにあたって、もっとも困難な課題の一つは開発環境の再現性を担保することです。 開発チームのメンバーは、それぞれが固有の経験と知識を持っていますし、プロジェクトに参画する際の契約が異なることも多いでしょう。全てのメンバーが、単一のプロジェクトに使いうる全ての 工数 を使ってプロジェクトに貢献できるわけではありません。人によっては、複数のプロジェクトを掛け持ちしていることはあります。 ある開発メンバーは慣れている Mac を使いたいと考える時、違うメンバーは会社から標準的に貸与される Windows で開発したいと考えるかもしれません。しかし、アプリケーションのデプロイ先は Ubuntu や Debian といった Linux だったりします。 こういった状況では、特定のメンバーでのみ発生する不具合が存在したり、または特定のメンバーのローカル環境でしかアプリケーションが動作しないといった事が起こります。 原因が明らかになれば、単にPATH 環境変数 の違いのような簡単なことかもしれませんが、その過程は往々にして困難なものになります。そういった細かい環境差異を原因とする問題の調査はシニアなメンバーにしかできないことが多いのです。 しかし、その調査の時間は明らかに無駄ですし、ほぼ何の価値もありません。 付加価値の高いシニアなメンバーの 工数 を、そういった調査で浪費することはプロジェクトにとって望ましくないでしょう。 この記事で紹介する手法では、Dockerベースのコンテナ技術を使うことで、メンバー毎の環境差分をできるだけ小さくできます。ハードウェアスペックやネットワークに起因するもの以外は差分が全く存在しなくなると言っても良いでしょう。つまり、開発メンバーのなかで誰かのマシンで起きた問題は、メンバー全員のマシンで再現します。 気になってきましたか?それでは、Dev Containerを使った開発環境構築について説明を始めます。 事前の準備 この記事が前提とする環境について軽く説明します。 まず、 VS Code を事前にインストールしておいてください。 次に、 Docker Desktop をインストールして動作する状態にしてください。基本的には単に インストーラ を実行すれば動作する状態になります。 そして、 VS Code に Dev Containers 拡張をインストールしておいてください。 最後に、作業用のプロジェクト ディレクト リを作成してください。ここでは、 devcontainer-python という ディレクト リを作成してプロジェクトのルート ディレクト リとしています。 最小限のDev Container まずは、Dev Containerで公式に提供されている Python の開発環境を導入してみましょう。 プロジェクトのルート ディレクト リに、 .devcontainer という ディレクト リを作って、その中に devcontainer.json というファイル名で以下の内容を保存します。 { " name ": " devcontainer-python ", " image ": " mcr.microsoft.com/devcontainers/python:3.12-bookworm ", " containerEnv ": { " TZ ": " Asia/Tokyo " } , " runArgs ": [ " --init " ] } name の値は、分かりやすい名前なら何でもいいです。ここでは、devcontainer- python としています。 image の値は、mcr. microsoft .com/devcontainers/ python :3.12-bookworm としています。これは、公式のイメージ名です。 ベースイメージや Python ランタイムのバージョンを別なものにしたい場合には、 https://github.com/microsoft/vscode-dev-containers/tree/main/containers/python-3 から探してください。 containerEnv の値は、コンテナ内で参照される 環境変数 です。ここでは タイムゾーン が Asia/Tokyo になるよう設定しています。時刻に起因する問題の調査は難しいので、ここで明示的に設定しています。 runArgs の値として --init を渡すことで、Dockerが /dev/init というシグナルハンドリング用のプロセスを起動してくれます。これによってコンテナを安定的にシャットダウンできるようになります。 Dev Containerの起動 それでは、作ったDev Containerを起動してみましょう。 プロジェクトのルート ディレクト リを VS Code で開いた上で画面左端のアイコンをクリックしてREMOTE EXPLORER を表示します。 reopen the current folder in a container のリンクをクリックするとDev Containerの起動が始まります。 画面右下に、Dev Containerの起動が始まったことを通知するダイアログが数秒間だけ表示されるので、サッとクリックしましょう。 クリックすると起動ログが流れていく様子を観察できます。 起動ログの流れが落ち着いたら、ターミナルを起動してみましょう。ログの右上あたりにある + ボタンです。Ctrlキーと@キーを同時に押しても構いません。 以下のようにターミナルが表示されますね。 コンテナ内では vscode というユーザーで、 workspace/devcontainer-python という ディレクト リをカレント ディレクト リにしています。 ディレクト リパスの devcontainer-python 部分はプロジェクトのルート ディレクト リと同じになっているはずです。 devcontainer. json を編集する環境の構築 ここから、devcontainer. json を編集しながら開発環境を構築していくので、まずは快適に json ファイルを編集できるようにしましょう。 devcontainer. json には、Dev Containerとして起動した VS Code を構成するための設定項目がありますので、それらを編集します。 { " name ": " devcontainer-python ", " image ": " mcr.microsoft.com/devcontainers/python:3.12-bookworm ", " containerEnv ": { " TZ ": " Asia/Tokyo " } , " runArgs ": [ " --init " ] , " customizations ": { " vscode ": { " settings ": { " editor.renderWhitespace ": " all ", " [json][jsonc] ": { " editor.defaultFormatter ": " esbenp.prettier-vscode ", " editor.formatOnSave ": true , " editor.codeActionsOnSave ": { " source.fixAll ": " explicit " } } } , " extensions ": [ " esbenp.prettier-vscode " ] } } } customizations というキーがDev Containerの構成を行うための設定項目です。この中に vscode という項目がありますね。 settings の中では、 VS Code の設定項目を管理します。 editor.renderWhitespace の値として、 all を設定しているのは、ファイルの中に紛れ込んだ全角スペースを見つけやすくするためです。私たちが IME を使っている以上、意図しない場所に全角スペースが入り込んでしまい、それによって理解が困難なエラーメッセージを読むことになるのは避けられません。全角スペースが見えていれば、そういったドハマりから抜け出しやすくなります。 [json][jsonc] の値として、いくつか設定しています。ちなみに、jsoncは、 JSON with commentsの略称です。 editor.defaultFormatter の値として、esbenp.prettier- vscode を設定しています。これによってprettierを使ったフォーマットが行われます。 editor.formatOnSave の値として、trueを設定することでファイル保存時にフォーマットが行われるようにしています。 editor.codeActionsOnSave の値として、source.fixAll に"explicit"を指定することで自動的に補正できるフォーマットエラーをprettierが積極的に補正してくれます。 extensions の中では、Dev Containerとして起動された VS Code にインストールされる VS Code 拡張を列挙します。ここでは、 JSON を自動フォーマットするための esbenp.prettier-vscode を設定しています。 最小限の Python 用 VS Code 拡張 次は、 Python 用の VS Code 拡張をいくつか追加していきましょう。 おすすめの拡張は、 Python Python Indent autoDocstring の三つです。他にも便利なものは多くありますが、特に議論の余地なく導入できるものはこれらです。 devcontainer. json のextensionsにこれらの拡張を追加すると、以下のようになります。 { " name ": " devcontainer-python ", " image ": " mcr.microsoft.com/devcontainers/python:3.12-bookworm ", " containerEnv ": { " TZ ": " Asia/Tokyo " } , " runArgs ": [ " --init " ] , " customizations ": { " vscode ": { " settings ": { " editor.renderWhitespace ": " all ", " [json][jsonc] ": { " editor.defaultFormatter ": " esbenp.prettier-vscode ", " editor.formatOnSave ": true , " editor.codeActionsOnSave ": { " source.fixAll ": " explicit " } } } , " extensions ": [ " esbenp.prettier-vscode ", " ms-python.python ", " njpwerner.autodocstring ", " KevinRose.vsc-python-indent " ] } } } devcontainer. json を修正したら、忘れずにDev Containerをリビルドしましょう。 リビルドするには、REMOTE EXPLORER を表示して動作しているコンテナを右クリックします。 ここで表示される コンテキストメニュー から Rebuild Container を選択します。 これで、 VS Code を Python 用のエディタとして使うための準備は整ったと言えます。 しかしながら、アプリケーションの開発環境と呼ぶには、まだまだ不足がありますので順次整えていきましょう。 uvの導入 Python でアプリケーション開発を行うなら、標準ライブラリを使うだけでなく巨大なコミュニティ内で提供されるモジュールを利用して、その恩恵にあずかりましょう。 pipコマンドだけで OSS のモジュールをインストールするという ストロングス タイルも良いものですが、私が推奨するのはuvを使ったモジュール管理です。以前はPoetryを推奨していましたが、今はRustで実装されていて高速に動作するuvをお勧めしています。 Dev Containerには、featuresという機能がありdevcontainer. json を少し書き加えるだけでuvを使えます。 { " name ": " devcontainer-python ", " image ": " mcr.microsoft.com/devcontainers/python:3.12-bookworm ", " containerEnv ": { " TZ ": " Asia/Tokyo " } , " runArgs ": [ " --init " ] , " features ": { " ghcr.io/jsburckhardt/devcontainer-features/uv:1 ": {} } , " customizations ": { " vscode ": { " settings ": { " editor.renderWhitespace ": " all ", " [json][jsonc] ": { " editor.defaultFormatter ": " esbenp.prettier-vscode ", " editor.formatOnSave ": true , " editor.codeActionsOnSave ": { " source.fixAll ": " explicit " } } } , " extensions ": [ " esbenp.prettier-vscode ", " ms-python.python ", " njpwerner.autodocstring ", " KevinRose.vsc-python-indent " ] } } } features の値として、 "ghcr.io/jsburckhardt/devcontainer-features/uv:1": {} が記述されていますね。 こうすることで、Dev Container起動時にuvがコンテナ内のゲストOSにグローバルインストールされるのです。 devcontainer. json を書き換えたらDev Containerをリビルドしましょう。 Python 仮想環境の構築 Dev Containerのリビルドから戻ってきたら、uvが使うプロジェクト構成ファイルを作りましょう。 プロジェクト構成ファイルの作成 プロジェクトのルート ディレクト リで、 uv init コマンドを実行するといくつかのファイルが作成されます。 特に重要なpyproject.tomlだけ抜粋します。 [project] name = "devcontainer-python" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [] このファイルを作る方法は色々ありますので、詳細を知りたい方はuvのドキュメントを参考にしてください。 Working on projects コンテナ作成時に動作する シェルスクリプト の追加 次は、コンテナをビルドした直後だけ動作する シェルスクリプト を追加します。 この シェルスクリプト を工夫するとコンテナ内で色んな作業をして、混乱状況になってもリビルドするだけで全てを元に戻せます。 まずは、簡単な シェルスクリプト から始めましょう。.devcontainer ディレクト リにpostCreateCommand.shというファイル名で以下の内容を保存します。このファイルの改行コードはLFにてください。 #!/bin/sh # postCreateCommand.sh echo "START Install" sudo chown -R vscode:vscode . echo "FINISH Install" 内容としては、プロジェクトのルート ディレクト リ以下にあるファイルや ディレクト リのオーナー権限を全て vscode ユーザーおよび vscode グループにするというものです。 以下のコマンドをターミナルで実行して実行権限を付与して下さい。 chmod +x .devcontainer/postCreateCommand.sh Dev Containerでは、ホストOS上にあるプロジェクトのルート ディレクト リを自動的にbindマウントしています。つまり、ゲストOSであるコンテナから見える既存のファイルや ディレクト リの権限がrootになってしまいます。Dev Container内で使うユーザーをrootにしても良いのですが、個人的には例えコンテナ内であったとしても普段使いするユーザーとしてrootを使いたくはありません。このような習慣を持つことで例えば、何か悪意のあるモジュールを誤ってインストールしてしまった時の被害を少しだけ低減できます。 便利な事にDev Containerが公式に提供しているコンテナイメージでは、 vscode ユーザーをsudoersとして登録しています。参考: common-debian.sh#L209-L213 この シェルスクリプト を動かすには、devcontainer. json にpostCreateCommandというキーで シェルスクリプト の ワークスペース からの 相対パス を設定します。 { " name ": " devcontainer-python ", " image ": " mcr.microsoft.com/devcontainers/python:3.12-bookworm ", " containerEnv ": { " TZ ": " Asia/Tokyo " } , " runArgs ": [ " --init " ] , " features ": { " ghcr.io/jsburckhardt/devcontainer-features/uv:1 ": {} } , " postCreateCommand ": " ./.devcontainer/postCreateCommand.sh ", " customizations ": { " vscode ": { " settings ": { " editor.renderWhitespace ": " all ", " [json][jsonc] ": { " editor.defaultFormatter ": " esbenp.prettier-vscode ", " editor.formatOnSave ": true , " editor.codeActionsOnSave ": { " source.fixAll ": " explicit " } } } , " extensions ": [ " esbenp.prettier-vscode ", " ms-python.python ", " njpwerner.autodocstring ", " KevinRose.vsc-python-indent " ] } } } devcontainer. json を書き換えたのでDev Containerをリビルドしましょう。 コンテナ作成時に、postCreateCommand.shが動作するのを確認できます。 これは作成時しか動作しないので、例えば単に VS Code を再起動しても動作しません。つまり少々コストの高い処理を記述しても待つのは一度だけになります。 Python の仮想環境用ボリュームのマウント 次は、uvが作る Python の仮想環境について考えてみましょう。 要はプロジェクトのルート ディレクト リ以下に、作成される .venv ディレクト リをどう扱うかということです。 取りうる選択肢として最初の候補は特に気にせず .gitignore ファイルへ .venv ディレクト リを追加する、というやり方です。プロジェクトのルート ディレクト リは、Dev Containerによって既にbindマウントされているのだから、そのままにしておいてもそれほど大きな問題はありません。とはいえ、ホストOSでは実行 不能 な実行バイナリが直接 ファイルシステム 上に現れるのは、あまり気持ちよくはありません。 次の選択肢は、 .venv ディレクト リを特別扱いしてvolumeマウントすることです。これによって、ホストOS上に Python の仮想環境が直接現れなくなります。加えて、volumeマウントはbindマウントするよりもI/Oのパフォーマンスが少しだけ改善します。また、gitなどの構成管理ツールから明示的に除外しなくていいのも利点です。 volumeマウントをDev Containerに追加するには、devcontainer. json にmountsという項目の設定を追加します。 { " name ": " devcontainer-python ", " image ": " mcr.microsoft.com/devcontainers/python:3.12-bookworm ", " containerEnv ": { " TZ ": " Asia/Tokyo " } , " runArgs ": [ " --init " ] , " features ": { " ghcr.io/jsburckhardt/devcontainer-features/uv:1 ": {} } , " postCreateCommand ": " ./.devcontainer/postCreateCommand.sh ", " mounts ": [ " source=venv-${devcontainerId},target=${containerWorkspaceFolder}/.venv,type=volume " ] , " customizations ": { " vscode ": { " settings ": { " editor.renderWhitespace ": " all ", " [json][jsonc] ": { " editor.defaultFormatter ": " esbenp.prettier-vscode ", " editor.formatOnSave ": true , " editor.codeActionsOnSave ": { " source.fixAll ": " explicit " } } } , " extensions ": [ " esbenp.prettier-vscode ", " ms-python.python ", " njpwerner.autodocstring ", " KevinRose.vsc-python-indent " ] } } } devcontainer. json では、設定値の中に ${ で始まり } で終わる部分があると、その中を変数として特別扱いします。 mountsの値だけを取り出してきたのがこれです。 "source=venv-${devcontainerId},target=${containerWorkspaceFolder}/.venv,type=volume" ここでは、 devcontainerId と containerWorkspaceFolder という変数が展開されてDockerの起動オプションに渡されます。それぞれにどんな値が入っているのかは、公式のマニュアルを確認してください。 https://containers.dev/implementors/json_reference/#variables-in-devcontainerjson プロジェクトローカルに仮想環境を作る 次は、いよいよ Python 仮想環境の構築です。と言ってもuvを使っているので極めて簡単です。 先ほど作った、postCreateCommand.sh にuv用のコマンドを2行を追加するだけです。 #!/bin/sh # postCreateCommand.sh echo "START Install" sudo chown -R vscode:vscode . uv venv --allow-existing uv sync echo "FINISH Install" まず、 uv venv --allow-existing コマンドを実行して仮想環境を作ります。 --allow-existing は既に.venv ディレクト リが存在する場合には、そのまま使うようにするオプションです。 uv venv コマンドが実行された時に既存の.venv ディレクト リが存在すると削除して再生成しようとするのですが、今回は .venv ディレクト リはボリュームマウントしているので削除できずにエラーになります。 もしここで uv venv --allow-existing --python 3.11 のように既にインストール済みの Python とは違ったバージョンの Python を指定すると自動的に実行バイナリをダウンロードしてきてくれます。 次の、 uv sync コマンドを実行するとpyproject.tomlに基づいて必要なライブラリやツールを自動的にインターネットからダウンロードしてくれます。 動作を確認するために、 シェルスクリプト の変更が終わったらDev Containerをリビルドしましょう。 さぁ、 Python を動かそう ここからは、いよいよ Python のコードを動かします。 ワークスペース のルート ディレクト リには uv init が作成した hello.py というファイルがあるはずです。 def main (): print ( "Hello from devcontainer-python!" ) if __name__ == "__main__" : main() 特別さのないコードですね。このファイルを開いた状態で VS Code の右下あたりに注目してください。 このような表示になっているなら、 Python の仮想環境に配置されている インタープリタ ーが使われています。 そうでない場合は赤く囲った内側をクリックしてください。そうすると、 インタープリタ ーを選択するダイアログが表示されるので、 .venv/bin/python というパスの インタープリタ ーをクリックすることで選択してください。 プロジェクト直下の.venv ディレクト リが VS Code に正しく認識された状態になると、ターミナルを起動した際、自動的にactivate スクリプト を実行してくれます。 これでuvで管理しているモジュールや実行バイナリが実行されるようになりました。 この状態を維持するために、devcontainer. json に python.defaultInterpreterPath という設定に ".venv/bin/python" を追加します。 設定追加後のdevcontainer. json は以下のようになります。 { " name ": " devcontainer-python ", " image ": " mcr.microsoft.com/devcontainers/python:3.12-bookworm ", " containerEnv ": { " TZ ": " Asia/Tokyo " } , " runArgs ": [ " --init " ] , " features ": { " ghcr.io/jsburckhardt/devcontainer-features/uv:1 ": {} } , " postCreateCommand ": " ./.devcontainer/postCreateCommand.sh ", " mounts ": [ " source=venv-${devcontainerId},target=${containerWorkspaceFolder}/.venv,type=volume " ] , " customizations ": { " vscode ": { " settings ": { " editor.renderWhitespace ": " all ", " python.defaultInterpreterPath ": " .venv/bin/python ", " [json][jsonc] ": { " editor.defaultFormatter ": " esbenp.prettier-vscode ", " editor.formatOnSave ": true , " editor.codeActionsOnSave ": { " source.fixAll ": " explicit " } } } , " extensions ": [ " esbenp.prettier-vscode ", " ms-python.python ", " njpwerner.autodocstring ", " KevinRose.vsc-python-indent " ] } } } devcontainer. json を書き換えたらDev Containerをリビルドしましょう。 フォーマッタとLinterの導入 コードを実行できるようになったので、次はフォーマッタとLinterを導入しましょう。ターミナルを VS Code から起動して以下のコマンドを実行します。 uv add ruff --dev これによって、 Python 用のコードフォーマッタかつ、LinterであるRuffがインストールされます。 ruff これらを VS Code と連携するための設定と拡張を追加しましょう。 { " name ": " devcontainer-python ", " image ": " mcr.microsoft.com/devcontainers/python:3.12-bookworm ", " containerEnv ": { " TZ ": " Asia/Tokyo " } , " runArgs ": [ " --init " ] , " features ": { " ghcr.io/jsburckhardt/devcontainer-features/uv:1 ": {} } , " postCreateCommand ": " ./.devcontainer/postCreateCommand.sh ", " mounts ": [ " source=venv-${devcontainerId},target=${containerWorkspaceFolder}/.venv,type=volume " ] , " customizations ": { " vscode ": { " settings ": { " editor.renderWhitespace ": " all ", " python.defaultInterpreterPath ": " .venv/bin/python ", " [python] ": { " editor.defaultFormatter ": " charliermarsh.ruff ", " editor.formatOnSave ": true , " editor.codeActionsOnSave ": { " source.fixAll ": " explicit ", " source.organizeImports ": " explicit " } } , " [json][jsonc] ": { " editor.defaultFormatter ": " esbenp.prettier-vscode ", " editor.formatOnSave ": true , " editor.codeActionsOnSave ": { " source.fixAll ": " explicit " } } } , " extensions ": [ " esbenp.prettier-vscode ", " ms-python.python ", " njpwerner.autodocstring ", " KevinRose.vsc-python-indent ", " charliermarsh.ruff " ] } } } ここでは、 以下のような変更を加えています。 [python] に設定を記述することでファイル保存時にフォーマットやLint、それに伴う自動補正が行われるようにしました。 charliermarsh.ruff を VS Code 拡張として追加しました。 RuffはBlackとの互換性のためにコードを折り返す際の基準とする文字数が 88 と異様に小さいのでここだけは設定を変更します。 Ruffの設定は、pyproject.tomlに記載します。 [project] name = "devcontainer-python" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [] [dependency-groups] dev = [ "ruff>=0.7.1", ] [tool.ruff] line-length = 200 ruffの設定を細かく調整することは少ないと思いますが、マニュアルはこちらです。 Configuring Ruff devcontainer. json を書き換えたのでDev Containerをリビルドですよね。 pytestの導入 コードを快適に書けるようになってきたので、次はテスティング フレームワーク の導入です。 Python には多くのテスティング フレームワーク がありますが、私が一番気に入っているのはpytestです。 uvを使ってpytestを導入しましょう。以下のコマンドを実行します。 uv add pytest --dev モジュールを追加したら VS Code の設定も変更します。 { " name ": " devcontainer-python ", " image ": " mcr.microsoft.com/devcontainers/python:3.12-bookworm ", " containerEnv ": { " TZ ": " Asia/Tokyo " } , " runArgs ": [ " --init " ] , " features ": { " ghcr.io/jsburckhardt/devcontainer-features/uv:1 ": {} } , " postCreateCommand ": " ./.devcontainer/postCreateCommand.sh ", " mounts ": [ " source=venv-${devcontainerId},target=${containerWorkspaceFolder}/.venv,type=volume " ] , " customizations ": { " vscode ": { " settings ": { " editor.renderWhitespace ": " all ", " python.defaultInterpreterPath ": " .venv/bin/python ", " python.testing.pytestArgs ": [ " tests ", " --capture=tee-sys ", " -vv " ] , " python.testing.pytestEnabled ": true , " [python] ": { " editor.defaultFormatter ": " charliermarsh.ruff ", " editor.formatOnSave ": true , " editor.codeActionsOnSave ": { " source.fixAll ": " explicit ", " source.organizeImports ": " explicit " } } , " [json][jsonc] ": { " editor.defaultFormatter ": " esbenp.prettier-vscode ", " editor.formatOnSave ": true , " editor.codeActionsOnSave ": { " source.fixAll ": " explicit " } } } , " extensions ": [ " esbenp.prettier-vscode ", " ms-python.python ", " njpwerner.autodocstring ", " KevinRose.vsc-python-indent ", " charliermarsh.ruff " ] } } } ここで追加したのは、以下の二つです。 python.testing.pytestEnabled を trueにすることで VS Code がpytestを使ってテストコードを検索します。 python.testing.pytestArgs にはpytest起動時のオプションを三つ設定しています。 最初の tests はこの ディレクト リ内にあるテストコードを実行するという意味です。 次の --capture=tee-sys は、テストコード内で標準出力された内容をpytestがキャプチャしてターミナルに出力してくれます。 最後の -vv を付けることで、pytestがキャプチャした出力を途中で切らずに全て出力します。 テストコードの追加 テストコードを追加して動かしてみましょう。 ワークスペース のルート ディレクト リに tests という ディレクト リを作って、その中にtest_sample.pyというファイル名で以下の内容を保存します。 # content of test_sample.py def inc (x): return x + 1 def test_answer (): assert inc( 3 ) == 5 与えられた数字に1を加算する関数と、それをテストする関数です。 テストをターミナルから動かす まずは、テストをターミナルで動かしてみましょう。以下のコマンドを実行します。 pytest 案の定 アサーション が正しくないのでテストは失敗します。 テストを GUI から動かす 次は、 VS Code からテストを動かしてみましょう。 左側のメニューからフラスコのようなアイコンをクリックしてTESTINGビューを表示した上で右向きの三角をクリックするとテストを実行できます。もしくは、単にテスト関数の近くにある右向き三角でも構いません。 予想通りテストは失敗します。 テストをデバッガから動かす 次はテストを デバッグ 実行しましょう。コードを見てもどうしてテストが失敗するのか分からない時は、デバッガを使うと便利です。 まずは、 ブレークポイント を設定します。エディタのガッター部分をクリックすると赤い〇が付いて、そこが ブレークポイント になります。 TESTINGビューの虫アイコンがついたボタンを押して実行するとデバッガが動作します。 設定された ブレークポイント で止まると変数の中身や スタックトレース が確認できますね。 これで快適にテストが実行できるようになったので、ソフトウェア開発環境としては十分だと言えるかもしれません。 高品質なソフトウェアを作るために、もうひと踏ん張りしてみましょう。 テスト カバレッジ を計測する テスト カバレッジ を取ることで、テストコードが意図したとおりにアプリケーションコードをテストできているか確認できるようにしましょう。 テスト カバレッジ は100%を目指して ホワイトボックステスト するのに使うと極めて不毛な時間を過ごすことになります。 しかし、大体75%~85%を目途に意図したとおりにアプリケーションコードが動作しているかを確認するのに使うと非常に便利です。自分は仕様を完全に把握しているのだと思っても抜け漏れは少なからずあるものです。 pytest- cov の導入 pytestには、 Coverage.py を使ってテスト カバレッジ をとれる プラグイン があるので、それを導入しましょう。以下のコマンドを実行します。 uv add pytest- cov --dev これでpytestを実行する際に カバレッジ を取得するオプションが使えます。例えば、以下のコマンドで カバレッジ を取得できます。 pytest -- cov =. -- cov -report html このコマンドを実行後はプロジェクトのルート ディレクト リに htmlcov という ディレクト リが作成されて、その中にhtmlで作成された カバレッジ レポートが格納されています。 HTMLでそれなりにきれいな表示がされていて、ソフトウェア開発が主たる業務でない人にとっては十分なレポートだと言えます。 テスト カバレッジ の微調整 テスト カバレッジ を取り始めると、細かい調整が必要になっていきます。 私自身、それほど Python に詳しいわけではありませんが、実案件での利用を通じて発生した微調整をいくつかご紹介します。 実行時オプションの調整 pyproject.tomlに [tool.coverage.run] という項目を追加します。 [project] name = "devcontainer-python" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [] [dependency-groups] dev = [ "pytest-cov>=5.0.0", "pytest>=8.3.3", "ruff>=0.7.1", ] [tool.ruff] line-length = 200 [tool.coverage.run] branch = true source = ["tests"] omit = ["tests/fixtures/*"] data_file = ".pytest_cache/.coverage" 設定項目は四つです。 branch を有効化することでブランチ カバレッジ を取得するようにしています。これによって カバレッジ の計測コストが若干上がります。 source に カバレッジ の計測対象となるファイルが格納されている ディレクト リを指定しています。 omit では、逆に カバレッジ の計測対象としないファイルが格納されている ディレクト リを指定しています。 data_file では、 カバレッジ 計測データのバイナリファイルを格納する ディレクト リをプロジェクトのルート ディレクト リから.pytest_cache ディレクト リ内に移動することで、普段はその存在を気にしないで済むようにしています。 カバレッジ 除外の調整 ここでは、pyproject.tomlに [tool.coverage.report] という項目を追加して細粒度の構文レベルで カバレッジ 取得対象から ソースコード を除外しています。 [project] name = "devcontainer-python" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [] [dependency-groups] dev = [ "pytest-cov>=5.0.0", "pytest>=8.3.3", "ruff>=0.7.1", ] [tool.ruff] line-length = 200 [tool.coverage.run] branch = true source = ["tests"] omit = ["tests/fixtures/*"] data_file = ".pytest_cache/.coverage" [tool.coverage.report] exclude_lines = [ "pragma: no cover", "def __repr__", "def __str__", "raise AssertionError", "raise NotImplementedError", "if __name__ == .__main__.:", "if TYPE_CHECKING:", "if typing.TYPE_CHECKING:", ] それぞれの詳細については、ここでは説明しません。 VS Code に カバレッジ レポートを統合する コードを書いている プログラマー としては、 カバレッジ レポートは普段使っているエディタ上に表示されてほしいものです。レポートを見るためだけにウィンドウを切り替えるのはわずらわしいですからね。 というわけで、拡張の導入と設定です。コード カバレッジ を表示する VS Code 拡張はいくつかあるのですが、私が試した範囲内では、 Coverage Gutters が最も期待通りに動作しました。 Coverage Guttersは、Coverage.pyが出力した XML のレポートを入力情報として使いますので、pyproject.tomlにレポートの出力先を設定します。 [project] name = "devcontainer-python" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [] [dependency-groups] dev = [ "pytest-cov>=5.0.0", "pytest>=8.3.3", "ruff>=0.7.1", ] [tool.ruff] line-length = 200 [tool.coverage.run] branch = true source = ["tests"] omit = ["tests/fixtures/*"] data_file = ".pytest_cache/.coverage" [tool.coverage.report] exclude_lines = [ "pragma: no cover", "def __repr__", "def __str__", "raise AssertionError", "raise NotImplementedError", "if __name__ == .__main__.:", "if TYPE_CHECKING:", "if typing.TYPE_CHECKING:", ] [tool.coverage.xml] output = ".pytest_cache/coverage.xml" 最後の二行が追加した項目です。これによって、出力レポートのタイプとして XML を指定した際に、.pytest_cache/coverage. xml へファイルが出力されます。 次は、devcontainer. json を修正します。 { " name ": " devcontainer-python ", " image ": " mcr.microsoft.com/devcontainers/python:3.12-bookworm ", " containerEnv ": { " TZ ": " Asia/Tokyo " } , " runArgs ": [ " --init " ] , " features ": { " ghcr.io/jsburckhardt/devcontainer-features/uv:1 ": {} } , " postCreateCommand ": " ./.devcontainer/postCreateCommand.sh ", " mounts ": [ " source=venv-${devcontainerId},target=${containerWorkspaceFolder}/.venv,type=volume " ] , " customizations ": { " vscode ": { " settings ": { " editor.renderWhitespace ": " all ", " python.defaultInterpreterPath ": " .venv/bin/python ", " python.testing.pytestArgs ": [ " tests ", " --capture=tee-sys ", " -vv " ] , " python.testing.pytestEnabled ": true , " [python] ": { " editor.defaultFormatter ": " charliermarsh.ruff ", " editor.formatOnSave ": true , " editor.codeActionsOnSave ": { " source.fixAll ": " explicit ", " source.organizeImports ": " explicit " } } , " coverage-gutters.showLineCoverage ": true , " coverage-gutters.showRulerCoverage ": true , " coverage-gutters.coverageFileNames ": [ " .pytest_cache/coverage.xml " ] , " [json][jsonc] ": { " editor.defaultFormatter ": " esbenp.prettier-vscode ", " editor.formatOnSave ": true , " editor.codeActionsOnSave ": { " source.fixAll ": " explicit " } } } , " extensions ": [ " esbenp.prettier-vscode ", " ms-python.python ", " njpwerner.autodocstring ", " KevinRose.vsc-python-indent ", " charliermarsh.ruff ", " ryanluker.vscode-coverage-gutters " ] } } } ここでは、三つの設定項目を追加すると共にインストールする拡張として ryanluker.vscode-coverage-gutters を追加しています。 coverage-gutters.showLineCoverage を有効化することで、行全体に色が付くようになります。 coverage-gutters.showRulerCoverage を有効化することで、行の左側にある目盛り部分に色が付くようになります。 coverage-gutters.coverageFileNames に設定しているパスと、pyproject.tomlが一致していることで、 カバレッジ レポートの結果を ryanluker.vscode-coverage-gutters が正しく処理できます。 devcontainer. json を書き換えたのでDev Containerをリビルドしましょう。 VS Code における カバレッジ レポートの確認方法 まずは、以下のコマンドを実行して カバレッジ レポートを作成します。 pytest -- cov =. -- cov -report xml 相変わらずテストは失敗していますね。しかし、 .pytest_cache/coverage.xml というファイルは出力されているはずです。これを使って カバレッジ を確認していきましょう。 VS Code の左下あたりに注目してください。 〇Watch という表示があるはずです。これをクリックすると カバレッジ レポートの表示が有効化されます。 カバレッジ レポートがエディタ上に表示されるとこのようになります。 これで、 カバレッジ レポートを確認しながらコードを書けるようになりました。 そろそろこの記事も終盤に差し掛かってきています。もう少しだけお付き合いください。 タ スクラン ナーの導入 最後は、プロジェクト構成ファイルであるpyproject.tomlに定型化された作業をタスクとして記述する方法を説明します。 長いコマンドでも構成ファイル内に書いてあればコマンドの実行ミスは無くなります。また、 GitHub ActionsのようなCIワークフローを構成する際にも、すでにタスクが定義されていれば非常に少ない 工数 で実現できます。 様々なタスク定義ツールがありますが、私が気に入って使っているのは、 Poe the Poet です。以下のコマンドを実行してインストールしましょう。 uv add poethepoet --dev インストールが終わったら、pyproject.tomlにここまで構成してきたタスクをいくつか書いてみましょう。 [project] name = "devcontainer-python" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [] [dependency-groups] dev = [ "poethepoet>=0.29.0", "pytest-cov>=5.0.0", "pytest>=8.3.3", "ruff>=0.7.1", ] [tool.ruff] line-length = 200 [tool.coverage.run] branch = true source = ["tests"] omit = ["tests/fixtures/*"] data_file = ".pytest_cache/.coverage" [tool.coverage.report] exclude_lines = [ "pragma: no cover", "def __repr__", "def __str__", "raise AssertionError", "raise NotImplementedError", "if __name__ == .__main__.:", "if TYPE_CHECKING:", "if typing.TYPE_CHECKING:", ] [tool.coverage.xml] output = ".pytest_cache/coverage.xml" [tool.poe.tasks] lint = "ruff check ." test = "pytest" cover = "pytest --cov=. --cov-report xml" fmt = "ruff format . --check" build = ["fmt", "lint", "test"] [tool.poe.tasks] 以下に書かれているものが実行可能なタスクです。ここでは、五つのタスクを定義しています。 lint タスクでは、 Ruffによるコードの検査を実行します。 testタスクでは、pytestを実行します。 coverタスクでは、コード カバレッジ を取得しながらpytestを実行します。 fmtタスクでは、Ruffによるコードの整形を実行します。 buildタスクでは、 fmtタスクを実行した後にlintタスクを実行し最後にtestタスクを実行します。 これらのタスクは、 poe コマンドから実行できます。例えば、lintタスクを実行する際には、以下のようなコマンドを実行します。 poe lint 非常に簡単ですね。testタスクのように内部的にはコマンドを引数なしで実行しているだけでもタスクとして定義しているのは、ツールの移行コストを下げるためです。 例えば、今回はlinterとしてRuffを使っていますが、使い込む過程で回避しようのない不具合が見つかりFlake8のような実績のあるツールに切り戻すことはありえます。そういった時にCIやCDのワークフローに対する影響をできるかぎり減らして移行できるようにするには、タ スクラン ナーによるコマンドの抽象化が有効です。 uvでのタ スクラン ナー poethepoetのような追加のライブラリ無しにuvだけで動作するタ スクラン ナーについて議論しているチケットがあります。 Using uv run as a task runner これが実装されれば、poethepoet のインストールは不要になるかもしれませんね。 まとめ ここまで、Dev Containerで Python アプリケーションの開発環境を構築する方法について説明してきました。 プロジェクトメンバーができるかぎり同じ環境でアプリケーション開発を行うことは、浪費される 工数 を減らしプロジェクトとして価値のある作業に集中するために必要な事です。 この記事では Python 開発環境を扱いましたが、同様の考え方でTypeScriptや、Rust、 Ruby 、 Java といった他の言語の開発環境を構築できます。 また、 GitHub Codespacesのような クラウド 開発環境の利用も難なくできます。 作っているアプリケーションの種類や、プロジェクトチーム内での役割次第ではブラウザ一つ動く環境さえあればソフトウェア開発できるというのは、非常に魅力的ですよね。 ここまで読んでいただいた皆さん、本当にお疲れさまでした。この記事を読んだ皆さんが、利便性の高い開発環境で本来のソフトウェア開発に集中できることを願って記事の結びとします。 この記事で紹介している開発環境の構成ファイル 最後に作った構成ファイルをまとめて紹介します。 .devcontainer/devcontainer. json { " name ": " devcontainer-python ", " image ": " mcr.microsoft.com/devcontainers/python:3.12-bookworm ", " containerEnv ": { " TZ ": " Asia/Tokyo " } , " runArgs ": [ " --init " ] , " features ": { " ghcr.io/jsburckhardt/devcontainer-features/uv:1 ": {} } , " postCreateCommand ": " ./.devcontainer/postCreateCommand.sh ", " mounts ": [ " source=venv-${devcontainerId},target=${containerWorkspaceFolder}/.venv,type=volume " ] , " customizations ": { " vscode ": { " settings ": { " editor.renderWhitespace ": " all ", " python.defaultInterpreterPath ": " .venv/bin/python ", " python.testing.pytestArgs ": [ " tests ", " --capture=tee-sys ", " -vv " ] , " python.testing.pytestEnabled ": true , " [python] ": { " editor.defaultFormatter ": " charliermarsh.ruff ", " editor.formatOnSave ": true , " editor.codeActionsOnSave ": { " source.fixAll ": " explicit ", " source.organizeImports ": " explicit " } } , " coverage-gutters.showLineCoverage ": true , " coverage-gutters.showRulerCoverage ": true , " coverage-gutters.coverageFileNames ": [ " .pytest_cache/coverage.xml " ] , " [json][jsonc] ": { " editor.defaultFormatter ": " esbenp.prettier-vscode ", " editor.formatOnSave ": true , " editor.codeActionsOnSave ": { " source.fixAll ": " explicit " } } } , " extensions ": [ " esbenp.prettier-vscode ", " ms-python.python ", " njpwerner.autodocstring ", " KevinRose.vsc-python-indent ", " charliermarsh.ruff ", " ryanluker.vscode-coverage-gutters " ] } } } .devcontainer/postCreateCommand.sh 改行コードはLFです。実行権限の付与を忘れずに。 #!/bin/sh # postCreateCommand.sh echo "START Install" sudo chown -R vscode:vscode . uv venv --allow-existing uv sync echo "FINISH Install" .gitignore 改行コードはLFです。 # Python-generated files __pycache__/ *.py[oc] build/ dist/ wheels/ *.egg-info # Virtual environments .venv *_cache pyproject.toml [project] name = "devcontainer-python" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [] [dependency-groups] dev = [ "poethepoet>=0.29.0", "pytest-cov>=5.0.0", "pytest>=8.3.3", "ruff>=0.7.1", ] [tool.ruff] line-length = 200 [tool.coverage.run] branch = true source = ["tests"] omit = ["tests/fixtures/*"] data_file = ".pytest_cache/.coverage" [tool.coverage.report] exclude_lines = [ "pragma: no cover", "def __repr__", "def __str__", "raise AssertionError", "raise NotImplementedError", "if __name__ == .__main__.:", "if TYPE_CHECKING:", "if typing.TYPE_CHECKING:", ] [tool.coverage.xml] output = ".pytest_cache/coverage.xml" [tool.poe.tasks] lint = "ruff check ." test = "pytest" cover = "pytest --cov=. --cov-report xml" fmt = "ruff format . --check" build = ["fmt", "lint", "test"] 執筆: @sato.taichi 、レビュー: @mizuno.kazuhiro ( Shodo で執筆されました )