コンテンツにスキップ
pip freezeして作られたであろう requirements.txt をどうにか整理する話

pip freezeして作られたであろう requirements.txt をどうにか整理する話

ECシステム開発チームのいまづです。
みなさん pip install してますか?

いやまあ pip 自体は使うにしても、依存管理は最近なら rye だろうとか uv 使おうぜとか聞こえてきますけれども、自社サービスを開発し続けていると、古くから存在し続けるリポジトリがあって、それらには requirements.txt しかないという状況がありまして。
今回はそれを整理したよ、というお話です。
残念ながら便利ツールで一気に解決できたというお話ではありません。

バージョン指定込みの requirements.txt

開発において人が必要と判断したものを単に追記していっただけの requirements.txt というのは扱いやすいですよね(バージョンを固定できていないという点を無視すれば)。

djnago
python-dateutil
requests
boto3

みたいなやつです。
手作業で編集したものではなく、pip freeze の内容を出力したものだとバージョン番号指定が入っているので、例えばこんな状態になっています。

asgiref==3.8.1
boto3==1.34.78
botocore==1.34.78
certifi==2024.2.2
charset-normalizer==3.3.2
Django==4.2.11
idna==3.6
jmespath==1.0.1
python-dateutil==2.9.0.post0
requests==2.31.0
s3transfer==0.10.1
six==1.16.0
sqlparse==0.4.4
urllib3==2.2.1

プロジェクトを維持していく中では依存ライブラリのバージョンを上げていきましょう、があるわけですが、この状態のファイルをメンテナンスするのはかなり大変な作業になってしまいます。
(これは例として作ったものですが、実際のファイルはそれはもうとてもお見せできない…)

直接依存しているライブラリはどれですか

そこで、依存管理の仕組みを持ち込みたいという話になります。
僕たちはPoetryを利用することが多かったので、今回整理したのもPoetryに移行したのですが、何を使うにせよ、「そもそもプロジェクトが直接依存しているライブラリはどれなのか」が欲しくなります。
間接的に依存しているライブラリのバージョンを指定したくないですしね。もしかするとバージョンアップに伴って不要になったものがあるかもしれませんし。

Python環境にインストールされているモジュールを調べるツールはないかなと探しますが、そのために何かをインストールするということは避けたいです。
見てみると、setuptoolsに含まれるpkg_resourcesが使えそうなので試してみました。
https://setuptools.pypa.io/en/latest/pkg_resources.html

#!/usr/bin/env python
import pkg_resources

def main():
    for pkg in pkg_resources.working_set:
        print(pkg.project_name, pkg.version)
        for req in lib.requires():
            print('\t', req.project_name)

if __name__ == '__main__':
    main()
対象の仮想環境で実行してみます。
❯ python check.py
print_depends.py:2: DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html
  import pkg_resources
Django 4.2.11
	asgiref
	sqlparse
asgiref 3.8.1
boto3 1.34.78
	botocore
	jmespath
	s3transfer
botocore 1.34.78
	jmespath
	python-dateutil
	urllib3
certifi 2024.2.2
charset-normalizer 3.3.2
idna 3.6
jmespath 1.0.1
pip 23.2
python-dateutil 2.9.0.post0
	six
requests 2.31.0
	charset-normalizer
	idna
	urllib3
	certifi
s3transfer 0.10.1
	botocore
setuptools 68.1.2
six 1.16.0
sqlparse 0.4.4
urllib3 2.2.1

お、わかりやすそうです。
でも DeprecationWarning が出てしまいました。
ドキュメントにも deprecated だって書いてありましたね(ドキュメント読みましょう、、っていつか書いた気がします => いつか)。
古い環境で実行する場合は pkg_resources で良さそうですが、importlib.metadata を使って書き直してみます。

from importlib.metadata import distributions

def main(args):
    for dst in distributions():
        print(dst.metadata['Name'], dst.version)
        if dst.requires:
            for req in dst.requires:
                print('\t', req)

実行してみます。

