python-library-configlib 0.1.0__tar.gz → 0.1.1__tar.gz
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.
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/.gitignore +3 -1
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/PKG-INFO +1 -1
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/configlib/yaml.py +6 -3
- python_library_configlib-0.1.1/configlib/yaml_compose.py +217 -0
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/pyproject.toml +1 -1
- python_library_configlib-0.1.1/tests/test_yaml_compose.py +206 -0
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/assets/base.yaml +0 -0
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/assets/data.json5 +0 -0
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/assets/data.toml +0 -0
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/assets/example.yaml +0 -0
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/configlib/__init__.py +0 -0
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/configlib/json.py +0 -0
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/configlib/loader.py +0 -0
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/configlib/resolver.py +0 -0
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/configlib/toml.py +0 -0
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/test.bat +0 -0
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/tests/test_json.py +0 -0
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/tests/test_load_config.py +0 -0
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/tests/test_loader.py +0 -0
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/tests/test_resolver.py +0 -0
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/tests/test_toml.py +0 -0
- {python_library_configlib-0.1.0 → python_library_configlib-0.1.1}/tests/test_yaml.py +0 -0
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
import yaml
|
|
8
8
|
from .resolver import resolve_variables
|
|
9
|
+
from .yaml_compose import apply_composition, preprocess_yaml_compose
|
|
9
10
|
|
|
10
11
|
SUFFIXES = {".yaml", ".yml"}
|
|
11
12
|
_YAML_LOAD_STACK: ContextVar[list[Path] | None] = ContextVar("_YAML_LOAD_STACK", default=None)
|
|
@@ -41,7 +42,8 @@ def load_yaml(file_path: str) -> dict | list:
|
|
|
41
42
|
path = Path(file_path).resolve()
|
|
42
43
|
with _include_guard(path):
|
|
43
44
|
data = _load_yaml_raw(path)
|
|
44
|
-
|
|
45
|
+
resolved = resolve_variables(data)
|
|
46
|
+
return apply_composition(resolved)
|
|
45
47
|
|
|
46
48
|
|
|
47
49
|
def load_yaml_raw(file_path: str) -> dict | list:
|
|
@@ -63,8 +65,9 @@ def _load_yaml_raw(path: Path) -> Any:
|
|
|
63
65
|
|
|
64
66
|
_YamlIncludeLoader.add_constructor("!include", construct_include)
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
text = path.read_text(encoding="utf-8")
|
|
69
|
+
text = preprocess_yaml_compose(text)
|
|
70
|
+
return yaml.load(text, Loader=_YamlIncludeLoader)
|
|
68
71
|
|
|
69
72
|
|
|
70
73
|
__all__ = ["is_yaml", "load_yaml", "load_yaml_raw"]
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Literal
|
|
6
|
+
|
|
7
|
+
SPREAD_KEY = "__configlib_spread__"
|
|
8
|
+
MERGES_KEY = "__configlib_merges__"
|
|
9
|
+
|
|
10
|
+
_BARE_VAR_LINE = re.compile(r"^(\s*)(\$\{[^{}]+\})\s*(#.*)?$")
|
|
11
|
+
_RESERVED_KEYS = frozenset({SPREAD_KEY, MERGES_KEY})
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def preprocess_yaml_compose(source: str) -> str:
|
|
15
|
+
"""将块式 YAML 中独占一行的 ${} 改写成可解析的占位结构。"""
|
|
16
|
+
if not source:
|
|
17
|
+
return source
|
|
18
|
+
lines = source.splitlines(keepends=True)
|
|
19
|
+
if not lines:
|
|
20
|
+
return source
|
|
21
|
+
|
|
22
|
+
infos = [_line_info(line) for line in lines]
|
|
23
|
+
out: list[str] = []
|
|
24
|
+
i = 0
|
|
25
|
+
while i < len(lines):
|
|
26
|
+
info = infos[i]
|
|
27
|
+
if not info.is_bare_var:
|
|
28
|
+
out.append(lines[i])
|
|
29
|
+
i += 1
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
parent_indent = _parent_indent(infos, i)
|
|
33
|
+
block_end = _block_end_index(infos, i, parent_indent)
|
|
34
|
+
block = infos[i:block_end]
|
|
35
|
+
siblings = _siblings_at_indent(infos, i, parent_indent)
|
|
36
|
+
mode = _infer_block_mode(siblings)
|
|
37
|
+
|
|
38
|
+
if mode == "seq":
|
|
39
|
+
for entry in block:
|
|
40
|
+
if entry.is_bare_var:
|
|
41
|
+
out.append(
|
|
42
|
+
f"{entry.indent_str}- {SPREAD_KEY}: {entry.bare_expr}{entry.comment_suffix}\n"
|
|
43
|
+
)
|
|
44
|
+
i = block_end
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
merge_exprs = [entry.bare_expr for entry in block if entry.is_bare_var]
|
|
48
|
+
if merge_exprs:
|
|
49
|
+
base_indent = block[0].indent_str
|
|
50
|
+
out.append(f"{base_indent}{MERGES_KEY}:\n")
|
|
51
|
+
for expr in merge_exprs:
|
|
52
|
+
out.append(f"{base_indent} - {expr}\n")
|
|
53
|
+
i = block_end
|
|
54
|
+
|
|
55
|
+
if source.endswith("\n") and out and not out[-1].endswith("\n"):
|
|
56
|
+
out[-1] = out[-1] + "\n"
|
|
57
|
+
return "".join(out)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def apply_composition(data: Any) -> Any:
|
|
61
|
+
"""在变量解析完成后展开列表拼接与字典合并占位。"""
|
|
62
|
+
if isinstance(data, dict):
|
|
63
|
+
return _compose_mapping(data)
|
|
64
|
+
if isinstance(data, list):
|
|
65
|
+
return _compose_sequence(data)
|
|
66
|
+
return data
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class _LineInfo:
|
|
70
|
+
__slots__ = (
|
|
71
|
+
"indent",
|
|
72
|
+
"indent_str",
|
|
73
|
+
"is_bare_var",
|
|
74
|
+
"bare_expr",
|
|
75
|
+
"comment_suffix",
|
|
76
|
+
"is_dash_item",
|
|
77
|
+
"is_mapping_entry",
|
|
78
|
+
"is_blank",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def __init__(self, line: str) -> None:
|
|
82
|
+
stripped = line.lstrip(" \t")
|
|
83
|
+
self.indent = len(line) - len(stripped)
|
|
84
|
+
self.indent_str = line[: self.indent]
|
|
85
|
+
self.is_blank = not stripped or stripped.startswith("#")
|
|
86
|
+
self.is_dash_item = stripped.startswith("- ")
|
|
87
|
+
self.is_mapping_entry = (
|
|
88
|
+
not self.is_blank
|
|
89
|
+
and not self.is_dash_item
|
|
90
|
+
and ":" in stripped
|
|
91
|
+
and not stripped.startswith("${")
|
|
92
|
+
)
|
|
93
|
+
match = _BARE_VAR_LINE.match(stripped)
|
|
94
|
+
self.is_bare_var = match is not None and not self.is_blank
|
|
95
|
+
self.bare_expr = match.group(2) if match else ""
|
|
96
|
+
self.comment_suffix = match.group(3) or "" if match else ""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _line_info(line: str) -> _LineInfo:
|
|
100
|
+
return _LineInfo(line)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _parent_indent(infos: list[_LineInfo], index: int) -> int:
|
|
104
|
+
indent = infos[index].indent
|
|
105
|
+
for pos in range(index - 1, -1, -1):
|
|
106
|
+
info = infos[pos]
|
|
107
|
+
if info.is_blank:
|
|
108
|
+
continue
|
|
109
|
+
if info.indent < indent:
|
|
110
|
+
return info.indent
|
|
111
|
+
return -1
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _siblings_at_indent(
|
|
115
|
+
infos: list[_LineInfo],
|
|
116
|
+
index: int,
|
|
117
|
+
parent_indent: int,
|
|
118
|
+
) -> list[_LineInfo]:
|
|
119
|
+
indent = infos[index].indent
|
|
120
|
+
siblings: list[_LineInfo] = []
|
|
121
|
+
for pos, info in enumerate(infos):
|
|
122
|
+
if info.is_blank:
|
|
123
|
+
continue
|
|
124
|
+
if info.indent != indent:
|
|
125
|
+
continue
|
|
126
|
+
if _parent_indent(infos, pos) != parent_indent:
|
|
127
|
+
continue
|
|
128
|
+
siblings.append(info)
|
|
129
|
+
return siblings
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _block_end_index(infos: list[_LineInfo], start: int, parent_indent: int) -> int:
|
|
133
|
+
indent = infos[start].indent
|
|
134
|
+
pos = start
|
|
135
|
+
while pos < len(infos):
|
|
136
|
+
info = infos[pos]
|
|
137
|
+
if not info.is_blank and info.indent < indent and pos > start:
|
|
138
|
+
break
|
|
139
|
+
if not info.is_blank and info.indent == indent:
|
|
140
|
+
if pos > start and not info.is_bare_var:
|
|
141
|
+
break
|
|
142
|
+
if not info.is_blank and info.indent > indent:
|
|
143
|
+
pos += 1
|
|
144
|
+
continue
|
|
145
|
+
pos += 1
|
|
146
|
+
return pos
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _infer_block_mode(block: list[_LineInfo]) -> Literal["seq", "map"]:
|
|
150
|
+
for info in block:
|
|
151
|
+
if info.is_dash_item:
|
|
152
|
+
return "seq"
|
|
153
|
+
if info.is_mapping_entry:
|
|
154
|
+
return "map"
|
|
155
|
+
return "map"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _deep_merge_dict(base: dict[str, Any], overlay: dict[str, Any]) -> dict[str, Any]:
|
|
159
|
+
"""递归合并 mapping;双方均为 dict 的同名键继续下钻,否则由 overlay 覆盖。"""
|
|
160
|
+
result = copy.deepcopy(base)
|
|
161
|
+
for key, value in overlay.items():
|
|
162
|
+
if (
|
|
163
|
+
key in result
|
|
164
|
+
and isinstance(result[key], dict)
|
|
165
|
+
and isinstance(value, dict)
|
|
166
|
+
):
|
|
167
|
+
result[key] = _deep_merge_dict(result[key], value)
|
|
168
|
+
else:
|
|
169
|
+
result[key] = copy.deepcopy(value)
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _compose_mapping(data: dict[str, Any]) -> dict[str, Any]:
|
|
174
|
+
merged: dict[str, Any] = {}
|
|
175
|
+
merge_sources: list[dict[str, Any]] = []
|
|
176
|
+
|
|
177
|
+
for key, value in data.items():
|
|
178
|
+
if key == MERGES_KEY:
|
|
179
|
+
resolved = _compose_sequence(value) if isinstance(value, list) else value
|
|
180
|
+
if not isinstance(resolved, list):
|
|
181
|
+
raise TypeError(f"{MERGES_KEY} 必须是列表")
|
|
182
|
+
for item in resolved:
|
|
183
|
+
if not isinstance(item, dict):
|
|
184
|
+
raise TypeError(f"{MERGES_KEY} 每一项必须是字典")
|
|
185
|
+
merge_sources.append(apply_composition(item))
|
|
186
|
+
continue
|
|
187
|
+
if key in _RESERVED_KEYS:
|
|
188
|
+
raise ValueError(f"保留键 {key!r} 只能由预处理生成")
|
|
189
|
+
merged[key] = apply_composition(value)
|
|
190
|
+
|
|
191
|
+
result: dict[str, Any] = {}
|
|
192
|
+
for source in merge_sources:
|
|
193
|
+
result = _deep_merge_dict(result, source)
|
|
194
|
+
return _deep_merge_dict(result, merged)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _compose_sequence(data: list[Any]) -> list[Any]:
|
|
198
|
+
result: list[Any] = []
|
|
199
|
+
for item in data:
|
|
200
|
+
if isinstance(item, dict) and SPREAD_KEY in item and len(item) == 1:
|
|
201
|
+
spread_value = apply_composition(item[SPREAD_KEY])
|
|
202
|
+
if not isinstance(spread_value, list):
|
|
203
|
+
raise TypeError(f"{SPREAD_KEY} 必须是列表")
|
|
204
|
+
result.extend(spread_value)
|
|
205
|
+
continue
|
|
206
|
+
if isinstance(item, dict) and SPREAD_KEY in item:
|
|
207
|
+
raise ValueError(f"{SPREAD_KEY} 不能与其他键混用")
|
|
208
|
+
result.append(apply_composition(item))
|
|
209
|
+
return result
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
__all__ = [
|
|
213
|
+
"SPREAD_KEY",
|
|
214
|
+
"MERGES_KEY",
|
|
215
|
+
"preprocess_yaml_compose",
|
|
216
|
+
"apply_composition",
|
|
217
|
+
]
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
import unittest
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from textwrap import dedent
|
|
7
|
+
|
|
8
|
+
from configlib import load_config, load_config_raw
|
|
9
|
+
from configlib.yaml_compose import apply_composition, preprocess_yaml_compose
|
|
10
|
+
from configlib.resolver import resolve_variables
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PreprocessTests(unittest.TestCase):
|
|
14
|
+
def test_list_spread_line_gets_dash_placeholder(self) -> None:
|
|
15
|
+
source = dedent("""
|
|
16
|
+
items:
|
|
17
|
+
- a
|
|
18
|
+
${extra}
|
|
19
|
+
- b
|
|
20
|
+
""")
|
|
21
|
+
out = preprocess_yaml_compose(source)
|
|
22
|
+
self.assertIn("- __configlib_spread__: ${extra}", out)
|
|
23
|
+
|
|
24
|
+
def test_dict_merge_lines_collapsed(self) -> None:
|
|
25
|
+
source = dedent("""
|
|
26
|
+
cfg:
|
|
27
|
+
${base}
|
|
28
|
+
${more}
|
|
29
|
+
flag: true
|
|
30
|
+
""")
|
|
31
|
+
out = preprocess_yaml_compose(source)
|
|
32
|
+
self.assertIn("__configlib_merges__:", out)
|
|
33
|
+
self.assertIn("- ${base}", out)
|
|
34
|
+
self.assertIn("- ${more}", out)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ComposeIntegrationTests(unittest.TestCase):
|
|
38
|
+
def test_list_spread_and_nested_item(self) -> None:
|
|
39
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
40
|
+
path = Path(tmp) / "cfg.yaml"
|
|
41
|
+
path.write_text(
|
|
42
|
+
dedent("""
|
|
43
|
+
shared: [s1, s2]
|
|
44
|
+
items:
|
|
45
|
+
- head
|
|
46
|
+
${shared}
|
|
47
|
+
- tail
|
|
48
|
+
"""),
|
|
49
|
+
encoding="utf-8",
|
|
50
|
+
)
|
|
51
|
+
data = load_config(path)
|
|
52
|
+
self.assertEqual(data["items"], ["head", "s1", "s2", "tail"])
|
|
53
|
+
|
|
54
|
+
def test_list_nested_variable_stays_one_element(self) -> None:
|
|
55
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
56
|
+
path = Path(tmp) / "cfg.yaml"
|
|
57
|
+
path.write_text(
|
|
58
|
+
dedent("""
|
|
59
|
+
shared: [1, 2]
|
|
60
|
+
items:
|
|
61
|
+
- ${shared}
|
|
62
|
+
"""),
|
|
63
|
+
encoding="utf-8",
|
|
64
|
+
)
|
|
65
|
+
data = load_config(path)
|
|
66
|
+
self.assertEqual(data["items"], [[1, 2]])
|
|
67
|
+
|
|
68
|
+
def test_dict_deep_merge_nested_override(self) -> None:
|
|
69
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
70
|
+
path = Path(tmp) / "cfg.yaml"
|
|
71
|
+
path.write_text(
|
|
72
|
+
dedent("""
|
|
73
|
+
base:
|
|
74
|
+
x2:
|
|
75
|
+
x3: v1
|
|
76
|
+
x4: v2
|
|
77
|
+
x5: v3
|
|
78
|
+
x1:
|
|
79
|
+
${base}
|
|
80
|
+
x2:
|
|
81
|
+
x4: vvvv
|
|
82
|
+
"""),
|
|
83
|
+
encoding="utf-8",
|
|
84
|
+
)
|
|
85
|
+
data = load_config(path)
|
|
86
|
+
self.assertEqual(
|
|
87
|
+
data["x1"],
|
|
88
|
+
{"x2": {"x3": "v1", "x4": "vvvv"}, "x5": "v3"},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def test_cfg_merge_base_then_nested_local_keys(self) -> None:
|
|
92
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
93
|
+
path = Path(tmp) / "cfg.yaml"
|
|
94
|
+
path.write_text(
|
|
95
|
+
dedent("""
|
|
96
|
+
base:
|
|
97
|
+
a: 1
|
|
98
|
+
b:
|
|
99
|
+
c: old
|
|
100
|
+
d: keep
|
|
101
|
+
cfg:
|
|
102
|
+
${base}
|
|
103
|
+
b:
|
|
104
|
+
c: v1
|
|
105
|
+
"""),
|
|
106
|
+
encoding="utf-8",
|
|
107
|
+
)
|
|
108
|
+
data = load_config(path)
|
|
109
|
+
self.assertEqual(
|
|
110
|
+
data["cfg"],
|
|
111
|
+
{"a": 1, "b": {"c": "v1", "d": "keep"}},
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def test_include_then_deep_merge_via_variable(self) -> None:
|
|
115
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
116
|
+
root = Path(tmp)
|
|
117
|
+
(root / "a.yaml").write_text(
|
|
118
|
+
dedent("""
|
|
119
|
+
x1:
|
|
120
|
+
x2:
|
|
121
|
+
x3: v1
|
|
122
|
+
x4: v2
|
|
123
|
+
x5: v3
|
|
124
|
+
"""),
|
|
125
|
+
encoding="utf-8",
|
|
126
|
+
)
|
|
127
|
+
(root / "b.yaml").write_text(
|
|
128
|
+
dedent("""
|
|
129
|
+
from_a: !include a.yaml
|
|
130
|
+
x1:
|
|
131
|
+
${from_a.x1}
|
|
132
|
+
x2:
|
|
133
|
+
x4: vvvv
|
|
134
|
+
"""),
|
|
135
|
+
encoding="utf-8",
|
|
136
|
+
)
|
|
137
|
+
data = load_config(root / "b.yaml")
|
|
138
|
+
self.assertEqual(
|
|
139
|
+
data["x1"],
|
|
140
|
+
{"x2": {"x3": "v1", "x4": "vvvv"}, "x5": "v3"},
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def test_dict_merge_then_local_override(self) -> None:
|
|
144
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
145
|
+
path = Path(tmp) / "cfg.yaml"
|
|
146
|
+
path.write_text(
|
|
147
|
+
dedent("""
|
|
148
|
+
base:
|
|
149
|
+
a: 1
|
|
150
|
+
b: old
|
|
151
|
+
cfg:
|
|
152
|
+
${base}
|
|
153
|
+
b: new
|
|
154
|
+
c: 3
|
|
155
|
+
"""),
|
|
156
|
+
encoding="utf-8",
|
|
157
|
+
)
|
|
158
|
+
data = load_config(path)
|
|
159
|
+
self.assertEqual(data["cfg"], {"a": 1, "b": "new", "c": 3})
|
|
160
|
+
|
|
161
|
+
def test_dict_nested_key_keeps_subtree(self) -> None:
|
|
162
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
163
|
+
path = Path(tmp) / "cfg.yaml"
|
|
164
|
+
path.write_text(
|
|
165
|
+
dedent("""
|
|
166
|
+
base:
|
|
167
|
+
x: 1
|
|
168
|
+
root:
|
|
169
|
+
nested: ${base}
|
|
170
|
+
"""),
|
|
171
|
+
encoding="utf-8",
|
|
172
|
+
)
|
|
173
|
+
data = load_config(path)
|
|
174
|
+
self.assertEqual(data["root"]["nested"], {"x": 1})
|
|
175
|
+
|
|
176
|
+
def test_raw_keeps_placeholders_without_resolve(self) -> None:
|
|
177
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
178
|
+
path = Path(tmp) / "cfg.yaml"
|
|
179
|
+
path.write_text(
|
|
180
|
+
dedent("""
|
|
181
|
+
items:
|
|
182
|
+
- head
|
|
183
|
+
${shared}
|
|
184
|
+
"""),
|
|
185
|
+
encoding="utf-8",
|
|
186
|
+
)
|
|
187
|
+
raw = load_config_raw(path)
|
|
188
|
+
self.assertIn("__configlib_spread__", str(raw))
|
|
189
|
+
|
|
190
|
+
def test_spread_non_list_raises(self) -> None:
|
|
191
|
+
tree = resolve_variables({"n": 1, "items": [{"__configlib_spread__": "${n}"}]})
|
|
192
|
+
with self.assertRaises(TypeError):
|
|
193
|
+
apply_composition(tree)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class ComposeUnitTests(unittest.TestCase):
|
|
197
|
+
def test_apply_list_spread(self) -> None:
|
|
198
|
+
data = [{"__configlib_spread__": [1, 2]}, 3]
|
|
199
|
+
self.assertEqual(apply_composition(data), [1, 2, 3])
|
|
200
|
+
|
|
201
|
+
def test_apply_dict_merges(self) -> None:
|
|
202
|
+
data = {
|
|
203
|
+
"__configlib_merges__": [{"a": 1}, {"b": 2}],
|
|
204
|
+
"b": 9,
|
|
205
|
+
}
|
|
206
|
+
self.assertEqual(apply_composition(data), {"a": 1, "b": 9})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|