Coverage for tsfpga/module_documentation.py: 65%

141 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-20 20:52 +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._module.registers is not None: 

174 register_note_rst = ( 

175 "This module has a register interface, so make sure to study the " 

176 f":ref:`register interface documentation <{self._module.name}.register_interface>` " 

177 "as well as this top-level document.\n" 

178 ) 

179 else: 

180 register_note_rst = "" 

181 

182 if self._repository_url: 

183 url_rst = ( 

184 f"To browse the source code, visit the " 

185 f"`repository on {self._repository_name} <{self._repository_url}>`__.\n" 

186 ) 

187 else: 

188 url_rst = "" 

189 

190 overview_rst = self.get_overview_rst() 

191 overview_rst = "" if overview_rst is None else overview_rst 

192 

193 registers_rst = self.get_register_rst(heading_character=heading_character_2) 

194 registers_rst = "" if registers_rst is None else registers_rst 

195 

196 submodule_rst = self.get_submodule_rst( 

197 heading_character=heading_character_2, 

198 heading_character_2=heading_character_3, 

199 exclude_module_folders=exclude_module_folders, 

200 ) 

201 

202 rst = f"""\ 

203 

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

205 

206{heading} 

207{heading_underline} 

208 

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

210{register_note_rst}\ 

211{url_rst}\ 

212 

213{overview_rst} 

214 

215{registers_rst} 

216 

217{submodule_rst} 

218""" 

219 

220 return rst 

221 

222 def create_rst_document( 

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

224 ) -> None: 

225 """ 

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

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

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

229 

230 Arguments: 

231 output_path: Document will be placed here. 

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

233 excluded from documentation. 

234 """ 

235 register_list = self._module.registers 

236 if register_list is not None: 

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

238 

239 rst = self.get_rst_document(exclude_module_folders=exclude_module_folders) 

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

241 

242 def _get_vhdl_files( 

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

244 ) -> list[Path]: 

245 """ 

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

247 """ 

248 # Exclude all file types except VHDL. 

249 hdl_files = self._module.get_documentation_files( 

250 files_avoid=exclude_files, include_verilog=False, include_systemverilog=False 

251 ) 

252 

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

254 if file_is_in_directory(hdl_file.path, exclude_folders): 

255 return False 

256 

257 return True 

258 

259 vhdl_file_paths = [ 

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

261 ] 

262 

263 # Sort by file name 

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

265 return path.name 

266 

267 vhdl_files = sorted(vhdl_file_paths, key=sort_key) 

268 

269 return vhdl_files 

270 

271 def _get_vhdl_file_rst( 

272 self, 

273 vhdl_file_path: Path, 

274 heading_character: str, 

275 heading_character_2: str, 

276 netlist_builds: list["VivadoNetlistProject"], 

277 ) -> str: 

278 """ 

279 Get reStructuredText documentation for a VHDL file. 

280 """ 

281 vhdl_file_documentation = VhdlFileDocumentation(vhdl_file_path) 

282 

283 file_rst = vhdl_file_documentation.get_header_rst() 

284 file_rst = "" if file_rst is None else file_rst 

285 

286 if self._repository_url: 

287 url_rst = ( 

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

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

290 ) 

291 else: 

292 url_rst = "" 

293 

294 symbolator_rst = self._get_symbolator_rst(vhdl_file_documentation) 

295 symbolator_rst = "" if symbolator_rst is None else symbolator_rst 

296 

297 entity_name = vhdl_file_path.stem 

298 

299 resource_utilization_rst = self._get_resource_utilization_rst( 

300 entity_name=entity_name, 

301 heading_character=heading_character_2, 

302 netlist_builds=netlist_builds, 

303 ) 

304 

305 heading = f"{vhdl_file_path.name}" 

306 heading_underline = heading_character * len(heading) 

307 

308 rst = f""" 

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

310 

311{heading} 

312{heading_underline} 

313 

314{url_rst} 

315 

316{symbolator_rst} 

317 

318{file_rst} 

319 

320{resource_utilization_rst} 

321""" 

322 

323 return rst 

324 

325 @staticmethod 

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

327 """ 

328 Get RST for rendering a symbolator component. 

329 """ 

330 component = vhdl_file_documentation.get_symbolator_component() 

331 if component is None: 

332 return "" 

333 

334 indent = " " 

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

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

337 

338 return rst 

339 

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

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

342 ) -> str: 

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

344 generics = [] 

345 checkers = [] 

346 for netlist_build in netlist_builds: 

347 if netlist_build.build_result_checkers: 

348 generics.append(netlist_build.static_generics) 

349 

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

351 checker_dict = {} 

352 for checker in netlist_build.build_result_checkers: 

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

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

355 

356 checkers.append(checker_dict) 

357 

358 # Make RST of the information 

359 rst = "" 

360 if generics: 

361 heading = "Resource utilization" 

362 heading_underline = heading_character * len(heading) 

363 

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

365 if self._repository_url: 

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

367 else: 

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

369 

370 rst = f""" 

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

372 

373{heading} 

374{heading_underline} 

375 

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

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

378in {module_py_rst}. 

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

380generic configuration. 

381 

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

383 :header-rows: 1 

384 

385""" 

386 

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

388 checker_names = [] 

389 for build_checkers in checkers: 

390 for checker_name in build_checkers: 

391 if checker_name not in checker_names: 

392 checker_names.append(checker_name) 

393 

394 # Fill in the header row 

395 rst += " * - Generics\n" 

396 for checker_name in checker_names: 

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

398 

399 # Make one row for each netlist build 

400 for build_idx, generic_dict in enumerate(generics): 

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

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

403 

404 rst += f"""\ 

405 * - {generics_rst}""" 

406 

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

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

409 # and fifo modules. 

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

411 if generic_strings: 

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

413 # placed on a new line. 

414 leader = "\n\n " 

415 else: 

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

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

418 # two builds in the reg_file module. 

419 leader = "" 

420 

421 rst += f"""\ 

422{leader}(Using wrapper 

423 

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

425 

426 rst += "\n" 

427 

428 for checker_name in checker_names: 

429 checker_value = checkers[build_idx][checker_name] 

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

431 

432 return rst