mirror of
				https://github.com/YosysHQ/yosys
				synced 2025-10-31 03:32:29 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			443 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			443 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/usr/bin/env python3
 | |
| from __future__ import annotations
 | |
| 
 | |
| from dataclasses import dataclass
 | |
| import json
 | |
| from pathlib import Path, PosixPath, WindowsPath
 | |
| import re
 | |
| 
 | |
| from typing import Any
 | |
| from sphinx.application import Sphinx
 | |
| from sphinx.ext import autodoc
 | |
| from sphinx.ext.autodoc import Documenter
 | |
| from sphinx.util import logging
 | |
| 
 | |
| logger = logging.getLogger(__name__)
 | |
| 
 | |
| # cmd signature
 | |
| cmd_ext_sig_re = re.compile(
 | |
|     r'''^ ([\w/]+::)?            # optional group
 | |
|           ([\w$._]+?)            # module name
 | |
|           (?:\.([\w_]+))?        # optional: thing name
 | |
|           (::[\w_]+)?            #           attribute
 | |
|           \s* $                  # and nothing more
 | |
|           ''', re.VERBOSE)
 | |
| 
 | |
| class YosysCmdContentListing:
 | |
|     type: str
 | |
|     body: str
 | |
|     source_file: str
 | |
|     source_line: int
 | |
|     options: dict[str, str]
 | |
|     content: list[YosysCmdContentListing]
 | |
| 
 | |
|     def __init__(
 | |
|             self,
 | |
|             type: str = "",
 | |
|             body: str = "",
 | |
|             source_file: str = "unknown",
 | |
|             source_line: int = 0,
 | |
|             options: dict[str, str] = {},
 | |
|             content: list[dict[str]] = [],
 | |
|     ):
 | |
|         self.type = type
 | |
|         self.body = body
 | |
|         self.source_file = source_file
 | |
|         self.source_line = source_line
 | |
|         self.options = options
 | |
|         self.content = [YosysCmdContentListing(**c) for c in content]
 | |
| 
 | |
| class YosysCmd:
 | |
|     name: str
 | |
|     title: str
 | |
|     content: list[YosysCmdContentListing]
 | |
|     group: str
 | |
|     source_file: str
 | |
|     source_line: int
 | |
|     source_func: str
 | |
|     experimental_flag: bool
 | |
|     internal_flag: bool
 | |
| 
 | |
|     def __init__(
 | |
|             self,
 | |
|             name:str = "", title:str = "",
 | |
|             content: list[dict[str]] = [],
 | |
|             group: str = 'unknown',
 | |
|             source_file: str = "",
 | |
|             source_line: int = 0,
 | |
|             source_func: str = "",
 | |
|             experimental_flag: bool = False,
 | |
|             internal_flag: bool = False,
 | |
|     ) -> None:
 | |
|         self.name = name
 | |
|         self.title = title
 | |
|         self.content = [YosysCmdContentListing(**c) for c in content]
 | |
|         self.group = group
 | |
|         self.source_file = source_file
 | |
|         self.source_line = source_line
 | |
|         self.source_func = source_func
 | |
|         self.experimental_flag = experimental_flag
 | |
|         self.internal_flag = internal_flag
 | |
|     
 | |
| class YosysCmdGroupDocumenter(Documenter):
 | |
|     objtype = 'cmdgroup'
 | |
|     priority = 10
 | |
|     object: tuple[str, list[str]]
 | |
|     lib_key = 'groups'
 | |
| 
 | |
|     option_spec = Documenter.option_spec.copy()
 | |
|     option_spec.update({
 | |
|         'caption': autodoc.annotation_option,
 | |
|         'members': autodoc.members_option,
 | |
|         'source': autodoc.bool_option,
 | |
|         'linenos': autodoc.bool_option,
 | |
|     })
 | |
| 
 | |
|     __cmd_lib: dict[str, list[str] | dict[str]] | None = None
 | |
|     @property
 | |
