Coverage for src/rhiza_hooks/update_readme_help.py: 100%
48 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-07-30 04:36 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-07-30 04:36 +0000
1#!/usr/bin/env python3
2"""Script to update README with Makefile help output.
4This hook runs 'make help' and embeds the output into README.md
5between special marker comments.
7Migrated from rhiza's local pre-commit hook that runs 'make readme'.
8This is a Python wrapper that provides the same functionality.
9"""
11from __future__ import annotations
13import re
14import subprocess # nosec B404
15import sys
16from pathlib import Path
18from rhiza_hooks._repo import find_repo_root
20# Markers used to identify the section to update in README
21START_MARKER = "<!-- MAKE_HELP_START -->"
22END_MARKER = "<!-- MAKE_HELP_END -->"
25def get_make_help_output() -> str | None:
26 """Run 'make help' and capture the output.
28 Returns:
29 The output from 'make help', or None if the command fails.
30 """
31 try:
32 result = subprocess.run( # nosec B603 B607
33 ["make", "help"], # noqa: S607
34 capture_output=True,
35 text=True,
36 check=True,
37 timeout=30,
38 )
39 except subprocess.CalledProcessError as e:
40 print(f"Error running 'make help': {e}")
41 return None
42 except subprocess.TimeoutExpired:
43 print("Error: 'make help' timed out")
44 return None
45 except FileNotFoundError:
46 print("Error: 'make' command not found")
47 return None
48 else:
49 return result.stdout
52def update_readme_with_help(readme_path: Path, help_output: str) -> bool:
53 """Update README.md with the make help output.
55 Args:
56 readme_path: Path to the README.md file.
57 help_output: The output from 'make help'.
59 Returns:
60 True if the file was modified, False otherwise.
61 """
62 if not readme_path.exists():
63 print(f"Warning: {readme_path} not found, skipping update")
64 return False
66 content = readme_path.read_text()
68 # Check if markers exist
69 # pragma below: equivalent mutant — with only one marker present the substitution
70 # pattern (START.*?END) cannot match either way, so `or`->`and` changes no observable
71 # behaviour (return value and file contents are identical).
72 if START_MARKER not in content or END_MARKER not in content: # pragma: no mutate
73 # No markers, nothing to update
74 return False
76 # Build the new content between markers
77 new_section = f"{START_MARKER}\n```\n{help_output}```\n{END_MARKER}"
79 # Replace the content between markers
80 pattern = re.compile(
81 re.escape(START_MARKER) + r".*?" + re.escape(END_MARKER),
82 re.DOTALL,
83 )
84 new_content = pattern.sub(new_section, content)
86 if new_content != content:
87 readme_path.write_text(new_content)
88 print(f"Updated {readme_path} with make help output")
89 return True
91 return False
94def main(argv: list[str] | None = None) -> int:
95 """Execute the script."""
96 # This hook doesn't use filenames, it always operates on the repo root
97 _ = argv # Unused # pragma: no mutate # equivalent: value is never read
99 repo_root = find_repo_root()
100 readme_path = repo_root / "README.md"
102 help_output = get_make_help_output()
103 if help_output is None:
104 # If make help fails, we don't fail the hook
105 # This allows the hook to be used in repos without a Makefile
106 return 0
108 if update_readme_with_help(readme_path, help_output):
109 # File was modified, fail so pre-commit knows to re-stage
110 return 1
112 return 0
115def _run() -> None:
116 """Entry point: delegate to :func:`main` and exit with its return code."""
117 sys.exit(main())
120if __name__ == "__main__": # pragma: no cover # pragma: no mutate
121 _run()