Veltioblog

Python でロックファイルを生成してくれるツールをいくつかピックアップし生成されるロックファイルがどうなっているか、どのように運用すれば良いかについてまとめてみました。

検証環境

Table of contents

アプリケーションとライブラリ

基本的にロックファイルを望んでいるのはアプリケーション開発者であると思います。

ライブラリ開発者は適切な依存関係のセットを pyproject.toml に記述しておけば基本は良いはずなので、基本的にはアプリケーション開発者の視点でまとめていきます。

今回とりあげるツール

※ 気が向いたら追加するかもです

pip

みんな大好き pip です。

ロックファイル(的なもの)を生成するには freeze コマンドを使います。

requirements.txt はこんな感じで特にバージョンは指定しない

opencv-python
# py312 venv 環境内での操作
pip install -r requirements.txt
pip freeze > requirements-py312.lock
cat requirements-py312.lock

numpy==1.26.4
opencv-python==4.9.0.80

Python 3.8 環境でも同様の操作をやってみる

# 省略
pip freeze > requirements-py38.lock
cat requirements-py38.lock

numpy==1.24.4
opencv-python==4.9.0.80

見ての通り、numpy のバージョンが異なります。 このように、現在の開発環境次第で lock ファイルにブレが生じてしまうことがあるというのがわかりました(OSによって違う依存が含まれる可能性もあるかもしれません)。

ただ、この問題は Docker コンテナを使えば割りと簡単に解決できると思います。

例えば、WebAPI を開発していて本番環境は Docker コンテナで動いていたとします。 このとき、開発用コンテナも本番と同じ環境にしておいて、そのコンテナ内で requirements.lock を生成すれば開発マシンに関係なく同じ lock ファイルが生成されるはずです。

自分で書いていてこの運用楽そうだなと思いました(実際には他の課題もあって rye を使いたいとか言い出しそうですが...)。

Rye with uv

❯ rye --version
rye 0.24.0
commit: 0.24.0 (8d56aa18a 2024-02-15)
platform: linux (x86_64)
self-python: [email protected]
symlink support: true

uv の生成する lock ファイルについて、README の制限事項に書いてあります。

pip-compile のように、uv はプラットフォーム固有の requirements.txt ファイルを生成します(例えば、poetry や pdm がプラットフォームに依存しない poetry.lock や pdm.lock ファイルを生成するのとは異なります)。そのため、uvのrequirements.txtファイルはプラットフォームやPythonのバージョン間で移植性がないかもしれません。

DeepL 翻訳: https://github.com/astral-sh/uv?tab=readme-ov-file#limitations

project.requires-python = ">=3.8"

rye init rye-lock
cd rye-lock
rye add opencv-python

# python 3.12.1 環境での操作
rye pin 3.12.1
rye sync
cat requirements.lock

-e file:.
numpy==1.26.4
    # via opencv-python
opencv-python==4.9.0.80
    # via rye-lock

# python 3.8 環境での操作
rye pin 3.8
# 省略

-e file:.
numpy==1.24.4
    # via opencv-python
opencv-python==4.9.0.80
    # via rye-lock

結局は pip と同様の課題があるようです。

例えば CI システムでこのプロジェクトをビルドしようとしたとき、その CI プラットフォームに合わせて lock ファイルを生成する必要があるかもしれません。

その時は自分なら以下のように解決します。

  1. rye lock で生成された lock ファイルは使わない
  2. CI システム側で uv を使って pyproject.toml から lock ファイルを別途生成する(そのプラットフォームにあったバージョンが選ばれるはず)
  3. その lock ファイルを使って pip install する

これの個人的な利点としては、uv pip compile で生成される lock ファイルはパッケージの editable install を lock ファイルに追加しないので、CI 時に不要な editable install が走らないということです。

多分以下のような step を CI に実装すれば良いと思います。

pip install uv
uv pip compile pyproject.toml -o requirements-ci.lock
pip install -r requirements-ci.lock

... 
# test や lint, build など

PDM

❯ pdm --version
PDM, version 2.12.3

取り上げた中で唯一、クロスプラットフォームな lock ファイルを生成する。

そもそも pip 互換な requirements.txt が生成されるツールはロックファイルが基本プラットフォーム依存になるので、そもそものアプローチが違う(先駆けは Poetry なのかな?)。

