lib-cli-exit-tools 2.3.4__tar.gz → 2.4.1__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 (57) hide show
  1. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/.github/workflows/default_cicd_public.yml +42 -6
  2. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/.gitignore +8 -5
  3. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/CHANGELOG.md +25 -0
  4. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/Makefile +38 -4
  5. lib_cli_exit_tools-2.3.4/README.md → lib_cli_exit_tools-2.4.1/PKG-INFO +102 -25
  6. lib_cli_exit_tools-2.3.4/PKG-INFO → lib_cli_exit_tools-2.4.1/README.md +59 -67
  7. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/pyproject.toml +16 -13
  8. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/src/lib_cli_exit_tools/__init__conf__.py +1 -1
  9. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/src/lib_cli_exit_tools/application/runner.py +52 -18
  10. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/src/lib_cli_exit_tools/cli/styling.py +5 -4
  11. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/tests/test_application_runner.py +84 -0
  12. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/tests/test_cli.py +7 -13
  13. lib_cli_exit_tools-2.4.1/uv.lock +1968 -0
  14. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/.codacy/cli-config.yaml +0 -0
  15. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/.codacy/cli.sh +0 -0
  16. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/.codacy/codacy.yaml +0 -0
  17. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/.codacy/tools-configs/lizard.yaml +0 -0
  18. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/.codacy/tools-configs/trivy.yaml +0 -0
  19. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/.data_arch_violations.json +0 -0
  20. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/.devcontainer/devcontainer.json +0 -0
  21. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/.devcontainer/settings.json +0 -0
  22. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/.env.example +0 -0
  23. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/.github/actions/extract-metadata/action.yml +0 -0
  24. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/.github/dependabot.yml +0 -0
  25. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/.github/workflows/codeql.yml +0 -0
  26. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/.github/workflows/default_release_public.yml +0 -0
  27. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/.qlty/qlty.toml +0 -0
  28. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/.snyk +0 -0
  29. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/CONTRIBUTING.md +0 -0
  30. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/DEVELOPMENT.md +0 -0
  31. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/INSTALL.md +0 -0
  32. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/LICENSE +0 -0
  33. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/codecov.yml +0 -0
  34. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/docs/system-design/module_reference.md +0 -0
  35. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/notebooks/Quickstart.ipynb +0 -0
  36. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/src/lib_cli_exit_tools/__init__.py +0 -0
  37. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/src/lib_cli_exit_tools/__main__.py +0 -0
  38. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/src/lib_cli_exit_tools/adapters/__init__.py +0 -0
  39. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/src/lib_cli_exit_tools/adapters/signals.py +0 -0
  40. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/src/lib_cli_exit_tools/application/__init__.py +0 -0
  41. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/src/lib_cli_exit_tools/cli/__init__.py +0 -0
  42. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/src/lib_cli_exit_tools/cli/commands.py +0 -0
  43. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/src/lib_cli_exit_tools/cli/group.py +0 -0
  44. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/src/lib_cli_exit_tools/cli/typed_click.py +0 -0
  45. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/src/lib_cli_exit_tools/core/__init__.py +0 -0
  46. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/src/lib_cli_exit_tools/core/configuration.py +0 -0
  47. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/src/lib_cli_exit_tools/core/exit_codes.py +0 -0
  48. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/src/lib_cli_exit_tools/lib_cli_exit_tools.py +0 -0
  49. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/src/lib_cli_exit_tools/py.typed +0 -0
  50. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/tests/conftest.py +0 -0
  51. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/tests/test_adapters_signals.py +0 -0
  52. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/tests/test_behaviors.py +0 -0
  53. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/tests/test_core_configuration.py +0 -0
  54. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/tests/test_core_exit_codes.py +0 -0
  55. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/tests/test_metadata.py +0 -0
  56. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/tests/test_module_entry.py +0 -0
  57. {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.1}/tests/test_signals_integration.py +0 -0
@@ -124,9 +124,36 @@ jobs:
124
124
  print("No CI system dependencies configured for macOS.")
125
125
 
126
126
  - name: Install dev deps
127
- shell: bash
127
+ shell: python
128
128
  run: |
129
- uv pip install -e .[dev] --system
129
+ import os, subprocess, tempfile
130
+ from pathlib import Path
131
+ try:
132
+ import tomllib
133
+ except ModuleNotFoundError:
134
+ import pip._vendor.tomli as tomllib
135
+
136
+ # Push and pull_request runs install exactly the tree uv.lock pins - versions
137
+ # plus file hashes, which uv verifies - so CI tests what the lock ships and no
138
+ # release published upstream since the last lock refresh can enter the build.
139
+ # The nightly schedule run resolves afresh instead (still inside the project's
140
+ # [tool.uv] exclude-newer cooldown, which uv honours here): it is the canary
141
+ # that reports upstream breakage before a lock refresh adopts it. A project
142
+ # without a uv.lock keeps the unlocked install.
143
+ with open("pyproject.toml", "rb") as f:
144
+ data = tomllib.load(f)
145
+ # Asking uv for an extra the project does not declare is a hard error.
146
+ extra = ["--extra", "dev"] if "dev" in data.get("project", {}).get("optional-dependencies", {}) else []
147
+ target = ["--system"] # setup-python's interpreter; the job runs no venv
148
+ locked = Path("uv.lock").is_file() and os.environ["GITHUB_EVENT_NAME"] != "schedule"
149
+ if locked:
150
+ req = Path(tempfile.mkdtemp()) / "locked-requirements.txt"
151
+ # --locked fails when pyproject.toml changed without a lock refresh.
152
+ subprocess.check_call(["uv", "export", "--locked", "--no-emit-project", *extra, "-o", str(req), "--quiet"])
153
+ subprocess.check_call(["uv", "pip", "install", *target, "-r", str(req)])
154
+ subprocess.check_call(["uv", "pip", "install", *target, "--no-deps", "-e", "."])
155
+ else:
156
+ subprocess.check_call(["uv", "pip", "install", *target, "-e", ".[dev]" if extra else "."])
130
157
 
