本文主要介绍flask中配置是如何实现的,加载配置文件的方式以及 flask中ConfigAttribute类的使用,旨在帮助不熟悉flask配置的小伙伴能够快速的加载配置。
一、flask中配置的实现
flask中的配置在其内部是通过config属性实现的,该属性通过调用make_config(instance_relative_config)方法返回Config对象。
1.1 root_path属性
root_path属性表示的是flask项目的根路径,可以手动传参,也可以根据 import_name属性自动获取。
# root_path的部分源码:
if root_path is None:
root_path = get_root_path(self.import_name)
self.root_path = root_path
# 根据import_name参数,获取包路径
def get_root_path(import_name: str) -> str:
# Module already imported and has a file attribute. Use that first.
mod = sys.modules.get(import_name)
if mod is not None and hasattr(mod, "__file__") and mod.__file__ is not None:
return os.path.dirname(os.path.abspath(mod.__file__))
# Next attempt: check the loader.
try:
spec = importlib.util.find_spec(import_name)
if spec is None:
raise ValueError
except (ImportError, ValueError):
loader = None
else:
loader = spec.loader
if loader is None:
return os.getcwd()
if hasattr(loader, "get_filename"):
filepath = loader.get_filename(import_name)
else:
# Fall back to imports.
__import__(import_name)
mod = sys.modules[import_name]
filepath = getattr(mod, "__file__", None)
if filepath is None:
raise RuntimeError(
"No root path can be found for the provided module"
f" {import_name!r}. This can happen because the module"
" came from an import hook that does not provide file"
" name information or because it's a namespace package."
" In this case the root path needs to be explicitly"
" provided."
)
return os.path.dirname(os.path.abspath(filepath)) # type: ignore[no-any-return]
1.2 instance_path属性
instance_path属性字面意思是实例路径,可以手动传参(必须是绝对路径),也可以根据 import_name属性自动获取实例路径。根据包路径所在的目录,判断配置文件的根目录。如果包路径是在系统目录(sys.prefix) 或者在 虚拟环境中,配置文件的根路径就是这个目录( prefix/var/包名称-instance );如果不在系统目录或者虚拟环境中,配置文件的根路径就是包路径下面的instance文件夹。
# 如果instance_path参数为空, 那么就会调用auto_find_instance_path(),自动获取instance_path值
if instance_path is None:
instance_path = self.auto_find_instance_path()
elif not os.path.isabs(instance_path): # 判断传入的instance_path参数或者自动加载返回的instance_path值是否是绝对路径
raise ValueError("If an instance path is provided it must be absolute."
" A relative path was given instead.")
self.instance_path = instance_path # 将传入的instance_path参数或者自动加载返回的
def auto_find_instance_path(self) -> str:
prefix, package_path = find_package(self.import_name) # 根据传入的import_name参数找到包路径
if prefix is None:
return os.path.join(package_path, "instance") # 如果没有前缀,则返回 包路径下面的instance文件夹中寻找
return os.path.join(prefix, "var", f"{self.name}-instance") # 如果有前缀,则在这个路径下(prefix/var/包名称-instance )寻找
1.3 make_config方法
make_config(instance_relative: bool)方法返回实例化后的Config对象,这个方法中,需要用到 root_path属性、instance_path属性,如果 instance_relative参数 (也就是instance_relative_config参数)为False,配置文件的根路径就是root_path属性,否则配置文件的根路径就是instance_path属性。配置文件的根路径就会作为Config类的一个参数,当使用from_envvar方法 、from_file方法或者from_pyfile方法加载配置时, 该参数就会和文件名进行路径拼接。
下列代码是flask部分源码, 只保留与本文主旨相关的部分代码。源码位置: flask\sansio\app.py
class App(Scaffold):
config_class = Config
testing = ConfigAttribute[bool]("TESTING")
default_config: dict[str, t.Any]
response_class: type[Response]
def __init__(
self,
import_name: str,
static_url_path: str | None = None,
static_folder: str | os.PathLike[str] | None = "static",
static_host: str | None = None,
host_matching: bool = False,
subdomain_matching: bool = False,
template_folder: str | os.PathLike[str] | None = "templates",
instance_path: str | None = None,
instance_relative_config: bool = False,
root_path: str | None = None,
):
super().__init__(
import_name=import_name,
static_folder=static_folder,
static_url_path=static_url_path,
template_folder=template_folder,
root_path=root_path,
)
self.config = self.make_config(instance_relative_config) # 通过make_config() 返回config类,instance_relative参数就是实例化flask对象时,传递的instance_relative_config参数
def make_config(self, instance_relative: bool = False) -> Config:
root_path = self.root_path # 将root_path属性值赋给 root_path变量
if instance_relative: # 如果instance_relative_config参数为True
root_path = self.instance_path # 将instance_path 属性值赋给root_path变量
defaults = dict(self.default_config)
defaults["DEBUG"] = get_debug_flag()
return self.config_class(root_path, defaults)
从flask源码中可以看出:配置项的根路径可以是flask项目的根路径,也可以是自定义的绝对路径。所以 import_name、 instance_path、instance_relative_config这三个参数的作用也就不言而喻。
- import_name参数:用于确定flask项目的根路径
- instance_path参数:用于指定配置文件的根路径
- instance_relative_config参数:用于配置文件加载是否在flask项目根路径下面的指定文件夹下进行加载(instance文件夹或者 prefix/var/包名称-instance )
二、flask中加载配置文件的方式
在flask中配置是如何实现的这一节中,提到 config属性就是通过 Config类实例化得到的,配置文件根路径作为参数传递给Config类,Config类继承dict类,在项目中 可以通过 config[属性名] = 属性值的方式,对配置项进行添加或者修改。
config对象源码,源码位置为:flask/config.py
class Config(dict): # type: ignore[type-arg]
def __init__(
self,
root_path: str | os.PathLike[str], # 这里的root_path参数就是传进来的 配置文件根路径
defaults: dict[str, t.Any] | None = None,
) -> None:
super().__init__(defaults or {})
self.root_path = root_path
def from_envvar(self, variable_name: str, silent: bool = False) -> bool:
rv = os.environ.get(variable_name)
if not rv:
if silent:
return False
raise RuntimeError(
f"The environment variable {variable_name!r} is not set"
" and as such configuration could not be loaded. Set"
" this variable and make it point to a configuration"
" file"
)
return self.from_pyfile(rv, silent=silent)
def from_prefixed_env(
self, prefix: str = "FLASK", *, loads: t.Callable[[str], t.Any] = json.loads
) -> bool:
prefix = f"{prefix}_"
len_prefix = len(prefix)
for key in sorted(os.environ):
if not key.startswith(prefix):
continue
value = os.environ[key]
try:
value = loads(value)
except Exception:
# Keep the value as a string if loading failed.
pass
# Change to key.removeprefix(prefix) on Python >= 3.9.
key = key[len_prefix:]
if "__" not in key:
# A non-nested key, set directly.
self[key] = value
continue
# Traverse nested dictionaries with keys separated by "__".
current = self
*parts, tail = key.split("__")
for part in parts:
# If an intermediate dict does not exist, create it.
if part not in current:
current[part] = {}
current = current[part]
current[tail] = value
return True
def from_pyfile(
self, filename: str | os.PathLike[str], silent: bool = False
) -> bool:
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 OSError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR, errno.ENOTDIR):
return False
e.strerror = f"Unable to load configuration file ({e.strerror})"
raise
self.from_object(d)
return True
def from_object(self, obj: object | str) -> None:
if isinstance(obj, str):
obj = import_string(obj)
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)
def from_file(
self,
filename: str | os.PathLike[str],
load: t.Callable[[t.IO[t.Any]], t.Mapping[str, t.Any]],
silent: bool = False,
text: bool = True,
) -> bool:
filename = os.path.join(self.root_path, filename)
try:
with open(filename, "r" if text else "rb") as f:
obj = load(f)
except OSError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
return False
e.strerror = f"Unable to load configuration file ({e.strerror})"
raise
return self.from_mapping(obj)
def from_mapping(
self, mapping: t.Mapping[str, t.Any] | None = None, **kwargs: t.Any
) -> bool:
mappings: dict[str, t.Any] = {}
if mapping is not None:
mappings.update(mapping)
mappings.update(kwargs)
for key, value in mappings.items():
if key.isupper():
self[key] = value
return True
flask中针对不同类型的配置文件提供了多种加载配置文件的方式,分为两类:
1.从文件中加载配置,这一类需要使用配置文件的根路径(os.path.join(self.root_path, filename)),
- from_envvar(variable_name: str, silent):表示从环境变量中获取配置文件信息,并将配置文件信息加载到flask项目中,这里的环境变量存放的是配置文件路径。
- from_prefixed_env(self, prefix, loads):表示从环境变量中获取指定前缀变量名对应的属性值,并默认通过json.load() 来加载到flask项目中,这里的环境变量存放的是具体的配置项。
- from_pyfile(filename, silent):表示从python文件中加载配置项,这个方法是 from_file()的一个特例。
- from_file(filename, load, silent,text):表示从文件中加载配置项,需要指定能够加载指定文件类型的加载器
2.从对象中加载配置。
- from_object(obj: object | str):表示从对象中加载配置项。
- from_mapping(mapping,**kwargs):表示从映射对象中加载配置项
三、ConfigAttribute类的使用
flask中,有一部分内置属性是通过 ConfigAttribute类进行定义,这些内置属性也可以通过配置文件进行配置。这些属性在使用时,可以直接调用属性名(obj.属性名),可以通过 app.config[属性名] 的方式调用。ConfigAttribute类中定义的__set__、__get__方法,其内部都是通过 config属性进行操作
内置配置项:
testing = ConfigAttribute[bool]("TESTING") # 使用了python中的泛型表示方式
secret_key = ConfigAttribute[t.Union[str, bytes, None]]("SECRET_KEY")
permanent_session_lifetime = ConfigAttribute[timedelta](
"PERMANENT_SESSION_LIFETIME",
get_converter=_make_timedelta, # 定义的转换器,将配置文件中参数值进行转换
)
ConfigAttribute类:
class ConfigAttribute(t.Generic[T]):
"""Makes an attribute forward to the config"""
def __init__(
self, name: str, get_converter: t.Callable[[t.Any], T] | None = None
) -> None:
self.__name__ = name
self.get_converter = get_converter
def __get__(self, obj: App | None, owner: type[App] | None = None) -> T | te.Self:
if obj is None:
return self
rv = obj.config[self.__name__] # 通过config属性进行配置项的操作
if self.get_converter is not None:
rv = self.get_converter(rv)
return rv
def __set__(self, obj: App, value: t.Any) -> None:
obj.config[self.__name__] = value # 通过config属性进行配置项的操作
如有讲解不清楚或者错误的地方,欢迎大家到评论区留言,让我们一起进步。