|     def cmd_lib(self) -> dict[str, list[str] | dict[str]]:
 | |
|         if not self.__cmd_lib:
 | |
|             self.__cmd_lib = {}
 | |
|             cmds_obj: dict[str, dict[str, dict[str]]]
 | |
|             try:
 | |
|                 with open(self.config.cmds_json, "r") as f:
 | |
|                     cmds_obj = json.loads(f.read())
 | |
|             except FileNotFoundError:
 | |
|                 logger.warning(
 | |
|                     f"unable to find cmd lib at {self.config.cmds_json}",
 | |
|                     type = 'cmdref',
 | |
|                     subtype = 'cmd_lib'
 | |
|                 )
 | |
|                 cmds_obj = {}
 | |
|             for (name, obj) in cmds_obj.get(self.lib_key, {}).items():
 | |
|                 self.__cmd_lib[name] = obj
 | |
|         return self.__cmd_lib
 | |
|     
 | |
|     @classmethod
 | |
|     def can_document_member(
 | |
|         cls,
 | |
|         member: Any,
 | |
|         membername: str,
 | |
|         isattr: bool,
 | |
|         parent: Any
 | |
|     ) -> bool:
 | |
|         return False
 | |
| 
 | |
|     def parse_name(self) -> bool:
 | |
|         if not self.options.caption:
 | |
|             self.content_indent = ''
 | |
|         self.fullname = self.modname = self.name
 | |
|         return True
 | |
|     
 | |
|     def import_object(self, raiseerror: bool = False) -> bool:
 | |
|         # get cmd
 | |
|         try:
 | |
|             self.object = (self.modname, self.cmd_lib[self.modname])
 | |
|         except KeyError:
 | |
|             if raiseerror:
 | |
|                 raise
 | |
|             return False
 | |
| 
 | |
|         self.real_modname = self.modname
 | |
|         return True
 | |
|     
 | |
|     def get_sourcename(self) -> str:
 | |
|         return self.env.doc2path(self.env.docname)
 | |
|     
 | |
|     def format_name(self) -> str:
 | |
|         return self.options.caption or ''
 | |
| 
 | |
|     def format_signature(self, **kwargs: Any) -> str:
 | |
|         return self.modname
 | |
|     
 | |
|     def add_directive_header(self, sig: str) -> None:
 | |
|         pass
 | |
|     
 | |
|     def add_content(self, more_content: Any | None) -> None:
 | |
|         pass
 | |
| 
 | |
|     def filter_members(
 | |
|         self,
 | |
|         members: list[tuple[str, Any]],
 | |
|         want_all: bool
 | |
|     ) -> list[tuple[str, Any, bool]]:
 | |
|         return [(x[0], x[1], False) for x in members]
 | |
| 
 | |
|     def get_object_members(
 | |
|         self,
 | |
|         want_all: bool
 | |
|     ) -> tuple[bool, list[tuple[str, Any]]]:
 | |
|         ret: list[tuple[str, str]] = []
 | |
| 
 | |
|         if want_all:
 | |
|             for member in self.object[1]:
 | |
|                 ret.append((member, self.modname))
 | |
|         else:
 | |
|             memberlist = self.options.members or []
 | |
|             for name in memberlist:
 | |
|                 if name in self.object:
 | |
|                     ret.append((name, self.modname))
 | |
|                 else:
 | |
|                     logger.warning(('unknown module mentioned in :members: option: '
 | |
|                                     f'group {self.modname}, module {name}'),
 | |
|                                    type='cmdref')
 | |
| 
 | |
|         return False, ret
 | |
| 
 | |
|     def document_members(self, all_members: bool = False) -> None:
 | |
|         want_all = (all_members or
 | |
|                     self.options.inherited_members or
 | |
|                     self.options.members is autodoc.ALL)
 | |
|         # find out which members are documentable
 | |
|         members_check_module, members = self.get_object_members(want_all)
 | |
| 
 | |
|         # document non-skipped members
 | |
