conclude 1.0.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 (44) hide show
  1. conclude-1.0.0/.github/dependabot.yml +10 -0
  2. conclude-1.0.0/.github/workflows/ci.yml +109 -0
  3. conclude-1.0.0/.github/workflows/publish.yml +125 -0
  4. conclude-1.0.0/.gitignore +29 -0
  5. conclude-1.0.0/CHANGELOG.md +7 -0
  6. conclude-1.0.0/LICENSE +7 -0
  7. conclude-1.0.0/PKG-INFO +260 -0
  8. conclude-1.0.0/README.md +229 -0
  9. conclude-1.0.0/docs/comparison.md +71 -0
  10. conclude-1.0.0/docs/guide.md +739 -0
  11. conclude-1.0.0/docs/reference.md +211 -0
  12. conclude-1.0.0/pyproject.toml +93 -0
  13. conclude-1.0.0/src/conclude/__init__.py +189 -0
  14. conclude-1.0.0/src/conclude/app.py +819 -0
  15. conclude-1.0.0/src/conclude/casters.py +262 -0
  16. conclude-1.0.0/src/conclude/developer.py +215 -0
  17. conclude-1.0.0/src/conclude/env.py +176 -0
  18. conclude-1.0.0/src/conclude/files.py +247 -0
  19. conclude-1.0.0/src/conclude/formatters.py +100 -0
  20. conclude-1.0.0/src/conclude/guard.py +172 -0
  21. conclude-1.0.0/src/conclude/infer.py +116 -0
  22. conclude-1.0.0/src/conclude/merge.py +58 -0
  23. conclude-1.0.0/src/conclude/naming.py +58 -0
  24. conclude-1.0.0/src/conclude/paths.py +26 -0
  25. conclude-1.0.0/src/conclude/py.typed +0 -0
  26. conclude-1.0.0/src/conclude/templates.py +83 -0
  27. conclude-1.0.0/src/conclude/tomlwrite.py +29 -0
  28. conclude-1.0.0/tests/test_app.py +650 -0
  29. conclude-1.0.0/tests/test_casters.py +260 -0
  30. conclude-1.0.0/tests/test_comparison.py +145 -0
  31. conclude-1.0.0/tests/test_developer.py +742 -0
  32. conclude-1.0.0/tests/test_docs.py +246 -0
  33. conclude-1.0.0/tests/test_dotenv_guard.py +213 -0
  34. conclude-1.0.0/tests/test_env.py +100 -0
  35. conclude-1.0.0/tests/test_files.py +354 -0
  36. conclude-1.0.0/tests/test_formatters.py +60 -0
  37. conclude-1.0.0/tests/test_guard.py +109 -0
  38. conclude-1.0.0/tests/test_infer.py +59 -0
  39. conclude-1.0.0/tests/test_merge.py +65 -0
  40. conclude-1.0.0/tests/test_naming.py +57 -0
  41. conclude-1.0.0/tests/test_repo_hygiene.py +190 -0
  42. conclude-1.0.0/tests/test_templates.py +425 -0
  43. conclude-1.0.0/tests/test_tomlwrite_and_paths.py +38 -0
  44. conclude-1.0.0/uv.lock +948 -0
