Coverage for src / taipanstack / config / generators.py: 100%

51 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-23 14:54 +0000

1""" 

2Configuration file generators. 

3 

4This module generates configuration files (pyproject.toml, pre-commit, etc.) 

5with proper validation and templating. 

6""" 

7 

8from pathlib import Path 

9 

10from taipanstack.config.models import StackConfig 

11 

12 

13def _generate_ruff_config(target_version: str) -> str: 

14 """Generate Ruff configuration. 

15 

16 Args: 

17 target_version: The target Python version. 

18 

19 Returns: 

20 Ruff configuration string. 

21 

22 """ 

23 return f"""[tool.ruff] 

24line-length = 88 

25target-version = "{target_version}" 

26 

27[tool.ruff.lint] 

28select = [ 

29 "F", # Pyflakes 

30 "E", # pycodestyle errors 

31 "W", # pycodestyle warnings 

32 "I", # isort 

33 "N", # pep8-naming 

34 "D", # pydocstyle 

35 "Q", # flake8-quotes 

36 "S", # flake8-bandit 

37 "B", # flake8-bugbear 

38 "A", # flake8-builtins 

39 "C4", # flake8-comprehensions 

40 "T20", # flake8-print 

41 "SIM", # flake8-simplify 

42 "PTH", # flake8-use-pathlib 

43 "TID", # flake8-tidy-imports 

44 "ARG", # flake8-unused-arguments 

45 "PIE", # flake8-pie 

46 "PLC", # Pylint Convention 

47 "PLE", # Pylint Error 

48 "PLR", # Pylint Refactor 

49 "PLW", # Pylint Warning 

50 "RUF", # Ruff-specific 

51 "UP", # pyupgrade 

52 "ERA", # eradicate 

53 "TRY", # tryceratops 

54] 

55ignore = ["D203", "D212", "D213", "D416", "D417"] 

56 

57[tool.ruff.lint.mccabe] 

58max-complexity = 10 

59 

60[tool.ruff.lint.per-file-ignores] 

61"tests/**/*.py" = ["S101", "D"] 

62 

63[tool.ruff.format] 

64quote-style = "double" 

65indent-style = "space" 

66""" 

67 

68 

69def _generate_mypy_config(python_version: str) -> str: 

70 """Generate Mypy configuration. 

71 

72 Args: 

73 python_version: The target Python version. 

74 

75 Returns: 

76 Mypy configuration string. 

77 

78 """ 

79 return f"""[tool.mypy] 

80python_version = "{python_version}" 

81warn_return_any = true 

82warn_unused_configs = true 

83disallow_untyped_defs = true 

84disallow_any_unimported = false 

85no_implicit_optional = true 

86check_untyped_defs = true 

87strict_optional = true 

88strict_equality = true 

89ignore_missing_imports = true 

90show_error_codes = true 

91enable_error_code = ["ignore-without-code", "redundant-cast", "truthy-bool"] 

92""" 

93 

94 

95def _generate_pytest_config() -> str: 

96 """Generate Pytest configuration. 

97 

98 Returns: 

99 Pytest configuration string. 

100 

101 """ 

102 return """[tool.pytest.ini_options] 

103testpaths = ["tests"] 

104addopts = "-v --cov=src --cov-report=html --cov-report=term-missing --cov-fail-under=80 --strict-markers" 

105markers = [ 

106 "slow: marks tests as slow (deselect with '-m \"not slow\"')", 

107 "security: marks tests as security-related", 

108] 

109""" 

110 

111 

112def _generate_coverage_config() -> str: 

113 """Generate Coverage configuration. 

114 

115 Returns: 

116 Coverage configuration string. 

117 

118 """ 

119 return """[tool.coverage.run] 

120branch = true 

121source = ["src"] 

122omit = ["*/tests/*", "*/__pycache__/*"] 

123 

124[tool.coverage.report] 

125exclude_lines = [ 

126 "pragma: no cover", 

127 "def __repr__", 

128 "raise NotImplementedError", 

129 "if TYPE_CHECKING:", 

130 "if __name__ == .__main__.:", 

131] 

132""" 

133 

134 

135def generate_pyproject_config(config: StackConfig) -> str: 

136 """Generate Ruff, Mypy, and Pytest configuration for pyproject.toml. 

137 

138 Args: 

139 config: The Stack configuration. 

140 

141 Returns: 

142 Configuration string to append to pyproject.toml. 

143 

144 """ 

