Coverage for tsfpga/tools/sphinx_doc.py: 0%

75 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-21 20:51 +0000

1# -------------------------------------------------------------------------------------------------- 

2# Copyright (c) Lukas Vik. All rights reserved. 

3# 

4# This file is part of the tsfpga project, a project platform for modern FPGA development. 

5# https://tsfpga.com 

6# https://github.com/tsfpga/tsfpga 

7# -------------------------------------------------------------------------------------------------- 

8# A set of methods for building sphinx docs. Should be reusable between projects. 

9# -------------------------------------------------------------------------------------------------- 

10 

11from __future__ import annotations 

12 

13import sys 

14from datetime import datetime 

15from subprocess import CalledProcessError 

16from typing import TYPE_CHECKING 

17 

18from git.repo import Repo 

19from packaging.version import Version, parse 

20 

21from tsfpga.system_utils import read_file, run_command 

22 

23if TYPE_CHECKING: 

24 from collections.abc import Iterable 

25 from pathlib import Path 

26 

27 

28def generate_release_notes( 

29 repo_root: Path, release_notes_directory: Path, project_name: str 

30) -> str: 

31 """ 

32 Generate release notes in RST format based on a directory full of release note files. 

33 Will match each file to a git tag. 

34 

35 Arguments: 

36 repo_root: Git commands will be executed here. 

37 release_notes_directory: Location of release notes files. 

38 project_name: Name of project will be used for the GitHub link. 

39 

40 Return: 

41 RST code with release notes. 

42 """ 

43 rst = "" 

44 

45 for release, previous_release_git_tag in _get_release_notes_files( 

46 repo_root=repo_root, release_notes_directory=release_notes_directory 

47 ): 

48 heading = f"{release.version} ({release.date})" 

49 rst += heading + "\n" 

50 rst += "-" * len(heading) + "\n" 

51 rst += "\n" 

52 if previous_release_git_tag is not None: 

53 diff_url = ( 

54 f"https://github.com/{project_name}/{project_name}/compare/" 

55 f"{previous_release_git_tag}...{release.git_tag}" 

56 ) 

57 rst += f"`Changes since previous release <{diff_url}>`__\n" 

58 rst += "\n" 

59 rst += read_file(release.release_notes_file) 

60 rst += "\n" 

61 

62 return rst 

63 

64 

65def _get_release_notes_files( 

66 repo_root: Path, release_notes_directory: Path 

67) -> Iterable[tuple[Release, str | None]]: 

68 """ 

69 Iterate the release notes. 

70 """ 

71 unreleased_notes_file = release_notes_directory / "unreleased.rst" 

72 

73 # All versioned release notes files. 

74 release_notes = [ 

75 release_notes_file 

76 for release_notes_file in release_notes_directory.glob("*.rst") 

77 if release_notes_file != unreleased_notes_file 

78 ] 

79 

80 # Sort by parsing the version number in the file name. Newest to oldest. 

81 def sort_key(path: Path) -> Version: 

82 return parse(path.stem) 

83 

84 release_notes.sort(key=sort_key, reverse=True) 

85 

86 # The "Unreleased" shall be first 

87 release_notes.insert(0, unreleased_notes_file) 

88 

89 repo = Repo(repo_root) 

90 releases = [ 

91 Release(repo=repo, release_notes_file=release_notes_file) 

92 for release_notes_file in release_notes 

93 ] 

94 

95 for idx, release in enumerate(releases): 

96 previous_release_git_tag = None if idx == len(releases) - 1 else releases[idx + 1].git_tag 

97 

98 yield release, previous_release_git_tag 

99 

100 

101class Release: 

102 """ 

103 Used to represent a release. 

104 """ 

105 

106 def __init__(self, repo: Repo, release_notes_file: Path) -> None: 

107 self.release_notes_file = release_notes_file 

108 

109 version = release_notes_file.stem 

110 if version == "unreleased": 

111 self.version = "Unreleased" 

112 self.git_tag = "main" 

113 self.date = "YYYY-MM-DD" 

114 else: 

115 self.version = version 

116 self.git_tag = "v" + self.version 

117 self.date = self.get_git_date_from_tag(repo=repo, tag=self.git_tag) 

118 

119 @staticmethod 

120 def get_git_date_from_tag(repo: Repo, tag: str) -> str: 

121 """ 

122 Get a formatted date string, gathered from git log based on tag name. 

123 """ 

124 timestamp = repo.tag(f"refs/tags/{tag}").commit.committed_date 

125 # This call will more or less guess the user's timezone. 

126 # In a general use case this is not reliable, hence the rule, but in our case 

127 # the date information is not critical in any way. 

128 # It is just there for extra info. 

129 # https://docs.astral.sh/ruff/rules/call-datetime-fromtimestamp/ 

130 time = datetime.fromtimestamp(timestamp) # noqa: DTZ006 

131 return f"{time.day} {time:%B} {time.year}".lower() 

132 

133 

134def build_sphinx(build_path: Path, output_path: Path) -> None: 

135 """ 

136 Execute sphinx on command line to build HTML documentation. 

137 

138 Arguments: 

139 build_path: The location that contains ``conf.py`` and ``index.rst``. 

140 output_path: Where to place the generated HTML. 

141 """ 

142 # Since we set the working directory when making the system call, paths must be absolute. 

143 build_path = build_path.resolve() 

144 output_path = output_path.resolve() 

145 

146 print("Building Sphinx documentation...") 

147 

148 cmd = [ 

149 sys.executable, 

150 "-m", 

151 "sphinx", 

152 # Turn warnings into errors. 

153 "-W", 

154 # Show full traceback upon error. 

155 "-T", 

156 str(build_path), 

157 str(output_path), 

158 ] 

159 try: 

160 run_command(cmd, cwd=build_path, capture_output=True) 

161 except CalledProcessError as exception: 

162 print(f"ERROR: Sphinx build command failed with exit code {exception.returncode}.") 

163 if exception.stdout: 

164 print("-" * 80) 

165 print("ERROR: Command STDOUT:\n") 

166 print(exception.stdout) 

167 if exception.stderr: 

168 print("-" * 80) 

169 print("ERROR: Command STDERR:\n") 

170 print(exception.stderr) 

171 raise 

172 

173 index_html = output_path / "index.html" 

174 if not index_html.exists(): 

175 raise FileNotFoundError(f"Creating HTML failed: {index_html}") 

176 

177 print(f"Documentation build done. Open with:\n firefox {index_html} &")