きたない requirements.txt から Pipenv への移行
2018/07/11

ソフトウェアエンジニアの花岡です。今回は、きたない requirements.txt から Pipenv に移行し pipenv graph を使って依存ライブラリを管理しやすくする方法について書きました。

きたない requirements.txt とは、依存ライブラリとその依存ライブラリの指定が混在している requirements.txt ファイルのことです。たとえば

$ pip install flask
$ pip freeze > requirements.txt

のように生成した requirements.txt を使っているとき、本記事ではその requirements.txt を、きたない requirements.txt と呼ぶことにします。

きたなくない requirements.txt とは pip の constraints の正しい用途のように、直接依存しているライブラリのみ指定された requirements.txt のことで、pip freeze の結果は別に管理します。

Pipenv の基本的な使い方については、本記事では触れないため公式サイトを参照してください。

Pipenv とは

Pipenv は Python のパッケージ管理ツールです。公式サイト

Pipenv: Python Dev Workflow for Humans

Pipenv is a tool that aims to bring the best of all packaging worlds (bundler, composer, npm, cargo, yarn, etc.) to the Python world.

とあるように、ひとのための Python の開発ワークフローを掲げて、いろいろなパッケージングツールの良いところを集結しようとしています。

現在は https://github.com/pypa/pipenv がリポジトリです。pypa は Python Packaging Authority の略で setuptools、pip、virtualenv なども管理しています。とても安心感がありますね。Pip Integration (eventual) によると、最終的には pip コマンドで -p/--pipfile オプションが使えるようになるらしいです。

Pipenv の良いところのひとつは、Pipfile.lock として pip freeze の結果を管理してくれるところと、Pipenv(pip --pipfile)が標準になるはずというところだと思います。Python のパッケージング管理も Bundler や npm に追いついてきました。

きたない requirements.txt から Pipenv への移行

説明のために、きたない requirements.txt を生成します。

$ pip install flask  # 本番用のライブラリ
$ pip install pytest pytest-cov  # テスト用のライブラリ
$ pip freeze | tee requirements.txt
atomicwrites==1.1.5
attrs==18.1.0
click==6.7
coverage==4.5.1
Flask==1.0.2
funcsigs==1.0.2
itsdangerous==0.24
Jinja2==2.10
MarkupSafe==1.0
more-itertools==4.2.0
pluggy==0.6.0
py==1.5.4
pytest==3.6.3
pytest-cov==2.5.1
six==1.11.0
Werkzeug==0.14.1

ここでは、ついでにテスト用のライブラリも指定しました。きたないですね。

まずこの requirements.txt を pipenv install -r requirements.txt でインストールします。

