【flask系列】:flask中的配置文件

425 阅读7分钟

本文主要介绍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属性进行配置项的操作

如有讲解不清楚或者错误的地方,欢迎大家到评论区留言,让我们一起进步。