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
« 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"""Pure CLI parsing and intent construction."""
6from __future__ import annotations
8from collections.abc import Mapping
9from dataclasses import dataclass
10import os
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)
34@dataclass(frozen=True)
35class CLIIntent:
36 """Resolved, side-effect-free CLI intent."""
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, ...]
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 )
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 )
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, []
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)
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 )
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 )
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 )
162def current_cli_intent() -> CLIIntent:
163 """Resolve the current CLI intent from DI or fallback to argv/env."""
164 import sys
166 from bijux_cli.core.di import DIContainer
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 )
182__all__ = [
183 "CLIIntent",
184 "build_cli_intent",
185 "current_cli_intent",
186 "parse_global_config",
187 "split_command_args",
188]