Coverage for  / home / runner / work / bijux-cli / bijux-cli / src / bijux_cli / core / intent.py: 96%

69 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"""Pure CLI parsing and intent construction.""" 

5 

6from __future__ import annotations 

7 

8from collections.abc import Mapping 

9from dataclasses import dataclass 

10import os 

11 

12from bijux_cli.cli.color import resolve_color_mode 

13from bijux_cli.cli.core.constants import ( 

14 ENV_COLOR, 

15 ENV_LOG_LEVEL, 

16 ENV_NO_COLOR, 

17 OPT_HELP, 

18) 

19from bijux_cli.cli.core.flags import collect_global_flag_errors, parse_global_flags 

20from bijux_cli.core.enums import ColorMode, LogLevel, OutputFormat 

21from bijux_cli.core.precedence import ( 

22 EffectiveConfig, 

23 FlagError, 

24 FlagLayer, 

25 Flags, 

26 GlobalCLIConfig, 

27 LogPolicy, 

28 resolve_effective_config, 

29 resolve_log_policy, 

30 validate_cli_flags, 

31) 

32 

33 

34@dataclass(frozen=True) 

35class CLIIntent: 

36 """Resolved, side-effect-free CLI intent.""" 

37 

38 command: str | None 

39 args: tuple[str, ...] 

40 flags: Flags 

41 output_format: OutputFormat 

42 log_level: LogLevel 

43 quiet: bool 

44 color: ColorMode 

45 pretty: bool 

46 include_runtime: bool 

47 log_policy: LogPolicy 

48 help: bool 

49 errors: tuple[FlagError, ...] 

50 

51 

52def parse_global_config(argv: list[str]) -> GlobalCLIConfig: 

53 """Parse global CLI flags once at the CLI root layer.""" 

54 flags = parse_global_flags(argv) 

55 help_flag = any(arg in OPT_HELP for arg in argv) 

56 errors = () if help_flag else collect_global_flag_errors(argv) 

57 return GlobalCLIConfig( 

58 help=help_flag, 

59 flags=FlagLayer( 

60 quiet=flags.quiet, 

61 log_level=flags.log_level, 

62 color=flags.color, 

63 format=flags.format, 

64 ), 

65 args=tuple(argv), 

66 errors=errors, 

67 ) 

68 

69 

70def split_command_args(args: list[str]) -> tuple[str | None, list[str]]: 

71 """Return the first command token and remaining args.""" 

72 from bijux_cli.cli.core.constants import ( 

73 OPT_COLOR, 

74 OPT_FORMAT, 

75 OPT_LOG_LEVEL, 

76 OPT_QUIET, 

77 PRETTY_FLAGS, 

78 ) 

79 

80 flags_with_values = {*OPT_FORMAT, *OPT_LOG_LEVEL, *OPT_COLOR} 

81 flags_no_values = {*OPT_QUIET, *PRETTY_FLAGS, *OPT_HELP} 

82 i = 0 

83 while i < len(args): 

84 arg = args[i] 

85 if arg in flags_with_values: 

86 i += 2 

87 continue 

88 if arg in flags_no_values or arg.startswith("-"): 

89 i += 1 

90 continue 

91 return arg, args[i + 1 :] 

92 return None, [] 

93 

94 

95def build_cli_intent( 

96 args: list[str], 

97 *, 

98 env: Mapping[str, str] | None = None, 

99 tty: bool = True, 

100) -> CLIIntent: 

101 """Build an intent from CLI args and supplied environment.""" 

102 env = env or os.environ 

103 parsed = parse_global_config(args) 

104 errors = validate_cli_flags(parsed) 

105 

106 env_log = env.get(ENV_LOG_LEVEL) 

107 env_color = env.get(ENV_COLOR) 

108 effective = resolve_effective_config( 

109 cli=parsed.flags, 

110 env=FlagLayer( 

111 log_level=LogLevel(env_log) if env_log else None, 

112 color=ColorMode(env_color) if env_color else None, 

113 ), 

114 file=FlagLayer(), 

115 defaults=Flags( 

116 quiet=False, 

117 log_level=LogLevel.INFO, 

118 color=ColorMode.AUTO, 

119 format=OutputFormat.JSON, 

120 ), 

121 ) 

122 

123 color_config = GlobalCLIConfig( 

124 help=parsed.help, 

125 flags=FlagLayer(color=effective.flags.color), 

126 args=parsed.args, 

127 errors=parsed.errors, 

128 ) 

129 resolved_color = resolve_color_mode( 

130 color_config, 

131 tty, 

132 no_color=env.get(ENV_NO_COLOR) == "1", 

133 ) 

134 if resolved_color != effective.flags.color: 134 ↛ 144line 134 didn't jump to line 144 because the condition on line 134 was always true

135 effective = EffectiveConfig( 

136 flags=Flags( 

137 quiet=effective.flags.quiet, 

138 log_level=effective.flags.log_level, 

139 color=resolved_color, 

140 format=effective.flags.format, 

141 ) 

142 ) 

143 

144 log_policy = resolve_log_policy(effective.flags.log_level) 

145 command, _ = split_command_args(args) 

146 return CLIIntent( 

147 command=command, 

148 args=tuple(args), 

149 flags=effective.flags, 

150 output_format=effective.flags.format, 

151 log_level=effective.flags.log_level, 

152 quiet=effective.flags.quiet, 

153 color=effective.flags.color, 

154 pretty=log_policy.pretty_default, 

155 include_runtime=log_policy.show_internal, 

156 log_policy=log_policy, 

157 help=parsed.help, 

158 errors=tuple(errors), 

159 ) 

160 

161 

162def current_cli_intent() -> CLIIntent: 

163 """Resolve the current CLI intent from DI or fallback to argv/env.""" 

164 import sys 

165 

166 from bijux_cli.core.di import DIContainer 

167 

168 try: 

169 intent: object = DIContainer.current().resolve(CLIIntent) 

170 except Exception: 

171 intent = None 

172 if isinstance(intent, CLIIntent): 172 ↛ 173line 172 didn't jump to line 173 because the condition on line 172 was never true

173 return intent 

174 args = [] if os.environ.get("PYTEST_CURRENT_TEST") else list(sys.argv[1:]) 

175 return build_cli_intent( 

176 args, 

177 env=os.environ, 

178 tty=sys.stdout.isatty(), 

179 ) 

180 

181 

182__all__ = [ 

183 "CLIIntent", 

184 "build_cli_intent", 

185 "current_cli_intent", 

186 "parse_global_config", 

187 "split_command_args", 

188]