Pythonプロジェクトの作成(Python 3.11, pyenv + Poetry, VSCode)

バージョン情報

定義・ディレクトリ構成

説明のため、プロジェクトディレクトリ名my_project、パッケージ名my-project、主要なモジュール名my_projectとします。

以下のようなディレクトリ構成にすることを想定しています。

- my_project/
- pyproject.toml
- Dockerfile
- my_project/
- __init__.py
- __main__.py
- cli.py
- tests/
- __init__.py
- test_my_project.py

Python/Poetryのインストール

pyenvでPythonをインストールします。 記事作成時点で最新のリビジョン(0.0.x)を記載していますが、適宜新しいバージョンが出ているか確認し、 更新してください。 マイナーバージョン(0.x.0)を変更する場合、依存する予定のライブラリが動作するかなど、プロジェクトの要件と相談してください。

env PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install 3.11.9

PYTHON_CONFIGURE_OPTS="--enable-shared"は、PyInstallerが動作するようにするために設定しています。

Poetryをインストールします。

# Linux, macOS, WSL
curl -sSL https://install.python-poetry.org | python3 -
# Windows (PowerShell)
(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -

Poetryプロジェクトの作成

Poetryのグローバル設定を変更し、Python仮想環境がプロジェクトのディレクトリ/.venvに作成されるようにします。 これは、VSCode拡張機能のPylanceがPython仮想環境を認識できるようにする、または手動で設定しやすくするための変更です。

poetry config virtualenvs.in-project true

プロジェクトディレクトリmy_projectを作成し、作業ディレクトリにします。

mkdir my_project
cd my_project

pyenvのPythonバージョン指定ファイル.python-versionを作成します。

pyenv local 3.11.9

現在のディレクトリにPoetryプロジェクトを作成します。 対話形式でプロジェクトの設定(pyproject.tomlの作成)をします。

poetry init

(おすすめ)最後に依存ライブラリを聞かれますが、個人的には、操作がややこしく間違ったライブラリをインストールしてしまうのが怖いので、いったんスキップして後で設定することが多いです。

Pythonパッケージ名の仕様

パッケージ名には、以下の文字が使用できます。

  • ラテン文字(A-Z、大文字・小文字は区別されない)
  • アラビア数字(0-9)
  • ピリオド、アンダースコア、ハイフン(これらは区別されない、先頭または末尾に使用できない)

パッケージ名の仕様について、記事作成時点では以下が参考になります。

.gitignoreの作成

GitHubの.gitignoreテンプレートをプロジェクトディレクトリにコピーします。

(おすすめ).gitignoreを編集し、pyenvのPythonバージョン指定ファイル.python-versionをGit管理から除外します。 .python-versionはPythonバージョンをリビジョンまで固定するため、以下のようなケースで 完全に一致したバージョンのPythonをそれぞれインストールすることになり、不便になります。

  • 複数の開発者がいる
  • 複数の開発環境がある(コンピュータ、OS、仮想環境)
  • 複数のPythonプロジェクトがある
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
.python-version

必須ファイルの作成

主要なドキュメントREADME.md、主要なモジュールのディレクトリmy_project/、ファイルmy_project/__init__.pyを作成します。 これらのファイルは、プロジェクトに対してPoetryを動作させるために必要です(Poetry実行時にファイルが存在しない旨のエラーが出ます)。

echo "# my_project" > README.md
mkdir my_project
touch my_project/__init__.py

メインスクリプトの作成

my_project/__init__.py

__version__ = "0.0.0"

my_project/___main__.py

from .cli import main
if __name__ == "__main__":
main()

my_project/cli.py

import logging
from argparse import ArgumentParser, Namespace
from logging import getLogger
from . import __version__ as APP_VERSION
logger = getLogger(__name__)
def execute_command(
args: Namespace,
) -> None:
pass
def execute_subcommand_mysubcommand(
args: Namespace,
) -> None:
pass
def main() -> None:
parser = ArgumentParser()
parser.add_argument(
"--version",
action="version",
version=APP_VERSION,
)
parser.set_defaults(handler=execute_command)
subparsers = parser.add_subparsers()
subparser_mysubcommand = subparsers.add_parser("mysubcommand")
subparser_mysubcommand.set_defaults(handler=execute_subcommand_mysubcommand)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s : %(message)s",
)
args = parser.parse_args()
if hasattr(args, "handler"):
args.handler(args)
else:
parser.print_help()

