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

1"""Init command for Lintro. 

2 

3Creates configuration files for Lintro and optionally native tool configs. 

4""" 

5 

6import json 

7from collections.abc import Mapping 

8from pathlib import Path 

9from typing import Any 

10 

11import click 

12from loguru import logger 

13from rich.console import Console 

14from rich.panel import Panel 

15 

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 

29 

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" 

35 

36execution: 

37 enabled_tools: [] 

38 tool_order: "priority" 

39 fail_fast: false 

40 

41defaults: 

42 mypy: 

43 strict: true 

44 ignore_missing_imports: true 

45 

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""" 

64 

65MINIMAL_CONFIG_TEMPLATE = """\ 

66# Lintro Configuration (Minimal) 

67# https://github.com/lgtm-hq/py-lintro 

68 

69enforce: 

70 line_length: 88 

71 target_python: "py313" 

72 

73defaults: 

74 mypy: 

75 strict: true 

76 ignore_missing_imports: true 

77 

78execution: 

79 tool_order: "priority" 

80 

81tools: 

82 ruff: 

83 enabled: true 

84 black: 

85 enabled: true 

86 mypy: 

87 enabled: true 

88""" 

89 

90# Native config templates 

91MARKDOWNLINT_TEMPLATE = { 

92 "config": { 

93 "MD013": { 

94 "line_length": 88, 

95 "code_blocks": False, 

96 "tables": False, 

97 }, 

98 }, 

99} 

100 

101 

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. 

109 

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. 

115 

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 

122 

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 

130 

131 

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. 

139 

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. 

145 

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) 

151 

152 

153def _generate_native_configs( 

154 console: Console, 

155 force: bool, 

156) -> list[str]: 

157 """Generate native tool configuration files. 

158 

159 Args: 

160 console: Rich console for output. 

161 force: Whether to overwrite existing files. 

162 

163 Returns: 

164 list[str]: List of created file names. 

165 """ 

166 created: list[str] = [] 

167 

168 console.print("\n[bold cyan]Generating native tool configs:[/bold cyan]") 

169 

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") 

178 

179 return created 

180 

181 

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. 

214 

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. 

218 

219 Use --with-native-configs to also generate native tool configuration 

220 files for IDE integration (e.g., markdownlint extension). 

221 

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. 

227 

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] = [] 

234 

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) 

242 

243 # Select template 

244 template = MINIMAL_CONFIG_TEMPLATE if minimal else DEFAULT_CONFIG_TEMPLATE 

245 

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()}") 

251 

252 except OSError as e: 

253 console.print(f"[red]Error: Failed to write {output_path}: {e}[/red]") 

254 raise SystemExit(1) from e 

255 

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) 

260 

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 ) 

279 

280 console.print() 

281 

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 )