botocore 1.34.78
	jmespath (<2.0.0,>=0.7.1)
	python-dateutil (<3.0.0,>=2.1)
	urllib3 (<1.27,>=1.25.4) ; python_version < "3.10"
	urllib3 (!=2.2.0,<3,>=1.25.4) ; python_version >= "3.10"
	awscrt (==0.19.19) ; extra == 'crt'
certifi 2024.2.2
charset-normalizer 3.3.2
boto3 1.34.78
	botocore (<1.35.0,>=1.34.78)
	jmespath (<2.0.0,>=0.7.1)
	s3transfer (<0.11.0,>=0.10.0)
	botocore[crt] (<2.0a0,>=1.21.0) ; extra == 'crt'
idna 3.6
urllib3 2.2.1
	brotli>=1.0.9; (platform_python_implementation == 'CPython') and extra == 'brotli'
	brotlicffi>=0.8.0; (platform_python_implementation != 'CPython') and extra == 'brotli'
	h2<5,>=4; extra == 'h2'
	pysocks!=1.5.7,<2.0,>=1.5.6; extra == 'socks'
	zstandard>=0.18.0; extra == 'zstd'
asgiref 3.8.1
	typing-extensions >=4 ; python_version < "3.11"
	pytest ; extra == 'tests'
	pytest-asyncio ; extra == 'tests'
	mypy >=0.800 ; extra == 'tests'
...

上記は途中までですが、めっちゃ細かく出てきます。
requiresは文字列のリストになっているのですが、今回の目的からするとextra が含まれているものは除外しても良さそうです。

from importlib.metadata import distributions

def main(args):
    for dst in distributions():
        print(dst.metadata['Name'], dst.version)
        if dst.requires:
            for req in dst.requires:
                if 'extra' in req:
                    continue
                print('\t', req)

出力内容は以下のようになりました。 pkg_resources 版とほぼ同じですね。

botocore 1.34.78
	jmespath (<2.0.0,>=0.7.1)
	python-dateutil (<3.0.0,>=2.1)
	urllib3 (<1.27,>=1.25.4) ; python_version < "3.10"
	urllib3 (!=2.2.0,<3,>=1.25.4) ; python_version >= "3.10"
certifi 2024.2.2
charset-normalizer 3.3.2
boto3 1.34.78
	botocore (<1.35.0,>=1.34.78)
	jmespath (<2.0.0,>=0.7.1)
	s3transfer (<0.11.0,>=0.10.0)
idna 3.6
urllib3 2.2.1
asgiref 3.8.1
	typing-extensions >=4 ; python_version < "3.11"
six 1.16.0
python-dateutil 2.9.0.post0
	six >=1.5
sqlparse 0.4.4
requests 2.31.0
	charset-normalizer (<4,>=2)
	idna (<4,>=2.5)
	urllib3 (<3,>=1.21.1)
	certifi (>=2017.4.17)
jmespath 1.0.1
pip 23.2
Django 4.2.11
	asgiref (<4,>=3.6.0)
	sqlparse (>=0.3.1)
	backports.zoneinfo ; python_version < "3.9"
	tzdata ; sys_platform == "win32"
setuptools 68.1.2
s3transfer 0.10.1
	botocore (<2.0a.0,>=1.33.2)

依存関係がわかったところで、できれば依存関係をたどってルートになるライブラリのみを割り出したいところなのですが、たとえば、django-storages のようなものを使っていると、ルート一覧から Django が消えてしまうのも、イマイチだなあと思ったので、このまま人力で整理します。

基本的には依存されている方を消していきます。
botocore, jmespath, s3transferboto3が依存しているので削除、といった具合です。

そうすると残るものは以下になります。

boto3 1.34.78
requests 2.31.0
Django 4.2.11

こうして得たライブラリを Poetry で管理するようにして、必要な環境は再現できるようになりました。

他の古くからあるリポジトリも、できれば標準的な pyproject.toml を使う形で、このあたりの依存関係を管理するように整理していきたいなと思います。

こうすればもっといいのに

と思ったあなた、スイッチサイエンスで一緒に実践してみませんか?

 

 


 

システムエンジニア募集中です!

興味のある方はぜひカジュアル面談へ!お待ちしています。

前の記事 Breathe Easy with the SCD41 and SEN55 Air Quality Sensor