145 target_version = config.to_target_version() 

146 python_version = config.python_version 

147 

148 return f""" 

149# --- Stack v2.0 Quality Configuration --- 

150{_generate_ruff_config(target_version)} 

151{_generate_mypy_config(python_version)} 

152{_generate_pytest_config()} 

153{_generate_coverage_config()}""" 

154 

155 

156def _generate_bandit_hook(severity: str) -> str: 

157 """Generate Bandit pre-commit hook. 

158 

159 Args: 

160 severity: Bandit severity level. 

161 

162 Returns: 

163 Bandit hook YAML string. 

164 

165 """ 

166 sev_char = severity[0].upper() 

167 return f""" 

168 - repo: https://github.com/PyCQA/bandit 

169 rev: '1.8.0' 

170 hooks: 

171 - id: bandit 

172 args: ["-r", ".", "-l{sev_char}"] 

173""" 

174 

175 

176def _generate_safety_hook() -> str: 

177 """Generate Safety pre-commit hook. 

178 

179 Returns: 

180 Safety hook YAML string. 

181 

182 """ 

183 return """ 

184 - repo: https://github.com/pyupio/safety 

185 rev: '3.2.11' 

186 hooks: 

187 - id: safety 

188 args: ["check", "--json"] 

189""" 

190 

191 

192def _generate_semgrep_hook() -> str: 

193 """Generate Semgrep pre-commit hook. 

194 

195 Returns: 

196 Semgrep hook YAML string. 

197 

198 """ 

199 return """ 

200 - repo: https://github.com/semgrep/pre-commit 

201 rev: 'v1.99.0' 

202 hooks: 

203 - id: semgrep 

204 args: ['--config=auto'] 

205""" 

206 

207 

208def _generate_detect_secrets_hook() -> str: 

209 """Generate detect-secrets pre-commit hook. 

210 

211 Returns: 

212 Detect-secrets hook YAML string. 

213 

214 """ 

215 return """ 

216 - repo: https://github.com/Yelp/detect-secrets 

217 rev: 'v1.5.0' 

218 hooks: 

219 - id: detect-secrets 

220 args: ['--baseline', '.secrets.baseline'] 

221""" 

222 

223 

224def _generate_paranoid_hooks() -> str: 

225 """Generate extra security hooks for paranoid mode. 

226 

227 Returns: 

228 Paranoid hooks YAML string. 

229 

230 """ 

231 return """ 

232 - repo: https://github.com/trailofbits/pip-audit 

233 rev: 'v2.7.3' 

234 hooks: 

235 - id: pip-audit 

236 

237 - repo: https://github.com/jendrikseipp/vulture 

238 rev: 'v2.11' 

239 hooks: 

240 - id: vulture 

241 

242 - repo: https://github.com/guilatrova/tryceratops 

243 rev: 'v2.3.3' 

244 hooks: 

245 - id: tryceratops 

246""" 

247 

248 

249def generate_pre_commit_config(config: StackConfig) -> str: 

250 """Generate .pre-commit-config.yaml content. 

251 

252 Args: 

253 config: The Stack configuration. 

254 

255 Returns: 

256 Pre-commit configuration YAML string. 

257 

258 """ 

259 security_hooks: list[str] = [] 

260 

261 if config.security.enable_bandit: 

262 security_hooks.append(_generate_bandit_hook(config.security.bandit_severity)) 

263 

264 if config.security.enable_safety: 

265 security_hooks.append(_generate_safety_hook()) 

266 

267 if config.security.enable_semgrep: 

268 security_hooks.append(_generate_semgrep_hook()) 

269 

270 if config.security.enable_detect_secrets: 

271 security_hooks.append(_generate_detect_secrets_hook()) 

272 

273 # Add extra hooks for paranoid mode 

274 if config.security.level == "paranoid": 

275 security_hooks.append(_generate_paranoid_hooks()) 

276 

277 return f"""# Stack v2.0 Pre-commit Configuration 

278# Security Level: {config.security.level} 

279repos: 

280 - repo: https://github.com/pre-commit/pre-commit-hooks 

281 rev: v5.0.0 

282 hooks: 

283 - id: trailing-whitespace 

284 - id: end-of-file-fixer 

285 - id: check-yaml 

286 - id: check-added-large-files 

287 - id: check-merge-conflict 

288 - id: check-case-conflict 

289 - id: detect-private-key 

290 

291 - repo: https://github.com/astral-sh/ruff-pre-commit 

292 rev: 'v0.8.4' 

293 hooks: 

294 - id: ruff 

295 args: [--fix, --exit-non-zero-on-fix] 

296 - id: ruff-format 

297 

298 - repo: https://github.com/pre-commit/mirrors-mypy 

299 rev: 'v1.13.0' 

300 hooks: 

301 - id: mypy 

302 additional_dependencies: [types-all, pydantic] 

303{"".join(security_hooks)}""" 

