python-library-configlib 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- configlib/__init__.py +38 -0
- configlib/json.py +22 -0
- configlib/loader.py +104 -0
- configlib/resolver.py +218 -0
- configlib/toml.py +25 -0
- configlib/yaml.py +70 -0
- python_library_configlib-0.1.0.dist-info/METADATA +8 -0
- python_library_configlib-0.1.0.dist-info/RECORD +9 -0
- python_library_configlib-0.1.0.dist-info/WHEEL +4 -0
configlib/__init__.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from .loader import ConfigLoader
|
|
4
|
+
from .json import is_json, load_json, load_json_raw
|
|
5
|
+
from .toml import is_toml, load_toml, load_toml_raw
|
|
6
|
+
from .yaml import is_yaml, load_yaml, load_yaml_raw
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_config_raw(file_path: str | Path) -> dict | list:
|
|
10
|
+
"""加载配置文件(不解析变量)"""
|
|
11
|
+
file_path = str(file_path)
|
|
12
|
+
if is_json(file_path):
|
|
13
|
+
return load_json_raw(file_path)
|
|
14
|
+
elif is_toml(file_path):
|
|
15
|
+
return load_toml_raw(file_path)
|
|
16
|
+
elif is_yaml(file_path):
|
|
17
|
+
return load_yaml_raw(file_path)
|
|
18
|
+
else:
|
|
19
|
+
raise ValueError(f"不支持的文件格式: {file_path}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_config(file_path: str | Path) -> dict | list:
|
|
23
|
+
"""加载配置文件"""
|
|
24
|
+
file_path = str(file_path)
|
|
25
|
+
if is_json(file_path):
|
|
26
|
+
return load_json(file_path)
|
|
27
|
+
elif is_toml(file_path):
|
|
28
|
+
return load_toml(file_path)
|
|
29
|
+
elif is_yaml(file_path):
|
|
30
|
+
return load_yaml(file_path)
|
|
31
|
+
else:
|
|
32
|
+
raise ValueError(f"不支持的文件格式: {file_path}")
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"load_config",
|
|
36
|
+
"load_config_raw",
|
|
37
|
+
"ConfigLoader",
|
|
38
|
+
]
|
configlib/json.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import json5
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from .resolver import resolve_variables
|
|
5
|
+
|
|
6
|
+
SUFFIXES = {'.json', '.json5'}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_json(file_path: str) -> bool:
|
|
10
|
+
return os.path.splitext(file_path)[1] in SUFFIXES
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_json(file_path: str) -> dict | list:
|
|
14
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
15
|
+
data = json5.load(f)
|
|
16
|
+
return resolve_variables(data)
|
|
17
|
+
|
|
18
|
+
def load_json_raw(file_path: str) -> dict | list:
|
|
19
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
20
|
+
return json5.load(f)
|
|
21
|
+
|
|
22
|
+
__all__ = ['is_json', 'load_json', 'load_json_raw']
|
configlib/loader.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import inspect
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Callable
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, PrivateAttr
|
|
6
|
+
from typing import TypeVar
|
|
7
|
+
T = TypeVar("T", bound="ConfigLoader")
|
|
8
|
+
|
|
9
|
+
ReloadCallback = Callable[..., None]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConfigLoader(BaseModel):
|
|
13
|
+
model_config = ConfigDict(validate_assignment=True)
|
|
14
|
+
|
|
15
|
+
_file_path: Path | None = PrivateAttr(default=None)
|
|
16
|
+
_on_update: ReloadCallback | None = PrivateAttr(default=None)
|
|
17
|
+
_file_state: tuple[int, int] | None = PrivateAttr(default=None)
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_file(
|
|
21
|
+
cls: type[T],
|
|
22
|
+
file_path: str | Path,
|
|
23
|
+
on_update: ReloadCallback | None = None,
|
|
24
|
+
) -> T:
|
|
25
|
+
path = Path(file_path).resolve()
|
|
26
|
+
if not path.exists():
|
|
27
|
+
raise FileNotFoundError(f"配置文件 {path} 不存在")
|
|
28
|
+
|
|
29
|
+
data = cls._load_dict(path)
|
|
30
|
+
|
|
31
|
+
obj = cls.model_validate(data)
|
|
32
|
+
obj._file_path = path
|
|
33
|
+
obj._on_update = on_update
|
|
34
|
+
obj._file_state = obj._get_file_state()
|
|
35
|
+
return obj
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def _load_dict(path: Path) -> dict:
|
|
39
|
+
from . import load_config
|
|
40
|
+
data = load_config(str(path))
|
|
41
|
+
if not isinstance(data, dict):
|
|
42
|
+
raise TypeError(f"配置顶层必须是 dict,实际得到 {type(data).__name__}")
|
|
43
|
+
return data
|
|
44
|
+
|
|
45
|
+
def _get_file_state(self) -> tuple[int, int]:
|
|
46
|
+
"""获取文件状态"""
|
|
47
|
+
if self._file_path is None:
|
|
48
|
+
raise RuntimeError("此实例未绑定配置文件,请使用 from_file() 创建")
|
|
49
|
+
stat = self._file_path.stat()
|
|
50
|
+
return (stat.st_mtime_ns, stat.st_size)
|
|
51
|
+
|
|
52
|
+
def has_changed(self) -> bool:
|
|
53
|
+
"""判断配置文件是否发生变化(纯查询,不更新内部状态)"""
|
|
54
|
+
if self._file_path is None:
|
|
55
|
+
return False
|
|
56
|
+
return self._file_state is None or self._file_state != self._get_file_state()
|
|
57
|
+
|
|
58
|
+
def reload(self) -> bool:
|
|
59
|
+
"""重新加载配置文件"""
|
|
60
|
+
if not self.has_changed():
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
data = self._load_dict(self._file_path)
|
|
64
|
+
|
|
65
|
+
old = self.model_copy(deep=True)
|
|
66
|
+
new_obj = self.__class__.model_validate(data)
|
|
67
|
+
|
|
68
|
+
for field_name in self.__class__.model_fields:
|
|
69
|
+
setattr(self, field_name, getattr(new_obj, field_name))
|
|
70
|
+
|
|
71
|
+
self._file_state = self._get_file_state()
|
|
72
|
+
self._call_update_callback(old)
|
|
73
|
+
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
def _call_update_callback(self, old: "ConfigLoader") -> None:
|
|
77
|
+
"""调用更新回调函数"""
|
|
78
|
+
if self._on_update is None:
|
|
79
|
+
return
|
|
80
|
+
callback = self._on_update
|
|
81
|
+
try:
|
|
82
|
+
sig = inspect.signature(callback)
|
|
83
|
+
except (ValueError, TypeError):
|
|
84
|
+
for args in [(self, old), (self,), ()]:
|
|
85
|
+
try:
|
|
86
|
+
callback(*args)
|
|
87
|
+
return
|
|
88
|
+
except TypeError:
|
|
89
|
+
continue
|
|
90
|
+
raise TypeError(f"无法调用回调函数 {callback!r},请检查其参数签名")
|
|
91
|
+
|
|
92
|
+
params = [
|
|
93
|
+
p for p in sig.parameters.values()
|
|
94
|
+
if p.default is inspect.Parameter.empty
|
|
95
|
+
and p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
|
|
96
|
+
]
|
|
97
|
+
if len(params) >= 2:
|
|
98
|
+
callback(self, old)
|
|
99
|
+
elif len(params) == 1:
|
|
100
|
+
callback(self)
|
|
101
|
+
else:
|
|
102
|
+
callback()
|
|
103
|
+
|
|
104
|
+
__all__ = ["ConfigLoader"]
|
configlib/resolver.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
_VAR_PATTERN = re.compile(r"\$\{([^{}]+)\}")
|
|
8
|
+
|
|
9
|
+
_SENTINEL = object()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def resolve_variables(data: Any) -> Any:
|
|
13
|
+
"""对已加载的配置数据进行 ${} 变量解析。"""
|
|
14
|
+
return _VariableResolver(data).resolve()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _auto_convert(value: str) -> str | int | float | bool | None:
|
|
18
|
+
"""将字符串自动转换为合适的 Python 类型。"""
|
|
19
|
+
lower = value.strip().lower()
|
|
20
|
+
if lower in ("true", "yes", "on"):
|
|
21
|
+
return True
|
|
22
|
+
if lower in ("false", "no", "off"):
|
|
23
|
+
return False
|
|
24
|
+
if lower in ("null", "none", "~"):
|
|
25
|
+
return None
|
|
26
|
+
try:
|
|
27
|
+
return int(value)
|
|
28
|
+
except ValueError:
|
|
29
|
+
pass
|
|
30
|
+
try:
|
|
31
|
+
return float(value)
|
|
32
|
+
except ValueError:
|
|
33
|
+
pass
|
|
34
|
+
return value
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _VariableResolver:
|
|
38
|
+
"""
|
|
39
|
+
解析三种变量语法:
|
|
40
|
+
- ${key.path} 绝对路径引用
|
|
41
|
+
- ${..key} 相对路径引用
|
|
42
|
+
- ${env:NAME} 显式环境变量
|
|
43
|
+
- ${env:NAME:默认值} 带默认值的环境变量
|
|
44
|
+
以及隐式回退:单级 ${KEY} 在配置中找不到时,自动查找同名环境变量。
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, root: Any):
|
|
48
|
+
self.root = root
|
|
49
|
+
self._cache: dict[tuple[Any, ...], Any] = {}
|
|
50
|
+
self._resolving: set[tuple[Any, ...]] = set()
|
|
51
|
+
|
|
52
|
+
def resolve(self) -> Any:
|
|
53
|
+
return self._resolve_at(())
|
|
54
|
+
|
|
55
|
+
def _resolve_at(self, path: tuple[Any, ...]) -> Any:
|
|
56
|
+
if path in self._cache:
|
|
57
|
+
return self._cache[path]
|
|
58
|
+
if path in self._resolving:
|
|
59
|
+
raise ValueError(f"检测到循环变量引用: {self._format_path(path)}")
|
|
60
|
+
|
|
61
|
+
self._resolving.add(path)
|
|
62
|
+
try:
|
|
63
|
+
node = self._get_node(path)
|
|
64
|
+
resolved = self._resolve_node(node, path)
|
|
65
|
+
self._cache[path] = resolved
|
|
66
|
+
return resolved
|
|
67
|
+
finally:
|
|
68
|
+
self._resolving.remove(path)
|
|
69
|
+
|
|
70
|
+
def _resolve_node(self, node: Any, path: tuple[Any, ...]) -> Any:
|
|
71
|
+
if isinstance(node, dict):
|
|
72
|
+
return {key: self._resolve_at(path + (key,)) for key in node}
|
|
73
|
+
if isinstance(node, list):
|
|
74
|
+
return [self._resolve_at(path + (index,)) for index, _ in enumerate(node)]
|
|
75
|
+
if isinstance(node, str):
|
|
76
|
+
return self._resolve_string(node, path)
|
|
77
|
+
return node
|
|
78
|
+
|
|
79
|
+
def _resolve_string(self, value: str, path: tuple[Any, ...]) -> Any:
|
|
80
|
+
full_match = _VAR_PATTERN.fullmatch(value)
|
|
81
|
+
if full_match:
|
|
82
|
+
expr = full_match.group(1)
|
|
83
|
+
env_result = self._try_resolve_env(expr)
|
|
84
|
+
if env_result is not _SENTINEL:
|
|
85
|
+
return env_result
|
|
86
|
+
target_path = self._resolve_reference(expr, path)
|
|
87
|
+
return self._resolve_at(target_path)
|
|
88
|
+
|
|
89
|
+
def replace(match: re.Match[str]) -> str:
|
|
90
|
+
expr = match.group(1)
|
|
91
|
+
env_result = self._try_resolve_env(expr)
|
|
92
|
+
if env_result is not _SENTINEL:
|
|
93
|
+
return str(env_result)
|
|
94
|
+
target_path = self._resolve_reference(expr, path)
|
|
95
|
+
return str(self._resolve_at(target_path))
|
|
96
|
+
|
|
97
|
+
return _VAR_PATTERN.sub(replace, value)
|
|
98
|
+
|
|
99
|
+
def _try_resolve_env(self, expr: str) -> Any:
|
|
100
|
+
"""解析 env:VAR 或 env:VAR:default。不匹配 env: 前缀则返回 _SENTINEL。"""
|
|
101
|
+
expr = expr.strip()
|
|
102
|
+
if not expr.startswith("env:"):
|
|
103
|
+
return _SENTINEL
|
|
104
|
+
|
|
105
|
+
rest = expr[4:]
|
|
106
|
+
colon_pos = rest.find(":")
|
|
107
|
+
if colon_pos == -1:
|
|
108
|
+
env_name = rest.strip()
|
|
109
|
+
default = _SENTINEL
|
|
110
|
+
else:
|
|
111
|
+
env_name = rest[:colon_pos].strip()
|
|
112
|
+
default = rest[colon_pos + 1:]
|
|
113
|
+
|
|
114
|
+
if not env_name:
|
|
115
|
+
raise ValueError("环境变量名不能为空: ${env:}")
|
|
116
|
+
|
|
117
|
+
value = os.environ.get(env_name)
|
|
118
|
+
if value is not None:
|
|
119
|
+
return _auto_convert(value)
|
|
120
|
+
if default is not _SENTINEL:
|
|
121
|
+
return _auto_convert(default)
|
|
122
|
+
raise KeyError(f"环境变量不存在: {env_name}")
|
|
123
|
+
|
|
124
|
+
# ---- 配置内引用 ----
|
|
125
|
+
|
|
126
|
+
def _resolve_reference(self, expr: str, current_path: tuple[Any, ...]) -> tuple[Any, ...]:
|
|
127
|
+
expr = expr.strip()
|
|
128
|
+
if not expr:
|
|
129
|
+
raise ValueError("变量表达式不能为空")
|
|
130
|
+
if expr.startswith("."):
|
|
131
|
+
return self._resolve_relative_path(expr, current_path)
|
|
132
|
+
return self._split_path(expr)
|
|
133
|
+
|
|
134
|
+
def _resolve_relative_path(
|
|
135
|
+
self,
|
|
136
|
+
expr: str,
|
|
137
|
+
current_path: tuple[Any, ...],
|
|
138
|
+
) -> tuple[Any, ...]:
|
|
139
|
+
dot_count = 0
|
|
140
|
+
while dot_count < len(expr) and expr[dot_count] == ".":
|
|
141
|
+
dot_count += 1
|
|
142
|
+
|
|
143
|
+
if dot_count < 2 or dot_count % 2 != 0:
|
|
144
|
+
raise ValueError(f"不支持的相对路径变量: {expr}")
|
|
145
|
+
|
|
146
|
+
remainder = expr[dot_count:]
|
|
147
|
+
if remainder.startswith(".") or not remainder:
|
|
148
|
+
raise ValueError(f"不支持的相对路径变量: {expr}")
|
|
149
|
+
|
|
150
|
+
up_levels = dot_count // 2
|
|
151
|
+
mapping_paths = self._get_mapping_paths(current_path)
|
|
152
|
+
if len(mapping_paths) < up_levels:
|
|
153
|
+
raise ValueError(f"相对路径越界: {expr},当前位置 {self._format_path(current_path)}")
|
|
154
|
+
base_path = mapping_paths[-up_levels]
|
|
155
|
+
return base_path + self._split_path(remainder)
|
|
156
|
+
|
|
157
|
+
def _get_mapping_paths(self, current_path: tuple[Any, ...]) -> list[tuple[Any, ...]]:
|
|
158
|
+
mapping_paths: list[tuple[Any, ...]] = []
|
|
159
|
+
node = self.root
|
|
160
|
+
walked: list[Any] = []
|
|
161
|
+
|
|
162
|
+
if isinstance(node, dict):
|
|
163
|
+
mapping_paths.append(())
|
|
164
|
+
|
|
165
|
+
for key in current_path[:-1]:
|
|
166
|
+
if isinstance(node, dict):
|
|
167
|
+
node = node[key]
|
|
168
|
+
elif isinstance(node, list):
|
|
169
|
+
node = node[key]
|
|
170
|
+
else:
|
|
171
|
+
break
|
|
172
|
+
walked.append(key)
|
|
173
|
+
if isinstance(node, dict):
|
|
174
|
+
mapping_paths.append(tuple(walked))
|
|
175
|
+
|
|
176
|
+
return mapping_paths
|
|
177
|
+
|
|
178
|
+
def _split_path(self, expr: str) -> tuple[Any, ...]:
|
|
179
|
+
parts = [part.strip() for part in expr.split(".")]
|
|
180
|
+
if any(not part for part in parts):
|
|
181
|
+
raise ValueError(f"无效的变量路径: {expr}")
|
|
182
|
+
|
|
183
|
+
result: list[Any] = []
|
|
184
|
+
for part in parts:
|
|
185
|
+
if part.isdigit():
|
|
186
|
+
result.append(int(part))
|
|
187
|
+
else:
|
|
188
|
+
result.append(part)
|
|
189
|
+
return tuple(result)
|
|
190
|
+
|
|
191
|
+
def _get_node(self, path: tuple[Any, ...]) -> Any:
|
|
192
|
+
node = self.root
|
|
193
|
+
for key in path:
|
|
194
|
+
try:
|
|
195
|
+
node = node[key]
|
|
196
|
+
except (KeyError, IndexError, TypeError):
|
|
197
|
+
if isinstance(key, int) and isinstance(node, dict):
|
|
198
|
+
try:
|
|
199
|
+
node = node[str(key)]
|
|
200
|
+
continue
|
|
201
|
+
except KeyError:
|
|
202
|
+
pass
|
|
203
|
+
if len(path) == 1 and isinstance(path[0], str):
|
|
204
|
+
env_val = os.environ.get(path[0])
|
|
205
|
+
if env_val is not None:
|
|
206
|
+
return _auto_convert(env_val)
|
|
207
|
+
raise KeyError(f"变量引用不存在: {self._format_path(path)}")
|
|
208
|
+
return node
|
|
209
|
+
|
|
210
|
+
def _format_path(self, path: tuple[Any, ...]) -> str:
|
|
211
|
+
if not path:
|
|
212
|
+
return "<root>"
|
|
213
|
+
return ".".join(str(part) for part in path)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
__all__ = [
|
|
217
|
+
"resolve_variables",
|
|
218
|
+
]
|
configlib/toml.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import os
|
|
2
|
+
try:
|
|
3
|
+
import tomllib
|
|
4
|
+
except ModuleNotFoundError:
|
|
5
|
+
import tomli as tomllib
|
|
6
|
+
|
|
7
|
+
from .resolver import resolve_variables
|
|
8
|
+
|
|
9
|
+
SUFFIXES = {'.toml'}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def is_toml(file_path: str) -> bool:
|
|
13
|
+
return os.path.splitext(file_path)[1] in SUFFIXES
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_toml(file_path: str) -> dict:
|
|
17
|
+
with open(file_path, "rb") as f:
|
|
18
|
+
data = tomllib.load(f)
|
|
19
|
+
return resolve_variables(data)
|
|
20
|
+
|
|
21
|
+
def load_toml_raw(file_path: str) -> dict:
|
|
22
|
+
with open(file_path, "rb") as f:
|
|
23
|
+
return tomllib.load(f)
|
|
24
|
+
|
|
25
|
+
__all__ = ['is_toml', 'load_toml', 'load_toml_raw']
|
configlib/yaml.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import os
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from contextvars import ContextVar
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
import yaml
|
|
8
|
+
from .resolver import resolve_variables
|
|
9
|
+
|
|
10
|
+
SUFFIXES = {".yaml", ".yml"}
|
|
11
|
+
_YAML_LOAD_STACK: ContextVar[list[Path] | None] = ContextVar("_YAML_LOAD_STACK", default=None)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@contextmanager
|
|
15
|
+
def _include_guard(path: Path):
|
|
16
|
+
"""管理 include 循环检测栈,顶层调用结束后复位。"""
|
|
17
|
+
stack = _YAML_LOAD_STACK.get()
|
|
18
|
+
is_top_level = stack is None
|
|
19
|
+
if is_top_level:
|
|
20
|
+
stack = []
|
|
21
|
+
_YAML_LOAD_STACK.set(stack)
|
|
22
|
+
|
|
23
|
+
if path in stack:
|
|
24
|
+
chain = " -> ".join(str(item) for item in [*stack, path])
|
|
25
|
+
raise ValueError(f"检测到循环 include: {chain}")
|
|
26
|
+
|
|
27
|
+
stack.append(path)
|
|
28
|
+
try:
|
|
29
|
+
yield
|
|
30
|
+
finally:
|
|
31
|
+
stack.pop()
|
|
32
|
+
if is_top_level:
|
|
33
|
+
_YAML_LOAD_STACK.set(None)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_yaml(file_path: str) -> bool:
|
|
37
|
+
return os.path.splitext(file_path)[1] in SUFFIXES
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_yaml(file_path: str) -> dict | list:
|
|
41
|
+
path = Path(file_path).resolve()
|
|
42
|
+
with _include_guard(path):
|
|
43
|
+
data = _load_yaml_raw(path)
|
|
44
|
+
return resolve_variables(data)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def load_yaml_raw(file_path: str) -> dict | list:
|
|
48
|
+
"""加载 YAML 但不解析变量(供 include 使用)"""
|
|
49
|
+
path = Path(file_path).resolve()
|
|
50
|
+
with _include_guard(path):
|
|
51
|
+
return _load_yaml_raw(path)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _load_yaml_raw(path: Path) -> Any:
|
|
55
|
+
class _YamlIncludeLoader(yaml.SafeLoader):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
def construct_include(loader: _YamlIncludeLoader, node: yaml.Node) -> Any:
|
|
59
|
+
relative_path = loader.construct_scalar(node)
|
|
60
|
+
target_path = (path.parent / relative_path).resolve()
|
|
61
|
+
from . import load_config_raw
|
|
62
|
+
return load_config_raw(str(target_path))
|
|
63
|
+
|
|
64
|
+
_YamlIncludeLoader.add_constructor("!include", construct_include)
|
|
65
|
+
|
|
66
|
+
with path.open("r", encoding="utf-8") as f:
|
|
67
|
+
return yaml.load(f, Loader=_YamlIncludeLoader)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = ["is_yaml", "load_yaml", "load_yaml_raw"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
configlib/__init__.py,sha256=nHRPxw4VSMahSttIrTI0r5yphgzKgk_mGZnPyckNh9s,1139
|
|
2
|
+
configlib/json.py,sha256=niF-J3FoHRsQKIZo4GUmkaEXpwLriRjF2Mpj1-F77rc,562
|
|
3
|
+
configlib/loader.py,sha256=IJX-50m_7WWa7vgDaiDA-8L8PbbiTlK09ImftCph2WY,3518
|
|
4
|
+
configlib/resolver.py,sha256=CoTOzgxvQvkAOS1qjHf3ZaLWZY7Yc2Y6iGfszX2KDrg,7524
|
|
5
|
+
configlib/toml.py,sha256=TwBvnLi055OqVd2qcSA8kh0VRx4MIA6DfhRHXX0LF_w,578
|
|
6
|
+
configlib/yaml.py,sha256=ngUjqAiL5kyoC3lciLX1TH5yIeNlAAWAU2lIdK0Dp8I,2092
|
|
7
|
+
python_library_configlib-0.1.0.dist-info/METADATA,sha256=XiN2pIWRZHs_ZHBGuYXmcsEfXAR2FTBgE8_odMBML-Y,233
|
|
8
|
+
python_library_configlib-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
9
|
+
python_library_configlib-0.1.0.dist-info/RECORD,,
|