深入Flask配置

本文最后更新于:2023年4月24日 下午

深入 Flask 配置

在 Flask 中,提供了丰富的全局配置来方便开发,以下是开发过程当中几个常用的配置选项。

配置名 作用
DEBUG 启用 / 禁用调试模式
SECRET_KEY 密钥
SERVER_NAME 服务器名和端口。需要这个选项来支持子域名 (例如: 'myapp.dev:5000' )。注意 localhost 不支持子域名,所以把这个选项设置为 “localhost” 没有意义。设置 SERVER_NAME 默认会允许在没有请求上下文而仅有应用上下文时生成 URL
SESSION_COOKIE_NAME 会话 cookie 的名称。

更多的配置详见官方文档

如果要在 Flask 中激活某些的配置,通常有以下 8 种方式,前面的两种都是针对某个单独配置。第三种方式,就是采用字典的更新键值对的方法,因为 Config 类本身就是继承自字典,所以同时也继承了字典的 update 方法。 而后面的几种方式,则可以对多个配置项进行处理。

1
2
3
4
5
6
7
8
app.debug  = True
app.config["debug"] = True
app.config.update()
app.config.from_envvar()
app.config.from_json()
app.config.from_mapping()
app.config.from_pyfile()
app.config.from_object()

下面谈谈其他几种方法的内部操作以及原理。

一、Config 配置类的创建过程

首先 Config 类是在 flask/config.py 文件里面。

1
2
3
4
5
# 从这里可以看出 Config类 继承了 dict
class Config(dict):
def __init__(self, root_path, defaults=None):
dict.__init__(self, defaults or {})
self.root_path = root_path

下面是 flask/app.py 里面的 Flask 类,由于这个类的代码数量庞大,所以只贴出一点用到了 Config 类的地方。

可以看到 default_config 是一个 ImmutableDict (不可变字典对象),里面是所有支持的配置项,并且都给出了默认值。

Config 类会被赋值给 Flask 的成员对象 config_class,但此时这个成员对象,也就是字典对象,还没有任何数据;所以要通过 Flaskmake_config 来为 config_class 赋值字典数据,而此时传入的配置就是 defaults,包含了 Flask 全部配置项。 初始时的两个配置项 ENVDEBUG 会通过 get_env() 方法 和 get_debug() 方法设置为 productionFalse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Flask(_PackageBoundObject):
# 这里的 Config 类就是 flask/Config.py 下面的 Config 类
config_class = Config

# 所有支持的配置项
default_config = ImmutableDict(
{
"ENV": None,
"DEBUG": None,
"TESTING": False,
"PROPAGATE_EXCEPTIONS": None,
"PRESERVE_CONTEXT_ON_EXCEPTION": None,
"SECRET_KEY": None,
"PERMANENT_SESSION_LIFETIME": timedelta(days=31),
"USE_X_SENDFILE": False,
"SERVER_NAME": None,
"APPLICATION_ROOT": "/",
"SESSION_COOKIE_NAME": "session",
"SESSION_COOKIE_DOMAIN": None,
"SESSION_COOKIE_PATH": None,
"SESSION_COOKIE_HTTPONLY": True,
"SESSION_COOKIE_SECURE": False,
"SESSION_COOKIE_SAMESITE": None,
"SESSION_REFRESH_EACH_REQUEST": True,
"MAX_CONTENT_LENGTH": None,
"SEND_FILE_MAX_AGE_DEFAULT": timedelta(hours=12),
"TRAP_BAD_REQUEST_ERRORS": None,
"TRAP_HTTP_EXCEPTIONS": False,
"EXPLAIN_TEMPLATE_LOADING": False,
"PREFERRED_URL_SCHEME": "http",
"JSON_AS_ASCII": True,
"JSON_SORT_KEYS": True,
"JSONIFY_PRETTYPRINT_REGULAR": False,
"JSONIFY_MIMETYPE": "application/json",
"TEMPLATES_AUTO_RELOAD": None,
"MAX_COOKIE_SIZE": 4093,
}
)

