monoconf 0.1.0__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.
- monoconf-0.1.0/.gitignore +13 -0
- monoconf-0.1.0/CHANGELOG.md +37 -0
- monoconf-0.1.0/LICENSE +21 -0
- monoconf-0.1.0/PKG-INFO +389 -0
- monoconf-0.1.0/PLAN.md +1227 -0
- monoconf-0.1.0/README.md +360 -0
- monoconf-0.1.0/examples/full.py +64 -0
- monoconf-0.1.0/examples/minimal.py +19 -0
- monoconf-0.1.0/examples/nested.py +43 -0
- monoconf-0.1.0/pyproject.toml +91 -0
- monoconf-0.1.0/src/monoconf/__init__.py +46 -0
- monoconf-0.1.0/src/monoconf/config.py +281 -0
- monoconf-0.1.0/src/monoconf/errors.py +285 -0
- monoconf-0.1.0/src/monoconf/fields.py +171 -0
- monoconf-0.1.0/src/monoconf/merge.py +151 -0
- monoconf-0.1.0/src/monoconf/py.typed +0 -0
- monoconf-0.1.0/src/monoconf/schema.py +643 -0
- monoconf-0.1.0/src/monoconf/settings.py +196 -0
- monoconf-0.1.0/src/monoconf/sources/__init__.py +72 -0
- monoconf-0.1.0/src/monoconf/sources/cli.py +435 -0
- monoconf-0.1.0/src/monoconf/sources/defaults.py +30 -0
- monoconf-0.1.0/src/monoconf/sources/env.py +95 -0
- monoconf-0.1.0/src/monoconf/sources/files.py +422 -0
- monoconf-0.1.0/src/monoconf/toml.py +195 -0
- monoconf-0.1.0/tests/conftest.py +61 -0
- monoconf-0.1.0/tests/snapshots/help.txt +31 -0
- monoconf-0.1.0/tests/snapshots/show_config.txt +15 -0
- monoconf-0.1.0/tests/snapshots/validation_errors.txt +15 -0
- monoconf-0.1.0/tests/test_chain.py +237 -0
- monoconf-0.1.0/tests/test_config.py +97 -0
- monoconf-0.1.0/tests/test_defaults.py +73 -0
- monoconf-0.1.0/tests/test_discovery.py +156 -0
- monoconf-0.1.0/tests/test_env.py +263 -0
- monoconf-0.1.0/tests/test_errors.py +244 -0
- monoconf-0.1.0/tests/test_examples.py +129 -0
- monoconf-0.1.0/tests/test_fields.py +149 -0
- monoconf-0.1.0/tests/test_files.py +440 -0
- monoconf-0.1.0/tests/test_help.py +221 -0
- monoconf-0.1.0/tests/test_merge.py +205 -0
- monoconf-0.1.0/tests/test_override.py +234 -0
- monoconf-0.1.0/tests/test_package.py +29 -0
- monoconf-0.1.0/tests/test_pydantic_assumptions.py +49 -0
- monoconf-0.1.0/tests/test_readme.py +74 -0
- monoconf-0.1.0/tests/test_resolve.py +278 -0
- monoconf-0.1.0/tests/test_schema.py +304 -0
- monoconf-0.1.0/tests/test_schema_errors.py +356 -0
- monoconf-0.1.0/tests/test_settings.py +160 -0
- monoconf-0.1.0/tests/test_show_config.py +196 -0
- monoconf-0.1.0/tests/test_snapshots.py +159 -0
- monoconf-0.1.0/tests/test_toml.py +136 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
User-visible changes. The design behind them is in PLAN.md; new design decisions
|
|
4
|
+
get a dated amendment there.
|
|
5
|
+
|
|
6
|
+
## Unreleased
|
|
7
|
+
|
|
8
|
+
- Nothing yet.
|
|
9
|
+
|
|
10
|
+
## 0.1.0
|
|
11
|
+
|
|
12
|
+
The first release: one pydantic model, one command line, one environment reader,
|
|
13
|
+
one layered TOML loader, one precedence order.
|
|
14
|
+
|
|
15
|
+
- `Config` with `load()` (CLI semantics: print and exit) and `resolve()` (raise
|
|
16
|
+
instead), and a `settings()` table of thirteen options.
|
|
17
|
+
- The fixed eight-layer precedence: defaults, system file and drop-ins, user file
|
|
18
|
+
and drop-ins, `--config-override`, environment, command line. POSIX/XDG and
|
|
19
|
+
Windows default paths.
|
|
20
|
+
- Required fields are positional and CLI-only; everything else is a flag, a
|
|
21
|
+
variable and a file key. Booleans are presence-only; lists are repeated flags
|
|
22
|
+
and replace wholesale.
|
|
23
|
+
- Schema errors at import, including the string-constructibility check.
|
|
24
|
+
- Provenance for every value: `cfg.origin_of("database.host")`.
|
|
25
|
+
- `--help` showing the defaults resolved on this machine; `--show-config` printing
|
|
26
|
+
the resolved configuration as round-trippable TOML with provenance comments.
|
|
27
|
+
- A mistyped flag is answered with a suggestion, as an unknown file key is:
|
|
28
|
+
`unrecognized arguments: --retires 5 (did you mean '--retries'?)`.
|
|
29
|
+
- Every bad value is reported in one run, rewritten for operators and naming its
|
|
30
|
+
source: a value pydantic rejects, and a variable monoconf cannot read at all,
|
|
31
|
+
such as `MYAPP_VERBOSE=maybe`, both raise `ValidationFailed`. `SourceError` is a
|
|
32
|
+
command line that cannot be parsed.
|
|
33
|
+
- A secret field's value is never printed: `--help` and `--show-config` show its
|
|
34
|
+
mask, and an error calls it `the secret` rather than quoting it.
|
|
35
|
+
- Warnings: `UnknownConfigKey`, `IgnoredValue`, `UnreadableSource`.
|
|
36
|
+
- Requires Python 3.11+ and pydantic 2.8+. Tested on Python 3.11 to 3.14, on
|
|
37
|
+
Linux, macOS and Windows.
|
monoconf-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 monoconf contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
monoconf-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: monoconf
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One pydantic model, one CLI, one env reader, one layered TOML loader, one precedence order.
|
|
5
|
+
Project-URL: Homepage, https://github.com/mushroomsoftca/monoconf
|
|
6
|
+
Project-URL: Repository, https://github.com/mushroomsoftca/monoconf
|
|
7
|
+
Project-URL: Issues, https://github.com/mushroomsoftca/monoconf/issues
|
|
8
|
+
Author: monoconf contributors
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: argparse,cli,config,configuration,environment,pydantic,toml
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Requires-Dist: pydantic>=2.8
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: hypothesis; extra == 'dev'
|
|
24
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
27
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# monoconf
|
|
31
|
+
|
|
32
|
+
An opinionated universal configuration library for Python 3.11+. You write one
|
|
33
|
+
pydantic v2 model, and monoconf turns it into a command-line parser, an environment
|
|
34
|
+
reader and a layered TOML file loader, all resolved through one fixed precedence
|
|
35
|
+
order. Every resolved value can tell you where it came from.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
pip install "monoconf @ git+https://github.com/mushroomsoftca/monoconf@v0.1.0"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
monoconf is not on PyPI yet. Each release is on the repository's
|
|
42
|
+
[releases page](https://github.com/mushroomsoftca/monoconf/releases), with a wheel
|
|
43
|
+
and an sdist attached, and the line above installs the same tagged commit.
|
|
44
|
+
|
|
45
|
+
The only runtime dependency is pydantic.
|
|
46
|
+
|
|
47
|
+
## Thirty seconds
|
|
48
|
+
|
|
49
|
+
<!-- source: examples/minimal.py -->
|
|
50
|
+
```python
|
|
51
|
+
"""The smallest thing that works: a class body and a load()."""
|
|
52
|
+
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
|
|
55
|
+
from monoconf import Config
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AppConfig(Config):
|
|
59
|
+
"""Serve the widget index."""
|
|
60
|
+
|
|
61
|
+
input_path: Path
|
|
62
|
+
output_dir: Path = Path("out/")
|
|
63
|
+
verbose: bool = False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
cfg = AppConfig.load()
|
|
68
|
+
print(f"{cfg.input_path} -> {cfg.output_dir} (verbose={cfg.verbose})")
|
|
69
|
+
print(f"output_dir came from {cfg.origin_of('output_dir').describe()}")
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
That class is already a complete command-line interface:
|
|
73
|
+
|
|
74
|
+
<!-- output: python examples/minimal.py --help -->
|
|
75
|
+
```
|
|
76
|
+
usage: minimal.py [-h] [--output-dir PATH] [--verbose] INPUT_PATH
|
|
77
|
+
|
|
78
|
+
Serve the widget index.
|
|
79
|
+
|
|
80
|
+
positional arguments:
|
|
81
|
+
INPUT_PATH
|
|
82
|
+
|
|
83
|
+
options:
|
|
84
|
+
-h, --help show this help message and exit
|
|
85
|
+
--output-dir PATH [out]
|
|
86
|
+
--verbose
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
It also reads `APP_OUTPUT_DIR` and `APP_VERBOSE` from the environment, and
|
|
90
|
+
`output_dir` and `verbose` from `/etc/app.toml`, `~/.config/app.toml` and the
|
|
91
|
+
drop-in directories beside them. Nothing was configured to get all of that: every
|
|
92
|
+
name comes from the application name, which monoconf infers here from the class
|
|
93
|
+
name. The value in brackets is the default that would actually apply on this
|
|
94
|
+
machine, after the files and the environment have been read, and not only the one
|
|
95
|
+
the schema declares.
|
|
96
|
+
|
|
97
|
+
`load()` is for `main()`: on bad input it prints a message and exits with status
|
|
98
|
+
2, and for `--help` it prints the help and exits with status 0. `resolve()` does
|
|
99
|
+
the same work but raises instead, which suits tests and code that embeds monoconf:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
cfg = AppConfig.resolve(argv=["in.csv", "--verbose"], env={})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Both take `argv` and `env` explicitly. Leave either out, and monoconf reads the real
|
|
106
|
+
command line and environment.
|
|
107
|
+
|
|
108
|
+
## Precedence
|
|
109
|
+
|
|
110
|
+
Lowest priority first. The order is fixed, and it cannot be changed.
|
|
111
|
+
|
|
112
|
+
| # | Source | Example |
|
|
113
|
+
| --- | --- | --- |
|
|
114
|
+
| 1 | Schema defaults | `port: int = 8080` |
|
|
115
|
+
| 2 | System config | `/etc/myapp.toml` |
|
|
116
|
+
| 3 | System drop-ins, lexicographic | `/etc/myapp.d/*.toml` |
|
|
117
|
+
| 4 | User config | `~/.config/myapp.toml` |
|
|
118
|
+
| 5 | User drop-ins, lexicographic | `~/.config/myapp.d/*.toml` |
|
|
119
|
+
| 6 | `--config-override` files, in argv order | opt-in |
|
|
120
|
+
| 7 | Environment | `MYAPP_PORT=9090` |
|
|
121
|
+
| 8 | CLI | `--port 9090` |
|
|
122
|
+
|
|
123
|
+
Merging is done per leaf value. A drop-in that sets `database.port` leaves the
|
|
124
|
+
`database.host` from the base file intact, just as systemd's `.d/` directories do.
|
|
125
|
+
Validation runs once, on the merged result, so one error message reports every
|
|
126
|
+
problem at once. A value that is not set anywhere is simply absent, and never
|
|
127
|
+
counts as null.
|
|
128
|
+
|
|
129
|
+
## Required means positional
|
|
130
|
+
|
|
131
|
+
This is the rule most likely to surprise you, so here is the reasoning.
|
|
132
|
+
|
|
133
|
+
| Field | Command line | Files and environment |
|
|
134
|
+
| --- | --- | --- |
|
|
135
|
+
| No default | positional argument, in declaration order | ignored, with a warning |
|
|
136
|
+
| Has a default | `--kebab-case-name` | accepted |
|
|
137
|
+
|
|
138
|
+
A field without a default is required, and a required value is always given on the
|
|
139
|
+
command line. It is never read from a file or the environment. The alternative
|
|
140
|
+
fails quietly. If `input_path` could come from `/etc/myapp.toml`, then a missing
|
|
141
|
+
argument on one machine becomes a successful run on another that happens to have
|
|
142
|
+
the file, with an input nobody typed. A required value is supposed to be one
|
|
143
|
+
the caller gives every time, and a positional argument makes that visible in the
|
|
144
|
+
usage line.
|
|
145
|
+
|
|
146
|
+
A keyword argument is therefore never required. There is no "optional in files
|
|
147
|
+
but required on the command line" state, and no way to declare one. When a file or
|
|
148
|
+
variable sets a required field, monoconf warns `IgnoredValue` rather than silently
|
|
149
|
+
accepting it or failing, because an ignored setting is exactly what the operator
|
|
150
|
+
needs to hear about.
|
|
151
|
+
|
|
152
|
+
Booleans are flags that take no value. A field that defaults to `False` becomes
|
|
153
|
+
`--verbose`, and one that defaults to `True` becomes `--no-color`. Lists are
|
|
154
|
+
repeated flags, one value each time: `tags: list[str]` becomes `--tag a --tag b`.
|
|
155
|
+
A nested model's fields become flags and variables named by their dotted path:
|
|
156
|
+
`database.host` is `--database-host` and `MYAPP_DATABASE__HOST`. See
|
|
157
|
+
[`examples/nested.py`](https://github.com/mushroomsoftca/monoconf/blob/main/examples/nested.py).
|
|
158
|
+
|
|
159
|
+
## Custom types take a string
|
|
160
|
+
|
|
161
|
+
> A type used in a field must be constructible from a single string.
|
|
162
|
+
|
|
163
|
+
The command line and the environment only have strings to give. monoconf checks
|
|
164
|
+
every field's type when the class is defined, so an unusable type fails on import,
|
|
165
|
+
not when an operator's input first reaches it:
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
SchemaError: AppConfig.endpoint: type 'Endpoint' cannot be constructed
|
|
169
|
+
from a string, so it cannot be set from the command line or the environment.
|
|
170
|
+
|
|
171
|
+
Either give Endpoint a single-string constructor, or annotate the field:
|
|
172
|
+
|
|
173
|
+
endpoint: Annotated[Endpoint, BeforeValidator(Endpoint.parse)] = ...
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The check passes for an `Enum` with string values, a type annotated with a
|
|
177
|
+
pydantic `BeforeValidator`, one that defines `__get_pydantic_core_schema__`, and any
|
|
178
|
+
of the standard library types pydantic already reads from a string, such as `Path`,
|
|
179
|
+
`datetime`, `UUID`, `Decimal` and `IPv4Address`. A plain class with a string
|
|
180
|
+
constructor is not enough on its own, because pydantic cannot build a schema for
|
|
181
|
+
it; give it one of those hooks. File values are not turned into strings first: a
|
|
182
|
+
TOML datetime reaches your field as a datetime.
|
|
183
|
+
|
|
184
|
+
## Files and drop-ins
|
|
185
|
+
|
|
186
|
+
| Platform | System | User |
|
|
187
|
+
| --- | --- | --- |
|
|
188
|
+
| POSIX | `$XDG_CONFIG_DIRS/<app>.toml` (default `/etc/xdg`), then `/etc/<app>.toml` | `${XDG_CONFIG_HOME:-~/.config}/<app>.toml` |
|
|
189
|
+
| Windows | `%PROGRAMDATA%\<app>\config.toml` | `%APPDATA%\<app>\config.toml` |
|
|
190
|
+
|
|
191
|
+
Every config file has a drop-in directory beside it, with `.d` in place of
|
|
192
|
+
`.toml`: `/etc/myapp.d/`, `~/.config/myapp.d/`. Only the `*.toml` files in it are
|
|
193
|
+
read. They are sorted by filename, byte by byte, and a later file wins. Use the
|
|
194
|
+
systemd numbering convention:
|
|
195
|
+
|
|
196
|
+
```
|
|
197
|
+
/etc/myapp.d/10-base.toml # shipped by the package
|
|
198
|
+
/etc/myapp.d/20-site.toml # configuration management
|
|
199
|
+
/etc/myapp.d/99-local.toml # the hand edit on this one machine
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The sort is on bytes, not numbers, so `9-a.toml` sorts *after* `20-b.toml`. Pad
|
|
203
|
+
your numbers.
|
|
204
|
+
|
|
205
|
+
A list in a later file replaces the earlier list rather than adding to it. TOML
|
|
206
|
+
has no null, so a file cannot clear a value. To clear a `str | None` setting, use
|
|
207
|
+
an empty variable (`MYAPP_PROXY=`) or an empty flag (`--proxy ""`).
|
|
208
|
+
|
|
209
|
+
A file may put its settings at the top level, or under a table named after the
|
|
210
|
+
application (`[myapp]`). The table form is for files shared between applications.
|
|
211
|
+
A file that has a `[myapp]` table and also has top-level keys this application
|
|
212
|
+
reads is an error, and so is a file that cannot be parsed. Both errors name the
|
|
213
|
+
file. A key that matches no field produces a warning, with a suggestion when it is
|
|
214
|
+
close to a real field name. With `settings(strict_unknown=True)`, an unknown key is
|
|
215
|
+
an error instead.
|
|
216
|
+
|
|
217
|
+
Every path can be set explicitly, which is also how tests stay hermetic:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
model_config = settings(system_config=tmp_path / "myapp.toml", user_config=None)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`None` turns a layer off.
|
|
224
|
+
|
|
225
|
+
## Where did that come from?
|
|
226
|
+
|
|
227
|
+
With `settings(show_config=True)`, `--show-config` prints the resolved
|
|
228
|
+
configuration as TOML, with a comment beside each value saying where it came from:
|
|
229
|
+
|
|
230
|
+
<!-- output: WIDGETD_DATABASE__HOST=db.internal python examples/full.py --show-config --port 9090 -->
|
|
231
|
+
```
|
|
232
|
+
# widgetd resolved configuration
|
|
233
|
+
# input_path # positional, CLI-only, not given
|
|
234
|
+
port = 9090 # --port
|
|
235
|
+
log_level = "info" # default
|
|
236
|
+
# proxy = null # default
|
|
237
|
+
tags = [] # default
|
|
238
|
+
|
|
239
|
+
[database]
|
|
240
|
+
host = "db.internal" # WIDGETD_DATABASE__HOST
|
|
241
|
+
port = 5432 # default
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The output is a valid config file. If you save it and pass it back with
|
|
245
|
+
`--config-override`, you get the same configuration. Required values are commented
|
|
246
|
+
out because they are positional, and so are values TOML cannot express, such as
|
|
247
|
+
null. Secrets are commented out as well, and shown only as their mask. A field
|
|
248
|
+
typed `SecretStr`, `SecretBytes` or `Secret[T]` is never printed anywhere: `--help`
|
|
249
|
+
masks it the same way, and an error message calls it "the secret" rather than
|
|
250
|
+
quoting it.
|
|
251
|
+
|
|
252
|
+
The same provenance is available in code. `cfg.origin_of("database.host")`
|
|
253
|
+
returns an `Origin`:
|
|
254
|
+
|
|
255
|
+
- `source` is a `SourceKind`, one per row of the precedence table: `DEFAULT`,
|
|
256
|
+
`SYSTEM_FILE`, `SYSTEM_DROPIN`, `USER_FILE`, `USER_DROPIN`, `OVERRIDE_FILE`,
|
|
257
|
+
`ENV` or `CLI`.
|
|
258
|
+
- `path` is the file, for the five file layers, and `None` otherwise.
|
|
259
|
+
- `detail` is the variable name, the flag (`--port`, however it was typed) or a
|
|
260
|
+
positional's metavar, and `None` for a default or a file.
|
|
261
|
+
- `describe()` is the short answer that `--show-config` and the error messages
|
|
262
|
+
print: the file, the variable, the flag, or `default`.
|
|
263
|
+
|
|
264
|
+
Only a configuration from `load()` or `resolve()` has provenance. One built
|
|
265
|
+
directly, as `AppConfig(input_path=...)`, raises `MonoconfError` when asked.
|
|
266
|
+
|
|
267
|
+
## Errors
|
|
268
|
+
|
|
269
|
+
Pydantic's validation errors are written for the model's author. monoconf rewrites
|
|
270
|
+
them for the operator: every error is reported, together with where its value came
|
|
271
|
+
from:
|
|
272
|
+
|
|
273
|
+
<!-- output: WIDGETD_PORT=high WIDGETD_LOG_LEVEL=loud python examples/full.py index.csv -->
|
|
274
|
+
```
|
|
275
|
+
widgetd: invalid configuration
|
|
276
|
+
|
|
277
|
+
port: 'high' is not a valid integer
|
|
278
|
+
from WIDGETD_PORT
|
|
279
|
+
|
|
280
|
+
log_level: 'loud' is not one of: debug, info, warning, error
|
|
281
|
+
from WIDGETD_LOG_LEVEL
|
|
282
|
+
|
|
283
|
+
Try 'widgetd --help' for usage.
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Everything monoconf raises is a subclass of `MonoconfError`:
|
|
287
|
+
|
|
288
|
+
- `SchemaError` is raised when the class is defined.
|
|
289
|
+
- `ConfigFileError` means a file is unreadable, cannot be parsed, or uses both
|
|
290
|
+
forms.
|
|
291
|
+
- `SourceError` means a command line that cannot be parsed: a flag that does not
|
|
292
|
+
exist, a flag missing its value, a missing positional. A flag that does not exist
|
|
293
|
+
is refused rather than guessed at, with a suggestion when it is close to a real
|
|
294
|
+
one: `unrecognized arguments: --retires 5 (did you mean '--retries'?)`.
|
|
295
|
+
- `ValidationFailed` means one or more bad values, wherever they came from: a value
|
|
296
|
+
pydantic rejected, or a variable monoconf could not read at all, such as a boolean
|
|
297
|
+
that is not one. Every one is in the same report. Where pydantic rejected any,
|
|
298
|
+
its original error is available as the `__cause__`.
|
|
299
|
+
- `OutputRequested` is not a failure. `resolve()` cannot print or exit, so
|
|
300
|
+
`--help` raises `HelpRequested` and `--show-config` raises
|
|
301
|
+
`ShowConfigRequested`. Both subclasses carry the rendered output as `.text` and
|
|
302
|
+
the status `load()` would exit with as `.exit_code`, which is 0.
|
|
303
|
+
|
|
304
|
+
Because `OutputRequested` is a `MonoconfError` too, code that embeds monoconf and
|
|
305
|
+
reports configuration errors should catch it first:
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
try:
|
|
309
|
+
cfg = AppConfig.resolve()
|
|
310
|
+
except OutputRequested as requested:
|
|
311
|
+
print(requested.text)
|
|
312
|
+
raise SystemExit(requested.exit_code) from None
|
|
313
|
+
except MonoconfError as error:
|
|
314
|
+
log.error("bad configuration: %s", error)
|
|
315
|
+
raise
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Warnings are real `Warning` subclasses of `MonoconfWarning`, so `-W error` or
|
|
319
|
+
`pytest.warns` handles them like any other warning:
|
|
320
|
+
|
|
321
|
+
- `UnknownConfigKey`: a file key that matches no field.
|
|
322
|
+
- `IgnoredValue`: a file or variable that sets a positional field.
|
|
323
|
+
- `UnreadableSource`: a config path that exists but cannot be read.
|
|
324
|
+
|
|
325
|
+
## Settings
|
|
326
|
+
|
|
327
|
+
Every argument is optional.
|
|
328
|
+
[`examples/full.py`](https://github.com/mushroomsoftca/monoconf/blob/main/examples/full.py)
|
|
329
|
+
uses all of them.
|
|
330
|
+
|
|
331
|
+
| Argument | Default | Purpose |
|
|
332
|
+
| --- | --- | --- |
|
|
333
|
+
| `app_name` | inferred from the module/class name | Basis for file names, env prefix, root table, prog name |
|
|
334
|
+
| `prog` | `sys.argv[0]` basename | Program name in help output |
|
|
335
|
+
| `description` | class docstring, else empty | Help header |
|
|
336
|
+
| `epilog` | `None` | Help footer |
|
|
337
|
+
| `env_prefix` | `f"{app_name.upper()}_"` | Env var prefix; `""` disables the prefix |
|
|
338
|
+
| `env_enabled` | `True` | Turn the env source off entirely |
|
|
339
|
+
| `root_table` | `app_name` | Optional TOML table |
|
|
340
|
+
| `system_config` | platform default | `Path`, a sequence of `Path`s, or `None` to disable |
|
|
341
|
+
| `user_config` | platform default | `Path`, a sequence of `Path`s, or `None` to disable |
|
|
342
|
+
| `dropins` | `True` | Whether to read the `.d/` sibling of each config path |
|
|
343
|
+
| `config_override` | `False` | Enable `--config-override` |
|
|
344
|
+
| `show_config` | `False` | Enable `--show-config` |
|
|
345
|
+
| `strict_unknown` | `False` | Error rather than warn on unknown file keys |
|
|
346
|
+
|
|
347
|
+
`Field(...)` is `pydantic.Field` with three additions: `short="p"` for a
|
|
348
|
+
one-letter flag, `metavar=`, and `flag=` for a list whose name does not singularise
|
|
349
|
+
regularly. `description` doubles as the help text.
|
|
350
|
+
|
|
351
|
+
## Why not…
|
|
352
|
+
|
|
353
|
+
**pydantic-settings.** It reads the environment, dotenv files, secrets
|
|
354
|
+
directories and, in recent versions, the command line, and it is the obvious
|
|
355
|
+
choice if you want those pieces to arrange yourself. It is built to be
|
|
356
|
+
configured: you pick your sources, their order and their delimiters. monoconf
|
|
357
|
+
removes those choices. The order is fixed, the CLI is part of it, required means
|
|
358
|
+
positional, and every value carries its origin. If you want those choices made
|
|
359
|
+
for you, use monoconf. If you want to make them yourself, use pydantic-settings.
|
|
360
|
+
|
|
361
|
+
**dynaconf.** It is broad: many file formats, environments like `[development]`,
|
|
362
|
+
secret stores and templating. That breadth is also what makes it hard to answer
|
|
363
|
+
"why is `port` 9090?" on a production box. monoconf supports one format and one
|
|
364
|
+
order, and it answers that question itself.
|
|
365
|
+
|
|
366
|
+
**click** (and typer). They are excellent command-line libraries, and
|
|
367
|
+
configuration files are left to you. As soon as a flag also needs to come from
|
|
368
|
+
`/etc` or an environment variable, you are writing the precedence rules by hand:
|
|
369
|
+
reading the files, deciding which layer wins, and keeping track of which values
|
|
370
|
+
the operator actually typed. monoconf's parser is generated from the same schema
|
|
371
|
+
as the other sources, so there are no rules for you to write.
|
|
372
|
+
|
|
373
|
+
**argparse alone.** monoconf is argparse underneath, and its help output looks
|
|
374
|
+
like any other argparse program's. What argparse cannot do on its own is show the
|
|
375
|
+
default that files and the environment have resolved, merge layers, or say where
|
|
376
|
+
a value came from.
|
|
377
|
+
|
|
378
|
+
## Not in 0.1
|
|
379
|
+
|
|
380
|
+
Subcommands, positional arguments that take several values, fields that cannot
|
|
381
|
+
be set from the command line, formats other than TOML, and writing config files
|
|
382
|
+
are all deliberately left out. `PLAN.md` explains why for each. monoconf is
|
|
383
|
+
pre-1.0: the opinions are stable, and the details may still change.
|
|
384
|
+
|
|
385
|
+
## Licence
|
|
386
|
+
|
|
387
|
+
MIT. See [LICENSE](https://github.com/mushroomsoftca/monoconf/blob/main/LICENSE).
|
|
388
|
+
[PLAN.md](https://github.com/mushroomsoftca/monoconf/blob/main/PLAN.md) is the design document, and
|
|
389
|
+
[AGENTS.md](https://github.com/mushroomsoftca/monoconf/blob/main/AGENTS.md) covers how to work on the repository.
|