lib-cli-exit-tools 1.3.1__tar.gz → 1.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.
Files changed (65) hide show
  1. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/AGENTS.md +4 -24
  2. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/CHANGELOG.md +6 -0
  3. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/PKG-INFO +6 -4
  4. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/README.md +5 -3
  5. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/docs/system-design/module_reference.md +50 -59
  6. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/packaging/brew/Formula/lib-cli-exit-tools.rb +1 -1
  7. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/packaging/conda/recipe/meta.yaml +1 -1
  8. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/packaging/nix/flake.nix +1 -1
  9. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/pyproject.toml +1 -1
  10. lib_cli_exit_tools-1.4.0/src/lib_cli_exit_tools/adapters/__init__.py +16 -0
  11. lib_cli_exit_tools-1.4.0/src/lib_cli_exit_tools/adapters/signals.py +177 -0
  12. lib_cli_exit_tools-1.4.0/src/lib_cli_exit_tools/application/__init__.py +16 -0
  13. lib_cli_exit_tools-1.4.0/src/lib_cli_exit_tools/application/runner.py +323 -0
  14. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/src/lib_cli_exit_tools/cli.py +1 -3
  15. lib_cli_exit_tools-1.4.0/src/lib_cli_exit_tools/core/__init__.py +16 -0
  16. lib_cli_exit_tools-1.4.0/src/lib_cli_exit_tools/core/configuration.py +58 -0
  17. lib_cli_exit_tools-1.4.0/src/lib_cli_exit_tools/core/exit_codes.py +149 -0
  18. lib_cli_exit_tools-1.4.0/src/lib_cli_exit_tools/lib_cli_exit_tools.py +53 -0
  19. lib_cli_exit_tools-1.4.0/tests/adapters/test_signals.py +75 -0
  20. lib_cli_exit_tools-1.4.0/tests/application/test_runner.py +233 -0
  21. lib_cli_exit_tools-1.4.0/tests/cli/test_cli_entrypoints.py +103 -0
  22. lib_cli_exit_tools-1.4.0/tests/conftest.py +36 -0
  23. lib_cli_exit_tools-1.4.0/tests/core/test_exit_codes.py +151 -0
  24. lib_cli_exit_tools-1.3.1/src/lib_cli_exit_tools/lib_cli_exit_tools.py +0 -749
  25. lib_cli_exit_tools-1.3.1/tests/test_cli.py +0 -85
  26. lib_cli_exit_tools-1.3.1/tests/test_cli_extra.py +0 -32
  27. lib_cli_exit_tools-1.3.1/tests/test_exit_tools.py +0 -167
  28. lib_cli_exit_tools-1.3.1/tests/test_extend_cli.py +0 -111
  29. lib_cli_exit_tools-1.3.1/tests/test_lib_extra.py +0 -110
  30. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/.env.example +0 -0
  31. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/.github/workflows/ci.yml +0 -0
  32. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/.github/workflows/codeql.yml +0 -0
  33. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/.github/workflows/release.yml +0 -0
  34. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/.gitignore +0 -0
  35. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/CONTRIBUTING.md +0 -0
  36. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/DEVELOPMENT.md +0 -0
  37. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/INSTALL.md +0 -0
  38. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/LICENSE +0 -0
  39. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/Makefile +0 -0
  40. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/codecov.yml +0 -0
  41. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/notebooks/Quickstart.ipynb +0 -0
  42. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/packaging/README.md +0 -0
  43. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/qlty.toml +0 -0
  44. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/scripts/__init__.py +0 -0
  45. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/scripts/_utils.py +0 -0
  46. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/scripts/build.py +0 -0
  47. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/scripts/bump.py +0 -0
  48. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/scripts/bump_major.py +0 -0
  49. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/scripts/bump_minor.py +0 -0
  50. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/scripts/bump_patch.py +0 -0
  51. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/scripts/bump_version.py +0 -0
  52. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/scripts/clean.py +0 -0
  53. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/scripts/dev.py +0 -0
  54. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/scripts/generate_nix_flake.py +0 -0
  55. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/scripts/install.py +0 -0
  56. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/scripts/menu.py +0 -0
  57. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/scripts/push.py +0 -0
  58. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/scripts/release.py +0 -0
  59. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/scripts/run_cli.py +0 -0
  60. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/scripts/test.py +0 -0
  61. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/scripts/version_current.py +0 -0
  62. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/src/lib_cli_exit_tools/__init__.py +0 -0
  63. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/src/lib_cli_exit_tools/__init__conf__.py +0 -0
  64. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/src/lib_cli_exit_tools/__main__.py +0 -0
  65. {lib_cli_exit_tools-1.3.1 → lib_cli_exit_tools-1.4.0}/src/lib_cli_exit_tools/py.typed +0 -0
