Coverage for tsfpga/module_documentation.py: 64%
138 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-28 08:42 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-28 08:42 +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._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 = ""
181 overview_rst = self.get_overview_rst()
182 overview_rst = "" if overview_rst is None else overview_rst
184 registers_rst = self.get_register_rst(heading_character=heading_character_2)
185 registers_rst = "" if registers_rst is None else registers_rst
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 )
193 rst = f"""\
195.. _module_{self._module.name}:
197{heading}
198{heading_underline}
200This document contains technical documentation for the ``{self._module.name}`` module.
201{url_rst}
203{overview_rst}
205{registers_rst}
207{submodule_rst}
208"""
210 return rst
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.
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()
229 rst = self.get_rst_document(exclude_module_folders=exclude_module_folders)
230 create_file(output_path / f"{self._module.name}.rst", contents=rst)
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 )
243 def file_should_be_included(hdl_file: "HdlFile") -> bool:
244 if file_is_in_directory(hdl_file.path, exclude_folders):
245 return False
247 return True
249 vhdl_file_paths = [
250 hdl_file.path for hdl_file in hdl_files if file_should_be_included(hdl_file)
251 ]
253 # Sort by file name
254 def sort_key(path: Path) -> str:
255 return path.name
257 vhdl_files = sorted(vhdl_file_paths, key=sort_key)
259 return vhdl_files
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)
273 file_rst = vhdl_file_documentation.get_header_rst()
274 file_rst = "" if file_rst is None else file_rst
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 = ""
284 symbolator_rst = self._get_symbolator_rst(vhdl_file_documentation)
285 symbolator_rst = "" if symbolator_rst is None else symbolator_rst
287 entity_name = vhdl_file_path.stem
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 )
295 heading = f"{vhdl_file_path.name}"
296 heading_underline = heading_character * len(heading)
298 rst = f"""
299.. _{self._module.name}.{entity_name}:
301{heading}
302{heading_underline}
304{url_rst}
306{symbolator_rst}
308{file_rst}
310{resource_utilization_rst}
311"""
313 return rst
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 ""
324 indent = " "
325 rst = ".. symbolator::\n\n"
326 rst += indent + component.replace("\n", f"\n{indent}")
328 return rst
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)
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)
346 checkers.append(checker_dict)
348 # Make RST of the information
349 rst = ""
350 if generics:
351 heading = "Resource utilization"
352 heading_underline = heading_character * len(heading)
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}``"
360 rst = f"""
361.. _{self._module.name}.{entity_name}.resource_utilization:
363{heading}
364{heading_underline}
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.
372.. list-table:: Resource utilization for **{entity_name}** netlist builds.
373 :header-rows: 1
375"""
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)
384 # Fill in the header row
385 rst += " * - Generics\n"
386 for checker_name in checker_names:
387 rst += f" - {checker_name}\n"
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)
394 rst += f"""\
395 * - {generics_rst}"""
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 = ""
411 rst += f"""\
412{leader}(Using wrapper
414 {netlist_builds[build_idx].top}.vhd)"""
416 rst += "\n"
418 for checker_name in checker_names:
419 checker_value = checkers[build_idx][checker_name]
420 rst += f" - {checker_value}\n"
422 return rst