This commit is contained in:
2025-05-13 01:34:53 +03:00
parent 427735e23d
commit 83f3f1c7d4
945 changed files with 633484 additions and 0 deletions
+373
View File
@@ -0,0 +1,373 @@
from docutils import nodes
from docutils.nodes import Element, Node
from docutils.parsers.rst import Directive, directives
from docutils.statemachine import StringList
import os
import re
from sphinx import addnodes
from sphinx.application import Sphinx
from sphinx.util.docutils import SphinxDirective, switch_source_input
# from sphinx.util.typing import OptionSpec # not present in windows/pip3 sphinx install
from typing import Dict, Callable, Any, Tuple # needed to recreate OptionSpec
OptionSpec = Dict[str, Callable[[str], Any]] # sphinx.util.typing.OptionSpec from GitHub
import sys
from typing import List
sys.path.append(os.path.abspath('../tools/fibre-tools'))
import interface_parser
import type_registry
def load_file(name, state):
state.document.settings.record_dependencies.add(name)
def add_indent(lines: List[str], indent_depth=1):
return [(' ' * indent_depth + l) for l in lines]
def format_docstring(obj, indent_depth=1):
return [
*(['', *add_indent(obj.brief.split('\n'), indent_depth)] if obj.brief else []),
*(['', *add_indent(obj.doc.split('\n'), indent_depth)] if obj.doc else []),
]
class Documenter():
pass
class MethodDocumenter(Documenter):
objtype = 'method'
#def load_object(self, registry, name: str):
# return registry.get_method(name)
@staticmethod
def generate(registry, decl_ns_path, method):
in_str = ', '.join(arg.name for arg in method.input_args)
if len(method.output_args) == 0:
out_str = ''
elif len(method.output_args) == 1:
out_str = ' -> ' + registry.get_py_val_type_name(decl_ns_path, method.output_args[0].type)
else:
out_str = ' -> tuple[' + ', '.join(registry.get_py_val_type_name(decl_ns_path, arg.type) for arg in method.output_args) + ']'
return [
'',
'.. py:method:: ' + method.name + '(' + in_str + ')' + out_str,
*(format_docstring(method, indent_depth=1)),
'',
*((' :param ' + registry.get_py_val_type_name(decl_ns_path, arg.type) + ' ' + arg.name + ':' + (' ' + arg.doc if arg.doc else '')) for arg in method.input_args),
'',
]
class AttributeDocumenter(Documenter):
objtype = 'attribute'
@staticmethod
def generate(registry, decl_ns_path, attr):
return [
'',
'.. py:attribute:: ' + attr.name,
' :type: ' + registry.get_py_ref_type_name(decl_ns_path, attr.type),
*(format_docstring(attr, indent_depth=1)),
''
]
class EnumDocumenter(Documenter):
objtype = 'enum'
@staticmethod
def generate(registry, decl_ns_path, enum, options):
lines = [
'',
'.. py:class:: ' + registry.get_py_val_type_name(decl_ns_path, enum),
''
]
for enumerator in enum.enumerators:
lines += [
'',
' .. py:attribute:: ' + enumerator.name,
' :value: {} (0x{:X})'.format(enumerator.value, enumerator.value),
*(format_docstring(enumerator, indent_depth=2)),
''
]
return lines
class BitfieldDocumenter(Documenter):
objtype = 'bitfield'
@staticmethod
def generate(registry, decl_ns_path, bitfield, options):
lines = [
'',
'.. py:class:: ' + registry.get_py_val_type_name(decl_ns_path, bitfield),
''
]
for flag in bitfield.flags:
lines += [
'',
' .. py:attribute:: ' + flag.name,
' :value: {} (0x{:X})'.format(1 << flag.bit, 1 << flag.bit),
*(format_docstring(flag, indent_depth=2)),
''
]
return lines
class ClassDocumenter(Documenter):
objtype = 'class'
@staticmethod
def load_object(registry, name: str):
cls = registry.get_class(name)
return registry.get_containing_ns(cls), cls
@staticmethod
def generate(registry, decl_ns_path, cls, options):
lines = [
'',
'.. py:class:: ' + registry.get_py_ref_type_name(decl_ns_path, cls),
''
]
sub_decl_ns_path = registry.get_containing_ns(cls).get_path()[:2]
for method in cls.functions:
lines += add_indent(MethodDocumenter().generate(registry, sub_decl_ns_path, method))
for attr in cls.attributes:
lines += add_indent(AttributeDocumenter().generate(registry, sub_decl_ns_path, attr))
return lines
class NamespaceDocumenter(Documenter):
objtype = 'namespace'
@staticmethod
def load_object(registry, name: str):
ns = registry.ns_from_name(name)
return ns, ns
@staticmethod
def generate(registry, decl_ns_path, ns, options):
lines = []
for subtype in ns.types.values():
if isinstance(subtype, interface_parser.EnumInfo) and ('enums' in options):
lines += EnumDocumenter().generate(registry, decl_ns_path, subtype, options)
elif isinstance(subtype, interface_parser.BitfieldInfo) and ('bitfields' in options):
lines += BitfieldDocumenter().generate(registry, decl_ns_path, subtype, options)
elif isinstance(subtype, interface_parser.ClassInfo) and ('classes' in options):
lines += ClassDocumenter().generate(registry, decl_ns_path, subtype, options)
else:
raise Exception("Don't know how to document {} type".format(type(subtype)))
if 'namespaces' in options:
for sub_ns in ns.namespaces.values():
lines += NamespaceDocumenter().generate(registry, decl_ns_path, sub_ns, options)
return lines
documenter_list = [
BitfieldDocumenter,
EnumDocumenter,
ClassDocumenter,
NamespaceDocumenter,
]
documenters = {d.objtype: d for d in documenter_list}
class FibredocDirective(SphinxDirective):
"""
Analogous to Sphinx autodoc class `AutodocDirective`.
"""
required_arguments = 1
optional_arguments = 0
option_spec: OptionSpec = {
'bitfields': directives.flag,
'enums': directives.flag,
'classes': directives.flag,
'namespaces': directives.flag,
}
def run(self) -> List[Node]:
# look up target Documenter
objtype = self.name[5:] # strip prefix (fibre-).
documenter = documenters[objtype]()
registry = self.env.app.fibre_registry
for file in self.config.fibre_interface_files:
self.env.note_dependency(file)
decl_ns, obj = documenter.load_object(registry, self.arguments[0])
lines = documenter.generate(registry, decl_ns.get_path()[:2], obj, self.options)
result_rest = StringList()
for line in lines:
result_rest.append(line, 'fibre autogen output', 0)
#print("reST output: ", result_rest)
# Parse nested reST
with switch_source_input(self.state, result_rest):
node = nodes.paragraph()
node.document = self.state.document
self.state.nested_parse(result_rest, 0, node)
return node.children
class fibresummary_toc(nodes.comment):
pass
def autosummary_toc_visit_html(self: nodes.NodeVisitor, node: fibresummary_toc) -> None:
"""Hide autosummary toctree list in HTML output."""
raise nodes.SkipNode
def autosummary_noop(self: nodes.NodeVisitor, node: Node) -> None:
pass
class FibresummaryDirective(SphinxDirective):
required_arguments = 1
optional_arguments = 0
option_spec: OptionSpec = {
'caption': directives.unchanged_required,
}
def run(self) -> List[Node]:
nodes = [] # TODO: generate table
docnames = ['fibre_types/' + self.arguments[0].replace('.', '_')]
tocnode = addnodes.toctree()
tocnode['includefiles'] = docnames
tocnode['entries'] = [(None, docn) for docn in docnames]
tocnode['maxdepth'] = -1
tocnode['glob'] = None
tocnode['caption'] = self.options.get('caption')
nodes.append(fibresummary_toc('', '', tocnode))
return nodes
def load_yaml_files(app, config):
registry = type_registry.TypeRegistry()
loader = interface_parser.Loader(registry)
for file in config.fibre_interface_files:
loader.load_from_yaml_file(file)
registry.resolve_all()
app.fibre_registry = registry
def generate_stub_file(app: Sphinx, ns_path: Tuple[str], filename: str, deep: bool):
title = '.'.join(ns_path[2:]) + " Reference"
lines = [
title,
"=" * len(title),
]
parent_ns = app.fibre_registry.global_namespace.ns_from_path(ns_path[:-1], construct_if_missing=False)
if not parent_ns is None:
type = parent_ns.get_type(ns_path[-1], kind=None, construct_if_missing=False)
if not type is None:
lines.extend([
"",
".. fibreclass:: " + '.'.join(ns_path),
"",
])
#import ipdb; ipdb.set_trace()
ns = app.fibre_registry.global_namespace.ns_from_path(ns_path, construct_if_missing=False)
if not ns is None:
lines.extend([
"",
".. fibrenamespace:: " + '.'.join(ns_path),
" :bitfields:",
" :enums:",
" :classes:",
" :namespaces:",
"",
])
content = '\n'.join(lines)
if os.path.isfile(filename):
with open(filename) as fp:
old_content = fp.read()
if content == old_content:
return False
with open(filename, 'w') as fp:
fp.write(content)
return True
def find_autosummary_in_lines(lines, filename):
"""
Inspired by find_autosummary_in_lines in the autosummary extension
"""
autosummary_re = re.compile(r'^(\s*)\.\.\s+fibreautosummary::\s*([A-Za-z0-9_.:]+)\s*$')
documented = []
for line in lines:
m = autosummary_re.match(line)
if m:
indent, name = m.groups()
path = os.path.join(os.path.dirname(filename), 'fibre_types')
documented.append((path, tuple(name.split('.'))))
return documented
def generate_stub_files(app: Sphinx):
"""
Inspired by process_generate_options in the autosummary extension.
"""
env = app.builder.env
genfiles = [env.doc2path(x, base=None) for x in env.found_docs
if os.path.isfile(env.doc2path(x))]
# read
documented = []
for filename in genfiles:
with open(filename, encoding='utf-8', errors='ignore') as f:
lines = f.read().splitlines()
documented.extend(find_autosummary_in_lines(lines, filename=filename))
# write
for out_dir, ns_name in documented:
os.makedirs(out_dir, exist_ok=True)
out_file = os.path.join(out_dir, '_'.join(ns_name) + '.rst')
generate_stub_file(app, ns_name, out_file, True)
def setup(app):
app.add_node(fibresummary_toc,
html=(autosummary_toc_visit_html, autosummary_noop),
latex=(autosummary_noop, autosummary_noop),
text=(autosummary_noop, autosummary_noop),
man=(autosummary_noop, autosummary_noop),
texinfo=(autosummary_noop, autosummary_noop))
app.add_config_value('fibre_interface_files', [], 'html')
for d in documenter_list:
app.add_directive('fibre' + d.objtype, FibredocDirective)
app.add_directive('fibreautosummary', FibresummaryDirective)
app.connect('config-inited', load_yaml_files)
app.connect('builder-inited', generate_stub_files)
return {
'version': '0.1',
'parallel_read_safe': False, # global state: loaded interfaces
'parallel_write_safe': True,
}