Coverage for tsfpga/module_documentation.py: 64%

138 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-20 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 

9# Standard libraries 

10from pathlib import Path 

11from typing import TYPE_CHECKING, Optional 

12 

13# Third party libraries 

14from hdl_registers.generator.html.page import HtmlPageGenerator 

15 

16# Local folder libraries 

17from .about import WEBSITE_URL 

18from .system_utils import create_file, file_is_in_directory, read_file 

19from .vhdl_file_documentation import VhdlFileDocumentation 

20from .vivado.project import VivadoNetlistProject 

21 

22if TYPE_CHECKING: 

23 # Local folder libraries 

24 from .hdl_file import HdlFile 

25 from .module import BaseModule 

26 

27 

28class ModuleDocumentation: 

29 """ 

30 Methods for generating a reStructuredText document with module documentation. 

31 The content is extracted from VHDL source file headers. 

32 """ 

33 

34 def __init__( 

35 self, 

36 module: "BaseModule", 

37 repository_url: Optional[str] = None, 

38 repository_name: Optional[str] = None, 

39 ) -> None: 

40 """ 

41 Arguments: 

42 module: The module which shall be documented. 

43 repository_url: Optionally specify an URL where the source code can be viewed. 

44 If this argument is specified, links will be added to the documentation. 

45 URL should be to the module folder within the repository. 

46 repository_name: Optionally specify the name of the repository URL. 

47 For example "GitLab". 

48 """ 

49 self._module = module 

50 self._repository_url = repository_url 

51 self._repository_name = repository_name 

52 

53 assert (repository_url is None) == ( 

54 repository_name is None 

55 ), "Both or none of the repository arguments must be set" 

56 

57 def get_overview_rst(self) -> Optional[str]: 

58 """ 

59 Get the contents of the module's ``doc/<name>.rst``, i.e. the module "overview" document. 

60 

61 Return: 

62 Module overview RST. ``None`` if file does not exist. 

63 """ 

64 overview_rst_file = self._module.path / "doc" / f"{self._module.name}.rst" 

65 if overview_rst_file.exists(): 

66 return read_file(overview_rst_file) 

67 

68 return None 

69 

70 def get_register_rst(self, heading_character: str) -> Optional[str]: 

71 """ 

72 Get an RST snippet with a link to the module's register documentation, if available. 

73 Note that this will create an RST ``:download:`` statement to the register .html page. 

74 When building, the ``.html`` file must be present in the same directory as the 

75 ``.rst`` file. 

76 This is done automatically by :meth:`.create_rst_document`. 

77 

78 Arguments: 

79 heading_character: Character to use for heading underline. 

80 

81 Return: 

82 RST snippet with link to register HTML. ``None`` if module does not have registers. 

83 """ 

84 if self._module.registers is not None: 

85 heading = "Register interface" 

86 heading_underline = heading_character * len(heading) 

87 return f"""\ 

88.. _{self._module.name}.register_interface: 

89 

90{heading} 

91{heading_underline} 

92 

93This module has register definitions. 

94Please see :download:`separate HTML page <{self._module.name}_regs.html>` for \ 

95register documentation. 

96""" 

97 

98 return None 

99 

100 def get_submodule_rst( 

101 self, 

102 heading_character: str, 

103 heading_character_2: str, 

104 exclude_files: Optional[set[Path]] = None, 

105 exclude_module_folders: Optional[list[str]] = None, 

106 ) -> str: 

107 """ 

108 Get RST code with documentation of the different sub-modules (files) of the module. 

109 Contains documentation that is extracted from the file headers, as well as a 

110 symbolator symbol of the entity. 

111 

112 Arguments: 

113 heading_character: Character to use for heading underline. 

114 heading_character_2: Character to use for next level of heading underline. 

115 exclude_files: Files that shall be excluded from the documentation. 

116 exclude_module_folders: Folder names within the module root that shall be 

117 excluded from documentation. For example, if you chosen module structure places 

118 only netlist build wrappers in the "rtl/" folder within modules, and you do not 

119 want them included in the documentation, then pass the argument ["rtl"]. 

120 

121 Return: 

122 RST code with sub-module documentation. 

123 """ 

124 exclude_module_folders = [] if exclude_module_folders is None else exclude_module_folders 

125 exclude_module_paths = [self._module.path / name for name in exclude_module_folders] 

126 

127 all_builds = self._module.get_build_projects() 

128 

129 rst = "" 

130 

131 for vhdl_file_path in self._get_vhdl_files( 

132 exclude_files=exclude_files, exclude_folders=exclude_module_paths 

133 ): 

134 netlist_build_base_name = f"{self._module.library_name}.{vhdl_file_path.stem}" 

135 

136 netlist_builds = [] 

137 # Include all netlist builds whose project name matches this file 

138 for project in all_builds: 

139 if isinstance(project, VivadoNetlistProject) and ( 

140 project.name == netlist_build_base_name 

141 or project.name.startswith(f"{netlist_build_base_name}.") 

142 ): 

