lib-layered-config 1.0.0__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.

Potentially problematic release.


This version of lib-layered-config might be problematic. Click here for more details.

Files changed (111) hide show
  1. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/.github/workflows/ci.yml +7 -27
  2. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/.github/workflows/codeql.yml +3 -3
  3. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/AGENTS.md +2 -2
  4. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/CHANGELOG.md +16 -0
  5. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/PKG-INFO +23 -18
  6. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/README.md +6 -1
  7. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/docs/systemdesign/module_reference.md +29 -0
  8. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/pyproject.toml +17 -17
  9. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/_utils.py +156 -15
  10. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/build.py +3 -8
  11. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/bump.py +4 -9
  12. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/bump_major.py +2 -6
  13. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/bump_minor.py +2 -6
  14. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/bump_patch.py +2 -6
  15. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/clean.py +2 -2
  16. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/cli.py +81 -74
  17. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/dev.py +2 -8
  18. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/help.py +2 -1
  19. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/install.py +2 -8
  20. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/menu.py +16 -16
  21. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/push.py +13 -12
  22. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/release.py +19 -37
  23. lib_layered_config-1.1.0/scripts/run_cli.py +154 -0
  24. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/target_metadata.py +19 -7
  25. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/test.py +112 -44
  26. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/version_current.py +1 -6
  27. lib_layered_config-1.1.0/src/lib_layered_config/__init__conf__.py +67 -0
  28. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/adapters/file_loaders/structured.py +103 -16
  29. lib_layered_config-1.1.0/src/lib_layered_config/cli/__init__.py +144 -0
  30. lib_layered_config-1.1.0/src/lib_layered_config/cli/common.py +440 -0
  31. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/examples/generate.py +1 -1
  32. lib_layered_config-1.1.0/tests/adapters/test_file_loaders.py +107 -0
  33. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/e2e/test_cli.py +25 -3
  34. lib_layered_config-1.1.0/tests/unit/test_cli_helpers.py +253 -0
  35. lib_layered_config-1.0.0/scripts/run_cli.py +0 -137
  36. lib_layered_config-1.0.0/src/lib_layered_config/cli/__init__.py +0 -162
  37. lib_layered_config-1.0.0/src/lib_layered_config/cli/common.py +0 -232
  38. lib_layered_config-1.0.0/tests/adapters/test_file_loaders.py +0 -89
  39. lib_layered_config-1.0.0/tests/unit/test_cli_helpers.py +0 -305
  40. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/.devcontainer/devcontainer.json +0 -0
  41. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/.devcontainer/settings.json +0 -0
  42. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/.env.example +0 -0
  43. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/.github/dependabot.yml +0 -0
  44. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/.github/workflows/release.yml +0 -0
  45. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/.gitignore +0 -0
  46. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/.qlty/qlty.toml +0 -0
  47. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/.snyk +0 -0
  48. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/CONTRIBUTING.md +0 -0
  49. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/DEVELOPMENT.md +0 -0
  50. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/LICENSE +0 -0
  51. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/Makefile +0 -0
  52. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/codecov.yml +0 -0
  53. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/docs/systemdesign/concept.md +0 -0
  54. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/docs/systemdesign/test_matrix.md +0 -0
  55. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/notebooks/Quickstart.ipynb +0 -0
  56. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/__init__.py +0 -0
  57. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/__main__.py +0 -0
  58. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/scripts/bump_version.py +0 -0
  59. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/__init__.py +0 -0
  60. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/__main__.py +0 -0
  61. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/_layers.py +0 -0
  62. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/_platform.py +0 -0
  63. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/adapters/__init__.py +0 -0
  64. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/adapters/dotenv/__init__.py +0 -0
  65. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/adapters/dotenv/default.py +0 -0
  66. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/adapters/env/__init__.py +0 -0
  67. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/adapters/env/default.py +0 -0
  68. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/adapters/file_loaders/__init__.py +0 -0
  69. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/adapters/path_resolvers/__init__.py +0 -0
  70. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/adapters/path_resolvers/default.py +0 -0
  71. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/application/__init__.py +0 -0
  72. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/application/merge.py +0 -0
  73. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/application/ports.py +0 -0
  74. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/cli/constants.py +0 -0
  75. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/cli/deploy.py +0 -0
  76. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/cli/fail.py +0 -0
  77. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/cli/generate.py +0 -0
  78. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/cli/info.py +0 -0
  79. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/cli/read.py +0 -0
  80. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/core.py +0 -0
  81. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/domain/__init__.py +0 -0
  82. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/domain/config.py +0 -0
  83. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/domain/errors.py +0 -0
  84. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/examples/__init__.py +0 -0
  85. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/examples/deploy.py +0 -0
  86. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/observability.py +0 -0
  87. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/py.typed +0 -0
  88. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/src/lib_layered_config/testing.py +0 -0
  89. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/test-serialable.toml +0 -0
  90. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/test.toml +0 -0
  91. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/__init__.py +0 -0
  92. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/adapters/test_dotenv_loader.py +0 -0
  93. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/adapters/test_env_loader.py +0 -0
  94. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/adapters/test_path_resolver.py +0 -0
  95. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/adapters/test_port_contracts.py +0 -0
  96. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/application/test_merge.py +0 -0
  97. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/conftest.py +0 -0
  98. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/e2e/test_notebooks.py +0 -0
  99. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/e2e/test_read_config.py +0 -0
  100. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/examples/test_deploy.py +0 -0
  101. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/examples/test_generate.py +0 -0
  102. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/support/__init__.py +0 -0
  103. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/support/layered.py +0 -0
  104. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/support/os_markers.py +0 -0
  105. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/unit/test_config.py +0 -0
  106. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/unit/test_core.py +0 -0
  107. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/unit/test_errors.py +0 -0
  108. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/unit/test_examples.py +0 -0
  109. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/unit/test_layers_helpers.py +0 -0
  110. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/unit/test_observability.py +0 -0
  111. {lib_layered_config-1.0.0 → lib_layered_config-1.1.0}/tests/unit/test_testing.py +0 -0
