環境変数で設定を管理したくなった場合のpython-dotenvのメモ
最近は設定ファイルだけではなく環境変数で設定を渡すことがけっこうある(コンテナ的な文脈だったり、12 factor app的な文脈だったり色々)。自前で頑張っても良いけれど。自分でコードを書かずに済ませたい。
何か良いライブラリは無いかなと探してみたらあったので使いかたのメモ。
python-dotenv
Get and set values in your .env file in local and production servers. tada
install
$ pip install python-dotenv
挙動
getting startedを読むだけでは挙動を把握できなかったので色々調べてみた。
挙動を一言で言うと、os.environに対してdefault値を与えるもの。
たとえば以下のような.env
があるとする。
.env
prefix=hello message=${prefix} world
このとき環境変数を指定して実行せずともdefault値として.envに記述された値が使われる。 (正確に言うなら環境変数として指定された名前の値を保持していない場合に.envから読み込む)
main.py
import os from dotenv import load_dotenv load_dotenv(verbose=True) print("prefix", os.environ.get("prefix")) print("message", os.environ.get("message"))
ここで
defaultは.envに記述された値
$ python main.py prefix hello message hello world
環境変数を与えるとその値が使われる
$ prefix=bye python main.py prefix bye message bye world
case sensitive(大文字小文字を区別する)
$ PREFIX=bye python main.py prefix hello message hello world
なのでlocal用のaccess tokenを.envに記述し本番では環境変数越しに値を渡すということが想定された使われ方(12 factor app的な文脈)。
override=True
override=Trueを使うとdotenv側の設定を尊重する。正直使いみちはあまりないと思う。
$ python main2.py prefix hello message hello world $ prefix=bye python main2.py prefix hello message hello world
環境変数で指定した値が使われなくなる。
変更のdiff
--- main.py 2019-04-29 15:48:35.499386628 +0900 +++ main2.py 2019-04-29 16:29:58.014889108 +0900 @@ -1,6 +1,6 @@ import os from dotenv import load_dotenv -load_dotenv(verbose=True) +load_dotenv(verbose=True, override=True) print("prefix", os.environ.get("prefix")) print("message", os.environ.get("message"))
.envで設定した部分だけを取り出す
これは dotenv_values()
で取り出せる。configとして扱う分にはこちらのほうが便利かもしれない。
from dotenv import load_dotenv, dotenv_values load_dotenv(verbose=True) print(dotenv_values(verbose=True))
実行結果
{'prefix': 'hello', 'message': 'hello world'}
このときの.env
prefix=hello message=${prefix} world
追記
:warning: 環境変数込みで渡した場合にも.env側の値が取り出されるので、configとして使うには危険かも(実装を考えるとこの挙動はあり得る)
$ python main.py {'prefix': 'hello', 'message': 'hello world'} $ prefix=bye python main*.py {'prefix': 'hello', 'message': 'bye world'}
prefixは.envに書かれたものが使われる。一方messageは環境変数の解釈が入りhelloではなくbyeが使われる。
ちょっとひと手間だけれど、以下の様にすれば期待したふるまいになりそう。
import os print({k: os.environ.get(k) for k in dotenv_values()}) # {'prefix': 'bye', 'message': 'bye world'}
複数の.envが用意されていた場合に読まれる.envについて
これはfind_dotenv()
の実行で確認できる。複数のファイルを全部マージするなどと言ったマジカルな挙動は存在しない。最初に見つかった.envファイルを使う。それでおしまい。
ただし現在の位置に存在しなかった場合に親方向を辿って探してはくれる。
$ tree -a . |-- .env |-- main.py |-- sub | |-- .env | `-- main.py `-- sub2 `-- main.py 2 directories, 5 files
ここでそれぞれ見られる.envは以下の様な形。
- main.py は.env
- sub/main.py は sub/.env
- sub2/main.py は .env
調査用のコードは以下のようなもの。
main.py
from dotenv import find_dotenv, dotenv_values print(find_dotenv()) print(dotenv_values())
実行結果
$ python main.py ~/my/example_dotenv/03find/.env {'x': 'root'} $ python sub/main.py ~/my/example_dotenv/03find/sub/.env {'x': 'sub', 'y': 'value'} $ python sub2/main.py ~/my/example_dotenv/03find/.env {'x': 'root'}
このときの.env
.env
x="root"
sub/.env
x=sub y=value
subprocessの実行時の環境変数
subprocessに環境変数を引き継ぐ方法は通常のsubprocessの利用と変わらない。dotenvで読み込んだときに環境変数に設定するので。むしろこれのために環境変数ベースで設定をしていると言っても良い(processとsubprocessとの間でのdynamic scope的な辞書)。
たとえば先程の.env(再掲)を以下の様なsubprocessを利用したコードで使う。
.env
prefix=hello message=${prefix} world
subprocessを利用したコード
main2.py
import subprocess from dotenv import load_dotenv load_dotenv(verbose=True) cmd = ["python", "-c", "import os; print(os.environ.get('message', '<none>'))"] subprocess.run(cmd, check=True)
messageという環境変数が引き継がれる。
$ python main2.py hello world
個別に設定したい場合
これはオフトピックだけれど、個別に設定したい場合はsubprocess.run
のenv
オプションに辞書を渡せば良い。
main3.py
import os import subprocess from dotenv import load_dotenv load_dotenv(verbose=True) cmd = ["python", "-c", "import os; print(os.environ.get('message', '<none>'))"] myenv = os.environ.copy() myenv["message"] = "bye, bye" subprocess.run(cmd, check=True) subprocess.run(cmd, check=True, env=myenv) subprocess.run(cmd, check=True)
結果
$ python main3.py hello world bye, bye hello world