|         memberdocumenters: list[tuple[Documenter, bool]] = []
 | |
|         for (mname, member, isattr) in self.filter_members(members, want_all):
 | |
|             classes = [cls for cls in self.documenters.values()
 | |
|                        if cls.can_document_member(member, mname, isattr, self)]
 | |
|             if not classes:
 | |
|                 # don't know how to document this member
 | |
|                 continue
 | |
|             # prefer the documenter with the highest priority
 | |
|             classes.sort(key=lambda cls: cls.priority)
 | |
|             # give explicitly separated module name, so that members
 | |
|             # of inner classes can be documented
 | |
|             full_mname = self.format_signature() + '::' + mname
 | |
|             documenter = classes[-1](self.directive, full_mname, self.indent)
 | |
|             memberdocumenters.append((documenter, isattr))
 | |
| 
 | |
|         member_order = self.options.member_order or self.config.autodoc_member_order
 | |
|         memberdocumenters = self.sort_members(memberdocumenters, member_order)
 | |
| 
 | |
|         for documenter, isattr in memberdocumenters:
 | |
|             documenter.generate(
 | |
|                 all_members=True, real_modname=self.real_modname,
 | |
|                 check_module=members_check_module and not isattr)
 | |
| 
 | |
|     def generate(
 | |
|         self,
 | |
|         more_content: Any | None = None,
 | |
|         real_modname: str | None = None,
 | |
|         check_module: bool = False,
 | |
|         all_members: bool = False
 | |
|     ) -> None:
 | |
|         if not self.parse_name():
 | |
|             # need a cmd lib to import from
 | |
|             logger.warning(
 | |
|                 f"don't know which cmd lib to import for autodocumenting {self.name}",
 | |
|                 type = 'cmdref'
 | |
|             )
 | |
|             return
 | |
| 
 | |
|         sourcename = self.get_sourcename()
 | |
| 
 | |
|         imported_object = self.import_object();
 | |
|         if self.lib_key == 'groups' and self.name == 'unknown':
 | |
|             if imported_object:
 | |
|                 logger.warning(f"Found commands assigned to group {self.name}: {[x[0] for x in self.object]}", type='cmdref')
 | |
|             else:
 | |
|                 return
 | |
|         elif not imported_object:
 | |
|             log_msg = f"unable to load {self.name} with {type(self)}"
 | |
|             if self.lib_key == 'groups':
 | |
|                 logger.info(log_msg, type = 'cmdref')
 | |
|                 self.add_line(f'.. warning:: No commands found for group {self.name!r}', sourcename)
 | |
|                 self.add_line('', sourcename)
 | |
|                 self.add_line('   Documentation may have been built without ``source_location`` support.', sourcename)
 | |
|                 self.add_line('   Try check :doc:`/cmd/index_other`.', sourcename)
 | |
|             else:
 | |
|                 logger.warning(log_msg, type = 'cmdref')
 | |
|             return
 | |
| 
 | |
|         # check __module__ of object (for members not given explicitly)
 | |
|         # if check_module:
 | |
|         #     if not self.check_module():
 | |
|         #         return
 | |
| 
 | |
|         self.add_line('', sourcename)
 | |
| 
 | |
|         # format the object's signature, if any
 | |
|         try:
 | |
|             sig = self.format_signature()
 | |
|         except Exception as exc:
 | |
|             logger.warning(('error while formatting signature for %s: %s'),
 | |
|                            self.fullname, exc, type='cmdref')
 | |
|             return
 | |
| 
 | |
|         # generate the directive header and options, if applicable
 | |
|         self.add_directive_header(sig)
 | |
|         self.add_line('', sourcename)
 | |
| 
 | |
|         # e.g. the module directive doesn't have content
 | |
|         self.indent += self.content_indent
 | |
| 
 | |
|         # add all content (from docstrings, attribute docs etc.)
 | |
|         self.add_content(more_content)
 | |
| 
 | |
|         # document members, if possible
 | |