@@ -0,0 +1,10 @@
1
+ # Keeps the SHA-pinned actions in .github/workflows/ current.
2
+ version: 2
3
+ updates:
4
+ - package-ecosystem: github-actions
5
+ directory: /
6
+ schedule:
7
+ interval: weekly
8
+ groups:
9
+ actions:
10
+ patterns: ["*"]
@@ -0,0 +1,109 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_call: # publish.yml runs this before it publishes anything
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ # Every third-party action is pinned to a full commit SHA (the version is
13
+ # in the trailing comment); .github/dependabot.yml keeps them current.
14
+
15
+ jobs:
16
+ test:
17
+ name: pytest (Python ${{ matrix.python-version }})
18
+ runs-on: ubuntu-latest
19
+ timeout-minutes: 10
20
+ strategy:
21
+ fail-fast: false
22
+ matrix:
23
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
24
+ steps:
25
+ - name: Check out
26
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
27
+ with:
28
+ persist-credentials: false
29
+ - name: Set up uv and Python ${{ matrix.python-version }}
30
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
31
+ with:
32
+ python-version: ${{ matrix.python-version }}
33
+ - name: Install the project and its dev tools (pathspec included)
34
+ run: uv sync --locked
35
+ - name: pytest
36
+ run: uv run pytest -q
37
+
38
+ lint:
39
+ name: ruff and mypy
40
+ runs-on: ubuntu-latest
41
+ timeout-minutes: 10
42
+ steps:
43
+ - name: Check out
44
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
45
+ with:
46
+ persist-credentials: false
47
+ - name: Set up uv and Python 3.11 (the oldest supported version)
48
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
49
+ with:
50
+ python-version: "3.11"
51
+ - name: Install the project and its dev tools
52
+ run: uv sync --locked
53
+ - name: ruff check
54
+ run: uv run ruff check .
55
+ - name: ruff format --check
56
+ run: uv run ruff format --check .
57
+ - name: mypy
58
+ run: uv run mypy
59
+
60
+ no-extras:
61
+ name: pytest with no optional dependencies
62
+ runs-on: ubuntu-latest
63
+ timeout-minutes: 10
64
+ steps:
65
+ - name: Check out
66
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
67
+ with:
68
+ persist-credentials: false
69
+ - name: Set up uv and Python 3.13
70
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
71
+ with:
72
+ python-version: "3.13"
73
+ - name: Install the package on its own, plus pytest (no pathspec)
74
+ run: |
75
+ uv venv "$RUNNER_TEMP/venv-plain"
76
+ uv pip install --python "$RUNNER_TEMP/venv-plain/bin/python" . pytest
77
+ - name: pytest (the pathspec-dependent tests skip themselves)
78
+ run: |
79
+ "$RUNNER_TEMP/venv-plain/bin/python" -m pytest -q
80
+
81
+ build:
82
+ name: build and twine check
83
+ runs-on: ubuntu-latest
84
+ timeout-minutes: 10
85
+ steps:
86
+ - name: Check out
87
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
88
+ with:
89
+ persist-credentials: false
90
+ - name: Set up uv and Python 3.13
91
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
92
+ with:
93
+ python-version: "3.13"
94
+ - name: Build the sdist and wheel
95
+ run: uv build
96
+ - name: twine check
97
+ run: uvx twine check --strict dist/*
98
+ - name: Smoke-test the wheel in a clean environment
99
+ run: |
100
+ uv venv "$RUNNER_TEMP/venv-wheel"
101
+ uv pip install --python "$RUNNER_TEMP/venv-wheel/bin/python" dist/*.whl
102
+ "$RUNNER_TEMP/venv-wheel/bin/python" - <<'PY'
103
+ import pathlib
104
+
105
+ import conclude
106
+
107
+ print("conclude", conclude.__version__)
108
+ assert (pathlib.Path(conclude.__file__).parent / "py.typed").exists()
109
+ PY
@@ -0,0 +1,125 @@
1
+ name: Publish
2
+
3
+ # One-time setup, before the first run:
4
+ # 1. On https://pypi.org (and https://test.pypi.org), add a trusted publisher
5
+ # for the project `conclude`: this repository, workflow `publish.yml`,
6
+ # environment `pypi` (`testpypi` on TestPyPI). For a project that doesn't
7
+ # exist yet, use "Publishing" -> "Add a new pending publisher".
8
+ # 2. In this repository's Settings -> Environments, create `pypi` and
9
+ # `testpypi`. Put required reviewers on `pypi` to make a real release
10
+ # wait for an explicit approval.
11
+ #
12
+ # Then:
13
+ # - A dry run: Actions -> Publish -> Run workflow -> target `testpypi`.
14
+ # - A release: publish a GitHub Release whose tag is `v<version>`.
15
+
16
+ on:
17
+ release:
18
+ types: [published]
19
+ workflow_dispatch:
20
+ inputs:
21
+ target:
22
+ description: Where to publish
23
+ type: choice
24
+ options: [testpypi, pypi]
25
+ default: testpypi
26
+
27
+ permissions:
28
+ contents: read
29
+
30
+ jobs:
31
+ ci:
32
+ name: CI
33
+ uses: ./.github/workflows/ci.yml
34
+
35
+ build:
36
+ name: Build the distributions
37
+ needs: ci
38
+ runs-on: ubuntu-latest
39
+ timeout-minutes: 10
40
+ env:
41
+ TARGET: ${{ github.event_name == 'release' && 'pypi' || inputs.target }}
42
+ steps:
43
+ - name: Check out
44
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
45
+ with:
46
+ persist-credentials: false
47
+ - name: Set up uv and Python 3.13
48
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
49
+ with:
50
+ python-version: "3.13"
51
+ - name: Read the version
52
+ id: version
53
+ run: echo "version=$(grep -oP '^__version__ = "\K[^"]+' src/conclude/__init__.py)" >> "$GITHUB_OUTPUT"
54
+ - name: Check the release is ready (real PyPI only)
55
+ if: env.TARGET == 'pypi'
56
+ env:
57
+ TAG: ${{ github.event.release.tag_name }}
58
+ VERSION: ${{ steps.version.outputs.version }}
59
+ run: |
60
+ if [ -n "$TAG" ] && [ "$TAG" != "v$VERSION" ]; then
61
+ echo "::error::The release tag $TAG does not match the package version v$VERSION."
62
+ exit 1
63
+ fi
64
+ if ! grep -q "^## \[$VERSION\]" CHANGELOG.md; then
65
+ echo "::error::CHANGELOG.md has no entry for $VERSION."
66
+ exit 1
67
+ fi
68
+ if grep -q "^## \[$VERSION\] - Unreleased" CHANGELOG.md; then
69
+ echo "::error::CHANGELOG.md still marks $VERSION as Unreleased; put the date on it."
70
+ exit 1
71
+ fi
72
+ - name: Build the sdist and wheel
73
+ run: uv build
74
+ - name: twine check
75
+ run: uvx twine check --strict dist/*
76
+ - name: Upload the distributions
77
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
78
+ with:
79
+ name: dist
80
+ path: dist/
81
+ if-no-files-found: error
82
+ retention-days: 7
83
+
84
+ publish-testpypi:
85
+ name: Publish to TestPyPI
86
+ needs: build
87
+ if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
88
+ runs-on: ubuntu-latest
89
+ timeout-minutes: 10
90
+ environment:
91
+ name: testpypi
92
+ url: https://test.pypi.org/p/conclude
93
+ permissions:
94
+ id-token: write # trusted publishing (OIDC); no API token is stored
95
+ steps:
96
+ - name: Download the distributions
97
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
98
+ with:
99
+ name: dist
100
+ path: dist/
101
+ - name: Publish to TestPyPI
102
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
103
+ with:
104
+ repository-url: https://test.pypi.org/legacy/
105
+ skip-existing: true
106
+
107
+ publish-pypi:
108
+ name: Publish to PyPI
109
+ needs: build
110
+ if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.target == 'pypi')
111
+ runs-on: ubuntu-latest
112
+ timeout-minutes: 10
113
+ environment:
114
+ name: pypi
115
+ url: https://pypi.org/p/conclude
116
+ permissions:
117
+ id-token: write # trusted publishing (OIDC); no API token is stored
118
+ steps:
119
+ - name: Download the distributions
120
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
121
+ with:
122
+ name: dist
123
+ path: dist/
124
+ - name: Publish to PyPI
125
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
@@ -0,0 +1,29 @@
1
+ # Local configuration / secrets (the names the docs use; never commit these)
2
+ .env
3
+ .env.local
4
+ .developer.toml
5
+ *.local.toml
6
+
7
+ # Python
8
+ __pycache__/
9
+ *.egg-info/
10
+ *.py[cod]
11
+
12
+ # Test / type-check / lint caches
13
+ .coverage
14
+ .mypy_cache/
15
+ .pytest_cache/
16
+ .ruff_cache/
17
+ htmlcov/
18
+
19
+ # Build artifacts
20
+ build/
21
+ dist/
22
+
23
+ # Virtual environments
24
+ .venv/
25
+ .venv-*/
26
+ venv/
27
+
28
+ # OS
29
+ .DS_Store
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file. The
4
+ format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
+ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [1.0.0] - 2026-09-20
conclude-1.0.0/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2026 Payam Tanaka
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,260 @@
1
+ Metadata-Version: 2.5
2
+ Name: conclude
3
+ Version: 1.0.0
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
+ Project-URL: Homepage, https://github.com/tanakapayam/conclude
6
+ Project-URL: Documentation, https://github.com/tanakapayam/conclude/blob/main/docs/guide.md
7
+ Project-URL: Repository, https://github.com/tanakapayam/conclude
8
+ Project-URL: Issues, https://github.com/tanakapayam/conclude/issues
9
+ Project-URL: Changelog, https://github.com/tanakapayam/conclude/blob/main/CHANGELOG.md
10
+ Author: Payam Tanaka
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: cli,config,configuration,dotenv,environment-variables,settings,toml
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Software Development :: Libraries
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.11
28
+ Provides-Extra: gitignore
29
+ Requires-Dist: pathspec>=0.12; extra == 'gitignore'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # conclude
33
+
34
+ A setting that can come from a flag, an environment variable, or a
35
+ config file usually gets written down three times: an argparse flag, an
36
+ environment variable, and a config-file key -- each with its own name,
37
+ its own type conversion, and its own copy of the default. Add a setting
38
+ and you touch three places; rename one and the other two quietly drift.
39
+
40
+ conclude has you write it once. Set up your defaults; conclude works
41
+ out the rest. You write down a setting's name, its type, and its
42
+ starting value -- as an entry in a plain dict -- and conclude infers
43
+ the environment variable name, the config-file key, the CLI flag, and
44
+ how to cast a raw value from any of those into the right type.
45
+
46
+ ```python
47
+ import conclude
48
+
49
+ DEFAULTS = {
50
+ "host": "localhost",
51
+ "port": 8080,
52
+ "debug": False,
53
+ "tags": conclude.opt(list), # unset by default, but still a list when set
54
+ }
55
+ app = conclude.App("myapp", DEFAULTS)
56
+
57
+ parser = app.build_arg_parser(prog="myapp")
58
+ namespace = parser.parse_args()
59
+ cli = {k: v for k, v in vars(namespace).items() if v is not None}
60
+ print(app.resolve(cli))
61
+ ```
62
+
63
+ ```
64
+ $ MYAPP_PORT=9000 myapp --debug --tags a,b
65
+ {'host': 'localhost', 'port': 9000, 'debug': True, 'tags': ['a', 'b']}
66
+ ```
67
+
68
+ That one dict gave you the CLI flags (`--host`, `--port`, `--debug`,
69
+ `--tags`), the environment variables (`MYAPP_HOST`, `MYAPP_PORT`, ...),
70
+ the config-file keys (a `[myapp]` table in `~/.config/myapp/config.toml`
71
+ or `./.config.toml`), and a caster for each -- with no second list of
72
+ names to keep in sync.
73
+
74
+ ## Where it fits
75
+
76
+ Configuration libraries tend to start from different things. conclude
77
+ starts from a dict of defaults and derives the rest:
78
+
79
+ | Package | Primary abstraction | Reach for it when... |
80
+ | ----------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------- |
81
+ | `argparse` | CLI parser | you only need flags |
82
+ | ConfigArgParse | argparse plus config files and environment variables | you have an argparse CLI and want files and env vars added |
83
+ | jsonargparse | CLI and config built from type hints | your program is functions, classes or dataclasses with type hints |
84
+ | python-dotenv | `.env` loader | you just need a `.env` in `os.environ` |
85
+ | python-decouple | individual value reader (env, then file, then default) | you read a handful of values, Django-style |
86
+ | pydantic-settings | typed, validated settings model | you want validation, nested models or secret-manager sources |
87
+ | Dynaconf | general-purpose layered configuration system | you want named environments, many file formats, or Vault |
88
+ | Hydra / OmegaConf | hierarchical, composable configuration | you compose config groups, run sweeps or manage experiments |
89
+ | **conclude** | **defaults → inferred env / TOML / CLI interfaces → layered resolution** | **you want all three interfaces from one dict, and safe private local overrides** |
90
+
91
+ conclude deliberately does not do schema validation (values are cast,
92
+ not validated), secret-manager backends, YAML or JSON files, or named
93
+ environments; the table says where to look for those. For a dated,
94
+ feature-by-feature comparison, see
95
+ [How conclude compares](https://github.com/tanakapayam/conclude/blob/main/docs/comparison.md).
96
+
97
+ ## Design principles
98
+
99
+ - **One source of truth.** The defaults dict. Flags, environment
100
+ variable names, config keys, casters and the generated templates are
101
+ all derived from it, so they can't drift apart.
102
+ - **Opt in to anything unusual.** The system file, `.env` and the
103
+ developer file are off until you ask; the user and project files are
104
+ one `None` from off.
105
+ - **Private files have to prove they're private.** The developer file
106
+ (and `.env`, if you ask) is read only when git is ignoring it.
107
+ - **Quiet for ordinary situations, loud for broken setups.** A missing
108
+ local file is normal and silent, and `describe_sources()` explains
109
+ it; a malformed developer file or a missing `pathspec` raises.
110
+ - **Standard library only.** `pathspec`, for the gitignore check, is an
111
+ optional extra.
112
+
113
+ ## The layers
114
+
115
+ ```
116
+ 1. CLI flags
117
+ 2. Developer config file (opt-in; private, gitignored, project-local,
118
+ TOML or dotenv -- beats the environment)
119
+ 3. Environment variables (with an opt-in `.env` file as a fallback)
120
+ 4. Config file(s), in this order (each overrides the previous):
121
+ a. /etc/<app>/config.toml (system-wide; opt-in, off by default)
122
+ b. ~/.config/<app>/config.toml (user-global, overrides system)
123
+ c. ./.config.toml (project-local, overrides user)
124
+ d. ./.config.*.toml (sibling files, sorted by name)
125
+ 5. Hardcoded defaults
126
+ ```
127
+
128
+ Or, lowest priority to highest:
129
+
130
+ ```
131
+ defaults < system < user < project < env < developer < CLI
132
+ ```
133
+
134
+ Everything hangs off that one dict: the layers it feeds, and what
135
+ conclude infers from it so you never write any of it twice:
136
+
137
+ ```
138
+ ┌── CLI
139
+ defaults ───────┼── developer config (explicit opt-in, must be gitignored)
140
+ │ ├── environment
141
+ │ ├── .env (explicit opt-in)
142
+ │ └── config files
143
+ │
144
+ └── inference
145
+ ├── caster
146
+ ├── formatter
147
+ ├── CLI name
148
+ └── environment name
149
+ ```
150
+
151
+ (The layers are listed highest priority first; `defaults` sits beneath
152
+ all of them.) The user and project config files are on by default, and
153
+ each is one `None` away from off. The system file, `.env`, and the
154
+ developer file are off until you ask for them.
155
+
156
+ ## Local files: which one?
157
+
158
+ Two of the layers are about your own machine, and each comes in two
159
+ formats:
160
+
161
+ | Format | Below the environment | Above the environment |
162
+ | ------ | ----------------------------------------- | ------------------------------------------ |
163
+ | TOML | the system, user and project config files | the developer file, e.g. `.developer.toml` |
164
+ | dotenv | `.env` | the developer file, e.g. `.env.local` |
165
+
166
+ - **Non-secret local defaults you're happy to commit** -- use `.env`.
167
+ A real `export` in the shell still beats it, so it can't override a
168
+ deployment.
169
+ - **Private values that must win over stray shell exports** (a local
170
+ `DATABASE_URL`, say) -- use the developer file. Make it TOML if only
171
+ your app reads it (it gets `[table]`s, including per-recipient ones);
172
+ make it dotenv, like `.env.local`, if docker compose, direnv or your
173
+ IDE need the same values.
174
+ - **A private `.env` that stays below the environment** -- use `.env`
175
+ with `dotenv_require_gitignored=True`.
176
+
177
+ The developer file's location lives in your committed `pyproject.toml`
178
+ (any name works; the name decides the format, and only `.toml` means
179
+ TOML):
180
+
181
+ ```toml
182
+ [tool.conclude.developer]
183
+ config = ".developer.toml" # or ".env.local"
184
+ ```
185
+
186
+ ```python
187
+ app = conclude.App(
188
+ "myapp",
189
+ DEFAULTS,
190
+ pyproject_path=conclude.AUTO, # the developer file
191
+ dotenv_path=conclude.AUTO, # .env
192
+ )
193
+ ```
194
+
195
+ The developer file is always guarded, and `.env` is when you ask: the
196
+ file is used only if it exists, sits in a git working tree, and is
197
+ gitignored. Otherwise it is skipped quietly, and `describe_sources()`
198
+ says why. Details: [the `.env` file](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#6-local-development-a-env-file), [the developer file](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#8-a-private-developer-config-file).
199
+
200
+ ## What's in it
201
+
202
+ - **Inference** of casters, formatters, CLI flags, env var names and
203
+ config keys from the defaults alone; `opt(type)` for a setting that
204
+ starts out unset ([guide, section 1](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#1-the-bare-minimum), [section 3](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#3-when-inference-isnt-quite-enough-casters-and-formatters)).
205
+ - **Config-file tables**, including a positional shorthand that picks a
206
+ table per recipient/deck/profile ([section 4](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#4-a-positional-shorthand--per-recipient-config-tables)).
207
+ - **`--print-invocation`** and `App.format_invocation()`: print the
208
+ command line that reproduces a resolved configuration
209
+ ([section 5](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#5-debugging-and-documentation---print-invocation)).
210
+ - **A `.env` fallback**, optionally required to be gitignored, sitting
211
+ beneath real environment variables ([section 6](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#6-local-development-a-env-file)).
212
+ - **A system-wide config file**, the lowest-priority file ([section 7](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#7-machine-wide-defaults-a-system-config-file)).
213
+ - **A private developer file**, TOML or dotenv, that beats ambient
214
+ environment variables ([section 8](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#8-a-private-developer-config-file)).
215
+ - **`describe_sources()`** for `--help`: which sources are in play, and
216
+ why a given one isn't ([section 9](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#9-turning-off-a-source-and-telling-the-user)).
217
+ - **Templates and docs generated from your defaults**:
218
+ `format_env()`, `format_toml()`, `format_cli()`
219
+ ([section 10](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md#10-generating-docs-and-templates)).
220
+ - Standard library only, fully typed (`py.typed`), Python 3.11+. The
221
+ gitignore check (developer file, strict `.env`) uses the optional
222
+ `pathspec` package.
223
+
224
+ ## Install
225
+
226
+ ```
227
+ pip install conclude
228
+ ```
229
+
230
+ The developer config layer (and a `.env` you ask to be held to the same
231
+ standard) needs one small pure-Python dependency, only to check that a
232
+ private file is gitignored:
233
+
234
+ ```
235
+ pip install 'conclude[gitignore]'
236
+ ```
237
+
238
+ ## Documentation
239
+
240
+ - [Guide](https://github.com/tanakapayam/conclude/blob/main/docs/guide.md) -- builds a small CLI, `remind`, one
241
+ idea at a time.
242
+ - [Reference](https://github.com/tanakapayam/conclude/blob/main/docs/reference.md) -- every class, method, and
243
+ module.
244
+ - [Changelog](https://github.com/tanakapayam/conclude/blob/main/CHANGELOG.md).
245
+
246
+ ## Development
247
+
248
+ ```
249
+ uv sync
250
+ uv run pytest
251
+ uv run ruff check . && uv run ruff format --check .
252
+ uv run mypy
253
+ uv build && uvx twine check --strict dist/*
254
+ ```
255
+
256
+ CI runs these same commands on Python 3.11 through 3.14.
257
+
258
+ ## License
259
+
260
+ MIT -- see [LICENSE](https://github.com/tanakapayam/conclude/blob/main/LICENSE).