131
158
  - name: Install CI system dependencies (Windows/choco)
132
159
  if: runner.os == 'Windows'
@@ -236,7 +263,7 @@ jobs:
236
263
  continue-on-error: true
237
264
  shell: python
238
265
  run: |
239
- import subprocess, sys, tempfile
266
+ import os, subprocess, sys, tempfile
240
267
  import rtoml
241
268
  from pathlib import Path
242
269
 
@@ -263,9 +290,18 @@ jobs:
263
290
 
264
291
  tmp = Path(tempfile.mkdtemp())
265
292
  req = tmp / "compiled-requirements.txt"
266
- subprocess.check_call(
267
- ["uv", "pip", "compile", *selector, "pyproject.toml", "-o", str(req), "--quiet"]
268
- )
293
+ # With a uv.lock the install step put exactly the locked tree on the
294
+ # runner, so audit that tree; the schedule canary and lock-less
295
+ # projects resolve pyproject.toml afresh, as their install did.
296
+ if Path("uv.lock").is_file() and os.environ["GITHUB_EVENT_NAME"] != "schedule":
297
+ subprocess.check_call(
298
+ ["uv", "export", "--locked", "--no-emit-project", "--no-hashes", *selector,
299
+ "-o", str(req), "--quiet"]
300
+ )
301
+ else:
302
+ subprocess.check_call(
303
+ ["uv", "pip", "compile", *selector, "pyproject.toml", "-o", str(req), "--quiet"]
304
+ )
269
305
 
270
306
  # Drop direct URL references ("pkg @ git+https://...") before auditing.
271
307
  # pip-audit cannot audit those (they are not on PyPI) but, given a
@@ -46,7 +46,8 @@ tmp_*
46
46
  # *.py[cod]: catches .pyc, .pyo, .pyd
47
47
  __pycache__/
48
48
  *.py[cod]
49
- *$py.class # other compiled artifacts
49
+ # other compiled artifacts
50
+ *$py.class
50
51
 
51
52
  # pytest, coverage, MyPy, Hypothesis
52
53
  .pytest_cache/
@@ -72,12 +73,15 @@ coverage.xml
72
73
  # source/package builds
73
74
  build/
74
75
  dist/
75
- *.egg-info/ # metadata for pip installs
76
+ # metadata for pip installs
77
+ *.egg-info/
76
78
  *.egg
77
79
  *.whl
78
80
  pip-wheel-metadata/
79
- *.manifest # PyInstaller metadata
80
- *.spec # PyInstaller spec files
81
+ # PyInstaller metadata
82
+ *.manifest
83
+ # PyInstaller spec files
84
+ *.spec
81
85
 
82
86
  # Eggs, parts, wheels, sdist, etc.
83
87
  develop-eggs/
@@ -177,7 +181,6 @@ result
177
181
  # Optional lockfiles (for alternative managers)
178
182
  poetry.lock
179
183
  pdm.lock
180
- uv.lock
181
184
  /.github_bak/
182
185
  prompts.md
183
186
  CLAUDE.md
@@ -4,6 +4,31 @@ All notable changes to this project are documented here. The format follows [Kee
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.4.1] 2026-10-12 22:22:57
8
+
9
+ ### Added
10
+ - Python 3.15 is a supported version: declared in the classifiers, so CI and `make test-all` run it.
11
+ - `uv.lock` is committed, and `[tool.uv] exclude-newer` sets a cooldown that keeps releases younger
12
+ than it out of the lock. CI installs exactly the locked tree on push and pull request; the nightly
13
+ scheduled run still resolves afresh to catch upstream breakage early.
14
+
15
+ ### Changed
16
+ - The install section explains why installing with uv protects against supply-chain attacks and why
17
+ a plain `pip install` from PyPI does not.
18
+ - `rtoml` floor raised to 0.14.0 (dev extra), the first release with Python 3.15 wheels.
19
+
20
+ ## [2.4.0] 2026-10-05 20:48:53
21
+
22
+ ### Fixed
23
+ - `cli_session()` now resolves exit codes the way `run_cli()`'s default handler does. Its handler went straight to a traceback summary plus `get_system_exit_code()`, skipping the resolver chain, so under a session a Click usage error (bad flag, unknown command) exited `1` and printed `NoSuchOption: ...` instead of exiting `2` with Click's usage message, a `ClickException` with its own `exit_code` lost it, and the `signal_specs` passed to the session runner were ignored. The session's `summary_limit`/`verbose_limit` still apply to an exception nothing else claims. CLIs built on `cli_session` now report a usage error as `2`, like Click and like `run_cli`.
24
+
25
+ ### Added
26
+ - `SessionRunner`, a `Protocol` typing the callable `cli_session()` yields. The previous annotation was a union whose first member took only the command, so a type checker rejected `run(cmd, argv=[...])` and callers had to `cast` the session.
27
+
28
+ ### Changed
29
+ - `README.md` is ASCII-only, and its `cli_session` section says how the session's handler resolves exit codes.
30
+ - Raised the dev tool floors.
31
+
7
32
  ## [2.3.4] 2026-07-29 15:20:59
8
33
 
9
34
  ### Fixed
@@ -1,4 +1,4 @@
1
- # BMK MAKEFILE 3.13.2
1
+ # BMK MAKEFILE 3.21.0
2
2
  # do not alter this file - it might be overwritten on new versions of BMK
3
3
  # if You want to alter it, remove the first line # BMK MAKEFILE 1.0 - then it is a custom makefile and will not be overwritten
4
4
  # bmk Makefile - thin wrapper using `uv tool upgrade` for persistent bmk
@@ -186,16 +186,50 @@ endif
186
186
  # BMK_MIN floors the FALLBACK install. The upgrade path does not consult it: uv re-resolves
