pipenvとGitとDockerを使ったPython開発フロー

# はじめに
最近話題のpipenvですが、実際にどのような開発フローをするのかわかりづらいところがあります。この記事ではGitとDockerの利用を前提としつつ、pipenvを使うための開発フローを解説します。

# 全体の流れ
1. 開発環境のインストール
2. プロジェクトの初期化
3. Docker化

# 前提環境
- Python3系
- UnixライクなOS

# 開発環境のインストール
まずは開発環境のインストールです。pipenvを入れていきましょう。

## pipenvのインストール
### macOSの場合
Homebrewで簡単に導入可能です。

```bash
$ brew install pipenv
```

### Arch Linuxの場合
こちらもリポジトリに登録されているので簡単に導入できます

```bash
$ sudo pacman -S python-pipenv
```

### Ubuntu 20.04の場合
公式リポジトリからインストールできるようになりました！

```bash
$ sudo apt install pipenv
```

### インストール後の設定
pipenvをインストールしたら、`PIPENV_VENV_IN_PROJECT`環境変数をセットしておきます。pipenvで作成される仮想環境はデフォルトで`~/.local/share/virtualenvs`に格納されますが、この設定を有効にするとプロジェクトルートディレクトリの`.venv`ディレクトリに仮想環境が作成される挙動に変更されます。特にPyCharmで開発するときはこの`.venv`ディレクトリをデフォルトで読み込んでくれるため、この設定を有効にしておくことをオススメします。

お使いのシェルの設定ファイル(`~/.bashrc`や`~/.zshrc`など)を開き、以下の行を追加してください。追加したら端末を再起動するか`source`コマンドを使って設定を再読込してください。

```bash:~/.bashrc
export PIPENV_VENV_IN_PROJECT=1
```

## その他ツールの導入
これから利用する`Git`と`Docker`を導入しておいてください。ここでは本題から外れるため、詳細なインストール手順は省きます。

# Pythonプロジェクトの初期化
ここからはいよいよPythonプロジェクトを作成していきます。

## プロジェクトの作成
適当なディレクトリを作り、`pipenv install`コマンドを使ってプロジェクトを初期化します。このとき`--python`オプションを使うことでpyenvを使って自動的にそのバージョンをインストールしてそこから仮想環境を切り出してくれます。

```bash
$ mkdir sample_project
$ cd sample_project
$ pipenv install
Creating a virtualenv for this project…
```

一連の作業が終わってから`ls`すると、`Pipfile`と`Pipfile.lock`の2種類のファイルができあがっていることが確認できます。これで初期化は完了です。

```bash
$ ls
Pipfile  Pipfile.lock
```

