*
This commit is contained in:
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user