Coverage for lintro / cli_utils / commands / init.py: 91%
67 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"""Init command for Lintro.
3Creates configuration files for Lintro and optionally native tool configs.
4"""
6import json
7from collections.abc import Mapping
8from pathlib import Path
9from typing import Any
11import click
12from loguru import logger
13from rich.console import Console
14from rich.panel import Panel
16# Default Lintro config template (project-recommended defaults)
17DEFAULT_CONFIG_TEMPLATE = """\
18# Lintro Configuration
19# https://github.com/lgtm-hq/py-lintro
20#
21# Lintro acts as the master configuration source for all tools.
22# Native tool configs are ignored by default unless
23# explicitly referenced via config_source.
24#
25# enforce: Cross-cutting settings injected via CLI flags
26# execution: What tools run and how
27# defaults: Fallback config when no native config exists
28# tools: Per-tool enable/disable and optional config source
30enforce:
31 # Applied to ruff/black and other tools that honor line length
32 line_length: 88
33 # Aligns with project requires-python (pyproject.toml)
34 target_python: "py313"
36execution:
37 enabled_tools: []
38 tool_order: "priority"
39 fail_fast: false
41defaults:
42 mypy:
43 strict: true
44 ignore_missing_imports: true
46tools:
47 ruff:
48 enabled: true
49 black:
50 enabled: true
51 mypy:
52 enabled: true
53 markdownlint:
54 enabled: true
55 yamllint:
56 enabled: true
57 bandit:
58 enabled: true
59 hadolint:
60 enabled: true
61 actionlint:
62 enabled: true
63"""
65MINIMAL_CONFIG_TEMPLATE = """\
66# Lintro Configuration (Minimal)
67# https://github.com/lgtm-hq/py-lintro
69enforce:
70 line_length: 88
71 target_python: "py313"
73defaults:
74 mypy:
75 strict: true
76 ignore_missing_imports: true
78execution:
79 tool_order: "priority"
81tools:
82 ruff:
83 enabled: true
84 black:
85 enabled: true
86 mypy:
87 enabled: true
88"""
90# Native config templates
91MARKDOWNLINT_TEMPLATE = {
92 "config": {
93 "MD013": {
94 "line_length": 88,
95 "code_blocks": False,
96 "tables": False,
97 },
98 },
99}
102def _write_file(
103 path: Path,
104 content: str,
105 console: Console,
106 force: bool,
107) -> bool:
108 """Write content to a file, handling existing files.
110 Args:
111 path: Path to write to.
112 content: Content to write.
113 console: Rich console for output.
114 force: Whether to overwrite existing files.
116 Returns:
117 bool: True if file was written, False if skipped.
118 """
119 if path.exists() and not force:
120 console.print(f" [yellow]⏭️ Skipped {path} (already exists)[/yellow]")
121 return False
123 try:
124 path.write_text(content, encoding="utf-8")
125 console.print(f" [green]✅ Created {path}[/green]")
126 return True
127 except OSError as e:
128 console.print(f" [red]❌ Failed to write {path}: {e}[/red]")
129 return False
132def _write_json_file(
133 path: Path,
134 data: Mapping[str, Any],
135 console: Console,
136 force: bool,
137) -> bool:
138 """Write JSON content to a file, handling existing files.
140 Args:
141 path: Path to write to.
142 data: Dictionary to serialize as JSON.
143 console: Rich console for output.
144 force: Whether to overwrite existing files.
146 Returns:
147 bool: True if file was written, False if skipped.
148 """
149 content = json.dumps(obj=data, indent=2) + "\n"
150 return _write_file(path=path, content=content, console=console, force=force)
153def _generate_native_configs(
154 console: Console,
155 force: bool,
156) -> list[str]:
157 """Generate native tool configuration files.
159 Args:
160 console: Rich console for output.
161 force: Whether to overwrite existing files.
163 Returns:
164 list[str]: List of created file names.
165 """
166 created: list[str] = []
168 console.print("\n[bold cyan]Generating native tool configs:[/bold cyan]")
170 # Markdownlint config
171 if _write_json_file(
172 path=Path(".markdownlint-cli2.jsonc"),
173 data=MARKDOWNLINT_TEMPLATE,
174 console=console,
175 force=force,
176 ):
177 created.append(".markdownlint-cli2.jsonc")
179 return created
182@click.command("init")
183@click.option(
184 "--minimal",
185 "-m",
186 is_flag=True,
187 help="Create a minimal config file with fewer comments.",
188)
189@click.option(
190 "--force",
191 "-f",
192 is_flag=True,
193 help="Overwrite existing configuration files.",
194)
195@click.option(
196 "--output",
197 "-o",
198 type=click.Path(),
199 default=".lintro-config.yaml",
200 help="Output file path (default: .lintro-config.yaml).",
201)
202@click.option(
203 "--with-native-configs",
204 is_flag=True,
205 help="Also generate native tool configs (.markdownlint-cli2.jsonc, etc.).",
206)
207def init_command(
208 minimal: bool,
209 force: bool,
210 output: str,
211 with_native_configs: bool,
212) -> None:
213 """Initialize Lintro configuration for your project.
215 Creates a scaffold configuration file with sensible defaults.
216 Lintro will use this file as the master configuration source,
217 ignoring native tool configs unless explicitly referenced.
219 Use --with-native-configs to also generate native tool configuration
220 files for IDE integration (e.g., markdownlint extension).
222 Args:
223 minimal: Use minimal template with fewer comments.
224 force: Overwrite existing config file if it exists.
225 output: Output file path for the config file.
226 with_native_configs: Also generate native tool config files.
228 Raises:
229 SystemExit: If file exists and --force not provided, or write fails.
230 """
231 console = Console()
232 output_path = Path(output)
233 created_files: list[str] = []
235 # Check if main config file already exists
236 if output_path.exists() and not force:
237 console.print(
238 f"[red]Error: {output_path} already exists. "
239 "Use --force to overwrite.[/red]",
240 )
241 raise SystemExit(1)
243 # Select template
244 template = MINIMAL_CONFIG_TEMPLATE if minimal else DEFAULT_CONFIG_TEMPLATE
246 # Write main config file
247 try:
248 output_path.write_text(template, encoding="utf-8")
249 created_files.append(str(output_path))
250 logger.debug(f"Created config file: {output_path.resolve()}")
252 except OSError as e:
253 console.print(f"[red]Error: Failed to write {output_path}: {e}[/red]")
254 raise SystemExit(1) from e
256 # Generate native configs if requested
257 if with_native_configs:
258 native_files = _generate_native_configs(console=console, force=force)
259 created_files.extend(native_files)
261 # Success panel
262 console.print()
263 if len(created_files) == 1:
264 console.print(
265 Panel.fit(
266 f"[bold green]✅ Created {output_path}[/bold green]",
267 border_style="green",
268 ),
269 )
270 else:
271 files_list = "\n".join(f" • {f}" for f in created_files)
272 msg = f"[bold green]✅ Created {len(created_files)} files:[/bold green]"
273 console.print(
274 Panel.fit(
275 f"{msg}\n{files_list}",
276 border_style="green",
277 ),
278 )
280 console.print()
282 # Next steps
283 console.print("[bold cyan]Next steps:[/bold cyan]")
284 console.print(" [dim]1.[/dim] Review and customize the configuration")
285 console.print(
286 " [dim]2.[/dim] Run [cyan]lintro config[/cyan] to view config",
287 )
288 console.print(" [dim]3.[/dim] Run [cyan]lintro check .[/cyan] to lint")
289 if with_native_configs:
290 console.print(
291 " [dim]4.[/dim] Commit the config files to your repository",
292 )