|         self.document_members(all_members)
 | |
| 
 | |
| class YosysCmdDocumenter(YosysCmdGroupDocumenter):
 | |
|     objtype = 'cmd'
 | |
|     priority = 15
 | |
|     object: YosysCmd
 | |
|     lib_key = 'cmds'
 | |
| 
 | |
|     @classmethod
 | |
|     def can_document_member(
 | |
|         cls,
 | |
|         member: Any,
 | |
|         membername: str,
 | |
|         isattr: bool,
 | |
|         parent: Any
 | |
|     ) -> bool:
 | |
|         if membername.startswith('$'):
 | |
|             return False
 | |
|         return isinstance(parent, YosysCmdGroupDocumenter)
 | |
| 
 | |
|     def parse_name(self) -> bool:
 | |
|         try:
 | |
|             matched = cmd_ext_sig_re.match(self.name)
 | |
|             group, modname, thing, attribute = matched.groups()
 | |
|         except AttributeError:
 | |
|             logger.warning(('invalid signature for auto%s (%r)') % (self.objtype, self.name),
 | |
|                            type='cmdref')
 | |
|             return False
 | |
| 
 | |
|         self.modname = modname
 | |
|         self.groupname = group or ''
 | |
|         self.attribute = attribute or ''
 | |
|         self.fullname = ((self.modname) + (thing or ''))
 | |
| 
 | |
|         return True
 | |
| 
 | |
|     def import_object(self, raiseerror: bool = False) -> bool:
 | |
|         if super().import_object(raiseerror):
 | |
|             self.object = YosysCmd(self.modname, **self.object[1])
 | |
|             return True
 | |
|         return False
 | |
| 
 | |
|     def get_sourcename(self) -> str:
 | |
|         try:
 | |
|             return self.object.source_file
 | |
|         except AttributeError:
 | |
|             return super().get_sourcename()
 | |
|     
 | |
|     def format_name(self) -> str:
 | |
|         return self.object.name
 | |
| 
 | |
|     def format_signature(self, **kwargs: Any) -> str:
 | |
|         return self.fullname + self.attribute
 | |
| 
 | |
|     def add_directive_header(self, sig: str) -> None:
 | |
|         domain = getattr(self, 'domain', self.objtype)
 | |
|         directive = getattr(self, 'directivetype', 'def')
 | |
|         source_name = self.object.source_file
 | |
|         source_line = self.object.source_line
 | |
| 
 | |
|         title = f'{self.object.name} - {self.object.title}'
 | |
|         self.add_line(title, source_name, source_line)
 | |
|         self.add_line('#' * len(title), source_name, source_line)
 | |
| 
 | |
|         # cmd definition
 | |
|         self.add_line(f'.. {domain}:{directive}:: {sig}', source_name, source_line)
 | |
|         if self.object.title:
 | |
|             self.add_line(f'   :title: {self.object.title}', source_name, source_line)
 | |
| 
 | |
|         if self.options.noindex:
 | |
|             self.add_line('   :noindex:', source_name)
 | |
|     
 | |
|     def add_content(self, more_content: Any | None) -> None:
 | |
|         # set sourcename and add content from attribute documentation
 | |
|         domain = getattr(self, 'domain', self.objtype)
 | |
|         source_name = self.object.source_file
 | |
|         source_line = self.object.source_line
 | |
| 
 | |
|         if self.object.experimental_flag:
 | |
|             self.add_line(f'.. warning:: This command is experimental', source_name, source_line)
 | |
|             self.add_line('\n', source_name)
 | |
| 
 | |
|         if self.object.internal_flag:
 | |
|             self.add_line(f'.. warning:: This command is intended for internal developer use only', source_name, source_line)
 | |
|             self.add_line('\n', source_name)
 | |
| 
 | |
|         def render(content_list: YosysCmdContentListing, indent: int=0):
 | |
|             content_source = content_list.source_file or source_name
 | |
|             indent_str = '   '*indent
 | |