# 返回初始完毕的配置类
def make_config(self, instance_relative=False):
root_path = self.root_path
if instance_relative:
root_path = self.instance_path
defaults = dict(self.default_config)
defaults["ENV"] = get_env()
defaults["DEBUG"] = get_debug_flag()
return self.config_class(root_path, defaults)

二、从环境变量中读取配置属性

当在环境变量中设置了配置文件的环境变量,那么则可以使用这个方法。

首先是使用 os 模块的 environ.get() 方法来获取环境变量属性值,而后再调用另一个方法,这个环境变量的属性值是一个文件路径,通常的话,这个配置文件应该方法在和启动文件在同一个路径下。

1
2
3
4
5
6
7
8
9
10
11
12
def from_envvar(self, variable_name, silent=False):
rv = os.environ.get(variable_name)
if not rv:
if silent:
return False
raise RuntimeError(
"The environment variable %r is not set "
"and as such configuration could not be "
"loaded. Set this variable and make it "
"point to a configuration file" % variable_name
)
return self.from_pyfile(rv, silent=silent)

在项目的同目录下创建一个 config.cfg 配置文件,写入两个简单的配置项。

1
2
DEBUG=True
SECRET_KEY="something"

使用 os 模块临时设置一个环境变量,当从环境变量中读取到配置文件后,在网页中能打印到配置属性的值。

还有一种办法是 (针对 Linux 环境),新开一个终端,切换到项目的目录下,在启动项目前,先使用 export FLASK_CONFIG=config.cfg,然后启动文件里面只需要写 app.config.from_envvar("FLASK_CONFIG")就可以,当然使用 export 设置的也只是一个临时变量,只对目前的终端有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import os

from flask import Flask

app = Flask(__name__)

os.environ.setdefault("FLASK_CONFIG", "config.cfg")

app.config.from_envvar("FLASK_CONFIG")

@app.route("/")
def index():
return "DEBUG %s SECRET_KEY %s" % (app.config.get("DEBUG"), app.config.get("SECRET_KEY"))

if __name__ == "__main__":
app.run()

我比较在意的是这个 silent 参数, 这个参数的含义是,当配置文件丢失时,或者环境变量没有设置时,设置 silent 参数为 True,那么就等于没有配置这个文件。

那么可以写一个函数来检测这个配置文件是否存在,是否设置环境变量,当两个都没有时,返回 True,然后 from_envvar 方法将会不起作用,触发异常。

下面是一个简单的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from flask import Flask

import os

app = Flask(__name__)

def is_set():
if not os.environ.get("FLASK_CONFIG"):
return True
elif not os.path.exists("config.cfg"):
return True
else:
return False

app.config.from_envvar("FLASK_CONFIG", silent=is_set())

@app.route("/")
def index
return "DEBUG %s SECRET_KEY %s" % (app.config.get("DEBUG"), app.config.get("SECRET_KEY"))

if __name__ == "__main__":
app.run()

针对 silent 参数,写出一个方法来最终决定 silent 的值,这样防止了中间环境变量配置出错以及文件不存在等等情况。

三、 从 python 文件中读取配置属性

当创建一个 Flask 的实例对象之后,使用 app.config.from_pyfile 方法,传入一个配置文件字符串,从配置文件中读取属性并且写入,前面的 from_envvar 方法获取到配置文件后最终也会调用这个方法,并且,这个方法最终也会调用下一个方法 from_object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def from_pyfile(self, filename, silent=False):
filename = os.path.join(self.root_path, filename)
d = types.ModuleType("config")
d.__file__ = filename
try:
with open(filename, mode="rb") as config_file:
exec(compile(config_file.read(), filename, "exec"), d.__dict__)
except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR, errno.ENOTDIR):
return False
e.strerror = "Unable to load configuration file (%s)" % e.strerror
raise
self.from_object(d)
return True

在项目的根目录下创建一个名为 config.cfg 简单配置文件。

1
2
DEBUG=True
SECRET_KEY="something"

先看 from_pyfile 文件前面的三段代码, 首先使用 os获取配置文件的绝对路径,然后用 types.ModuleType 方法动态创建了一个 config 模块,并且设置文件名为传进来的文件名的绝对路径文件名,此时这个 config 算是一个模块了,不是用普通的 import 方法导入的。

