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.
Files changed (50) hide show
  1. monoconf-0.1.0/.gitignore +13 -0
  2. monoconf-0.1.0/CHANGELOG.md +37 -0
  3. monoconf-0.1.0/LICENSE +21 -0
  4. monoconf-0.1.0/PKG-INFO +389 -0
  5. monoconf-0.1.0/PLAN.md +1227 -0
  6. monoconf-0.1.0/README.md +360 -0
  7. monoconf-0.1.0/examples/full.py +64 -0
  8. monoconf-0.1.0/examples/minimal.py +19 -0
  9. monoconf-0.1.0/examples/nested.py +43 -0
  10. monoconf-0.1.0/pyproject.toml +91 -0
  11. monoconf-0.1.0/src/monoconf/__init__.py +46 -0
  12. monoconf-0.1.0/src/monoconf/config.py +281 -0
  13. monoconf-0.1.0/src/monoconf/errors.py +285 -0
  14. monoconf-0.1.0/src/monoconf/fields.py +171 -0
  15. monoconf-0.1.0/src/monoconf/merge.py +151 -0
  16. monoconf-0.1.0/src/monoconf/py.typed +0 -0
  17. monoconf-0.1.0/src/monoconf/schema.py +643 -0
  18. monoconf-0.1.0/src/monoconf/settings.py +196 -0
  19. monoconf-0.1.0/src/monoconf/sources/__init__.py +72 -0
  20. monoconf-0.1.0/src/monoconf/sources/cli.py +435 -0
  21. monoconf-0.1.0/src/monoconf/sources/defaults.py +30 -0
  22. monoconf-0.1.0/src/monoconf/sources/env.py +95 -0
  23. monoconf-0.1.0/src/monoconf/sources/files.py +422 -0
  24. monoconf-0.1.0/src/monoconf/toml.py +195 -0
  25. monoconf-0.1.0/tests/conftest.py +61 -0
  26. monoconf-0.1.0/tests/snapshots/help.txt +31 -0
  27. monoconf-0.1.0/tests/snapshots/show_config.txt +15 -0
  28. monoconf-0.1.0/tests/snapshots/validation_errors.txt +15 -0
  29. monoconf-0.1.0/tests/test_chain.py +237 -0
  30. monoconf-0.1.0/tests/test_config.py +97 -0
  31. monoconf-0.1.0/tests/test_defaults.py +73 -0
  32. monoconf-0.1.0/tests/test_discovery.py +156 -0
  33. monoconf-0.1.0/tests/test_env.py +263 -0
  34. monoconf-0.1.0/tests/test_errors.py +244 -0
  35. monoconf-0.1.0/tests/test_examples.py +129 -0
  36. monoconf-0.1.0/tests/test_fields.py +149 -0
  37. monoconf-0.1.0/tests/test_files.py +440 -0
  38. monoconf-0.1.0/tests/test_help.py +221 -0
  39. monoconf-0.1.0/tests/test_merge.py +205 -0
  40. monoconf-0.1.0/tests/test_override.py +234 -0
  41. monoconf-0.1.0/tests/test_package.py +29 -0
  42. monoconf-0.1.0/tests/test_pydantic_assumptions.py +49 -0
  43. monoconf-0.1.0/tests/test_readme.py +74 -0
  44. monoconf-0.1.0/tests/test_resolve.py +278 -0
  45. monoconf-0.1.0/tests/test_schema.py +304 -0
  46. monoconf-0.1.0/tests/test_schema_errors.py +356 -0
  47. monoconf-0.1.0/tests/test_settings.py +160 -0
  48. monoconf-0.1.0/tests/test_show_config.py +196 -0
  49. monoconf-0.1.0/tests/test_snapshots.py +159 -0
  50. monoconf-0.1.0/tests/test_toml.py +136 -0
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ build/
5
+ dist/
6
+ *.egg-info/
7
+ .coverage
8
+ .coverage.*
9
+ htmlcov/
10
+ .pytest_cache/
11
+ .mypy_cache/
12
+ .ruff_cache/
13
+ .hypothesis/
@@ -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.
@@ -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.