lib-cli-exit-tools 2.2.4__py3-none-any.whl → 2.3.0__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.
@@ -17,6 +17,7 @@ from . import lib_cli_exit_tools as _facade
17
17
  # the facade module. Attributes are assigned explicitly so static type checkers
18
18
  # understand the exports, while the debug assertion keeps this module aligned
19
19
  # with the facade surface.
20
+ ExitCodeStyle = _facade.ExitCodeStyle
20
21
  CliSignalError = _facade.CliSignalError
21
22
  SigBreakInterrupt = _facade.SigBreakInterrupt
22
23
  SigIntInterrupt = _facade.SigIntInterrupt
@@ -24,7 +24,7 @@ name = "lib_cli_exit_tools"
24
24
  #: Human-readable summary shown in CLI help output.
25
25
  title = "CLI exit handling helpers: clean signals, exit codes, and error printing"
26
26
  #: Current release version pulled from ``pyproject.toml`` by automation.
27
- version = "2.2.4"
27
+ version = "2.3.0"
28
28
  #: Repository homepage presented to users.
29
29
  homepage = "https://github.com/bitranox/lib_cli_exit_tools"
30
30
  #: Author attribution surfaced in CLI output.
@@ -20,7 +20,7 @@ from __future__ import annotations
20
20
 
21
21
  import sys
22
22
  from contextlib import contextmanager, nullcontext, suppress
23
- from typing import Callable, ContextManager, Iterable, Iterator, Literal, Mapping, Protocol, Sequence, TextIO, TypedDict, cast
23
+ from typing import Callable, ContextManager, Iterable, Iterator, Literal, Protocol, Sequence, TextIO, TypedDict, cast
24
24
 
25
25
  import rich_click as click
26
26
  from rich_click import rich_click as rich_config
@@ -29,17 +29,12 @@ from rich.text import Text
29
29
  from rich.traceback import Traceback
30
30
 
31
31
  from ..adapters.signals import SignalSpec, default_signal_specs, install_signal_handlers
32
- from ..core.configuration import config, config_overrides
32
+ from ..core.configuration import ExitCodeStyle, config, config_overrides
33
33
  from ..core.exit_codes import get_system_exit_code
34
34
 
35
35
  RichColorSystem = Literal["auto", "standard", "256", "truecolor", "windows"]
36
36
  ExitResolver = Callable[[BaseException], int | None]
37
37
 
38
- # Configuration field name constants to ensure type-safe access
39
- # These correspond to fields in _Config dataclass (core/configuration.py)
40
- _CONFIG_TRACEBACK = "traceback"
41
- _CONFIG_TRACEBACK_FORCE_COLOR = "traceback_force_color"
42
-
43
38
 
44
39
  class SessionOverrides(TypedDict, total=False):
45
40
  """Type-safe override mapping for cli_session configuration.
@@ -49,7 +44,7 @@ class SessionOverrides(TypedDict, total=False):
49
44
  """
50
45
 
51
46
  traceback: bool
52
- exit_code_style: Literal["errno", "sysexits"]
47
+ exit_code_style: ExitCodeStyle
53
48
  broken_pipe_exit_code: int
54
49
  traceback_force_color: bool
55
50
 
