详细文档

配置数据路径语法

一种简便的配置数据访问路径表示方法, 在一些高级方法如 retrieve() 中使用

由三种语法组成

属性键

\. 开头,紧随的字符串组成

r"\.key1\.key2\.key3"

小技巧

如果路径字符串以 属性键 开头可以省略 \.

r"key1\.key2\.key3"

# 这将被视为

r"\.key1\.key2\.key3"

索引键

\[ 开头,紧随的数字与 \] 组成

r"\[0\]"

元信息

\{ 开头,紧随的字符串与 \} 组成

元信息会被附加到紧随的键上

r"\{meta 1\}\.key\{meta 2\}\[0\]"

# 这相当于

[AttrKey("key", meta="meta 1"), IndexKey(0, meta="meta 2")]

转义

键名或元信息中如有 \ 需要转义 为 \\

r"\{1\\2\}\.\\key\{2\\2\}\[0\]"

# 这将解析为

[AttrKey(r"\key", meta=r"1\2"), IndexKey(0, meta=r"2\2")]

可以简单的通过 str.replace("\\", "\\\\") 来转义

注意

如果没有转义,且 \ 后面的字符不是以上特殊转义字符,则会原样保留并发出警告

不应依赖此行为

requireConfig

用于加载并验证配置数据的高层方法

小技巧

提供参数 static_config 以获得更高性能

参见

RequiredPath

有手动调用和装饰器两种获取验证数据的方式

手动调用和装饰器
 1from c41811.config import MappingConfigData
 2from c41811.config import JsonSL
 3from c41811.config import requireConfig
 4
 5JsonSL().register_to()
 6
 7require = requireConfig(
 8    "", "config.json", {
 9        "config": "data",
10    },
11)
12
13# 调用check手动获取配置数据
14config: MappingConfigData = require.check()
15print(config)  # 打印:{'config': 'data'}
16
17
18# 使用装饰器自动注入配置数据
19@require
20def test(cfg):
21    print(cfg)  # 打印:{'config': 'data'}
22
23
24test()
25
26
27class Test:
28    @require
29    def __init__(self, cfg):
30        print(self, cfg)  # 打印:<__main__.Test object at 0x0000025B37D812E0> {'config': 'data'}
31
32    @classmethod
33    @require
34    def cls_func(cls, cfg):
35        print(cls, cfg)  # 打印:<class '__main__.Test'> {'config': 'data'}
36        return cls
37
38    @staticmethod
39    @require
40    def static_func(cfg):
41        print(cfg)  # 打印:{'config': 'data'}
42
43
44Test().cls_func().static_func()

Pydantic验证器工厂

提示

pydantic 验证器工厂不支持 skip_missing 选项 这是因为pydantic自带该功能 如果提供了该参数会产生一个警告 不会起到任何实际作用

validator_factory 参数设为 PYDANTIC"pydantic" 时使用该验证工厂

validator 参数为任意合法的 BaseModel

一个简单的pydantic验证器
 1from pydantic import BaseModel
 2from pydantic import Field
 3
 4from c41811.config import MappingConfigData
 5from c41811.config import ConfigFile
 6from c41811.config import JsonSL
 7from c41811.config import requireConfig
 8from c41811.config import save
 9
10JsonSL().register_to()
11
12save("", "test.json", config=ConfigFile(MappingConfigData({
13    "key": "value"
14})))
15
16
17class Config(BaseModel):
18    key: str = "default value"
19    unknown_key: dict = Field(default_factory=dict)
20
21
22print(requireConfig("", "test.json", Config, "pydantic").check())
23# 打印:{'key': 'value', 'unknown_key': {}}

默认验证器工厂

validator_factory 参数设为 DEFAULTNone 时使用该验证工厂

validator 参数可以为 Iterable[str]Mapping[str | ABCPath, Any]

Iterable 的元素或 Mapping 的键会被作为 配置数据路径语法 解析,如非特殊配置结果将一定包含这些 配置数据路径

