conclude 1.0.2__tar.gz → 1.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 (59) hide show
  1. {conclude-1.0.2 → conclude-1.1.0}/CHANGELOG.md +22 -0
  2. {conclude-1.0.2 → conclude-1.1.0}/PKG-INFO +1 -1
  3. {conclude-1.0.2 → conclude-1.1.0}/docs/comparison.md +20 -20
  4. {conclude-1.0.2 → conclude-1.1.0}/docs/concept.md +113 -57
  5. {conclude-1.0.2 → conclude-1.1.0}/docs/guide.md +10 -1
  6. {conclude-1.0.2 → conclude-1.1.0}/docs/reference.md +10 -2
  7. conclude-1.1.0/spec/README.md +76 -0
  8. {conclude-1.0.2 → conclude-1.1.0}/spec/casters.json +1 -1
  9. conclude-1.1.0/spec/cli.json +771 -0
  10. {conclude-1.0.2 → conclude-1.1.0}/spec/config_layers.json +1 -1
  11. {conclude-1.0.2 → conclude-1.1.0}/spec/config_tables.json +1 -1
  12. {conclude-1.0.2 → conclude-1.1.0}/spec/dotenv.json +1 -1
  13. {conclude-1.0.2 → conclude-1.1.0}/spec/gitignore.json +1 -1
  14. {conclude-1.0.2 → conclude-1.1.0}/spec/guard.json +1 -1
  15. {conclude-1.0.2 → conclude-1.1.0}/spec/invocation.json +80 -4
  16. {conclude-1.0.2 → conclude-1.1.0}/spec/merge.json +1 -1
  17. {conclude-1.0.2 → conclude-1.1.0}/spec/naming.json +49 -2
  18. {conclude-1.0.2 → conclude-1.1.0}/spec/sources.json +1 -1
  19. {conclude-1.0.2 → conclude-1.1.0}/spec/templates.json +41 -3
  20. {conclude-1.0.2 → conclude-1.1.0}/src/conclude/__init__.py +10 -5
  21. {conclude-1.0.2 → conclude-1.1.0}/src/conclude/app.py +33 -7
  22. conclude-1.1.0/src/conclude/cli.py +66 -0
  23. {conclude-1.0.2 → conclude-1.1.0}/src/conclude/formatters.py +27 -11
  24. {conclude-1.0.2 → conclude-1.1.0}/src/conclude/naming.py +14 -0
  25. {conclude-1.0.2 → conclude-1.1.0}/src/conclude/templates.py +10 -3
  26. {conclude-1.0.2 → conclude-1.1.0}/test/test_app.py +85 -1
  27. {conclude-1.0.2 → conclude-1.1.0}/test/test_formatters.py +3 -1
  28. {conclude-1.0.2 → conclude-1.1.0}/test/test_repo_hygiene.py +123 -4
  29. {conclude-1.0.2 → conclude-1.1.0}/test/test_spec.py +22 -2
  30. {conclude-1.0.2 → conclude-1.1.0}/test/test_templates.py +9 -1
  31. conclude-1.0.2/spec/README.md +0 -72
  32. {conclude-1.0.2 → conclude-1.1.0}/.gitignore +0 -0
  33. {conclude-1.0.2 → conclude-1.1.0}/LICENSE +0 -0
  34. {conclude-1.0.2 → conclude-1.1.0}/README.md +0 -0
  35. {conclude-1.0.2 → conclude-1.1.0}/RELEASING.md +0 -0
  36. {conclude-1.0.2 → conclude-1.1.0}/hatch_build.py +0 -0
  37. {conclude-1.0.2 → conclude-1.1.0}/pyproject.toml +0 -0
  38. {conclude-1.0.2 → conclude-1.1.0}/src/conclude/casters.py +0 -0
  39. {conclude-1.0.2 → conclude-1.1.0}/src/conclude/developer.py +0 -0
  40. {conclude-1.0.2 → conclude-1.1.0}/src/conclude/env.py +0 -0
  41. {conclude-1.0.2 → conclude-1.1.0}/src/conclude/files.py +0 -0
  42. {conclude-1.0.2 → conclude-1.1.0}/src/conclude/guard.py +0 -0
  43. {conclude-1.0.2 → conclude-1.1.0}/src/conclude/infer.py +0 -0
  44. {conclude-1.0.2 → conclude-1.1.0}/src/conclude/merge.py +0 -0
  45. {conclude-1.0.2 → conclude-1.1.0}/src/conclude/paths.py +0 -0
  46. {conclude-1.0.2 → conclude-1.1.0}/src/conclude/py.typed +0 -0
  47. {conclude-1.0.2 → conclude-1.1.0}/src/conclude/tomlwrite.py +0 -0
  48. {conclude-1.0.2 → conclude-1.1.0}/test/test_casters.py +0 -0
  49. {conclude-1.0.2 → conclude-1.1.0}/test/test_comparison.py +0 -0
  50. {conclude-1.0.2 → conclude-1.1.0}/test/test_developer.py +0 -0
  51. {conclude-1.0.2 → conclude-1.1.0}/test/test_docs.py +0 -0
  52. {conclude-1.0.2 → conclude-1.1.0}/test/test_dotenv_guard.py +0 -0
  53. {conclude-1.0.2 → conclude-1.1.0}/test/test_env.py +0 -0
  54. {conclude-1.0.2 → conclude-1.1.0}/test/test_files.py +0 -0
  55. {conclude-1.0.2 → conclude-1.1.0}/test/test_guard.py +0 -0
  56. {conclude-1.0.2 → conclude-1.1.0}/test/test_infer.py +0 -0
  57. {conclude-1.0.2 → conclude-1.1.0}/test/test_merge.py +0 -0
  58. {conclude-1.0.2 → conclude-1.1.0}/test/test_naming.py +0 -0
  59. {conclude-1.0.2 → conclude-1.1.0}/test/test_tomlwrite_and_paths.py +0 -0