@@ -13,6 +13,8 @@ Review the system prompt files in `/media/srv-main/softdev/*/projects/softwarest
13
13
  - python_structure_template.md
14
14
  - self_documenting.md
15
15
  - self_documenting_template.md
16
+ - python_jupyter_notebooks.md
17
+ - python_testing.md
16
18
 
17
19
  always apply those Rules :
18
20
 
@@ -41,18 +43,6 @@ when writing or refracturing Python scripts, apply those Rules :
41
43
  - `packaging/`: Conda, Homebrew, and Nix specs kept in sync via scripts.
42
44
  - `tests/`: placeholder suite skipping until logging features exist.
43
45
 
44
-
45
- ## Build, Test, and Development Commands
46
-
47
- - `make help` — list all targets with one‑line docs.
48
- - `make test` — run ruff (lint + format check), pyright, pytest with coverage (enabled by default), and upload to Codecov (if configured via `.env`).
49
- - Auto‑bootstrap: `make test` installs dev tools (`pip install -e .[dev]`) if linters/test deps are missing. Use `SKIP_BOOTSTRAP=1 make test` to disable.
50
- - Coverage control: `COVERAGE=on|auto|off` (default `on` locally). Uses a unique `COVERAGE_FILE` each run to avoid DB locks.
51
- - Before uploading to Codecov the harness creates an allow-empty commit (`test: auto commit before Codecov upload`) so the report attaches to a revision. Reset or amend if you do not want to keep it.
52
- - `make build` — build Python wheel/sdist and attempt Conda/Homebrew/Nix builds (auto‑installs missing tools when needed).
53
- - `make push` — runs the full `scripts/test.py` flow, prompts for/accepts a commit message (or `COMMIT_MESSAGE`), creates an allow-empty commit if needed, then pushes to the selected remote.
54
- - `make clean` — remove caches, coverage, and build artifacts (includes `dist/` and `build/`).
55
-
56
46
  ### Versioning & Releases
57
47
 
58
48
  - Single source of truth for the package version is `pyproject.toml` (`[project].version`).
@@ -84,13 +74,6 @@ when writing or refracturing Python scripts, apply those Rules :
84
74
 
85
75
  ## Coding Style & Naming Conventions
86
76
 
87
- ## Testing Guidelines
88
-
89
- - Unit and integration-style tests live under `tests/`; keep them up to date when adding features.
90
- - Extend coverage for new CLI or library behaviour (the suite exercises CLI commands, package metadata, and automation scripts).
91
- - When adding functionality, replace or remove placeholders and ensure `make test` remains green.
92
- - write good doctests which clearly show how the function works and what are the intensions. Use it as an enhancement of the inline documentation
93
-
94
77
  ## Commit & Pull Request Guidelines
95
78
 
96
79
  ## Architecture Overview
@@ -119,13 +102,10 @@ when writing or refracturing Python scripts, apply those Rules :
119
102
  - run "make test" before any push to avoid lint/test breakage.
120
103
  - after push, monitor errors in the github actions and try to correct the errors
121
104
 
122
- ## Jupyter Notebooks
123
- use at least nbformat >=4.5
124
- - "nbformat": 4,
125
- - "nbformat_minor": 5
126
-
127
105
  ---
128
106
 
107
+ # ONLY ON USER REQEST !!!! :
108
+
129
109
  # 📘 Documentation Review & Enhancement Prompt
130
110
 
131
111
  You are a **senior software developer specializing in code documentation and system design clarity**. Your task is to review, correct, and enhance all documentation (external and inline) across the codebase with the following principles and workflow.
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.4.0] - 2025-09-26
4
+
5
+ - Reafracture, TestSuite
6
+
3
7
  ## [1.3.1] - 2025-09-26
4
8
 
5
9
  - patch colored traceback
@@ -51,6 +55,8 @@
51
55
  - Metadata lookup in `__init__conf__` resolved once and reused.
52
56
  - Coverage gate relaxed to 80% and removed `# pragma: no cover` comments.
53
57
  - Quickstart notebook defaults to install from GitHub; added cell IDs; last cell prints exit code example instead of exiting.
58
+ - Repartitioned the exit tooling into `core`, `adapters`, and `application` packages with `lib_cli_exit_tools` now acting as a facade to preserve import paths.
59
+ - `run_cli` accepts injectable `exception_handler` and `signal_installer` hooks; CLI rich-click configuration now runs during `main()` execution instead of at import time.
54
60
 