@@ -118,7 +118,7 @@ jobs:
118
118
  pipx install dist/*.whl
119
119
  "$CLI_BIN" --version 2>/dev/null || python -m "$PACKAGE_MODULE" --version
120
120
  - name: Install uv
121
- uses: astral-sh/setup-uv@v6
121
+ uses: astral-sh/setup-uv@v7
122
122
  with:
123
123
  enable-cache: true
124
124
  - name: uv tool install
@@ -145,42 +145,22 @@ jobs:
145
145
  PIP_NO_INPUT: "1"
146
146
  run: |
147
147
  python - << 'PY'
148
- import json, sys
149
148
  from pathlib import Path
149
+
150
150
  import nbformat
151
- try:
152
- from nbformat.validator import normalize # type: ignore
153
- except Exception:
154
- normalize = None # type: ignore
155
151
  from nbclient import NotebookClient
156
152
 
157
153
  nb_path = Path('notebooks/Quickstart.ipynb')
158
154
  if not nb_path.exists():
159
155
  raise SystemExit(f"Notebook not found: {nb_path}")
160
156
 
161
- with nb_path.open('r', encoding='utf-8') as f:
162
- nb = nbformat.read(f, as_version=4)
163
- # Normalize to ensure required fields like cell ids are present (supports older nbformat return types)
164
- if normalize is not None:
165
- nb_norm = normalize(nb) # may return notebook or a tuple
166
- # pick the first element that looks like a notebook
167
- cand = nb_norm
168
- if isinstance(nb_norm, tuple):
169
- for item in nb_norm:
170
- if hasattr(item, 'cells') and hasattr(item, 'metadata'):
171
- cand = item
172
- break
173
- nb = cand
174
- # Final guard for robustness
175
- if isinstance(nb, tuple) or not (hasattr(nb, 'cells') and hasattr(nb, 'metadata')):
176
- raise SystemExit(f"Unexpected notebook object type after normalize: {type(nb)}")
177
-
178
- client = NotebookClient(nb, timeout=900, kernel_name='python3', allow_errors=False)
157
+ with nb_path.open('r', encoding='utf-8') as handle:
158
+ notebook = nbformat.read(handle, as_version=4)
159
+ client = NotebookClient(notebook, timeout=900, kernel_name='python3', allow_errors=False)
179
160
  client.execute()
180
161
 
181
- # Optionally write executed notebook artifact (not committed)
182
162
  out_path = Path('notebooks/Quickstart-executed.ipynb')
183
- with out_path.open('w', encoding='utf-8') as f:
184
- nbformat.write(nb, f)
163
+ with out_path.open('w', encoding='utf-8') as handle:
164
+ nbformat.write(notebook, handle)
185
165
  print(f"Executed notebook written to: {out_path}")
186
166
  PY
@@ -26,15 +26,15 @@ jobs:
26
26
  uses: actions/checkout@v4
27
27
 
28
28
  - name: Initialize CodeQL
29
- uses: github/codeql-action/init@v3
29
+ uses: github/codeql-action/init@v4
30
30
  with:
31
31
  languages: ${{ matrix.language }}
32
32
 
33
33
  - name: Autobuild
34
- uses: github/codeql-action/autobuild@v3
34
+ uses: github/codeql-action/autobuild@v4
35
35
 
36
36
  - name: Perform CodeQL Analysis
37
- uses: github/codeql-action/analyze@v3
37
+ uses: github/codeql-action/analyze@v4
38
38
  with:
39
39
  category: "/language:${{ matrix.language }}"
40
40
 
@@ -54,8 +54,8 @@ when writing or refracturing Python scripts, apply those Rules :
54
54
  ### Versioning & Releases
55
55
 
56
56
  - Single source of truth for the package version is `pyproject.toml` (`[project].version`).
57
- - Runtime code reads metadata via `importlib.metadata`; do not duplicate the version in code files.
58
- - On a version bump, update only `pyproject.toml` and the `CHANGELOG.md` entry; runtime code should continue to read the version via `importlib.metadata` (no separate `__init__conf__` file).
57
+ - Release automation mirrors the metadata into `src/lib_layered_config/__init__conf__.py`; runtime code imports those constants instead of calling `importlib.metadata`.
58
+ - On a version bump, update `pyproject.toml`, run the metadata sync script (or `make bump`), and record the change in `CHANGELOG.md`.
59
59
  - Tag releases `vX.Y.Z` and push tags; CI will build artifacts and publish when configured.
60
60
 
61
61
  ### Common Make Targets (Alphabetical)
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.1.0] - 2025-10-13
4
+
5
+ - Refactor CLI metadata commands (`info`, `--version`) to read from the
6
+ statically generated `__init__conf__` module, removing runtime
7
+ `importlib.metadata` lookups.
8
+ - Update CLI entrypoint to use `lib_cli_exit_tools.cli_session` for traceback
9
+ management, keeping the shared configuration in sync with the newer
10
+ `lib_cli_exit_tools` API without manual state restoration.
11
+ - Retire the `lib_layered_config.cli._default_env_prefix` compatibility export;
12
+ import `default_env_prefix` from `lib_layered_config.core` instead.
13
+ - Refresh dependency baselines to the latest stable releases (rich-click 1.9.3,
14
+ codecov-cli 11.2.3, PyYAML 6.0.3, ruff 0.14.0, etc.) and mark dataclasses with
15
+ `slots=True` where appropriate to embrace Python 3.13 idioms.
16
+ - Simplify the CI notebook smoke test to rely on upstream nbformat behaviour,
17
+ dropping compatibility shims for older notebook metadata schemas.
18
+
3
19
  ## [1.0.0] - 2025-10-09
4
20
 
5
21
  - Add optional `default_file` support to the composition root and CLI so baseline configuration files load ahead of layered overrides.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lib_layered_config
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Cross-platform layered configuration loader for Python
5
5
  Project-URL: Homepage, https://github.com/bitranox/lib_layered_config
6
6
  Project-URL: Repository, https://github.com/bitranox/lib_layered_config.git
@@ -16,26 +16,26 @@ Classifier: Programming Language :: Python :: 3 :: Only
16
16
  Classifier: Programming Language :: Python :: 3.13
17
17
  Classifier: Typing :: Typed
18
18
  Requires-Python: >=3.13
19
- Requires-Dist: lib-cli-exit-tools>=1.5.0
20
- Requires-Dist: rich-click>=1.9.2
19
+ Requires-Dist: lib-cli-exit-tools>=2.1.0
20
+ Requires-Dist: rich-click>=1.9.3
21
21
  Provides-Extra: dev
22
- Requires-Dist: bandit>=1.7.9; extra == 'dev'
23
- Requires-Dist: build>=1.3; extra == 'dev'
24
- Requires-Dist: codecov-cli>=0.6; extra == 'dev'
22
+ Requires-Dist: bandit>=1.8.6; extra == 'dev'
23
+ Requires-Dist: build>=1.3.0; extra == 'dev'
24
+ Requires-Dist: codecov-cli>=11.2.3; extra == 'dev'
25
25
  Requires-Dist: coverage[toml]>=7.10.7; extra == 'dev'
26
26
  Requires-Dist: hypothesis>=6.140.3; extra == 'dev'
27
- Requires-Dist: import-linter>=2.0; extra == 'dev'
28
- Requires-Dist: pip-audit>=2.7; extra == 'dev'
29
- Requires-Dist: pyright>=1.1; extra == 'dev'
30
- Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
31
- Requires-Dist: pytest-cov>=7.0; extra == 'dev'
32
- Requires-Dist: pytest>=8.4; extra == 'dev'
33
- Requires-Dist: pyyaml>=6.0; extra == 'dev'
34
- Requires-Dist: ruff>=0.13.3; extra == 'dev'
35
- Requires-Dist: textual>=6.1.0; extra == 'dev'
36
- Requires-Dist: twine>=6.2; extra == 'dev'
27
+ Requires-Dist: import-linter>=2.5.2; extra == 'dev'
28
+ Requires-Dist: pip-audit>=2.9.0; extra == 'dev'
29
+ Requires-Dist: pyright>=1.1.406; extra == 'dev'
30
+ Requires-Dist: pytest-asyncio>=1.2.0; extra == 'dev'
31
+ Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
32
+ Requires-Dist: pytest>=8.4.2; extra == 'dev'
33
+ Requires-Dist: pyyaml>=6.0.3; extra == 'dev'
34
+ Requires-Dist: ruff>=0.14.0; extra == 'dev'
35
+ Requires-Dist: textual>=6.3.0; extra == 'dev'
36
+ Requires-Dist: twine>=6.2.0; extra == 'dev'
37
37
  Provides-Extra: yaml
38
- Requires-Dist: pyyaml>=6.0; extra == 'yaml'
38
+ Requires-Dist: pyyaml>=6.0.3; extra == 'yaml'
39
39
  Description-Content-Type: text/markdown
40
40
 
41
41
  # lib_layered_config
@@ -342,6 +342,10 @@ make build # build wheel / sdist artifacts
342
342
  make run -- --help # run the CLI via the repo entrypoint
343
343
  ```
344
344
 
345
+ The development extra now targets the latest stable releases of the toolchain
346
+ (pytest 8.4.2, ruff 0.14.0, codecov-cli 11.2.3, etc.), so upgrading your local
347
+ environment before running `make` is recommended.
348
+
345
349
  *Formatting gate:* Ruff formatting runs in check mode during `make test`. Run `ruff format .` (or `pre-commit run --all-files`) before pushing and consider `pre-commit install` to keep local edits aligned.
346
350
 
347
351
  *Coverage gate:* the maintained test suite must stay ≥90% (see `pyproject.toml`). Add targeted unit tests if you extend functionality.
@@ -357,7 +361,8 @@ The GitHub Actions workflow executes three jobs:
357
361
 
358
362
  - **Test matrix** (Linux/macOS/Windows, Python 3.13 + latest 3.x) running the same pipeline as `make test`.
359
363
  - **pipx / uv verification** to prove the built wheel installs cleanly with the common Python app launchers.
360
- - **Notebook smoke test** that executes `notebooks/Quickstart.ipynb` to keep the tutorial in sync.
364
+ - **Notebook smoke test** that executes `notebooks/Quickstart.ipynb` to keep the tutorial in sync using the native nbformat workflow (no compatibility shims required).
365
+ - CLI jobs run through `lib_cli_exit_tools.cli_session`, ensuring the `--traceback` flag behaves the same locally and in automation.
361
366
 
362
367
  Packaging-specific jobs (conda, Nix, Homebrew sync) were retired; the Python packaging metadata in `pyproject.toml` remains the single source of truth.
363
368
 
@@ -302,6 +302,10 @@ make build # build wheel / sdist artifacts
302
302
  make run -- --help # run the CLI via the repo entrypoint
303
303
  ```
304
304
 
305
+ The development extra now targets the latest stable releases of the toolchain
306
+ (pytest 8.4.2, ruff 0.14.0, codecov-cli 11.2.3, etc.), so upgrading your local
307
+ environment before running `make` is recommended.
308
+
305
309
  *Formatting gate:* Ruff formatting runs in check mode during `make test`. Run `ruff format .` (or `pre-commit run --all-files`) before pushing and consider `pre-commit install` to keep local edits aligned.
306
310
 
307
311
  *Coverage gate:* the maintained test suite must stay ≥90% (see `pyproject.toml`). Add targeted unit tests if you extend functionality.
@@ -317,7 +321,8 @@ The GitHub Actions workflow executes three jobs:
317
321
 
318
322
  - **Test matrix** (Linux/macOS/Windows, Python 3.13 + latest 3.x) running the same pipeline as `make test`.
319
323
  - **pipx / uv verification** to prove the built wheel installs cleanly with the common Python app launchers.
320
- - **Notebook smoke test** that executes `notebooks/Quickstart.ipynb` to keep the tutorial in sync.
324
+ - **Notebook smoke test** that executes `notebooks/Quickstart.ipynb` to keep the tutorial in sync using the native nbformat workflow (no compatibility shims required).
325
+ - CLI jobs run through `lib_cli_exit_tools.cli_session`, ensuring the `--traceback` flag behaves the same locally and in automation.
321
326
 
322
327
  Packaging-specific jobs (conda, Nix, Homebrew sync) were retired; the Python packaging metadata in `pyproject.toml` remains the single source of truth.
323
328
 
@@ -715,6 +715,9 @@ observability helpers.
715
715
  ### Format Loaders
716
716
  - **Purpose:** Parse TOML/JSON/YAML into mappings.
717
717
  - **Location:** `structured.py`
718
+ - **Supporting Helpers:** `_ensure_yaml_available`, `_require_yaml_module`,
719
+ `_parse_yaml_bytes` lazily import PyYAML, explain missing dependencies, and
720
+ wrap parser errors with domain-specific context.
718
721
 
719
722
  ---
720
723
 
@@ -724,6 +727,8 @@ observability helpers.
724
727
  - Raises `NotFound` for missing files.
725
728
  - Raises `InvalidFormat` for parse failures with format-specific context.
726
729
  - Emits `config_file_read` / `config_file_loaded` events.
730
+ - YAML parsing uses `_parse_yaml_bytes` to normalise `None` payloads to empty
731
+ mappings and translate `YAMLError` into actionable `InvalidFormat`.
727
732
 
728
733
  ---
729
734
 
@@ -915,6 +920,12 @@ generating examples, and surfacing metadata without exposing internal APIs.
915
920
 
916
921
  - Rich Click command group with subcommands `read`, `read-json`, `deploy`,
917
922
  `generate-examples`, `env-prefix`, `info`, and `fail`.
923
+ - Metadata surfaces (`info`, `--version`) read directly from
924
+ `lib_layered_config.__init__conf__` so automation keeps CLI output in sync
925
+ with `pyproject.toml` without hitting `importlib.metadata` at runtime.
926
+ - Traceback handling delegates to `lib_cli_exit_tools.cli_session`, which
927
+ applies the `--traceback` preference via configuration overrides and restores
928
+ prior settings after each invocation.
918
929
  - Helpers to normalise options, render human output, and manage traceback
919
930
  preferences.
920
931
  - Integrates with ``lib_cli_exit_tools`` for consistent error messaging.
@@ -945,6 +956,24 @@ helpers.
945
956
  ### Helper Functions (`_render_json`, `_render_human`, `_normalise_*`)
946
957
  - **Purpose:** Keep command handlers declarative and reusable.
947
958
 
959
+ ### Metadata Helpers (`version_string`, `describe_distribution`)
960
+ - **Purpose:** Produce CLI-friendly metadata strings sourced from
961
+ `lib_layered_config.__init__conf__`.
962
+ - **Input:** Constants exposed via `__init__conf__.info_lines()` and related
963
+ helpers maintained by release automation.
964
+ - **Output:** Click `--version` banner and `info` command lines.
965
+ - **Location:** `src/lib_layered_config/cli/common.py`
966
+ - **Supporting Docs:** The metadata module also exports
967
+ `metadata_fields()`/`info_lines()` for structured and human rendering.
968
+
969
+ ### Traceback Support Helpers (`_session_overrides`)
970
+ - **Purpose:** Derive `lib_cli_exit_tools.cli_session` overrides from parsed CLI
971
+ arguments so the global `--traceback` flag works across entry points.
972
+ - **Input:** Raw argument sequences passed to the root CLI.
973
+ - **Output:** Mapping containing `{"traceback": True}` when verbose tracebacks
974
+ were requested; empty mapping otherwise.
975
+ - **Location:** `src/lib_layered_config/cli/__init__.py`
976
+
948
977
  ---
949
978
 
950
979
  ## Testing Approach
@@ -1,12 +1,12 @@
1
1
  [project]
2
2
  name = "lib_layered_config"
3
- version = "1.0.0"
3
+ version = "1.1.0"
4
4
  description = "Cross-platform layered configuration loader for Python"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
7
7
  dependencies = [
8
- "rich-click>=1.9.2",
9
- "lib_cli_exit_tools>=1.5.0",
8
+ "rich-click>=1.9.3",
9
+ "lib_cli_exit_tools>=2.1.0",
10
10
  ]
11
11
  license = { text = "MIT" }
12
12
  authors = [{ name = "bitranox", email = "bitranox@gmail.com" }]
@@ -27,24 +27,24 @@ Issues = "https://github.com/bitranox/lib_layered_config/issues"
27
27
 
28
28
  [project.optional-dependencies]
29
29
  dev = [
30
- "build>=1.3",
31
- "codecov-cli>=0.6",
30
+ "build>=1.3.0",
31
+ "codecov-cli>=11.2.3",
32
32
  "coverage[toml]>=7.10.7",
33
33
  "hypothesis>=6.140.3",
34
- "import-linter>=2.0",
35
- "bandit>=1.7.9",
36
- "pip-audit>=2.7",
37
- "PyYAML>=6.0",
38
- "pyright>=1.1",
39
- "pytest>=8.4",
40
- "pytest-asyncio>=0.23",
41
- "pytest-cov>=7.0",
42
- "ruff>=0.13.3",
43
- "textual>=6.1.0",
44
- "twine>=6.2"
34
+ "import-linter>=2.5.2",
35
+ "bandit>=1.8.6",
36
+ "pip-audit>=2.9.0",
37
+ "PyYAML>=6.0.3",
38
+ "pyright>=1.1.406",
39
+ "pytest>=8.4.2",
40
+ "pytest-asyncio>=1.2.0",
41
+ "pytest-cov>=7.0.0",
42
+ "ruff>=0.14.0",
43
+ "textual>=6.3.0",
44
+ "twine>=6.2.0"
45
45
  ]
46
46
  yaml = [
47
- "PyYAML>=6.0"
47
+ "PyYAML>=6.0.3"
48
48
  ]
49
49
 
50
50
  [project.scripts]
@@ -4,7 +4,7 @@ Purpose
4
4
  -------
5
5
  Collect helper functions used by the ``scripts/`` entry points (build, test,
6
6
  release) so git helpers and subprocess wrappers live in one place. The behaviour mirrors the operational guidance described in
7
- ``docs/systemdesign/concept_architecture_plan.md`` and ``DEVELOPMENT.md``.
7
+ ``docs/systemdesign/module_reference.md`` and ``DEVELOPMENT.md``.
8
8
 
9
9
  Contents
10
10
  --------
@@ -21,16 +21,19 @@ avoid duplication and keep CI/CD behaviour consistent with documentation.
21
21
 
22
22
  from __future__ import annotations
23
23
 
24
+ import json
24
25
  import os
25
26
  import re
26
27
  import shlex
28
+ import shutil
27
29
  import subprocess
28
30
  import sys
29
31
  import tomllib
32
+ import textwrap
30
33
  from dataclasses import dataclass
31
34
  from pathlib import Path
32
35
  from subprocess import CompletedProcess
33
- from typing import Any, Callable, Mapping, Sequence, cast
36
+ from typing import Any, Mapping, Sequence, cast
34
37
  from urllib.parse import urlparse
35
38
 
36
39
 
@@ -54,6 +57,12 @@ class ProjectMetadata:
54
57
  import_package: str
55
58
  coverage_source: str
56
59
  scripts: dict[str, str]
60
+ metadata_module: Path
61
+ version: str
62
+ summary: str
63
+ author_name: str
64
+ author_email: str
65
+ shell_command: str
57
66
 
58
67
  def github_tarball_url(self, version: str) -> str:
59
68
  if self.repo_host == "github.com" and self.repo_owner and self.repo_name:
@@ -90,6 +99,7 @@ class ProjectMetadata:
90
99
  summary.append(f"repository={self.repo_url}")
91
100
  if self.homepage:
92
101
  summary.append(f"homepage={self.homepage}")
102
+ summary.append(f"version={self.version}")
93
103
  return tuple(summary)
94
104
 
95
105
 
@@ -131,14 +141,7 @@ def run(
131
141
 
132
142
 
133
143
  def cmd_exists(name: str) -> bool:
134
- return (
135
- subprocess.call(
136
- ["bash", "-lc", f"command -v {shlex.quote(name)} >/dev/null 2>&1"],
137
- stdout=subprocess.DEVNULL,
138
- stderr=subprocess.DEVNULL,
139
- )
140
- == 0
141
- )
144
+ return shutil.which(name) is not None
142
145
 
143
146
 
144
147
  def _normalize_slug(value: str) -> str:
@@ -185,12 +188,11 @@ def _load_pyproject(pyproject: Path) -> dict[str, object]:
185
188
  if cached is not None:
186
189
  return cached
187
190
  raw_text = path.read_text(encoding="utf-8")
188
- data: dict[str, object] = {}
189
191
  try:
190
- load_toml = cast(Callable[[str], dict[str, Any]], getattr(tomllib, "loads"))
191
- parsed_obj = load_toml(raw_text)
192
- except Exception:
193
- parsed_obj = {}
192
+ parsed_obj = tomllib.loads(raw_text)
193
+ except tomllib.TOMLDecodeError as exc: # pragma: no cover - invalid pyproject fails fast
194
+ msg = f"Unable to parse {path}: {exc}"
195
+ raise ValueError(msg) from exc
194
196
  data = {str(key): value for key, value in parsed_obj.items()}
195
197
  _PYPROJECT_DATA_CACHE[path] = data
196
198
  return data
@@ -314,6 +316,47 @@ def get_project_metadata(pyproject: Path = Path("pyproject.toml")) -> ProjectMet
314
316
  coverage_source = _derive_coverage_source(data, import_package)
315
317
  scripts = _derive_scripts(data)
316
318
 
319
+ version = read_version_from_pyproject(pyproject)
320
+ summary = description.strip() if description else ""
321
+ if not summary:
322
+ summary_candidate = project_table.get("summary")
323
+ summary = summary_candidate.strip() if isinstance(summary_candidate, str) else ""
324
+ if not summary:
325
+ summary = name
326
+
327
+ author_name = ""
328
+ author_email = ""
329
+ authors_value = project_table.get("authors")
330
+ if isinstance(authors_value, list):
331
+ authors_list = cast(list[object], authors_value)
332
+ for author_entry in authors_list:
333
+ if not isinstance(author_entry, dict):
334
+ continue
335
+ author_dict = cast(dict[str, object], author_entry)
336
+ name_field = author_dict.get("name")
337
+ email_field = author_dict.get("email")
338
+ if not author_name and isinstance(name_field, str) and name_field.strip():
339
+ author_name = name_field.strip()
340
+ if not author_email and isinstance(email_field, str) and email_field.strip():
341
+ author_email = email_field.strip()
342
+ if not author_name:
343
+ author_name = repo_owner or name
344
+
345
+ shell_command = slug.replace("_", "-")
346
+ preferred_entry = _select_cli_entry(
347
+ scripts,
348
+ (
349
+ slug,
350
+ name,
351
+ import_package,
352
+ import_package.replace("_", "-"),
353
+ ),
354
+ )
355
+ if preferred_entry is not None:
356
+ shell_command = preferred_entry[0]
357
+
358
+ metadata_module = (Path("src") / import_package / "__init__conf__.py").resolve()
359
+
317
360
  meta = ProjectMetadata(
318
361
  name=name,
319
362
  description=description,
@@ -326,11 +369,109 @@ def get_project_metadata(pyproject: Path = Path("pyproject.toml")) -> ProjectMet
326
369
  import_package=import_package,
327
370
  coverage_source=coverage_source,
328
371
  scripts=scripts,
372
+ metadata_module=metadata_module,
373
+ version=version,
374
+ summary=summary,
375
+ author_name=author_name,
376
+ author_email=author_email,
377
+ shell_command=shell_command,
329
378
  )
330
379
  _METADATA_CACHE[path] = meta
331
380
  return meta
332
381
 
333
382
 
383
+ def _quote(value: str) -> str:
384
+ return json.dumps(value, ensure_ascii=False)
385
+
386
+
387
+ def _render_metadata_module(project: ProjectMetadata) -> str:
388
+ homepage = project.homepage or project.repo_url or ""
389
+ body = f'''"""Static package metadata surfaced to CLI commands and documentation.
390
+
391
+ Purpose
392
+ -------
393
+ Expose the current project metadata as simple constants. These values are kept
394
+ in sync with ``pyproject.toml`` by development automation (tests, push
395
+ pipelines), so runtime code does not query packaging metadata.
396
+
397
+ Contents
398
+ --------
399
+ * Module-level constants describing the published package.
400
+ * :func:`print_info` rendering the constants for the CLI ``info`` command.
401
+
402
+ System Role
403
+ -----------
404
+ Lives in the adapters/platform layer; CLI transports import these constants to
405
+ present authoritative project information without invoking packaging APIs.
406
+ """
407
+
408
+ from __future__ import annotations
409
+
410
+ #: Distribution name declared in ``pyproject.toml``.
411
+ name = {_quote(project.name)}
412
+ #: Human-readable summary shown in CLI help output.
413
+ title = {_quote(project.summary)}
414
+ #: Current release version pulled from ``pyproject.toml`` by automation.
415
+ version = {_quote(project.version)}
416
+ #: Repository homepage presented to users.
417
+ homepage = {_quote(homepage)}
418
+ #: Author attribution surfaced in CLI output.
419
+ author = {_quote(project.author_name)}
420
+ #: Contact email surfaced in CLI output.
421
+ author_email = {_quote(project.author_email)}
422
+ #: Console-script name published by the package.
423
+ shell_command = {_quote(project.shell_command)}
424
+
425
+
426
+ def print_info() -> None:
427
+ """Print the summarised metadata block used by the CLI ``info`` command.
428
+
429
+ Why
430
+ Provides a single, auditable rendering function so documentation and
431
+ CLI output always match the system design reference.
432
+
433
+ Side Effects
434
+ Writes to ``stdout``.
435
+
436
+ Examples
437
+ --------
438
+ >>> print_info() # doctest: +ELLIPSIS
439
+ Info for {project.name}:
440
+ ...
441
+ """
442
+
443
+ fields = [
444
+ ("name", name),
445
+ ("title", title),
446
+ ("version", version),
447
+ ("homepage", homepage),
448
+ ("author", author),
449
+ ("author_email", author_email),
450
+ ("shell_command", shell_command),
451
+ ]
452
+ pad = max(len(label) for label, _ in fields)
453
+ lines = [f"Info for {{name}}:", ""]
454
+ lines.extend(f" {{label.ljust(pad)}} = {{value}}" for label, value in fields)
455
+ print("\\n".join(lines))
456
+ '''
457
+ return textwrap.dedent(body)
458
+
459
+
460
+ def sync_metadata_module(project: ProjectMetadata) -> None:
461
+ """Write ``__init__conf__.py`` so the constants mirror ``pyproject.toml``."""
462
+
463
+ content = _render_metadata_module(project)
464
+ module_path = project.metadata_module
465
+ module_path.parent.mkdir(parents=True, exist_ok=True)
466
+ try:
467
+ existing = module_path.read_text(encoding="utf-8")
468
+ except FileNotFoundError:
469
+ existing = ""
470
+ if existing == content:
471
+ return
472
+ module_path.write_text(content, encoding="utf-8")
473
+
474
+
334
475
  def read_version_from_pyproject(pyproject: Path = Path("pyproject.toml")) -> str:
335
476
  data = _load_pyproject(pyproject)
336
477
  project_table = _as_str_mapping(data.get("project"))
@@ -1,15 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import sys
4
- from pathlib import Path
5
4
 
6
5
  import rich_click as click
7
6
 
8
- try:
9
- from ._utils import get_project_metadata, run
10
- except ImportError:
11
- sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
12
- from scripts._utils import get_project_metadata, run
7
+ from ._utils import get_project_metadata, run, sync_metadata_module
13
8
 
14
9
  __all__ = ["build_artifacts"]
15
10
 
@@ -27,6 +22,7 @@ def _failure(label: str) -> str:
27
22
  def build_artifacts() -> None:
28
23
  """Build Python wheel and sdist artifacts."""
29
24
 
25
+ sync_metadata_module(PROJECT)
30
26
  click.echo("[build] Building wheel/sdist via python -m build")
31
27
  build_result = run(["python", "-m", "build"], check=False, capture=False)
32
28
  click.echo(f"[build] {_status('success') if build_result.code == 0 else _failure('failed')}")
@@ -39,7 +35,6 @@ def main() -> None: # pragma: no cover
39
35
 
40
36
 
41
37
  if __name__ == "__main__": # pragma: no cover
42
- sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
43
- from scripts.cli import main as cli_main
38
+ from .cli import main as cli_main
44
39
 
45
40
  cli_main(["build", *sys.argv[1:]])
@@ -2,21 +2,16 @@ from __future__ import annotations
2
2
 
3
3
  import sys
4
4
  from pathlib import Path
5
- from typing import Optional
6
5
 
7
- try:
8
- from ._utils import run
9
- except ImportError:
10
- sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
11
- from scripts._utils import run
6
+ from ._utils import run
12
7
 
13
8
  __all__ = ["bump"]
14
9
 
15
10
 
16
11
  def bump(
17
12
  *,
18
- version: Optional[str] = None,
19
- part: Optional[str] = None,
13
+ version: str | None = None,
14
+ part: str | None = None,
20
15
  pyproject: Path = Path("pyproject.toml"),
21
16
  changelog: Path = Path("CHANGELOG.md"),
22
17
  ) -> None:
@@ -32,6 +27,6 @@ def bump(
32
27
 
33
28
 
34
29
  if __name__ == "__main__": # pragma: no cover
35
- from scripts.cli import main as cli_main
30
+ from .cli import main as cli_main
36
31
 
37
32
  cli_main(["bump", *sys.argv[1:]])
@@ -3,11 +3,7 @@ from __future__ import annotations
3
3
  import sys
4
4
  from pathlib import Path
5
5
 
6
- try:
7
- from .bump import bump
8
- except ImportError:
9
- sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
10
- from scripts.bump import bump
6
+ from .bump import bump
11
7
 
12
8
  __all__ = ["bump_major"]
13
9
 
@@ -19,6 +15,6 @@ def bump_major(pyproject: Path = Path("pyproject.toml"), changelog: Path = Path(
19
15
 
20
16
 
21
17
  if __name__ == "__main__": # pragma: no cover
22
- from scripts.cli import main as cli_main
18
+ from .cli import main as cli_main
23
19
 
24
20
  cli_main(["bump", "--part", "major", *sys.argv[1:]])
@@ -3,11 +3,7 @@ from __future__ import annotations
3
3
  import sys
4
4
  from pathlib import Path
5
5
 
6
- try:
7
- from .bump import bump
8
- except ImportError:
9
- sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
10
- from scripts.bump import bump
6
+ from .bump import bump
11
7
 
12
8
  __all__ = ["bump_minor"]
13
9
 
@@ -19,6 +15,6 @@ def bump_minor(pyproject: Path = Path("pyproject.toml"), changelog: Path = Path(
19
15
 
20
16
 
21
17
  if __name__ == "__main__": # pragma: no cover
22
- from scripts.cli import main as cli_main
18
+ from .cli import main as cli_main
23
19
 
24
20
  cli_main(["bump", "--part", "minor", *sys.argv[1:]])