@@ -4,6 +4,28 @@ All notable changes to this project are documented in this file. The
4
4
  format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [Unreleased]
8
+
9
+ ## [1.1.0] - 2026-10-04
10
+
11
+ ### Added
12
+
13
+ - **A negation for every boolean flag.** A `bool` setting gets `--no-flag` as
14
+ well as `--flag` (the new `NegatableFlag` action): `--no-flag` stores
15
+ `False`, the last one given wins, and giving neither leaves the setting unset.
16
+ A key that already starts with `no_` is turned off by dropping it
17
+ (`no_color`: `--no-color` on, `--color` off). New: `cli_negated_flag_name`,
18
+ `NegatableFlag`, `NEGATED`, and the `conclude.cli` module.
19
+ - Two settings that claim the same flag (`cache` and `no_cache`) now raise
20
+ `ValueError` from `add_arguments`, `format_cli` and `format_invocation`.
21
+
22
+ ### Changed
23
+
24
+ - `format_invocation` writes a bool that is off as its negation, so a bool whose
25
+ default is `True` reproduces (`format_bool(False)` is now `NEGATED`, not
26
+ `None`); `format_cli` shows the flag that changes a bool's default.
27
+ - Conforms to spec version 2 (new `spec/cli.json`).
28
+
7
29
  ## [1.0.2] - 2026-09-26
8
30
 
9
31
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: conclude
3
- Version: 1.0.2
3
+ Version: 1.1.0
4
4
  Summary: Set up your defaults; conclude infers the env vars, config-file keys, and CLI flags, and resolves them all into one settings object.
5
5
  Project-URL: Homepage, https://github.com/tanakapayam/conclude
6
6
  Project-URL: Documentation, https://github.com/tanakapayam/conclude/blob/main/docs/python/guide.md
@@ -3,7 +3,7 @@
3
3
  A feature-by-feature comparison with five libraries people often reach
4
4
  for instead. **Verified 2026-09-19** against each project's
5
5
  documentation and its PyPI metadata; the versions checked are
6
- conclude 1.0.2, ConfigArgParse 1.7.7, jsonargparse 4.52.0,
6
+ conclude 1.1.0, ConfigArgParse 1.7.7, jsonargparse 4.52.0,
7
7
  pydantic-settings 2.15.0, Dynaconf 3.3.5 and python-decouple 3.8.
8
8
 
9
9
  A dash (--) means no built-in support that the project's documentation
@@ -14,29 +14,29 @@ the README; this page is the detail.
14
14
 
15
15
  ## What you write
16
16
 
17
- | conclude | ConfigArgParse | jsonargparse | pydantic-settings | Dynaconf | python-decouple |
18
- | ----------------- | ---------------------- | ----------------------------------------- | -------------------- | --------------------------------------- | ----------------------- |
17
+ | conclude | ConfigArgParse | jsonargparse | pydantic-settings | Dynaconf | python-decouple |
18
+ | ------------------ | -------------------------- | --------------------------------------------- | ---------------------- | ---------------------------------------- | --------------------------- |
19
19
  | a dict of defaults | an `add()` call per option | type hints on functions, classes, dataclasses | a pydantic model class | settings files, plus optional validators | a `config()` call per value |
20
20
 
21
21
  ## Capabilities
22
22
 
