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
« 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# --------------------------------------------------------------------------------------------------
9# First party libraries
10from tsfpga.system_utils import create_file, file_is_in_directory, read_file
11from tsfpga.vhdl_file_documentation import VhdlFileDocumentation
14class ModuleDocumentation:
16 """
17 Methods for generating a reStructuredText document with module documentation.
18 The content is extracted from VHDL source file headers.
19 """
21 def __init__(self, module):
22 """
23 Arguments:
24 module (:class:`.BaseModule`): The module which shall be documented.
25 """
26 self._module = module
28 def get_overview_rst(self):
29 """
30 Get the contents of the module's ``doc/<name>.rst``, i.e. the module "overview" document.
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)
39 return None
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`.
49 Arguments:
50 heading_character (str): Character to use for heading underline.
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:
61{heading}
62{heading_underline}
64This module has register definitions.
65Please see :download:`separate HTML page <{self._module.name}_regs.html>` for \
66register documentation.
67"""
69 return None
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.
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"].
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]
98 all_builds = self._module.get_build_projects()
100 rst = ""
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}"
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)
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 )
124 return rst
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.
131 Arguments:
132 exclude_module_folders (list(str)): Folder names within the module root that shall be
133 excluded from documentation.
135 Returns:
136 str: An RST document.
137 """
138 heading_character_1 = "="
139 heading_character_2 = "-"
140 heading_character_3 = "_"
142 heading = f"Module {self._module.name}"
143 heading_underline = heading_character_1 * len(heading)
145 overview_rst = self.get_overview_rst()
146 overview_rst = "" if overview_rst is None else overview_rst
148 registers_rst = self.get_register_rst(heading_character=heading_character_2)
149 registers_rst = "" if registers_rst is None else registers_rst
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 )
157 rst = f"""\
159.. _module_{self._module.name}:
161{heading}
162{heading_underline}
164This document contains technical documentation for the ``{self._module.name}`` module.
166{overview_rst}
168{registers_rst}
170{submodule_rst}
171"""
173 return rst
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.
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)
190 rst = self.get_rst_document(exclude_module_folders=exclude_module_folders)
191 create_file(output_path / f"{self._module.name}.rst", contents=rst)
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)
199 module_regs_pkg = self._module.path / f"{self._module.name}_regs_pkg.vhd"
201 def file_should_be_included(hdl_file):
202 if file_is_in_directory(hdl_file.path, exclude_folders):
203 return False
205 if not hdl_file.is_vhdl:
206 return False
208 if hdl_file.path == module_regs_pkg:
209 return False
211 return True
213 vhdl_files = [hdl_file for hdl_file in hdl_files if file_should_be_included(hdl_file)]
215 # Sort by file name
216 def sort_key(vhdl_file):
217 return vhdl_file.path.name
219 vhdl_files = sorted(vhdl_files, key=sort_key)
221 return vhdl_files
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)
231 file_rst = vhdl_file_documentation.get_header_rst()
232 file_rst = "" if file_rst is None else file_rst
234 symbolator_rst = self._get_symbolator_rst(vhdl_file_documentation)
235 symbolator_rst = "" if symbolator_rst is None else symbolator_rst
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 )
243 entity_name = vhdl_file_path.stem
244 heading = f"{vhdl_file_path.name}"
245 heading_underline = heading_character * len(heading)
247 rst = f"""
248.. _{self._module.name}.{entity_name}:
250{heading}
251{heading_underline}
253{symbolator_rst}
255{file_rst}
257{resource_utilization_rst}
258"""
260 return rst
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 ""
271 indent = " "
272 rst = ".. symbolator::\n\n"
273 rst += indent + component.replace("\n", f"\n{indent}")
275 return rst
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)
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)
296 checkers.append(checker_dict)
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}
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.
313.. list-table:: Resource utilization for {vhdl_file_path.name} netlist builds.
314 :header-rows: 1
316"""
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)
325 # Fill in the header row
326 rst += " * - Generics\n"
327 for checker_name in checker_names:
328 rst += f" - {checker_name}\n"
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)
335 rst += f"""\
336 * - {generics_rst}"""
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 = ""
352 rst += f"""\
353{leader}(Using wrapper
355 {netlist_builds[build_idx].top}.vhd)"""
357 rst += "\n"
359 for checker_name in checker_names:
360 checker_value = checkers[build_idx][checker_name]
361 rst += f" - {checker_value}\n"
363 return rst