374 lines
12 KiB
Python
374 lines
12 KiB
Python
|
|
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,
|
|
}
|