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

1# SPDX-License-Identifier: Apache-2.0 

2# Copyright © 2025 Bijan Mousavi 

3 

4"""Formal error-to-exit behavior mapping.""" 

5 

6from __future__ import annotations 

7 

8from dataclasses import dataclass 

9from typing import TYPE_CHECKING, Any 

10 

11from bijux_cli.core.enums import ErrorType, ExitCode, OutputFormat 

12 

13if TYPE_CHECKING: 

14 from bijux_cli.core.precedence import LogPolicy 

15 

16 

17@dataclass(frozen=True) 

18class ExitBehavior: 

19 """Defines how an error should exit and where it should emit.""" 

20 

21 code: ExitCode 

22 stream: str | None 

23 show_traceback: bool 

24 

25 

26@dataclass(frozen=True) 

27class ExitIntent: 

28 """Represents an execution intent for exiting with optional output.""" 

29 

30 code: ExitCode 

31 stream: str | None 

32 payload: Any | None 

33 fmt: OutputFormat 

34 pretty: bool 

35 show_traceback: bool 

36 

37 

38class ExitIntentError(Exception): 

39 """Raised to signal an exit intent without performing the exit.""" 

40 

41 def __init__(self, intent: ExitIntent) -> None: 

42 """Store the exit intent payload.""" 

43 super().__init__("exit_intent") 

44 self.intent = intent 

45 

46 

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 ) 

63 

64 

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) 

79 

80 

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 

92 

93 

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 ) 

107 

108 

109__all__ = [ 

110 "ExitBehavior", 

111 "ExitIntent", 

112 "ExitIntentError", 

113 "resolve_error_type", 

114 "resolve_error_behavior", 

115 "resolve_exit_behavior", 

116]