55
61
  ### Fixed
56
62
  - Pyright 3.10 compatibility in `__init__conf__` by typing to a minimal metadata protocol; resolved unknown `.get` errors.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lib_cli_exit_tools
3
- Version: 1.3.1
3
+ Version: 1.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
@@ -155,7 +155,7 @@ Mutable dataclass-like singleton holding process-wide settings. Configure it dur
155
155
  - `broken_pipe_exit_code` (`int`): Overrides the exit status for `BrokenPipeError` (default `141`).
156
156
  - `traceback_force_color` (`bool`): Forces Rich-coloured tracebacks even when stderr is not a TTY.
157
157
 
158
- ### `run_cli(cli, argv=None, *, prog_name=None, signal_specs=None, install_signals=True) -> int`
158
+ ### `run_cli(cli, argv=None, *, prog_name=None, signal_specs=None, install_signals=True, exception_handler=None, signal_installer=None) -> int`
159
159
  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.
160
160
 
161
161
  Parameters:
@@ -164,6 +164,8 @@ Parameters:
164
164
  - `prog_name`: Override the program name shown in help/version output.
165
165
  - `signal_specs`: Iterable of `SignalSpec` objects to customise signal handling; defaults to `default_signal_specs()`.
166
166
  - `install_signals`: Set `False` when the host application already manages signal handlers.
167
+ - `exception_handler`: Optional callable receiving the raised exception and returning an exit code; defaults to ``handle_cli_exception``.
168
+ - `signal_installer`: Optional callable mirroring ``install_signal_handlers`` for embedding scenarios.
167
169
 
168
170
  ### `handle_cli_exception(exc, *, signal_specs=None, echo=None) -> int`
169
171
  Translate exceptions raised by Click commands into deterministic exit codes, honouring configured signal mappings and traceback policy.
@@ -222,9 +224,9 @@ When `run_cli` executes your Click command it will:
222
224
  2. Optionally install handlers that raise the exceptions above.
223
225
  3. Execute the command with `standalone_mode=False`.
224
226
  4. Funnel any exception through `handle_cli_exception`.
225
- 5. Restore prior signal handlers and flush streams before returning.
227
+ 5. Restore prior signal handlers and flush streams before returning (or rely on any injected replacements).
226
228
 
227
- This behaviour keeps CLI wiring consistent across projects embedding `lib_cli_exit_tools`.
229
+ This behaviour keeps CLI wiring consistent across projects embedding `lib_cli_exit_tools` while still allowing custom hooks when needed.
228
230
 
229
231
  ### Advanced CLI wiring
230
232
 
@@ -124,7 +124,7 @@ Mutable dataclass-like singleton holding process-wide settings. Configure it dur
124
124
  - `broken_pipe_exit_code` (`int`): Overrides the exit status for `BrokenPipeError` (default `141`).
125
125
  - `traceback_force_color` (`bool`): Forces Rich-coloured tracebacks even when stderr is not a TTY.
126
126
 
127
- ### `run_cli(cli, argv=None, *, prog_name=None, signal_specs=None, install_signals=True) -> int`
127
+ ### `run_cli(cli, argv=None, *, prog_name=None, signal_specs=None, install_signals=True, exception_handler=None, signal_installer=None) -> int`
128
128
  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.
129
129
 
130
130
  Parameters:
@@ -133,6 +133,8 @@ Parameters:
133
133
  - `prog_name`: Override the program name shown in help/version output.
134
134
  - `signal_specs`: Iterable of `SignalSpec` objects to customise signal handling; defaults to `default_signal_specs()`.
135
135
  - `install_signals`: Set `False` when the host application already manages signal handlers.
136
+ - `exception_handler`: Optional callable receiving the raised exception and returning an exit code; defaults to ``handle_cli_exception``.
137
+ - `signal_installer`: Optional callable mirroring ``install_signal_handlers`` for embedding scenarios.
136
138
 
137
139
  ### `handle_cli_exception(exc, *, signal_specs=None, echo=None) -> int`
138
140
  Translate exceptions raised by Click commands into deterministic exit codes, honouring configured signal mappings and traceback policy.
@@ -191,9 +193,9 @@ When `run_cli` executes your Click command it will:
191
193
  2. Optionally install handlers that raise the exceptions above.
192
194
  3. Execute the command with `standalone_mode=False`.
193
195
  4. Funnel any exception through `handle_cli_exception`.