1
2
3
filename = os.path.join(self.root_path, filename)
d = types.ModuleType("config")
d.__file__ = filename

types 属于 Python的标准库,里面的几个常用的方法没怎么了解,有如下几个。

  • FunctionType:通过不使用 def 的方式动态创建一个函数。
  • MethodType:将创建在类外的某个方法动态绑定到类的实例上。
  • ModuleType:动态的创建一个临时的模块。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
>>> import sys
>>> import types

# 动态创建一个模块
>>> m = types.ModuleType("sample", "sample module.") # 传入一个模块名,以及模块的文档字符串
>>> m
<module 'sample'>
>>> m.__dict__
{'__name__': 'sample', '__doc__': 'sample module.', '__package__': None, '__loader__': None, '__spec__': None}
>>> m in sys.modules # 模块不包含在系统模块中
False

# 动态添加类方法
>>> class Person:
... pass
...
>>> p = Person()
>>> def say(self): print("hello")
>>> p.say = types.MethodType(say, p)
>>> p.say()
hello

# 动态创建一个函数
>> foo_code = compile('def foo(): return "bar"', "<string>", "exec")
>> foo_func = types.FunctionType(foo_code.co_consts[0], globals(), "foo")
>> print(foo_func())
bar

继续看 from_pyfile 方法剩下的代码,读取配置文件的配置属性,此时把属性放进动态创建的模块的字典里头,最后是调用另一个方法。

1
2
3
4
5
6
7
8
9
10
try:
with open(filename, mode="rb") as config_file:
exec(compile(config_file.read(), filename, "exec"), d.__dict__)
except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR, errno.ENOTDIR):
return False
e.strerror = "Unable to load configuration file (%s)" % e.strerror
raise
self.from_object(d)
return True

四、从 python对象中提取属性

python 对象中提取配置相对简单,一般写一个配置类的 python 文件,里面定义一个基类,设定一些基本配置,然后使用类继承的方法为各种环境设置扩展配置类,一个简单的配置类如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Config(object):
DEBUG = False
TESTING = False
DATABASE_URI = 'sqlite://:memory:'

class ProductionConfig(Config):
DATABASE_URI = 'mysql://user@localhost/foo'

class DevelopmentConfig(Config):
DEBUG = True

class TestingConfig(Config):
TESTING = True

之后就是在 from_object 方法里传入一个类名,或者一个完整的模块字符串就可以配置好配置属性。如果传入的是一个配置类,那么 if isinstance(obj, string_types) 直接为 False,然后这个传入的配置类的所有属性,如果包含大写的属性,将存入 app.config 的字典中。如果是字符串,那么会先把模块里面的类导入再提取属性。

1
2
3
4
5
6
def from_object(self, obj):
if isinstance(obj, string_types):
obj = import_string(obj)
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)

string_types ,这个变量在 flask 包的 _compat.py 下,其实就是string 类型,所以简单对传进来的参数检查是不是个字符串,如果是字符串,则会调用 import_string 方法,如果不是字符串而是一个具体的类则直接执行 for 循环对类的属性遍历。

1
2
3
4
5
6
7
8
try:  
text_type = unicode
string_types = (str, unicode)
integer_types = (int, long)
except NameError:
text_type = str
string_types = (str,)
integer_types = (int,)

然后是 import_string 方法,因为传进来的仅仅是个字符串,还没对模块进行导入,所以会用到 werkzeug.utils 包下的 import_string 方法对模块进行导入。

可以看到 import_string 的第一行代码会对字符串进行替换,那说明有两种写法,而恰恰函数文档也说明了。

1
2
3
4
5
6
7
8
"""
提供两种模块写法:例如
xml.sax.saxutils.escape
xml.sax.saxutils:escape
无论是那种写法,最后都会变成下面这种写法
xml.sax.saxutils.escape
"""
import_name = str(import_name).replace(":", ".")

接下来就是使用不寻常的导包方式,一般导包都是两种方式,import package 或者是 from package import module,因为我们这里传进来的是字符串,所以不能用正常的导包方式,只能使用 __import__ 这个内建方法,实际上 import 也是调用 __import__。假设配置类 BaseConfigConfig 包下的 Settings.py 模块下,那么可以写为 Config.Settings:BaseConfig,或者 Config.Settings.BaseConfig ,两者的可以。最终,如果导入为空时,那么会从 sys.modules 里面查询这个包。