备注

[path, path1, path2, ...] 与 {path: Any, path1: Any, path2: Any, ...} 等价

小技巧

如果validator同时包含路径和路径的父路径

例: r"\.first\.second\.third"r"\.first" 同时出现

这时 first 中不仅包含 second ,还会允许任意额外的键

 1from c41811.config import MappingConfigData
 2from c41811.config import ConfigFile
 3from c41811.config import JsonSL
 4from c41811.config import requireConfig
 5from c41811.config import save
 6
 7JsonSL().register_to()
 8
 9save("", "test.json", config=ConfigFile(MappingConfigData({
10    "first": {
11        "second": {
12            "third": 111,
13            "foo": 222
14        },
15        "bar": 333
16    },
17    "baz": 444
18})))
19
20print(requireConfig("", "test.json", ["first", "first\\.second\\.third"]).check())
21# 打印:{'first': {'second': {'third': 111}, 'bar': 333}}

Iterable[str]

一组需求的 配置数据路径 ,会检查路径存在与否,不会校验数据类型

 1from c41811.config import MappingConfigData
 2from c41811.config import ConfigFile
 3from c41811.config import JsonSL
 4from c41811.config import requireConfig
 5from c41811.config import save
 6
 7JsonSL().register_to()
 8
 9save("", "test.json", config=ConfigFile(MappingConfigData({
10    "foo": {
11        "bar": {
12            "baz": "value"
13        },
14        "bar1": "value1"
15    },
16    "foo1": "value2"
17})))
18
19print(requireConfig("", "test.json", ["foo", "foo\\.bar\\.baz", "foo1"]).check())
20# 打印:{'foo': {'bar': {'baz': 'value'}, 'bar1': 'value1'}, 'foo1': 'value2'}

Mapping[str | ABCPath, Any]

键为 配置数据路径 ,值为需求的数据类型,会检查路径存在与否,并校验数据类型

小技巧

r"first\.second\.third": int"first": {"second": {"third": int}} 等价

  • 允许混用路径与嵌套字典

路径与嵌套字典的等价操作
 1from c41811.config import MappingConfigData
 2from c41811.config import ConfigFile
 3from c41811.config import JsonSL
 4from c41811.config import requireConfig
 5from c41811.config import save
 6
 7JsonSL().register_to()
 8
 9save("", "test.json", config=ConfigFile(MappingConfigData({
10    "first": {
11        "second": {
12            "third": 111,
13            "foo": 222
14        },
15        "bar": 333
16    },
17    "baz": 444
18})))
19
20paths = requireConfig("", "test.json", {
21    r"first\.second\.third": int,
22    r"first\.bar": int,
23}).check()
24nested_dict = requireConfig("", "test.json", {
25    "first": {
26        "second": {
27            "third": int
28        },
29        "bar": int
30    }
31}).check()
32
33print(paths)  # 打印: {'first': {'second': {'third': 111}, 'bar': 333}}
34print(nested_dict)  # 打印: {'first': {'second': {'third': 111}, 'bar': 333}}
35print(paths == nested_dict)  # 打印: True

以下是两种验证器语法

两种验证器语法
 1from c41811.config import MappingConfigData
 2from c41811.config import ConfigFile
 3from c41811.config import JsonSL
 4from c41811.config import requireConfig
 5from c41811.config import save
 6
 7JsonSL().register_to()
 8
 9save("", "test.json", config=ConfigFile(MappingConfigData({
10    "first": {
11        "second": {
12            "third": 111,
13            "foo": 222
14        },
15        "bar": 333
16    },
17    "baz": 444
18})))
19
20# 使用路径字符串
21print(requireConfig("", "test.json", {
22    "first\\.second\\.third": int,
23    "first\\.bar": int,
24}).check())  # 打印:{'first': {'second': {'third': 111}, 'bar': 333}}
25
26# 使用嵌套字典
27print(requireConfig("", "test.json", {
28    "first": {
29        "second": {
30            "third": int
31        },
32        "bar": int
33    }
34}).check())  # 打印:{'first': {'second': {'third': 111}, 'bar': 333}}
35
36# 混搭
37print(requireConfig("", "test.json", {
38    "first": {
39        "second\\.third": int,
40        "second": dict,
41        "bar": int
42    },
43    "baz": int
44}).check())  # 打印: {'first': {'second': {'third': 111, 'foo': 222}, 'bar': 333}, 'baz': 444}