23
- | Capability | conclude | ConfigArgParse | jsonargparse | pydantic-settings | Dynaconf | python-decouple |
24
- | ----------------------------------------------------- | --------------------------------- | --------------------------------------- | --------------------------------- | ---------------------------------------- | ----------------------------------------- | -------------------------------- |
25
- | Environment variables | yes | yes | yes | yes | yes | yes |
26
- | Your program's CLI flags | yes | yes | yes | yes (opt in) | -- (its CLI manages settings files) | -- |
27
- | TOML files | yes | yes (`toml` extra) | yes (`toml` extra) | yes (`toml` extra) | yes | -- |
28
- | YAML, JSON or INI files | -- | yes (YAML, INI) | yes (YAML, JSON) | yes (YAML, JSON) | yes (YAML, JSON, INI) | yes (INI) |
29
- | `.env` files | yes | -- | -- | yes | yes | yes |
30
- | Sources layered, precedence documented | yes | yes | yes | yes (reorderable) | yes | yes (env, then file, then default) |
31
- | Where the config files live | system, user and project built in | you list the paths | you list the paths | you list the paths | you list the paths | searched up from your module |
32
- | Names inferred from the declaration | flags, env vars and config keys | config keys | flags and config keys; env vars with `default_env` | env vars, flags and TOML keys, from field names | -- | -- |
33
- | Config templates or reference generated from it | env, TOML and CLI templates | `-h` lists env vars and config keys | `--print_config` (YAML) | -- (a model can export JSON Schema) | `dynaconf init` scaffolds files | -- |
34
- | Local file above the environment, built in | yes | -- | -- | possible by reordering sources | -- | -- |
35
- | Verifies a private file is gitignored before reading | yes | -- | -- | -- | -- (`init` adds `.secrets.*` to `.gitignore`; no load-time check documented) | -- |
36
- | Schema validation of values | -- (casting only) | -- (argparse `type=`, `choices`) | yes (type hints) | yes | yes (validators) | -- (casting only) |
37
- | External secret backends | -- | -- | -- | yes (AWS, Azure, GCP) | yes (Vault, Redis) | -- |
38
- | Named environments (development, production, ...) | -- | -- | -- | -- | yes | -- |
39
- | Hard runtime dependencies (PyPI metadata) | 0 | 0 | 1 (PyYAML) | 3 | 0 | 0 |
23
+ | Capability | conclude | ConfigArgParse | jsonargparse | pydantic-settings | Dynaconf | python-decouple |
24
+ | ----------------------------------------------------- | --------------------------------- | --------------------------------------- | -------------------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------- |
25
+ | Environment variables | yes | yes | yes | yes | yes | yes |
26
+ | Your program's CLI flags | yes | yes | yes | yes (opt in) | -- (its CLI manages settings files) | -- |
27
+ | TOML files | yes | yes (`toml` extra) | yes (`toml` extra) | yes (`toml` extra) | yes | -- |
28
+ | YAML, JSON or INI files | -- | yes (YAML, INI) | yes (YAML, JSON) | yes (YAML, JSON) | yes (YAML, JSON, INI) | yes (INI) |
29
+ | `.env` files | yes | -- | -- | yes | yes | yes |
30
+ | Sources layered, precedence documented | yes | yes | yes | yes (reorderable) | yes | yes (env, then file, then default) |
31
+ | Where the config files live | system, user and project built in | you list the paths | you list the paths | you list the paths | you list the paths | searched up from your module |
32
+ | Names inferred from the declaration | flags, env vars and config keys | config keys | flags and config keys; env vars with `default_env` | env vars, flags and TOML keys, from field names | -- | -- |
33
+ | Config templates or reference generated from it | env, TOML and CLI templates | `-h` lists env vars and config keys | `--print_config` (YAML) | -- (a model can export JSON Schema) | `dynaconf init` scaffolds files | -- |
34
+ | Local file above the environment, built in | yes | -- | -- | possible by reordering sources | -- | -- |
35
+ | Verifies a private file is gitignored before reading | yes | -- | -- | -- | -- (`init` adds `.secrets.*` to `.gitignore`; no load-time check documented) | -- |
36
+ | Schema validation of values | -- (casting only) | -- (argparse `type=`, `choices`) | yes (type hints) | yes | yes (validators) | -- (casting only) |
37
+ | External secret backends | -- | -- | -- | yes (AWS, Azure, GCP) | yes (Vault, Redis) | -- |
38
+ | Named environments (development, production, ...) | -- | -- | -- | -- | yes | -- |
39
+ | Hard runtime dependencies (PyPI metadata) | 0 | 0 | 1 (PyYAML) | 3 | 0 | 0 |
40
40
 
41
41
  ## Reading the table
42
42
 
