Coverage for / home / runner / work / bijux-cli / bijux-cli / src / bijux_cli / core / exit_policy.py: 93%
48 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-01-26 17:59 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-01-26 17:59 +0000
1# SPDX-License-Identifier: Apache-2.0
2# Copyright © 2025 Bijan Mousavi
4"""Formal error-to-exit behavior mapping."""
6from __future__ import annotations
8from dataclasses import dataclass
9from typing import TYPE_CHECKING, Any
11from bijux_cli.core.enums import ErrorType, ExitCode, OutputFormat
13if TYPE_CHECKING:
14 from bijux_cli.core.precedence import LogPolicy
17@dataclass(frozen=True)
18class ExitBehavior:
19 """Defines how an error should exit and where it should emit."""
21 code: ExitCode
22 stream: str | None
23 show_traceback: bool
26@dataclass(frozen=True)
27class ExitIntent:
28 """Represents an execution intent for exiting with optional output."""
30 code: ExitCode
31 stream: str | None
32 payload: Any | None
33 fmt: OutputFormat
34 pretty: bool
35 show_traceback: bool
38class ExitIntentError(Exception):
39 """Raised to signal an exit intent without performing the exit."""
41 def __init__(self, intent: ExitIntent) -> None:
42 """Store the exit intent payload."""
43 super().__init__("exit_intent")
44 self.intent = intent
47_BASE_BEHAVIOR: dict[ErrorType, ExitBehavior] = {
48 ErrorType.USAGE: ExitBehavior(ExitCode.USAGE, "stdout", False),
49 ErrorType.ASCII: ExitBehavior(ExitCode.ASCII, "stderr", False),
50 ErrorType.USER_INPUT: ExitBehavior(ExitCode.USAGE, "stderr", False),
51 ErrorType.PLUGIN: ExitBehavior(ExitCode.ERROR, "stderr", True),
52 ErrorType.CONFIG: ExitBehavior(ExitCode.ERROR, "stderr", False),
53 ErrorType.INTERNAL: ExitBehavior(ExitCode.ERROR, "stderr", True),
54 ErrorType.ABORTED: ExitBehavior(ExitCode.ABORTED, "stderr", False),
55}
56_EXPECTED_ERROR_TYPES = set(ErrorType)
57if set(_BASE_BEHAVIOR) != _EXPECTED_ERROR_TYPES: 57 ↛ 58line 57 didn't jump to line 58 because the condition on line 57 was never true
58 missing = _EXPECTED_ERROR_TYPES - set(_BASE_BEHAVIOR)
59 extra = set(_BASE_BEHAVIOR) - _EXPECTED_ERROR_TYPES
60 raise RuntimeError(
61 f"Exit policy is incomplete. missing={sorted(missing)} extra={sorted(extra)}"
62 )
65def resolve_exit_behavior(
66 error_type: ErrorType,
67 *,
68 quiet: bool,
69 fmt: OutputFormat,
70 log_policy: LogPolicy,
71) -> ExitBehavior:
72 """Return the exit behavior for a given error type and output context."""
73 _ = fmt
74 base = _BASE_BEHAVIOR[error_type]
75 show_traceback = base.show_traceback and log_policy.show_traceback
76 if quiet:
77 return ExitBehavior(base.code, None, show_traceback)
78 return ExitBehavior(base.code, base.stream, show_traceback)
81def resolve_error_type(code: int, explicit: ErrorType | None = None) -> ErrorType:
82 """Resolve an error type from an explicit override or exit code."""
83 if explicit is not None:
84 return explicit
85 if code == ExitCode.USAGE:
86 return ErrorType.USAGE
87 if code == ExitCode.ASCII:
88 return ErrorType.ASCII
89 if code == ExitCode.ABORTED:
90 return ErrorType.ABORTED
91 return ErrorType.INTERNAL
94def resolve_error_behavior(
95 code: int,
96 *,
97 quiet: bool,
98 fmt: OutputFormat,
99 log_policy: LogPolicy,
100 error_type: ErrorType | None = None,
101) -> ExitBehavior:
102 """Resolve exit behavior for an error code and context."""
103 resolved_type = resolve_error_type(code, error_type)
104 return resolve_exit_behavior(
105 resolved_type, quiet=quiet, fmt=fmt, log_policy=log_policy
106 )
109__all__ = [
110 "ExitBehavior",
111 "ExitIntent",
112 "ExitIntentError",
113 "resolve_error_type",
114 "resolve_error_behavior",
115 "resolve_exit_behavior",
116]