PDMによるPythonのプロジェクト管理

# PDMとは

[PDM](https://pdm.fming.dev/latest/)はPythonのプロジェクト、依存関係のマネージメントツールです。私は以前まで[Poetry](https://python-poetry.org/)を利用していましたが以下の点からPDMに乗り換えました。

1. `pyproject.toml`を利用する点はPoetryと同じだが、よりPEP準拠になっている。
1. Poetryよりも依存解決が速い
1. Poetryはいつまで経ってもv1.2がリリースされない。
1. 単体でタスクランナー機能を有している。(Poetryはプラグインが必要)
1. 指定したファイルの`__version__`を読み込んでくれる。
1. プロジェクトごとにvenvを作るが、インストールされるライブラリは中央管理されていてシンボリックリンクを張ることでディスク容量を節約でき、インストールも速い。

特に私が一番気に入っているのは最後のインストールキャッシュ機能で、例えばDebian 11にてPoetryでNumPyだけをインストールした場合、`.venv`のサイズは88MBですが、PDMの場合はなんとわずか176KBです。これはPDMはライブラリの実体を`$HOME/.cache/pdm/packages`以下にインストールし、`.venv/lib/python<version>/site-packages`内にリンクを張っているからです。したがってPoetryの場合はNumPyを利用する各プロジェクトごとに88MB消費しますが、PDMの場合はいくらプロジェクトを増やしても同じバージョンであれば合計88MBで済みます。

# インストール

Debian 11ではpipxを利用するのが一番簡単です。

```sh
sudo apt install pipx
pipx install pdm
```

そして忘れずに

```sh
pdm config --global install.cache True
```

を実行して前述のインストールキャッシュ機能を有効にします。(なぜかこれはデフォルトではFalse)

# 使い方

https://pdm.fming.dev/latest/usage/cli_reference/

```sh
# add dependency
pdm add numpy

# add optional dependency
pdm add -G <group_name> scipy

# add dev dependency
pdm add -d flake8 mypy black isort pytest-cov

# install from pyproject.toml
pdm install

# without dev dependency
pdm install --prod

# with optional dependency
pdm install -G <group_name>

# run with installed dependency
pdm run <cmd>

# build wheel
pdm build
```

なお、プロジェクトのメインのソースコードは`src/`以下に入れておくのがデフォルトのしきたりになっています。

https://pdm.fming.dev/latest/pyproject/build/#select-another-package-directory-to-look-for-packages

また、`pdm install/sync`はプロジェクト自体は`setup.py develop`相当になっていて、`setup.py install`相当にするには`pdm install/sync --no-editable`にします。

https://pdm.fming.dev/latest/usage/dependency/#editable-dependencies


# pyproject.tomlの設定

`pdm add` コマンド以外で手動で編集するべき項目。詳細は 

https://pdm.fming.dev/latest/pyproject/pep621/ 

を参照。

## dynamic versioning

```toml
[project]
dynamic = ["version"]

[tool.pdm]
version = { source = "file", path = "src/app/version.py" }
```

これで`src/app/version.py`内の`__version__`を読んでくれます。

## プロジェクトスクリプト

```toml
[project.scripts]
app = "app.cli:main"
```

これで`pdm install`したときに`.venv/bin/app`が作成され、実際には`src/app/cli.py`の中の`main`という名前の関数が実行されます。

## タスクランナー

https://pdm.fming.dev/latest/usage/scripts/

```toml
black = "black ."
isort = "isort ."
flake8 = "flake8 --exit-zero ."
mypy = "mypy --show-column-numbers ."
test = "pytest tests --cov=app --cov-report=term --cov-report=xml"
format = { composite = ["black", "isort"] }
lint = { composite = ["flake8", "mypy"] }
check = { composite = ["format", "lint", "test"] }
```

上記のように書くと`pdm run check`でblackとisortとflake8とmypyとpytestをすべてやってくれます。

# Dockerfileの書き方

https://pdm.fming.dev/latest/usage/advanced/#use-pdm-in-a-multi-stage-dockerfile

をベースに少しいじっています。

```
FROM python:3.10-slim as pdm

RUN pip install -U pip setuptools wheel
RUN pip install pdm
# PDM 2.0からはデフォルトでvenvを使うのでoffにしてpep 582モードにする
RUN pdm config python.use_venv False


FROM pdm as builder
WORKDIR /project

# 一回目は依存ライブラリのみをインストール
COPY pyproject.toml pdm.lock /project/
RUN pdm sync -G server --prod --no-self

# 二回目はプロジェクト本体をインストール
# 分けることによって依存ライブラリのインストールをDockerのビルドキャッシュに載せることができる
COPY src/ /project/src
RUN pdm sync -G server --prod --no-editable


FROM python:3.10-slim

ENV PYTHONPATH=/project/lib

COPY --from=builder /project/__pypackages__/3.10/lib /project/lib
COPY --from=builder /project/__pypackages__/3.10/bin /project/bin

WORKDIR /project
COPY src/ /project/src


ENV N_WORKERS 4

CMD bin/gunicorn \
    --access-logfile - \
    --bind=0.0.0.0:8000 \
    --workers=${N_WORKERS} \
    -k uvicorn.workers.UvicornWorker \
    src.app.server:app                         
```

# まとめ

https://github.com/lucidfrontier45/python-pdm-template

にテンプレートとして使えるプロジェクトを作成しました。上記の内容以外にlinterやformatterの設定を`setup.cfg`にまとめてあり、またVSCodeの設定も`vscode_templates`に入れてあるのでこれを`.vscode`にコピーし、`extensinos.json`に挙げられている拡張をインストールするとすぐに快適な開発環境になります。