@@ -464,7 +459,7 @@ def cli_session(
464
459
  *,
465
460
  summary_limit: int = 500,
466
461
  verbose_limit: int = 10_000,
467
- overrides: Mapping[str, object] | None = None,
462
+ overrides: SessionOverrides | None = None,
468
463
  restore: bool = True,
469
464
  ) -> Iterator[
470
465
  Callable[
@@ -498,7 +493,7 @@ def cli_session(
498
493
  verbose_limit:
499
494
  Character budget used when tracebacks are enabled.
500
495
  overrides:
501
- Optional mapping of configuration fields temporarily applied during
496
+ Optional :class:`SessionOverrides` mapping temporarily applied during
502
497
  the session. When ``traceback`` is supplied and
503
498
  ``traceback_force_color`` is omitted, colour output is forced to align
504
499
  with the verbose mode.
@@ -544,26 +539,29 @@ def cli_session(
544
539
  yield _run
545
540
 
546
541
 
547
- def _normalise_session_overrides(overrides: Mapping[str, object] | None) -> Mapping[str, object]:
542
+ def _normalise_session_overrides(overrides: SessionOverrides | None) -> SessionOverrides:
548
543
  """Prepare configuration overrides, forcing colour when verbose tracebacks are enabled.
549
544
 
550
- Returns a Mapping instead of dict to avoid unnecessary conversions.
551
- Uses typed constants for field names to ensure type safety.
545
+ Why:
546
+ When traceback mode is enabled but force_color is not explicitly set,
547
+ automatically enable colour output to align with verbose mode.
548
+ Parameters:
549
+ overrides: Optional typed override mapping from the caller.
550
+ Returns:
551
+ Normalised override mapping with inferred defaults applied.
552
552
  """
553
553
  if not overrides:
554
- return {}
554
+ return SessionOverrides()
555
555
 
556
- # If traceback is enabled but force_color is not set, add it
557
- if _CONFIG_TRACEBACK in overrides and _CONFIG_TRACEBACK_FORCE_COLOR not in overrides:
558
- # Need to create a new dict to add the extra field
559
- result: dict[str, object] = {**overrides}
560
- result[_CONFIG_TRACEBACK_FORCE_COLOR] = bool(overrides[_CONFIG_TRACEBACK])
556
+ if "traceback" in overrides and "traceback_force_color" not in overrides:
557
+ result = SessionOverrides(**overrides)
558
+ result["traceback_force_color"] = bool(overrides["traceback"])
561
559
  return result
562
560
 
563
561
  return overrides
564
562
 
565
563
 
566
- def _session_config_manager(applied: Mapping[str, object], restore: bool) -> ContextManager[object]:
564
+ def _session_config_manager(applied: SessionOverrides, restore: bool) -> ContextManager[object]:
567
565
  """Return the context manager used to apply session overrides."""
568
566
  if restore:
569
567
  return config_overrides(**applied)
@@ -573,17 +571,11 @@ def _session_config_manager(applied: Mapping[str, object], restore: bool) -> Con
573
571
 
574
572
 
575
573
  @contextmanager
576
- def _apply_overrides_without_restore(applied: Mapping[str, object]) -> Iterator[None]:
574
+ def _apply_overrides_without_restore(applied: SessionOverrides) -> Iterator[None]:
577
575
  """Apply overrides without restoring prior state on exit.
578
576
 
579
- Safety Note:
580
- The setattr calls here are type-safe because all field names in ``applied``
581
- have been validated by ``_reject_unknown_fields`` in configuration.py
582
- (line 114) before reaching this function. The config_overrides context
583
- manager ensures only valid _Config field names can be used.
584
-
585
577
  Parameters:
586
- applied: Validated mapping of configuration field names to values.
578
+ applied: Typed override mapping of configuration field names to values.
587
579
  """
588
580
  for name, value in applied.items():
589
581
  setattr(config, name, value)
@@ -0,0 +1,27 @@
1
+ """Click-based CLI adapter for lib_cli_exit_tools.
2
+
3
+ Purpose:
4
+ Provide the end-user command-line surface while delegating error-handling
5
+ and exit-code logic to :mod:`lib_cli_exit_tools.lib_cli_exit_tools`.
6
+ Contents:
7
+ * :func:`cli` group exposing shared options.
8
+ * :func:`cli_info` subcommand reporting distribution metadata.
9
+ * :func:`cli_fail` subcommand triggering a deterministic failure for testing error paths.
10
+ * :func:`main` entry point used by console scripts and ``python -m``.
11
+ System Integration:
12
+ The CLI mutates :data:`lib_cli_exit_tools.config` based on the ``--traceback``
13
+ flag before handing execution off to :func:`lib_cli_exit_tools.run_cli`.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ # --- Public surface re-exports (backward compatibility) ---
19
+ from .commands import cli_fail as cli_fail
20
+ from .commands import cli_info as cli_info
21
+ from .group import CliContextState as CliContextState
22
+ from .group import cli as cli
23
+ from .group import main as main
24
+
25
+ # --- Register subcommands with the root group ---
26
+ cli.add_command(cli_info)
27
+ cli.add_command(cli_fail)
@@ -0,0 +1,69 @@
1
+ """Click subcommands for the lib_cli_exit_tools CLI.
2
+
3
+ Purpose:
4
+ Define individual CLI subcommands that expose library functionality to
5
+ end users (``info``, ``fail``).
6
+ Contents:
7
+ * :data:`CLICK_CONTEXT_SETTINGS` shared context settings for all commands.
8
+ * :func:`cli_info` subcommand reporting distribution metadata.
9
+ * :func:`cli_fail` subcommand triggering a deterministic failure.
10
+ System Integration:
11
+ Commands are registered with the root group in :mod:`lib_cli_exit_tools.cli`.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import rich_click as click
17
+
18
+ from .. import __init__conf__
19
+ from .. import lib_cli_exit_tools
20
+
21
+ #: Help flag aliases applied to every Click command so documentation and CLI
22
+ #: behaviour stay consistent (`-h` mirrors `--help`).
23
+ #: Note: This uses dict literal syntax as required by Click framework API.
24
+ CLICK_CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
25
+
26
+
27
+ @click.command("info", context_settings=CLICK_CONTEXT_SETTINGS)
28
+ def cli_info() -> None:
29
+ """Display package metadata exported by :mod:`lib_cli_exit_tools.__init__conf__`.
30
+
31
+ Why:
32
+ Offer a zero-dependency way for users to confirm the installed version
33
+ and provenance of the CLI.
34
+ Side Effects:
35
+ Writes formatted metadata to stdout.
36
+ Examples:
37
+ >>> from click.testing import CliRunner
38
+ >>> from lib_cli_exit_tools.cli.group import cli
39
+ >>> runner = CliRunner()
40
+ >>> result = runner.invoke(cli, ["info"])
41
+ >>> "Info for" in result.output
42
+ True
43
+ """
44
+ __init__conf__.print_info()
45
+
46
+
47
+ @click.command("fail", context_settings=CLICK_CONTEXT_SETTINGS)
48
+ def cli_fail() -> None:
49
+ """Intentionally raise a failure to validate error handling.
50
+
51
+ Why:
52
+ Exercise the error-reporting path so engineers can confirm exit-code
53
+ translation and optional traceback behaviour without crafting custom
54
+ failing commands.
55
+ Side Effects:
56
+ Delegates to :func:`lib_cli_exit_tools.i_should_fail`, which raises
57
+ ``RuntimeError`` for the caller to handle.
58
+ Examples:
59
+ >>> from click.testing import CliRunner
60
+ >>> from lib_cli_exit_tools.cli.group import cli
61
+ >>> runner = CliRunner()
62
+ >>> result = runner.invoke(cli, ["fail"])
63
+ >>> result.exit_code != 0
64
+ True
65
+ >>> "i should fail" in str(result.exception)
66
+ True
67
+ """
68
+
69
+ lib_cli_exit_tools.i_should_fail()
@@ -0,0 +1,123 @@
1
+ """Root Click group, context state, and CLI entry point.
2
+
3
+ Purpose:
4
+ Define the top-level Click group with shared options (``--traceback``,
5
+ ``--version``) and the :func:`main` entry point used by console scripts.
6
+ Contents:
7
+ * :class:`CliContextState` typed container for Click context state.
8
+ * :func:`cli` root Click group.
9
+ * :func:`main` entry point for console scripts and ``python -m``.
10
+ System Integration:
11
+ The CLI mutates :data:`lib_cli_exit_tools.config` based on the ``--traceback``
12
+ flag before handing execution off to :func:`lib_cli_exit_tools.run_cli`.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from collections.abc import Sequence
18
+ from dataclasses import dataclass
19
+
20
+ import rich_click as click
21
+
22
+ from .. import __init__conf__
23
+ from .. import lib_cli_exit_tools
24
+ from .commands import CLICK_CONTEXT_SETTINGS
25
+ from .styling import _temporary_rich_click_configuration # pyright: ignore[reportPrivateUsage]
26
+
27
+
28
+ @dataclass
29
+ class CliContextState:
30
+ """Type-safe container for Click context object state.
31
+
32
+ Why:
33
+ Replaces untyped dict usage in ctx.obj to enforce type safety and
34
+ prevent string literal key access violations.
35
+
36
+ Attributes:
37
+ traceback: Whether to show full Python tracebacks on errors.
38
+ """
39
+
40
+ traceback: bool = False
41
+
42
+
43
+ @click.group(help=__init__conf__.title, context_settings=CLICK_CONTEXT_SETTINGS)
44
+ @click.version_option(
45
+ version=__init__conf__.version,
46
+ prog_name=__init__conf__.shell_command,
47
+ message=f"{__init__conf__.shell_command} version {__init__conf__.version}",
48
+ )
49
+ @click.option(
50
+ "--traceback/--no-traceback",
51
+ is_flag=True,
52
+ default=False,
53
+ help="Show full Python traceback on errors",
54
+ )
55
+ @click.pass_context
56
+ def cli(ctx: click.Context, traceback: bool) -> None:
57
+ """Root Click group that primes shared configuration state.
58
+
59
+ Why:
60
+ Accept a single ``--traceback`` flag that determines whether downstream
61
+ helpers emit stack traces.
62
+ Parameters:
63
+ ctx: Click context object for the current invocation.
64
+ traceback: When ``True`` enables traceback output for subsequent commands.
65
+ Side Effects:
66
+ Mutates ``ctx.obj`` and :data:`lib_cli_exit_tools.config.traceback`.
67
+ Examples:
68
+ >>> from click.testing import CliRunner
69
+ >>> runner = CliRunner()
70
+ >>> result = runner.invoke(cli, ["--help"])
71
+ >>> result.exit_code == 0
72
+ True
73
+ """
74
+ _store_traceback_flag(ctx, traceback)
75
+ lib_cli_exit_tools.config.traceback = traceback
76
+
77
+
78
+ def _store_traceback_flag(ctx: click.Context, traceback: bool) -> None:
79
+ """Store traceback flag in typed context state.
80
+
81
+ Parameters:
82
+ ctx: Click context object for the current invocation.
83
+ traceback: Whether to show full Python tracebacks on errors.
84
+
85
+ Side Effects:
86
+ Initializes ctx.obj as CliContextState if not already set and updates
87
+ the traceback field.
88
+ """
89
+ ctx.ensure_object(CliContextState)
90
+ # Type narrowing: ctx.obj is guaranteed to be CliContextState after ensure_object
91
+ state = ctx.obj
92
+ if isinstance(state, CliContextState): # nosec B101 - type guard, not assertion
93
+ state.traceback = traceback
94
+
95
+
96
+ def main(argv: Sequence[str] | None = None) -> int:
97
+ """Run the CLI with :func:`lib_cli_exit_tools.run_cli` wiring.
98
+
99
+ Why:
100
+ Serve as the target for console scripts and ``python -m`` execution by
101
+ returning an integer exit code instead of exiting directly.
102
+ Parameters:
103
+ argv: Optional iterable of arguments passed to Click (without program name).
104
+ Returns:
105
+ Integer exit code from :func:`lib_cli_exit_tools.run_cli`.
106
+ Side Effects:
107
+ Delegates to Click and may write to stdout/stderr.
108
+ Examples:
109
+ >>> import contextlib, io
110
+ >>> buffer = io.StringIO()
111
+ >>> with contextlib.redirect_stdout(buffer):
112
+ ... exit_code = main(["info"])
113
+ >>> exit_code
114
+ 0
115
+ >>> "Info for" in buffer.getvalue()
116
+ True
117
+ """
118
+ with _temporary_rich_click_configuration():
119
+ return lib_cli_exit_tools.run_cli(
120
+ cli,
121
+ argv=list(argv) if argv is not None else None,
122
+ prog_name=__init__conf__.shell_command,
123
+ )
@@ -0,0 +1,111 @@
1
+ """Rich-click styling adapter for terminal capability detection.
2
+
3
+ Purpose:
4
+ Detect terminal capabilities (TTY, encoding) and configure rich-click
5
+ global styling accordingly, falling back to ASCII-safe defaults when
6
+ the output stream cannot render Rich markup.
7
+ Contents:
8
+ * :class:`RichClickSnapshot` typed snapshot of rich-click globals.
9
+ * Stream inspection helpers for TTY and encoding detection.
10
+ * :func:`_temporary_rich_click_configuration` context manager.
11
+ System Integration:
12
+ Called by :func:`lib_cli_exit_tools.cli.group.main` before every CLI
13
+ invocation to ensure rich-click output matches terminal capabilities.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from contextlib import contextmanager
19
+ from typing import Any, Iterator, TypedDict
20
+
21
+ import rich_click as click
22
+ from rich_click import rich_click as rich_config
23
+
24
+
25
+ class RichClickSnapshot(TypedDict):
26
+ """Type-safe snapshot of rich-click global styling configuration.
27
+
28
+ Attributes:
29
+ FORCE_TERMINAL: Whether to force terminal styling.
30
+ COLOR_SYSTEM: Color system to use (e.g., "auto", "256", "truecolor", None).
31
+ STYLE_OPTIONS_PANEL_BOX: Box style for options panel.
32
+ STYLE_COMMANDS_PANEL_BOX: Box style for commands panel.
33
+ STYLE_ERRORS_PANEL_BOX: Box style for errors panel.
34
+
35
+ Note:
36
+ Uses Any for field types to accommodate the dynamic nature of rich-click
37
+ configuration, which doesn't provide stable typing guarantees.
38
+ """
39
+
40
+ FORCE_TERMINAL: Any
41
+ COLOR_SYSTEM: Any
42
+ STYLE_OPTIONS_PANEL_BOX: Any
43
+ STYLE_COMMANDS_PANEL_BOX: Any
44
+ STYLE_ERRORS_PANEL_BOX: Any
45
+
46
+
47
+ def _needs_plain_output(stream: object) -> bool:
48
+ """Tell whether ``stream`` will honour Rich styling."""
49
+
50
+ return (not _stream_is_tty(stream)) or (not _stream_supports_utf(stream))
51
+
52
+
53
+ def _stream_is_tty(stream: object) -> bool:
54
+ """Return ``True`` when ``stream`` behaves like a TTY."""
55
+ checker = getattr(stream, "isatty", lambda: False)
56
+ try:
57
+ return bool(checker())
58
+ except Exception: # pragma: no cover - defensive shield
59
+ return False
60
+
61
+
62
+ def _stream_supports_utf(stream: object) -> bool:
63
+ """Tell whether ``stream`` reports a UTF-friendly encoding."""
64
+ encoding = (getattr(stream, "encoding", "") or "").lower()
65
+ return "utf" in encoding
66
+
67
+
68
+ def _prefer_ascii_layout() -> None:
69
+ """Downgrade rich-click global styling to ASCII-friendly defaults."""
70
+ rich_config.FORCE_TERMINAL = False
71
+ rich_config.COLOR_SYSTEM = None
72
+ rich_config.STYLE_OPTIONS_PANEL_BOX = None
73
+ rich_config.STYLE_COMMANDS_PANEL_BOX = None
74
+ rich_config.STYLE_ERRORS_PANEL_BOX = None
75
+
76
+
77
+ def _snapshot_rich_click_options() -> RichClickSnapshot:
78
+ """Capture rich-click global styling toggles for later restoration."""
79
+
80
+ return RichClickSnapshot(
81
+ FORCE_TERMINAL=getattr(rich_config, "FORCE_TERMINAL", None),
82
+ COLOR_SYSTEM=getattr(rich_config, "COLOR_SYSTEM", None),
83
+ STYLE_OPTIONS_PANEL_BOX=getattr(rich_config, "STYLE_OPTIONS_PANEL_BOX", None),
84
+ STYLE_COMMANDS_PANEL_BOX=getattr(rich_config, "STYLE_COMMANDS_PANEL_BOX", None),
85
+ STYLE_ERRORS_PANEL_BOX=getattr(rich_config, "STYLE_ERRORS_PANEL_BOX", None),
86
+ )
87
+
88
+
89
+ def _restore_rich_click_options(snapshot: RichClickSnapshot) -> None:
90
+ """Restore rich-click globals to the captured snapshot."""
91
+
92
+ rich_config.FORCE_TERMINAL = snapshot["FORCE_TERMINAL"]
93
+ rich_config.COLOR_SYSTEM = snapshot["COLOR_SYSTEM"]
94
+ rich_config.STYLE_OPTIONS_PANEL_BOX = snapshot["STYLE_OPTIONS_PANEL_BOX"]
95
+ rich_config.STYLE_COMMANDS_PANEL_BOX = snapshot["STYLE_COMMANDS_PANEL_BOX"]
96
+ rich_config.STYLE_ERRORS_PANEL_BOX = snapshot["STYLE_ERRORS_PANEL_BOX"]
97
+
98
+
99
+ @contextmanager
100
+ def _temporary_rich_click_configuration() -> Iterator[None]: # pyright: ignore[reportUnusedFunction]
101
+ """Apply plain-output safeguards and restore rich-click globals afterwards."""
102
+
103
+ snapshot = _snapshot_rich_click_options()
104
+ stdout_stream = click.get_text_stream("stdout")
105
+ stderr_stream = click.get_text_stream("stderr")
106
+ if _needs_plain_output(stdout_stream) and _needs_plain_output(stderr_stream):
107
+ _prefer_ascii_layout()
108
+ try:
109
+ yield
110
+ finally:
111
+ _restore_rich_click_options(snapshot)
@@ -23,9 +23,25 @@ from __future__ import annotations
23
23
  from collections.abc import Iterator, Mapping
24
24
  from contextlib import contextmanager
25
25
  from dataclasses import dataclass, fields
26
- from typing import Literal, TypedDict
26
+ from enum import Enum
27
+ from typing import TypedDict
27
28
 
28
- __all__ = ["_Config", "config", "config_overrides", "reset_config"]
29
+ __all__ = ["ExitCodeStyle", "_Config", "config", "config_overrides", "reset_config"]
30
+
31
+
32
+ class ExitCodeStyle(str, Enum):
33
+ """Exit-code mapping strategy selector.
34
+
35
+ Why:
36
+ Replace raw string literals with a closed set of values so comparisons
37
+ are type-safe and spelling mistakes are caught at import time.
38
+ Members:
39
+ ERRNO: Use POSIX/Windows errno-based exit codes (default).
40
+ SYSEXITS: Use BSD sysexits.h conventions for shell-centric workflows.
41
+ """
42
+
43
+ ERRNO = "errno"
44
+ SYSEXITS = "sysexits"
29
45
 
30
46
 
31
47
  class ConfigSnapshot(TypedDict):
@@ -43,7 +59,7 @@ class ConfigSnapshot(TypedDict):
43
59
  """
44
60
 
45
61
  traceback: bool
46
- exit_code_style: Literal["errno", "sysexits"]
62
+ exit_code_style: ExitCodeStyle
47
63
  broken_pipe_exit_code: int
48
64
  traceback_force_color: bool
49
65
 
@@ -77,7 +93,7 @@ class _Config:
77
93
  """
78
94
 
79
95
  traceback: bool = False
80
- exit_code_style: Literal["errno", "sysexits"] = "errno"
96
+ exit_code_style: ExitCodeStyle = ExitCodeStyle.ERRNO
81
97
  broken_pipe_exit_code: int = 141
82
98
  traceback_force_color: bool = False
83
99
 
@@ -19,7 +19,7 @@ import os
19
19
  import subprocess # nosec B404 - imported for CalledProcessError type inspection
20
20
  from typing import Callable, Iterable, Mapping
21
21
 
22
- from .configuration import config
22
+ from .configuration import ExitCodeStyle, config
23
23
 
24
24
  __all__ = ["get_system_exit_code"]
25
25
 
@@ -217,7 +217,7 @@ def _code_from_sysexits_mode(exc: BaseException) -> int | None:
217
217
  Side Effects:
218
218
  None.
219
219
  """
220
- if config.exit_code_style != "sysexits":
220
+ if config.exit_code_style != ExitCodeStyle.SYSEXITS:
221
221
  return None
222
222
  return _sysexits_resolved_code(exc)
223
223
 
@@ -37,10 +37,11 @@ from .application.runner import (
37
37
  print_exception_message,
38
38
  run_cli,
39
39
  )
40
- from .core.configuration import config, config_overrides, reset_config
40
+ from .core.configuration import ExitCodeStyle, config, config_overrides, reset_config
41
41
  from .core.exit_codes import get_system_exit_code
42
42
 
43
43
  __all__ = [
44
+ "ExitCodeStyle",
44
45
  "config",
45
46
  "get_system_exit_code",
46
47
  "print_exception_message",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lib_cli_exit_tools
3
- Version: 2.2.4
3
+ Version: 2.3.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
@@ -16,22 +16,22 @@ Classifier: Programming Language :: Python :: 3
16
16
  Classifier: Programming Language :: Python :: 3 :: Only
17
17
  Classifier: Typing :: Typed
18
18
  Requires-Python: >=3.10
19
- Requires-Dist: rich-click>=1.9.6
19
+ Requires-Dist: rich-click>=1.9.7
20
20
  Provides-Extra: dev
21
21
  Requires-Dist: bandit>=1.9.3; extra == 'dev'
22
22
  Requires-Dist: build>=1.4.0; extra == 'dev'
23
23
  Requires-Dist: codecov-cli>=11.2.6; extra == 'dev'
24
- Requires-Dist: coverage[toml]>=7.13.2; extra == 'dev'
25
- Requires-Dist: hypothesis>=6.151.2; extra == 'dev'
26
- Requires-Dist: import-linter>=2.9; extra == 'dev'
24
+ Requires-Dist: coverage[toml]>=7.13.4; extra == 'dev'
25
+ Requires-Dist: hypothesis>=6.151.6; extra == 'dev'
26
+ Requires-Dist: import-linter>=2.10; extra == 'dev'
27
27
  Requires-Dist: pip-audit>=2.10.0; extra == 'dev'
28
28
  Requires-Dist: pyright[nodejs]>=1.1.408; extra == 'dev'
29
29
  Requires-Dist: pytest-asyncio>=1.3.0; extra == 'dev'
30
30
  Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
31
31
  Requires-Dist: pytest>=9.0.2; extra == 'dev'
32
32
  Requires-Dist: rtoml>=0.13.0; extra == 'dev'
33
- Requires-Dist: ruff>=0.14.14; extra == 'dev'
34
- Requires-Dist: textual>=7.4.0; extra == 'dev'
33
+ Requires-Dist: ruff>=0.15.1; extra == 'dev'
34
+ Requires-Dist: textual>=7.5.0; extra == 'dev'
35
35
  Requires-Dist: twine>=6.2.0; extra == 'dev'
36
36
  Description-Content-Type: text/markdown
37
37
 
@@ -0,0 +1,21 @@
1
+ lib_cli_exit_tools/__init__.py,sha256=KzpZAMrc328V_nYm2GuTR0Sxyqh0XuKtlHP6dVCs2pc,1905
2
+ lib_cli_exit_tools/__init__conf__.py,sha256=tYrmAVN5mo_12V1_m_un7FtroeeZY0M9zHrPFXvnOdU,2521
3
+ lib_cli_exit_tools/__main__.py,sha256=wODJaNu_NyO7tknvlpPArq0MaDwDum8zqyb73-qshzo,257
4
+ lib_cli_exit_tools/lib_cli_exit_tools.py,sha256=fCLgJypLi6VHjmnh0McUn3CbhBhdzGYdDGcwcLokL4o,2561
5
+ lib_cli_exit_tools/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ lib_cli_exit_tools/adapters/__init__.py,sha256=Jj8FCGY1lAPAduYSW3wTDh8ojpr9BZms7wKEMPq8pWE,504
7
+ lib_cli_exit_tools/adapters/signals.py,sha256=Q2D9Htv64nsT5n66cJFDDHRWVXJokaG77I-V8gTDMKg,5772
8
+ lib_cli_exit_tools/application/__init__.py,sha256=Yikej3OOOaAjWOvFimg1jUYrCUGu5d3-RBiiai4tHDM,566
9
+ lib_cli_exit_tools/application/runner.py,sha256=oTzARoieuN0JK7KRSzrMhKTFWF6GA8cxt1pK6YVjgVM,24413
10
+ lib_cli_exit_tools/cli/__init__.py,sha256=IVyU1DEgn2xBYfp1BBpund9nucUEPyOu7yt8DShu470,1094
11
+ lib_cli_exit_tools/cli/commands.py,sha256=WDF6hDsyczAb6cQp4fA6KMF1iqJQ1lzjv-RvIxq1itI,2418
12
+ lib_cli_exit_tools/cli/group.py,sha256=UFKb8VCkkQEB4CwIDckItrPHTRxSCddyLXKKr-2xU-w,4224
13
+ lib_cli_exit_tools/cli/styling.py,sha256=2W9-0Ig4X3RO8wVPukZEg2V_kBJfZKJANlawUOJAlhg,4236
14
+ lib_cli_exit_tools/core/__init__.py,sha256=aR7OIBWOQ1PsA33blOTgLrq5ffKDCuaMVBS5mbrDAZ8,531
15
+ lib_cli_exit_tools/core/configuration.py,sha256=ND0BBRBY7BkRbQWliWj-pp7pY1g8QULxgkeXfu2wfI8,6121
16
+ lib_cli_exit_tools/core/exit_codes.py,sha256=ukinlUROFKb6KmF_-8luxE1C2VJc6UwxIMMr0eB9xFg,12349
17
+ lib_cli_exit_tools-2.3.0.dist-info/METADATA,sha256=PQkASspOF7lCn8k3UG2y4lGdLJo_w-nHQWpoOirduC4,26234
18
+ lib_cli_exit_tools-2.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
19
+ lib_cli_exit_tools-2.3.0.dist-info/entry_points.txt,sha256=8R-_3UT0KcQ7HQ7idDx6rlG77GhHb0E2N5QnN41dj-M,161
20
+ lib_cli_exit_tools-2.3.0.dist-info/licenses/LICENSE,sha256=6s9SL2InRrwynPjJYiER7ONHBshbGj_yC5EBzfo7l9I,1072
21
+ lib_cli_exit_tools-2.3.0.dist-info/RECORD,,
lib_cli_exit_tools/cli.py DELETED
@@ -1,261 +0,0 @@
1
- """Click-based CLI adapter for lib_cli_exit_tools.
2
-
3
- Purpose:
4
- Provide the end-user command-line surface while delegating error-handling
5
- and exit-code logic to :mod:`lib_cli_exit_tools.lib_cli_exit_tools`.
6
- Contents:
7
- * :func:`cli` group exposing shared options.
8
- * :func:`cli_info` subcommand reporting distribution metadata.
9
- * :func:`cli_fail` subcommand triggering a deterministic failure for testing error paths.
10
- * :func:`main` entry point used by console scripts and ``python -m``.
11
- System Integration:
12
- The CLI mutates :data:`lib_cli_exit_tools.config` based on the ``--traceback``
13
- flag before handing execution off to :func:`lib_cli_exit_tools.run_cli`.
14
- """
15
-
16
- from __future__ import annotations
17
-
18
- from contextlib import contextmanager
19
- from dataclasses import dataclass
20
- from typing import Any, Iterator, Sequence, TypedDict
21
-
22
- import rich_click as click
23
- from rich_click import rich_click as rich_config
24
-
25
- from . import __init__conf__
26
- from . import lib_cli_exit_tools
27
-
28
- #: Help flag aliases applied to every Click command so documentation and CLI
29
- #: behaviour stay consistent (`-h` mirrors `--help`).
30
- #: Note: This uses dict literal syntax as required by Click framework API.
31
- CLICK_CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
32
-
33
-
34
- class RichClickSnapshot(TypedDict):
35
- """Type-safe snapshot of rich-click global styling configuration.
36
-
37
- Attributes:
38
- FORCE_TERMINAL: Whether to force terminal styling.
39
- COLOR_SYSTEM: Color system to use (e.g., "auto", "256", "truecolor", None).
40
- STYLE_OPTIONS_PANEL_BOX: Box style for options panel.
41
- STYLE_COMMANDS_PANEL_BOX: Box style for commands panel.
42
- STYLE_ERRORS_PANEL_BOX: Box style for errors panel.
43
-
44
- Note:
45
- Uses Any for field types to accommodate the dynamic nature of rich-click
46
- configuration, which doesn't provide stable typing guarantees.
47
- """
48
-
49
- FORCE_TERMINAL: Any
50
- COLOR_SYSTEM: Any
51
- STYLE_OPTIONS_PANEL_BOX: Any
52
- STYLE_COMMANDS_PANEL_BOX: Any
53
- STYLE_ERRORS_PANEL_BOX: Any
54
-
55
-
56
- @dataclass
57
- class CliContextState:
58
- """Type-safe container for Click context object state.
59
-
60
- Why:
61
- Replaces untyped dict usage in ctx.obj to enforce type safety and
62
- prevent string literal key access violations.
63
-
64
- Attributes:
65
- traceback: Whether to show full Python tracebacks on errors.
66
- """
67
-
68
- traceback: bool = False
69
-
70
-
71
- def _needs_plain_output(stream: object) -> bool:
72
- """Tell whether ``stream`` will honour Rich styling."""
73
-
74
- return (not _stream_is_tty(stream)) or (not _stream_supports_utf(stream))
75
-
76
-
77
- def _stream_is_tty(stream: object) -> bool:
78
- """Return ``True`` when ``stream`` behaves like a TTY."""
79
- checker = getattr(stream, "isatty", lambda: False)
80
- try:
81
- return bool(checker())
82
- except Exception: # pragma: no cover - defensive shield
83
- return False
84
-
85
-
86
- def _stream_supports_utf(stream: object) -> bool:
87
- """Tell whether ``stream`` reports a UTF-friendly encoding."""
88
- encoding = (getattr(stream, "encoding", "") or "").lower()
89
- return "utf" in encoding
90
-
91
-
92
- def _prefer_ascii_layout() -> None:
93
- """Downgrade rich-click global styling to ASCII-friendly defaults."""
94
- rich_config.FORCE_TERMINAL = False
95
- rich_config.COLOR_SYSTEM = None
96
- rich_config.STYLE_OPTIONS_PANEL_BOX = None
97
- rich_config.STYLE_COMMANDS_PANEL_BOX = None
98
- rich_config.STYLE_ERRORS_PANEL_BOX = None
99
-
100
-
101
- def _snapshot_rich_click_options() -> RichClickSnapshot:
102
- """Capture rich-click global styling toggles for later restoration."""
103
-
104
- return RichClickSnapshot(
105
- FORCE_TERMINAL=getattr(rich_config, "FORCE_TERMINAL", None),
106
- COLOR_SYSTEM=getattr(rich_config, "COLOR_SYSTEM", None),
107
- STYLE_OPTIONS_PANEL_BOX=getattr(rich_config, "STYLE_OPTIONS_PANEL_BOX", None),
108
- STYLE_COMMANDS_PANEL_BOX=getattr(rich_config, "STYLE_COMMANDS_PANEL_BOX", None),
109
- STYLE_ERRORS_PANEL_BOX=getattr(rich_config, "STYLE_ERRORS_PANEL_BOX", None),
110
- )
111
-
112
-
113
- def _restore_rich_click_options(snapshot: RichClickSnapshot) -> None:
114
- """Restore rich-click globals to the captured snapshot."""
115
-
116
- rich_config.FORCE_TERMINAL = snapshot["FORCE_TERMINAL"]
117
- rich_config.COLOR_SYSTEM = snapshot["COLOR_SYSTEM"]
118
- rich_config.STYLE_OPTIONS_PANEL_BOX = snapshot["STYLE_OPTIONS_PANEL_BOX"]
119
- rich_config.STYLE_COMMANDS_PANEL_BOX = snapshot["STYLE_COMMANDS_PANEL_BOX"]
120
- rich_config.STYLE_ERRORS_PANEL_BOX = snapshot["STYLE_ERRORS_PANEL_BOX"]
121
-
122
-
123
- @contextmanager
124
- def _temporary_rich_click_configuration() -> Iterator[None]:
125
- """Apply plain-output safeguards and restore rich-click globals afterwards."""
126
-
127
- snapshot = _snapshot_rich_click_options()
128
- stdout_stream = click.get_text_stream("stdout")
129
- stderr_stream = click.get_text_stream("stderr")
130
- if _needs_plain_output(stdout_stream) and _needs_plain_output(stderr_stream):
131
- _prefer_ascii_layout()
132
- try:
133
- yield
134
- finally:
135
- _restore_rich_click_options(snapshot)
136
-
137
-
138
- @click.group(help=__init__conf__.title, context_settings=CLICK_CONTEXT_SETTINGS)
139
- @click.version_option(
140
- version=__init__conf__.version,
141
- prog_name=__init__conf__.shell_command,
142
- message=f"{__init__conf__.shell_command} version {__init__conf__.version}",
143
- )
144
- @click.option(
145
- "--traceback/--no-traceback",
146
- is_flag=True,
147
- default=False,
148
- help="Show full Python traceback on errors",
149
- )
150
- @click.pass_context
151
- def cli(ctx: click.Context, traceback: bool) -> None:
152
- """Root Click group that primes shared configuration state.
153
-
154
- Why:
155
- Accept a single ``--traceback`` flag that determines whether downstream
156
- helpers emit stack traces.
157
- Parameters:
158
- ctx: Click context object for the current invocation.
159
- traceback: When ``True`` enables traceback output for subsequent commands.
160
- Side Effects:
161
- Mutates ``ctx.obj`` and :data:`lib_cli_exit_tools.config.traceback`.
162
- Examples:
163
- >>> from click.testing import CliRunner
164
- >>> runner = CliRunner()
165
- >>> result = runner.invoke(cli, ["--help"])
166
- >>> result.exit_code == 0
167
- True
168
- """
169
- _store_traceback_flag(ctx, traceback)
170
- lib_cli_exit_tools.config.traceback = traceback
171
-
172
-
173
- def _store_traceback_flag(ctx: click.Context, traceback: bool) -> None:
174
- """Store traceback flag in typed context state.
175
-
176
- Parameters:
177
- ctx: Click context object for the current invocation.
178
- traceback: Whether to show full Python tracebacks on errors.
179
-
180
- Side Effects:
181
- Initializes ctx.obj as CliContextState if not already set and updates
182
- the traceback field.
183
- """
184
- ctx.ensure_object(CliContextState)
185
- # Type narrowing: ctx.obj is guaranteed to be CliContextState after ensure_object
186
- state = ctx.obj
187
- if isinstance(state, CliContextState): # nosec B101 - type guard, not assertion
188
- state.traceback = traceback
189
-
190
-
191
- @cli.command("info", context_settings=CLICK_CONTEXT_SETTINGS)
192
- def cli_info() -> None:
193
- """Display package metadata exported by :mod:`lib_cli_exit_tools.__init__conf__`.
194
-
195
- Why:
196
- Offer a zero-dependency way for users to confirm the installed version
197
- and provenance of the CLI.
198
- Side Effects:
199
- Writes formatted metadata to stdout.
200
- Examples:
201
- >>> from click.testing import CliRunner
202
- >>> runner = CliRunner()
203
- >>> result = runner.invoke(cli, ["info"])
204
- >>> "Info for" in result.output
205
- True
206
- """
207
- __init__conf__.print_info()
208
-
209
-
210
- @cli.command("fail", context_settings=CLICK_CONTEXT_SETTINGS)
211
- def cli_fail() -> None:
212
- """Intentionally raise a failure to validate error handling.
213
-
214
- Why:
215
- Exercise the error-reporting path so engineers can confirm exit-code
216
- translation and optional traceback behaviour without crafting custom
217
- failing commands.
218
- Side Effects:
219
- Delegates to :func:`lib_cli_exit_tools.i_should_fail`, which raises
220
- ``RuntimeError`` for the caller to handle.
221
- Examples:
222
- >>> from click.testing import CliRunner
223
- >>> runner = CliRunner()
224
- >>> result = runner.invoke(cli, ["fail"])
225
- >>> result.exit_code != 0
226
- True
227
- >>> "i should fail" in result.output or "i should fail" in result.stderr
228
- True
229
- """
230
-
231
- lib_cli_exit_tools.i_should_fail()
232
-
233
-
234
- def main(argv: Sequence[str] | None = None) -> int:
235
- """Run the CLI with :func:`lib_cli_exit_tools.run_cli` wiring.
236
-
237
- Why:
238
- Serve as the target for console scripts and ``python -m`` execution by
239
- returning an integer exit code instead of exiting directly.
240
- Parameters:
241
- argv: Optional iterable of arguments passed to Click (without program name).
242
- Returns:
243
- Integer exit code from :func:`lib_cli_exit_tools.run_cli`.
244
- Side Effects:
245
- Delegates to Click and may write to stdout/stderr.
246
- Examples:
247
- >>> import contextlib, io
248
- >>> buffer = io.StringIO()
249
- >>> with contextlib.redirect_stdout(buffer):
250
- ... exit_code = main(["info"])
251
- >>> exit_code
252
- 0
253
- >>> "Info for" in buffer.getvalue()
254
- True
255
- """
256
- with _temporary_rich_click_configuration():
257
- return lib_cli_exit_tools.run_cli(
258
- cli,
259
- argv=list(argv) if argv is not None else None,
260
- prog_name=__init__conf__.shell_command,
261
- )
@@ -1,18 +0,0 @@
1
- lib_cli_exit_tools/__init__.py,sha256=Z-VXcrmjZCgJZ8NW0UCVIc0HBOsjQicCbzgHj7r22eA,1867
2
- lib_cli_exit_tools/__init__conf__.py,sha256=8b5wcSJwywaIvuqYoosuL3G0mpUqQ8kuVjpp67wdON4,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.4.dist-info/METADATA,sha256=RzJ0PBuOrRXwpWqnJkyjrd-mNlHxqi6jL2Nro9q2zoc,26234
15
- lib_cli_exit_tools-2.2.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
16
- lib_cli_exit_tools-2.2.4.dist-info/entry_points.txt,sha256=8R-_3UT0KcQ7HQ7idDx6rlG77GhHb0E2N5QnN41dj-M,161
17
- lib_cli_exit_tools-2.2.4.dist-info/licenses/LICENSE,sha256=6s9SL2InRrwynPjJYiER7ONHBshbGj_yC5EBzfo7l9I,1072
18
- lib_cli_exit_tools-2.2.4.dist-info/RECORD,,