194
- 5. Restore prior signal handlers and flush streams before returning.
196
+ 5. Restore prior signal handlers and flush streams before returning (or rely on any injected replacements).
195
197
 
196
- This behaviour keeps CLI wiring consistent across projects embedding `lib_cli_exit_tools`.
198
+ This behaviour keeps CLI wiring consistent across projects embedding `lib_cli_exit_tools` while still allowing custom hooks when needed.
197
199
 
198
200
  ### Advanced CLI wiring
199
201
 
@@ -10,6 +10,10 @@ Complete
10
10
  **Related Files:**
11
11
 
12
12
  * `src/lib_cli_exit_tools/lib_cli_exit_tools.py`
13
+ * `src/lib_cli_exit_tools/core/configuration.py`
14
+ * `src/lib_cli_exit_tools/core/exit_codes.py`
15
+ * `src/lib_cli_exit_tools/application/runner.py`
16
+ * `src/lib_cli_exit_tools/adapters/signals.py`
13
17
  * `src/lib_cli_exit_tools/cli.py`
14
18
  * `src/lib_cli_exit_tools/__init__conf__.py`
15
19
  * `src/lib_cli_exit_tools/__init__.py`
@@ -42,8 +46,8 @@ Automations and downstream CLIs rely on `lib_cli_exit_tools` for deterministic e
42
46
  1. Console script (or `python -m lib_cli_exit_tools`) resolves to `lib_cli_exit_tools.cli.main`.
43
47
  2. The Click group stores global flags and mutates `lib_cli_exit_tools.config`.
44
48
  3. `run_cli` optionally installs signal handlers, executes the requested Click command, and funnels exceptions into `handle_cli_exception`.
45
- 4. `handle_cli_exception` maps signals and errors to exit codes using `default_signal_specs`, `_sysexits_mapping`, and `get_system_exit_code`.
46
- 5. `print_exception_message` and `_print_output` render diagnostics when tracebacks are suppressed.
49
+ 4. `handle_cli_exception` maps signals and errors to exit codes using `default_signal_specs` and `get_system_exit_code`.
50
+ 5. `print_exception_message` and `_print_output` (within `application.runner`) render diagnostics when tracebacks are suppressed.
47
51
  6. Packaging metadata from `__init__conf__` feeds CLI help/version output so docs and runtime stay synchronized.
48
52
 
49
53
  **System Dependencies:** Standard library (`signal`, `subprocess`, `sys`, `importlib.metadata`) plus `click` and `rich-click` for CLI/TUI behavior.
@@ -53,80 +57,63 @@ Automations and downstream CLIs rely on `lib_cli_exit_tools` for deterministic e
53
57
  ## Core Components
54
58
 
55
59
  ### Module: lib_cli_exit_tools/lib_cli_exit_tools.py
56
- * **Purpose:** Centralize configuration, signal registration, and exception-to-exit-code translation so all CLIs share identical behavior.
60
+ * **Purpose:** Provide a façade that re-exports configuration, signal helpers, and orchestration utilities so existing imports stay valid after the refactor.
57
61
  * **Location:** `src/lib_cli_exit_tools/lib_cli_exit_tools.py`
62
+ * **Notes:** Contains no implementation logic; it simply aggregates symbols from the layered modules to minimise breaking changes.
63
+
64
+ ### Module: lib_cli_exit_tools/core/configuration.py
65
+ * **Purpose:** Own the mutable runtime configuration shared across CLI executions.
66
+ * **Location:** `src/lib_cli_exit_tools/core/configuration.py`
58
67
 
59
68
  #### Dataclass: `_Config`
60
- * **Purpose:** Mutable singleton holding traceback, exit-code style, and broken-pipe overrides.
61
- * **Fields:**
62
- * `traceback` (bool) — emit full tracebacks when `True`.
63
- * `exit_code_style` (`"errno" | "sysexits"`) — choose mapping strategy.
64
- * `broken_pipe_exit_code` (int) — exit status for `BrokenPipeError`.
65
- * `traceback_force_color` (bool) — opt-in flag to force ANSI-coloured Rich tracebacks on non-TTY streams.
66
- * **Notes:** Mutations apply process-wide; callers should restore defaults in tests.
69
+ * **Role:** Captures traceback, exit-code style, broken-pipe handling, and Rich colouring toggles.
70
+ * **Notes:** Mutations apply process-wide; tests must restore defaults to avoid leakage.
67
71
 
68
72
  #### Constant: `config`