[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
atomicwrites = "==1.1.5"
attrs = "==18.1.0"
click = "==6.7"
coverage = "==4.5.1"
funcsigs = "==1.0.2"
itsdangerous = "==0.24"
more-itertools = "==4.2.0"
pluggy = "==0.6.0"
py = "==1.5.4"
pytest = "==3.6.3"
pytest-cov = "==2.5.1"
six = "==1.11.0"
Flask = "==1.0.2"
"Jinja2" = "==2.10"
MarkupSafe = "==1.0"
Werkzeug = "==0.14.1"

[dev-packages]

[requires]
python_version = "3.7"

という内容の Pipfile(と Pipfile.lock)が生成されます。ここで pipenv graph を実行すると

$ pipenv graph
Flask==1.0.2
  - click [required: >=5.1, installed: 6.7]
  - itsdangerous [required: >=0.24, installed: 0.24]
  - Jinja2 [required: >=2.10, installed: 2.10]
    - MarkupSafe [required: >=0.23, installed: 1.0]
  - Werkzeug [required: >=0.14, installed: 0.14.1]
pytest-cov==2.5.1
  - coverage [required: >=3.7.1, installed: 4.5.1]
  - pytest [required: >=2.6.0, installed: 3.6.3]
    - atomicwrites [required: >=1.0, installed: 1.1.5]
    - attrs [required: >=17.4.0, installed: 18.1.0]
    - funcsigs [required: Any, installed: 1.0.2]
    - more-itertools [required: >=4.0.0, installed: 4.2.0]
      - six [required: >=1.0.0,<2.0.0, installed: 1.11.0]
    - pluggy [required: >=0.5,<0.7, installed: 0.6.0]
    - py [required: >=1.5.0, installed: 1.5.4]
    - setuptools [required: Any, installed: 40.0.0]
    - six [required: >=1.10.0, installed: 1.11.0]

のように依存関係のグラフが表示されます。この結果から Pipfile

[packages]
Flask = "==1.0.2"

[dev-packages]
pytest-cov = "==2.5.1"

のように書き換えて pipenv install -d するのが最短です。これでテストもパスするのであれば何も問題ありません。

しかし、たとえばこの場合 pytest は pytest-cov によって >=2.6.0 に依存しているので、もし pytest 4.0.0 がリリースされ破壊的変更があれば、pipenv install -d してテストがパスしないかもしれません。そのような場合は

[dev-packages]
pytest = "==3.6.3"
pytest-cov = "==2.5.1"

のように pytest も指定しておく必要があります。このようにテストがパスしなかった場合には以下のように、pipenv graph を利用して最初の Pipfile から徐々に整理し最適化していくことができます。

まずテスト用のライブラリを [packages] から [dev-packages] に移動します。今回は pipenv graph の結果がシンプルなので手動で移動することができます。

[packages]
click = "==6.7"
itsdangerous = "==0.24"
Flask = "==1.0.2"
"Jinja2" = "==2.10"
MarkupSafe = "==1.0"
Werkzeug = "==0.14.1"

[dev-packages]
atomicwrites = "==1.1.5"
attrs = "==18.1.0"
coverage = "==4.5.1"
funcsigs = "==1.0.2"
more-itertools = "==4.2.0"
pluggy = "==0.6.0"
py = "==1.5.4"
pytest = "==3.6.3"
pytest-cov = "==2.5.1"
six = "==1.11.0"

パッケージ数が多くなってくると手動で移動するのは大変かもしれません。そういう場合は --json または --json-tree というオプションがあるので、自動で振り分けてくれるようなスクリプトを書いて公開してください。

それまでの間は、逆向きの依存関係を出力する --reverse というオプションを使うことにします。

$ pipenv graph --reverse
atomicwrites==1.1.5
  - pytest==3.6.3 [requires: atomicwrites>=1.0]
    - pytest-cov==2.5.1 [requires: pytest>=2.6.0]
attrs==18.1.0
  - pytest==3.6.3 [requires: attrs>=17.4.0]
    - pytest-cov==2.5.1 [requires: pytest>=2.6.0]
click==6.7
  - Flask==1.0.2 [requires: click>=5.1]
coverage==4.5.1
  - pytest-cov==2.5.1 [requires: coverage>=3.7.1]
funcsigs==1.0.2
  - pytest==3.6.3 [requires: funcsigs]
    - pytest-cov==2.5.1 [requires: pytest>=2.6.0]
itsdangerous==0.24
  - Flask==1.0.2 [requires: itsdangerous>=0.24]
MarkupSafe==1.0
  - Jinja2==2.10 [requires: MarkupSafe>=0.23]
    - Flask==1.0.2 [requires: Jinja2>=2.10]
pip==10.0.1
pluggy==0.6.0
  - pytest==3.6.3 [requires: pluggy>=0.5,<0.7]
    - pytest-cov==2.5.1 [requires: pytest>=2.6.0]
py==1.5.4
  - pytest==3.6.3 [requires: py>=1.5.0]
    - pytest-cov==2.5.1 [requires: pytest>=2.6.0]
setuptools==40.0.0
  - pytest==3.6.3 [requires: setuptools]
    - pytest-cov==2.5.1 [requires: pytest>=2.6.0]
six==1.11.0
  - more-itertools==4.2.0 [requires: six>=1.0.0,<2.0.0]
    - pytest==3.6.3 [requires: more-itertools>=4.0.0]
      - pytest-cov==2.5.1 [requires: pytest>=2.6.0]
  - pytest==3.6.3 [requires: six>=1.10.0]
    - pytest-cov==2.5.1 [requires: pytest>=2.6.0]
Werkzeug==0.14.1
  - Flask==1.0.2 [requires: Werkzeug>=0.14]

この結果を利用して、たとえば atomicwrites は pytest からのみ依存されていることがわかるので [dev-packages] に移動、という作業を繰り返します。

後は間接依存ライブラリをそれぞれ Pipfile では指定せずにテストしていきます。もしテストがパスしなければ、そのバージョンを Pipfile で指定したままにしておきテストがパスしない理由を調査します。

さいごに

不要なエントリを削除した後は pipenv update でライブラリの更新ができるように、バージョンの指定を調整するといいと思います。たとえば

[packages]
Flask = "~=1.0"

[dev-packages]
pytest-cov = "~=2.5"

のようにすると pipenv update したときに、条件にあった新しいリリースがあれば Pipfile.lock が更新されてインストールされます。バージョンのフォーマットについては PEP 440RFC 3986 で定義されていると PEP 508 で定義されています。

弊社では Python できれいなコードを書きたいエンジニアを募集しています。興味のある方は是非こちらからご応募ください。