## Gitの初期化
続いてこのプロジェクトをGitで管理するためにGitの初期化を行います。[gitignore.io](https://www.gitignore.io/)を利用して、まず`.gitignore`ファイルを作ってしまいましょう。今回はPythonとvirtualenvを指定して作ってもらいます。`.gitignore`に追加の設定が必要な方はここで編集しておいてください。

`.gitignore`ができたら手元のGitリポジトリを初期化して、ざっくり全ファイルを追加してcommitしておきます。PipfileとPipfile.lockはともにGit管理下に置くことを想定しているファイルですので、忘れずにaddしておきましょう。

```bash
$ curl -o .gitignore https://www.gitignore.io/api/python,virtualenv
$ git init
$ git add Pipfile Pipfile.lock .gitignore
$ git commit -m "Initial Commit"
```

## パッケージの追加
pipenv環境下では`pipenv`コマンドを使ってパッケージをインストールします。こうすることで`Pipfile`にそのパッケージが列挙され、その時点で解決されたバージョンやそのパッケージの依存関係にある別のパッケージの詳細が`Pipfile.lock`に記載されます。今回は試しにDjangoをインストールしてみることにします。

```bash
$ pipenv install django
```

インストール後にそれぞれのファイルの中身を覗いてみましょう。

Pipfileには`django = "*"`が追加されています。これは、インストール時点での最新版を使う設定です。もしバージョンを固定したい場合はインストール時に`pipenv install django==2.0.0`などとすると良いでしょう。`django>=1.11.0,<2.0.0`のように範囲指定をすることもできます。

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

[packages]
django = "*"

[dev-packages]

[requires]
python_version = "3.7"
```

Pipfile.lockには依存関係にあるパッケージも列挙されています。また、パッケージのハッシュ値も記載されるようになっています。このハッシュ値はダウンロードされたパッケージそのものから生成されており、これによって異なる環境下でも同一のパッケージがインストールされることを保証することができます。

```json:Pipfile.lock
{
    "_meta": {
        "hash": {
            "sha256": "627ef89f247ecee27e9ef0dabe116108d09c47abf171c900a8817befa64f9dd2"
        },
        "pipfile-spec": 6,
        "requires": {
            "python_version": "3.7"
        },
        "sources": [
            {
                "name": "pypi",
                "url": "https://pypi.org/simple",
                "verify_ssl": true
            }
        ]
    },
    "default": {
        "django": {
            "hashes": [
                "sha256:97886b8a13bbc33bfeba2ff133035d3eca014e2309dff2b6da0bdfc0b8656613",
                "sha256:e900b73beee8977c7b887d90c6c57d68af10066b9dac898e1eaf0f82313de334"
            ],
            "index": "pypi",
            "version": "==2.0.7"
        },
        "pytz": {
            "hashes": [
                "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
                "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
            ],
            "version": "==2018.5"
        }
    },
    "develop": {}
}
```

もし追加のパッケージをインストールしたくなったら、その時点でいつでも`pipenv install`コマンドを使ってパッケージを追加してください。

また、開発時にしか使わないパッケージを導入することも可能です。例えばLinterに`black`と`mypy`を採用することにした場合、以下のように`-d`オプションを付けてインストールします。なおblackはstable版が存在していない都合上、バージョンを明示的に指定しないとバージョン解決に失敗してインストールできないようです。

```bash
$ pipenv install -d black==19.10b0 mypy
```

もろもろをインストールしたら、`Pipfile`と`Pipfile.lock`をaddしてcommitしておきましょう。

```bash
$ git add Pipfile Pipfile.lock
$ git commit -m "install django, black and mypy"
```

## Pythonスクリプトの実行
pipenvで作られるのは仮想環境であるため、pipenvでインストールしたパッケージを利用したPythonスクリプトを実行するためにはまずその仮想環境に入る必要があります。以下のコマンドで仮想環境に入れます。仮想環境に入るとプロンプトの先頭に仮想環境名が表示されるようになります。仮想環境を抜けるときは`exit`コマンドを利用します。

```bash
$ pipenv shell
(sample_project)$ python foo.py
(sample_project)$ exit
```

# Docker化
ここからはDockerに載せるための設定をしていきます。

## .dockerignoreの準備
まず余計なファイルを転送しないために`.dockerignore`を書きましょう。特に`.venv`ディレクトリは無視するようにします。その他不要なものも一緒に書いておくと良いでしょう。

```:.dockerignore
.mypy_cache/
.venv/
__pycache__/
```

## Dockerfileの書き方
`.dockerignore`が書けたら`Dockerfile`を書きます。公式のPythonイメージから作っていきます。プログラムを多少変えただけなのにいちいち依存パッケージのインストールから始めるのは無駄なので、まずは`Pipfile`と`Pipfile.lock`だけを転送し、先にパッケージをインストールしてから、Pythonスクリプトをコピーして作成したアプリケーションを実行するようにします。

また、Python公式イメージにはデフォルトでpipenvが含まれていないため、Dockerfile内でインストールする必要があります。pipenvがインストールできたら、依存パッケージをインストールします。このとき、開発用のパッケージは導入する必要が無いので`-d`オプションは付けないでおきます。

また、`--system`オプションを使うことで、システムデフォルトの`pip`を使ってパッケージをインストールしてくれます。Dockerで使う場合はこのオプションを付けると良いでしょう。逆にそうしないといちいちpipenvで作成した仮想環境に入る手間が加わってしまいます。

今回はマルチステージビルドを利用して、pipenvでインストールしたPythonパッケージを動作用イメージにまるっとコピーして、出来上がりサイズを小さくできるようにしています[^1]。

```Dockerfile:Dockerfile
FROM python:3.8 AS builder

WORKDIR /usr/src/app

COPY Pipfile Pipfile.lock ./

RUN pip install pipenv \
 && pipenv install --system


FROM python:3.8-slim

ENV PYTHONUNBUFFERED=1

COPY --from=builder /usr/local/lib/python3.8/site-packages /usr/local/lib/python3.8/site-packages

COPY . ./

CMD ["python", "app.py"]

```

あとはよしなに開発を進めてください。

# FAQ
## パッケージの更新方法
pipenvで管理しているパッケージに更新があった場合は、`pipenv update`コマンドでそれらのパッケージを依存関係ごとアップデートすることができます。

## パッケージの削除方法
不要になったパッケージは`pipenv uninstall PACKAGE_NAME`で削除できます。

## 途中から開発に参加した場合のパッケージインストール方法
途中から開発に参加し、まっさらな環境の人は環境構築とGitリポジトリのcloneを済ませた上で、`Pipfile`が存在しているディレクトリで以下のコマンドを入力すれば開発に必要なパッケージを全て導入することが可能です。

```bash
$ pipenv install -d
```

## 別のブランチでインストールされたパッケージをマージ後に適用する方法
共同開発でブランチを多数切っていると、他のブランチでもパッケージが追加されたものがmasterにマージされ、それを手元のブランチにrebaseやマージするなどして取り込んだときに`Pipfile.lock`がコンフリクトする現象が発生します。なお`Pipfile`の方はよしなにマージされることが多いです。よしなにマージされなくてもコンフリクト箇所を注意深く観察して手動解決できます。

`Pipfile.lock`がコンフリクトしている場合は、コンフリクトが発生している状態でいったん手元のPipfile.lockを削除し、新しく生成し直す方法を使います。コンフリクトが解決したら忘れずに`add`しておきましょう。以下にコマンド例を示します。

```bash
$ rm -rf Pipfile.lock # お好みで`git rm`でも可
$ pipenv install
$ git add Pipfile.lock
```

## GitリポジトリからPythonパッケージをインストールしたい場合
開発版など、Gitリポジトリにしかパッケージが存在していない場合があります。その場合も、pipenvを使ってパッケージをインストールすることが可能です。この場合少し表記が複雑になりますが

- リポジトリのアドレス
- コミットハッシュやタグ名などのリビジョン
- パッケージ名

の3つを抑えておけばOkです。あとは下記のフォーマットに従って入力すれば大丈夫です。

```bash
$ pipenv install git+[git_repository_url]@[revision]#egg=[package_name]
```

例えば拙作のhackingのフォークをGitリポジトリから導入する場合、以下のようにします。今回は開発用にしか使わないので`-d`オプションを付けています。リビジョンはmasterブランチの最新のコミットから取得したものです。パッケージ名は自由に付けて良いのですが、わかりやすく`hacking`にしてあります。

```bash
$ pipenv install -d git+https://github.com/aruneko/hacking@733ffe4a711ed6c92063d8074410ac05c97608dc#egg=hacking
```

# おわりに
ここまでpipenvとGitとDockerを使ったPython開発フローを紹介してきました。virtualenvなどを使っていた頃と比べるとかなりシンプルに開発できていると思います。また、Pipfile.lockのおかげで再現性のある環境をどこでも立ち上げることができるようになり、利便性も増しています。皆さんもぜひpipenvを使ってPython開発環境を快適にしてみましょう！

[^1]: 参考: [フューチャー開発者ブログ](https://future-architect.github.io/articles/20200513/)