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,47 @@
|
|
|
1
|
+
"""Public re-export surface for lib_cli_exit_tools helpers.
|
|
2
|
+
|
|
3
|
+
Purpose:
|
|
4
|
+
Provide a stable import path for consumers (`from lib_cli_exit_tools import run_cli`).
|
|
5
|
+
Contents:
|
|
6
|
+
Re-exports signal helpers, configuration, and CLI orchestration functions.
|
|
7
|
+
System Integration:
|
|
8
|
+
Keeps the package interface aligned with the module reference documented in
|
|
9
|
+
``docs/system-design/module_reference.md`` while hiding implementation modules.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from . import lib_cli_exit_tools as _facade
|
|
15
|
+
|
|
16
|
+
# Re-export core helpers while keeping a single authoritative list of names in
|
|
17
|
+
# the facade module. Attributes are assigned explicitly so static type checkers
|
|
18
|
+
# understand the exports, while the debug assertion keeps this module aligned
|
|
19
|
+
# with the facade surface.
|
|
20
|
+
CliSignalError = _facade.CliSignalError
|
|
21
|
+
SigBreakInterrupt = _facade.SigBreakInterrupt
|
|
22
|
+
SigIntInterrupt = _facade.SigIntInterrupt
|
|
23
|
+
SigTermInterrupt = _facade.SigTermInterrupt
|
|
24
|
+
SignalSpec = _facade.SignalSpec
|
|
25
|
+
config = _facade.config
|
|
26
|
+
config_overrides = _facade.config_overrides
|
|
27
|
+
default_signal_specs = _facade.default_signal_specs
|
|
28
|
+
flush_streams = _facade.flush_streams
|
|
29
|
+
get_system_exit_code = _facade.get_system_exit_code
|
|
30
|
+
handle_cli_exception = _facade.handle_cli_exception
|
|
31
|
+
i_should_fail = _facade.i_should_fail
|
|
32
|
+
install_signal_handlers = _facade.install_signal_handlers
|
|
33
|
+
print_exception_message = _facade.print_exception_message
|
|
34
|
+
reset_config = _facade.reset_config
|
|
35
|
+
run_cli = _facade.run_cli
|
|
36
|
+
cli_session = _facade.cli_session
|
|
37
|
+
|
|
38
|
+
__all__ = list(_facade.PUBLIC_API) # pyright: ignore[reportUnsupportedDunderAll]
|
|
39
|
+
|
|
40
|
+
if __debug__:
|
|
41
|
+
exported = {name for name in __all__ if name in globals()}
|
|
42
|
+
if len(exported) != len(__all__):
|
|
43
|
+
missing = sorted(set(__all__) - exported)
|
|
44
|
+
raise ImportError(f"lib_cli_exit_tools exports out of sync: missing {missing}")
|
|
45
|
+
del exported
|
|
46
|
+
|
|
47
|
+
del _facade
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Static package metadata surfaced to CLI commands and documentation.
|
|
2
|
+
|
|
3
|
+
Purpose
|
|
4
|
+
-------
|
|
5
|
+
Expose the current project metadata as simple constants. These values are kept
|
|
6
|
+
in sync with ``pyproject.toml`` by development automation (tests, push
|
|
7
|
+
pipelines), so runtime code does not query packaging metadata.
|
|
8
|
+
|
|
9
|
+
Contents
|
|
10
|
+
--------
|
|
11
|
+
* Module-level constants describing the published package.
|
|
12
|
+
* :func:`print_info` rendering the constants for the CLI ``info`` command.
|
|
13
|
+
|
|
14
|
+
System Role
|
|
15
|
+
-----------
|
|
16
|
+
Lives in the adapters/platform layer; CLI transports import these constants to
|
|
17
|
+
present authoritative project information without invoking packaging APIs.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
#: Distribution name declared in ``pyproject.toml``.
|
|
23
|
+
name = "lib_cli_exit_tools"
|
|
24
|
+
#: Human-readable summary shown in CLI help output.
|
|
25
|
+
title = "CLI exit handling helpers: clean signals, exit codes, and error printing"
|
|
26
|
+
#: Current release version pulled from ``pyproject.toml`` by automation.
|
|
27
|
+
version = "2.2.1"
|
|
28
|
+
#: Repository homepage presented to users.
|
|
29
|
+
homepage = "https://github.com/bitranox/lib_cli_exit_tools"
|
|
30
|
+
#: Author attribution surfaced in CLI output.
|
|
31
|
+
author = "bitranox"
|
|
32
|
+
#: Contact email surfaced in CLI output.
|
|
33
|
+
author_email = "bitranox@gmail.com"
|
|
34
|
+
#: Console-script name published by the package.
|
|
35
|
+
shell_command = "lib-cli-exit-tools"
|
|
36
|
+
|
|
37
|
+
#: Vendor identifier for lib_layered_config paths (macOS/Windows)
|
|
38
|
+
LAYEREDCONF_VENDOR: str = "bitranox"
|
|
39
|
+
#: Application display name for lib_layered_config paths (macOS/Windows)
|
|
40
|
+
LAYEREDCONF_APP: str = "Lib Cli Exit Tools"
|
|
41
|
+
#: Configuration slug for lib_layered_config Linux paths and environment variables
|
|
42
|
+
LAYEREDCONF_SLUG: str = "lib-cli-exit-tools"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def print_info() -> None:
|
|
46
|
+
"""Print the summarised metadata block used by the CLI ``info`` command.
|
|
47
|
+
|
|
48
|
+
Why
|
|
49
|
+
Provides a single, auditable rendering function so documentation and
|
|
50
|
+
CLI output always match the system design reference.
|
|
51
|
+
|
|
52
|
+
Side Effects
|
|
53
|
+
Writes to ``stdout``.
|
|
54
|
+
|
|
55
|
+
Examples
|
|
56
|
+
--------
|
|
57
|
+
>>> print_info() # doctest: +ELLIPSIS
|
|
58
|
+
Info for lib_cli_exit_tools:
|
|
59
|
+
...
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
fields = [
|
|
63
|
+
("name", name),
|
|
64
|
+
("title", title),
|
|
65
|
+
("version", version),
|
|
66
|
+
("homepage", homepage),
|
|
67
|
+
("author", author),
|
|
68
|
+
("author_email", author_email),
|
|
69
|
+
("shell_command", shell_command),
|
|
70
|
+
]
|
|
71
|
+
pad = max(len(label) for label, _ in fields)
|
|
72
|
+
lines = [f"Info for {name}:", ""]
|
|
73
|
+
lines.extend(f" {label.ljust(pad)} = {value}" for label, value in fields)
|
|
74
|
+
print("\n".join(lines))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Let ``python -m lib_cli_exit_tools`` behave like the console script."""
|
|
2
|
+
|
|
3
|
+
from . import cli
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main() -> int:
|
|
7
|
+
"""Forward module execution to the Click entry point."""
|
|
8
|
+
|
|
9
|
+
return cli.main()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if __name__ == "__main__":
|
|
13
|
+
raise SystemExit(main())
|
|
@@ -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,178 @@
|
|
|
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 contextlib import ExitStack
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from types import FrameType
|
|
22
|
+
from typing import Callable, Iterable, Sequence
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"CliSignalError",
|
|
26
|
+
"SigIntInterrupt",
|
|
27
|
+
"SigTermInterrupt",
|
|
28
|
+
"SigBreakInterrupt",
|
|
29
|
+
"SignalSpec",
|
|
30
|
+
"default_signal_specs",
|
|
31
|
+
"install_signal_handlers",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CliSignalError(RuntimeError):
|
|
36
|
+
"""Base class for translating OS signals into structured CLI errors.
|
|
37
|
+
|
|
38
|
+
Why:
|
|
39
|
+
Provide a dedicated hierarchy so exit handlers can recognise signal-driven
|
|
40
|
+
interruptions and map them to deterministic exit codes.
|
|
41
|
+
Usage:
|
|
42
|
+
Raised automatically by handlers created via :func:`install_signal_handlers`.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SigIntInterrupt(CliSignalError):
|
|
47
|
+
"""Raised when the process receives ``SIGINT`` (Ctrl+C)."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SigTermInterrupt(CliSignalError):
|
|
51
|
+
"""Raised when the process receives ``SIGTERM`` (termination request)."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SigBreakInterrupt(CliSignalError):
|
|
55
|
+
"""Raised when the process receives ``SIGBREAK`` on Windows consoles."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(slots=True)
|
|
59
|
+
class SignalSpec:
|
|
60
|
+
"""Describe how to translate a low-level signal into CLI-facing behaviour.
|
|
61
|
+
|
|
62
|
+
Fields:
|
|
63
|
+
signum: Numeric identifier registered with :mod:`signal`.
|
|
64
|
+
exception: Exception type raised by the generated handler.
|
|
65
|
+
message: User-facing text echoed to stderr when the signal fires.
|
|
66
|
+
exit_code: Numeric code returned to the operating system.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
signum: int
|
|
70
|
+
exception: type[BaseException]
|
|
71
|
+
message: str
|
|
72
|
+
exit_code: int
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
_Handler = Callable[[int, FrameType | None], None]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def default_signal_specs(extra: Iterable[SignalSpec] | None = None) -> list[SignalSpec]:
|
|
79
|
+
"""Build the default list of signal specifications for the host platform.
|
|
80
|
+
|
|
81
|
+
Why:
|
|
82
|
+
The application runner needs a predictable baseline of signals to
|
|
83
|
+
install without duplicating platform checks.
|
|
84
|
+
Parameters:
|
|
85
|
+
extra: Optional iterable of additional ``SignalSpec`` instances to
|
|
86
|
+
append for caller-specific behaviour.
|
|
87
|
+
Returns:
|
|
88
|
+
List of signal specifications tailored to the current interpreter.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
specs: list[SignalSpec] = _standard_signal_specs()
|
|
92
|
+
if extra is not None:
|
|
93
|
+
specs.extend(extra)
|
|
94
|
+
return specs
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _make_raise_handler(exc_type: type[BaseException]) -> _Handler:
|
|
98
|
+
"""Wrap ``exc_type`` in a signal-compatible callable."""
|
|
99
|
+
|
|
100
|
+
def _handler(signo: int, frame: FrameType | None) -> None: # pragma: no cover - just raises
|
|
101
|
+
raise exc_type()
|
|
102
|
+
|
|
103
|
+
return _handler
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def install_signal_handlers(specs: Sequence[SignalSpec] | None = None) -> Callable[[], None]:
|
|
107
|
+
"""Install signal handlers that re-raise as structured exceptions."""
|
|
108
|
+
|
|
109
|
+
active_specs = _choose_specs(specs)
|
|
110
|
+
stack = _register_handlers(active_specs)
|
|
111
|
+
return stack.close
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _choose_specs(specs: Sequence[SignalSpec] | None) -> list[SignalSpec]:
|
|
115
|
+
"""Return a concrete list of signal specs, defaulting when ``None``."""
|
|
116
|
+
if specs is None:
|
|
117
|
+
return default_signal_specs()
|
|
118
|
+
return list(specs)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _register_handlers(specs: Sequence[SignalSpec]) -> ExitStack:
|
|
122
|
+
"""Register handlers for each ``SignalSpec`` and capture previous handlers."""
|
|
123
|
+
|
|
124
|
+
stack = ExitStack()
|
|
125
|
+
for spec in specs:
|
|
126
|
+
handler = _make_raise_handler(spec.exception)
|
|
127
|
+
try:
|
|
128
|
+
previous = signal.getsignal(spec.signum)
|
|
129
|
+
signal.signal(spec.signum, handler)
|
|
130
|
+
except (AttributeError, OSError, RuntimeError): # pragma: no cover - platform differences
|
|
131
|
+
continue
|
|
132
|
+
stack.callback(signal.signal, spec.signum, previous)
|
|
133
|
+
return stack
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _standard_signal_specs() -> list[SignalSpec]:
|
|
137
|
+
"""Return the base set of signal specifications for all platforms."""
|
|
138
|
+
specs: list[SignalSpec] = [_sigint_spec()]
|
|
139
|
+
specs.extend(_optional_specs())
|
|
140
|
+
return specs
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _sigint_spec() -> SignalSpec:
|
|
144
|
+
"""Return the ``SIGINT`` specification shared across all platforms."""
|
|
145
|
+
return SignalSpec(
|
|
146
|
+
signum=signal.SIGINT,
|
|
147
|
+
exception=SigIntInterrupt,
|
|
148
|
+
message="Aborted (SIGINT).",
|
|
149
|
+
exit_code=130,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _optional_specs() -> Iterable[SignalSpec]:
|
|
154
|
+
"""Yield platform-conditional signal specifications."""
|
|
155
|
+
yield from _maybe_sigterm_spec()
|
|
156
|
+
yield from _maybe_sigbreak_spec()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _maybe_sigterm_spec() -> Iterable[SignalSpec]:
|
|
160
|
+
"""Yield the ``SIGTERM`` specification when supported by the host."""
|
|
161
|
+
if hasattr(signal, "SIGTERM"):
|
|
162
|
+
yield SignalSpec(
|
|
163
|
+
signum=getattr(signal, "SIGTERM"),
|
|
164
|
+
exception=SigTermInterrupt,
|
|
165
|
+
message="Terminated (SIGTERM/SIGBREAK).",
|
|
166
|
+
exit_code=143,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _maybe_sigbreak_spec() -> Iterable[SignalSpec]:
|
|
171
|
+
"""Yield the ``SIGBREAK`` specification when running on Windows."""
|
|
172
|
+
if hasattr(signal, "SIGBREAK"):
|
|
173
|
+
yield SignalSpec(
|
|
174
|
+
signum=getattr(signal, "SIGBREAK"),
|
|
175
|
+
exception=SigBreakInterrupt,
|
|
176
|
+
message="Terminated (SIGBREAK).",
|
|
177
|
+
exit_code=149,
|
|
178
|
+
)
|
|
@@ -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] = []
|