类型检查和填充默认值功能

类型检查和填充默认值
 1from typing import Sequence
 2
 3from c41811.config import MappingConfigData
 4from c41811.config import ConfigFile
 5from c41811.config import FieldDefinition
 6from c41811.config import JsonSL
 7from c41811.config import requireConfig
 8from c41811.config import save
 9from c41811.config.errors import ConfigDataTypeError
10
11JsonSL().register_to()
12
13save("", "test.json", config=ConfigFile(MappingConfigData({
14    "first": {
15        "second": {
16            "third": 111,
17            "foo": 222
18        },
19        "bar": 333
20    },
21    "baz": [444]
22})))
23
24# 类型检查,如果不满足会报错
25print(requireConfig("", "test.json", {
26    "first\\.second": dict[str, int],
27    "baz": list[int],
28}).check())  # 打印:{'first': {'second': {'third': 111, 'foo': 222}}, 'baz': [444]}
29
30try:
31    requireConfig("", "test.json", {
32        "first\\.second": dict[str, str]  # 类型不匹配
33    }).check()
34except ConfigDataTypeError as err:
35    print(err)  # 打印:\.first\.second\.third -> \.third (3 / 3) Must be '<class 'str'>', Not '<class 'int'>'
36
37try:
38    requireConfig("", "test.json", {
39        "baz": list[str]
40    }).check()
41except ConfigDataTypeError as err:
42    print(err)  # 打印:\.baz\[0\] -> \[0\] (2 / 2) Must be '<class 'str'>', Not '<class 'int'>'
43
44# 默认值,路径不存在时自动填充
45print(requireConfig("", "test.json", {
46    "first\\.second\\.third": 999,  # 因为路径已存在所以不会填充
47    "not\\.exists": 987
48}).check())  # 打印: {'first': {'second': {'third': 111}}, 'not': {'exists': 987}}
49
50# 在提供默认值的同时提供类型检查
51# 一般情况下用不着,因为会自动根据默认值的类型来设置类型检查
52# 一般在传入的默认值类型与类型检查的类型不同或规避特殊语法时使用
53print(requireConfig("", "test.json", {
54    "first\\.second\\.third": FieldDefinition(int, 999),
55    "not\\.exists": FieldDefinition(int, 987),
56    "baz": FieldDefinition(Sequence[int], [654]),
57}).check())  # 打印:{'first': {'second': {'third': 111}}, 'not': {'exists': 987}, 'baz': [444]}
58print(requireConfig("", "test.json", {
59    "first\\.second": FieldDefinition(dict, {"key": int}, allow_recursive=False),  # 并不会被递归处理,会被当作默认值处理
60    "not exists": FieldDefinition(dict, {"key": int}, allow_recursive=False),
61    "type": FieldDefinition(type, frozenset),
62}).check())
63# 打印:
64#  {'first': {'second': {'third': 111, 'foo': 222}}, 'not exists': {'key': <class 'int'>}, 'type': <class 'frozenset'>}
65
66# 含有非字符串键的子Mapping不会被递归处理
67print(requireConfig("", "test.json", {
68    "first\\.second": {"third": str, 3: 4},
69    # 效果等同于FieldDefinition(dict, {"third": str, 3: 4}, allow_recursive=False)
70    "not exists": {1: 2},
71}).check())  # 打印:{'first': {'second': {'third': 111, 'foo': 222}}, 'not exists': {'key': <class 'int'>}}

几个关键字参数

