Pythonでdirenvを使う。上位ディレクトリからimportする方法

# この記事で解決したい問題

今まで適当に配置していたpythonファイルたち、プロジェクトが大きくなってフォルダ整理することにしました。ここで発生したのが自作ライブラリをインポートできない問題です。pythonでは実行フォルダおよびその配下のフォルダだけがモジュール検索対象のパスとなっているので、自分より上位にあるファイルからインポートができません。

今まではsys.path.appendを使って上位にあるファイルからインポートしていましたが、以下のようなデメリットがあり、sys.path.append を使わずに解決する方法はないものかと悩んでいました。

## sys.path.append の弱点

sys.path.appendを使って上位ファイルからインポートをしようとすると、ファイルの最初の数行が必ず次のようになります。

```python
import sys
import pathlib

sys.path.append(str(pathlib.Path(__file__).resolve().parents[1]))
from MyModule.MyClass import MyClass
```

これの何が嫌かと言うと、

- 上位ファイルからインポートしたい全部のファイルでsys.path.appendしなければならない
- autopep8などコードの自動整形ツールを使うとimportの文が勝手に上に移動されてしまう

という点です。きれいなコードを書きたいという理想にどうしても相反してしまいますよね。

そんな悩みを今回解決してくれるのが direnv です。direnv はディレクトリ毎に .envrc に環境変数を定義することで、.envrcがあるディレクトリがカレントディレクトリになった時だけ環境変数を有効にしてくれるツールです。

# 筆者の開発環境
OS：macOS Catalina 10.15.6
brew 2.5.2
bash : 3.2.57(1)-release (x86_64-apple-darwin19)
Python : 3.6.10

# 今回の説明で用いるフォルダ構造

```
grand_mother
┣ mother
┃　┗ me.py
┗ aunt
　　┗ cousin.py 
```

`me.py`で`cousin.py`をインポートして使うことを想定して説明します。
`me.py`と`cousin.py`はそれぞれ以下のようになっています。

```python:cousin.py
class Cousin:
    name = 'Reon'
    age = 14
```

```python:me.py
from aunt.cousin import Cousin
print('name :', Cousin.name)
print('age :', Cousin.age)
```

# [direnv](https://github.com/direnv/direnv)をインストールする

Macの場合はbrewコマンド一発です。

```terminal
$ brew install direnv
```

Macでない場合はgit cloneします。

```terminal
$ cd /path/to/directory # インストールしたい場所
$ git clone https://github.com/direnv/direnv
$ cd direnv
$ sudo make install
```

bashにhookを追加します。

```bash:~/.bashrc
eval "$(direnv hook bash)"
```

~/.bashrcの再読み込みしてdirenvを動作するようにします。

```terminal
$ source ~/.bashrc
```

# direnvを使えるようにする

環境変数を設定するプロジェクトのトップ（今回であれば grand_mother/ ）に移動して、以下のコマンドを実行します。

```terminal
$ cd 〇〇/grand_mother
$ direnv edit .
```

すると grand_mother/.envrc が作成されるので PYTHONPATH を追加します。

```bash:grand_mother/.envrc
export PYTHONPATH="〇〇/grand_mother:$PYTHONPATH"
```

これで grand_mother 内のファイルで `from mother import` や `from aunt import` などができるようになります。

## PYTHONPATH とは

PYTHONPATHはpythonがモジュールを読み込む時にデフォルトのpythonモジュールに加えてモジュールを探しに行くディレクトリを追加するものです。以下のコマンドで今設定されているPYTHONPATHを確認できます。

```terminal
# PYTHONPATHを確認
$ echo $PYTHONPATH

# PYTHONを含む環境変数を全て表示
$ printenv | grep PYTHON
```

また、pythonがモジュールを読み込む時に読み込むディレクトリを全て見たいのであればpythonのsys.pathで確認できます。

```python
import sys
print(sys.path)
```

## (必要な人のみ) .envに環境変数を書きたい場合