187
187
  # the requirement recorded at install time, which is itself a `bmk>=...`, so it always lands
188
188
  # on the newest release and clears the floor anyway.
189
- BMK_MIN := 3.13.2
189
+ BMK_MIN := 3.21.0
190
190
 
191
191
  # Absent env, missing check, or damaged env - all three mean "do not upgrade, rebuild".
192
192
  # `test -x` keeps a fresh machine quiet: there is no interpreter to run yet.
193
193
  BMK_INTACT := $(if $(BMK_PY),test -x "$(BMK_PY)" && "$(BMK_PY)" -m bmk_selfcheck,false)
194
194
 
195
+ # The env this recipe mutates is shared by EVERY repo on the machine, so the mutation has to
196
+ # wait for the bmk processes running out of it. Every bmk started from that env holds a
197
+ # SHARED lock on <uv tool dir>/.bmk-tool.lock for its whole lifetime; these wrappers take
198
+ # the same lock EXCLUSIVE for the length of the upgrade. Without it, a make here deleted the
199
+ # site-packages out from under a bmk minutes into a test suite in an UNRELATED repo, and it
200
+ # surfaced as an ImportError inside bmk's OWN dependencies that cleared on a re-run - so it
201
+ # read as a flake rather than as the environment vanishing mid-gate.
202
+ #
203
+ # `bmk_toollock` is a second stdlib-only TOP-LEVEL module bmk ships, run on the tool env's
204
+ # own interpreter. That is not circular: a uv tool env's bin/python is a SYMLINK to a base
205
+ # interpreter OUTSIDE the tool dir, so the stdlib the guard runs on is not part of the tree
206
+ # uv is about to replace, and the guard is read and compiled before uv is ever spawned.
207
+ #
208
+ # NOT uv's own <uv tool dir>/.lock, which uv does honour: `uv tool upgrade` and `uv tool
209
+ # list` block on it too (measured), and the upgrade runs before every target - so a
210
+ # gate-lifetime shared lock there would make every make on the machine, and every unrelated
211
+ # `uv tool install`, wait for the longest-running gate.
212
+ #
213
+ # $(wildcard) rather than a shell test: on a machine with no tool env yet these expand to
214
+ # NOTHING and the install runs bare, which is exactly what a first install needs.
215
+ #
216
+ # The bounded wait is load-bearing in both directions. An unbounded one would serialise
217
+ # every repo's make behind the longest gate on the machine. A SKIPPED upgrade costs nothing
218
+ # (the next make picks it up), so the upgrade skips. The REPAIR may not skip: proceeding
219
+ # would leave a damaged env in place, so it fails and drops through to the retry.
220
+ #
221
+ # The LAST attempt is deliberately NEVER guarded, and a test pins that. The guard lives
222
+ # inside the env being repaired, so on the path where that env is absent or broken the guard
223
+ # may be broken too; an unguarded last resort is what stops a defective guard from making
224
+ # `make` unrunnable in every repo at once. If bmk_toollock ever falls out of the wheel, every
225
+ # make everywhere drops to a full reinstall - there is a packaging test for exactly that.
226
+ BMK_LOCK := $(if $(wildcard $(BMK_PY)),"$(BMK_PY)" -m bmk_toollock --exclusive --timeout 10 --on-timeout skip --,)
227
+ BMK_LOCK_REPAIR := $(if $(wildcard $(BMK_PY)),"$(BMK_PY)" -m bmk_toollock --exclusive --timeout 60 --on-timeout fail --,)
228
+
195
229
  .PHONY: _ensure_bmk
196
230
  _ensure_bmk:
197
- @$(BMK_INTACT) && uv tool upgrade bmk \
198
- || uv tool install --reinstall --force "bmk>=$(BMK_MIN)" \
231
+ @$(BMK_INTACT) && $(BMK_LOCK) uv tool upgrade bmk \
232
+ || $(BMK_LOCK_REPAIR) uv tool install --reinstall --force "bmk>=$(BMK_MIN)" \
199
233
  || uv tool install --reinstall --force "bmk>=$(BMK_MIN)"
200
234
 
201
235
  # ──────────────────────────────────────────────────────────────
@@ -1,3 +1,46 @@
1
+ Metadata-Version: 2.5
2
+ Name: lib_cli_exit_tools
3
+ Version: 2.4.1
4
+ Summary: CLI exit handling helpers: clean signals, exit codes, and error printing
5
+ Project-URL: Homepage, https://github.com/bitranox/lib_cli_exit_tools
6
+ Project-URL: Repository, https://github.com/bitranox/lib_cli_exit_tools.git
7
+ Project-URL: Issues, https://github.com/bitranox/lib_cli_exit_tools/issues
8
+ Author-email: bitranox <bitranox@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: cli,click,errors,exit,signals
12
+ Classifier: Environment :: Console
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Programming Language :: Python :: 3.15
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: rich-click>=1.9.9
26
+ Provides-Extra: dev
27
+ Requires-Dist: bandit>=1.9.4; extra == 'dev'
28
+ Requires-Dist: build>=1.6.1; extra == 'dev'
29
+ Requires-Dist: click>=8.5.0; extra == 'dev'
30
+ Requires-Dist: coverage[toml]>=7.16.2; extra == 'dev'
31
+ Requires-Dist: hypothesis>=6.168.5; extra == 'dev'
32
+ Requires-Dist: import-linter>=2.15; extra == 'dev'
33
+ Requires-Dist: pip-audit>=2.10.1; extra == 'dev'
34
+ Requires-Dist: pyright[nodejs]>=1.1.414; extra == 'dev'
35
+ Requires-Dist: pytest-asyncio>=1.4.0; extra == 'dev'
36
+ Requires-Dist: pytest-cov>=7.1.0; extra == 'dev'
37
+ Requires-Dist: pytest>=9.1.1; extra == 'dev'
38
+ Requires-Dist: rtoml>=0.14.0; extra == 'dev'
39
+ Requires-Dist: ruff>=0.17.0; extra == 'dev'
40
+ Requires-Dist: textual>=8.2.8; extra == 'dev'
41
+ Requires-Dist: twine>=7.0.0; extra == 'dev'
42
+ Description-Content-Type: text/markdown
43
+
1
44
  # lib_cli_exit_tools