69
- * **Purpose:** Shared `_Config` instance consulted by all helpers.
70
- * **Interactions:** Mutated by CLI options and tests; read by `handle_cli_exception`, `get_system_exit_code`, and `print_exception_message`.
71
-
72
- #### Exception Hierarchy: `CliSignalError`, `SigIntInterrupt`, `SigTermInterrupt`, `SigBreakInterrupt`
73
- * **Purpose:** Represent signal-triggered interruptions so exit handlers can differentiate them from generic errors.
74
- * **Notes:** Raised by generated signal handlers; mapped to exit codes 130 (SIGINT), 143 (SIGTERM), 149 (SIGBREAK).
73
+ * **Role:** Singleton instance consumed by adapters and orchestration code.
74
+ * **Interactions:** Mutated by the CLI (`--traceback`) and inspected by error handlers and exit-code mappers.
75
75
 
76
- #### Dataclass: `SignalSpec`
77
- * **Purpose:** Describe how to translate a signal into an exception, stderr message, and exit code.
78
- * **Key Fields:** `signum`, `exception`, `message`, `exit_code`.
79
- * **Usage:** Consumed by `install_signal_handlers`, `handle_cli_exception`.
76
+ ### Module: lib_cli_exit_tools/core/exit_codes.py
77
+ * **Purpose:** Convert exceptions into deterministic OS exit statuses.
78
+ * **Location:** `src/lib_cli_exit_tools/core/exit_codes.py`
80
79
 
81
- #### Protocol: `_Echo`
82
- * **Purpose:** Structural type for echo-like callables (defaulting to `click.echo`).
83
- * **Usage:** Enables dependency injection/testing of output behavior.
80
+ #### Function: `get_system_exit_code(exc: BaseException) -> int`
81
+ * **Role:** Mirrors platform-specific errno/winerror semantics or BSD sysexits based on `config.exit_code_style`.
82
+ * **Notes:** Handles `CalledProcessError`, `SystemExit`, and `BrokenPipeError` explicitly before falling back to errno tables.
84
83
 
85
- #### Function: `_default_echo(message: str, *, err: bool = True) -> None`
86
- * **Purpose:** Bridge to `click.echo` when no custom echo is provided.
87
- * **Side Effects:** Writes text to stdout/stderr via Click.
84
+ ### Module: lib_cli_exit_tools/adapters/signals.py
85
+ * **Purpose:** Translate operating-system signals into structured exceptions and reversible handlers.
86
+ * **Location:** `src/lib_cli_exit_tools/adapters/signals.py`
88
87
 
89
- #### Function: `default_signal_specs() -> list[SignalSpec]`
90
- * **Purpose:** Produce platform-aware default signal specs.
91
- * **Behavior:** Always includes SIGINT; adds SIGTERM/SIGBREAK when available.
88
+ #### Exceptions: `CliSignalError`, `SigIntInterrupt`, `SigTermInterrupt`, `SigBreakInterrupt`
89
+ * **Role:** Represent distinct signal pathways so exit handling can map them to stable codes (130/143/149).
92
90
 
93
- #### Function: `_make_raise_handler(exc_type) -> Callable`
94
- * **Purpose:** Wrap an exception type into a signal-compatible `(signum, frame)` handler that raises the type immediately.
95
- * **Usage:** Internal helper for `install_signal_handlers`.
96
-
97
- #### Function: `install_signal_handlers(specs | None) -> Callable[[], None]`
98
- * **Purpose:** Register handlers for provided signal specs and return a restoration callback.
99
- * **Considerations:** Handlers are process-wide; callers must invoke the returned function to restore prior state.
100
-
101
- #### Function: `handle_cli_exception(exc, *, signal_specs=None, echo=None) -> int`
102
- * **Purpose:** Convert exceptions into deterministic exit codes, honoring configured signal mappings and traceback settings.
103
- * **Behavior:** Detects signal exceptions, `BrokenPipeError`, `click.ClickException`, and `SystemExit`; otherwise defers to `print_exception_message` and `get_system_exit_code`.
104
-
105
- #### Function: `run_cli(cli, argv=None, *, prog_name=None, signal_specs=None, install_signals=True) -> int`
106
- * **Purpose:** Execute a Click command under shared signal/error handling, flushing streams before returning.
107
- * **Behavior:** Optionally installs handlers, invokes `cli.main(...)`, funnels exceptions to `handle_cli_exception`, and restores handlers.
91
+ #### Dataclass: `SignalSpec`
92
+ * **Role:** Describe signal metadata (signum, exception, message, exit code) used by installers and handlers.
108
93
 