关键字参数
 1from c41811.config import MappingConfigData
 2from c41811.config import ConfigFile
 3from c41811.config import JsonSL
 4from c41811.config import requireConfig
 5from c41811.config import save
 6from c41811.config.errors import RequiredPathNotFoundError
 7
 8JsonSL().register_to()
 9
10raw_data = MappingConfigData({
11    "first": {
12        "second": {
13            "third": 111,
14            "foo": 222
15        },
16        "bar": 333
17    },
18    "baz": [444]
19})
20
21save("", "test.json", config=ConfigFile(raw_data))
22
23# allow_modify, 在填充默认值时将默认值填充到源数据
24requireConfig("", "test.json", {
25    "not\\.exists": 987
26}).check(allow_modify=False)
27
28# 未提供allow_modify参数时不会影响源数据
29print(raw_data.exists("not\\.exists"))  # 打印:False
30
31# ConfigRequirementDecorator.__init__将allow_modify默认值设为True
32requireConfig("", "test.json", {
33    "not\\.exists": 987
34}).check()
35
36print(raw_data.exists("not\\.exists"))  # 打印:True
37raw_data.delete("not\\.exists")
38
39# skip_missing, 在没提供默认值且键不存在时忽略
40try:
41    requireConfig("", "test.json", {
42        "not\\.exists": int
43    }).check()
44except RequiredPathNotFoundError as err:
45    print(err)  # 打印:\.not\.exists -> \.exists (2 / 2) Operate: Read
46
47data: MappingConfigData = requireConfig("", "test.json", {
48    "not\\.exists": int
49}).check(skip_missing=True)
50
51print(data.exists("not\\.exists"))  # 打印:False

自定义验证器

validator_factory 参数设为 CUSTOM"custom" 时采用该策略

这将直接把 validator 参数当作验证器 Callable[[Ref[D], ValidatorOptions], D] 来使用,如果 validatorNone 则验证器默认为 lambda ref:ref.value ,即无验证

一个修改所有值为"modified!"的验证器
 1from copy import deepcopy
 2from typing import Any
 3
 4from c41811.config import ConfigFile
 5from c41811.config import JsonSL
 6from c41811.config import MappingConfigData
 7from c41811.config import ValidatorOptions
 8from c41811.config import requireConfig
 9from c41811.config import save
10from c41811.config.utils import Ref
11
12
13def modify_value_validator[D: MappingConfigData[Any]](ref: Ref[D], cfg: ValidatorOptions) -> D:
14    data = deepcopy(ref.value)
15    for path in data.keys(recursive=True, end_point_only=True):
16        data.modify(path, "modified!")
17    if cfg.allow_modify:
18        ref.value = data
19    return data
20
21
22JsonSL().register_to()
23
24save("", "test.json", config=ConfigFile(MappingConfigData({
25    "key": "value"
26})))
27print(requireConfig("", "test.json", modify_value_validator, "custom").check())
28# 输出:{'key': 'modified!'}

组件验证工厂

validator_factory 参数设为 COMPONENT"component" 时使用该验证工厂

validator 参数为 Mapping[str | None, Any]

键为组件成员文件名,值为成员对应的验证器,组件成员文件名为None则为元配置信息验证器

危险

永远不应该尝试验证 NoneConfigData ,这将创建一个 parserNoneComponentMeta,如果你没有在 额外验证器选项 传入默认的 组件元数据验证器 这将可能导致(至少目前默认情况下会)无法将组件元配置同步到组件元信息,最终导致元信息和组件成员不匹配抛出错误

ConfigDataFactory

此类本身不提供任何实际配置数据操作,仅根据传入的参数类型从注册表中选择对应的类实例化并返回

注册表存储在 TYPES

传入的数据类型及其对应类

优先级从上倒下,取初始化参数的第一个参数的类型进行判断,未传入参数时取 None

备注

是的, ComponentConfigData 不在这里面,仅由 componentSLComponentValidatorFactory 创建