2
45
 
3
46
  <!-- Badges -->
@@ -21,15 +64,15 @@ Small helpers for robust CLI exit handling:
21
64
 
22
65
  Building CLI applications that behave correctly in shell pipelines, CI/CD systems, and across platforms is surprisingly tricky. This library solves five common pain points:
23
66
 
24
- 1. **Correct Exit Codes** — Exceptions are automatically mapped to platform-appropriate exit codes (POSIX errno or BSD sysexits). `FileNotFoundError` returns `2`, `PermissionError` returns `13` on POSIX, and `KeyboardInterrupt` returns `130` — matching shell conventions without manual bookkeeping.
67
+ 1. **Correct Exit Codes** - Exceptions are automatically mapped to platform-appropriate exit codes (POSIX errno or BSD sysexits). `FileNotFoundError` returns `2`, `PermissionError` returns `13` on POSIX, and `KeyboardInterrupt` returns `130` - matching shell conventions without manual bookkeeping.
25
68
 
26
- 2. **Portable Signal Handling** — SIGINT (Ctrl+C), SIGTERM, and Windows SIGBREAK are translated into structured Python exceptions with deterministic exit codes. Your CLI behaves consistently whether terminated by a user, a process manager, or a CI runner.
69
+ 2. **Portable Signal Handling** - SIGINT (Ctrl+C), SIGTERM, and Windows SIGBREAK are translated into structured Python exceptions with deterministic exit codes. Your CLI behaves consistently whether terminated by a user, a process manager, or a CI runner.
27
70
 
28
- 3. **Clean Error Output** — Uncaught exceptions produce concise, coloured error messages by default. Toggle `--traceback` for full Rich-formatted stack traces during debugging — no code changes required.
71
+ 3. **Clean Error Output** - Uncaught exceptions produce concise, coloured error messages by default. Toggle `--traceback` for full Rich-formatted stack traces during debugging - no code changes required.
29
72
 
30
- 4. **Pipeline-Friendly** — `BrokenPipeError` is handled gracefully (exit 141 by default, matching `128 + SIGPIPE`), so piping output to `head` or other truncating tools doesn't produce noisy tracebacks.
73
+ 4. **Pipeline-Friendly** - `BrokenPipeError` is handled gracefully (exit 141 by default, matching `128 + SIGPIPE`), so piping output to `head` or other truncating tools doesn't produce noisy tracebacks.
31
74
 
32
- 5. **Zero Boilerplate** — Wrap any Click command with `run_cli()` and get signal handling, exit-code translation, and stream flushing in one call. Configuration is centralised and test-friendly via context managers.
75
+ 5. **Zero Boilerplate** - Wrap any Click command with `run_cli()` and get signal handling, exit-code translation, and stream flushing in one call. Configuration is centralised and test-friendly via context managers.
33
76
 
34
77
  ## Install
35
78
 
@@ -41,6 +84,36 @@ pip install lib_cli_exit_tools
41
84
 
42
85
  See [INSTALL.md](INSTALL.md) for editable installs, pipx/uv usage, and troubleshooting tips.
43
86
 
87
+ ### Why we recommend uv: supply-chain protection
88
+
89
+ This repository commits a `uv.lock`. It pins every package the project needs, transitive
90
+ dependencies included, to an exact version and to the hashes of that version's files. The
91
+ `exclude-newer` setting under `[tool.uv]` in `pyproject.toml` adds a cooldown: uv leaves any
92
+ release younger than that out of the lock. CI installs from the lock, so a hijacked release that
93
+ gets pulled again within the cooldown never reaches our builds, and a file whose hash does not
94
+ match is refused.
95
+
96
+ That protection stays in this repository. The installable package declares only lower bounds
97
+ (`>=`) for its dependencies, so a plain `pip install` takes the newest release of every transitive
98
+ dependency at the moment you run it. If one of them was hijacked an hour earlier, you get the
99
+ hijacked version. `pipx install` and `uv tool install` without options do the same.
100
+
101
+ To install the command line tool with a cooldown of your own (uv then skips any release younger
102
+ than 7 days, transitive ones included), or to run the exact tree our CI tests:
103
+
104
+ ```bash
105
+ uv tool install --exclude-newer "7 days" lib_cli_exit_tools
106
+ git clone https://github.com/bitranox/lib_cli_exit_tools.git && cd lib_cli_exit_tools && uv sync --locked && uv run lib_cli_exit_tools --help
107
+ ```
108
+
109
+ If you depend on lib_cli_exit_tools in your own project, give that project the same cooldown (7 days here, as
110
+ an example) and commit the `uv.lock` that `uv add lib_cli_exit_tools` writes:
111
+
112
+ ```toml
113
+ [tool.uv]
114
+ exclude-newer = "7 days"
115
+ ```
116
+
44
117
  ## Usage
45
118
 
46
119
  Console script (all commands map to ``lib_cli_exit_tools.cli:main``):
