refactor(gen_vimdoc): use typing for function API vimdoc generation

This commit is contained in:
Jongwook Choi 2023-12-28 14:11:13 -05:00
parent 5e2d4b3c4d
commit 1a31d4cf2b

View File

@ -112,6 +112,11 @@ lua2dox = os.path.join(base_dir, 'scripts', 'lua2dox.lua')
SectionName = str SectionName = str
Docstring = str # Represents (formatted) vimdoc string
FunctionName = str
@dataclasses.dataclass @dataclasses.dataclass
class Config: class Config:
"""Config for documentation.""" """Config for documentation."""
@ -881,6 +886,44 @@ def is_program_listing(para):
return len(children) == 1 and children[0].nodeName == 'programlisting' return len(children) == 1 and children[0].nodeName == 'programlisting'
FunctionParam = Tuple[
str, # type
str, # parameter name
]
@dataclasses.dataclass
class FunctionDoc:
"""Data structure for function documentation. Also exported as msgpack."""
annotations: List[str]
"""Attributes, e.g., FUNC_API_REMOTE_ONLY. See annotation_map"""
signature: str
"""Function signature with *tags*."""
parameters: List[FunctionParam]
"""Parameters: (type, name)"""
parameters_doc: Dict[str, Docstring]
"""Parameters documentation. Key is parameter name, value is doc."""
doc: List[Docstring]
"""Main description for the function. Separated by paragraph."""
return_: List[Docstring]
"""Return:, or Return (multiple): (@return strings)"""
seealso: List[Docstring]
"""See also: (@see strings)"""
# for fmt_node_as_vimhelp
desc_node: Element | None = None
brief_desc_node: Element | None = None
# for INCLUDE_C_DECL
c_decl: str | None = None
def fmt_node_as_vimhelp(parent: Element, width=text_width - indentation, indent='', def fmt_node_as_vimhelp(parent: Element, width=text_width - indentation, indent='',
fmt_vimhelp=False): fmt_vimhelp=False):
"""Renders (nested) Doxygen <para> nodes as Vim :help text. """Renders (nested) Doxygen <para> nodes as Vim :help text.
@ -946,7 +989,10 @@ def fmt_node_as_vimhelp(parent: Element, width=text_width - indentation, indent=
return clean_lines('\n'.join(rendered_blocks).strip()) return clean_lines('\n'.join(rendered_blocks).strip())
def extract_from_xml(filename, target, width, fmt_vimhelp): def extract_from_xml(filename, target, width, fmt_vimhelp) -> Tuple[
Dict[FunctionName, FunctionDoc],
Dict[FunctionName, FunctionDoc],
]:
"""Extracts Doxygen info as maps without formatting the text. """Extracts Doxygen info as maps without formatting the text.
Returns two maps: Returns two maps:
@ -958,8 +1004,8 @@ def extract_from_xml(filename, target, width, fmt_vimhelp):
""" """
config: Config = CONFIG[target] config: Config = CONFIG[target]
fns = {} # Map of func_name:docstring. fns: Dict[FunctionName, FunctionDoc] = {}
deprecated_fns = {} # Map of func_name:docstring. deprecated_fns: Dict[FunctionName, FunctionDoc] = {}
dom = minidom.parse(filename) dom = minidom.parse(filename)
compoundname = get_text(dom.getElementsByTagName('compoundname')[0]) compoundname = get_text(dom.getElementsByTagName('compoundname')[0])
@ -1084,7 +1130,7 @@ def extract_from_xml(filename, target, width, fmt_vimhelp):
# Tracks `xrefsect` titles. As of this writing, used only for separating # Tracks `xrefsect` titles. As of this writing, used only for separating
# deprecated functions. # deprecated functions.
xrefs_all = set() xrefs_all = set()
paras = [] paras: List[Dict[str, Any]] = []
brief_desc = find_first(member, 'briefdescription') brief_desc = find_first(member, 'briefdescription')
if brief_desc: if brief_desc:
for child in brief_desc.childNodes: for child in brief_desc.childNodes:
@ -1103,47 +1149,48 @@ def extract_from_xml(filename, target, width, fmt_vimhelp):
desc.toprettyxml(indent=' ', newl='\n')), desc.toprettyxml(indent=' ', newl='\n')),
' ' * indentation)) ' ' * indentation))
fn = { fn = FunctionDoc(
'annotations': list(annotations), annotations=list(annotations),
'signature': signature, signature=signature,
'parameters': params, parameters=params,
'parameters_doc': collections.OrderedDict(), parameters_doc=collections.OrderedDict(),
'doc': [], doc=[],
'return': [], return_=[],
'seealso': [], seealso=[],
} )
if fmt_vimhelp: if fmt_vimhelp:
fn['desc_node'] = desc fn.desc_node = desc
fn['brief_desc_node'] = brief_desc fn.brief_desc_node = brief_desc
for m in paras: for m in paras:
if 'text' in m: if m.get('text', ''):
if not m['text'] == '': fn.doc.append(m['text'])
fn['doc'].append(m['text'])
if 'params' in m: if 'params' in m:
# Merge OrderedDicts. # Merge OrderedDicts.
fn['parameters_doc'].update(m['params']) fn.parameters_doc.update(m['params'])
if 'return' in m and len(m['return']) > 0: if 'return' in m and len(m['return']) > 0:
fn['return'] += m['return'] fn.return_ += m['return']
if 'seealso' in m and len(m['seealso']) > 0: if 'seealso' in m and len(m['seealso']) > 0:
fn['seealso'] += m['seealso'] fn.seealso += m['seealso']
if INCLUDE_C_DECL: if INCLUDE_C_DECL:
fn['c_decl'] = c_decl fn.c_decl = c_decl
if 'Deprecated' in str(xrefs_all): if 'Deprecated' in str(xrefs_all):
deprecated_fns[name] = fn deprecated_fns[name] = fn
elif name.startswith(config.fn_name_prefix): elif name.startswith(config.fn_name_prefix):
fns[name] = fn fns[name] = fn
# sort functions by name (lexicographically)
fns = collections.OrderedDict(sorted( fns = collections.OrderedDict(sorted(
fns.items(), fns.items(),
key=lambda key_item_tuple: key_item_tuple[0].lower())) key=lambda key_item_tuple: key_item_tuple[0].lower(),
))
deprecated_fns = collections.OrderedDict(sorted(deprecated_fns.items())) deprecated_fns = collections.OrderedDict(sorted(deprecated_fns.items()))
return fns, deprecated_fns return fns, deprecated_fns
def fmt_doxygen_xml_as_vimhelp(filename, target): def fmt_doxygen_xml_as_vimhelp(filename, target) -> Tuple[Docstring, Docstring]:
"""Entrypoint for generating Vim :help from from Doxygen XML. """Entrypoint for generating Vim :help from from Doxygen XML.
Returns 2 items: Returns 2 items:
@ -1154,20 +1201,26 @@ def fmt_doxygen_xml_as_vimhelp(filename, target):
fns_txt = {} # Map of func_name:vim-help-text. fns_txt = {} # Map of func_name:vim-help-text.
deprecated_fns_txt = {} # Map of func_name:vim-help-text. deprecated_fns_txt = {} # Map of func_name:vim-help-text.
fns: Dict[FunctionName, FunctionDoc]
fns, _ = extract_from_xml(filename, target, text_width, True) fns, _ = extract_from_xml(filename, target, text_width, True)
for name, fn in fns.items(): for fn_name, fn in fns.items():
# Generate Vim :help for parameters. # Generate Vim :help for parameters.
if fn['desc_node']:
doc = fmt_node_as_vimhelp(fn['desc_node'], fmt_vimhelp=True) # Generate body.
if not doc and fn['brief_desc_node']: doc = ''
doc = fmt_node_as_vimhelp(fn['brief_desc_node']) if fn.desc_node:
if not doc and name.startswith("nvim__"): doc = fmt_node_as_vimhelp(fn.desc_node, fmt_vimhelp=True)
if not doc and fn.brief_desc_node:
doc = fmt_node_as_vimhelp(fn.brief_desc_node)
if not doc and fn_name.startswith("nvim__"):
continue continue
if not doc: if not doc:
doc = 'TODO: Documentation' doc = 'TODO: Documentation'
annotations = '\n'.join(fn['annotations']) # Annotations: put before Parameters
annotations: str = '\n'.join(fn.annotations)
if annotations: if annotations:
annotations = ('\n\nAttributes: ~\n' + annotations = ('\n\nAttributes: ~\n' +
textwrap.indent(annotations, ' ')) textwrap.indent(annotations, ' '))
@ -1177,18 +1230,22 @@ def fmt_doxygen_xml_as_vimhelp(filename, target):
else: else:
doc = doc[:i] + annotations + '\n\n' + doc[i:] doc = doc[:i] + annotations + '\n\n' + doc[i:]
# C Declaration: (debug only)
if INCLUDE_C_DECL: if INCLUDE_C_DECL:
doc += '\n\nC Declaration: ~\n>\n' doc += '\n\nC Declaration: ~\n>\n'
doc += fn['c_decl'] assert fn.c_decl is not None
doc += fn.c_decl
doc += '\n<' doc += '\n<'
func_doc = fn['signature'] + '\n' # Start of function documentations. e.g.,
# nvim_cmd({*cmd}, {*opts}) *nvim_cmd()*
func_doc = fn.signature + '\n'
func_doc += textwrap.indent(clean_lines(doc), ' ' * indentation) func_doc += textwrap.indent(clean_lines(doc), ' ' * indentation)
# Verbatim handling. # Verbatim handling.
func_doc = re.sub(r'^\s+([<>])$', r'\1', func_doc, flags=re.M) func_doc = re.sub(r'^\s+([<>])$', r'\1', func_doc, flags=re.M)
split_lines = func_doc.split('\n') split_lines: List[str] = func_doc.split('\n')
start = 0 start = 0
while True: while True:
try: try:
@ -1214,12 +1271,14 @@ def fmt_doxygen_xml_as_vimhelp(filename, target):
func_doc = "\n".join(map(align_tags, split_lines)) func_doc = "\n".join(map(align_tags, split_lines))
if (name.startswith(config.fn_name_prefix) if (fn_name.startswith(config.fn_name_prefix)
and name != "nvim_error_event"): and fn_name != "nvim_error_event"):
fns_txt[name] = func_doc fns_txt[fn_name] = func_doc
return ('\n\n'.join(list(fns_txt.values())), return (
'\n\n'.join(list(deprecated_fns_txt.values()))) '\n\n'.join(list(fns_txt.values())),
'\n\n'.join(list(deprecated_fns_txt.values())),
)
def delete_lines_below(filename, tokenstr): def delete_lines_below(filename, tokenstr):
@ -1402,9 +1461,11 @@ def main(doxygen_config, args):
with open(doc_file, 'ab') as fp: with open(doc_file, 'ab') as fp:
fp.write(docs.encode('utf8')) fp.write(docs.encode('utf8'))
fn_map_full = collections.OrderedDict(sorted(fn_map_full.items())) fn_map_full = collections.OrderedDict(sorted(
(name, fn_doc.__dict__) for (name, fn_doc) in fn_map_full.items()
))
with open(mpack_file, 'wb') as fp: with open(mpack_file, 'wb') as fp:
fp.write(msgpack.packb(fn_map_full, use_bin_type=True)) fp.write(msgpack.packb(fn_map_full, use_bin_type=True)) # type: ignore
if not args.keep_tmpfiles: if not args.keep_tmpfiles:
shutil.rmtree(output_dir) shutil.rmtree(output_dir)