lib-cli-exit-tools 2.3.4__tar.gz → 2.4.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.
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.gitignore +8 -4
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/CHANGELOG.md +12 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/Makefile +38 -4
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/PKG-INFO +39 -35
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/README.md +29 -25
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/pyproject.toml +10 -10
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/__init__conf__.py +1 -1
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/application/runner.py +52 -18
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/cli/styling.py +5 -4
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/tests/test_application_runner.py +84 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/tests/test_cli.py +7 -13
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.codacy/cli-config.yaml +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.codacy/cli.sh +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.codacy/codacy.yaml +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.codacy/tools-configs/lizard.yaml +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.codacy/tools-configs/trivy.yaml +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.data_arch_violations.json +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.devcontainer/devcontainer.json +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.devcontainer/settings.json +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.env.example +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.github/actions/extract-metadata/action.yml +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.github/dependabot.yml +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.github/workflows/codeql.yml +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.github/workflows/default_cicd_public.yml +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.github/workflows/default_release_public.yml +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.qlty/qlty.toml +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.snyk +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/CONTRIBUTING.md +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/DEVELOPMENT.md +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/INSTALL.md +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/LICENSE +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/codecov.yml +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/docs/system-design/module_reference.md +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/notebooks/Quickstart.ipynb +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/__init__.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/__main__.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/adapters/__init__.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/adapters/signals.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/application/__init__.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/cli/__init__.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/cli/commands.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/cli/group.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/cli/typed_click.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/core/__init__.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/core/configuration.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/core/exit_codes.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/lib_cli_exit_tools.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/py.typed +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/tests/conftest.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/tests/test_adapters_signals.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/tests/test_behaviors.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/tests/test_core_configuration.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/tests/test_core_exit_codes.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/tests/test_metadata.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/tests/test_module_entry.py +0 -0
- {lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/tests/test_signals_integration.py +0 -0
|
@@ -46,7 +46,8 @@ tmp_*
|
|
|
46
46
|
# *.py[cod]: catches .pyc, .pyo, .pyd
|
|
47
47
|
__pycache__/
|
|
48
48
|
*.py[cod]
|
|
49
|
-
|
|
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
|
-
|
|
76
|
+
# metadata for pip installs
|
|
77
|
+
*.egg-info/
|
|
76
78
|
*.egg
|
|
77
79
|
*.whl
|
|
78
80
|
pip-wheel-metadata/
|
|
79
|
-
|
|
80
|
-
*.
|
|
81
|
+
# PyInstaller metadata
|
|
82
|
+
*.manifest
|
|
83
|
+
# PyInstaller spec files
|
|
84
|
+
*.spec
|
|
81
85
|
|
|
82
86
|
# Eggs, parts, wheels, sdist, etc.
|
|
83
87
|
develop-eggs/
|
|
@@ -4,6 +4,18 @@ All notable changes to this project are documented here. The format follows [Kee
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.4.0] 2026-10-05 20:48:53
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- `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`.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `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.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- `README.md` is ASCII-only, and its `cli_session` section says how the session's handler resolves exit codes.
|
|
17
|
+
- Raised the dev tool floors.
|
|
18
|
+
|
|
7
19
|
## [2.3.4] 2026-07-29 15:20:59
|
|
8
20
|
|
|
9
21
|
### Fixed
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# BMK MAKEFILE 3.
|
|
1
|
+
# BMK MAKEFILE 3.18.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.
|
|
189
|
+
BMK_MIN := 3.18.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,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: lib_cli_exit_tools
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: CLI exit handling helpers: clean signals, exit codes, and error printing
|
|
5
5
|
Project-URL: Homepage, https://github.com/bitranox/lib_cli_exit_tools
|
|
6
6
|
Project-URL: Repository, https://github.com/bitranox/lib_cli_exit_tools.git
|
|
@@ -21,21 +21,21 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.14
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Requires-Python: >=3.10
|
|
24
|
-
Requires-Dist: rich-click>=1.9.
|
|
24
|
+
Requires-Dist: rich-click>=1.9.9
|
|
25
25
|
Provides-Extra: dev
|
|
26
26
|
Requires-Dist: bandit>=1.9.4; extra == 'dev'
|
|
27
|
-
Requires-Dist: build>=1.
|
|
28
|
-
Requires-Dist: click>=8.
|
|
29
|
-
Requires-Dist: coverage[toml]>=7.
|
|
30
|
-
Requires-Dist: hypothesis>=6.
|
|
31
|
-
Requires-Dist: import-linter>=2.
|
|
27
|
+
Requires-Dist: build>=1.6.1; extra == 'dev'
|
|
28
|
+
Requires-Dist: click>=8.5.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: coverage[toml]>=7.16.2; extra == 'dev'
|
|
30
|
+
Requires-Dist: hypothesis>=6.168.4; extra == 'dev'
|
|
31
|
+
Requires-Dist: import-linter>=2.15; extra == 'dev'
|
|
32
32
|
Requires-Dist: pip-audit>=2.10.1; extra == 'dev'
|
|
33
|
-
Requires-Dist: pyright[nodejs]>=1.1.
|
|
33
|
+
Requires-Dist: pyright[nodejs]>=1.1.414; extra == 'dev'
|
|
34
34
|
Requires-Dist: pytest-asyncio>=1.4.0; extra == 'dev'
|
|
35
35
|
Requires-Dist: pytest-cov>=7.1.0; extra == 'dev'
|
|
36
36
|
Requires-Dist: pytest>=9.1.1; extra == 'dev'
|
|
37
37
|
Requires-Dist: rtoml>=0.13.0; extra == 'dev'
|
|
38
|
-
Requires-Dist: ruff>=0.16.
|
|
38
|
+
Requires-Dist: ruff>=0.16.10; extra == 'dev'
|
|
39
39
|
Requires-Dist: textual>=8.2.8; extra == 'dev'
|
|
40
40
|
Requires-Dist: twine>=7.0.0; extra == 'dev'
|
|
41
41
|
Description-Content-Type: text/markdown
|
|
@@ -63,15 +63,15 @@ Small helpers for robust CLI exit handling:
|
|
|
63
63
|
|
|
64
64
|
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
65
|
|
|
66
|
-
1. **Correct Exit Codes**
|
|
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.
|
|
67
67
|
|
|
68
|
-
2. **Portable Signal Handling**
|
|
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.
|
|
69
69
|
|
|
70
|
-
3. **Clean Error Output**
|
|
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.
|
|
71
71
|
|
|
72
|
-
4. **Pipeline-Friendly**
|
|
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.
|
|
73
73
|
|
|
74
|
-
5. **Zero Boilerplate**
|
|
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.
|
|
75
75
|
|
|
76
76
|
## Install
|
|
77
77
|
|
|
@@ -101,11 +101,11 @@ The library installs three equivalent console scripts: `lib-cli-exit-tools` (pri
|
|
|
101
101
|
|
|
102
102
|
### Global Options
|
|
103
103
|
|
|
104
|
-
| Option
|
|
105
|
-
|
|
104
|
+
| Option | Default | Description |
|
|
105
|
+
|----------------------------------|---------|--------------------------------------|
|
|
106
106
|
| `--traceback` / `--no-traceback` | `False` | Show full Python traceback on errors |
|
|
107
|
-
| `--version`
|
|
108
|
-
| `-h`, `--help`
|
|
107
|
+
| `--version` | - | Show program version and exit |
|
|
108
|
+
| `-h`, `--help` | - | Show help message and exit |
|
|
109
109
|
|
|
110
110
|
### Commands
|
|
111
111
|
|
|
@@ -126,7 +126,7 @@ lib-cli-exit-tools --traceback fail # show full traceback
|
|
|
126
126
|
|
|
127
127
|
### Examples
|
|
128
128
|
|
|
129
|
-
The snippets below move from a minimal
|
|
129
|
+
The snippets below move from a minimal "hello world" through a production-ready
|
|
130
130
|
CLI that demonstrates configuration hooks and structured error handling.
|
|
131
131
|
|
|
132
132
|
#### 1. Minimal command (copy-paste ready)
|
|
@@ -223,7 +223,7 @@ def info(settings: Settings) -> None:
|
|
|
223
223
|
@cli.command()
|
|
224
224
|
@click.pass_obj
|
|
225
225
|
def fail(settings: Settings) -> None:
|
|
226
|
-
click.echo("About to fail
|
|
226
|
+
click.echo("About to fail...")
|
|
227
227
|
raise RuntimeError("intentional failure for diagnostics")
|
|
228
228
|
|
|
229
229
|
|
|
@@ -244,7 +244,7 @@ if __name__ == "__main__":
|
|
|
244
244
|
|
|
245
245
|
This version parses `--traceback` early (before Click sees it), then wires
|
|
246
246
|
configuration overrides, custom signal specs, and multiple commands into a
|
|
247
|
-
single composition point
|
|
247
|
+
single composition point - mirroring how a production CLI can layer policy logic
|
|
248
248
|
around Click while still delegating exit-code translation to `lib_cli_exit_tools`.
|
|
249
249
|
|
|
250
250
|
#### 4. Custom signal handlers
|
|
@@ -357,20 +357,20 @@ All configuration lives on the module-level `lib_cli_exit_tools.config` object.
|
|
|
357
357
|
from lib_cli_exit_tools import config
|
|
358
358
|
|
|
359
359
|
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,
|
|
360
|
+
config.exit_code_style = "sysexits" # emit BSD-style exit codes (EX_USAGE, EX_NOINPUT, ...)
|
|
361
361
|
config.broken_pipe_exit_code = 0 # treat BrokenPipeError as a benign truncation
|
|
362
362
|
```
|
|
363
363
|
|
|
364
364
|
Field reference:
|
|
365
365
|
|
|
366
|
-
| Field
|
|
367
|
-
|
|
368
|
-
| `traceback`
|
|
369
|
-
| `exit_code_style`
|
|
370
|
-
| `broken_pipe_exit_code` | `int`
|
|
371
|
-
| `traceback_force_color` | `bool`
|
|
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. |
|
|
372
372
|
|
|
373
|
-
Remember that `config` is module-level
|
|
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:
|
|
374
374
|
|
|
375
375
|
```python
|
|
376
376
|
from lib_cli_exit_tools import config_overrides, config
|
|
@@ -427,13 +427,13 @@ python -m scripts.test --coverage=off --verbose
|
|
|
427
427
|
The suite includes OS-aware cases (POSIX, Windows-specific signal handling), so
|
|
428
428
|
run it on each target platform you support to keep coverage consistent.
|
|
429
429
|
|
|
430
|
-
If your environment reports
|
|
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/`).
|
|
431
431
|
|
|
432
432
|
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
433
|
|
|
434
434
|
## Public API Reference
|
|
435
435
|
|
|
436
|
-
The package re-exports the helpers below via `lib_cli_exit_tools.__all__`. Import them directly with `from lib_cli_exit_tools import
|
|
436
|
+
The package re-exports the helpers below via `lib_cli_exit_tools.__all__`. Import them directly with `from lib_cli_exit_tools import ...`.
|
|
437
437
|
|
|
438
438
|
### `config`
|
|
439
439
|
Mutable dataclass-like singleton holding process-wide settings. Configure it during CLI startup.
|
|
@@ -457,8 +457,12 @@ Parameters:
|
|
|
457
457
|
|
|
458
458
|
### `cli_session(*, summary_limit=500, verbose_limit=10_000, overrides=None, restore=True)`
|
|
459
459
|
Context manager that snapshots `lib_cli_exit_tools.config`, optionally
|
|
460
|
-
applies temporary overrides, and yields a callable
|
|
461
|
-
`run_cli
|
|
460
|
+
applies temporary overrides, and yields a `SessionRunner`: a callable with
|
|
461
|
+
`run_cli`'s signature. Its exception handler resolves exit codes exactly like
|
|
462
|
+
`handle_cli_exception` - signals, broken pipes, Click exceptions and `SystemExit`
|
|
463
|
+
keep their own code and message, so a usage error exits `2` with Click's usage
|
|
464
|
+
text - and applies `summary_limit`/`verbose_limit` only to an exception none of
|
|
465
|
+
those claims.
|
|
462
466
|
|
|
463
467
|
Parameters:
|
|
464
468
|
- `summary_limit` (`int`, default `500`): Character budget when tracebacks are disabled.
|
|
@@ -466,7 +470,7 @@ Parameters:
|
|
|
466
470
|
- `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
471
|
- `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
472
|
|
|
469
|
-
Use it to restore configuration automatically
|
|
473
|
+
Use it to restore configuration automatically - even when the wrapped command
|
|
470
474
|
raises:
|
|
471
475
|
|
|
472
476
|
```python
|
|
@@ -21,15 +21,15 @@ Small helpers for robust CLI exit handling:
|
|
|
21
21
|
|
|
22
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:
|
|
23
23
|
|
|
24
|
-
1. **Correct Exit Codes**
|
|
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.
|
|
25
25
|
|
|
26
|
-
2. **Portable Signal Handling**
|
|
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.
|
|
27
27
|
|
|
28
|
-
3. **Clean Error Output**
|
|
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.
|
|
29
29
|
|
|
30
|
-
4. **Pipeline-Friendly**
|
|
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.
|
|
31
31
|
|
|
32
|
-
5. **Zero Boilerplate**
|
|
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.
|
|
33
33
|
|
|
34
34
|
## Install
|
|
35
35
|
|
|
@@ -59,11 +59,11 @@ The library installs three equivalent console scripts: `lib-cli-exit-tools` (pri
|
|
|
59
59
|
|
|
60
60
|
### Global Options
|
|
61
61
|
|
|
62
|
-
| Option
|
|
63
|
-
|
|
62
|
+
| Option | Default | Description |
|
|
63
|
+
|----------------------------------|---------|--------------------------------------|
|
|
64
64
|
| `--traceback` / `--no-traceback` | `False` | Show full Python traceback on errors |
|
|
65
|
-
| `--version`
|
|
66
|
-
| `-h`, `--help`
|
|
65
|
+
| `--version` | - | Show program version and exit |
|
|
66
|
+
| `-h`, `--help` | - | Show help message and exit |
|
|
67
67
|
|
|
68
68
|
### Commands
|
|
69
69
|
|
|
@@ -84,7 +84,7 @@ lib-cli-exit-tools --traceback fail # show full traceback
|
|
|
84
84
|
|
|
85
85
|
### Examples
|
|
86
86
|
|
|
87
|
-
The snippets below move from a minimal
|
|
87
|
+
The snippets below move from a minimal "hello world" through a production-ready
|
|
88
88
|
CLI that demonstrates configuration hooks and structured error handling.
|
|
89
89
|
|
|
90
90
|
#### 1. Minimal command (copy-paste ready)
|
|
@@ -181,7 +181,7 @@ def info(settings: Settings) -> None:
|
|
|
181
181
|
@cli.command()
|
|
182
182
|
@click.pass_obj
|
|
183
183
|
def fail(settings: Settings) -> None:
|
|
184
|
-
click.echo("About to fail
|
|
184
|
+
click.echo("About to fail...")
|
|
185
185
|
raise RuntimeError("intentional failure for diagnostics")
|
|
186
186
|
|
|
187
187
|
|
|
@@ -202,7 +202,7 @@ if __name__ == "__main__":
|
|
|
202
202
|
|
|
203
203
|
This version parses `--traceback` early (before Click sees it), then wires
|
|
204
204
|
configuration overrides, custom signal specs, and multiple commands into a
|
|
205
|
-
single composition point
|
|
205
|
+
single composition point - mirroring how a production CLI can layer policy logic
|
|
206
206
|
around Click while still delegating exit-code translation to `lib_cli_exit_tools`.
|
|
207
207
|
|
|
208
208
|
#### 4. Custom signal handlers
|
|
@@ -315,20 +315,20 @@ All configuration lives on the module-level `lib_cli_exit_tools.config` object.
|
|
|
315
315
|
from lib_cli_exit_tools import config
|
|
316
316
|
|
|
317
317
|
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,
|
|
318
|
+
config.exit_code_style = "sysexits" # emit BSD-style exit codes (EX_USAGE, EX_NOINPUT, ...)
|
|
319
319
|
config.broken_pipe_exit_code = 0 # treat BrokenPipeError as a benign truncation
|
|
320
320
|
```
|
|
321
321
|
|
|
322
322
|
Field reference:
|
|
323
323
|
|
|
324
|
-
| Field
|
|
325
|
-
|
|
326
|
-
| `traceback`
|
|
327
|
-
| `exit_code_style`
|
|
328
|
-
| `broken_pipe_exit_code` | `int`
|
|
329
|
-
| `traceback_force_color` | `bool`
|
|
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. |
|
|
330
330
|
|
|
331
|
-
Remember that `config` is module-level
|
|
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:
|
|
332
332
|
|
|
333
333
|
```python
|
|
334
334
|
from lib_cli_exit_tools import config_overrides, config
|
|
@@ -385,13 +385,13 @@ python -m scripts.test --coverage=off --verbose
|
|
|
385
385
|
The suite includes OS-aware cases (POSIX, Windows-specific signal handling), so
|
|
386
386
|
run it on each target platform you support to keep coverage consistent.
|
|
387
387
|
|
|
388
|
-
If your environment reports
|
|
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/`).
|
|
389
389
|
|
|
390
390
|
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
391
|
|
|
392
392
|
## Public API Reference
|
|
393
393
|
|
|
394
|
-
The package re-exports the helpers below via `lib_cli_exit_tools.__all__`. Import them directly with `from lib_cli_exit_tools import
|
|
394
|
+
The package re-exports the helpers below via `lib_cli_exit_tools.__all__`. Import them directly with `from lib_cli_exit_tools import ...`.
|
|
395
395
|
|
|
396
396
|
### `config`
|
|
397
397
|
Mutable dataclass-like singleton holding process-wide settings. Configure it during CLI startup.
|
|
@@ -415,8 +415,12 @@ Parameters:
|
|
|
415
415
|
|
|
416
416
|
### `cli_session(*, summary_limit=500, verbose_limit=10_000, overrides=None, restore=True)`
|
|
417
417
|
Context manager that snapshots `lib_cli_exit_tools.config`, optionally
|
|
418
|
-
applies temporary overrides, and yields a callable
|
|
419
|
-
`run_cli
|
|
418
|
+
applies temporary overrides, and yields a `SessionRunner`: a callable with
|
|
419
|
+
`run_cli`'s signature. Its exception handler resolves exit codes exactly like
|
|
420
|
+
`handle_cli_exception` - signals, broken pipes, Click exceptions and `SystemExit`
|
|
421
|
+
keep their own code and message, so a usage error exits `2` with Click's usage
|
|
422
|
+
text - and applies `summary_limit`/`verbose_limit` only to an exception none of
|
|
423
|
+
those claims.
|
|
420
424
|
|
|
421
425
|
Parameters:
|
|
422
426
|
- `summary_limit` (`int`, default `500`): Character budget when tracebacks are disabled.
|
|
@@ -424,7 +428,7 @@ Parameters:
|
|
|
424
428
|
- `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
429
|
- `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
430
|
|
|
427
|
-
Use it to restore configuration automatically
|
|
431
|
+
Use it to restore configuration automatically - even when the wrapped command
|
|
428
432
|
raises:
|
|
429
433
|
|
|
430
434
|
```python
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "lib_cli_exit_tools"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.4.0"
|
|
4
4
|
description = "CLI exit handling helpers: clean signals, exit codes, and error printing"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
|
-
dependencies = ["rich-click>=1.9.
|
|
7
|
+
dependencies = ["rich-click>=1.9.9"]
|
|
8
8
|
license = { text = "MIT" }
|
|
9
9
|
authors = [{ name = "bitranox", email = "bitranox@gmail.com" }]
|
|
10
10
|
keywords = ["cli", "exit", "signals", "errors", "click"]
|
|
@@ -40,7 +40,7 @@ Issues = "https://github.com/bitranox/lib_cli_exit_tools/issues"
|
|
|
40
40
|
dev = [
|
|
41
41
|
# 1.5.1 was yanked upstream (unintended breaking changes), so 1.5.0 is the newest usable
|
|
42
42
|
# release; a >=1.5.1 floor has ZERO candidates and makes this extra unresolvable.
|
|
43
|
-
"build>=1.
|
|
43
|
+
"build>=1.6.1",
|
|
44
44
|
# codecov-cli is disabled, not dropped: it requires click<8.3.0, which drags click back to
|
|
45
45
|
# 8.2.1 - the version with CVE-2026-7246 (command injection in click.edit(), fixed in 8.3.3).
|
|
46
46
|
# Nothing else here caps click, so with this line active the audit gate goes red on every
|
|
@@ -49,17 +49,17 @@ dev = [
|
|
|
49
49
|
# Re-enable once codecov-cli relaxes its click<8.3.0 pin.
|
|
50
50
|
# "codecov-cli>=11.2.8",
|
|
51
51
|
# Transitive CVE pins
|
|
52
|
-
"click>=8.
|
|
53
|
-
"coverage[toml]>=7.
|
|
54
|
-
"hypothesis>=6.
|
|
55
|
-
"import-linter>=2.
|
|
52
|
+
"click>=8.5.0", # CVE-2026-7246
|
|
53
|
+
"coverage[toml]>=7.16.2",
|
|
54
|
+
"hypothesis>=6.168.4",
|
|
55
|
+
"import-linter>=2.15",
|
|
56
56
|
"bandit>=1.9.4",
|
|
57
57
|
"pip-audit>=2.10.1",
|
|
58
|
-
"pyright[nodejs]>=1.1.
|
|
58
|
+
"pyright[nodejs]>=1.1.414",
|
|
59
59
|
"pytest>=9.1.1",
|
|
60
60
|
"pytest-asyncio>=1.4.0",
|
|
61
61
|
"pytest-cov>=7.1.0",
|
|
62
|
-
"ruff>=0.16.
|
|
62
|
+
"ruff>=0.16.10",
|
|
63
63
|
"textual>=8.2.8",
|
|
64
64
|
"rtoml>=0.13.0",
|
|
65
65
|
"twine>=7.0.0"
|
|
@@ -72,7 +72,7 @@ cli-exit-tools = "lib_cli_exit_tools.cli:main"
|
|
|
72
72
|
lib-cli-exit-tools = "lib_cli_exit_tools.cli:main"
|
|
73
73
|
|
|
74
74
|
[build-system]
|
|
75
|
-
requires = ["hatchling>=1.
|
|
75
|
+
requires = ["hatchling>=1.32.4"]
|
|
76
76
|
build-backend = "hatchling.build"
|
|
77
77
|
|
|
78
78
|
[tool.hatch.build.targets.wheel]
|
{lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/__init__conf__.py
RENAMED
|
@@ -26,7 +26,7 @@ name = "lib_cli_exit_tools"
|
|
|
26
26
|
#: Human-readable summary shown in CLI help output.
|
|
27
27
|
title = "CLI exit handling helpers: clean signals, exit codes, and error printing"
|
|
28
28
|
#: Current release version pulled from ``pyproject.toml`` by automation.
|
|
29
|
-
version = "2.
|
|
29
|
+
version = "2.4.0"
|
|
30
30
|
#: Repository homepage presented to users.
|
|
31
31
|
homepage = "https://github.com/bitranox/lib_cli_exit_tools"
|
|
32
32
|
#: Author attribution surfaced in CLI output.
|
{lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/application/runner.py
RENAMED
|
@@ -67,6 +67,7 @@ class ClickCommand(Protocol):
|
|
|
67
67
|
|
|
68
68
|
__all__ = [
|
|
69
69
|
"SessionOverrides",
|
|
70
|
+
"SessionRunner",
|
|
70
71
|
"cli_session",
|
|
71
72
|
"flush_streams",
|
|
72
73
|
"handle_cli_exception",
|
|
@@ -75,6 +76,22 @@ __all__ = [
|
|
|
75
76
|
]
|
|
76
77
|
|
|
77
78
|
|
|
79
|
+
class SessionRunner(Protocol):
|
|
80
|
+
"""The callable :func:`cli_session` yields: :func:`run_cli` with the session's handler."""
|
|
81
|
+
|
|
82
|
+
def __call__( # pragma: no cover - structural typing
|
|
83
|
+
self,
|
|
84
|
+
command: ClickCommand,
|
|
85
|
+
*,
|
|
86
|
+
argv: Sequence[str] | None = None,
|
|
87
|
+
prog_name: str | None = None,
|
|
88
|
+
signal_specs: Sequence[SignalSpec] | None = None,
|
|
89
|
+
install_signals: bool = True,
|
|
90
|
+
exception_handler: Callable[[BaseException], int] | None = None,
|
|
91
|
+
signal_installer: Callable[[Sequence[SignalSpec] | None], Callable[[], None]] | None = None,
|
|
92
|
+
) -> int: ...
|
|
93
|
+
|
|
94
|
+
|
|
78
95
|
class _Echo(Protocol):
|
|
79
96
|
"""Protocol describing the echo interface expected by error handlers."""
|
|
80
97
|
|
|
@@ -360,7 +377,7 @@ def handle_cli_exception(
|
|
|
360
377
|
|
|
361
378
|
specs = _resolve_signal_specs(signal_specs)
|
|
362
379
|
echo_fn = _choose_echo(echo)
|
|
363
|
-
return _resolve_exit_code(exc, specs, echo_fn)
|
|
380
|
+
return _resolve_exit_code(exc, specs, echo_fn, fallback=_render_and_translate)
|
|
364
381
|
|
|
365
382
|
|
|
366
383
|
def _choose_echo(echo: _Echo | None) -> _Echo:
|
|
@@ -372,13 +389,20 @@ def _resolve_exit_code(
|
|
|
372
389
|
exc: BaseException,
|
|
373
390
|
specs: Sequence[SignalSpec],
|
|
374
391
|
echo: _Echo,
|
|
392
|
+
*,
|
|
393
|
+
fallback: Callable[[BaseException], int],
|
|
375
394
|
) -> int:
|
|
376
|
-
"""Walk the resolver chain
|
|
395
|
+
"""Walk the resolver chain; hand an exception none of them claims to ``fallback``.
|
|
396
|
+
|
|
397
|
+
Signals, broken pipes, Click exceptions and ``SystemExit`` carry their own
|
|
398
|
+
exit code and message, so only an otherwise unhandled exception reaches
|
|
399
|
+
``fallback``, which renders it and maps it to a code.
|
|
400
|
+
"""
|
|
377
401
|
for resolver in _exception_resolvers(specs, echo):
|
|
378
402
|
code = resolver(exc)
|
|
379
403
|
if code is not None:
|
|
380
404
|
return code
|
|
381
|
-
return
|
|
405
|
+
return fallback(exc)
|
|
382
406
|
|
|
383
407
|
|
|
384
408
|
def _exception_resolvers(
|
|
@@ -466,15 +490,7 @@ def cli_session(
|
|
|
466
490
|
verbose_limit: int = 10_000,
|
|
467
491
|
overrides: SessionOverrides | None = None,
|
|
468
492
|
restore: bool = True,
|
|
469
|
-
) -> Generator[
|
|
470
|
-
Callable[
|
|
471
|
-
[
|
|
472
|
-
ClickCommand,
|
|
473
|
-
],
|
|
474
|
-
int,
|
|
475
|
-
]
|
|
476
|
-
| Callable[..., int]
|
|
477
|
-
]:
|
|
493
|
+
) -> Generator[SessionRunner]:
|
|
478
494
|
"""Provide a managed execution context around :func:`run_cli`.
|
|
479
495
|
|
|
480
496
|
Why
|
|
@@ -509,7 +525,7 @@ def cli_session(
|
|
|
509
525
|
|
|
510
526
|
Yields
|
|
511
527
|
------
|
|
512
|
-
|
|
528
|
+
SessionRunner
|
|
513
529
|
Function that accepts a Click command and forwards optional ``run_cli``
|
|
514
530
|
keyword arguments, returning the resulting exit code.
|
|
515
531
|
"""
|
|
@@ -518,7 +534,6 @@ def cli_session(
|
|
|
518
534
|
manager = _session_config_manager(applied, restore=restore)
|
|
519
535
|
|
|
520
536
|
with manager:
|
|
521
|
-
handler = _session_exception_handler(summary_limit, verbose_limit)
|
|
522
537
|
|
|
523
538
|
def _run(
|
|
524
539
|
command: ClickCommand,
|
|
@@ -530,7 +545,11 @@ def cli_session(
|
|
|
530
545
|
exception_handler: Callable[[BaseException], int] | None = None,
|
|
531
546
|
signal_installer: Callable[[Sequence[SignalSpec] | None], Callable[[], None]] | None = None,
|
|
532
547
|
) -> int:
|
|
533
|
-
chosen_handler = exception_handler or
|
|
548
|
+
chosen_handler = exception_handler or _session_exception_handler(
|
|
549
|
+
summary_limit=summary_limit,
|
|
550
|
+
verbose_limit=verbose_limit,
|
|
551
|
+
signal_specs=signal_specs,
|
|
552
|
+
)
|
|
534
553
|
return run_cli(
|
|
535
554
|
command,
|
|
536
555
|
argv=argv,
|
|
@@ -587,15 +606,30 @@ def _apply_overrides_without_restore(applied: SessionOverrides) -> Generator[Non
|
|
|
587
606
|
yield
|
|
588
607
|
|
|
589
608
|
|
|
590
|
-
def _session_exception_handler(
|
|
591
|
-
|
|
609
|
+
def _session_exception_handler(
|
|
610
|
+
*,
|
|
611
|
+
summary_limit: int,
|
|
612
|
+
verbose_limit: int,
|
|
613
|
+
signal_specs: Sequence[SignalSpec] | None,
|
|
614
|
+
) -> Callable[[BaseException], int]:
|
|
615
|
+
"""Build the exception handler used inside :func:`cli_session`.
|
|
592
616
|
|
|
593
|
-
|
|
617
|
+
It runs the same resolver chain as :func:`handle_cli_exception`, so a Click
|
|
618
|
+
usage error exits with Click's own code and message under a session too;
|
|
619
|
+
the session's character budgets apply only to the exception nothing else
|
|
620
|
+
claims.
|
|
621
|
+
"""
|
|
622
|
+
specs = _resolve_signal_specs(signal_specs)
|
|
623
|
+
|
|
624
|
+
def _render_with_session_limits(exc: BaseException) -> int:
|
|
594
625
|
active = bool(config.traceback)
|
|
595
626
|
limit = verbose_limit if active else summary_limit
|
|
596
627
|
print_exception_message(trace_back=active, length_limit=limit)
|
|
597
628
|
return get_system_exit_code(exc)
|
|
598
629
|
|
|
630
|
+
def _handler(exc: BaseException) -> int:
|
|
631
|
+
return _resolve_exit_code(exc, specs, _default_echo, fallback=_render_with_session_limits)
|
|
632
|
+
|
|
599
633
|
return _handler
|
|
600
634
|
|
|
601
635
|
|
|
@@ -15,10 +15,10 @@ System Integration:
|
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
+
import sys
|
|
18
19
|
from contextlib import contextmanager
|
|
19
20
|
from typing import TYPE_CHECKING, Any, TypedDict
|
|
20
21
|
|
|
21
|
-
import rich_click as click
|
|
22
22
|
from rich_click import rich_click as rich_config
|
|
23
23
|
|
|
24
24
|
if TYPE_CHECKING:
|
|
@@ -104,9 +104,10 @@ def _temporary_rich_click_configuration() -> Generator[None]: # pyright: ignore
|
|
|
104
104
|
"""Apply plain-output safeguards and restore rich-click globals afterwards."""
|
|
105
105
|
|
|
106
106
|
snapshot = _snapshot_rich_click_options()
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
# sys.stdout/sys.stderr, not click's resolution of them: rich renders through
|
|
108
|
+
# its own Console on these streams, and click 8.5.0 deprecated get_text_stream
|
|
109
|
+
# (removed in 9.0) with no supported replacement.
|
|
110
|
+
if _needs_plain_output(sys.stdout) and _needs_plain_output(sys.stderr):
|
|
110
111
|
_prefer_ascii_layout()
|
|
111
112
|
try:
|
|
112
113
|
yield
|
|
@@ -417,6 +417,90 @@ def test_session_config_manager_with_no_overrides_returns_null_context() -> None
|
|
|
417
417
|
pass # Should not raise
|
|
418
418
|
|
|
419
419
|
|
|
420
|
+
class _ExitThree(click.ClickException):
|
|
421
|
+
"""A Click exception that chooses its own exit code."""
|
|
422
|
+
|
|
423
|
+
exit_code = 3
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class _HangupError(Exception):
|
|
427
|
+
"""A caller-defined signal exception for signal-spec tests."""
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@click.command("ok")
|
|
431
|
+
def _ok_command() -> None:
|
|
432
|
+
"""Succeed."""
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@click.command("chosen")
|
|
436
|
+
def _chosen_command() -> None:
|
|
437
|
+
"""Fail with a Click exception that sets exit_code 3."""
|
|
438
|
+
raise _ExitThree("chosen failure")
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@click.command("boom")
|
|
442
|
+
def _boom_command() -> None:
|
|
443
|
+
"""Fail with an ordinary exception."""
|
|
444
|
+
raise RuntimeError("boom")
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@click.command("hangup")
|
|
448
|
+
def _hangup_command() -> None:
|
|
449
|
+
"""Raise the caller-defined signal exception."""
|
|
450
|
+
raise _HangupError
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _session_probe_cli() -> click.Group:
|
|
454
|
+
"""Build a real Click group whose commands cover every exit-code path."""
|
|
455
|
+
return click.Group("probe", commands=[_ok_command, _chosen_command, _boom_command, _hangup_command])
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
@pytest.mark.os_agnostic
|
|
459
|
+
def test_a_usage_error_under_cli_session_exits_two_with_clicks_usage_message(reset_config: None, capsys: pytest.CaptureFixture[str]) -> None:
|
|
460
|
+
with runner.cli_session() as execute:
|
|
461
|
+
code = execute(_session_probe_cli(), argv=["--bad-flag"], install_signals=False)
|
|
462
|
+
|
|
463
|
+
err = capsys.readouterr().err
|
|
464
|
+
assert code == 2
|
|
465
|
+
assert "Usage:" in err
|
|
466
|
+
assert "No such option" in err
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
@pytest.mark.os_agnostic
|
|
470
|
+
@pytest.mark.parametrize(
|
|
471
|
+
"argv",
|
|
472
|
+
[["ok"], ["--bad-flag"], ["no-such-command"], ["ok", "--bad-flag"], ["chosen"], ["boom"]],
|
|
473
|
+
ids=["success", "bad-flag", "unknown-command", "bad-subcommand-flag", "click-chosen-code", "plain-exception"],
|
|
474
|
+
)
|
|
475
|
+
def test_cli_session_exits_with_the_code_run_cli_gives(reset_config: None, argv: list[str]) -> None:
|
|
476
|
+
expected = runner.run_cli(_session_probe_cli(), argv=argv, install_signals=False)
|
|
477
|
+
|
|
478
|
+
with runner.cli_session() as execute:
|
|
479
|
+
code = execute(_session_probe_cli(), argv=argv, install_signals=False)
|
|
480
|
+
|
|
481
|
+
assert code == expected
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@pytest.mark.os_agnostic
|
|
485
|
+
def test_cli_session_honours_the_signal_specs_its_caller_passes(reset_config: None, capsys: pytest.CaptureFixture[str]) -> None:
|
|
486
|
+
specs = [SignalSpec(signum=0, exception=_HangupError, message="hung up", exit_code=129)]
|
|
487
|
+
|
|
488
|
+
with runner.cli_session() as execute:
|
|
489
|
+
code = execute(_session_probe_cli(), argv=["hangup"], signal_specs=specs, install_signals=False)
|
|
490
|
+
|
|
491
|
+
assert code == 129
|
|
492
|
+
assert "hung up" in capsys.readouterr().err
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
@pytest.mark.os_agnostic
|
|
496
|
+
def test_cli_session_still_truncates_an_unhandled_exception_at_its_summary_limit(reset_config: None, capsys: pytest.CaptureFixture[str]) -> None:
|
|
497
|
+
with runner.cli_session(summary_limit=5) as execute:
|
|
498
|
+
code = execute(_session_probe_cli(), argv=["boom"], install_signals=False)
|
|
499
|
+
|
|
500
|
+
assert code != 0
|
|
501
|
+
assert "[TRUNCATED at 5 characters]" in capsys.readouterr().err
|
|
502
|
+
|
|
503
|
+
|
|
420
504
|
# =============================================================================
|
|
421
505
|
# Run CLI
|
|
422
506
|
# =============================================================================
|
|
@@ -10,10 +10,10 @@ Each test verifies exactly one CLI behavior:
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import sys
|
|
13
14
|
from typing import TYPE_CHECKING, Any
|
|
14
15
|
|
|
15
16
|
import pytest
|
|
16
|
-
import rich_click as click
|
|
17
17
|
from rich_click import rich_click as rich_config
|
|
18
18
|
|
|
19
19
|
import lib_cli_exit_tools
|
|
@@ -260,10 +260,8 @@ def test_ascii_streams_disable_force_terminal(monkeypatch: pytest.MonkeyPatch) -
|
|
|
260
260
|
|
|
261
261
|
original = _snapshot_rich_click_options()
|
|
262
262
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
monkeypatch.setattr(click, "get_text_stream", _get_dummy_stream)
|
|
263
|
+
monkeypatch.setattr(sys, "stdout", DummyStream())
|
|
264
|
+
monkeypatch.setattr(sys, "stderr", DummyStream())
|
|
267
265
|
|
|
268
266
|
with _temporary_rich_click_configuration():
|
|
269
267
|
assert rich_config.FORCE_TERMINAL is False
|
|
@@ -282,10 +280,8 @@ def test_ascii_streams_disable_color_system(monkeypatch: pytest.MonkeyPatch) ->
|
|
|
282
280
|
|
|
283
281
|
original = _snapshot_rich_click_options()
|
|
284
282
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
monkeypatch.setattr(click, "get_text_stream", _get_dummy_stream)
|
|
283
|
+
monkeypatch.setattr(sys, "stdout", DummyStream())
|
|
284
|
+
monkeypatch.setattr(sys, "stderr", DummyStream())
|
|
289
285
|
|
|
290
286
|
with _temporary_rich_click_configuration():
|
|
291
287
|
assert rich_config.COLOR_SYSTEM is None
|
|
@@ -304,10 +300,8 @@ def test_utf8_tty_streams_preserve_color_system(monkeypatch: pytest.MonkeyPatch)
|
|
|
304
300
|
|
|
305
301
|
original = _snapshot_rich_click_options()
|
|
306
302
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
monkeypatch.setattr(click, "get_text_stream", _get_fancy_stream)
|
|
303
|
+
monkeypatch.setattr(sys, "stdout", FancyStream())
|
|
304
|
+
monkeypatch.setattr(sys, "stderr", FancyStream())
|
|
311
305
|
|
|
312
306
|
with _temporary_rich_click_configuration():
|
|
313
307
|
assert original.get("COLOR_SYSTEM") == rich_config.COLOR_SYSTEM
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.github/actions/extract-metadata/action.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.github/workflows/default_cicd_public.yml
RENAMED
|
File without changes
|
{lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/.github/workflows/default_release_public.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/docs/system-design/module_reference.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/adapters/__init__.py
RENAMED
|
File without changes
|
{lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/adapters/signals.py
RENAMED
|
File without changes
|
{lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/application/__init__.py
RENAMED
|
File without changes
|
{lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/cli/__init__.py
RENAMED
|
File without changes
|
{lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/cli/commands.py
RENAMED
|
File without changes
|
|
File without changes
|
{lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/cli/typed_click.py
RENAMED
|
File without changes
|
{lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/core/__init__.py
RENAMED
|
File without changes
|
{lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/core/configuration.py
RENAMED
|
File without changes
|
{lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/core/exit_codes.py
RENAMED
|
File without changes
|
{lib_cli_exit_tools-2.3.4 → lib_cli_exit_tools-2.4.0}/src/lib_cli_exit_tools/lib_cli_exit_tools.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|