@@ -59,11 +132,11 @@ The library installs three equivalent console scripts: `lib-cli-exit-tools` (pri
59
132
 
60
133
  ### Global Options
61
134
 
62
- | Option | Default | Description |
63
- |--------|---------|-------------|
135
+ | Option | Default | Description |
136
+ |----------------------------------|---------|--------------------------------------|
64
137
  | `--traceback` / `--no-traceback` | `False` | Show full Python traceback on errors |
65
- | `--version` | — | Show program version and exit |
66
- | `-h`, `--help` | — | Show help message and exit |
138
+ | `--version` | - | Show program version and exit |
139
+ | `-h`, `--help` | - | Show help message and exit |
67
140
 
68
141
  ### Commands
69
142
 
@@ -84,7 +157,7 @@ lib-cli-exit-tools --traceback fail # show full traceback
84
157
 
85
158
  ### Examples
86
159
 
87
- The snippets below move from a minimal “hello world” through a production-ready
160
+ The snippets below move from a minimal "hello world" through a production-ready
88
161
  CLI that demonstrates configuration hooks and structured error handling.
89
162
 
90
163
  #### 1. Minimal command (copy-paste ready)
@@ -181,7 +254,7 @@ def info(settings: Settings) -> None:
181
254
  @cli.command()
182
255
  @click.pass_obj
183
256
  def fail(settings: Settings) -> None:
184
- click.echo("About to fail…")
257
+ click.echo("About to fail...")
185
258
  raise RuntimeError("intentional failure for diagnostics")
186
259
 
187
260
 
@@ -202,7 +275,7 @@ if __name__ == "__main__":
202
275
 
203
276
  This version parses `--traceback` early (before Click sees it), then wires
204
277
  configuration overrides, custom signal specs, and multiple commands into a
205
- single composition point—mirroring how a production CLI can layer policy logic
278
+ single composition point - mirroring how a production CLI can layer policy logic
206
279
  around Click while still delegating exit-code translation to `lib_cli_exit_tools`.
207
280
 
208
281
  #### 4. Custom signal handlers
@@ -315,20 +388,20 @@ All configuration lives on the module-level `lib_cli_exit_tools.config` object.
315
388
  from lib_cli_exit_tools import config
316
389
 
317
390
  config.traceback = True # emit full tracebacks instead of short messages
318
- config.exit_code_style = "sysexits" # emit BSD-style exit codes (EX_USAGE, EX_NOINPUT, …)
391
+ config.exit_code_style = "sysexits" # emit BSD-style exit codes (EX_USAGE, EX_NOINPUT, ...)
319
392
  config.broken_pipe_exit_code = 0 # treat BrokenPipeError as a benign truncation
320
393
  ```
321
394
 
322
395
  Field reference:
323
396
 
324
- | Field | Type | Default | Description |
325
- |-------|------|---------|-------------|
326
- | `traceback` | `bool` | `False` | When `True`, `handle_cli_exception` renders a full Rich traceback to stderr. The bundled CLI toggles this via `--traceback/--no-traceback`. |
327
- | `exit_code_style` | `"errno"` \| `"sysexits"` | `"errno"` | Controls exit code mapping. `errno` returns POSIX/Windows-style codes; `sysexits` returns BSD-style semantic codes (EX_USAGE, EX_NOINPUT, etc.). |
328
- | `broken_pipe_exit_code` | `int` | `141` | Exit status for `BrokenPipeError` (default mirrors `128 + SIGPIPE`). Set to `0` to treat truncation as success. |
329
- | `traceback_force_color` | `bool` | `False` | Force Rich to emit ANSI-coloured tracebacks even when stderr is not a TTY. Useful for CI logs. |
397
+ | Field | Type | Default | Description |
398
+ |-------------------------|---------------------------|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------|
399
+ | `traceback` | `bool` | `False` | When `True`, `handle_cli_exception` renders a full Rich traceback to stderr. The bundled CLI toggles this via `--traceback/--no-traceback`. |
400
+ | `exit_code_style` | `"errno"` \| `"sysexits"` | `"errno"` | Controls exit code mapping. `errno` returns POSIX/Windows-style codes; `sysexits` returns BSD-style semantic codes (EX_USAGE, EX_NOINPUT, etc.). |
401
+ | `broken_pipe_exit_code` | `int` | `141` | Exit status for `BrokenPipeError` (default mirrors `128 + SIGPIPE`). Set to `0` to treat truncation as success. |
402
+ | `traceback_force_color` | `bool` | `False` | Force Rich to emit ANSI-coloured tracebacks even when stderr is not a TTY. Useful for CI logs. |
330
403
 
331
- Remember that `config` is module-level—if you call the library from multiple threads or embed it in another CLI, configure it once during bootstrap before handing control to user code. When you need temporary overrides (for tests or nested CLIs), wrap the change with the built-in context manager so state is restored automatically:
404
+ Remember that `config` is module-level - if you call the library from multiple threads or embed it in another CLI, configure it once during bootstrap before handing control to user code. When you need temporary overrides (for tests or nested CLIs), wrap the change with the built-in context manager so state is restored automatically:
332
405
 
333
406
  ```python
334
407
  from lib_cli_exit_tools import config_overrides, config
@@ -385,13 +458,13 @@ python -m scripts.test --coverage=off --verbose
385
458
  The suite includes OS-aware cases (POSIX, Windows-specific signal handling), so
386
459
  run it on each target platform you support to keep coverage consistent.
387
460
 
388
- If your environment reports “cannot execute” when running `pytest`, the auto-generated entry-point script likely points at a removed interpreter. Reinstall the dev extras or invoke tests with `python -m pytest` (for example, `python -m pytest tests/`).
461
+ If your environment reports "cannot execute" when running `pytest`, the auto-generated entry-point script likely points at a removed interpreter. Reinstall the dev extras or invoke tests with `python -m pytest` (for example, `python -m pytest tests/`).
389
462
 
390
463
  When coverage uploads are skipped (no Codecov token), `make test` still writes `coverage.xml` and `codecov.xml` to the project root so you can inspect results locally or feed them into other tooling.
391
464
 
392
465
  ## Public API Reference
393
466
 
394
- The package re-exports the helpers below via `lib_cli_exit_tools.__all__`. Import them directly with `from lib_cli_exit_tools import …`.
467
+ The package re-exports the helpers below via `lib_cli_exit_tools.__all__`. Import them directly with `from lib_cli_exit_tools import ...`.
395
468
 
396
469
  ### `config`
397
470
  Mutable dataclass-like singleton holding process-wide settings. Configure it during CLI startup.
@@ -415,8 +488,12 @@ Parameters:
415
488
 
416
489
  ### `cli_session(*, summary_limit=500, verbose_limit=10_000, overrides=None, restore=True)`
417
490
  Context manager that snapshots `lib_cli_exit_tools.config`, optionally
418
- applies temporary overrides, and yields a callable compatible with
419
- `run_cli`.
491
+ applies temporary overrides, and yields a `SessionRunner`: a callable with
492
+ `run_cli`'s signature. Its exception handler resolves exit codes exactly like
493
+ `handle_cli_exception` - signals, broken pipes, Click exceptions and `SystemExit`
494
+ keep their own code and message, so a usage error exits `2` with Click's usage
495
+ text - and applies `summary_limit`/`verbose_limit` only to an exception none of
496
+ those claims.
420
497
 
421
498
  Parameters:
422
499
  - `summary_limit` (`int`, default `500`): Character budget when tracebacks are disabled.
@@ -424,7 +501,7 @@ Parameters:
424
501
  - `overrides` (`Mapping[str, object] | None`, default `None`): Mapping of configuration field/value pairs applied during the session. When `traceback` is supplied and `traceback_force_color` is omitted, colour output is automatically forced.
425
502
  - `restore` (`bool`, default `True`): When `True`, configuration state is restored after the session. Set to `False` to leave overrides in place once the context exits.
426
503
 
427
- Use it to restore configuration automatically—even when the wrapped command
504
+ Use it to restore configuration automatically - even when the wrapped command
428
505
  raises:
429
506
 
430
507
  ```python
@@ -1,45 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: lib_cli_exit_tools
3
- Version: 2.3.4
4
- Summary: CLI exit handling helpers: clean signals, exit codes, and error printing
5
- Project-URL: Homepage, https://github.com/bitranox/lib_cli_exit_tools
6
- Project-URL: Repository, https://github.com/bitranox/lib_cli_exit_tools.git
7
- Project-URL: Issues, https://github.com/bitranox/lib_cli_exit_tools/issues
8
- Author-email: bitranox <bitranox@gmail.com>
9
- License: MIT
10
- License-File: LICENSE
11
- Keywords: cli,click,errors,exit,signals
12
- Classifier: Environment :: Console
13
- Classifier: License :: OSI Approved :: MIT License
14
- Classifier: Operating System :: OS Independent
15
- Classifier: Programming Language :: Python :: 3
16
- Classifier: Programming Language :: Python :: 3 :: Only
17
- Classifier: Programming Language :: Python :: 3.10
18
- Classifier: Programming Language :: Python :: 3.11
19
- Classifier: Programming Language :: Python :: 3.12
20
- Classifier: Programming Language :: Python :: 3.13
21
- Classifier: Programming Language :: Python :: 3.14
22
- Classifier: Typing :: Typed
23
- Requires-Python: >=3.10
24
- Requires-Dist: rich-click>=1.9.8
25
- Provides-Extra: dev
26
- Requires-Dist: bandit>=1.9.4; extra == 'dev'
27
- Requires-Dist: build>=1.5.0; extra == 'dev'
28
- Requires-Dist: click>=8.4.2; extra == 'dev'
29
- Requires-Dist: coverage[toml]>=7.15.2; extra == 'dev'
30
- Requires-Dist: hypothesis>=6.163.0; extra == 'dev'
31
- Requires-Dist: import-linter>=2.13; extra == 'dev'
32
- Requires-Dist: pip-audit>=2.10.1; extra == 'dev'
33
- Requires-Dist: pyright[nodejs]>=1.1.411; extra == 'dev'
34
- Requires-Dist: pytest-asyncio>=1.4.0; extra == 'dev'
35
- Requires-Dist: pytest-cov>=7.1.0; extra == 'dev'
36
- Requires-Dist: pytest>=9.1.1; extra == 'dev'
37
- Requires-Dist: rtoml>=0.13.0; extra == 'dev'
38
- Requires-Dist: ruff>=0.16.0; extra == 'dev'
39
- Requires-Dist: textual>=8.2.8; extra == 'dev'
40
- Requires-Dist: twine>=7.0.0; extra == 'dev'
41
- Description-Content-Type: text/markdown
42
-
43
1
  # lib_cli_exit_tools
44
2
 
45
3
  <!-- Badges -->
@@ -63,15 +21,15 @@ Small helpers for robust CLI exit handling:
63
21
 
64
22
  Building CLI applications that behave correctly in shell pipelines, CI/CD systems, and across platforms is surprisingly tricky. This library solves five common pain points:
65
23
 
66
- 1. **Correct Exit Codes** — Exceptions are automatically mapped to platform-appropriate exit codes (POSIX errno or BSD sysexits). `FileNotFoundError` returns `2`, `PermissionError` returns `13` on POSIX, and `KeyboardInterrupt` returns `130` — matching shell conventions without manual bookkeeping.
24
+ 1. **Correct Exit Codes** - Exceptions are automatically mapped to platform-appropriate exit codes (POSIX errno or BSD sysexits). `FileNotFoundError` returns `2`, `PermissionError` returns `13` on POSIX, and `KeyboardInterrupt` returns `130` - matching shell conventions without manual bookkeeping.
67
25
 
68
- 2. **Portable Signal Handling** — SIGINT (Ctrl+C), SIGTERM, and Windows SIGBREAK are translated into structured Python exceptions with deterministic exit codes. Your CLI behaves consistently whether terminated by a user, a process manager, or a CI runner.
26
+ 2. **Portable Signal Handling** - SIGINT (Ctrl+C), SIGTERM, and Windows SIGBREAK are translated into structured Python exceptions with deterministic exit codes. Your CLI behaves consistently whether terminated by a user, a process manager, or a CI runner.
69
27
 
70
- 3. **Clean Error Output** — Uncaught exceptions produce concise, coloured error messages by default. Toggle `--traceback` for full Rich-formatted stack traces during debugging — no code changes required.
28
+ 3. **Clean Error Output** - Uncaught exceptions produce concise, coloured error messages by default. Toggle `--traceback` for full Rich-formatted stack traces during debugging - no code changes required.
71
29
 
72
- 4. **Pipeline-Friendly** — `BrokenPipeError` is handled gracefully (exit 141 by default, matching `128 + SIGPIPE`), so piping output to `head` or other truncating tools doesn't produce noisy tracebacks.
30
+ 4. **Pipeline-Friendly** - `BrokenPipeError` is handled gracefully (exit 141 by default, matching `128 + SIGPIPE`), so piping output to `head` or other truncating tools doesn't produce noisy tracebacks.
73
31
 
74
- 5. **Zero Boilerplate** — Wrap any Click command with `run_cli()` and get signal handling, exit-code translation, and stream flushing in one call. Configuration is centralised and test-friendly via context managers.
32
+ 5. **Zero Boilerplate** - Wrap any Click command with `run_cli()` and get signal handling, exit-code translation, and stream flushing in one call. Configuration is centralised and test-friendly via context managers.
75
33
 
76
34
  ## Install
77
35
 
@@ -83,6 +41,36 @@ pip install lib_cli_exit_tools
83
41
 
84
42
  See [INSTALL.md](INSTALL.md) for editable installs, pipx/uv usage, and troubleshooting tips.
85
43
 
44
+ ### Why we recommend uv: supply-chain protection
45
+
46
+ This repository commits a `uv.lock`. It pins every package the project needs, transitive
47
+ dependencies included, to an exact version and to the hashes of that version's files. The
48
+ `exclude-newer` setting under `[tool.uv]` in `pyproject.toml` adds a cooldown: uv leaves any
49
+ release younger than that out of the lock. CI installs from the lock, so a hijacked release that
50
+ gets pulled again within the cooldown never reaches our builds, and a file whose hash does not
51
+ match is refused.
52
+
53
+ That protection stays in this repository. The installable package declares only lower bounds
54
+ (`>=`) for its dependencies, so a plain `pip install` takes the newest release of every transitive
55
+ dependency at the moment you run it. If one of them was hijacked an hour earlier, you get the
56
+ hijacked version. `pipx install` and `uv tool install` without options do the same.
57
+
58
+ To install the command line tool with a cooldown of your own (uv then skips any release younger
59
+ than 7 days, transitive ones included), or to run the exact tree our CI tests:
60
+
61
+ ```bash
62
+ uv tool install --exclude-newer "7 days" lib_cli_exit_tools
63
+ git clone https://github.com/bitranox/lib_cli_exit_tools.git && cd lib_cli_exit_tools && uv sync --locked && uv run lib_cli_exit_tools --help
64
+ ```
65
+
66
+ If you depend on lib_cli_exit_tools in your own project, give that project the same cooldown (7 days here, as
67
+ an example) and commit the `uv.lock` that `uv add lib_cli_exit_tools` writes:
68
+
69
+ ```toml
70
+ [tool.uv]
71
+ exclude-newer = "7 days"
72
+ ```
73
+
86
74
  ## Usage
87
75
 
88
76
  Console script (all commands map to ``lib_cli_exit_tools.cli:main``):
@@ -101,11 +89,11 @@ The library installs three equivalent console scripts: `lib-cli-exit-tools` (pri
101
89
 
102
90
  ### Global Options
103
91
 
104
- | Option | Default | Description |
105
- |--------|---------|-------------|
92
+ | Option | Default | Description |
93
+ |----------------------------------|---------|--------------------------------------|
106
94
  | `--traceback` / `--no-traceback` | `False` | Show full Python traceback on errors |
107
- | `--version` | — | Show program version and exit |
108
- | `-h`, `--help` | — | Show help message and exit |
95
+ | `--version` | - | Show program version and exit |
96
+ | `-h`, `--help` | - | Show help message and exit |
109
97
 
110
98
  ### Commands
111
99
 
@@ -126,7 +114,7 @@ lib-cli-exit-tools --traceback fail # show full traceback
126
114
 
127
115
  ### Examples
128
116
 
129
- The snippets below move from a minimal “hello world” through a production-ready
117
+ The snippets below move from a minimal "hello world" through a production-ready
130
118
  CLI that demonstrates configuration hooks and structured error handling.
131
119
 
132
120
  #### 1. Minimal command (copy-paste ready)
@@ -223,7 +211,7 @@ def info(settings: Settings) -> None:
223
211
  @cli.command()
224
212
  @click.pass_obj
225
213
  def fail(settings: Settings) -> None:
226
- click.echo("About to fail…")
214
+ click.echo("About to fail...")
227
215
  raise RuntimeError("intentional failure for diagnostics")
228
216
 
229
217
 
@@ -244,7 +232,7 @@ if __name__ == "__main__":
244
232
 
245
233
  This version parses `--traceback` early (before Click sees it), then wires
246
234
  configuration overrides, custom signal specs, and multiple commands into a
247
- single composition point—mirroring how a production CLI can layer policy logic
235
+ single composition point - mirroring how a production CLI can layer policy logic
248
236
  around Click while still delegating exit-code translation to `lib_cli_exit_tools`.
249
237
 
250
238
  #### 4. Custom signal handlers
@@ -357,20 +345,20 @@ All configuration lives on the module-level `lib_cli_exit_tools.config` object.
357
345
  from lib_cli_exit_tools import config
358
346
 
359
347
  config.traceback = True # emit full tracebacks instead of short messages
360
- config.exit_code_style = "sysexits" # emit BSD-style exit codes (EX_USAGE, EX_NOINPUT, …)
348
+ config.exit_code_style = "sysexits" # emit BSD-style exit codes (EX_USAGE, EX_NOINPUT, ...)
361
349
  config.broken_pipe_exit_code = 0 # treat BrokenPipeError as a benign truncation
362
350
  ```
363
351
 
364
352
  Field reference:
365
353
 
366
- | Field | Type | Default | Description |
367
- |-------|------|---------|-------------|
368
- | `traceback` | `bool` | `False` | When `True`, `handle_cli_exception` renders a full Rich traceback to stderr. The bundled CLI toggles this via `--traceback/--no-traceback`. |
369
- | `exit_code_style` | `"errno"` \| `"sysexits"` | `"errno"` | Controls exit code mapping. `errno` returns POSIX/Windows-style codes; `sysexits` returns BSD-style semantic codes (EX_USAGE, EX_NOINPUT, etc.). |
370
- | `broken_pipe_exit_code` | `int` | `141` | Exit status for `BrokenPipeError` (default mirrors `128 + SIGPIPE`). Set to `0` to treat truncation as success. |
371
- | `traceback_force_color` | `bool` | `False` | Force Rich to emit ANSI-coloured tracebacks even when stderr is not a TTY. Useful for CI logs. |
354
+ | Field | Type | Default | Description |
355
+ |-------------------------|---------------------------|-----------|--------------------------------------------------------------------------------------------------------------------------------------------------|
356
+ | `traceback` | `bool` | `False` | When `True`, `handle_cli_exception` renders a full Rich traceback to stderr. The bundled CLI toggles this via `--traceback/--no-traceback`. |
357
+ | `exit_code_style` | `"errno"` \| `"sysexits"` | `"errno"` | Controls exit code mapping. `errno` returns POSIX/Windows-style codes; `sysexits` returns BSD-style semantic codes (EX_USAGE, EX_NOINPUT, etc.). |
358
+ | `broken_pipe_exit_code` | `int` | `141` | Exit status for `BrokenPipeError` (default mirrors `128 + SIGPIPE`). Set to `0` to treat truncation as success. |
359
+ | `traceback_force_color` | `bool` | `False` | Force Rich to emit ANSI-coloured tracebacks even when stderr is not a TTY. Useful for CI logs. |
372
360
 
373
- Remember that `config` is module-level—if you call the library from multiple threads or embed it in another CLI, configure it once during bootstrap before handing control to user code. When you need temporary overrides (for tests or nested CLIs), wrap the change with the built-in context manager so state is restored automatically:
361
+ Remember that `config` is module-level - if you call the library from multiple threads or embed it in another CLI, configure it once during bootstrap before handing control to user code. When you need temporary overrides (for tests or nested CLIs), wrap the change with the built-in context manager so state is restored automatically:
374
362
 
375
363
  ```python
376
364
  from lib_cli_exit_tools import config_overrides, config
@@ -427,13 +415,13 @@ python -m scripts.test --coverage=off --verbose
427
415
  The suite includes OS-aware cases (POSIX, Windows-specific signal handling), so
428
416
  run it on each target platform you support to keep coverage consistent.
429
417
 
430
- If your environment reports “cannot execute” when running `pytest`, the auto-generated entry-point script likely points at a removed interpreter. Reinstall the dev extras or invoke tests with `python -m pytest` (for example, `python -m pytest tests/`).
418
+ If your environment reports "cannot execute" when running `pytest`, the auto-generated entry-point script likely points at a removed interpreter. Reinstall the dev extras or invoke tests with `python -m pytest` (for example, `python -m pytest tests/`).
431
419
 
432
420
  When coverage uploads are skipped (no Codecov token), `make test` still writes `coverage.xml` and `codecov.xml` to the project root so you can inspect results locally or feed them into other tooling.
433
421
 
434
422
  ## Public API Reference
435
423
 
436
- The package re-exports the helpers below via `lib_cli_exit_tools.__all__`. Import them directly with `from lib_cli_exit_tools import …`.
424
+ The package re-exports the helpers below via `lib_cli_exit_tools.__all__`. Import them directly with `from lib_cli_exit_tools import ...`.
437
425
 
438
426
  ### `config`
439
427
  Mutable dataclass-like singleton holding process-wide settings. Configure it during CLI startup.
@@ -457,8 +445,12 @@ Parameters:
457
445
 
458
446
  ### `cli_session(*, summary_limit=500, verbose_limit=10_000, overrides=None, restore=True)`
459
447
  Context manager that snapshots `lib_cli_exit_tools.config`, optionally
460
- applies temporary overrides, and yields a callable compatible with
461
- `run_cli`.
448
+ applies temporary overrides, and yields a `SessionRunner`: a callable with
449
+ `run_cli`'s signature. Its exception handler resolves exit codes exactly like
450
+ `handle_cli_exception` - signals, broken pipes, Click exceptions and `SystemExit`
451
+ keep their own code and message, so a usage error exits `2` with Click's usage
452
+ text - and applies `summary_limit`/`verbose_limit` only to an exception none of
453
+ those claims.
462
454
 
463
455
  Parameters:
464
456
  - `summary_limit` (`int`, default `500`): Character budget when tracebacks are disabled.
@@ -466,7 +458,7 @@ Parameters:
466
458
  - `overrides` (`Mapping[str, object] | None`, default `None`): Mapping of configuration field/value pairs applied during the session. When `traceback` is supplied and `traceback_force_color` is omitted, colour output is automatically forced.
467
459
  - `restore` (`bool`, default `True`): When `True`, configuration state is restored after the session. Set to `False` to leave overrides in place once the context exits.
468
460
 
469
- Use it to restore configuration automatically—even when the wrapped command
461
+ Use it to restore configuration automatically - even when the wrapped command
470
462
  raises:
471
463
 
472
464
  ```python