Coverage for tsfpga/module_documentation.py: 65%

123 statements  

« prev     ^ index     » next       coverage.py v7.2.1, created at 2023-09-27 20:00 +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://gitlab.com/tsfpga/tsfpga 

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

8 

9# First party libraries 

10from tsfpga.system_utils import create_file, file_is_in_directory, read_file 

11from tsfpga.vhdl_file_documentation import VhdlFileDocumentation 

12 

13 

14class ModuleDocumentation: 

15 

16 """ 

17 Methods for generating a reStructuredText document with module documentation. 

18 The content is extracted from VHDL source file headers. 

19 """ 

20 

21 def __init__(self, module): 

22 """ 

23 Arguments: 

24 module (:class:`.BaseModule`): The module which shall be documented. 

25 """ 

26 self._module = module 

27 

28 def get_overview_rst(self): 

29 """ 

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

31 

32 Return: 

33 str: Module overview RST. ``None`` if file does not exist. 

34 """ 

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

36 if overview_rst_file.exists(): 

37 return read_file(overview_rst_file) 

38 

39 return None 

40 

41 def get_register_rst(self, heading_character): 

42 """ 

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

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

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

46 ``.rst`` file. 

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

48 

49 Arguments: 

50 heading_character (str): Character to use for heading underline. 

51 

52 Return: 

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

54 """ 

55 if self._module.registers is not None: 

56 heading = "Register interface" 

57 heading_underline = heading_character * len(heading) 

58 return f"""\ 

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

60 

61{heading} 

62{heading_underline} 

63 

64This module has register definitions. 

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

66register documentation. 

67""" 

68 

69 return None 

70 

71 def get_submodule_rst( 

72 self, 

73 heading_character, 

74 heading_character_2, 

75 exclude_files=None, 

76 exclude_module_folders=None, 

77 ): 

78 """ 

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

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

81 symbolator symbol of the entity. 

82 

83 Arguments: 

84 heading_character (str): Character to use for heading underline. 

85 heading_character_2 (str): Character to use for next level of heading underline. 

86 exclude_files (set(pathlib.Path)): Files that shall be excluded from the documentation. 

87 exclude_module_folders (list(str)): Folder names within the module root that shall be 

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

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

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

91 

92 Return: 

93 str: RST code with sub-module documentation. 

94 """ 

95 exclude_module_folders = [] if exclude_module_folders is None else exclude_module_folders 

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

97 

98 all_builds = self._module.get_build_projects() 

99 

100 rst = "" 

101 

102 for hdl_file in self._get_vhdl_files( 

103 exclude_files=exclude_files, exclude_folders=exclude_module_paths 

104 ): 

105 vhdl_file_path = hdl_file.path 

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

107 

108 netlist_builds = [] 

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

110 for project in all_builds: 

111 if project.is_netlist_build and ( 

112 project.name == netlist_build_base_name 

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

114 ): 

115 netlist_builds.append(project) 

116 

117 rst += self._get_vhdl_file_rst( 

118 vhdl_file_path=vhdl_file_path, 

119 heading_character=heading_character, 

120 heading_character_2=heading_character_2, 

121 netlist_builds=netlist_builds, 

122 ) 

123 

124 return rst 

125 

126 def get_rst_document(self, exclude_module_folders=None): 

127 """ 

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

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

130 

131 Arguments: 

132 exclude_module_folders (list(str)): Folder names within the module root that shall be 

133 excluded from documentation. 

134 

135 Returns: 

136 str: An RST document. 

137 """ 

138 heading_character_1 = "=" 

139 heading_character_2 = "-" 

140 heading_character_3 = "_" 

141 

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

143 heading_underline = heading_character_1 * len(heading) 

144 

145 overview_rst = self.get_overview_rst() 

146 overview_rst = "" if overview_rst is None else overview_rst 

147 

148 registers_rst = self.get_register_rst(heading_character=heading_character_2) 

149 registers_rst = "" if registers_rst is None else registers_rst 

150 

151 submodule_rst = self.get_submodule_rst( 

152 heading_character=heading_character_2, 

153 heading_character_2=heading_character_3, 

154 exclude_module_folders=exclude_module_folders, 

155 ) 

156 

157 rst = f"""\ 

158 

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

160 

161{heading} 

162{heading_underline} 

163 

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

165 

166{overview_rst} 

167 

168{registers_rst} 

169 

170{submodule_rst} 

171""" 

172 

173 return rst 

174 

175 def create_rst_document(self, output_path, exclude_module_folders=None): 

176 """ 

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

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

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

180 

181 Arguments: 

182 output_path (pathlib.Path): Document will be placed here. 

183 exclude_module_folders (list(str)): Folder names within the module root that shall be 

184 excluded from documentation. 

185 """ 

186 register_list = self._module.registers 

187 if register_list is not None: 

188 register_list.create_html_page(output_path) 

189 

190 rst = self.get_rst_document(exclude_module_folders=exclude_module_folders) 

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