# pdm use で 3.12 を選んだ 
❯ pdm add opencv-python
Adding packages to default dependencies: opencv-python
/home/zztkm/.rye/tools/pdm/lib/python3.12/site-packages/pdm/resolver/providers.py:202: PackageWarning: Skipping [email protected] because it requires Python>=3.9 but the
project claims to work with Python>=3.8. Instead, another version of numpy that supports Python>=3.8 will be used.
If you want to install [email protected], narrow down the `requires-python` range to include this version. For example, ">=3.9" should work.
  return self.repository.find_candidates(
/home/zztkm/.rye/tools/pdm/lib/python3.12/site-packages/pdm/resolver/providers.py:202: PackageWarning: Skipping [email protected] because it requires Python>=3.9 but the
project claims to work with Python>=3.8. Instead, another version of numpy that supports Python>=3.8 will be used.
If you want to install [email protected], narrow down the `requires-python` range to include this version. For example, ">=3.9" should work.
  return self.repository.find_candidates(
/home/zztkm/.rye/tools/pdm/lib/python3.12/site-packages/pdm/resolver/providers.py:202: PackageWarning: Skipping [email protected] because it requires Python>=3.9 but the
project claims to work with Python>=3.8. Instead, another version of numpy that supports Python>=3.8 will be used.
If you want to install [email protected], narrow down the `requires-python` range to include this version. For example, ">=3.9" should work.
  return self.repository.find_candidates(
/home/zztkm/.rye/tools/pdm/lib/python3.12/site-packages/pdm/resolver/providers.py:202: PackageWarning: Skipping [email protected] because it requires Python<3.13,>=3.9 but
the project claims to work with Python>=3.8. Instead, another version of numpy that supports Python>=3.8 will be used.
If you want to install [email protected], narrow down the `requires-python` range to include this version. For example, ">=3.9,<3.13" should work.
  return self.repository.find_candidates(
/home/zztkm/.rye/tools/pdm/lib/python3.12/site-packages/pdm/resolver/providers.py:202: PackageWarning: Skipping [email protected] because it requires Python<3.13,>=3.9 but
the project claims to work with Python>=3.8. Instead, another version of numpy that supports Python>=3.8 will be used.
If you want to install [email protected], narrow down the `requires-python` range to include this version. For example, ">=3.9,<3.13" should work.
  return self.repository.find_candidates(
/home/zztkm/.rye/tools/pdm/lib/python3.12/site-packages/pdm/resolver/providers.py:202: PackageWarning: Skipping [email protected] because it requires Python>=3.9 but the
project claims to work with Python>=3.8. Instead, another version of numpy that supports Python>=3.8 will be used.
If you want to install [email protected], narrow down the `requires-python` range to include this version. For example, ">=3.9" should work.
  return self.repository.find_candidates(
/home/zztkm/.rye/tools/pdm/lib/python3.12/site-packages/pdm/resolver/providers.py:202: PackageWarning: Skipping [email protected] because it requires Python>=3.9 but the
project claims to work with Python>=3.8. Instead, another version of numpy that supports Python>=3.8 will be used.
If you want to install [email protected], narrow down the `requires-python` range to include this version. For example, ">=3.9" should work.
  return self.repository.find_candidates(
/home/zztkm/.rye/tools/pdm/lib/python3.12/site-packages/pdm/resolver/providers.py:202: PackageWarning: Skipping [email protected] because it requires Python>=3.9 but the
project claims to work with Python>=3.8. Instead, another version of numpy that supports Python>=3.8 will be used.
If you want to install [email protected], narrow down the `requires-python` range to include this version. For example, ">=3.9" should work.
  return self.repository.find_candidates(
INFO: Use `-q/--quiet` to suppress these warnings, or ignore them per-package with `ignore_package_warnings` config in [tool.pdm] table.
🔒 Lock successful
Changes are written to pyproject.toml.
Synchronizing working set with resolved packages: 2 to add, 0 to update, 0 to remove

このブログの趣旨からはずれるが面白かったので残しておきます。

上から読んでいくと、numpy 1.25.0 以降は Python 3.9 以上が必要なので、project.requires-python = ">=3.8" と設定していることが原因で、numpy 1.24.4 が選ばれたようです。

がこのあと、Python3.12 でインストールしようとして以下のエラーが出ます。これは Python 3.12 で distutils削除されたためです。Python の依存管理地味に難しいですね。

ERRORS:
add numpy failed:
Traceback (most recent call last):
  File "/home/zztkm/.rye/py/[email protected]/install/lib/python3.12/concurrent/futures/thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/zztkm/.rye/tools/pdm/lib/python3.12/site-packages/pdm/installers/synchronizers.py", line 286, in install_candidate
    self.manager.install(can)
  File "/home/zztkm/.rye/tools/pdm/lib/python3.12/site-packages/pdm/installers/manager.py", line 34, in install
    dist_info = installer(str(prepared.build()), self.environment, prepared.direct_url())
                              ^^^^^^^^^^^^^^^^
  File "/home/zztkm/.rye/tools/pdm/lib/python3.12/site-packages/pdm/models/candidates.py", line 418, in build
    self.wheel = Path(builder.build(build_dir, metadata_directory=self._metadata_dir))
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/zztkm/.rye/tools/pdm/lib/python3.12/site-packages/pdm/builders/wheel.py", line 26, in build
    requires = self._hook.get_requires_for_build_wheel(config_settings)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/zztkm/.rye/tools/pdm/lib/python3.12/site-packages/pyproject_hooks/_impl.py", line 166, in get_requires_for_build_wheel
    return self._call_hook('get_requires_for_build_wheel', {
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/zztkm/.rye/tools/pdm/lib/python3.12/site-packages/pyproject_hooks/_impl.py", line 321, in _call_hook
    raise BackendUnavailable(data.get('traceback', ''))
pyproject_hooks._impl.BackendUnavailable: Traceback (most recent call last):
  File "/home/zztkm/.rye/tools/pdm/lib/python3.12/site-packages/pyproject_hooks/_in_process/_in_process.py", line 77, in _build_backend
    obj = import_module(mod_path)
          ^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/zztkm/.rye/py/[email protected]/install/lib/python3.12/importlib/__init__.py", line 90, in import_module
    return _bootstrap._gcd_import(name, package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1310, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 994, in exec_module
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "/tmp/pdm-build-env-3x4_vnfi-shared/lib/python3.12/site-packages/setuptools/__init__.py", line 10, in <module>
    import distutils.core
ModuleNotFoundError: No module named 'distutils'

面倒くさとなったので、PDM は気が向いたらちゃんとまとめます。

まとめ

気が向いたらちょこちょこふかぼります。