lib-cli-exit-tools 2.2.1__py3-none-any.whl
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/__init__.py +47 -0
- lib_cli_exit_tools/__init__conf__.py +74 -0
- lib_cli_exit_tools/__main__.py +13 -0
- lib_cli_exit_tools/adapters/__init__.py +16 -0
- lib_cli_exit_tools/adapters/signals.py +178 -0
- lib_cli_exit_tools/application/__init__.py +16 -0
- lib_cli_exit_tools/application/runner.py +714 -0
- lib_cli_exit_tools/cli.py +261 -0
- lib_cli_exit_tools/core/__init__.py +16 -0
- lib_cli_exit_tools/core/configuration.py +155 -0
- lib_cli_exit_tools/core/exit_codes.py +420 -0
- lib_cli_exit_tools/lib_cli_exit_tools.py +83 -0
- lib_cli_exit_tools/py.typed +0 -0
- lib_cli_exit_tools-2.2.1.dist-info/METADATA +647 -0
- lib_cli_exit_tools-2.2.1.dist-info/RECORD +18 -0
- lib_cli_exit_tools-2.2.1.dist-info/WHEEL +4 -0
- lib_cli_exit_tools-2.2.1.dist-info/entry_points.txt +4 -0
- lib_cli_exit_tools-2.2.1.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lib_cli_exit_tools
|
|
3
|
+
Version: 2.2.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: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: rich-click>=1.9.4
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: bandit>=1.9.2; extra == 'dev'
|
|
22
|
+
Requires-Dist: build>=1.3.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: codecov-cli>=11.2.6; extra == 'dev'
|
|
24
|
+
Requires-Dist: coverage[toml]>=7.13.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: hypothesis>=6.148.7; extra == 'dev'
|
|
26
|
+
Requires-Dist: import-linter>=2.9; extra == 'dev'
|
|
27
|
+
Requires-Dist: pip-audit>=2.10.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pyright>=1.1.407; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-asyncio>=1.3.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=9.0.2; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.14.8; extra == 'dev'
|
|
33
|
+
Requires-Dist: textual>=6.8.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: tomli>=2.3.0; (python_version < '3.11') and extra == 'dev'
|
|
35
|
+
Requires-Dist: twine>=6.2.0; extra == 'dev'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# lib_cli_exit_tools
|
|
39
|
+
|
|
40
|
+
<!-- Badges -->
|
|
41
|
+
[](https://github.com/bitranox/lib_cli_exit_tools/actions/workflows/ci.yml)
|
|
42
|
+
[](https://github.com/bitranox/lib_cli_exit_tools/actions/workflows/codeql.yml)
|
|
43
|
+
[](LICENSE)
|
|
44
|
+
[](https://codespaces.new/bitranox/lib_cli_exit_tools?quickstart=1)
|
|
45
|
+
[](https://pypi.org/project/lib_cli_exit_tools/)
|
|
46
|
+
[](https://pypi.org/project/lib_cli_exit_tools/)
|
|
47
|
+
[](https://docs.astral.sh/ruff/)
|
|
48
|
+
[](https://codecov.io/gh/bitranox/lib_cli_exit_tools)
|
|
49
|
+
[](https://qlty.sh/gh/bitranox/projects/lib_cli_exit_tools)
|
|
50
|
+
[](https://snyk.io/test/github/bitranox/lib_cli_exit_tools)
|
|
51
|
+
[](https://github.com/PyCQA/bandit)
|
|
52
|
+
|
|
53
|
+
Small helpers for robust CLI exit handling:
|
|
54
|
+
- Portable signal handling (SIGINT, SIGTERM/SIGBREAK)
|
|
55
|
+
- Consistent exception → exit code mapping
|
|
56
|
+
- Concise error printing with optional traceback and subprocess stdout/stderr capture
|
|
57
|
+
|
|
58
|
+
## Why lib_cli_exit_tools?
|
|
59
|
+
|
|
60
|
+
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:
|
|
61
|
+
|
|
62
|
+
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.
|
|
63
|
+
|
|
64
|
+
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.
|
|
65
|
+
|
|
66
|
+
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.
|
|
67
|
+
|
|
68
|
+
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.
|
|
69
|
+
|
|
70
|
+
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.
|
|
71
|
+
|
|
72
|
+
## Install
|
|
73
|
+
|
|
74
|
+
Requires Python 3.10 or newer.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install lib_cli_exit_tools
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
See [INSTALL.md](INSTALL.md) for editable installs, pipx/uv usage, and troubleshooting tips.
|
|
81
|
+
|
|
82
|
+
## Usage
|
|
83
|
+
|
|
84
|
+
Console script (all commands map to ``lib_cli_exit_tools.cli:main``):
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# After install (pip/pipx/uv tool)
|
|
88
|
+
lib-cli-exit-tools --help
|
|
89
|
+
lib-cli-exit-tools info
|
|
90
|
+
lib-cli-exit-tools fail # intentionally trigger RuntimeError to test error paths
|
|
91
|
+
# Aliases are also generated: `cli-exit-tools`, `lib_cli_exit_tools`
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## CLI Reference
|
|
95
|
+
|
|
96
|
+
The library installs three equivalent console scripts: `lib-cli-exit-tools` (primary), `cli-exit-tools`, and `lib_cli_exit_tools`. All invoke `lib_cli_exit_tools.cli:main`.
|
|
97
|
+
|
|
98
|
+
### Global Options
|
|
99
|
+
|
|
100
|
+
| Option | Default | Description |
|
|
101
|
+
|--------|---------|-------------|
|
|
102
|
+
| `--traceback` / `--no-traceback` | `False` | Show full Python traceback on errors |
|
|
103
|
+
| `--version` | — | Show program version and exit |
|
|
104
|
+
| `-h`, `--help` | — | Show help message and exit |
|
|
105
|
+
|
|
106
|
+
### Commands
|
|
107
|
+
|
|
108
|
+
#### `info`
|
|
109
|
+
Display package metadata (name, version, homepage, author).
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
lib-cli-exit-tools info
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### `fail`
|
|
116
|
+
Intentionally raise `RuntimeError('i should fail')` to validate error handling paths.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
lib-cli-exit-tools fail
|
|
120
|
+
lib-cli-exit-tools --traceback fail # show full traceback
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Examples
|
|
124
|
+
|
|
125
|
+
The snippets below move from a minimal “hello world” through a production-ready
|
|
126
|
+
CLI that demonstrates configuration hooks and structured error handling.
|
|
127
|
+
|
|
128
|
+
#### 1. Minimal command (copy-paste ready)
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from __future__ import annotations
|
|
132
|
+
|
|
133
|
+
import click
|
|
134
|
+
|
|
135
|
+
from lib_cli_exit_tools import run_cli
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@click.command()
|
|
139
|
+
def hello() -> None:
|
|
140
|
+
"""Say hello with automatic signal-aware exit handling."""
|
|
141
|
+
|
|
142
|
+
click.echo("Hello from lib_cli_exit_tools!")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
raise SystemExit(run_cli(hello))
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Run it with:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
python hello.py
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### 2. Traceback-mode helper using `cli_session`
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from __future__ import annotations
|
|
159
|
+
|
|
160
|
+
import click
|
|
161
|
+
|
|
162
|
+
from lib_cli_exit_tools import cli_session, run_cli
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@click.command()
|
|
166
|
+
@click.option("--traceback", is_flag=True, help="Show full tracebacks on error")
|
|
167
|
+
def cli(traceback: bool) -> int:
|
|
168
|
+
with cli_session(overrides={"traceback": traceback}) as execute:
|
|
169
|
+
return execute(failable, argv=[])
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@click.command()
|
|
173
|
+
def failable() -> None:
|
|
174
|
+
raise RuntimeError("toggle --traceback to see the full traceback")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == "__main__":
|
|
178
|
+
raise SystemExit(run_cli(cli))
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Invoking `python cli.py --traceback` enables coloured tracebacks just for that
|
|
182
|
+
run and resets the configuration afterwards.
|
|
183
|
+
|
|
184
|
+
#### 3. Full-featured multi-command CLI
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from __future__ import annotations
|
|
188
|
+
|
|
189
|
+
import json
|
|
190
|
+
from dataclasses import dataclass
|
|
191
|
+
|
|
192
|
+
import click
|
|
193
|
+
|
|
194
|
+
from lib_cli_exit_tools import SignalSpec, cli_session, default_signal_specs
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass(slots=True)
|
|
198
|
+
class Settings:
|
|
199
|
+
pretty: bool
|
|
200
|
+
traceback: bool
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@click.group()
|
|
204
|
+
@click.option("--pretty/--no-pretty", default=True)
|
|
205
|
+
@click.option("--traceback", is_flag=True, help="Show full tracebacks on error")
|
|
206
|
+
@click.pass_context
|
|
207
|
+
def cli(ctx: click.Context, pretty: bool, traceback: bool) -> None:
|
|
208
|
+
ctx.obj = Settings(pretty=pretty, traceback=traceback)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@cli.command()
|
|
212
|
+
@click.pass_obj
|
|
213
|
+
def info(settings: Settings) -> None:
|
|
214
|
+
payload = {"traceback": settings.traceback, "pretty": settings.pretty}
|
|
215
|
+
click.echo(json.dumps(payload, indent=2 if settings.pretty else None))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@cli.command()
|
|
219
|
+
@click.pass_obj
|
|
220
|
+
def fail(settings: Settings) -> None:
|
|
221
|
+
click.echo("About to fail…")
|
|
222
|
+
raise RuntimeError("intentional failure for diagnostics")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def main() -> int:
|
|
226
|
+
overrides = {"traceback": True, "traceback_force_color": True}
|
|
227
|
+
signals: list[SignalSpec] = default_signal_specs()
|
|
228
|
+
with cli_session(overrides=overrides) as execute:
|
|
229
|
+
return execute(cli, signal_specs=signals)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
if __name__ == "__main__":
|
|
233
|
+
raise SystemExit(main())
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
This version wires configuration overrides, custom signal specs, and multiple
|
|
237
|
+
commands into a single composition point—mirroring how a production CLI can
|
|
238
|
+
layer policy logic around Click while still delegating exit-code translation to
|
|
239
|
+
`lib_cli_exit_tools`.
|
|
240
|
+
|
|
241
|
+
#### 4. Custom signal handlers
|
|
242
|
+
|
|
243
|
+
`run_cli` accepts an explicit `signal_spec` sequence and a `signal_installer`
|
|
244
|
+
hook so you can provide bespoke behaviour. The example below installs a custom
|
|
245
|
+
SIGUSR1 handler alongside the library defaults:
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from __future__ import annotations
|
|
249
|
+
|
|
250
|
+
import signal
|
|
251
|
+
from contextlib import ExitStack
|
|
252
|
+
from typing import Callable
|
|
253
|
+
|
|
254
|
+
import click
|
|
255
|
+
|
|
256
|
+
from lib_cli_exit_tools import SignalSpec, default_signal_specs, run_cli
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
CUSTOM_SIG = getattr(signal, "SIGUSR1", None)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def custom_signal_specs() -> list[SignalSpec]:
|
|
263
|
+
specs = default_signal_specs()
|
|
264
|
+
if CUSTOM_SIG is not None:
|
|
265
|
+
specs.append(
|
|
266
|
+
SignalSpec(
|
|
267
|
+
signum=CUSTOM_SIG,
|
|
268
|
+
exception=RuntimeError,
|
|
269
|
+
message="Received SIGUSR1",
|
|
270
|
+
exit_code=75,
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
return specs
|
|
274
|
+
def install_custom_signals(specs: list[SignalSpec] | None) -> Callable[[], None]:
|
|
275
|
+
stack = ExitStack()
|
|
276
|
+
|
|
277
|
+
for spec in specs or []:
|
|
278
|
+
def _handler(signum: int, frame: object | None, *, spec: SignalSpec = spec) -> None:
|
|
279
|
+
raise spec.exception(f"Signal {spec.message}")
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
previous = signal.getsignal(spec.signum)
|
|
283
|
+
signal.signal(spec.signum, _handler)
|
|
284
|
+
except OSError:
|
|
285
|
+
continue
|
|
286
|
+
stack.callback(signal.signal, spec.signum, previous)
|
|
287
|
+
|
|
288
|
+
return stack.close
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@click.command()
|
|
292
|
+
def main() -> None:
|
|
293
|
+
click.echo("Send SIGUSR1 to trigger the custom handler...")
|
|
294
|
+
if hasattr(signal, "pause"):
|
|
295
|
+
signal.pause()
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
if __name__ == "__main__":
|
|
299
|
+
custom_specs = custom_signal_specs()
|
|
300
|
+
raise SystemExit(
|
|
301
|
+
run_cli(
|
|
302
|
+
main,
|
|
303
|
+
signal_specs=custom_specs,
|
|
304
|
+
signal_installer=install_custom_signals,
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
The `signal_installer` callable receives the resolved `SignalSpec` list and
|
|
310
|
+
must return a zero-argument restorer. In this case, the helper installs handlers
|
|
311
|
+
via `ExitStack`, raising the specified exception whenever the signal fires and
|
|
312
|
+
restoring the previous handlers once `run_cli` completes.
|
|
313
|
+
|
|
314
|
+
Library:
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
import lib_cli_exit_tools
|
|
318
|
+
|
|
319
|
+
lib_cli_exit_tools.config.traceback = False # show short messages
|
|
320
|
+
try:
|
|
321
|
+
raise FileNotFoundError("missing.txt")
|
|
322
|
+
except Exception as e:
|
|
323
|
+
code = lib_cli_exit_tools.get_system_exit_code(e) # 2 on POSIX
|
|
324
|
+
lib_cli_exit_tools.print_exception_message() # prints: FileNotFoundError: missing.txt
|
|
325
|
+
raise SystemExit(code)
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Command names registered on install (all invoke ``lib_cli_exit_tools.cli:main``)
|
|
329
|
+
- lib-cli-exit-tools (default console script)
|
|
330
|
+
- cli-exit-tools (alias)
|
|
331
|
+
- lib_cli_exit_tools (alias)
|
|
332
|
+
- python -m lib_cli_exit_tools (module entry)
|
|
333
|
+
|
|
334
|
+
If you installed with --user or in a venv, make sure the corresponding bin directory is on PATH:
|
|
335
|
+
- Linux/macOS venv: .venv/bin
|
|
336
|
+
- Linux/macOS user: ~/.local/bin
|
|
337
|
+
- Windows venv: .venv\Scripts
|
|
338
|
+
- Windows user: %APPDATA%\Python\PythonXY\Scripts
|
|
339
|
+
|
|
340
|
+
### Runtime configuration
|
|
341
|
+
|
|
342
|
+
All configuration lives on the module-level `lib_cli_exit_tools.config` object. Adjust it once during startup; the settings apply process-wide:
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
from lib_cli_exit_tools import config
|
|
346
|
+
|
|
347
|
+
config.traceback = True # emit full tracebacks instead of short messages
|
|
348
|
+
config.exit_code_style = "sysexits" # emit BSD-style exit codes (EX_USAGE, EX_NOINPUT, …)
|
|
349
|
+
config.broken_pipe_exit_code = 0 # treat BrokenPipeError as a benign truncation
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Field reference:
|
|
353
|
+
|
|
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. |
|
|
360
|
+
|
|
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:
|
|
362
|
+
|
|
363
|
+
```python
|
|
364
|
+
from lib_cli_exit_tools import config_overrides, config
|
|
365
|
+
|
|
366
|
+
with config_overrides(traceback=True):
|
|
367
|
+
# tracebacks enabled only within this block
|
|
368
|
+
run_something()
|
|
369
|
+
|
|
370
|
+
# state restored to previous values here
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
To return to baseline defaults, call `lib_cli_exit_tools.reset_config()`.
|
|
374
|
+
|
|
375
|
+
## Testing
|
|
376
|
+
|
|
377
|
+
Install the project with development extras before running the full test matrix:
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
pip install -e .[dev]
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Afterwards, execute the consolidated quality gate:
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
make test
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Need to run coverage manually without the helper? Use the new convenience target so
|
|
390
|
+
you never depend on a platform-specific `coverage` shim:
|
|
391
|
+
|
|
392
|
+
```bash
|
|
393
|
+
make coverage
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Which internally delegates to the scripts toolbox:
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
python -m scripts coverage
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
The target expands to `python -m coverage run -m pytest -vv` and therefore works
|
|
403
|
+
even when the `coverage` console script is not available on your PATH. The helper
|
|
404
|
+
also sets `COVERAGE_NO_SQL=1` ahead of each run so coverage falls back to the
|
|
405
|
+
file-based data format instead of SQLite, avoiding "database is locked" errors
|
|
406
|
+
on shared workstations.
|
|
407
|
+
|
|
408
|
+
Prefer the automation CLI when you need to tweak options without editing the
|
|
409
|
+
Makefile:
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
python -m scripts.test --coverage=off --verbose
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
The suite includes OS-aware cases (POSIX, Windows-specific signal handling), so
|
|
416
|
+
run it on each target platform you support to keep coverage consistent.
|
|
417
|
+
|
|
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/`).
|
|
419
|
+
|
|
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.
|
|
421
|
+
|
|
422
|
+
## Public API Reference
|
|
423
|
+
|
|
424
|
+
The package re-exports the helpers below via `lib_cli_exit_tools.__all__`. Import them directly with `from lib_cli_exit_tools import …`.
|
|
425
|
+
|
|
426
|
+
### `config`
|
|
427
|
+
Mutable dataclass-like singleton holding process-wide settings. Configure it during CLI startup.
|
|
428
|
+
|
|
429
|
+
- `traceback` (`bool`): `True` to surface full Python tracebacks; `False` keeps short, coloured summaries.
|
|
430
|
+
- `exit_code_style` (`'errno' | 'sysexits'`): Selects POSIX/Windows errno-style exit codes or BSD `sysexits` semantics.
|
|
431
|
+
- `broken_pipe_exit_code` (`int`): Overrides the exit status for `BrokenPipeError` (default `141`).
|
|
432
|
+
- `traceback_force_color` (`bool`): Forces Rich-coloured tracebacks even when stderr is not a TTY.
|
|
433
|
+
|
|
434
|
+
### `run_cli(cli, argv=None, *, prog_name=None, signal_specs=None, install_signals=True, exception_handler=None, signal_installer=None) -> int`
|
|
435
|
+
Wrap a Click command or group so every invocation shares the same signal handling and exit-code policy. Returns the numeric exit code instead of exiting the process.
|
|
436
|
+
|
|
437
|
+
Parameters:
|
|
438
|
+
- `cli`: `click.BaseCommand` to execute.
|
|
439
|
+
- `argv`: Iterable of CLI arguments (excluding the program name) or `None` to defer to Click's defaults.
|
|
440
|
+
- `prog_name`: Override the program name shown in help/version output.
|
|
441
|
+
- `signal_specs`: Iterable of `SignalSpec` objects to customise signal handling; defaults to `default_signal_specs()`.
|
|
442
|
+
- `install_signals`: Set `False` when the host application already manages signal handlers.
|
|
443
|
+
- `exception_handler`: Callable receiving the raised exception and returning an exit code; defaults to ``handle_cli_exception``.
|
|
444
|
+
- `signal_installer`: Callable mirroring ``install_signal_handlers`` for embedding scenarios.
|
|
445
|
+
|
|
446
|
+
### `cli_session(*, summary_limit=500, verbose_limit=10_000, overrides=None, restore=True)`
|
|
447
|
+
Context manager that snapshots `lib_cli_exit_tools.config`, optionally
|
|
448
|
+
applies temporary overrides, and yields a callable compatible with
|
|
449
|
+
`run_cli`.
|
|
450
|
+
|
|
451
|
+
Parameters:
|
|
452
|
+
- `summary_limit` (`int`, default `500`): Character budget when tracebacks are disabled.
|
|
453
|
+
- `verbose_limit` (`int`, default `10_000`): Character budget when tracebacks are enabled.
|
|
454
|
+
- `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.
|
|
455
|
+
- `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.
|
|
456
|
+
|
|
457
|
+
Use it to restore configuration automatically—even when the wrapped command
|
|
458
|
+
raises:
|
|
459
|
+
|
|
460
|
+
```python
|
|
461
|
+
from lib_cli_exit_tools import cli_session
|
|
462
|
+
|
|
463
|
+
with cli_session(overrides={"traceback": True}) as run:
|
|
464
|
+
exit_code = run(my_click_command, argv=["--help"])
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### `handle_cli_exception(exc, *, signal_specs=None, echo=None) -> int`
|
|
468
|
+
Translate exceptions raised by Click commands into deterministic exit codes, honouring configured signal mappings and traceback policy.
|
|
469
|
+
|
|
470
|
+
Parameters:
|
|
471
|
+
- `exc`: Exception instance to classify.
|
|
472
|
+
- `signal_specs`: Optional iterable of `SignalSpec` objects for custom signal handling.
|
|
473
|
+
- `echo`: Callable matching `click.echo` signature, allowing custom stderr routing during tests or embedding.
|
|
474
|
+
|
|
475
|
+
### `get_system_exit_code(exc) -> int`
|
|
476
|
+
Compute a platform-aware exit status for arbitrary exceptions (errno mappings on POSIX/Windows or BSD `sysexits` when enabled).
|
|
477
|
+
|
|
478
|
+
Parameters:
|
|
479
|
+
- `exc`: Exception instance to classify.
|
|
480
|
+
|
|
481
|
+
### `print_exception_message(trace_back=config.traceback, length_limit=500, stream=None) -> None`
|
|
482
|
+
Emit the active exception using Rich formatting. Produces a coloured traceback when `trace_back` is `True`, otherwise prints a truncated summary in red. Respects `config.traceback_force_color` and mirrors the behaviour of `handle_cli_exception` (tracebacks are rendered before the helper returns an exit status).
|
|
483
|
+
|
|
484
|
+
Parameters:
|
|
485
|
+
- `trace_back`: Toggle between full traceback rendering (`True`) and short summary (`False`).
|
|
486
|
+
- `length_limit`: Maximum characters for summary output.
|
|
487
|
+
- `stream`: Target text stream; defaults to `sys.stderr`.
|
|
488
|
+
|
|
489
|
+
### `i_should_fail()`
|
|
490
|
+
Deterministically raise `RuntimeError('i should fail')` to exercise error-handling paths. Useful for smoke-testing exit-code translation, CLI traceback toggles, and log formatting without inventing ad-hoc failing commands.
|
|
491
|
+
|
|
492
|
+
### `flush_streams() -> None`
|
|
493
|
+
Best-effort flush of `sys.stdout` and `sys.stderr`, ensuring buffered output is written before exit.
|
|
494
|
+
|
|
495
|
+
### `default_signal_specs() -> list[SignalSpec]`
|
|
496
|
+
Return the default signal mapping for the current platform (always includes `SIGINT`, plus `SIGTERM`/`SIGBREAK` when available).
|
|
497
|
+
|
|
498
|
+
### `install_signal_handlers(specs=None) -> Callable[[], None]`
|
|
499
|
+
Register handlers that raise structured exceptions for the provided specs and return a restoration callback. Invoke the callback (typically in a `finally` block) to restore previous handlers.
|
|
500
|
+
|
|
501
|
+
Parameters:
|
|
502
|
+
- `specs`: Iterable of `SignalSpec` objects; defaults to `default_signal_specs()`.
|
|
503
|
+
|
|
504
|
+
### `SignalSpec(signum: int, exception: type[BaseException], message: str, exit_code: int)`
|
|
505
|
+
Lightweight dataclass describing how a signal maps to an exception, stderr message, and exit code.
|
|
506
|
+
|
|
507
|
+
Fields:
|
|
508
|
+
- `signum`: Numeric signal value passed to `signal.signal`.
|
|
509
|
+
- `exception`: Exception type raised by the handler.
|
|
510
|
+
- `message`: Human-readable text echoed when the signal fires.
|
|
511
|
+
- `exit_code`: Exit status returned to the OS.
|
|
512
|
+
|
|
513
|
+
### `CliSignalError` and subclasses
|
|
514
|
+
Hierarchy of marker exceptions raised when signal handlers trigger. Use them to differentiate signal-driven exits from other failures.
|
|
515
|
+
|
|
516
|
+
- `CliSignalError`: Base class.
|
|
517
|
+
- `SigIntInterrupt`: Raised on `SIGINT` (Ctrl+C); maps to exit code `130`.
|
|
518
|
+
- `SigTermInterrupt`: Raised on `SIGTERM`; maps to exit code `143`.
|
|
519
|
+
- `SigBreakInterrupt`: Raised on Windows `SIGBREAK`; maps to exit code `149`.
|
|
520
|
+
|
|
521
|
+
### `default_signal_specs`, `install_signal_handlers`, and `run_cli` contract summary
|
|
522
|
+
When `run_cli` executes your Click command it will:
|
|
523
|
+
1. Build a signal spec list (custom or default).
|
|
524
|
+
2. Optionally install handlers that raise the exceptions above.
|
|
525
|
+
3. Execute the command with `standalone_mode=False`.
|
|
526
|
+
4. Funnel any exception through `handle_cli_exception`.
|
|
527
|
+
5. Restore prior signal handlers and flush streams before returning (or rely on any injected replacements).
|
|
528
|
+
|
|
529
|
+
This behaviour keeps CLI wiring consistent across projects embedding `lib_cli_exit_tools` while still allowing custom hooks when needed.
|
|
530
|
+
|
|
531
|
+
### Advanced CLI wiring
|
|
532
|
+
|
|
533
|
+
For larger applications, keep module execution, console scripts, and shared helpers aligned. The snippet below shows how `__main__.py` can catch unexpected errors and map them through `lib_cli_exit_tools` before exiting:
|
|
534
|
+
|
|
535
|
+
```python
|
|
536
|
+
# src/your_package/__main__.py
|
|
537
|
+
from __future__ import annotations
|
|
538
|
+
|
|
539
|
+
import lib_cli_exit_tools
|
|
540
|
+
|
|
541
|
+
from .cli import main
|
|
542
|
+
|
|
543
|
+
if __name__ == "__main__":
|
|
544
|
+
try:
|
|
545
|
+
exit_code = int(main())
|
|
546
|
+
except BaseException as exc: # fallback to shared exit helpers
|
|
547
|
+
lib_cli_exit_tools.print_exception_message()
|
|
548
|
+
exit_code = lib_cli_exit_tools.get_system_exit_code(exc)
|
|
549
|
+
raise SystemExit(exit_code)
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
A multi-command Click CLI can reuse the same configuration object and expose custom commands while still delegating wiring to `run_cli`:
|
|
553
|
+
|
|
554
|
+
```python
|
|
555
|
+
# src/your_package/cli.py
|
|
556
|
+
from __future__ import annotations
|
|
557
|
+
|
|
558
|
+
from typing import Sequence
|
|
559
|
+
|
|
560
|
+
import click
|
|
561
|
+
import lib_cli_exit_tools
|
|
562
|
+
|
|
563
|
+
from . import __init__conf__
|
|
564
|
+
from .lib_template import hello_world as _hello_world
|
|
565
|
+
from .lib_template import i_should_fail as _fail
|
|
566
|
+
|
|
567
|
+
CLICK_CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) # noqa: C408
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
@click.group(help=__init__conf__.title, context_settings=CLICK_CONTEXT_SETTINGS)
|
|
571
|
+
@click.version_option(
|
|
572
|
+
version=__init__conf__.version,
|
|
573
|
+
prog_name=__init__conf__.shell_command,
|
|
574
|
+
message=f"{__init__conf__.shell_command} version {__init__conf__.version}",
|
|
575
|
+
)
|
|
576
|
+
@click.option(
|
|
577
|
+
"--traceback/--no-traceback",
|
|
578
|
+
is_flag=True,
|
|
579
|
+
default=False,
|
|
580
|
+
help="Show full Python traceback on errors",
|
|
581
|
+
)
|
|
582
|
+
@click.pass_context
|
|
583
|
+
def cli(ctx: click.Context, traceback: bool) -> None:
|
|
584
|
+
"""Root CLI group. Stores global opts in context & shared config."""
|
|
585
|
+
ctx.ensure_object(dict)
|
|
586
|
+
ctx.obj["traceback"] = traceback
|
|
587
|
+
lib_cli_exit_tools.config.traceback = traceback
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
@cli.command("info", context_settings=CLICK_CONTEXT_SETTINGS)
|
|
591
|
+
def cli_info() -> None:
|
|
592
|
+
"""Print project information."""
|
|
593
|
+
__init__conf__.print_info()
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
@cli.command("hello", context_settings=CLICK_CONTEXT_SETTINGS)
|
|
597
|
+
def cli_hello() -> None:
|
|
598
|
+
"""Print the standard hello message."""
|
|
599
|
+
_hello_world()
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
@cli.command("fail", context_settings=CLICK_CONTEXT_SETTINGS)
|
|
603
|
+
def cli_fail() -> None:
|
|
604
|
+
"""Trigger the intentional failure helper."""
|
|
605
|
+
_fail()
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
609
|
+
"""Entrypoint returning an exit code via shared run_cli helper."""
|
|
610
|
+
return lib_cli_exit_tools.run_cli(
|
|
611
|
+
cli,
|
|
612
|
+
argv=list(argv) if argv is not None else None,
|
|
613
|
+
prog_name=__init__conf__.shell_command,
|
|
614
|
+
)
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
When installed, the generated console scripts (`lib-cli-exit-tools`, `cli-exit-tools`, `lib_cli_exit_tools`) will import `your_package.cli:main`, and `python -m your_package` will follow the same code path via `__main__.py`.
|
|
618
|
+
|
|
619
|
+
## Exit Codes
|
|
620
|
+
|
|
621
|
+
- SIGINT → 130, SIGTERM → 143 (POSIX), SIGBREAK → 149 (Windows)
|
|
622
|
+
- SystemExit(n) → n
|
|
623
|
+
- Common exceptions map to POSIX/Windows codes (FileNotFoundError, PermissionError, ValueError, etc.)
|
|
624
|
+
|
|
625
|
+
### Broken pipe behavior
|
|
626
|
+
- Default: exit 141 quietly (128+SIGPIPE), no noisy error output.
|
|
627
|
+
- Configure: `config.broken_pipe_exit_code = 0` to treat as benign truncation, or `32` (EPIPE).
|
|
628
|
+
|
|
629
|
+
### Sysexits mode (optional)
|
|
630
|
+
- Set `config.exit_code_style = "sysexits"` to map ValueError/TypeError → EX_USAGE(64),
|
|
631
|
+
FileNotFoundError → EX_NOINPUT(66), PermissionError → EX_NOPERM(77), generic OSError → EX_IOERR(74).
|
|
632
|
+
|
|
633
|
+
## Modern Python Toolchain
|
|
634
|
+
|
|
635
|
+
- Targets Python 3.10+ for modern language features and broad compatibility.
|
|
636
|
+
- Development extras track the latest stable releases published on PyPI so quality gates match local and CI environments.
|
|
637
|
+
- GitHub Actions workflows rely on the current major releases of `actions/checkout`, `actions/setup-python`, and `astral-sh/setup-uv`, aligning the automation stack with the 2025 runner images.
|
|
638
|
+
|
|
639
|
+
## Additional Documentation
|
|
640
|
+
|
|
641
|
+
- [INSTALL](INSTALL.md)
|
|
642
|
+
- [CHANGELOG](CHANGELOG.md)
|
|
643
|
+
- [CONTRIBUTING](CONTRIBUTING.md)
|
|
644
|
+
- [DEVELOPMENT](DEVELOPMENT.md)
|
|
645
|
+
- [MODULE REFERENCE](docs/system-design/module_reference.md)
|
|
646
|
+
- [AGENTS_README](AGENTS_README.md)
|
|
647
|
+
- [LICENSE](LICENSE)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
lib_cli_exit_tools/__init__.py,sha256=Z-VXcrmjZCgJZ8NW0UCVIc0HBOsjQicCbzgHj7r22eA,1867
|
|
2
|
+
lib_cli_exit_tools/__init__conf__.py,sha256=ErEaU4DpJA1XSci3o5hXVDuyPGDRznIlKof7F3eBz-A,2521
|
|
3
|
+
lib_cli_exit_tools/__main__.py,sha256=wODJaNu_NyO7tknvlpPArq0MaDwDum8zqyb73-qshzo,257
|
|
4
|
+
lib_cli_exit_tools/cli.py,sha256=k7LC08nVfse4i9RbsOGKTc9y-rrVbR1nOSKiwBvNxrE,9322
|
|
5
|
+
lib_cli_exit_tools/lib_cli_exit_tools.py,sha256=-_kjKGiWyMQ715KxOqrS6uA57SXOgJmMeowU7AmtC8A,2525
|
|
6
|
+
lib_cli_exit_tools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
lib_cli_exit_tools/adapters/__init__.py,sha256=Jj8FCGY1lAPAduYSW3wTDh8ojpr9BZms7wKEMPq8pWE,504
|
|
8
|
+
lib_cli_exit_tools/adapters/signals.py,sha256=Q2D9Htv64nsT5n66cJFDDHRWVXJokaG77I-V8gTDMKg,5772
|
|
9
|
+
lib_cli_exit_tools/application/__init__.py,sha256=Yikej3OOOaAjWOvFimg1jUYrCUGu5d3-RBiiai4tHDM,566
|
|
10
|
+
lib_cli_exit_tools/application/runner.py,sha256=AlCWX5o4VTRzsCom9EgnEK0iVCIZwyIdXtmqq5qegMM,24937
|
|
11
|
+
lib_cli_exit_tools/core/__init__.py,sha256=aR7OIBWOQ1PsA33blOTgLrq5ffKDCuaMVBS5mbrDAZ8,531
|
|
12
|
+
lib_cli_exit_tools/core/configuration.py,sha256=Ro6R2cmOIFhdOUogjK1VOxEJInx24Wgme0t-ucYJGEg,5658
|
|
13
|
+
lib_cli_exit_tools/core/exit_codes.py,sha256=nqx-e6h-ji-FiECPnDV8b0yrjcLPJyU-3xf4zhfRCBE,12322
|
|
14
|
+
lib_cli_exit_tools-2.2.1.dist-info/METADATA,sha256=qwjNOxOhhpj6yLzC4Cq5s7wb7AyoOvDrObe2mXPRGFQ,25971
|
|
15
|
+
lib_cli_exit_tools-2.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
16
|
+
lib_cli_exit_tools-2.2.1.dist-info/entry_points.txt,sha256=8R-_3UT0KcQ7HQ7idDx6rlG77GhHb0E2N5QnN41dj-M,161
|
|
17
|
+
lib_cli_exit_tools-2.2.1.dist-info/licenses/LICENSE,sha256=6s9SL2InRrwynPjJYiER7ONHBshbGj_yC5EBzfo7l9I,1072
|
|
18
|
+
lib_cli_exit_tools-2.2.1.dist-info/RECORD,,
|