143 netlist_builds.append(project) 

144 

145 rst += self._get_vhdl_file_rst( 

146 vhdl_file_path=vhdl_file_path, 

147 heading_character=heading_character, 

148 heading_character_2=heading_character_2, 

149 netlist_builds=netlist_builds, 

150 ) 

151 

152 return rst 

153 

154 def get_rst_document(self, exclude_module_folders: Optional[list[str]] = None) -> str: 

155 """ 

156 Get a complete RST document with the content of :meth:`.get_overview_rst`, 

157 :meth:`.get_register_rst`, and :meth:`.get_submodule_rst`, as well as a top level heading. 

158 

159 Arguments: 

160 exclude_module_folders: Folder names within the module root that shall be 

161 excluded from documentation. 

162 

163 Return: 

164 An RST document. 

165 """ 

166 heading_character_1 = "=" 

167 heading_character_2 = "-" 

168 heading_character_3 = "_" 

169 

170 heading = f"Module {self._module.name}" 

171 heading_underline = heading_character_1 * len(heading) 

172 

173 if self._repository_url: 

174 url_rst = ( 

175 f"To browse the source code, please visit the " 

176 f"`repository on {self._repository_name} <{self._repository_url}>`__." 

177 ) 

178 else: 

179 url_rst = "" 

180 

181 overview_rst = self.get_overview_rst() 

182 overview_rst = "" if overview_rst is None else overview_rst 

183 

184 registers_rst = self.get_register_rst(heading_character=heading_character_2) 

185 registers_rst = "" if registers_rst is None else registers_rst 

186 

187 submodule_rst = self.get_submodule_rst( 

188 heading_character=heading_character_2, 

189 heading_character_2=heading_character_3, 

190 exclude_module_folders=exclude_module_folders, 

191 ) 

192 

193 rst = f"""\ 

194 

195.. _module_{self._module.name}: 

196 

197{heading} 

198{heading_underline} 

199 

200This document contains technical documentation for the ``{self._module.name}`` module. 

201{url_rst} 

202 

203{overview_rst} 

204 

205{registers_rst} 

206 

207{submodule_rst} 

208""" 

209 

210 return rst 

211 

212 def create_rst_document( 

213 self, output_path: Path, exclude_module_folders: Optional[list[str]] = None 

214 ) -> None: 

215 """ 

216 Create an ``.rst`` file in ``output_path`` with the content from :meth:`.get_rst_document`. 

217 If the module has registers, the HTML page will also be generated in ``output_path``, so 

218 that e.g. sphinx can be run directly. 

219 

220 Arguments: 

221 output_path: Document will be placed here. 

222 exclude_module_folders: Folder names within the module root that shall be 

223 excluded from documentation. 

224 """ 

225 register_list = self._module.registers 

226 if register_list is not None: 

227 HtmlPageGenerator(register_list=register_list, output_folder=output_path).create() 

228 

229 rst = self.get_rst_document(exclude_module_folders=exclude_module_folders) 

230 create_file(output_path / f"{self._module.name}.rst", contents=rst) 

231 

232 def _get_vhdl_files( 

233 self, exclude_files: Optional[set[Path]], exclude_folders: list[Path] 

234 ) -> list[Path]: 

235 """ 

236 Get VHDL files that shall be included in the documentation, in order. 

237 """ 

238 # Exclude all file types except VHDL. 

239 hdl_files = self._module.get_documentation_files( 

240 files_avoid=exclude_files, include_verilog=False, include_systemverilog=False 

241 ) 

242 

243 def file_should_be_included(hdl_file: "HdlFile") -> bool: 

244 if file_is_in_directory(hdl_file.path, exclude_folders): 

245 return False 

246 

247 return True 

248 

249 vhdl_file_paths = [ 

250 hdl_file.path for hdl_file in hdl_files if file_should_be_included(hdl_file) 

251 ] 

252 

253 # Sort by file name 

254 def sort_key(path: Path) -> str: 

255 return path.name 

256 

257 vhdl_files = sorted(vhdl_file_paths, key=sort_key) 

258 

259 return vhdl_files 

260 

261 def _get_vhdl_file_rst( 

262 self, 

263 vhdl_file_path: Path, 

264 heading_character: str, 

265 heading_character_2: str, 

266 netlist_builds: list["VivadoNetlistProject"], 

267 ) -> str: 

268 """ 

269 Get reStructuredText documentation for a VHDL file. 

270 """ 

271 vhdl_file_documentation = VhdlFileDocumentation(vhdl_file_path) 

272 

273 file_rst = vhdl_file_documentation.get_header_rst() 

274 file_rst = "" if file_rst is None else file_rst 

275 

276 if self._repository_url: 

277 url_rst = ( 

278 f"`View source code on {self._repository_name} " 

279 f"<{self._repository_url}/{vhdl_file_path.relative_to(self._module.path)}>`__." 

280 ) 

281 else: 

282 url_rst = "" 

283 