参见

具体原因与 组件验证工厂 所述大同小异

若希望作为类型提示请考虑下表

配置数据类型

描述

ABCConfigData

所有配置数据的抽象基类,仅提供了最基础的 freeze() from_data() 等方法

ABCIndexedConfigData

支持复杂嵌套数据的抽象基类,提供了 retrieve() modify() 等高级嵌套数据访问方法

BasicSingleConfigData

单文件配置数据的基类,提供的单文件配置数据的基本实现,如 data

NoneConfigData

无参数调用 ConfigDataFactory 的默认值,也是 initialize() 的默认返回值

初始化参数永远必须为 None 或压根不传,允许传参更大是为了兼容父类接口

MappingConfigData

最常见的配置数据类型,提供了 MutableMapping 的完整实现。

retrieve() 等高级方法当返回值为 MappingSequence 时, retrieve() 会返回 MappingConfigDataSequenceConfigData

SequenceConfigData

提供了 MutableSequence 的完整实现

retrieve() 等高级方法当返回值为 MappingSequence 时, retrieve() 会返回 MappingConfigDataSequenceConfigData

StringConfigData

字符串与字节串的配置数据

尚未完整实现 UserString 的接口

NumberConfigData

提供了 numbers.Integralnumbers.Real 的大部分实现

BoolConfigData

继承自 NumberConfigData ,提供了 bool 的实现

ComponentConfigData

组件配置数据由元信息与成员配置组成

元信息

存储了 元配置成员定义处理顺序解析器 几部分必须的值。

参见

ComponentMeta

元配置

元信息默认存储在 __meta__ 配置文件内,元配置就是 __meta__ 内的原始配置数据,文件名由 meta_file 指定

注意

原始配置数据结构完全由 解析器 定义,除非你能保证数据结构在你的预期内,否则不应该直接对其进行操作(不同 解析器 的实现可能使用完全不同的数据结构甚至数据类型!)

MappingConfigData 存储

成员定义

成员 文件名别名 ,及其 配置格式

文件名 应严格与 成员 一一对应

别名 可以在 处理顺序 中或 键元信息语法指定成员进行操作 中使用

配置格式 会在保存加载期间优先使用

处理顺序

retrieve() 等方法从成员的搜索顺序,参见下表:

顺序表

使用该表的方法

create

modify(), setdefault()

read

retrieve(), exists(), get(), setdefault()

update

modify()

delete

delete(), unset()

解析器

负责将 元配置元信息 以一定格式互相转换,由 parser 属性存储,可以在 ComponentSL() 中通过初始化方法的 meta_parser 参数指定

参见

默认实现: ComponentMetaParser 讲解: 元配置数据结构

成员

成员配置文件的配置数据,支持所有 ABCIndexedConfigData 的子类

键元信息语法指定成员进行操作

retrieve() 等方法支持使用 键元信息 指定成员进行操作

指定从成员member.json读取数据
comp_data.retrieve(r"\{member.json\}\.key")
# 如果有别名也可以使用别名
comp_data.retrieve(r"\{alies-member\}\.key")
# 如果成员为SequenceConfigData
comp_data.retrieve(r"\{member.json\}\[0\]")

具体来说,会读取 path[0].meta ,所以只有第一个键的元信息起到作用

EnvironmentConfigData

继承自 MappingConfigData ,内部维护了与初始化参数的键差异

参见

Difference

SL处理器

项目中的 SL 都是 SaveLoad 的缩写

配置格式

处理器

注册名

支持的文件后缀

简介

JSON

JsonSL

json

.json

基于内置 json 模块

HJSON

HJsonSL

hjson

.hjson

基于第三方库 hjson

Pickle

PickleSL

pickle

.pickle .pkl

基于内置 pickle 模块

YAML

PyYamlSL

yaml

.yaml .yml

基于第三方库 PyYAML

YAML

RuamelYamlSL

ruamel_yaml

.yaml .yml

基于第三方库 ruamel.yaml