109
- #### Function: `get_system_exit_code(exc) -> int`
110
- * **Purpose:** Map exceptions to platform-appropriate exit codes using errno, Windows `winerror`, or sysexits semantics.
111
- * **Behavior:** Supports subprocess return codes, `SystemExit`, `KeyboardInterrupt`, `BrokenPipeError`, and default fallbacks.
94
+ #### Function: `default_signal_specs(extra: Iterable[SignalSpec] | None = None) -> list[SignalSpec]`
95
+ * **Role:** Provide platform-aware defaults (always SIGINT; conditional SIGTERM/SIGBREAK) with optional extension points.
112
96
 
113
- #### Function: `_sysexits_mapping(exc) -> int`
114
- * **Purpose:** Provide BSD `sysexits` codes when `config.exit_code_style == "sysexits"`.
97
+ #### Function: `install_signal_handlers(specs: Sequence[SignalSpec] | None = None) -> Callable[[], None]`
98
+ * **Role:** Register handlers that raise the specified exceptions and return a restoration callback for clean-up.
115
99
 
116
- #### Function: `_build_console(stream=None) -> rich.console.Console`
117
- * **Purpose:** Construct a Rich console aligned with current rich-click styling for consistent error/help rendering.
118
- * **Usage:** Employed by `print_exception_message`.
100
+ ### Module: lib_cli_exit_tools/application/runner.py
101
+ * **Purpose:** Orchestrate Click command execution with shared signal wiring, exception translation, and Rich-based diagnostics.
102
+ * **Location:** `src/lib_cli_exit_tools/application/runner.py`
119
103
 
120
- #### Function: `print_exception_message(trace_back=config.traceback, length_limit=500, stream=None) -> None`
121
- * **Purpose:** Emit the active exception message (and optional Rich traceback) with truncation safeguards.
122
- * **Behavior:** Flushes streams, surfaces captured subprocess output, renders highlighted tracebacks when enabled.
104
+ #### Protocol: `_Echo`
105
+ * **Role:** Structural type enabling dependency injection for stderr writers in tests.
123
106
 
124
- #### Function: `_print_output(exc_info, attr, stream=None) -> None`
125
- * **Purpose:** Print `stdout`/`stderr` captured on exceptions such as `subprocess.CalledProcessError`.
107
+ #### Utilities: `flush_streams`, `_build_console`, `_print_output`, `print_exception_message`
108
+ * **Role:** Manage stream flushing and Rich rendering when tracebacks are suppressed or forced.
126
109
 
127
- #### Function: `flush_streams() -> None`
128
- * **Purpose:** Best-effort flush of `sys.stdout` / `sys.stderr` to avoid buffered output loss during termination.
110
+ #### Function: `handle_cli_exception(exc, *, signal_specs=None, echo=None) -> int`
111
+ * **Role:** Map exceptions to exit codes, emit signal messages, honour `config.traceback`, and delegate to `get_system_exit_code` when needed.
112
+ * **Notes:** Raises the original exception when traceback mode is enabled; otherwise trims output and prints captured subprocess streams.
129
113
 
114
+ #### Function: `run_cli(cli, argv=None, *, prog_name=None, signal_specs=None, install_signals=True, exception_handler=None, signal_installer=None) -> int`
115
+ * **Role:** Install signal handlers (unless disabled), execute the passed Click command, and funnel exceptions through an injectable handler before flushing streams.
116
+ * **Notes:** New `exception_handler` and `signal_installer` hooks support advanced embedding and unit testing scenarios.
130
117
  ### Module: lib_cli_exit_tools/cli.py
131
118
  * **Purpose:** Provide the Click command group and subcommands that surface the exit tooling.
132
119
  * **Location:** `src/lib_cli_exit_tools/cli.py`
@@ -187,6 +174,10 @@ Automations and downstream CLIs rely on `lib_cli_exit_tools` for deterministic e
187
174
  * `install_signal_handlers` returns a restoration callback; callers must execute it (preferably in `finally`) to restore original handlers.
188
175
  * SIGTERM and SIGBREAK specs are conditionally added based on platform capabilities.
189
176
 
177
+ **Extensibility:**
178
+
179
+ * `run_cli` exposes `exception_handler` and `signal_installer` hooks so embedders and unit tests can replace the default wiring without patching globals.
180
+
190
181
  ---
191
182
 
192
183
  ## Testing Approach
@@ -3,7 +3,7 @@ class LibCliExitTools < Formula
3
3
 
4
4
  desc "CLI exit handling helpers: clean signals, exit codes, and error printing"
5
5
  homepage "https://github.com/bitranox/lib_cli_exit_tools"
