Coverage for lintro / cli_utils / commands / config.py: 83%
140 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Config command for displaying Lintro configuration status."""
3from dataclasses import asdict
4from pathlib import Path
5from typing import Any, cast
7import click
8from rich.console import Console
9from rich.panel import Panel
10from rich.table import Table
12from lintro.config import LintroConfig, get_config
13from lintro.utils.unified_config import (
14 _load_native_tool_config,
15 get_ordered_tools,
16 get_tool_priority,
17 is_tool_injectable,
18 validate_config_consistency,
19)
22def _get_all_tool_names() -> list[str]:
23 """Get list of all registered tool names.
25 Dynamically retrieves tool names from the plugin registry to ensure
26 all tools are included without manual maintenance.
28 Returns:
29 list[str]: Sorted list of tool names.
30 """
31 from lintro.plugins.registry import ToolRegistry
33 return ToolRegistry.get_names()
36@click.command()
37@click.option(
38 "--verbose",
39 "-v",
40 is_flag=True,
41 help="Show detailed configuration including native tool configs.",
42)
43@click.option(
44 "--json",
45 "json_output",
46 is_flag=True,
47 help="Output configuration as JSON.",
48)
49@click.option(
50 "--export",
51 "export_path",
52 type=click.Path(),
53 help="Export effective configuration as a .lintro-config.yaml file.",
54)
55def config_command(
56 verbose: bool,
57 json_output: bool,
58 export_path: str | None,
59) -> None:
60 """Display Lintro configuration status.
62 Shows the unified configuration for all tools including:
63 - Config source (.lintro-config.yaml or pyproject.toml)
64 - Global settings (line_length, tool ordering strategy)
65 - Tool execution order based on configured strategy
66 - Per-tool effective configuration
67 - Configuration warnings and inconsistencies
69 Args:
70 verbose: Show detailed configuration including native tool configs.
71 json_output: Output configuration as JSON.
72 export_path: Path to export effective configuration as YAML file.
73 """
74 console = Console()
75 config = get_config(reload=True)
77 if export_path:
78 _export_yaml(config=config, export_path=export_path, console=console)
79 return
81 if json_output:
82 _output_json(config=config, verbose=verbose)
83 return
85 _output_rich(
86 console=console,
87 config=config,
88 verbose=verbose,
89 )
92def _config_to_export_dict(config: LintroConfig) -> dict[str, Any]:
93 """Convert LintroConfig to a YAML-friendly dictionary.
95 Args:
96 config: The LintroConfig instance to convert.
98 Returns:
99 dict[str, Any]: Dictionary suitable for YAML serialization.
100 """
101 tools = {name: asdict(cfg) for name, cfg in config.tools.items()}
102 return {
103 "enforce": asdict(config.enforce),
104 "execution": asdict(config.execution),
105 "defaults": config.defaults,
106 "tools": tools,
107 }
110def _export_yaml(
111 config: LintroConfig,
112 export_path: str,
113 console: Console,
114) -> None:
115 """Export the effective configuration as YAML.
117 Args:
118 config: Loaded configuration.
119 export_path: Destination file path.
120 console: Rich console for user feedback.
122 Raises:
123 SystemExit: If PyYAML is not installed.
124 """
125 try:
126 import yaml
127 except ImportError as exc: # pragma: no cover - enforced by packaging
128 console.print(
129 "[red]PyYAML is required to export configuration. "
130 "Install it with `pip install pyyaml`.[/red]",
131 )
132 raise SystemExit(1) from exc
134 export_file = Path(export_path)
135 data = _config_to_export_dict(config)
136 header = "# Generated by `lintro config --export`\n"
137 export_file.write_text(
138 header + yaml.safe_dump(data, sort_keys=False),
139 encoding="utf-8",
140 )
141 console.print(f"[green]Exported configuration to {export_file}[/green]")
144def _output_json(
145 config: LintroConfig,
146 verbose: bool = False,
147) -> None:
148 """Output configuration as JSON.
150 Args:
151 config: LintroConfig instance from get_config()
152 verbose: Include native configs in output when True
153 """
154 import json
156 # Get tool order settings
157 tool_order = config.execution.tool_order
158 if isinstance(tool_order, list):
159 order_strategy = "custom"
160 custom_order = tool_order
161 else:
162 order_strategy = tool_order or "priority"
163 custom_order = []
165 # Get list of all known tools
166 tool_names = _get_all_tool_names()
167 ordered_tools = get_ordered_tools(
168 tool_names=tool_names,
169 tool_order=config.execution.tool_order,
170 )
172 output: dict[str, Any] = {
173 "config_source": config.config_path or "defaults",
174 "global_settings": {
175 "line_length": config.enforce.line_length,
176 "target_python": config.enforce.target_python,
177 "tool_order": order_strategy,
178 "custom_order": custom_order,
179 },
180 "execution": {
181 "enabled_tools": config.execution.enabled_tools or "all",
182 "fail_fast": config.execution.fail_fast,
183 "parallel": config.execution.parallel,
184 },
185 "tool_execution_order": [
186 {"tool": t, "priority": get_tool_priority(t)} for t in ordered_tools
187 ],
188 "tool_configs": {},
189 "warnings": validate_config_consistency(),
190 }
192 tool_configs = cast(dict[str, Any], output["tool_configs"])
194 for tool_name in tool_names:
195 tool_config = config.get_tool_config(tool_name)
196 effective_ll = config.get_effective_line_length(tool_name)
198 tool_output: dict[str, Any] = {
199 "enabled": tool_config.enabled,
200 "is_injectable": is_tool_injectable(tool_name),
201 "effective_line_length": effective_ll,
202 "config_source": tool_config.config_source,
203 }
204 if verbose:
205 native = _load_native_tool_config(tool_name)
206 tool_output["native_config"] = native if native else None
207 tool_output["defaults"] = config.get_tool_defaults(tool_name) or None
209 tool_configs[tool_name] = tool_output
211 print(json.dumps(output, indent=2))
214def _output_rich(
215 console: Console,
216 config: LintroConfig,
217 verbose: bool,
218) -> None:
219 """Output configuration using Rich formatting.
221 Args:
222 console: Rich Console instance
223 config: LintroConfig instance from get_config()
224 verbose: Whether to show verbose output
225 """
226 # Header panel
227 console.print(
228 Panel.fit(
229 "[bold cyan]Lintro Configuration Report[/bold cyan]",
230 border_style="cyan",
231 ),
232 )
233 console.print()
235 # Config Source Section
236 config_source = config.config_path or "[dim]No config file (using defaults)[/dim]"
237 console.print(f"[bold]Config Source:[/bold] {config_source}")
238 console.print()
240 # Global Settings Section
241 global_table = Table(
242 title="Enforce Settings",
243 show_header=False,
244 box=None,
245 )
246 global_table.add_column("Setting", style="cyan", width=25)
247 global_table.add_column("Value", style="yellow")
249 line_length = config.enforce.line_length
250 global_table.add_row(
251 "line_length",
252 str(line_length) if line_length else "[dim]Not configured[/dim]",
253 )
255 target_python = config.enforce.target_python
256 global_table.add_row(
257 "target_python",
258 target_python if target_python else "[dim]Not configured[/dim]",
259 )
261 console.print(global_table)
262 console.print()
264 # Execution Settings Section
265 exec_table = Table(
266 title="Execution Settings",
267 show_header=False,
268 box=None,
269 )
270 exec_table.add_column("Setting", style="cyan", width=25)
271 exec_table.add_column("Value", style="yellow")
273 tool_order = config.execution.tool_order
274 if isinstance(tool_order, list):
275 order_strategy = "custom"
276 exec_table.add_row("tool_order", order_strategy)
277 exec_table.add_row("custom_order", ", ".join(tool_order))
278 else:
279 exec_table.add_row("tool_order", tool_order or "priority")
281 enabled_tools = config.execution.enabled_tools
282 exec_table.add_row(
283 "enabled_tools",
284 ", ".join(enabled_tools) if enabled_tools else "[dim]all[/dim]",
285 )
286 exec_table.add_row("fail_fast", str(config.execution.fail_fast))
287 exec_table.add_row("parallel", str(config.execution.parallel))
289 console.print(exec_table)
290 console.print()
292 # Tool Execution Order Section
293 tool_names = _get_all_tool_names()
294 ordered_tools = get_ordered_tools(
295 tool_names=tool_names,
296 tool_order=config.execution.tool_order,
297 )
299 order_table = Table(title="Tool Execution Order")
300 order_table.add_column("#", style="dim", justify="right", width=3)
301 order_table.add_column("Tool", style="cyan")
302 order_table.add_column("Priority", justify="center", style="yellow")
303 order_table.add_column("Type", style="green")
304 order_table.add_column("Enabled", justify="center")
306 for idx, tool_name in enumerate(ordered_tools, 1):
307 priority = get_tool_priority(tool_name)
308 injectable = is_tool_injectable(tool_name)
309 tool_type = "Syncable" if injectable else "Native only"
310 enabled = config.is_tool_enabled(tool_name)
311 enabled_display = "[green]✓[/green]" if enabled else "[red]✗[/red]"
313 order_table.add_row(
314 str(idx),
315 tool_name,
316 str(priority),
317 tool_type,
318 enabled_display,
319 )
321 console.print(order_table)
322 console.print()
324 # Per-Tool Configuration Section
325 config_table = Table(title="Per-Tool Configuration")
326 config_table.add_column("Tool", style="cyan")
327 config_table.add_column("Sync Status", justify="center")
328 config_table.add_column("Line Length", justify="center", style="yellow")
329 config_table.add_column("Config Source", style="dim")
331 if verbose:
332 config_table.add_column("Native Config", style="dim")
334 for tool_name in tool_names:
335 tool_config = config.get_tool_config(tool_name)
336 injectable = is_tool_injectable(tool_name)
337 status = (
338 "[green]✓ Syncable[/green]"
339 if injectable
340 else "[yellow]⚠ Native only[/yellow]"
341 )
342 effective_ll = config.get_effective_line_length(tool_name)
343 ll_display = str(effective_ll) if effective_ll else "[dim]default[/dim]"
345 cfg_source = tool_config.config_source or "[dim]auto[/dim]"
347 row = [tool_name, status, ll_display, cfg_source]
349 if verbose:
350 native = _load_native_tool_config(tool_name)
351 native_cfg = str(native) if native else "[dim]None[/dim]"
352 row.append(native_cfg)
354 config_table.add_row(*row)
356 console.print(config_table)
357 console.print()
359 # Warnings Section
360 warnings = validate_config_consistency()
361 if warnings:
362 console.print("[bold red]Configuration Warnings[/bold red]")
363 for warning in warnings:
364 console.print(f" [yellow]⚠️[/yellow] {warning}")
365 console.print()
366 console.print(
367 "[dim]Tools marked 'Native only' cannot be configured by Lintro. "
368 "Update their config files manually for consistency.[/dim]",
369 )
370 else:
371 console.print(
372 "[green]✅ All configurations are consistent![/green]",
373 )
375 console.print()
377 # Help text
378 console.print(
379 "[dim]Configure Lintro in .lintro-config.yaml:[/dim]",
380 )
381 console.print(
382 "[dim] Run 'lintro init' to create a config file[/dim]",
383 )
384 console.print(
385 '[dim] tool_order: "priority" | "alphabetical" | ["tool1", "tool2"][/dim]',
386 )