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
« 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# --------------------------------------------------------------------------------------------------
9# Standard libraries
10from pathlib import Path
11from typing import TYPE_CHECKING, Optional
13# Third party libraries
14from hdl_registers.generator.html.page import HtmlPageGenerator
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
22if TYPE_CHECKING:
23 # Local folder libraries
24 from .hdl_file import HdlFile
25 from .module import BaseModule
28class ModuleDocumentation:
29 """
30 Methods for generating a reStructuredText document with module documentation.
31 The content is extracted from VHDL source file headers.
32 """
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
53 assert (repository_url is None) == (
54 repository_name is None
55 ), "Both or none of the repository arguments must be set"
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.
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)
68 return None
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`.
78 Arguments:
79 heading_character: Character to use for heading underline.
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:
90{heading}
91{heading_underline}
93This module has register definitions.
94Please see :download:`separate HTML page <{self._module.name}_regs.html>` for \
95register documentation.
96"""
98 return None
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.
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"].
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]
127 all_builds = self._module.get_build_projects()
129 rst = ""
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}"
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)
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 )
152 return rst
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.
159 Arguments:
160 exclude_module_folders: Folder names within the module root that shall be
161 excluded from documentation.
163 Return:
164 An RST document.
165 """
166 heading_character_1 = "="
167 heading_character_2 = "-"
168 heading_character_3 = "_"
170 heading = f"Module {self._module.name}"
171 heading_underline = heading_character_1 * len(heading)
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 = ""
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 = ""
190 overview_rst = self.get_overview_rst()
191 overview_rst = "" if overview_rst is None else overview_rst
193 registers_rst = self.get_register_rst(heading_character=heading_character_2)
194 registers_rst = "" if registers_rst is None else registers_rst
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 )
202 rst = f"""\
204.. _module_{self._module.name}:
206{heading}
207{heading_underline}
209This document contains technical documentation for the ``{self._module.name}`` module.
210{register_note_rst}\
211{url_rst}\
213{overview_rst}
215{registers_rst}
217{submodule_rst}
218"""
220 return rst
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.
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()
239 rst = self.get_rst_document(exclude_module_folders=exclude_module_folders)
240 create_file(output_path / f"{self._module.name}.rst", contents=rst)
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 )
253 def file_should_be_included(hdl_file: "HdlFile") -> bool:
254 if file_is_in_directory(hdl_file.path, exclude_folders):
255 return False
257 return True
259 vhdl_file_paths = [
260 hdl_file.path for hdl_file in hdl_files if file_should_be_included(hdl_file)
261 ]
263 # Sort by file name
264 def sort_key(path: Path) -> str:
265 return path.name
267 vhdl_files = sorted(vhdl_file_paths, key=sort_key)
269 return vhdl_files
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)
283 file_rst = vhdl_file_documentation.get_header_rst()
284 file_rst = "" if file_rst is None else file_rst
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 = ""
294 symbolator_rst = self._get_symbolator_rst(vhdl_file_documentation)
295 symbolator_rst = "" if symbolator_rst is None else symbolator_rst
297 entity_name = vhdl_file_path.stem
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 )
305 heading = f"{vhdl_file_path.name}"
306 heading_underline = heading_character * len(heading)
308 rst = f"""
309.. _{self._module.name}.{entity_name}:
311{heading}
312{heading_underline}
314{url_rst}
316{symbolator_rst}
318{file_rst}
320{resource_utilization_rst}
321"""
323 return rst
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 ""
334 indent = " "
335 rst = ".. symbolator::\n\n"
336 rst += indent + component.replace("\n", f"\n{indent}")
338 return rst
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)
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)
356 checkers.append(checker_dict)
358 # Make RST of the information
359 rst = ""
360 if generics:
361 heading = "Resource utilization"
362 heading_underline = heading_character * len(heading)
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}``"
370 rst = f"""
371.. _{self._module.name}.{entity_name}.resource_utilization:
373{heading}
374{heading_underline}
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.
382.. list-table:: Resource utilization for **{entity_name}** netlist builds.
383 :header-rows: 1
385"""
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)
394 # Fill in the header row
395 rst += " * - Generics\n"
396 for checker_name in checker_names:
397 rst += f" - {checker_name}\n"
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)
404 rst += f"""\
405 * - {generics_rst}"""
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 = ""
421 rst += f"""\
422{leader}(Using wrapper
424 {netlist_builds[build_idx].top}.vhd)"""
426 rst += "\n"
428 for checker_name in checker_names:
429 checker_value = checkers[build_idx][checker_name]
430 rst += f" - {checker_value}\n"
432 return rst