6
- url "https://github.com/bitranox/lib_cli_exit_tools/archive/refs/tags/v1.3.1.tar.gz"
6
+ url "https://github.com/bitranox/lib_cli_exit_tools/archive/refs/tags/v1.4.0.tar.gz"
7
7
  k3cc316e7d0b09dd79e617cb3e91b14ee414b7e6695b108a326d9d3ccd08dd9"
8
8
  license "MIT"
9
9
 
@@ -1,5 +1,5 @@
1
1
  {% set name = "lib_cli_exit_tools" %}
2
- {% set version = "1.3.1" %}
2
+ {% set version = "1.4.0" %}
3
3
 
4
4
  package:
5
5
  name: {{ name|lower }}
@@ -44,7 +44,7 @@
44
44
  {
45
45
  packages.default = pypkgs.buildPythonPackage {
46
46
  pname = "lib_cli_exit_tools";
47
- version = "1.3.1";
47
+ version = "1.4.0";
48
48
  pyproject = true;
49
49
  src = ../..;
50
50
  nativeBuildInputs = [ hatchlingVendor ];
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lib_cli_exit_tools"
3
- version = "1.3.1"
3
+ version = "1.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"
@@ -0,0 +1,16 @@
1
+ """Adapter layer for lib_cli_exit_tools.
2
+
3
+ Purpose:
4
+ Provide integration points with concrete frameworks (Click, Rich, signal
5
+ handlers) while delegating policy decisions to the core module.
6
+ Contents:
7
+ Modules added here will contain Click helpers, signal installers, and other
8
+ I/O-centric code paths.
9
+ System Integration:
10
+ Imported by the application layer to wire concrete behaviour without
11
+ violating dependency rules.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ __all__: list[str] = []
@@ -0,0 +1,177 @@
1
+ """Signal handling adapters for lib_cli_exit_tools.
2
+
3
+ Purpose:
4
+ Translate operating-system signals into structured Python exceptions and
5
+ provide installation helpers that keep process-wide handlers reversible.
6
+ Contents:
7
+ * :class:`CliSignalError` hierarchy capturing supported interrupts.
8
+ * :class:`SignalSpec` dataclass describing signal→exception mappings.
9
+ * :func:`default_signal_specs` building platform-aware defaults.
10
+ * :func:`install_signal_handlers` installing reversible handlers.
11
+ System Integration:
12
+ The application runner leverages these helpers to provide consistent exit
13
+ codes across console entry points while allowing tests to inject fakes.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import signal
19
+ from dataclasses import dataclass
20
+ from types import FrameType
21
+ from typing import Callable, Iterable, List, Sequence
22
+
23
+ __all__ = [
24
+ "CliSignalError",
25
+ "SigIntInterrupt",
26
+ "SigTermInterrupt",
27
+ "SigBreakInterrupt",
28
+ "SignalSpec",
29
+ "default_signal_specs",
30
+ "install_signal_handlers",
31
+ ]
32
+
33
+
34
+ class CliSignalError(RuntimeError):
35
+ """Base class for translating OS signals into structured CLI errors.
36
+
37
+ Why:
38
+ Provide a dedicated hierarchy so exit handlers can recognise signal-driven
39
+ interruptions and map them to deterministic exit codes.
40
+ Usage:
41
+ Raised automatically by handlers created via :func:`install_signal_handlers`.
42
+ """
43
+
44
+
45
+ class SigIntInterrupt(CliSignalError):
46
+ """Raised when the process receives ``SIGINT`` (Ctrl+C)."""
47
+
48
+
49
+ class SigTermInterrupt(CliSignalError):
50
+ """Raised when the process receives ``SIGTERM`` (termination request)."""
51
+
52
+
53
+ class SigBreakInterrupt(CliSignalError):
54
+ """Raised when the process receives ``SIGBREAK`` on Windows consoles."""
55
+
56
+
57
+ @dataclass(slots=True)
58
+ class SignalSpec:
59
+ """Describe how to translate a low-level signal into CLI-facing behaviour.
60
+
61
+ Fields:
62
+ signum: Numeric identifier registered with :mod:`signal`.
63
+ exception: Exception type raised by the generated handler.
64
+ message: User-facing text echoed to stderr when the signal fires.
65
+ exit_code: Numeric code returned to the operating system.
66
+ """
67
+
68
+ signum: int
69
+ exception: type[BaseException]
70
+ message: str
71
+ exit_code: int
72
+
73
+
74
+ _Handler = Callable[[int, FrameType | None], None]
75
+
76
+
77
+ def default_signal_specs(extra: Iterable[SignalSpec] | None = None) -> List[SignalSpec]:
78
+ """Build the default list of signal specifications for the host platform.
79
+
80
+ Why:
81
+ Ensure every CLI managed by this project responds consistently to Ctrl+C
82
+ and termination signals without duplicating configuration.
83
+ Parameters:
84
+ extra: Optional iterable of additional :class:`SignalSpec` records that
85
+ extend the standard platform-aware set.
86
+ Returns:
87
+ Fresh list of :class:`SignalSpec` entries ready for
88
+ :func:`install_signal_handlers` or :func:`handle_cli_exception`.
89
+ """
90
+
91
+ specs: List[SignalSpec] = [
92
+ SignalSpec(
93
+ signum=signal.SIGINT,
94
+ exception=SigIntInterrupt,
95
+ message="Aborted (SIGINT).",
96
+ exit_code=130,
97
+ )
98
+ ]
99
+
100
+ if hasattr(signal, "SIGTERM"):
101
+ specs.append(
102
+ SignalSpec(
103
+ signum=getattr(signal, "SIGTERM"),
104
+ exception=SigTermInterrupt,
105
+ message="Terminated (SIGTERM/SIGBREAK).",
106
+ exit_code=143,
107
+ )
108
+ )
109
+ if hasattr(signal, "SIGBREAK"):
110
+ specs.append(
111
+ SignalSpec(
112
+ signum=getattr(signal, "SIGBREAK"),
113
+ exception=SigBreakInterrupt,
114
+ message="Terminated (SIGBREAK).",
115
+ exit_code=149,
116
+ )
117
+ )
118
+
119
+ if extra is not None:
120
+ specs.extend(extra)
121
+
122
+ return specs
123
+
124
+
125
+ def _make_raise_handler(exc_type: type[BaseException]) -> _Handler:
126
+ """Wrap ``exc_type`` in a signal-compatible callable.
127
+
128
+ Why:
129
+ ``signal.signal`` expects handlers with a ``(signum, frame)`` signature.
130
+ The returned closure ignores those arguments and raises ``exc_type``
131
+ immediately so higher layers see a structured exception.
132
+ Parameters:
133
+ exc_type: Exception subclass to raise when the signal fires.
134
+ Returns:
135
+ Callable compatible with ``signal.signal`` registration.
136
+ """
137
+
138
+ def _handler(signo: int, frame: FrameType | None) -> None: # pragma: no cover - just raises
139
+ raise exc_type()
140
+
141
+ return _handler
142
+
143
+
144
+ def install_signal_handlers(specs: Sequence[SignalSpec] | None = None) -> Callable[[], None]:
145
+ """Install signal handlers that re-raise as structured exceptions.
146
+
147
+ Why:
148
+ Centralise signal wiring so CLI entry points can opt in with a single
149
+ call and reliably restore previous handlers afterwards.
150
+ Parameters:
151
+ specs: Iterable of :class:`SignalSpec` entries. When omitted the
152
+ platform-aware defaults from :func:`default_signal_specs` are used.
153
+ Returns:
154
+ Callable that restores the prior handlers. Invoke it in ``finally`` to
155
+ avoid leaking process-wide state into callers or tests.
156
+ """
157
+
158
+ active_specs = list(default_signal_specs() if specs is None else specs)
159
+ previous: List[tuple[int, object]] = []
160
+
161
+ for spec in active_specs:
162
+ handler = _make_raise_handler(spec.exception)
163
+ try:
164
+ current = signal.getsignal(spec.signum)
165
+ signal.signal(spec.signum, handler)
166
+ previous.append((spec.signum, current))
167
+ except (AttributeError, OSError, RuntimeError): # pragma: no cover - platform differences
168
+ continue
169
+
170
+ def restore() -> None:
171
+ for signum, prior in previous:
172
+ try:
173
+ signal.signal(signum, prior) # type: ignore[arg-type]
174
+ except Exception: # pragma: no cover - restore best-effort
175
+ pass
176
+
177
+ return restore
@@ -0,0 +1,16 @@
1
+ """Application orchestration for lib_cli_exit_tools.
2
+
3
+ Purpose:
4
+ Coordinate core exit-code logic with adapter implementations (Click,
5
+ signal handlers) while exposing a small surface for CLI entry points.
6
+ Contents:
7
+ The `runner` module will host the refactored `run_cli` orchestration and any
8
+ supporting protocols.
9
+ System Integration:
10
+ Imports only from `lib_cli_exit_tools.core` and adapter packages, keeping the
11
+ dependency direction consistent with Clean Architecture guidelines.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ __all__: list[str] = []