mirror of
https://github.com/YosysHQ/yosys
synced 2025-07-26 22:17:55 +00:00
Keep techlibs folder hierarchy. techlibs/* and passes/* groups are now nested under index_techlibs and index_passes respectively, with most (all?) of the passes/* pages getting proper headings, as well as backends/frontends/kernel. `index_passes_techmap` also references `index_techlibs`. Split command reference toc in twain, one with maxdepth=2 and one with maxdepth=3, since passes and techlibs now have an extra level of nesting. Move the `cmd_ref` link to the command reference, instead of top of the page. Remove `index_internal` and `index_other` from the toc, and mark the pages as orphan. Internal commands get a note callout after the command reference toc (although this doesn't work for the pdf build), while other commands are linked in the warning for missing `source_location` (since that *should* be the only time when there are any commands in the "unknown" group). Update autodoc extension versions, and mark the directives extension as not `parallel_read_safe` (it might be, but I'm not sure about how the xref lookups work if it is parallel so better to be safe).
413 lines
14 KiB
Python
413 lines
14 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
|
|
content: list[YosysCmdContentListing]
|
|
|
|
def __init__(
|
|
self,
|
|
type: str = "",
|
|
body: str = "",
|
|
source_file: str = "unknown",
|
|
source_line: int = 0,
|
|
content: list[dict[str]] = [],
|
|
):
|
|
self.type = type
|
|
self.body = body
|
|
self.source_file = source_file
|
|
self.source_line = source_line
|
|
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 = {
|
|
'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 in ['usage', 'optiongroup']:
|
|
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 in ['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':
|
|
self.add_line(f'{indent_str}::', source_name)
|
|
self.add_line('', source_name)
|
|
self.add_line(f'{indent_str} {content_list.body}', 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, []
|
|
|
|
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)
|
|
return {
|
|
'version': '2',
|
|
'parallel_read_safe': True,
|
|
}
|