Gitリポジトリの作成

ローカルGitリポジトリを作成します。 名前、メールアドレスは適宜変更してください。 GitホスティングサービスとしてGitHubを使っていて、メールアドレスを公開したくない場合、 設定ページに記載されているダミーのメールアドレスが利用できます。

git init
git config user.name "John Doe"
git config user.email "mail@example.com"
git commit -m "Initial Commit" --allow-empty

開発を支援するパッケージのインストール

基本的なリンター、フォーマッター、テストツールのPythonパッケージを開発環境向けとして、プロジェクトに追加します。

poetry add --group dev pysen black isort flake8 flake8-bugbear mypy pytest

この記事で想定しているバージョン情報

プロジェクトに追加したPythonパッケージをインストールします。

poetry install

pyproject.tomlに以下のように追記して、pysenの設定をします。

[tool.pysen]
version = "0.11"
[tool.pysen.lint]
enable_black = true
enable_flake8 = true
enable_isort = true
enable_mypy = true
mypy_preset = "strict"
line_length = 88
py_version = "py311"
[[tool.pysen.lint.mypy_targets]]
paths = ["."]

pysenでは、リンター・フォーマッターは、Gitリポジトリが管理しているファイルだけに適用されます。 Gitリポジトリが管理していないファイルを扱わせたい場合は、少なくともステージングしておく必要がある点に注意してください。

次のように、リンター・フォーマッターを適用します。

git add .
poetry run pysen run lint
poetry run pysen run format

依存パッケージのインストール

PyPIに登録されたパッケージのインストール

poetry add requests

PyPI以外のパッケージリポジトリに登録されたパッケージのインストール

# PyTorch for CUDA 11.8
poetry source add --priority=explicit pytorch-cu118 "https://download.pytorch.org/whl/cu118"
poetry add --source pytorch-cu118 torch torchvision torchaudio
# PyTorch for CUDA 12.1
poetry source add --priority=explicit pytorch-cu121 "https://download.pytorch.org/whl/cu121"
poetry add --source pytorch-cu121 torch torchvision torchaudio

Gitリポジトリで管理されたパッケージをインストール

poetry add git+https://github.com/org/repo.git#commithash
poetry add git+https://github.com/org/repo.git#commithash&subdirectory=subdir

開発時だけ使うパッケージのインストール

poetry add --group dev types-requests

requirements.txtの出力

NOTE: この項目で扱うエクスポート機能は、Poetry 1.2からPoetry 1.7までPoetry本体に実装されていましたが、 Poetry 1.8からプラグインpoetry-plugin-exportとして分離されました。

Poetry 1.8ではpoetry-plugin-exportがPoetryの実行環境にデフォルトでインストールされているため、 Poetry 1.7までと同じ挙動が維持されていますが、今後のアップデートでデフォルトではインストールされなくなります。 以下のコマンドで明示的にインストールしておきましょう。

poetry self add poetry-plugin-export
# exportコマンド実行時に上記内容を説明する警告の表示を無効化
poetry config warnings.export false

Poetryが管理する依存関係に基づいて、requirements.txtを作成します。 Poetryを使わずに実行する場合や、Dockerイメージを作る場合に有用です。 インストール先の環境が限定されるのを避けるため、ハッシュ値を含めないようにするオプション--without-hashesを指定しています。

poetry export --without-hashes -o requirements.txt
poetry export --without-hashes --with dev -o requirements-dev.txt