|             if content_list.type == 'usage':
 | |
|                 if content_list.body:
 | |
|                     self.add_line(f'{indent_str}.. {domain}:{content_list.type}:: {self.name}::{content_list.body}', content_source)
 | |
|                 else:
 | |
|                     self.add_line(f'{indent_str}.. {domain}:{content_list.type}:: {self.name}::', content_source)
 | |
|                     self.add_line(f'{indent_str}   :noindex:', source_name)
 | |
|                 self.add_line('', source_name)
 | |
|             elif content_list.type == 'option':
 | |
|                 self.add_line(f'{indent_str}:{content_list.type} {content_list.body}:', content_source)
 | |
|             elif content_list.type == 'text':
 | |
|                 self.add_line(f'{indent_str}{content_list.body}', content_source)
 | |
|                 self.add_line('', source_name)
 | |
|             elif content_list.type == 'code':
 | |
|                 language_str = content_list.options.get('language', '')
 | |
|                 self.add_line(f'{indent_str}.. code-block:: {language_str}', source_name)
 | |
|                 self.add_line('', source_name)
 | |
|                 for body_line in content_list.body.splitlines():
 | |
|                     self.add_line(f'{indent_str}   {body_line}', content_source)
 | |
|                 self.add_line('', source_name)
 | |
|             else:
 | |
|                 logger.warning(f"unknown content type '{content_list.type}'")
 | |
|             for content in content_list.content:
 | |
|                 render(content, indent+1)
 | |
| 
 | |
|         for content in self.object.content:
 | |
|             render(content)
 | |
| 
 | |
|         if self.get_sourcename() != 'unknown':
 | |
|             self.add_line('\n', source_name)
 | |
|             self.add_line(f'.. note:: Help text automatically generated from :file:`{source_name}:{source_line}`', source_name)
 | |
| 
 | |
|         # add additional content (e.g. from document), if present
 | |
|         if more_content:
 | |
|             for line, src in zip(more_content.data, more_content.items):
 | |
|                 self.add_line(line, src[0], src[1])
 | |
| 
 | |
|     def get_object_members(
 | |
|         self,
 | |
|         want_all: bool
 | |
|     ) -> tuple[bool, list[tuple[str, Any]]]:
 | |
| 
 | |
|         return False, []
 | |
| 
 | |
| class YosysCmdRstDocumenter(YosysCmdDocumenter):
 | |
|     objtype = 'cmd_rst'
 | |
|     priority = 0
 | |
| 
 | |
|     @classmethod
 | |
|     def can_document_member(cls, *args) -> bool:
 | |
|         return False
 | |
| 
 | |
|     def add_directive_header(self, sig):
 | |
|         source_name = self.object.source_file
 | |
|         cmd = self.object.name
 | |
|         self.add_line(f'.. code-block:: rst', source_name)
 | |
|         self.add_line(f'   :caption: Generated rst for ``.. autocmd:: {cmd}``', source_name)
 | |
| 
 | |
|     def add_content(self, more_content):
 | |
|         source_name = self.object.source_file
 | |
|         cmd = self.object.name
 | |
|         self.domain = 'cmd'
 | |
|         super().add_directive_header(cmd)
 | |
|         self.add_line('', source_name)
 | |
|         self.indent += self.content_indent
 | |
|         super().add_content(more_content)
 | |
| 
 | |
| def setup(app: Sphinx) -> dict[str, Any]:
 | |
|     app.add_config_value('cmds_json', False, 'html', [Path, PosixPath, WindowsPath])
 | |
|     app.setup_extension('sphinx.ext.autodoc')
 | |
|     app.add_autodocumenter(YosysCmdGroupDocumenter)
 | |
|     app.add_autodocumenter(YosysCmdDocumenter)
 | |
|     app.add_autodocumenter(YosysCmdRstDocumenter)
 | |
|     return {
 | |
|         'version': '2',
 | |
|         'parallel_read_safe': True,
 | |
|     }
 |