@@ -8,8 +8,8 @@ is the reference implementation ([guide](python/guide.md), [reference](python/re
8
8
  other implementations follow this document and pass the conformance
9
9
  fixtures in [`spec/`](../spec/README.md).
10
10
 
11
- This is **concept version 1**, which is **spec version 1** of the fixtures
12
- (the Python package 1.0.x implements it). The words "must" and "may" are
11
+ This is **concept version 2**, which is **spec version 2** of the fixtures
12
+ (the Python, TypeScript and Bash packages implement it). The words "must" and "may" are
13
13
  meant literally: a conforming implementation does what "must" says, and
14
14
  anything a sentence does not require is up to the implementation.
15
15
 
@@ -46,13 +46,13 @@ Settings are ordered (declaration order): generated templates keep it.
46
46
 
47
47
  ## 3. Declaring settings
48
48
 
49
- | Token | Meaning | Casts to |
50
- | --- | --- | --- |
51
- | `bool` | a flag | true or false |
52
- | `int` | a number that prints as a whole number when it is one | an integer, or a fraction if that is what was given |
53
- | `float` | a number | a number |
54
- | `str` | text | text, or unset |
55
- | `list` | a list of text items | a list of text, or unset |
49
+ | Token | Meaning | Casts to |
50
+ | ------- | ----------------------------------------------------- | --------------------------------------------------- |
51
+ | `bool` | a flag | true or false |
52
+ | `int` | a number that prints as a whole number when it is one | an integer, or a fraction if that is what was given |
53
+ | `float` | a number | a number |
54
+ | `str` | text | text, or unset |
55
+ | `list` | a list of text items | a list of text, or unset |
56
56
 
57
57
  The declared type -- not the runtime shape of the default -- decides the
58
58
  caster and how a default is rendered in a template. A language that can
@@ -69,10 +69,23 @@ from `3.0` (JavaScript) must let the author say it.
69
69
  setting may override its name.
70
70
  - **CLI flag**: `--` and the key with `_` replaced by `-`; case is kept.
71
71
  `filter_col` is `--filter-col`.
72
+ - **Negated CLI flag** (a `bool` only): the flag that sets it to false.
73
+ It is `--no-` and the flag's name: `debug` is `--no-debug`. A flag name
74
+ that already starts with `no-` and continues drops that `no-` instead of
75
+ stacking another, so the opposite of `--no-color` (the setting `no_color`)
76
+ is `--color`, not `--no-no-color`. A setting that is not a `bool` has no
77
+ negation.
72
78
 
73
79
  Two application names that differ only in which non-identifier character
74
80
  they use sanitize to the same prefix; that is a known, accepted limit.
75
81
 
82
+ No two settings may claim the same CLI flag, counting each `bool`'s negation
83
+ as a flag of its own: a `bool` `cache` and a setting `no_cache` both want
84
+ `--no-cache` (and, if `no_cache` is a `bool` too, `--cache` as well), and
85
+ two keys that differ only in `_` against `-` want one flag. An implementation
86
+ reports this as an error when the flags are derived, not by silently letting
87
+ one setting shadow the other (`cli.json`).
88
+
76
89
  ## 5. Casting (`casters.json`)
77
90
 
78
91
  Every layer's value passes through the caster for its setting's declared
@@ -128,11 +141,11 @@ process environment, with an optional `.env` file beneath it (section 8);
128
141
 
129
142
  ## 7. Config files (`config_layers.json`, `config_tables.json`)
130
143
 
131
- | Layer | Location | On by default? |
132
- | --- | --- | --- |
133
- | system | `/etc/<name>/config.toml` | no (opt-in) |
134
- | user | `~/.config/<name>/config.toml` | yes |
135
- | project | `./.config.toml` | yes |
144
+ | Layer | Location | On by default? |
145
+ | ---------------- | ---------------------------------- | --------------------------------------------------------------- |
146
+ | system | `/etc/<name>/config.toml` | no (opt-in) |
147
+ | user | `~/.config/<name>/config.toml` | yes |
148
+ | project | `./.config.toml` | yes |
136
149
  | project siblings | `./.config.*.toml`, sorted by name | yes (the pattern is configurable; it can be switched off alone) |
137
150
 
138
151
  The files are merged key by key in that order, so a key set in a later file
@@ -279,8 +292,10 @@ and every other control character (U+0000-U+001F and U+007F) as `\uXXXX` with
279
292
  upper-case hexadecimal. An unset setting is `# key =`. An empty table path
280
293
  means no header.
281
294
 
282
- **CLI reference**: one line per flag. A `bool` is the bare flag; any other
283
- setting is the flag and `<METAVAR>` (the key upper-cased, or the override).
295
+ **CLI reference**: one line per setting. A `bool` is the bare flag that
296
+ *changes* its default -- `--flag` when the default is false, the negation
297
+ (`--no-flag`) when it is true, and `--flag | --no-flag` when it has none, as
298
+ either one changes that; any other setting is the flag and `<METAVAR>` (the key upper-cased, or the override).
284
299
  Each line ends with `(default: text)`, where an unset default reads `none`,
285
300
  an empty text reads `""`, and the flag column is padded to the widest so the
286
301
  `(default:` parts align, with one space after it.
@@ -289,10 +304,10 @@ an empty text reads `""`, and the flag column is padded to the widest so the
289
304
  a standalone command line that gets back to it with no environment variables,
290
305
  config files or shorthand -- what a `--print-invocation` flag prints. Settings
291
306
  are visited in declaration order, and each is written as `--flag=value` (a bare
292
- `--flag` for a `bool` that is on) unless it is left out. A setting is left out
307
+ `--flag` for a `bool` that is on, and its negation `--no-flag` for one that is
308
+ off) unless it is left out. A setting is left out
293
309
  when it equals its default -- omitting a flag already reproduces its default, and
294
- this covers every setting automatically -- when it is a `bool` that is off (there
295
- is no flag to turn one off), when its value is unset, or when the caller skips it
310
+ this covers every setting automatically -- when its value is unset, or when the caller skips it
296
311
  (`skip` beats `alwaysInclude`, which forces a setting that equals its default to
297
312
  be written). `compareDefaults` replaces what counts as each setting's default for
298
313
  one call, for a setting whose effective default is substituted after resolving.
@@ -328,55 +343,93 @@ for an inactive file.
328
343
 
329
344
  ## 13. Conformance
330
345
 
331
- An implementation conforms to spec version 1 when it passes every fixture in
346
+ An implementation conforms to spec version 2 when it passes every fixture in
332
347
  [`spec/`](../spec/README.md). The fixtures are data, not code; each
333
348
  implementation writes a small adapter from a fixture file to its own API.
334
349
 
335
- | Fixture | Sections | What it covers |
336
- | --- | --- | --- |
337
- | `naming.json` | 4 | environment variable, flag and key names |
338
- | `casters.json` | 3, 5 | casting for every declared type |
339
- | `dotenv.json` | 8 | the `.env` dialect |
340
- | `merge.json` | 6 | precedence, no-opinion, empty values, errors |
341
- | `config_layers.json` | 7 | merging system, user, project and sibling files; tables |
342
- | `config_tables.json` | 7 | table selection and the positional shorthand |
343
- | `templates.json` | 11 | the environment, TOML and CLI templates |
344
- | `invocation.json` | 11 | reproducing a resolved configuration as a command line |
345
- | `gitignore.json` | 9 | rule matching, judged by real `git` |
346
- | `guard.json` | 9 | the guard's checks and reasons |
347
- | `sources.json` | 10, 12 | the report of which sources are in play |
350
+ | Fixture | Sections | What it covers |
351
+ | -------------------- | -------: | ------------------------------------------------------- |
352
+ | `naming.json` | 4 | environment variable, flag, negated flag and key names |
353
+ | `casters.json` | 3, 5 | casting for every declared type |
354
+ | `dotenv.json` | 8 | the `.env` dialect |
355
+ | `merge.json` | 6 | precedence, no-opinion, empty values, errors |
356
+ | `config_layers.json` | 7 | merging system, user, project and sibling files; tables |
357
+ | `config_tables.json` | 7 | table selection and the positional shorthand |
358
+ | `templates.json` | 11 | the environment, TOML and CLI templates |
359
+ | `invocation.json` | 11 | reproducing a resolved configuration as a command line |
360
+ | `cli.json` | 4 | what a command line gives a setting; flag collisions |
361
+ | `gitignore.json` | 9 | rule matching, judged by real `git` |
362
+ | `guard.json` | 9 | the guard's checks and reasons |
363
+ | `sources.json` | 10, 12 | the report of which sources are in play |
348
364
 
349
365
  Not yet covered by fixtures: how each binding reads its manifest (section 10;
350
366
  the wording of a `not configured` reason is binding-specific), and how a
351
- language exposes flags to its CLI parser.
367
+ language hands its flags to its CLI parser (`cli.json` pins only what a command
368
+ line gives each setting, not the parser's own help or error text).
352
369
 
353
370
  ## 14. What varies by language
354
371
 
355
- | Concern | Python (reference) | TypeScript ([`node/`](../node/README.md)) |
356
- | --- | --- | --- |
357
- | Declaring settings | a dict of defaults; `opt(type)` for an unset one | an object of defaults, with `int()`, `float()`, `str()`, ... for numbers and unset settings |
358
- | Type inference | the default's runtime type | the default's runtime type, plus explicit `int` and `float` (D2) |
359
- | Key style in the API | `snake_case` | `camelCase`, converted (D1) |
360
- | Manifest binding | `pyproject.toml`, `[tool.conclude.developer] config = "..."` | `package.json`, `"conclude": { "developer": { "config": "..." } }` (D3) |
361
- | CLI | builds `argparse` flags | flag definitions in `node:util.parseArgs`' shape, plus a thin `parseArgs` wrapper (D4) |
362
- | TOML | standard library | a small dependency (D5) |
363
- | Gitignore matching | the optional `pathspec` extra | an optional dependency (D5) |
364
-
365
- **Decisions for other bindings.** The TypeScript package follows all five (its
366
- [guide](node/guide.md) and [reference](node/reference.md) show how):
372
+ | Concern | Python ([`python/`](../python/README.md)) | TypeScript ([`node/`](../node/README.md)) | Bash ([`bash/`](../bash/README.md)) |
373
+ | ----------------------------- | ----------------------------------------- | ----------------------------------------- | ----------------------------------- |
374
+ | Declaring settings | a dict of defaults; `opt(type)` for an unset one | an object of defaults, with `int()`, `float()`, `str()`, ... for numbers and unset settings | `conclude_define app key type [default]`; a 3-argument call (no default) and a 4-argument call with `""` are distinct |
375
+ | Type inference | the default's runtime type | the default's runtime type, plus explicit `int` and `float` (D2) | always explicit -- Bash has no runtime types to infer from |
376
+ | Key style in the API | `snake_case` | `camelCase`, converted (D1) | whatever the app declares; there is no separate API casing to convert from |
377
+ | Custom casters/formatters | yes, per setting or app-wide | no | no |
378
+ | System config / `.env` layers | built in, opt-in (`AUTO`-able) | built in, opt-in (`AUTO`-able) | not wired into `conclude_resolve`; build them from the public primitives (guide section 7) |
379
+ | Manifest binding | `pyproject.toml`, `[tool.conclude.developer] config = "..."` | `package.json`, `"conclude": { "developer": { "config": "..." } }` | none (D3) -- `--developer-file`/`--developer-opt-in`, optionally resolved through a personal `~/.config/conclude/control.toml` |
380
+ | CLI | builds `argparse` flags | flag definitions in `node:util.parseArgs`' shape, plus a thin `parseArgs` wrapper | owns parsing itself (D4) |
381
+ | `--help` | automatic (via `argparse`), with per-flag custom text | none built in | opt-in (`conclude_resolve --help-flag`), assembled from the same declarations, not per-flag customizable |
382
+ | TOML | standard library | a small dependency | hand-written; a deliberate subset (no arrays, two table levels) (D5) |
383
+ | Gitignore matching | the optional `pathspec` extra | an optional dependency | the real `git`, required rather than optional (D5) |
384
+
385
+ **Decisions for other bindings.** The TypeScript package follows D1, D2 and D3
386
+ as written; Bash's own shape is noted inline above and in D3-D5 below, since a
387
+ language with no object/class API and no package-manifest convention cannot
388
+ follow some of these the same way:
367
389
 
368
390
  - **D1: canonical names in files and the environment.** Config-file keys and
369
391
  environment variable names use the canonical `snake_case` of section 4, so
370
392
  one config file or `.env` serves every implementation; a binding's API may
371
- use its own casing and converts (`filterCol` is `filter_col`).
393
+ use its own casing and converts (`filterCol` is `filter_col`). Bash has no
394
+ such conversion to make -- a setting's key *is* its name everywhere, with no
395
+ separate in-language identifier style sitting above it.
372
396
  - **D2: numeric kinds are explicit** where the language cannot distinguish
373
- them, and templates render per declared kind (section 11).
374
- - **D3: each ecosystem uses its own manifest** to name the developer file;
375
- in a repository with several, they may name the same file.
376
- - **D4: no bundled CLI parser.** The binding exposes derived flag
377
- definitions rather than owning the command line.
397
+ them, and templates render per declared kind (section 11). Bash's casters
398
+ take this further: every setting's type is explicit, numeric or not, since
399
+ Bash values carry no runtime type for a default to be inferred from.
400
+ - **D3: each ecosystem uses its own manifest** to name the developer file; in
401
+ a repository with several, they may name the same file. Bash has no
402
+ manifest convention to use, so it has none: an app names the file directly
403
+ with `--developer-file`, or opts in with no opinion of its own
404
+ (`--developer-opt-in`) and leaves it to a personal, per-user
405
+ `~/.config/conclude/control.toml` (or `.developer.toml` if even that says
406
+ nothing) -- see [`conclude_resolve_developer_file`](bash/reference.md) in
407
+ the Bash reference. This file is Bash's own addition, not a
408
+ cross-language concept; another binding is free to do something else
409
+ entirely; it is not what D3 means by "manifest."
410
+ - **D4: no bundled CLI parser.** Python and Node each expose derived flag
411
+ definitions for an external parser (`argparse`, `node:util.parseArgs`) to
412
+ own, rather than parsing the command line themselves. Bash has no
413
+ ecosystem-standard parser to defer to, so `conclude_resolve` necessarily
414
+ owns this step itself -- a deliberate departure from D4, not an oversight.
415
+ Its CLI vocabulary is narrower than `argparse`'s as a result: `--flag
416
+ value`, `--flag=value`, a bare `--flag` for a bool with its negation
417
+ `--no-flag` (section 4), and no abbreviation.
378
418
  - **D5: dependencies are minimal.** A TOML parser is required; gitignore
379
- matching is optional, and its absence is a setup error (section 9).
419
+ matching is optional, and its absence is a setup error (section 9). Bash
420
+ goes further on the first (no library at all -- a hand-written reader
421
+ scoped to exactly what section 7 requires, which is also why it cannot read
422
+ a TOML array or a table nested past two levels) and differs on the second
423
+ (`git` is required outright, not an optional extra, since Bash has nothing
424
+ to fall back to without it).
425
+
426
+ Help generation is not yet a cross-language policy, so it has no lettered
427
+ decision above: Python gets a full `-h`/`--help` automatically from
428
+ `argparse`; Node has nothing built in, and an app using it builds one from
429
+ `formatCli` itself; Bash's `conclude_format_help` is opt-in and assembled from
430
+ the same declarations every other template reads from, with no per-flag
431
+ custom text the way `argparse`'s `help=` allows. If another binding adds
432
+ something similar later, this section is where that would get written down.
380
433
 
381
434
  ## 15. Implementation-defined
382
435
 
@@ -388,10 +441,13 @@ and should document what they do:
388
441
  - line breaks other than `\n`, `\r\n` and `\r` in a `.env` file;
389
442
  - how a very small or very large `float` is written (exponent form) in a
390
443
  template;
391
- - reproducing a run for a `bool` whose default is true (there is no flag to turn
392
- it off, so it cannot be reproduced), and for a non-`bool` setting whose
393
- resolved value is unset while its default is not (Python writes `None`; the
394
- TypeScript binding leaves it out);
444
+ - reproducing a run for a non-`bool` setting whose resolved value is unset while
445
+ its default is not (Python writes `None`; the TypeScript binding leaves it
446
+ out);
447
+ - abbreviated flags (`--fil` for `--filter-col`: `argparse` accepts them, the
448
+ others do not), and a value that starts with `-` given as a separate argument
449
+ (`--port -5`: `argparse` takes it, `node:util.parseArgs` rejects it; the
450
+ `--port=-5` form, which reproducing a run writes, works everywhere);
395
451
  - a default of empty text on a `str` setting when it is *resolved* (the
396
452
  template shows it; the merge casts it to unset);
397
453
  - what a boolean, a list or another non-text value does when cast to `str`,
@@ -140,6 +140,14 @@ config_file = app.load_config_files(table_path)
140
140
  settings = app.resolve(cli, config_file=config_file)
141
141
  ```
142
142
 
143
+ A `bool` setting's flag is bare (`--repeat`) and comes with a negation, `--no-repeat`,
144
+ that sets it to `False` -- so a setting whose default is `True` can be turned off
145
+ from the command line, and the last of the two on the line wins. (A key that
146
+ already starts with `no_`, like `no_color`, is turned off by dropping the `no`:
147
+ `--color`.) Leaving both out is no opinion, so a lower layer decides. Two settings
148
+ may not claim the same flag -- `cache` and `no_cache` both want `--no-cache` --
149
+ and `add_arguments` raises `ValueError` if they do.
150
+
143
151
  With this in `.config.toml`:
144
152
 
145
153
  ```toml
@@ -665,7 +673,8 @@ A few things worth knowing:
665
673
  the TOML header is the app's `[default_table]` (pass `table="mom"` or
666
674
  `table=["remind", "mom"]` for another, or `header=False` for just the
667
675
  key lines), and the CLI lines are exactly the flags `add_arguments`
668
- adds -- a bool is a bare flag, everything else shows its `<METAVAR>`
676
+ adds -- a bool is the bare flag that *changes* its default (`--flag`, or
677
+ `--no-flag` for a default of true), everything else shows its `<METAVAR>`
669
678
  (pass the same `overrides=` you gave `add_arguments` if you renamed
670
679
  one).
671
680
  - **No default means a placeholder.** A setting that starts out unset
@@ -47,7 +47,7 @@ Computed from the fields above, never stored: `resolved_defaults`,
47
47
 
48
48
  | Group | Method | Purpose |
49
49
  | ----------------- | --------------------------------- | ----------------------------------------------------------------------------------------------- |
50
- | Build a CLI | `add_arguments(parser)` | Add one inferred flag per setting to an existing `ArgumentParser` (`skip=`/`overrides=`). |
50
+ | Build a CLI | `add_arguments(parser)` | Add one inferred flag per setting -- and a `--no-` negation for every `bool` -- to an existing `ArgumentParser` (`skip=`/`overrides=`). |
51
51
  | | `build_arg_parser()` | A fresh parser with those flags already added. |
52
52
  | | `add_print_invocation_argument()` | Add the conventional `--print-invocation` flag ([section 5](guide.md#5-debugging-and-documentation---print-invocation)). |
53
53
  | Read a layer | `load_env()` | The environment layer (and the `.env` fallback, if opted in). |
@@ -98,6 +98,7 @@ Computed from the fields above, never stored: `resolved_defaults`,
98
98
  | Function | Purpose |
99
99
  | ---------------------------- | ----------------------------------------------------------- |
100
100
  | `cli_flag_name(key)` | `"filter_col"` -> `"--filter-col"`. |
101
+ | `cli_negated_flag_name(key)` | The flag that turns a `bool` off: `"debug"` -> `"--no-debug"`; `"no_color"` -> `"--color"`. |
101
102
  | `env_var_name(app_name, key)` | `("remind", "retries")` -> `"REMIND_RETRIES"`. |
102
103
  | `config_key_name(key)` | The config-file key -- just `key` itself. |
103
104
 
@@ -116,10 +117,17 @@ Computed from the fields above, never stored: `resolved_defaults`,
116
117
  | ------------------------------------ | ------------------------------------------------------ |
117
118
  | `infer_formatter(type_)` | The formatter for a bare type. |
118
119
  | `infer_formatters(defaults, overrides)` | A formatter for every key, with your overrides applied. |
119
- | `format_bool(value)` | `True` -> the bare flag; `False` -> not rendered. |
120
+ | `format_bool(value)` | `True` -> the bare flag; `False` -> `NEGATED` (the flag's `--no-` form); `None` -> not rendered. |
120
121
  | `format_list(value)` | Shell-quoted, comma-joined. |
121
122
  | `format_scalar(value)` | Shell-quoted `str(value)`. |
122
123
 
124
+ **`conclude.cli`** -- the command-line surface derived from the settings.
125
+
126
+ | Name | Purpose |
127
+ | ---- | ------- |
128
+ | `NegatableFlag` | The argparse action behind a `bool`'s flag: `--flag` stores `True`, `--no-flag` stores `False`, last one wins, neither leaves it unset. |
129
+ | `cli_flags(defaults, skip=())` | `(key, flag, is_negation)` for every flag the settings get; raises `ValueError` if two settings claim one. |
130
+
123
131
  **`conclude.casters`** -- reusable casters (inference picks from these; they also work written out by hand).
124
132
 
125
133
  | Function | Purpose |
@@ -0,0 +1,76 @@
1
+ # Conformance fixtures
2
+
3
+ Language-neutral test data for [the conclude concept](../docs/concept.md).
4
+ Every file is plain JSON: inputs and expected outputs, with no code in it,
5
+ so any implementation -- the Python package in this repository, a
6
+ TypeScript one, anything else -- can run the same cases and be held to the
7
+ same behavior. **Spec version 2.**
8
+
9
+ The Python implementation runs them in
10
+ [`python/test/test_spec.py`](../python/test/test_spec.py), which is also the best
11
+ example of an adapter: a small function per fixture file that turns each
12
+ case into a call on the implementation and compares the result.
13
+
14
+ ## Who runs them
15
+
16
+ | Implementation | Harness |
17
+ | ------------------------------------- | ------------------------------------------------------------------------------------------------- |
18
+ | Python (the reference implementation) | [`python/test/test_spec.py`](../python/test/test_spec.py) |
19
+ | TypeScript / Node.js | [`node/test/spec.test.ts`](../node/test/spec.test.ts) |
20
+ | Bash | [`bash/test/*.bats`](../bash/test) -- one file per fixture (`cli.bats` for `cli.json`, and so on) |
21
+
22
+ The Python and TypeScript harnesses also check that they run every `*.json`
23
+ file here, so a new fixture cannot be added without them learning about it.
24
+ The Bash tests are one file per fixture, so a new fixture needs a new `.bats`
25
+ file by hand.
26
+
27
+ ## Format
28
+
29
+ Each file is `{"spec_version": 1, "description": "...", "cases": [...]}`,
30
+ and every case has a unique `name`. A case that must fail carries
31
+ `"error": true` instead of an expected value; an expected `null` means
32
+ "unset".
33
+
34
+ A *setting* is `{"key": "port", "type": "int", "default": 8080}` -- `type`
35
+ is one of `bool`, `int`, `float`, `str`, `list`, and a `null` default means
36
+ the setting starts out unset. Settings are given as arrays so their order
37
+ is unambiguous. Declaring the type explicitly (rather than inferring it
38
+ from the default) keeps the fixtures usable from a language where `3` and
39
+ `3.0` are the same number.
40
+
41
+ | File | What it pins down | Case fields |
42
+ | -------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
43
+ | `naming.json` | env var, CLI flag, negated CLI flag and config key for a setting | `app`, `key`; `env_var`, `cli_flag`, `cli_negated_flag`, `config_key` |
44
+ | `casters.json` | casting a raw value to a declared type | `type`, `input`; `output` or `error` |
45
+ | `dotenv.json` | the `.env` dialect | `text` (the file, UTF-8); `expect` |
46
+ | `merge.json` | layering and precedence | `settings`, `layers` (`config`, `env`, `developer`, `cli`); `expect` or `error` |
47
+ | `config_layers.json` | merging system, user, project and sibling config files | `settings`, `table_path`, `files`, optional `aux_pattern`; `expect` or `error` |
48
+ | `config_tables.json` | choosing which TOML table to read | `op` (`parse` or `resolve`), inputs; `expect` or `error` |
49
+ | `templates.json` | generated env, TOML and CLI templates | `app`, `settings`, optional `options`; `expect` (`env`, `toml`, `cli`) |
50
+ | `gitignore.json` | whether gitignore rules match a path | `files`, `paths` (`path`, `ignored`) |
51
+ | `guard.json` | the gitignore guard for a private file | `files`, `target`, optional `kill_switch_var` and `env`; `expect` (`active`, `reason`) |
52
+ | `invocation.json` | reproducing a resolved configuration as a command line | `settings`, `resolved`, optional `options` (`prog`, `always_include`, `skip`, `compare_defaults`); `expect` (the command line) |
53
+ | `cli.json` | what a command line gives a setting, and flag collisions | `settings`, `argv`; `expect` (the CLI layer) or `error` |
54
+ | `sources.json` | the report of which sources are in play | `app`, `options` (the sources configured), `files`, optional `env`; `expect` (the whole report) |
55
+
56
+ The top-level fields of each file, and the meaning of every option, are in
57
+ its `description`.
58
+
59
+ ## Rules for the fixtures
60
+
61
+ - **Expected values are written by hand from the intended behavior**, not
62
+ recorded from an implementation, so the fixtures can disagree with one --
63
+ that is their job. The exception is `gitignore.json`, where every
64
+ verdict comes from `git check-ignore --no-index` (with no global or
65
+ system git configuration); the Python suite re-checks them against a
66
+ real `git` whenever one is installed.
67
+ - **Only intended behavior is pinned down.** Where implementations may
68
+ reasonably differ (numbers beyond 2^53, `inf`, underscores in numerals,
69
+ exotic Unicode line breaks, exponent formatting, ...) there is
70
+ deliberately no case; the concept document lists them under
71
+ "Implementation-defined".
72
+ - **Reason strings are part of the spec** (`guard.json`): they are what a
73
+ person sees when a private file is not being used.
74
+ - **Adding or changing a case is a spec change.** Say so in the concept
75
+ document; bump the spec version only for a change that a conforming
76
+ implementation must react to.
@@ -1,5 +1,5 @@
1
1
  {
2
- "spec_version": 1,
2
+ "spec_version": 2,
3
3
  "description": "Turning a raw value from any layer into a setting's declared type. `output: null` means unset; `error: true` means the cast fails.",
4
4
  "cases": [
5
5
  {