304 

305 

306def generate_dependabot_config() -> str: 

307 """Generate .github/dependabot.yml content. 

308 

309 Returns: 

310 Dependabot configuration YAML string. 

311 

312 """ 

313 return """# Stack v2.0 Dependabot Configuration 

314version: 2 

315updates: 

316 - package-ecosystem: "pip" 

317 directory: "/" 

318 schedule: 

319 interval: "daily" 

320 open-pull-requests-limit: 10 

321 groups: 

322 dev-dependencies: 

323 patterns: 

324 - "ruff" 

325 - "mypy" 

326 - "bandit" 

327 - "safety" 

328 - "pytest*" 

329 - "pre-commit" 

330 - "semgrep" 

331 - "py-spy" 

332 security-tools: 

333 patterns: 

334 - "bandit" 

335 - "safety" 

336 - "semgrep" 

337 - "pip-audit" 

338 reviewers: 

339 - "gabrielima7" 

340 

341 - package-ecosystem: "github-actions" 

342 directory: "/" 

343 schedule: 

344 interval: "weekly" 

345 groups: 

346 actions: 

347 patterns: 

348 - "*" 

349""" 

350 

351 

352def generate_security_policy() -> str: 

353 """Generate SECURITY.md content. 

354 

355 Returns: 

356 Security policy markdown string. 

357 

358 """ 

359 return """# Security Policy 

360 

361## Supported Versions 

362 

363We prioritize security fixes for the latest version (Rolling Release). 

364 

365| Version | Supported | 

366| ------- | ------------------ | 

367| Latest | :white_check_mark: | 

368| Older | :x: | 

369 

370## Security Features 

371 

372This project includes multiple layers of security: 

373 

374- **SAST**: Bandit for static security analysis 

375- **SCA**: Safety/pip-audit for dependency vulnerabilities 

376- **Secrets**: detect-secrets for preventing credential leaks 

377- **Type Safety**: Mypy + Pydantic for runtime validation 

378- **Runtime Guards**: Protection against path traversal and injection 

379 

380## Reporting a Vulnerability 

381 

3821. **DO NOT** create a public issue for security vulnerabilities 

3832. Report via the [Security tab](../../security/advisories/new) 

3843. Or email the maintainer directly 

3854. Include: 

386 - Description of the vulnerability 

387 - Steps to reproduce 

388 - Potential impact 

389 - Suggested fix (if any) 

390 

391## Response Timeline 

392 

393- **Acknowledgment**: Within 48 hours 

394- **Initial Assessment**: Within 1 week 

395- **Fix Release**: Depends on severity (critical: ASAP, others: next release) 

396""" 

397 

398 

399def generate_editorconfig() -> str: 

400 """Generate .editorconfig content. 

401 

402 Returns: 

403 EditorConfig content string. 

404 

405 """ 

406 return """# Stack v2.0 EditorConfig 

407root = true 

408 

409[*] 

410end_of_line = lf 

411insert_final_newline = true 

412trim_trailing_whitespace = true 

413charset = utf-8 

414indent_style = space 

415indent_size = 4 

416 

417[*.py] 

418max_line_length = 88 

419 

420[*.{yml,yaml,toml,json}] 

421indent_size = 2 

422 

423[Makefile] 

424indent_style = tab 

425""" 

426 

427 

428def write_config_file( 

429 path: Path, 

430 content: str, 

431 config: StackConfig, 

432) -> bool: 

433 """Write configuration file with backup support. 

434 

435 Args: 

436 path: Path to write the file. 

437 content: Content to write. 

438 config: Stack configuration. 

439 

440 Returns: 

441 True if file was written, False if in dry-run mode. 

442 

443 """ 

444 if config.dry_run: 

445 return False 

446 

447 if path.exists() and not config.force: 

448 backup_path = path.with_suffix(f"{path.suffix}.bak") 

449 path.rename(backup_path) 

450 

451 path.write_text(content, encoding="utf-8") 

452 return True