284 symbolator_rst = self._get_symbolator_rst(vhdl_file_documentation) 

285 symbolator_rst = "" if symbolator_rst is None else symbolator_rst 

286 

287 entity_name = vhdl_file_path.stem 

288 

289 resource_utilization_rst = self._get_resource_utilization_rst( 

290 entity_name=entity_name, 

291 heading_character=heading_character_2, 

292 netlist_builds=netlist_builds, 

293 ) 

294 

295 heading = f"{vhdl_file_path.name}" 

296 heading_underline = heading_character * len(heading) 

297 

298 rst = f""" 

299.. _{self._module.name}.{entity_name}: 

300 

301{heading} 

302{heading_underline} 

303 

304{url_rst} 

305 

306{symbolator_rst} 

307 

308{file_rst} 

309 

310{resource_utilization_rst} 

311""" 

312 

313 return rst 

314 

315 @staticmethod 

316 def _get_symbolator_rst(vhdl_file_documentation: VhdlFileDocumentation) -> str: 

317 """ 

318 Get RST for rendering a symbolator component. 

319 """ 

320 component = vhdl_file_documentation.get_symbolator_component() 

321 if component is None: 

322 return "" 

323 

324 indent = " " 

325 rst = ".. symbolator::\n\n" 

326 rst += indent + component.replace("\n", f"\n{indent}") 

327 

328 return rst 

329 

330 def _get_resource_utilization_rst( # pylint: disable=too-many-locals,too-many-branches 

331 self, entity_name: str, heading_character: str, netlist_builds: list["VivadoNetlistProject"] 

332 ) -> str: 

333 # First, loop over all netlist builds for this module and assemble information 

334 generics = [] 

335 checkers = [] 

336 for netlist_build in netlist_builds: 

337 if netlist_build.build_result_checkers: 

338 generics.append(netlist_build.static_generics) 

339 

340 # Create a dictionary for each build, that maps "Checker name": "value" 

341 checker_dict = {} 

342 for checker in netlist_build.build_result_checkers: 

343 # Casting the limit to string yields e.g. "< 4", "4" or "> 4" 

344 checker_dict[checker.name] = str(checker.limit) 

345 

346 checkers.append(checker_dict) 

347 

348 # Make RST of the information 

349 rst = "" 

350 if generics: 

351 heading = "Resource utilization" 

352 heading_underline = heading_character * len(heading) 

353 

354 module_py_name = f"module_{self._module.name}.py" 

355 if self._repository_url: 

356 module_py_rst = f"`{module_py_name} <{self._repository_url}/{module_py_name}>`__" 

357 else: 

358 module_py_rst = f"``{module_py_name}``" 

359 

360 rst = f""" 

361.. _{self._module.name}.{entity_name}.resource_utilization: 

362 

363{heading} 

364{heading_underline} 

365 

366This entity has `netlist builds <{WEBSITE_URL}/netlist_build.html>`__ set up with 

367`automatic size checkers <{WEBSITE_URL}/netlist_build.html#build-result-checkers>`__ 

368in {module_py_rst}. 

369The following table lists the resource utilization for the entity, depending on 

370generic configuration. 

371 

372.. list-table:: Resource utilization for **{entity_name}** netlist builds. 

373 :header-rows: 1 

374 

375""" 

376 

377 # Make a list of the unique checker names. Use list rather than set to preserve order. 

378 checker_names = [] 

379 for build_checkers in checkers: 

380 for checker_name in build_checkers: 

381 if checker_name not in checker_names: 

382 checker_names.append(checker_name) 

383 

384 # Fill in the header row 

385 rst += " * - Generics\n" 

386 for checker_name in checker_names: 

387 rst += f" - {checker_name}\n" 

388 

389 # Make one row for each netlist build 

390 for build_idx, generic_dict in enumerate(generics): 

391 generic_strings = [f"{name} = {value}" for name, value in generic_dict.items()] 

392 generics_rst = "\n\n ".join(generic_strings) 

393 

394 rst += f"""\ 

395 * - {generics_rst}""" 

396 

397 # If the "top" of the project is different than the entity, we assume that it 

398 # is a wrapper. Add a note to the table about this. This occurs e.g. in the reg_file 

399 # and fifo modules. 

400 if netlist_builds[build_idx].top != entity_name: 

401 if generic_strings: 

402 # If there is already something in the generic column, this note shall be 

403 # placed on a new line. 

404 leader = "\n\n " 

405 else: 

406 # Otherwise, i.e. if the netlist build has no generics set, 

407 # the note shall be placed as the first thing. This is the case with 

408 # two builds in the reg_file module. 

409 leader = "" 

410 

411 rst += f"""\ 

412{leader}(Using wrapper 

413 

414 {netlist_builds[build_idx].top}.vhd)""" 

415 

416 rst += "\n" 

417 

418 for checker_name in checker_names: 

419 checker_value = checkers[build_idx][checker_name] 

420 rst += f" - {checker_value}\n" 

421 

422 return rst