flaskでconfigファイルの指定に物理的なパスを指定できるようにしたくなったという話
はじめに
flaskを使い始めたのだけれど。flaskのconfigファイルの指定が対象のflaskアプリの存在するモジュールの位置からの相対パスを指定するらしく使いづらい。
例えば、fooパッケージの中で、以下の様なコードがあった場合には、fooパッケージからの相対位置を指定する必要がある。
from flask import Flask app = Flask(__name__) app.config.from_python("<config path>") app.run()
この __name__
を使っているパッケージの位置からの相対位置を指定する必要がある。だるい。これを止めて直接特定のファイルの位置を指定できるようにしようという話。
Appのconfig_classを変更すれば良い
はじめに手抜きをするために以下のmagicalimportというパッケージをインストールしておく*1。
$ pip install magicalimport
configオブジェクトを作るファクトリーの設定を変更する必要がある。以下の様な形で作れば良い。
from flask import Flask, Config import logging import magicalimport logger = logging.getLogger(__name__) class CustomConfig(Config): def from_pyfile(self, path): return self.from_object(magicalimport.import_from_physical_path(path)) class App(Flask): config_class = CustomConfig # or monkey patching # Flask.config_class = CustomConfig if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() parser.add_argument("-c", "--config", default=None, required=True) args = parser.parse_args() app = App(__name__) if args.config: app.config.from_pyfile(args.config) logging.basicConfig( level=app.config["LOGGING_LEVEL"].upper(), format=app.config["LOGGING_FORMAT"] ) logger.info("running.. (port=%s)", app.config["PORT"]) app.run(port=app.config["PORT"])
あとは以下のように好きな位置のファイルを指定してアプリを起動する事ができる。
$ tree config config ├── base.py └── local.py 1 directory, 2 files $ python foo/app.py --config=./config/local.py asctime2016-10-02 00:52:40,828 loglevel:INFO message:running.. (port=8888) asctime2016-10-02 00:52:40,847 loglevel:INFO message: * Running on http://127.0.0.1:8888/ (Press CTRL+C to quit)
やりましたね。
設定ファイルの内容
設定ファイルの内容は以下
config/base.py
PLAIN_LOGGING_FORMAT = "%(asctime)s %(levelname)s %(message)s" LTSV_LOGGING_FORMAT = "asctime%(asctime)s\tloglevel:%(levelname)s\tmessage:%(message)s"
config/local.py
import magicalimport import os.path here = os.path.dirname(os.path.abspath(__file__)) base = magicalimport.import_from_physical_path(os.path.join(here, "./base.py"), as_="base") LOGGING_LEVEL = "info" LOGGING_FORMAT = base.LTSV_LOGGING_FORMAT PORT = 8888
*1:実はこのためについ先程作りました
magicalimportというライブラリを作ってました
magicalimportというライブラリを作ってました。
はじめに
これは何をするライブラリかというと物理的なファイル名を指定して、指定したファイルをpython moduleとしてimportするためのライブラリです。
用途
例えばconfigファイルの読み込みに便利かもしれません。
使い方
例えば、以下のようなファイル構造の時に、以下のようなfoo.pyがあった時に。
. ├── a │ └── b │ └── c │ └── foo.py └── main.py
a/b/c/foo.py
name = "foo" _age = "*secret*"
main.pyでは以下の様なコードでfoo.pyを読み込むことができます。
from magicalimport import import_from_physical_path foo = import_from_physical_path("./a/b/c/foo.py")
ちなみに、importするmodule名を指定する事もできて、 as_
オプションを付けます。sys.modulesに登録されるのでその後は普通にimportできます。
from magicalimport import import_from_physical_path foo = import_from_physical_path("./a/b/c/foo.py", as_="foo2") import foo2 # fooとfoo2は同じもの
注意点
moduleの階層構造に関係なくimportしているところがあるので読み込んだ先のファイルでrelative importなどは上手くいかないです。
例えば、以下のような設定ファイルの構造でlocal.py,test.pyがbase.pyの設定を共有したいときなどに。
config ├── base.py ├── test.py └── local.py
普通にrelative importしたくなりますがこれは動きません。
from .base import *
以下の様に書く必要があります。star importする場合にはexpose_all_membersを使うと便利です。
# 以下はだいたい `from .base import *` と同じ import magicalimport import os.path here = os.path.dirname(os.path.abspath(__file__)) base = magicalimport.import_from_physical_path(os.path.join(here, "./base.py"), as_="base") magicalimport.expose_all_members(base)
moduleの"_"で始まるものはimportされないですが。
追記
python2もサポートしました
追記
here オプションをサポートしました。以下の様に書ける様になりました。
import magicalimport base = magicalimport.import_from_physical_path("base.py", as_="base", here=__file__) magicalimport.expose_all_members(base)