Python でも Vendoring すれば環境汚染しないパッケージが書けて嬉しい

*これまで依存パッケージ管理で遅れをとっていた Python ですが、[Pipenv というモダンなツール](https://qiita.com/sl2/items/1e503952b9506a0539ea)が定着の兆しをみせており、Python エンジニアの一人として非常に喜ばしく感じています。
一方で、Go を使うことも多い自分にとっては、依存パッケージ管理に加えて Vendoring もやりたいというのが正直なところなんですよね。リッチな機能を提供する Pipenv ですが Vendoring についてのサポートはなく、[開発コミュニティ上でもあまり積極的に受け入れられていない](https://github.com/pypa/pipenv/issues/1143#issuecomment-352483807)ようです。
私のような考え方はマイノリティなのかもしれませんが、後述するように Python でも Vendoring が出来ればいくつかのメリットがあります。という訳で実現するためにはどうすればよいかというところを調べたので、これを共有したいとおもいます！* 

## そもそも Vendoring ってなに？

一般的な依存管理というのは、依存するパッケージとそのバージョンを明記したリストを管理して、メンバーがおのおのの開発環境に依存パッケージをインストールするというアプローチをとりますよね。それに対して Vendoring は、**依存パッケージをソースごとプロジェクトに取り込んでしまうアプローチ**のことです。例えば Go ではこのアプローチは市民権を得ていて、dep, glide といったメジャーな依存管理ツールでサポートされています。

最初にこのアプローチを聞いたとき、正直なところ私は若干の抵抗がありました。「リポジトリからすぐに取ってこれるものを、プロジェクト内で抱えるのは冗長ではないか？」というところです。しかし、よくよく考えてみるとこのアプローチを取るのが一番安全で、最近ではむしろ合理的なんじゃないかとすら感じるようになりました。

## Vendoring ってなにが嬉しいの？

パッケージの開発者目線から見てもユーザ目線から見ても、依存性管理が楽になります。

まず、開発者目線から考えると、**そのパッケージが実際に利用される環境での再現性が高まる**ことがメリットです。依存パッケージが突然 PyPI から消えてしまったり、同じバージョンで別のパッケージに改変されるということもあり得なくはないです。意識の高い現場では、こういったケースに備えてキャッシュ用の社内 PyPI を立てたり、パッケージを落としてきてファイルサーバ等で別途管理するかと思いますが、Vendoring してしまえばそういった備えが要らなくなります。

また、ユーザ目線から考えてみます。ある Python アプリを使いたいと思って該当パッケージを環境にインストールするとき、多くのエンジニアは環境が汚れることを懸念して仮想環境の作成を検討すると思います。環境が汚れるというのは、将来的に別のパッケージをインストールしたときに、依存パッケージがインストール済みのバージョンと競合してしまって、そのパッケージ自体のインストールが拒否されるという問題に起因します。しかし、**全ての依存パッケージを Vendoring すると、パッケージレベルで他パッケージへの依存がない状態になります**。その結果、そのパッケージのインストールが将来のインストールに影響することもありませんし、そのパッケージ自体のインストールが拒否されるという心配もありません。つまり、パッケージが Vendoring されていることによって、**実質的にインストール先の環境は汚れなくなる**と言えます。環境が汚れないので、インタプリタのバージョンさえマッチしていれば、**もはや仮想環境を作成する必要もありません**。

その他にも、依存パッケージが作業ディレクトリ配下におさまることで、依存パッケージのソースへのアクセスがしやすかったり、IDE のインデクシング設定もいくぶん楽になります。

## Python で Vendoring なんて出来るの？

実は **既存のオープンソースプロジェクトでも、Vendoring を採用しているところがいくつかあります。** 特に謳われているわけではないものの、pip プロジェクトはそういったプロジェクトの一つです。

pip プロジェクトのリポジトリをみてみると、[src/pip/\_vendor](https://github.com/pypa/pip/tree/10.0.1/src/pip/_vendor) というディレクトリがあるのがわかります。これが依存パッケージをソースごとまとめて管理しているディレクトリで、中をみるとパッケージ名のついたディレクトリが並んでいることがわかるとおもいます。そして、注目すべきは [vendor.txt](https://github.com/pypa/pip/blob/10.0.1/src/pip/_vendor/vendor.txt) と [Makefile](https://github.com/pypa/pip/blob/10.0.1/src/pip/_vendor/Makefile) です。

```Makefile:src/pip/_vendor/Makefile
...（省略）

vendor:
	@# Install vendored libraries
	pip install -t . -r vendor.txt

	@# Cleanup .egg-info directories
	rm -rf *.egg-info
	rm -rf *.dist-info
```

pip プロジェクトでは **プロジェクトルートの requirements.txt とは別に Vendoring 用の依存パッケージリスト（vendor.txt）を管理して、そのリストをもとに pip install でソースを専用のディレクトリ配下（src/pip/\_vendor）に落としています。**
そして、これと同じことをすれば自分のプロジェクトでも Vendoring が出来るわけですね！

※ pip プロジェクトでは、パッケージ自体の依存性を少なくしたいという理由の他に、[依存パッケージのソースにパッチあててオリジナルに改変を加えるようなことをやっていて](https://github.com/pypa/pip/tree/b4aa62a490bd06c55531bddee9faf7cd09b21481/tasks/vendoring/patches)、そのためにソースをプロジェクトごと取り込んでいる背景もあります。ただ理由はどうであれ、Vendoring をやっていることには変わりがないので、採用例として挙げています。

## Vendoring はこうやれば導入できるよ

pip プロジェクトという事例から、どうやれば Vendoring 出来るかがわかりました。最後にこれを一般化してまとめようと思います。ついでに、パッケージ管理にはモダンな Pipenv を採用することにします。ちなみに、私が最近書き始めた [escher](https://github.com/msh5/escher/tree/master/escher/vendor) という Python アプリのリポジトリを、Vendoring 導入のサンプルとして利用します。

まず、vendor ディレクトリを作成します。作成するパスはディレクトリ構成にもよるので、各プロジェクトでよさそうなところを決めてください。

```shell
mkdir escher/vendor
```

次に、vendor ディレクトリ以下に Pipfile を作成します。ここに Vendoring 対象のパッケージを書きます。
プロジェクトで既に作成済みの Pipfile がある場合は、それをコピペすればよいでしょう。

```toml:escher/vendor/Pipfile
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
click = ">=6.0,<7"
elasticsearch = ">=6.0.0,<7"
tabulate = "*"

[requires]
python_version = "3.6"
```

```pipenv lock``` を実行して requirements.txt を生成します。
標準出力にはかれるのでリダイレクトしてファイルに受けましょう。

```shell
cd escher/vendor
pipenv lock -r > requirements.txt
```

そして、vendor 以下にパッケージをダウンロードするために ```pip install``` します。```pip download``` では whl ファイルが落ちてくるのでダメです。このコマンドは今後のパッケージのアップデートにも必要になってくるので、シェルスクリプト化しておくといいですよ。（せっかく Pipenv を採用しているので、Pipenv のカスタムスクリプトとして用意してもいいですね！）

```shell
pipenv run pip install -t . -r requirements.txt -U
```

最後に、スクリプト内から普通にインポート出来るように vendor ディレクトリを sys.path に追加します。vendor ディレクトリのパスを取得するのに \_\_file\_\_ シンボルを使うと便利ですが、\_\_file\_\_ シンボルは厳密にはあったりなかったりするものなので、プロジェクトによっては注意してください。

```python:escher/__init__.py
import os
import sys

from escher.__version__ import __version__

PROJECT_ROOT = os.path.dirname(os.path.realpath(__file__))
PROJECT_VENDOR = os.sep.join([PROJECT_ROOT, 'vendor'])
sys.path.insert(0, PROJECT_VENDOR)
```

## Vendoring 導入に関して補足

プロジェクトルートの Pipfile は残しておいてもおかなくても良いですが、プロジェクトで使用するインタプリタのバージョンを指定するには必要になります。また、pip プロジェクトのように Vendoring するパッケージと Vendoring しないパッケージを区別したい場合もあるかもしれません。そういった場合には Vendoring しないパッケージをここで指定することで、Vendoring するパッケージと区別できます。

また、基本的に開発用のパッケージは Vendoring する必要ありません。開発用のパッケージというのは、pylint のようなコーディング補助用途のパッケージや、pytest のようなテスト用途のパッケージを指します。こういったパッケージもプロジェクトルートの Pipfile で指定しておいて、開発者のローカル環境で取得／管理してもらえばいいでしょう。

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

[dev-packages]
isort = "*"
pylint = "*"
autopep8 = "*"
rope = "*"
yapf = "*"

[requires]
python_version = "3.6"
```

## Vendoring の注意点

Vendoring の導入は、**他のパッケージから依存されない想定のパッケージに限定すべき**です。具体的にいうと、Vendoring はアプリとして振る舞うパッケージには適しますが、ライブラリとして振る舞うパッケージには適しません。なぜかというと Vendoring には特有の問題があって、これは vendor ディレクトリがネストしてしまうことに起因します。

* [How to handle nested "vendor" directories in Go packages? - Stack Overflow](https://stackoverflow.com/questions/38597046/how-to-handle-nested-vendor-directories-in-go-packages?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa)

ちなみに、vendor ディレクトリ以下を直接パス指定して import するアプローチもありますが、こちらもこちらで似たような問題が出るようです。

* [Go言語のDependency/Vendoringの問題と今後．gbあるいはGo1.5 | SOTA](https://deeeet.com/writing/2015/06/26/golang-dependency-vendoring/)

その結果、Go のパッケージ管理ツールでは prune という機能が用意されました。prune 機能は vendor ディレクトリがネストしないように、依存パッケージ内の vendor ディレクトリを消してしまう機能です。

* [Vendor pruning · Issue #120 · golang/dep](https://github.com/golang/dep/issues/120)
* [Commands - Glide Documentation](https://glide.readthedocs.io/en/latest/commands/#glide-install)（install コマンドの -v オプション）

もちろん、Vendoring がツールレベルで整備されていない Python では機械的な prune 処理はできませんので、ライブラリとして採用される想定のパッケージでは、そもそも Vendoring をしないようにするのが無難かと思います。

## まとめ

Python でも Vendoring を導入できること、Vendoring を導入することで既存の問題が解決できることについて述べました。
ツールレベルでの Vendoring サポートがなかったり Vendoring 特有の問題もありますが、Vendoring を採用することで Python の可能性が広がるのでぜひぜひ試してみてください！