* [direnvを使うときは環境変数を.envrcに書くより.envに書いた方が使い勝手が良い](https://blog.p1ass.com/posts/direnv-dotenv/)

この記事を参考にして、`grand_mother/.env` の方に環境変数を書く場合も説明します。自分もすでに他の環境変数を .env に書いていたので、この方式を採用しました。

.envrcはこれだけです。

```bash:grand_mother/.envrc
dotenv
```
.envの方に以下の一文を追加しましょう。

```:grand_mother/.env
PYTHONPATH=/〇〇/grand_mother/
```

# direnvの動作を確認する

.envrc を設定したら

```
direnv: error .envrc is blocked. Run `direnv allow` to approve its content.
```

こんなエラー文が表示されているかもしれません。その時はエラー文にある通り、`direnv allow`を実行して、PYTHONPATHが追加されたことを確認します。.envrcを書き換えるたびにこの文章は表示されます。

```terminal
$ direnv allow
direnv: loading ~/〇〇/grand_mother/.envrc
direnv: export +PYTHONPATH
```

.envrcがあるディレクトリから抜けると、direnvが一時無効化されます。

```terminal
$ cd ..
direnv: unloading
```

再度、.envrcがあるディレクトリに入ると.envrcの内容が環境変数に追加されていることがわかります。

```terminal
$ cd grand_mother/
direnv: loading .envrc
```

# 解決しました

これで me.py を実行するとうまくいきます。

```terminal
$ python mother/me.py
name : Reon
age : 14
```

# .envrcの運用

GitHubなどでソースコードを共有する場合、 .env や .envrc には盗まれると困る情報が乗っている。あるいは実行環境によって異なる値を設定すべき場合があるので、.gitignoreに.envrcを追加するなどして、リポジトリにコミットしないようにしましょう。


# (補足) Amazon Linux にもdirenvを導入する

今回自分のローカルに合わせて、運用していたAmazon Linuxの方にもdirenvを入れる必要があったので、そのときの作業内容も書いておきます。

## Amazon Linuxの環境

```terminal
$ cat /etc/os-release
Amazon Linux AMI 2018.03
$ bash --version
GNU bash, バージョン 4.2.46(2)-release (x86_64-redhat-linux-gnu)
```


まず、Amazon Linux にはdirenvを動かすためのgoが入ってないのでインストールする必要があります。

```terminal
$ sudo yum install golang -y
```

そして、この記事の上の方にあるように、git cloneしてdirenvをインストールします。

```terminal
$ cd /path/to/directory # インストールしたい場所
$ git clone https://github.com/direnv/direnv
$ cd direnv
$ sudo make install
```

bashにGOPATHとdirenvのhookを追加します。GOPATHは、ビルドをするための場所の指定なので、任意の場所で大丈夫です。

```bash:~/.bashrc
export GOPATH=$HOME/go

eval "$(direnv hook bash)"
```

~/.bashrcの再読み込みして、設定結果を確認しておきましょう。

```
$ source ~/.bashrc
$ echo $GOPATH
/home/ec2-user/go
```

あとは、記事の上の方に書いた **direnvを使えるようにする** のやり方で Amazon Linux でもdirenvが使えるようになりました。

# 参考記事

* [Macで環境変数を管理するためのdirenvの設定方法](https://note.com/shimakaze_soft/n/n53949cc818d1)
* [direnv の使い方](https://pyteyon.hatenablog.com/entry/2020/04/28/122552)
* [PYTHONPATHとは](https://ja.stackoverflow.com/questions/12174/pythonpath%E3%81%A8%E3%81%AF)
* [direnvを使おう](https://qiita.com/kompiro/items/5fc46089247a56243a62)
* [pythonにおける親/兄弟階層のモジュールimportの方法](https://rinoguchi.hatenablog.com/entry/2019/11/29/130224)
* [pythonにおけるローカル環境での環境変数の設定](https://rinoguchi.hatenablog.com/entry/2019/10/17/121642)