192 

193 def _get_vhdl_files(self, exclude_files, exclude_folders): 

194 """ 

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

196 """ 

197 hdl_files = self._module.get_documentation_files(files_avoid=exclude_files) 

198 

199 module_regs_pkg = self._module.path / f"{self._module.name}_regs_pkg.vhd" 

200 

201 def file_should_be_included(hdl_file): 

202 if file_is_in_directory(hdl_file.path, exclude_folders): 

203 return False 

204 

205 if not hdl_file.is_vhdl: 

206 return False 

207 

208 if hdl_file.path == module_regs_pkg: 

209 return False 

210 

211 return True 

212 

213 vhdl_files = [hdl_file for hdl_file in hdl_files if file_should_be_included(hdl_file)] 

214 

215 # Sort by file name 

216 def sort_key(vhdl_file): 

217 return vhdl_file.path.name 

218 

219 vhdl_files = sorted(vhdl_files, key=sort_key) 

220 

221 return vhdl_files 

222 

223 def _get_vhdl_file_rst( 

224 self, vhdl_file_path, heading_character, heading_character_2, netlist_builds 

225 ): 

226 """ 

227 Get reStructuredText documentation for a VHDL file. 

228 """ 

229 vhdl_file_documentation = VhdlFileDocumentation(vhdl_file_path) 

230 

231 file_rst = vhdl_file_documentation.get_header_rst() 

232 file_rst = "" if file_rst is None else file_rst 

233 

234 symbolator_rst = self._get_symbolator_rst(vhdl_file_documentation) 

235 symbolator_rst = "" if symbolator_rst is None else symbolator_rst 

236 

237 resource_utilization_rst = self._get_resource_utilization_rst( 

238 vhdl_file_path=vhdl_file_path, 

239 heading_character=heading_character_2, 

240 netlist_builds=netlist_builds, 

241 ) 

242 

243 entity_name = vhdl_file_path.stem 

244 heading = f"{vhdl_file_path.name}" 

245 heading_underline = heading_character * len(heading) 

246 

247 rst = f""" 

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

249 

250{heading} 

251{heading_underline} 

252 

253{symbolator_rst} 

254 

255{file_rst} 

256 

257{resource_utilization_rst} 

258""" 

259 

260 return rst 

261 

262 @staticmethod 

263 def _get_symbolator_rst(vhdl_file_documentation): 

264 """ 

265 Get RST for rendering a symbolator component. 

266 """ 

267 component = vhdl_file_documentation.get_symbolator_component() 

268 if component is None: 

269 return "" 

270 

271 indent = " " 

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

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

274 

275 return rst 

276 

277 def _get_resource_utilization_rst( 

278 self, 

279 vhdl_file_path, 

280 heading_character, 

281 netlist_builds, 

282 ): # pylint: disable=too-many-locals,too-many-branches 

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

284 generics = [] 

285 checkers = [] 

286 for netlist_build in netlist_builds: 

287 if netlist_build.build_result_checkers: 

288 generics.append(netlist_build.static_generics) 

289 

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

291 checker_dict = {} 

292 for checker in netlist_build.build_result_checkers: 

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

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

295 

296 checkers.append(checker_dict) 

297 

298 # Make RST of the information 

299 rst = "" 

300 if generics: 

301 heading = "Resource utilization" 

302 heading_underline = heading_character * len(heading) 

303 rst = f""" 

304{heading} 

305{heading_underline} 

306 

307This entity has `netlist builds <https://tsfpga.com/netlist_build.html>`__ set up with 

308`automatic size checkers <https://tsfpga.com/netlist_build.html#build-result-checkers>`__ 

309in ``module_{self._module.name}.py``. 

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

311generic configuration. 

312 

313.. list-table:: Resource utilization for {vhdl_file_path.name} netlist builds. 

314 :header-rows: 1 

315 

316""" 

317 

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

319 checker_names = [] 

320 for build_checkers in checkers: 

321 for checker_name in build_checkers: 

322 if checker_name not in checker_names: 

323 checker_names.append(checker_name) 

324 

325 # Fill in the header row 

326 rst += " * - Generics\n" 

327 for checker_name in checker_names: 

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

329 

330 # Make one row for each netlist build 

331 for build_idx, generic_dict in enumerate(generics): 

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

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

334 

335 rst += f"""\ 

336 * - {generics_rst}""" 

337 

338 # If the "top" of the project is different than this file name, we assume that it 

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

340 # and fifo modules. 

341 if netlist_builds[build_idx].top != vhdl_file_path.stem: 

342 if generic_strings: 

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

344 # placed on a new line. 

345 leader = "\n\n " 

346 else: 

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

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

349 # two builds in the reg_file module. 

350 leader = "" 

351 

352 rst += f"""\ 

353{leader}(Using wrapper 

354 

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

356 

357 rst += "\n" 

358 

359 for checker_name in checker_names: 

360 checker_value = checkers[build_idx][checker_name] 

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

362 

363 return rst