confarg 0.0.1.dev2__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.
- confarg-0.0.1.dev2/PKG-INFO +9 -0
- confarg-0.0.1.dev2/pyproject.toml +35 -0
- confarg-0.0.1.dev2/src/confarg/__init__.py +440 -0
- confarg-0.0.1.dev2/src/confarg/_argparse.py +958 -0
- confarg-0.0.1.dev2/src/confarg/_callable.py +593 -0
- confarg-0.0.1.dev2/src/confarg/_completion.py +318 -0
- confarg-0.0.1.dev2/src/confarg/_defaults.py +15 -0
- confarg-0.0.1.dev2/src/confarg/_errors.py +85 -0
- confarg-0.0.1.dev2/src/confarg/_files.py +426 -0
- confarg-0.0.1.dev2/src/confarg/_merge.py +284 -0
- confarg-0.0.1.dev2/src/confarg/_parse_cli.py +507 -0
- confarg-0.0.1.dev2/src/confarg/_parse_env.py +279 -0
- confarg-0.0.1.dev2/src/confarg/_serialize.py +206 -0
- confarg-0.0.1.dev2/src/confarg/_types.py +614 -0
- confarg-0.0.1.dev2/src/confarg/dictexpr/__init__.py +34 -0
- confarg-0.0.1.dev2/src/confarg/dictexpr/_expressions.py +566 -0
- confarg-0.0.1.dev2/src/confarg/typedload/__init__.py +44 -0
- confarg-0.0.1.dev2/src/confarg/typedload/_coerce.py +178 -0
- confarg-0.0.1.dev2/src/confarg/typedload/_construct.py +685 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: confarg
|
|
3
|
+
Version: 0.0.1.dev2
|
|
4
|
+
Summary: A tool to manage complex, dynamic configurations.
|
|
5
|
+
Author: confarg
|
|
6
|
+
Author-email: confarg <280620574+confarg@users.noreply.github.com>
|
|
7
|
+
Requires-Dist: argcomplete>=3.0 ; extra == 'completion'
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Provides-Extra: completion
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
authors = [
|
|
3
|
+
{ name = "confarg", email = "280620574+confarg@users.noreply.github.com" },
|
|
4
|
+
]
|
|
5
|
+
dependencies = []
|
|
6
|
+
description = "A tool to manage complex, dynamic configurations."
|
|
7
|
+
name = "confarg"
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
version = "0.0.1.dev2"
|
|
10
|
+
|
|
11
|
+
[project.optional-dependencies]
|
|
12
|
+
completion = ["argcomplete>=3.0"]
|
|
13
|
+
|
|
14
|
+
[dependency-groups]
|
|
15
|
+
dev = [
|
|
16
|
+
"click>=8.3.3",
|
|
17
|
+
"cyclopts>=4.11.2",
|
|
18
|
+
"hypothesis>=6.100",
|
|
19
|
+
"ipython>=9.12.0",
|
|
20
|
+
"jsonargparse>=4.48.0",
|
|
21
|
+
"mkdocstrings-python>=2.0.3",
|
|
22
|
+
"mkdocstrings>=1.0.4",
|
|
23
|
+
"pre-commit>=4.5.1",
|
|
24
|
+
"pytest-cov>=7.0.0",
|
|
25
|
+
"pytest-markdown-docs>=0.9.2",
|
|
26
|
+
"pytest>=8.0",
|
|
27
|
+
"pyyaml>=6.0",
|
|
28
|
+
"tomli-w>=1.0",
|
|
29
|
+
"typer>=0.25.1",
|
|
30
|
+
"zensical>=0.0.41",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
build-backend = "uv_build"
|
|
35
|
+
requires = ["uv_build>=0.11.13,<0.12.0"]
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
"""confarg — read configuration from CLI args, env vars, and config files into dataclasses."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from collections.abc import Mapping, Sequence
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from confarg import _defaults
|
|
16
|
+
from confarg._argparse import FieldMeta, from_namespace, populate_parser
|
|
17
|
+
from confarg._completion import setup_completion
|
|
18
|
+
from confarg._errors import (
|
|
19
|
+
AmbiguousUnionError,
|
|
20
|
+
CircularReferenceError,
|
|
21
|
+
ConfargError,
|
|
22
|
+
ConfargWarning,
|
|
23
|
+
ExpressionEvalError,
|
|
24
|
+
InvalidConfigFileError,
|
|
25
|
+
MissingFieldError,
|
|
26
|
+
MissingReferenceError,
|
|
27
|
+
TypeCoercionError,
|
|
28
|
+
UnknownArgumentError,
|
|
29
|
+
UnsafeExpressionError,
|
|
30
|
+
)
|
|
31
|
+
from confarg._files import INCLUDE_KEY, _dump_file, _load_file, _load_file_item
|
|
32
|
+
from confarg._merge import LIST_APPEND_KEY, _deep_merge
|
|
33
|
+
from confarg._parse_cli import _parse_cli
|
|
34
|
+
from confarg._parse_env import _parse_env
|
|
35
|
+
from confarg._serialize import _serialize
|
|
36
|
+
from confarg._types import _MISSING, TagPolicy, _is_dc, _is_struct, _is_struct_like, _resolve_type
|
|
37
|
+
from confarg.dictexpr import resolve_expressions
|
|
38
|
+
from confarg.typedload import construct as _tc
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def merge(
|
|
42
|
+
target: type,
|
|
43
|
+
*,
|
|
44
|
+
args: Sequence[str] | None = None,
|
|
45
|
+
env: Mapping[str, str] | None = None,
|
|
46
|
+
env_prefix: str | None = _defaults.ENV_PREFIX,
|
|
47
|
+
env_separator: str = "__",
|
|
48
|
+
cli_prefix: str = "",
|
|
49
|
+
config_flag: str = "config",
|
|
50
|
+
files: Sequence[str | Path] = (),
|
|
51
|
+
env_config: str | None = None,
|
|
52
|
+
union_tag: str = "class",
|
|
53
|
+
) -> dict[str, Any]:
|
|
54
|
+
"""Collect and merge configuration from all sources into a raw dict.
|
|
55
|
+
|
|
56
|
+
Sources are merged in priority order: config files (lowest), then
|
|
57
|
+
environment variables, then CLI arguments (highest). No expression
|
|
58
|
+
resolution and no dataclass construction are performed — the returned
|
|
59
|
+
dict reflects the config input exactly as written, with ${...}
|
|
60
|
+
expression strings preserved.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
target: The dataclass type (or scalar type) used to guide CLI parsing.
|
|
64
|
+
args: CLI arguments to parse. Defaults to sys.argv[1:].
|
|
65
|
+
env: Environment variable mapping to scan. Defaults to os.environ.
|
|
66
|
+
env_prefix: Prefix that env vars must start with. Defaults to ``None``,
|
|
67
|
+
which disables environment variable parsing entirely. Set to ``""``
|
|
68
|
+
to read all env vars without filtering, or to e.g. ``"MYAPP_"`` to
|
|
69
|
+
read only vars with that prefix.
|
|
70
|
+
env_separator: Separator used to split env var names into nested keys.
|
|
71
|
+
cli_prefix: Required prefix for CLI flags.
|
|
72
|
+
config_flag: The flag name used to specify config files on the CLI.
|
|
73
|
+
files: Paths to config files to load.
|
|
74
|
+
env_config: Name of an env var whose value is a config file path to load.
|
|
75
|
+
Loaded after ``files`` but before CLI ``--config`` files.
|
|
76
|
+
union_tag: The field name used as a discriminator tag in unions.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
A plain dict of the merged configuration, with expression strings intact.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
InvalidConfigFileError: If a config file cannot be loaded.
|
|
83
|
+
UnknownArgumentError: If an unrecognized CLI argument is encountered.
|
|
84
|
+
"""
|
|
85
|
+
if args is None:
|
|
86
|
+
args = sys.argv[1:]
|
|
87
|
+
if env is None:
|
|
88
|
+
env = os.environ
|
|
89
|
+
|
|
90
|
+
# 1. Parse CLI
|
|
91
|
+
cli_data, cli_configs = _parse_cli(args, target, cli_prefix, config_flag, union_tag)
|
|
92
|
+
|
|
93
|
+
# 2. Parse env vars (done here so env-specified config files are loaded in order)
|
|
94
|
+
if env_prefix is None:
|
|
95
|
+
env_data: dict[str, Any] = {}
|
|
96
|
+
env_configs: list[tuple[str, Path]] = []
|
|
97
|
+
else:
|
|
98
|
+
# Exclude the env_config key so it is not mistakenly treated as a field.
|
|
99
|
+
env_for_fields = {k: v for k, v in env.items() if k != env_config} if env_config else env
|
|
100
|
+
env_data, env_configs = _parse_env(env_for_fields, env_prefix, env_separator, target, config_flag)
|
|
101
|
+
|
|
102
|
+
# 3. Load config files in priority order (all become config-level, below inline env/CLI)
|
|
103
|
+
config_data: dict[str, Any] = {}
|
|
104
|
+
for f in files:
|
|
105
|
+
config_data = _deep_merge(config_data, _load_file(Path(f)), union_tag=union_tag)
|
|
106
|
+
if env_config is not None:
|
|
107
|
+
env_config_path = env.get(env_config)
|
|
108
|
+
if env_config_path:
|
|
109
|
+
config_data = _deep_merge(config_data, _load_file(Path(env_config_path)), union_tag=union_tag)
|
|
110
|
+
for subpath, fpath in env_configs:
|
|
111
|
+
fdata: dict[str, Any] = _load_file(fpath)
|
|
112
|
+
if subpath:
|
|
113
|
+
for part in reversed(subpath.split(".")):
|
|
114
|
+
fdata = {part: fdata}
|
|
115
|
+
config_data = _deep_merge(config_data, fdata, union_tag=union_tag)
|
|
116
|
+
for subpath, fpath in cli_configs:
|
|
117
|
+
if subpath.endswith("+"):
|
|
118
|
+
# Append mode: --config.foo.bar+ file → subpath = "foo.bar+"
|
|
119
|
+
real_subpath = subpath[:-1].rstrip(".")
|
|
120
|
+
if not real_subpath:
|
|
121
|
+
raise ConfargError(
|
|
122
|
+
f"--{config_flag}+ requires a field path. Use --{config_flag}.fieldname+ /path/to/file."
|
|
123
|
+
)
|
|
124
|
+
last_key = real_subpath.rsplit(".", 1)[-1]
|
|
125
|
+
fitem = _load_file_item(fpath)
|
|
126
|
+
# Disambiguation (see convention):
|
|
127
|
+
# - top-level list → the element to append IS the list
|
|
128
|
+
# - dict with exactly one key == last_key whose value is a list
|
|
129
|
+
# → those list items are appended individually
|
|
130
|
+
# - anything else → appended as a single element
|
|
131
|
+
if isinstance(fitem, list):
|
|
132
|
+
append_items: list[Any] = [fitem]
|
|
133
|
+
elif (
|
|
134
|
+
isinstance(fitem, dict) and len(fitem) == 1 and last_key in fitem and isinstance(fitem[last_key], list)
|
|
135
|
+
):
|
|
136
|
+
append_items = fitem[last_key]
|
|
137
|
+
else:
|
|
138
|
+
append_items = [fitem]
|
|
139
|
+
fdata: dict[str, Any] = {LIST_APPEND_KEY: append_items}
|
|
140
|
+
for part in reversed(real_subpath.split(".")):
|
|
141
|
+
fdata = {part: fdata}
|
|
142
|
+
else:
|
|
143
|
+
fdata = _load_file(fpath)
|
|
144
|
+
if subpath:
|
|
145
|
+
for part in reversed(subpath.split(".")):
|
|
146
|
+
fdata = {part: fdata}
|
|
147
|
+
config_data = _deep_merge(config_data, fdata, union_tag=union_tag)
|
|
148
|
+
|
|
149
|
+
# 4. Merge: config (lowest) → env → CLI (highest)
|
|
150
|
+
merged = _deep_merge(config_data, env_data, union_tag=union_tag)
|
|
151
|
+
merged = _deep_merge(merged, cli_data, union_tag=union_tag)
|
|
152
|
+
|
|
153
|
+
return merged
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def from_dict[T](
|
|
157
|
+
target: type[T],
|
|
158
|
+
data: dict[str, Any],
|
|
159
|
+
*,
|
|
160
|
+
union_tag: str = "class",
|
|
161
|
+
) -> T:
|
|
162
|
+
"""Construct a dataclass instance from a plain config dict.
|
|
163
|
+
|
|
164
|
+
Resolves ${...} expressions then constructs the target type. Use this as
|
|
165
|
+
the second step after merge(), or to load configuration from a dict you
|
|
166
|
+
have assembled yourself.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
target: The dataclass type (or scalar type) to construct.
|
|
170
|
+
data: The raw config dict (e.g. the output of merge()).
|
|
171
|
+
union_tag: The field name used as a discriminator tag in unions.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
An instance of the target type.
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
MissingFieldError: If a required field is not provided.
|
|
178
|
+
TypeCoercionError: If a value cannot be coerced to the target type.
|
|
179
|
+
AmbiguousUnionError: If a Union cannot be disambiguated.
|
|
180
|
+
CircularReferenceError: If expression references form a cycle.
|
|
181
|
+
UnsafeExpressionError: If an expression contains disallowed constructs.
|
|
182
|
+
MissingReferenceError: If an expression references a field that does not exist.
|
|
183
|
+
ExpressionEvalError: If an expression fails at runtime.
|
|
184
|
+
"""
|
|
185
|
+
target_r = _resolve_type(target)
|
|
186
|
+
is_dataclass = _is_struct_like(target_r)
|
|
187
|
+
|
|
188
|
+
resolved = resolve_expressions(data)
|
|
189
|
+
|
|
190
|
+
if not is_dataclass:
|
|
191
|
+
raw = resolved.get("__root__", _MISSING)
|
|
192
|
+
if raw is _MISSING:
|
|
193
|
+
raise MissingFieldError(
|
|
194
|
+
f"No value provided for target type {target_r!r}."
|
|
195
|
+
" Provide a value via CLI flag (--<prefix> <value>), environment variable, or config file."
|
|
196
|
+
)
|
|
197
|
+
return _tc(target_r, raw, union_tag=union_tag) # type: ignore[return-value]
|
|
198
|
+
|
|
199
|
+
return _tc(target_r, resolved, union_tag=union_tag) # type: ignore[return-value]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def interpolate(data: dict[str, Any]) -> dict[str, Any]:
|
|
203
|
+
"""Resolve ${...} expressions in a merged config dict.
|
|
204
|
+
|
|
205
|
+
This is the first half of from_dict(). Call it to get the fully-resolved
|
|
206
|
+
dict before passing it to construct() or inspecting values.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
data: A plain config dict, e.g. the output of merge().
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
A new dict with all ${...} expression strings replaced by their values.
|
|
213
|
+
|
|
214
|
+
Raises:
|
|
215
|
+
CircularReferenceError: If expression references form a cycle.
|
|
216
|
+
UnsafeExpressionError: If an expression contains disallowed constructs.
|
|
217
|
+
MissingReferenceError: If an expression references a field that does not exist.
|
|
218
|
+
ExpressionEvalError: If an expression fails at runtime.
|
|
219
|
+
"""
|
|
220
|
+
return resolve_expressions(data)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def construct[T](
|
|
224
|
+
target: type[T],
|
|
225
|
+
data: dict[str, Any],
|
|
226
|
+
*,
|
|
227
|
+
union_tag: str = "class",
|
|
228
|
+
) -> T:
|
|
229
|
+
"""Construct a typed object from an already-interpolated config dict.
|
|
230
|
+
|
|
231
|
+
This is the second half of from_dict(). Unlike from_dict(), it does NOT
|
|
232
|
+
resolve ${...} expressions — call interpolate() first if needed.
|
|
233
|
+
|
|
234
|
+
Use this together with interpolate() when you want to keep the interpolated
|
|
235
|
+
dict around (e.g. to dump it with dump_file()):
|
|
236
|
+
|
|
237
|
+
raw = confarg.merge(MyConfig, ...)
|
|
238
|
+
resolved = confarg.interpolate(raw)
|
|
239
|
+
confarg.dump_file(resolved, "out.yaml") # serialize the dict
|
|
240
|
+
cfg = confarg.construct(MyConfig, resolved) # build the typed object
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
target: The dataclass or plain-class type to construct.
|
|
244
|
+
data: An interpolated config dict (output of interpolate() or merge()).
|
|
245
|
+
union_tag: The field name used as a discriminator tag in unions.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
An instance of the target type.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
MissingFieldError: If a required field is not provided.
|
|
252
|
+
TypeCoercionError: If a value cannot be coerced to the target type.
|
|
253
|
+
AmbiguousUnionError: If a Union cannot be disambiguated.
|
|
254
|
+
"""
|
|
255
|
+
target_r = _resolve_type(target)
|
|
256
|
+
return _tc(target_r, data, union_tag=union_tag) # type: ignore[return-value]
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def load[T](
|
|
260
|
+
target: type[T],
|
|
261
|
+
*,
|
|
262
|
+
args: Sequence[str] | None = None,
|
|
263
|
+
env: Mapping[str, str] | None = None,
|
|
264
|
+
env_prefix: str | None = _defaults.ENV_PREFIX,
|
|
265
|
+
env_separator: str = "__",
|
|
266
|
+
cli_prefix: str = "",
|
|
267
|
+
config_flag: str = "config",
|
|
268
|
+
files: Sequence[str | Path] = (),
|
|
269
|
+
env_config: str | None = None,
|
|
270
|
+
union_tag: str = "class",
|
|
271
|
+
) -> T:
|
|
272
|
+
"""Load configuration into the target type from CLI args, env vars, and config files.
|
|
273
|
+
|
|
274
|
+
Sources are merged in priority order: config files (lowest), then
|
|
275
|
+
environment variables, then CLI arguments (highest).
|
|
276
|
+
|
|
277
|
+
This is a convenience wrapper around merge() + from_dict(). For more
|
|
278
|
+
control — e.g. to inspect or save the raw merged dict before construction —
|
|
279
|
+
call those two functions directly.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
target: The dataclass type (or scalar type) to load configuration into.
|
|
283
|
+
args: CLI arguments to parse. Defaults to sys.argv[1:].
|
|
284
|
+
env: Environment variable mapping to scan. Defaults to os.environ.
|
|
285
|
+
env_prefix: Prefix that env vars must start with. Defaults to ``None``,
|
|
286
|
+
which disables environment variable parsing entirely. Set to ``""``
|
|
287
|
+
to read all env vars without filtering, or to e.g. ``"MYAPP_"`` to
|
|
288
|
+
read only vars with that prefix.
|
|
289
|
+
env_separator: Separator used to split env var names into nested keys.
|
|
290
|
+
cli_prefix: Required prefix for CLI flags.
|
|
291
|
+
config_flag: The flag name used to specify config files on the CLI.
|
|
292
|
+
files: Paths to config files to load.
|
|
293
|
+
env_config: Name of an env var whose value is a config file path to load.
|
|
294
|
+
Loaded after ``files`` but before CLI ``--config`` files.
|
|
295
|
+
union_tag: The field name used as a discriminator tag in unions.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
An instance of the target type populated with the merged configuration.
|
|
299
|
+
|
|
300
|
+
Raises:
|
|
301
|
+
MissingFieldError: If a required field is not provided by any source.
|
|
302
|
+
TypeCoercionError: If a value cannot be coerced to the target type.
|
|
303
|
+
InvalidConfigFileError: If a config file cannot be loaded.
|
|
304
|
+
UnknownArgumentError: If an unrecognized CLI argument is encountered.
|
|
305
|
+
AmbiguousUnionError: If a Union cannot be disambiguated.
|
|
306
|
+
CircularReferenceError: If expression references form a cycle.
|
|
307
|
+
UnsafeExpressionError: If an expression contains disallowed constructs.
|
|
308
|
+
MissingReferenceError: If an expression references a field that does not exist.
|
|
309
|
+
ExpressionEvalError: If an expression fails at runtime.
|
|
310
|
+
"""
|
|
311
|
+
data = merge(
|
|
312
|
+
target,
|
|
313
|
+
args=args,
|
|
314
|
+
env=env,
|
|
315
|
+
env_prefix=env_prefix,
|
|
316
|
+
env_separator=env_separator,
|
|
317
|
+
cli_prefix=cli_prefix,
|
|
318
|
+
config_flag=config_flag,
|
|
319
|
+
files=files,
|
|
320
|
+
env_config=env_config,
|
|
321
|
+
union_tag=union_tag,
|
|
322
|
+
)
|
|
323
|
+
return from_dict(target, data, union_tag=union_tag)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _strip_str_tokens(value: Any) -> Any:
|
|
327
|
+
"""Recursively convert _StrToken instances to plain str for serialization."""
|
|
328
|
+
from confarg._types import _StrToken as _ST
|
|
329
|
+
|
|
330
|
+
if type(value) is _ST:
|
|
331
|
+
return str(value)
|
|
332
|
+
if isinstance(value, dict):
|
|
333
|
+
return {k: _strip_str_tokens(v) for k, v in value.items()}
|
|
334
|
+
if isinstance(value, list):
|
|
335
|
+
return [_strip_str_tokens(v) for v in value]
|
|
336
|
+
return value
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def dump(
|
|
340
|
+
value: Any,
|
|
341
|
+
*,
|
|
342
|
+
union_tag: str = "class",
|
|
343
|
+
tag_policy: TagPolicy = "auto",
|
|
344
|
+
) -> dict[str, Any]:
|
|
345
|
+
"""Serialize to a plain dict.
|
|
346
|
+
|
|
347
|
+
Dispatches on the value type:
|
|
348
|
+
|
|
349
|
+
- **Dataclass instance**: serializes to a config-compatible dict.
|
|
350
|
+
``union_tag`` and ``tag_policy`` apply.
|
|
351
|
+
- **Raw dict** (e.g. from ``merge()``): normalizes internal tokens to plain
|
|
352
|
+
``str``. ``union_tag`` and ``tag_policy`` are ignored.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
value: A dataclass instance or a raw config dict.
|
|
356
|
+
union_tag: The field name used as a discriminator tag in unions.
|
|
357
|
+
tag_policy: "auto" (tag only when needed) or "always" (tag every union DC).
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
A plain dict representation.
|
|
361
|
+
|
|
362
|
+
Raises:
|
|
363
|
+
TypeError: If value is not a dataclass instance or a dict.
|
|
364
|
+
"""
|
|
365
|
+
if isinstance(value, dict):
|
|
366
|
+
return _strip_str_tokens(value)
|
|
367
|
+
if isinstance(value, type) or not _is_dc(type(value)):
|
|
368
|
+
tp_name = type(value).__name__
|
|
369
|
+
if _is_struct(type(value)):
|
|
370
|
+
raise TypeError(
|
|
371
|
+
f"dump() only supports dataclass instances, not plain classes.\n"
|
|
372
|
+
f"{tp_name} is a plain class — keep the merged dict and dump that instead:\n"
|
|
373
|
+
f" raw = confarg.merge(...)\n"
|
|
374
|
+
f" confarg.dump_file(raw, path)"
|
|
375
|
+
)
|
|
376
|
+
raise TypeError(f"Expected a dataclass instance or dict, got {tp_name}")
|
|
377
|
+
tp = type(value)
|
|
378
|
+
return _serialize(tp, value, "", union_tag, tag_policy)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def dump_file(
|
|
382
|
+
value: Any,
|
|
383
|
+
path: str | Path,
|
|
384
|
+
*,
|
|
385
|
+
union_tag: str = "class",
|
|
386
|
+
tag_policy: TagPolicy = "auto",
|
|
387
|
+
) -> None:
|
|
388
|
+
"""Write to a config file.
|
|
389
|
+
|
|
390
|
+
Accepts dataclass instances or raw config dicts — see ``dump()`` for
|
|
391
|
+
dispatch behaviour. The output format is determined by the file extension
|
|
392
|
+
(.toml, .yaml, .yml, .json).
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
value: A dataclass instance or a raw config dict.
|
|
396
|
+
path: Path to the output file.
|
|
397
|
+
union_tag: The field name used as a discriminator tag in unions.
|
|
398
|
+
tag_policy: "auto" or "always".
|
|
399
|
+
|
|
400
|
+
Raises:
|
|
401
|
+
TypeError: If value is not a dataclass instance or a dict.
|
|
402
|
+
InvalidConfigFileError: If the format is unsupported or the required library is not installed.
|
|
403
|
+
"""
|
|
404
|
+
_dump_file(dump(value, union_tag=union_tag, tag_policy=tag_policy), Path(path))
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
__all__ = [
|
|
408
|
+
# Two-step API
|
|
409
|
+
"merge",
|
|
410
|
+
"from_dict",
|
|
411
|
+
# Three-step API (dict-centric)
|
|
412
|
+
"interpolate",
|
|
413
|
+
"construct",
|
|
414
|
+
# One-step convenience
|
|
415
|
+
"load",
|
|
416
|
+
# Dump
|
|
417
|
+
"dump",
|
|
418
|
+
"dump_file",
|
|
419
|
+
# Types
|
|
420
|
+
"TagPolicy",
|
|
421
|
+
# argparse helpers
|
|
422
|
+
"populate_parser",
|
|
423
|
+
"from_namespace",
|
|
424
|
+
"FieldMeta",
|
|
425
|
+
"setup_completion",
|
|
426
|
+
# Errors / warnings
|
|
427
|
+
"ConfargError",
|
|
428
|
+
"ConfargWarning",
|
|
429
|
+
"MissingFieldError",
|
|
430
|
+
"TypeCoercionError",
|
|
431
|
+
"InvalidConfigFileError",
|
|
432
|
+
"UnknownArgumentError",
|
|
433
|
+
"AmbiguousUnionError",
|
|
434
|
+
"CircularReferenceError",
|
|
435
|
+
"MissingReferenceError",
|
|
436
|
+
"UnsafeExpressionError",
|
|
437
|
+
"ExpressionEvalError",
|
|
438
|
+
# Constants
|
|
439
|
+
"INCLUDE_KEY",
|
|
440
|
+
]
|