1
2
3
4
5
6
7
try:
__import__(import_name)
except ImportError:
if "." not in import_name:
raise
else:
return sys.modules[import_name]

如果这个传入的配置类字符串为 Config.Settings.BaseConfig,那么先把模块名和对象名分开,再尝试使用 __import__ 方法导入,此时已经把模块名和对象名分开了,分别把模块名和对象名传入 __import__ 就可以正常导入。再使用 getattr 方法获取模块里面的对象,这个方法告一段落。最后就是把返回的对象遍历获取里面的配置属性添加到 Config 对象中。

__import_ 的四个参数:

  • name (required): 被加载 module 的名称
  • globals (optional) : 包含全局变量的字典,该选项很少使用,采用默认值 global()
  • locals (optional): 包含局部变量的字典,内部标准实现未用到该变量,采用默认值 local()
  • fromlist (Optional) : 被导入的子模块名称
1
2
3
4
5
6
module_name, obj_name = import_name.rsplit(".", 1)
module = __import__(module_name, globals(), locals(), [obj_name])
try:
return getattr(module, obj_name)
except AttributeError as e:
raise ImportError(e)

总的来说这个方法提供了两种导入配置的选择,一种是传入模块字符串,一种是直接传入配置类。便于导入的时候选择导入方式和可扩展性。

五、从 json 文件中读取配置属性到映射为字典

这个方法最终也会调用 Config 类的最后一个方法,对于这个方法而言,只是简单的读取一下 json 文件,并且把 json 文件里面的数据转化为 Python 当中的字典类型。同时可以设置 silentTrue,当文件读取失败的时候,方法直接失效,如果不设置为 True 的话,也可以,直接触发标准错误。

1
2
3
4
5
6
7
8
9
10
11
12
def from_json(self, filename, silent=False):
filename = os.path.join(self.root_path, filename)

try:
with open(filename) as json_file:
obj = json.loads(json_file.read())
except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
return False
e.strerror = "Unable to load configuration file (%s)" % e.strerror
raise
return self.from_mapping(obj)

在项目的根目录下创建一个名为 config.json 的配置文件。

1
2
3
4
{
"DEBUG": "True",
"SECRET_KEY": "something"
}

示例代码,当程序启动时,打开 http://127.0.0.1:5000/ 就可以看到 DEBUGSECRET_KEY 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask

app = Flask(__name__)

app.config.from_json("config.json")

@app.route("/")
def index():
return "DEBUG %s SECRET_KEY %s" % (app.config.get("DEBUG"), app.config.get("SECRET_KEY"))

if __name__ == "__main__":
app.run()
print(app.config.items())

六、从 python 键值对 ( dict ) 中配置属性

设置一个简单的字典对象,待会传入 from_mappings 方法。

1
2
3
4
configs = {
"DEBUG": True,
"SECRET_KEY": "Something"
}

最后一个方法,针对传进来的键值对,也就是字典,这里对应第三个参数 **kwargs,使用 kwargs.items() 提取出所有的键值对 (列表格式) 存放进 mappings 列表里,然后再通过二层循环提取出每个配置的键和值,存放进 Config 类的里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def from_mapping(self, *mapping, **kwargs):
mappings = []
if len(mapping) == 1:
if hasattr(mapping[0], "items"):
mappings.append(mapping[0].items())
else:
mappings.append(mapping[0])
elif len(mapping) > 1:
raise TypeError(
"expected at most 1 positional argument, got %d" % len(mapping)
)
mappings.append(kwargs.items())
for mapping in mappings:
for (key, value) in mapping:
if key.isupper():
self[key] = value
return True

例子示范,当启动程序时,在浏览器进入 http://127.0.0.1:5000/ 就能看到 DEBUGSECRET_KEY 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from flask import Flask

app = Flask(__name__)

configs = {
"DEBUG": True,
"SECRET_KEY": "Something"
}

app.config.from_mappings(configs)