Dockerfileの作成

Dockerイメージは様々な作り方が考えられますが、一例として紹介します。

CPUだけ使う場合

# syntax=docker/dockerfile:1.6
FROM python:3.11
ARG DEBIAN_FRONTEND=noninteractive
ENV PYTHONUNBUFFERED=1
ENV PATH=/home/user/.local/bin:${PATH}
RUN <<EOF
set -eu
apt-get update
apt-get install -y \
gosu
apt-get clean
rm -rf /var/lib/apt/lists/*
EOF
ARG CONTAINER_UID=1000
ARG CONTAINER_GID=1000
RUN <<EOF
set -eu
groupadd --non-unique --gid "${CONTAINER_GID}" user
useradd --non-unique --uid "${CONTAINER_UID}" --gid "${CONTAINER_GID}" --create-home user
EOF
ADD ./requirements.txt /tmp/
RUN --mount=type=cache,uid=${CONTAINER_UID},gid=${CONTAINER_GID},target=/home/user/.cache/pip <<EOF
set -eu
gosu user pip install -r /tmp/requirements.txt
EOF
ADD ./pyproject.toml ./README.md /code/my_project/
ADD ./my_project /code/my_project/my_project
RUN --mount=type=cache,uid=${CONTAINER_UID},gid=${CONTAINER_GID},target=/home/user/.cache/pip <<EOF
set -eu
gosu user pip install -e /code/my_project
EOF
RUN <<EOF
set -eu
mkdir -p /work
chown -R "${CONTAINER_UID}:${CONTAINER_GID}" /work
EOF
WORKDIR /work
# 引数を受け付けない場合(環境変数や設定ファイルで設定する場合、docker compose up -dでの実行を想定する場合)
CMD [ "gosu", "user", "python", "-m", "my_project" ]
# main.pyが引数を受け付ける場合(docker runコマンドやdocker compose runでの実行を想定する場合)
# ENTRYPOINT [ "gosu", "user", "python", "-m", "my_project" ]

NVIDIA GPUを使う場合

--build-arg BASE_RUNTIME_IMAGEリポジトリ
CPUubuntu:22.04Docker Hub: ubuntu
NVIDIA Driver v525nvcr.io/nvidia/driver:525-signed-ubuntu22.04NGC: NVIDIA GPU Driver
CUDA 11.8 + cuDNN 8nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04Docker Hub: nvidia/cuda
CUDA 11.8 + cuDNN 8(開発用ライブラリ入)nvidia/cuda:11.8.0-cudnn8-devel-ubuntu22.04Docker Hub: nvidia/cuda

ビルド環境、実行環境にNVIDIA Container Toolkitのインストールが必要です。

ビルド時、docker build --build-arg BASE_RUNTIME_IMAGE=nvcr.io/nvidia/driver:525-signed-ubuntu22.04のようにベースイメージを切り替える想定のDockerfileになっています。

Dockerイメージ実行時、GPUを使用するにはdocker run --gpus allのように--gpusオプションでGPUの使用を明示する必要があります。

# syntax=docker/dockerfile:1.6
ARG BASE_IMAGE=ubuntu:22.04
ARG BASE_RUNTIME_IMAGE=${BASE_IMAGE}
FROM ${BASE_IMAGE} AS python-env
ARG DEBIAN_FRONTEND=noninteractive
ARG PIP_NO_CACHE_DIR=1
ENV PYTHONUNBUFFERED=1
ARG PYENV_VERSION=v2.4.0
ARG PYTHON_VERSION=3.11.9
RUN <<EOF
set -eu
apt-get update
apt-get install -y \
make \
build-essential \
libssl-dev \
zlib1g-dev \
libbz2-dev \
libreadline-dev \
libsqlite3-dev \
wget \
curl \
llvm \
libncursesw5-dev \
xz-utils \
tk-dev \
libxml2-dev \
libxmlsec1-dev \
libffi-dev \
liblzma-dev \
git
apt-get clean
rm -rf /var/lib/apt/lists/*
EOF
RUN <<EOF
set -eu
git clone https://github.com/pyenv/pyenv.git /opt/pyenv
cd /opt/pyenv
git checkout "${PYENV_VERSION}"
PREFIX=/opt/python-build /opt/pyenv/plugins/python-build/install.sh
/opt/python-build/bin/python-build -v "${PYTHON_VERSION}" /opt/python
rm -rf /opt/python-build /opt/pyenv
EOF
FROM ${BASE_RUNTIME_IMAGE} AS runtime-env
ARG DEBIAN_FRONTEND=noninteractive
ENV PYTHONUNBUFFERED=1
ENV PATH=/home/user/.local/bin:/opt/python/bin:${PATH}
RUN <<EOF
set -eu
apt-get update
apt-get install -y \
gosu
apt-get clean
rm -rf /var/lib/apt/lists/*
EOF
ARG CONTAINER_UID=1000
ARG CONTAINER_GID=1000
RUN <<EOF
set -eu
groupadd --non-unique --gid "${CONTAINER_GID}" user
useradd --non-unique --uid "${CONTAINER_UID}" --gid "${CONTAINER_GID}" --create-home user
EOF
COPY --from=python-env /opt/python /opt/python
ADD ./requirements.txt /tmp/
RUN --mount=type=cache,uid=${CONTAINER_UID},gid=${CONTAINER_GID},target=/home/user/.cache/pip <<EOF
set -eu
gosu user pip install -r /tmp/requirements.txt
EOF
ADD ./pyproject.toml ./README.md /code/my_project/
ADD ./my_project /code/my_project/my_project
RUN --mount=type=cache,uid=${CONTAINER_UID},gid=${CONTAINER_GID},target=/home/user/.cache/pip <<EOF
set -eu
gosu user pip install -e /code/my_project
EOF
RUN <<EOF
set -eu
mkdir -p /work
chown -R "${CONTAINER_UID}:${CONTAINER_GID}" /work
EOF
WORKDIR /work
# 引数を受け付けない場合(環境変数や設定ファイルで設定する場合、docker compose up -dでの実行を想定する場合)
CMD [ "gosu", "user", "python", "-m", "my_project" ]
# main.pyが引数を受け付ける場合(docker runコマンドやdocker compose runでの実行を想定する場合)
# ENTRYPOINT [ "gosu", "user", "python", "-m", "my_project" ]

GitHub Actions Workflowの作成

GitHub Actions リンターによる静的検査

# lint.yml
name: Lint
on:
push:
pull_request:
workflow_dispatch:
env:
PYTHON_VERSION: '3.11.9'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Poetry
shell: bash
run: pipx install poetry
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "${{ env.PYTHON_VERSION }}"
cache: 'poetry'
- name: Install Dependencies
shell: bash
run: poetry install
- name: Run lint
shell: bash
run: poetry run pysen run lint

GitHub Actions PyInstallerによるバイナリビルド・リリース

GitHub Actions Dockerイメージのビルド・リリース

GitHub VariablesにDOCKERHUB_USERNAMEを設定し、GitHub SecretsにDOCKERHUB_TOKENを設定する必要があります。

my_project/__init__.py__version__ = "0.0.0"を記述し、 pyproject.tomlversion = "0.0.0"を記述します。 これらのバージョンは、開発中は0.0.0となり、リリース時はリリースバージョンに置換されます。

CPUだけ使うDockerfileの場合

# build-docker.yml
name: Build Docker
on:
push:
branches:
- main
release:
types:
- created
workflow_dispatch:
env:
IMAGE_NAME: aoirint/my_project
IMAGE_TAG: ${{ github.event.release.tag_name != '' && github.event.release.tag_name || 'latest' }}
VERSION: ${{ (github.event.release.tag_name != '' && github.event.release.tag_name) || '0.0.0' }}
jobs:
docker-build-and-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Replace Version
shell: bash
run: |
sed -i "s/__version__ = \"0.0.0\"/__version__ = \"${{ env.VERSION }}\"/" my_project/__init__.py
sed -i "s/version = \"0.0.0\"/version = \"${{ env.VERSION }}\"/" pyproject.toml
- name: Build and Deploy Docker image
uses: docker/build-push-action@v5
env:
IMAGE_NAME_AND_TAG: ${{ format('{0}:{1}', env.IMAGE_NAME, env.IMAGE_TAG) }}
IMAGE_CACHE_FROM: ${{ format('type=registry,ref={0}:latest-buildcache', env.IMAGE_NAME) }}
IMAGE_CACHE_TO: ${{ env.IMAGE_TAG == 'latest' && format('type=registry,ref={0}:latest-buildcache,mode=max', env.IMAGE_NAME) || '' }}
with:
context: .
builder: ${{ steps.buildx.outputs.name }}
file: ./Dockerfile
push: true
tags: ${{ env.IMAGE_NAME_AND_TAG }}
cache-from: ${{ env.IMAGE_CACHE_FROM }}
cache-to: ${{ env.IMAGE_CACHE_TO }}

CPU版イメージとGPU版イメージをビルドする場合

# build-docker.yml
name: Build Docker
on:
push:
branches:
- main
release:
types:
- created
workflow_dispatch:
env:
IMAGE_NAME: aoirint/my_project
IMAGE_VERSION_NAME: ${{ (github.event.release.tag_name != '' && github.event.release.tag_name) || 'latest' }}
VERSION: ${{ (github.event.release.tag_name != '' && github.event.release.tag_name) || '0.0.0' }}
PYTHON_VERSION: '3.11.9'
jobs:
docker-build-and-push:
strategy:
fail-fast: false
matrix:
include:
-
base_image: 'ubuntu:22.04'
base_runtime_image: 'ubuntu:22.04'
image_variant_name: 'ubuntu'
-
base_image: 'ubuntu:22.04'
base_runtime_image: 'nvcr.io/nvidia/driver:525-signed-ubuntu22.04'
image_variant_name: 'nvidia'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Replace Version
shell: bash
run: |
sed -i "s/__version__ = \"0.0.0\"/__version__ = \"${{ env.VERSION }}\"/" my_project/__init__.py
sed -i "s/version = \"0.0.0\"/version = \"${{ env.VERSION }}\"/" pyproject.toml
- name: Build and Deploy Docker image
uses: docker/build-push-action@v5
env:
IMAGE_NAME_AND_TAG: ${{ format('{0}:{1}-{2}', env.IMAGE_NAME, matrix.image_variant_name, env.IMAGE_VERSION_NAME) }}
LATEST_IMAGE_NAME_AND_TAG: ${{ format('{0}:{1}-{2}', env.IMAGE_NAME, matrix.image_variant_name, 'latest') }}
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./Dockerfile
push: true
tags: ${{ env.IMAGE_NAME_AND_TAG }}
build-args: |
BASE_IMAGE=${{ matrix.base_image }}
BASE_RUNTIME_IMAGE=${{ matrix.base_runtime_image }}
PYTHON_VERSION=${{ env.PYTHON_VERSION }}
target: runtime-env
cache-from: |
type=registry,ref=${{ env.IMAGE_NAME_AND_TAG }}-buildcache
type=registry,ref=${{ env.LATEST_IMAGE_NAME_AND_TAG }}-buildcache
cache-to: |
type=registry,ref=${{ env.IMAGE_NAME_AND_TAG }}-buildcache,mode=max

GitLab CI Pipelineの作成

TBW。気が向いたら書きます。

GitLab CI リンターによる静的検査

GitLab CI PyInstallerによるバイナリビルド・リリース

GitLab CI Dockerイメージのビルド・リリース