Coverage for src / rhiza_hooks / update_readme_help.py: 100%

54 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-08 08:53 +0000

1#!/usr/bin/env python3 

2"""Script to update README with Makefile help output. 

3 

4This hook runs 'make help' and embeds the output into README.md 

5between special marker comments. 

6 

7Migrated from rhiza's local pre-commit hook that runs 'make readme'. 

8This is a Python wrapper that provides the same functionality. 

9""" 

10 

11from __future__ import annotations 

12 

13import re 

14import subprocess # nosec B404 

15import sys 

16from pathlib import Path 

17 

18# Markers used to identify the section to update in README 

19START_MARKER = "<!-- MAKE_HELP_START -->" 

20END_MARKER = "<!-- MAKE_HELP_END -->" 

21 

22 

23def get_make_help_output() -> str | None: 

24 """Run 'make help' and capture the output. 

25 

26 Returns: 

27 The output from 'make help', or None if the command fails. 

28 """ 

29 try: 

30 result = subprocess.run( # nosec B603 B607 

31 ["make", "help"], 

32 capture_output=True, 

33 text=True, 

34 check=True, 

35 timeout=30, 

36 ) 

37 except subprocess.CalledProcessError as e: 

38 print(f"Error running 'make help': {e}") 

39 return None 

40 except subprocess.TimeoutExpired: 

41 print("Error: 'make help' timed out") 

42 return None 

43 except FileNotFoundError: 

44 print("Error: 'make' command not found") 

45 return None 

46 else: 

47 return result.stdout 

48 

49 

50def update_readme_with_help(readme_path: Path, help_output: str) -> bool: 

51 """Update README.md with the make help output. 

52 

53 Args: 

54 readme_path: Path to the README.md file. 

55 help_output: The output from 'make help'. 

56 

57 Returns: 

58 True if the file was modified, False otherwise. 

59 """ 

60 if not readme_path.exists(): 

61 print(f"Warning: {readme_path} not found, skipping update") 

62 return False 

63 

64 content = readme_path.read_text() 

65 

66 # Check if markers exist 

67 if START_MARKER not in content or END_MARKER not in content: 

68 # No markers, nothing to update 

69 return False 

70 

71 # Build the new content between markers 

72 new_section = f"{START_MARKER}\n```\n{help_output}```\n{END_MARKER}" 

73 

74 # Replace the content between markers 

75 pattern = re.compile( 

76 re.escape(START_MARKER) + r".*?" + re.escape(END_MARKER), 

77 re.DOTALL, 

78 ) 

79 new_content = pattern.sub(new_section, content) 

80 

81 if new_content != content: 

82 readme_path.write_text(new_content) 

83 print(f"Updated {readme_path} with make help output") 

84 return True 

85 

86 return False 

87 

88 

89def find_repo_root() -> Path: 

90 """Find the repository root directory. 

91 

92 Returns: 

93 Path to the repository root. 

94 """ 

95 current = Path.cwd() 

96 while current != current.parent: 

97 if (current / ".git").exists(): 

98 return current 

99 current = current.parent 

100 return Path.cwd() 

101 

102 

103def main(argv: list[str] | None = None) -> int: 

104 """Execute the script.""" 

105 # This hook doesn't use filenames, it always operates on the repo root 

106 _ = argv # Unused 

107 

108 repo_root = find_repo_root() 

109 readme_path = repo_root / "README.md" 

110 

111 help_output = get_make_help_output() 

112 if help_output is None: 

113 # If make help fails, we don't fail the hook 

114 # This allows the hook to be used in repos without a Makefile 

115 return 0 

116 

117 if update_readme_with_help(readme_path, help_output): 

118 # File was modified, fail so pre-commit knows to re-stage 

119 return 1 

120 

121 return 0 

122 

123 

124if __name__ == "__main__": 

125 sys.exit(main())