バージョン情報

定義・ディレクトリ構成

説明のため、プロジェクトディレクトリ名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 "[email protected]"

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だけ使う場合

Details
# 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 リポジトリ
CPU ubuntu:22.04 Docker Hub: ubuntu
NVIDIA Driver v525 nvcr.io/nvidia/driver:525-signed-ubuntu22.04 NGC: NVIDIA GPU Driver
CUDA 11.8 + cuDNN 8 nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04 Docker Hub: nvidia/cuda
CUDA 11.8 + cuDNN 8(開発用ライブラリ入) nvidia/cuda:11.8.0-cudnn8-devel-ubuntu22.04 Docker 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の使用を明示する必要があります。

Details
# 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 リンターによる静的検査

Details
# 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の場合

Details
# 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版イメージをビルドする場合

Details
# 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イメージのビルド・リリース