標準ライブラリだけでpythonモジュールを設定ファイルとして使う方法のメモ
標準ライブラリだけでpythonモジュールを設定ファイルとして使う方法のメモ。importlibの関数を使う方法もあるけれど。たまたまrunpyというモジュールも発見したのでそれのメモ。
pythonモジュールを設定ファイルとして使う
pythonモジュールを設定ファイルとして使うのは幾つかのフレームワークで行われている。代表的なものとしてあげられるのはdjangoやflaskかもしれない。
各々けっこう色々な方法で行っているような印象がある。一番シンプルなものはファイルをopenしてexecだけれど。最近は他の方法が取られていることが多い。
settings.py
import logging # ログ設定 LOGGER = logging.getLogger("myapp") # 一番多いのはdatabaseの設定かもしれない DB = get_something_db_accessor()
app側では以下の様にして使う。慣習的に設定は全て大文字表されることが多いかもしれない。
from app import setting setting.LOGGER.info("何か")
.jsonや.yamlや.iniなどを設定ファイルとして使うところとは異なりオブジェクトを設定できる所が違う。何より簡易的な間接参照(DI的なもの)に使えるところが便利。
runpyを使った方法
runpyについて
runpyにはrun_path()
という関数が用意されていてこれには物理的なファイルパスを指定することもできる。なのでこれを使って設定ファイルとしてpythonモジュールを読み込ますことにすると物理的なファイルパスを指定できる様になるので便利。
読み込んだモジュールは辞書として返される。
import runpy import pprint d = runpy.run_path("./settings.py") print(type(d)) pprint.pprint({k: repr(v)[:100] for k, v in d.items()}) # -- stdout -------------------- # >> <class 'dict'> # >> {'LOGGER': '<Logger app (INFO)>', # >> '__builtins__': '{\'__name__\': \'builtins\', \'__doc__\': "Built-in ' # >> 'functions, exceptions, and other objects.\\n\\nNoteworth', # >> '__cached__': 'None', # >> '__doc__': 'None', # >> '__file__': "'./settings.py'", # >> '__loader__': 'None', # >> '__name__': "'<run_path>'", # >> '__package__': "''", # >> '__spec__': 'None', # >> 'logging': "<module 'logging' from '/usr/lib/python3.7/logging/__init__.py'>", # >> 'os': "<module 'os' from '/usr/lib/python3.7/os.py'>"}
runpyを使った設定ファイルとしての利用
以下の様なコードを書いてあげると、設定ファイルとしてpythonモジュールを取れるようなコードが書ける。
import runpy def run(*, setting: str) -> None: setting = runpy.run_path(setting) setting["LOGGER"].info("hello") def main(argv=None): import argparse parser = argparse.ArgumentParser(description=None) parser.print_usage = parser.print_help parser.add_argument("--setting", required=True) args = parser.parse_args(argv) run(**vars(args)) if __name__ == "__main__": main()
実行結果は以下。
$ python app.py --setting=./settings.py INFO:app:hello
このときのsettings.pyは以下。
import logging import os # ログ設定(本来はここで直接basicConfigを呼ぶことは少ないけれど) LOGGER = logging.getLogger("app") logging.basicConfig(level=getattr(logging, os.environ.get("LOGLEVEL") or "INFO"))
settings.pyで作られたloggerオブジェクトをrunpy経由で読み込んで使うことができている。
importlibを使った方法
ついでなのでimportlibを使った方法も。辞書ではなくこちらは通常のモジュールとして扱うことになる。importlib.utilで提供されている関数を使って、以下の様な関数をつくる。utilモジュールなのでAPIとしてはあまり安定していない気はしている。
module_id
で指定された名前のモジュールとしてimportされる。
import sys from importlib.util import spec_from_file_location from importlib.util import module_from_spec def load_module_by_path(module_id, path): spec = spec_from_file_location(module_id, path) module = module_from_spec(spec) spec.loader.exec_module(module) sys.modules[module_id] = module return module
後は以下のように変更する。
--- app.py 2019-03-10 23:18:46.607199037 +0900 +++ app2.py 2019-03-10 23:29:05.894543006 +0900 @@ -1,12 +1,23 @@ -import runpy +import sys +from importlib.util import spec_from_file_location +from importlib.util import module_from_spec + + +def load_module_by_path(module_id, path): + spec = spec_from_file_location(module_id, path) + module = module_from_spec(spec) + spec.loader.exec_module(module) + sys.modules[module_id] = module + return module def run(*, setting: str) -> None: - setting = runpy.run_path(setting) - setting["LOGGER"].info("hello") + setting = load_module_by_path("setting", setting) + setting.LOGGER.info("hello")
辞書では無くモジュールとしてimportされるのでsetting["LOGGER"]
ではなくsetting.LOGGER
。
実行結果。
$ python app2.py --setting=./settings.py INFO:app:hello
はじめはimportlib経由でのインターフェイス(つまりgetattrでアクセスできること)が自然だと思い、モジュールではないことに違和感を持っていたこともあったけれど。最近はむしろコンポーネントがDIされているということが明示的になる分辞書的なインターフェイスだけを持っていた方が便利なんじゃないかと思ったりもしている。
そんなわけで、runpyの注意事項が許容できたり、辞書として扱うのが問題ないのであれば、runpyを使うのが手軽かもしれない。