
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
, s3transfer
はboto3
が依存しているので削除、といった具合です。
そうすると残るものは以下になります。
boto3 1.34.78
requests 2.31.0
Django 4.2.11
こうして得たライブラリを Poetry で管理するようにして、必要な環境は再現できるようになりました。
他の古くからあるリポジトリも、できれば標準的な pyproject.toml
を使う形で、このあたりの依存関係を管理するように整理していきたいなと思います。
こうすればもっといいのに
と思ったあなた、スイッチサイエンスで一緒に実践してみませんか?
システムエンジニア募集中です!
興味のある方はぜひカジュアル面談へ!お待ちしています。