TOML

RTomlSL

toml

.rtoml

基于第三方库 rtoml

TOML

TomlKitSL

tomlkit

.toml

基于第三方库 tomlkit

CBOR

CBOR2SL

cbor2

.cbor

基于第三方库 cbor2

Properties

JPropertiesSL

jproperties

.properties

基于第三方库 jproperties

Python

PythonSL

python

.py

基于 exec(),尝试保存会将配置数据作为 locals() 传入 ,建议与 plaintext 搭配使用

PythonLiteral

PythonLiteralSL

python_literal

.python_literal .pyl .py

基于 literal_eval()pformat()

PlainText

PlainTextSL

plaintext

.md .markdown .rst .txt

纯文本格式,支持额外参数 linesep: str 在保存时额外添加换行符, split_line: bool 加载时使用 readlines()remove_linesep: str 在加载时使用 str.removesuffix() 移除换行符

TarFile

TarFileSL

tarfile:$compression_shortname$

.tar .tar.$compression_shortname$ .tar.$compression_fullname$

基于内置 tarfile 模块

ZipFile

ZipFileSL

zipfile:$compression_shortname$-$compress_level$

.$compress_level$.zip .zip .$compress_level$.$compression_fullname$ .$compress_level$.$compression_shortname$ .$compression_shortname$ .$compression_fullname$

基于内置 zipfile 模块

Component

ComponentSL

component

.component .comp

组合多个 ABCIndexedConfigData 为一个 ComponentConfigData

OSEnv

OSEnvSL

os.environ

.os.env .os.environ

基于内置 os.environ

ComponentMetaParser

ComponentSL 的默认 元配置 解析器。

元配置数据结构

详解
{
    "members": [  # 声明组件成员,此处声明的文件名必须与提供的组件成员严格匹配
        "filename.json",  # 可以直接提供文件名
        {
            "filename": "name",  # 也可以通过键值对声明
            # 通过键值对声明可以附带额外信息
            "alias": "na",  # 此成员别名为"na",可被`键元信息`语法或下面的操作顺序使用
            "config_format": "ruamel_yaml",  # 此成员配置格式为ruamel.yaml的YAML解析器
        },
        {"filename": "my-member.pickle"},  # 当然上面这些额外信息是可选的
    ],
    # 操作顺序,并没有严格的检查,已经声明的组件成员可以不被使用但是禁止出现完全重复的名称
    "order": [  # 定义基础的操作顺序,若未提供则顺序敏感地使用members所声明的文件名(若存在则优先使用别名),可以提供一个空列表以禁用默认行为
        # "filename.json",  # 越靠前优先级越高
        # "na",  # 这里是允许别名的
        # 这里order为空以禁用默认行为(只要提供了此项就会禁用默认行为,无论是否完全为空列表或只填充了部分/全部成员)
    ],
    "orders": {  # order会被同步追加到orders的create/read/update/delete,所以orders优先级最高
        # 会简单的检查将要追加的名称是否已经在表中(如果是则跳过),这并不会同时检查文件名与别名是否同时存在
        "create": [],  # 禁止创建新的键,此项目控制setdefault方法在路径不存在时的写入顺序与modify无法替换一个现有的数据时来创建数据
        "read": ["filename.json", "my-member.pickle"],  # retrieve等方法仅按照此顺序读取配置数据
        "update": ["filename.json", "na"],  # 显然这是针对modify一类方法替换现有数据的
        "delete": ["filename.json", "my-member.pickle", "na"],  # delete,unset一类涉及删除路径的操作
        # 注意,当unset等方法最终得到的orders其中某项为空时(例如"delete": [])
        # 会抛出RequiredPathNotFoundError且未找到路径一定为根键
    },
}  # 显然,以上的顺序表都是允许完全为空(且不必包含全部成员)的,这将导致永远无法通过普通的顺序查找检索到未在顺序表中成员,但是仍然可以通过`键元信息`语法指定成员访问