@app.route("/")
def index():
return "DEBUG %s SECRET_KEY %s" % (app.config.get("DEBUG"), app.config.get("SECRET_KEY"))

if __name__ == "__main__":
app.run()
print(app.config.items())

然后这个方法还提供另一种细化的使用,上面只是传入了第三个参数,第二个参数还没使用,显然这个函数是会使用到第二参数,那么这个参数格式有几种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
mapping = (
{
"DEBUG": True,
"SECRET_KEY": "Something"
}
)
# 直接执行 mappings.append(mapping[0])

mapping = (
('DEBUG', True),
('TESTING', False)
)
# 传入元祖,直接执行 mappings.append(mapping[0])

此时就不需要传入第三个参数,也就是说这个方法提供两种参数传入方式,也方便扩展,如果单纯使用键值对,那么前面的代码将不会被执行,如果使用 tuple ,也会对这个参数进行操作,提取里面的属性值。

七、自定义读取 yaml/properties 配置文件

上面提到可以用 types.MethodType 来创建动态方法,这里就可以利用这个来为 config 扩展读取更多类型的配置文件。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import types

# 创建一个 Person 类,此时类里还没有任何方法
class Person:
pass

def say(self, name):
self.name = name
print("hello", name)
p = Person()

# 强行把方法赋值到实例对象上
p.say = say
p.say(name="nick")
# 报错
# say() missing 1 required positional argument: 'self'

p.say = types.MethodType(say, p)
p.say(name="nick")
# result: hello nick

创建一个名为 config.yaml 的配置文件,写入两个简单的配置项。

1
2
DEBUG: True
SECRET_KEY: something

读取 yaml 文件,编写读取 yaml 文件方法,利用了 pyyaml 库,使用 yaml 读取出来的数据是字典格式,然后传递给 Config 对象的 from_mapping 方法,然后利用 types.MethodType 方法为 Config 类动态添加方法,绑定在 config 对象上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from flask import Flask

import types
import os
import yaml

app = Flask(__name__)

def from_yaml(self, filename, silent=False):
filename = os.path.join(self.root_path, filename)
try:
with open(filename) as yaml_file:
obj = yaml.load(yaml_file.read(), Loader=yaml.FullLoader)
except IOError as e:
if silent:
return False
return self.from_mapping(obj)

# MethodType 方法第一个参数是需要动态添加的方法名,第二个参数是类的实例对象。
app.config.from_yaml = types.MethodType(from_yaml, app.config)
app.config.from_yaml("config.yaml")

@app.route("/")
def index():
return "DEBUG %s SECRET_KEY %s" % (app.config.get("DEBUG"), app.config.get("SECRET_KEY"))

if __name__ == "__main__":
app.run()
print(app.config.items())

创建一个名为 config.properties 的简单配置文件,写入以下简单配置项。

1
2
DEBUG=True
SECRET_KEY=something

创建一个读取 from_properties 方法, 这个方法遍历 properties 文件的每一行,把 = 两边的属性名和属性值放进 obj 中,最终会调用现有的 from_mapping 方法,最后还是要利用 types.MethodType 方法为 Config 类动态添加方法,绑定在 config 对象上,这样这个方法才会起作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app = Flask(__name__)

def from_properties(self, filename, silent=False, encode=None):
filename = os.path.join(self.root_path, filename)
try:
with open(filename) as properties_file:
obj = {}
for line in properties_file:
if line.find('=') > 0:
s = line.replace('\n', '').split("=")
obj[s[0]] = s[1]
except IOError as e:
if silent:
return False
return self.from_mapping(obj)

app.config.from_properties = types.MethodType(from_properties, app.config)
app.config.from_properties("config.properties")

当然,这两个方法可能也有不完善的地方,例如,yaml 文件可能是多层级的,这里只考虑到一层级,什么时候下才会出现多层级的配置项,例如,可以在一个 yaml 文件里面设置多个环境配置,开发环境配置,生产环境配置,部署环境配置等。

  • yaml 方法修改。
  • 实例代码上传至 github

深入Flask配置
http://aim467.github.io/2020/07/10/深入Flask配置/
作者
Dedsec2z
发布于
2020年7月10日
更新于
2023年4月24日
许可协议