17640410dSJohn Snow# coding=utf-8 2*45d483a8SJohn Snow# type: ignore 37640410dSJohn Snow# 47640410dSJohn Snow# QEMU qapidoc QAPI file parsing extension 57640410dSJohn Snow# 67640410dSJohn Snow# Copyright (c) 2020 Linaro 77640410dSJohn Snow# 87640410dSJohn Snow# This work is licensed under the terms of the GNU GPLv2 or later. 97640410dSJohn Snow# See the COPYING file in the top-level directory. 107640410dSJohn Snow 117640410dSJohn Snow""" 127640410dSJohn Snowqapidoc is a Sphinx extension that implements the qapi-doc directive 137640410dSJohn Snow 147640410dSJohn SnowThe purpose of this extension is to read the documentation comments 157640410dSJohn Snowin QAPI schema files, and insert them all into the current document. 167640410dSJohn Snow 177640410dSJohn SnowIt implements one new rST directive, "qapi-doc::". 187640410dSJohn SnowEach qapi-doc:: directive takes one argument, which is the 197640410dSJohn Snowpathname of the schema file to process, relative to the source tree. 207640410dSJohn Snow 217640410dSJohn SnowThe docs/conf.py file must set the qapidoc_srctree config value to 227640410dSJohn Snowthe root of the QEMU source tree. 237640410dSJohn Snow 247640410dSJohn SnowThe Sphinx documentation on writing extensions is at: 257640410dSJohn Snowhttps://www.sphinx-doc.org/en/master/development/index.html 267640410dSJohn Snow""" 277640410dSJohn Snow 287640410dSJohn Snowimport re 297640410dSJohn Snowimport textwrap 307640410dSJohn Snow 317640410dSJohn Snowfrom docutils import nodes 327640410dSJohn Snowfrom docutils.statemachine import ViewList 337640410dSJohn Snowfrom qapi.error import QAPISemError 347640410dSJohn Snowfrom qapi.gen import QAPISchemaVisitor 357640410dSJohn Snowfrom qapi.parser import QAPIDoc 367640410dSJohn Snow 377640410dSJohn Snow 387640410dSJohn Snowdef dedent(text: str) -> str: 397640410dSJohn Snow # Adjust indentation to make description text parse as paragraph. 407640410dSJohn Snow 417640410dSJohn Snow lines = text.splitlines(True) 427640410dSJohn Snow if re.match(r"\s+", lines[0]): 437640410dSJohn Snow # First line is indented; description started on the line after 447640410dSJohn Snow # the name. dedent the whole block. 457640410dSJohn Snow return textwrap.dedent(text) 467640410dSJohn Snow 477640410dSJohn Snow # Descr started on same line. Dedent line 2+. 487640410dSJohn Snow return lines[0] + textwrap.dedent("".join(lines[1:])) 497640410dSJohn Snow 507640410dSJohn Snow 517640410dSJohn Snowclass QAPISchemaGenRSTVisitor(QAPISchemaVisitor): 527640410dSJohn Snow """A QAPI schema visitor which generates docutils/Sphinx nodes 537640410dSJohn Snow 547640410dSJohn Snow This class builds up a tree of docutils/Sphinx nodes corresponding 557640410dSJohn Snow to documentation for the various QAPI objects. To use it, first 567640410dSJohn Snow create a QAPISchemaGenRSTVisitor object, and call its 577640410dSJohn Snow visit_begin() method. Then you can call one of the two methods 587640410dSJohn Snow 'freeform' (to add documentation for a freeform documentation 597640410dSJohn Snow chunk) or 'symbol' (to add documentation for a QAPI symbol). These 607640410dSJohn Snow will cause the visitor to build up the tree of document 617640410dSJohn Snow nodes. Once you've added all the documentation via 'freeform' and 627640410dSJohn Snow 'symbol' method calls, you can call 'get_document_nodes' to get 637640410dSJohn Snow the final list of document nodes (in a form suitable for returning 647640410dSJohn Snow from a Sphinx directive's 'run' method). 657640410dSJohn Snow """ 667640410dSJohn Snow def __init__(self, sphinx_directive): 677640410dSJohn Snow self._cur_doc = None 687640410dSJohn Snow self._sphinx_directive = sphinx_directive 697640410dSJohn Snow self._top_node = nodes.section() 707640410dSJohn Snow self._active_headings = [self._top_node] 717640410dSJohn Snow 727640410dSJohn Snow def _make_dlitem(self, term, defn): 737640410dSJohn Snow """Return a dlitem node with the specified term and definition. 747640410dSJohn Snow 757640410dSJohn Snow term should be a list of Text and literal nodes. 767640410dSJohn Snow defn should be one of: 777640410dSJohn Snow - a string, which will be handed to _parse_text_into_node 787640410dSJohn Snow - a list of Text and literal nodes, which will be put into 797640410dSJohn Snow a paragraph node 807640410dSJohn Snow """ 817640410dSJohn Snow dlitem = nodes.definition_list_item() 827640410dSJohn Snow dlterm = nodes.term('', '', *term) 837640410dSJohn Snow dlitem += dlterm 847640410dSJohn Snow if defn: 857640410dSJohn Snow dldef = nodes.definition() 867640410dSJohn Snow if isinstance(defn, list): 877640410dSJohn Snow dldef += nodes.paragraph('', '', *defn) 887640410dSJohn Snow else: 897640410dSJohn Snow self._parse_text_into_node(defn, dldef) 907640410dSJohn Snow dlitem += dldef 917640410dSJohn Snow return dlitem 927640410dSJohn Snow 937640410dSJohn Snow def _make_section(self, title): 947640410dSJohn Snow """Return a section node with optional title""" 957640410dSJohn Snow section = nodes.section(ids=[self._sphinx_directive.new_serialno()]) 967640410dSJohn Snow if title: 977640410dSJohn Snow section += nodes.title(title, title) 987640410dSJohn Snow return section 997640410dSJohn Snow 1007640410dSJohn Snow def _nodes_for_ifcond(self, ifcond, with_if=True): 1017640410dSJohn Snow """Return list of Text, literal nodes for the ifcond 1027640410dSJohn Snow 1037640410dSJohn Snow Return a list which gives text like ' (If: condition)'. 1047640410dSJohn Snow If with_if is False, we don't return the "(If: " and ")". 1057640410dSJohn Snow """ 1067640410dSJohn Snow 1077640410dSJohn Snow doc = ifcond.docgen() 1087640410dSJohn Snow if not doc: 1097640410dSJohn Snow return [] 1107640410dSJohn Snow doc = nodes.literal('', doc) 1117640410dSJohn Snow if not with_if: 1127640410dSJohn Snow return [doc] 1137640410dSJohn Snow 1147640410dSJohn Snow nodelist = [nodes.Text(' ('), nodes.strong('', 'If: ')] 1157640410dSJohn Snow nodelist.append(doc) 1167640410dSJohn Snow nodelist.append(nodes.Text(')')) 1177640410dSJohn Snow return nodelist 1187640410dSJohn Snow 1197640410dSJohn Snow def _nodes_for_one_member(self, member): 1207640410dSJohn Snow """Return list of Text, literal nodes for this member 1217640410dSJohn Snow 1227640410dSJohn Snow Return a list of doctree nodes which give text like 1237640410dSJohn Snow 'name: type (optional) (If: ...)' suitable for use as the 1247640410dSJohn Snow 'term' part of a definition list item. 1257640410dSJohn Snow """ 1267640410dSJohn Snow term = [nodes.literal('', member.name)] 1277640410dSJohn Snow if member.type.doc_type(): 1287640410dSJohn Snow term.append(nodes.Text(': ')) 1297640410dSJohn Snow term.append(nodes.literal('', member.type.doc_type())) 1307640410dSJohn Snow if member.optional: 1317640410dSJohn Snow term.append(nodes.Text(' (optional)')) 1327640410dSJohn Snow if member.ifcond.is_present(): 1337640410dSJohn Snow term.extend(self._nodes_for_ifcond(member.ifcond)) 1347640410dSJohn Snow return term 1357640410dSJohn Snow 1367640410dSJohn Snow def _nodes_for_variant_when(self, branches, variant): 1377640410dSJohn Snow """Return list of Text, literal nodes for variant 'when' clause 1387640410dSJohn Snow 1397640410dSJohn Snow Return a list of doctree nodes which give text like 1407640410dSJohn Snow 'when tagname is variant (If: ...)' suitable for use in 1417640410dSJohn Snow the 'branches' part of a definition list. 1427640410dSJohn Snow """ 1437640410dSJohn Snow term = [nodes.Text(' when '), 1447640410dSJohn Snow nodes.literal('', branches.tag_member.name), 1457640410dSJohn Snow nodes.Text(' is '), 1467640410dSJohn Snow nodes.literal('', '"%s"' % variant.name)] 1477640410dSJohn Snow if variant.ifcond.is_present(): 1487640410dSJohn Snow term.extend(self._nodes_for_ifcond(variant.ifcond)) 1497640410dSJohn Snow return term 1507640410dSJohn Snow 1517640410dSJohn Snow def _nodes_for_members(self, doc, what, base=None, branches=None): 1527640410dSJohn Snow """Return list of doctree nodes for the table of members""" 1537640410dSJohn Snow dlnode = nodes.definition_list() 1547640410dSJohn Snow for section in doc.args.values(): 1557640410dSJohn Snow term = self._nodes_for_one_member(section.member) 1567640410dSJohn Snow # TODO drop fallbacks when undocumented members are outlawed 1577640410dSJohn Snow if section.text: 1587640410dSJohn Snow defn = dedent(section.text) 1597640410dSJohn Snow else: 1607640410dSJohn Snow defn = [nodes.Text('Not documented')] 1617640410dSJohn Snow 1627640410dSJohn Snow dlnode += self._make_dlitem(term, defn) 1637640410dSJohn Snow 1647640410dSJohn Snow if base: 1657640410dSJohn Snow dlnode += self._make_dlitem([nodes.Text('The members of '), 1667640410dSJohn Snow nodes.literal('', base.doc_type())], 1677640410dSJohn Snow None) 1687640410dSJohn Snow 1697640410dSJohn Snow if branches: 1707640410dSJohn Snow for v in branches.variants: 1717640410dSJohn Snow if v.type.name == 'q_empty': 1727640410dSJohn Snow continue 1737640410dSJohn Snow assert not v.type.is_implicit() 1747640410dSJohn Snow term = [nodes.Text('The members of '), 1757640410dSJohn Snow nodes.literal('', v.type.doc_type())] 1767640410dSJohn Snow term.extend(self._nodes_for_variant_when(branches, v)) 1777640410dSJohn Snow dlnode += self._make_dlitem(term, None) 1787640410dSJohn Snow 1797640410dSJohn Snow if not dlnode.children: 1807640410dSJohn Snow return [] 1817640410dSJohn Snow 1827640410dSJohn Snow section = self._make_section(what) 1837640410dSJohn Snow section += dlnode 1847640410dSJohn Snow return [section] 1857640410dSJohn Snow 1867640410dSJohn Snow def _nodes_for_enum_values(self, doc): 1877640410dSJohn Snow """Return list of doctree nodes for the table of enum values""" 1887640410dSJohn Snow seen_item = False 1897640410dSJohn Snow dlnode = nodes.definition_list() 1907640410dSJohn Snow for section in doc.args.values(): 1917640410dSJohn Snow termtext = [nodes.literal('', section.member.name)] 1927640410dSJohn Snow if section.member.ifcond.is_present(): 1937640410dSJohn Snow termtext.extend(self._nodes_for_ifcond(section.member.ifcond)) 1947640410dSJohn Snow # TODO drop fallbacks when undocumented members are outlawed 1957640410dSJohn Snow if section.text: 1967640410dSJohn Snow defn = dedent(section.text) 1977640410dSJohn Snow else: 1987640410dSJohn Snow defn = [nodes.Text('Not documented')] 1997640410dSJohn Snow 2007640410dSJohn Snow dlnode += self._make_dlitem(termtext, defn) 2017640410dSJohn Snow seen_item = True 2027640410dSJohn Snow 2037640410dSJohn Snow if not seen_item: 2047640410dSJohn Snow return [] 2057640410dSJohn Snow 2067640410dSJohn Snow section = self._make_section('Values') 2077640410dSJohn Snow section += dlnode 2087640410dSJohn Snow return [section] 2097640410dSJohn Snow 2107640410dSJohn Snow def _nodes_for_arguments(self, doc, arg_type): 2117640410dSJohn Snow """Return list of doctree nodes for the arguments section""" 2127640410dSJohn Snow if arg_type and not arg_type.is_implicit(): 2137640410dSJohn Snow assert not doc.args 2147640410dSJohn Snow section = self._make_section('Arguments') 2157640410dSJohn Snow dlnode = nodes.definition_list() 2167640410dSJohn Snow dlnode += self._make_dlitem( 2177640410dSJohn Snow [nodes.Text('The members of '), 2187640410dSJohn Snow nodes.literal('', arg_type.name)], 2197640410dSJohn Snow None) 2207640410dSJohn Snow section += dlnode 2217640410dSJohn Snow return [section] 2227640410dSJohn Snow 2237640410dSJohn Snow return self._nodes_for_members(doc, 'Arguments') 2247640410dSJohn Snow 2257640410dSJohn Snow def _nodes_for_features(self, doc): 2267640410dSJohn Snow """Return list of doctree nodes for the table of features""" 2277640410dSJohn Snow seen_item = False 2287640410dSJohn Snow dlnode = nodes.definition_list() 2297640410dSJohn Snow for section in doc.features.values(): 2307640410dSJohn Snow dlnode += self._make_dlitem( 2317640410dSJohn Snow [nodes.literal('', section.member.name)], dedent(section.text)) 2327640410dSJohn Snow seen_item = True 2337640410dSJohn Snow 2347640410dSJohn Snow if not seen_item: 2357640410dSJohn Snow return [] 2367640410dSJohn Snow 2377640410dSJohn Snow section = self._make_section('Features') 2387640410dSJohn Snow section += dlnode 2397640410dSJohn Snow return [section] 2407640410dSJohn Snow 2417640410dSJohn Snow def _nodes_for_sections(self, doc): 2427640410dSJohn Snow """Return list of doctree nodes for additional sections""" 2437640410dSJohn Snow nodelist = [] 2447640410dSJohn Snow for section in doc.sections: 2457640410dSJohn Snow if section.kind == QAPIDoc.Kind.TODO: 2467640410dSJohn Snow # Hide TODO: sections 2477640410dSJohn Snow continue 2487640410dSJohn Snow 2497640410dSJohn Snow if section.kind == QAPIDoc.Kind.PLAIN: 2507640410dSJohn Snow # Sphinx cannot handle sectionless titles; 2517640410dSJohn Snow # Instead, just append the results to the prior section. 2527640410dSJohn Snow container = nodes.container() 2537640410dSJohn Snow self._parse_text_into_node(section.text, container) 2547640410dSJohn Snow nodelist += container.children 2557640410dSJohn Snow continue 2567640410dSJohn Snow 2577640410dSJohn Snow snode = self._make_section(section.kind.name.title()) 2587640410dSJohn Snow self._parse_text_into_node(dedent(section.text), snode) 2597640410dSJohn Snow nodelist.append(snode) 2607640410dSJohn Snow return nodelist 2617640410dSJohn Snow 2627640410dSJohn Snow def _nodes_for_if_section(self, ifcond): 2637640410dSJohn Snow """Return list of doctree nodes for the "If" section""" 2647640410dSJohn Snow nodelist = [] 2657640410dSJohn Snow if ifcond.is_present(): 2667640410dSJohn Snow snode = self._make_section('If') 2677640410dSJohn Snow snode += nodes.paragraph( 2687640410dSJohn Snow '', '', *self._nodes_for_ifcond(ifcond, with_if=False) 2697640410dSJohn Snow ) 2707640410dSJohn Snow nodelist.append(snode) 2717640410dSJohn Snow return nodelist 2727640410dSJohn Snow 2737640410dSJohn Snow def _add_doc(self, typ, sections): 2747640410dSJohn Snow """Add documentation for a command/object/enum... 2757640410dSJohn Snow 2767640410dSJohn Snow We assume we're documenting the thing defined in self._cur_doc. 2777640410dSJohn Snow typ is the type of thing being added ("Command", "Object", etc) 2787640410dSJohn Snow 2797640410dSJohn Snow sections is a list of nodes for sections to add to the definition. 2807640410dSJohn Snow """ 2817640410dSJohn Snow 2827640410dSJohn Snow doc = self._cur_doc 2837640410dSJohn Snow snode = nodes.section(ids=[self._sphinx_directive.new_serialno()]) 2847640410dSJohn Snow snode += nodes.title('', '', *[nodes.literal(doc.symbol, doc.symbol), 2857640410dSJohn Snow nodes.Text(' (' + typ + ')')]) 2867640410dSJohn Snow self._parse_text_into_node(doc.body.text, snode) 2877640410dSJohn Snow for s in sections: 2887640410dSJohn Snow if s is not None: 2897640410dSJohn Snow snode += s 2907640410dSJohn Snow self._add_node_to_current_heading(snode) 2917640410dSJohn Snow 2927640410dSJohn Snow def visit_enum_type(self, name, info, ifcond, features, members, prefix): 2937640410dSJohn Snow doc = self._cur_doc 2947640410dSJohn Snow self._add_doc('Enum', 2957640410dSJohn Snow self._nodes_for_enum_values(doc) 2967640410dSJohn Snow + self._nodes_for_features(doc) 2977640410dSJohn Snow + self._nodes_for_sections(doc) 2987640410dSJohn Snow + self._nodes_for_if_section(ifcond)) 2997640410dSJohn Snow 3007640410dSJohn Snow def visit_object_type(self, name, info, ifcond, features, 3017640410dSJohn Snow base, members, branches): 3027640410dSJohn Snow doc = self._cur_doc 3037640410dSJohn Snow if base and base.is_implicit(): 3047640410dSJohn Snow base = None 3057640410dSJohn Snow self._add_doc('Object', 3067640410dSJohn Snow self._nodes_for_members(doc, 'Members', base, branches) 3077640410dSJohn Snow + self._nodes_for_features(doc) 3087640410dSJohn Snow + self._nodes_for_sections(doc) 3097640410dSJohn Snow + self._nodes_for_if_section(ifcond)) 3107640410dSJohn Snow 3117640410dSJohn Snow def visit_alternate_type(self, name, info, ifcond, features, 3127640410dSJohn Snow alternatives): 3137640410dSJohn Snow doc = self._cur_doc 3147640410dSJohn Snow self._add_doc('Alternate', 3157640410dSJohn Snow self._nodes_for_members(doc, 'Members') 3167640410dSJohn Snow + self._nodes_for_features(doc) 3177640410dSJohn Snow + self._nodes_for_sections(doc) 3187640410dSJohn Snow + self._nodes_for_if_section(ifcond)) 3197640410dSJohn Snow 3207640410dSJohn Snow def visit_command(self, name, info, ifcond, features, arg_type, 3217640410dSJohn Snow ret_type, gen, success_response, boxed, allow_oob, 3227640410dSJohn Snow allow_preconfig, coroutine): 3237640410dSJohn Snow doc = self._cur_doc 3247640410dSJohn Snow self._add_doc('Command', 3257640410dSJohn Snow self._nodes_for_arguments(doc, arg_type) 3267640410dSJohn Snow + self._nodes_for_features(doc) 3277640410dSJohn Snow + self._nodes_for_sections(doc) 3287640410dSJohn Snow + self._nodes_for_if_section(ifcond)) 3297640410dSJohn Snow 3307640410dSJohn Snow def visit_event(self, name, info, ifcond, features, arg_type, boxed): 3317640410dSJohn Snow doc = self._cur_doc 3327640410dSJohn Snow self._add_doc('Event', 3337640410dSJohn Snow self._nodes_for_arguments(doc, arg_type) 3347640410dSJohn Snow + self._nodes_for_features(doc) 3357640410dSJohn Snow + self._nodes_for_sections(doc) 3367640410dSJohn Snow + self._nodes_for_if_section(ifcond)) 3377640410dSJohn Snow 3387640410dSJohn Snow def symbol(self, doc, entity): 3397640410dSJohn Snow """Add documentation for one symbol to the document tree 3407640410dSJohn Snow 3417640410dSJohn Snow This is the main entry point which causes us to add documentation 3427640410dSJohn Snow nodes for a symbol (which could be a 'command', 'object', 'event', 3437640410dSJohn Snow etc). We do this by calling 'visit' on the schema entity, which 3447640410dSJohn Snow will then call back into one of our visit_* methods, depending 3457640410dSJohn Snow on what kind of thing this symbol is. 3467640410dSJohn Snow """ 3477640410dSJohn Snow self._cur_doc = doc 3487640410dSJohn Snow entity.visit(self) 3497640410dSJohn Snow self._cur_doc = None 3507640410dSJohn Snow 3517640410dSJohn Snow def _start_new_heading(self, heading, level): 3527640410dSJohn Snow """Start a new heading at the specified heading level 3537640410dSJohn Snow 3547640410dSJohn Snow Create a new section whose title is 'heading' and which is placed 3557640410dSJohn Snow in the docutils node tree as a child of the most recent level-1 3567640410dSJohn Snow heading. Subsequent document sections (commands, freeform doc chunks, 3577640410dSJohn Snow etc) will be placed as children of this new heading section. 3587640410dSJohn Snow """ 3597640410dSJohn Snow if len(self._active_headings) < level: 3607640410dSJohn Snow raise QAPISemError(self._cur_doc.info, 3617640410dSJohn Snow 'Level %d subheading found outside a ' 3627640410dSJohn Snow 'level %d heading' 3637640410dSJohn Snow % (level, level - 1)) 3647640410dSJohn Snow snode = self._make_section(heading) 3657640410dSJohn Snow self._active_headings[level - 1] += snode 3667640410dSJohn Snow self._active_headings = self._active_headings[:level] 3677640410dSJohn Snow self._active_headings.append(snode) 3687640410dSJohn Snow return snode 3697640410dSJohn Snow 3707640410dSJohn Snow def _add_node_to_current_heading(self, node): 3717640410dSJohn Snow """Add the node to whatever the current active heading is""" 3727640410dSJohn Snow self._active_headings[-1] += node 3737640410dSJohn Snow 3747640410dSJohn Snow def freeform(self, doc): 3757640410dSJohn Snow """Add a piece of 'freeform' documentation to the document tree 3767640410dSJohn Snow 3777640410dSJohn Snow A 'freeform' document chunk doesn't relate to any particular 3787640410dSJohn Snow symbol (for instance, it could be an introduction). 3797640410dSJohn Snow 3807640410dSJohn Snow If the freeform document starts with a line of the form 3817640410dSJohn Snow '= Heading text', this is a section or subsection heading, with 3827640410dSJohn Snow the heading level indicated by the number of '=' signs. 3837640410dSJohn Snow """ 3847640410dSJohn Snow 3857640410dSJohn Snow # QAPIDoc documentation says free-form documentation blocks 3867640410dSJohn Snow # must have only a body section, nothing else. 3877640410dSJohn Snow assert not doc.sections 3887640410dSJohn Snow assert not doc.args 3897640410dSJohn Snow assert not doc.features 3907640410dSJohn Snow self._cur_doc = doc 3917640410dSJohn Snow 3927640410dSJohn Snow text = doc.body.text 3937640410dSJohn Snow if re.match(r'=+ ', text): 3947640410dSJohn Snow # Section/subsection heading (if present, will always be 3957640410dSJohn Snow # the first line of the block) 3967640410dSJohn Snow (heading, _, text) = text.partition('\n') 3977640410dSJohn Snow (leader, _, heading) = heading.partition(' ') 3987640410dSJohn Snow node = self._start_new_heading(heading, len(leader)) 3997640410dSJohn Snow if text == '': 4007640410dSJohn Snow return 4017640410dSJohn Snow else: 4027640410dSJohn Snow node = nodes.container() 4037640410dSJohn Snow 4047640410dSJohn Snow self._parse_text_into_node(text, node) 4057640410dSJohn Snow self._cur_doc = None 4067640410dSJohn Snow 4077640410dSJohn Snow def _parse_text_into_node(self, doctext, node): 4087640410dSJohn Snow """Parse a chunk of QAPI-doc-format text into the node 4097640410dSJohn Snow 4107640410dSJohn Snow The doc comment can contain most inline rST markup, including 4117640410dSJohn Snow bulleted and enumerated lists. 4127640410dSJohn Snow As an extra permitted piece of markup, @var will be turned 4137640410dSJohn Snow into ``var``. 4147640410dSJohn Snow """ 4157640410dSJohn Snow 4167640410dSJohn Snow # Handle the "@var means ``var`` case 4177640410dSJohn Snow doctext = re.sub(r'@([\w-]+)', r'``\1``', doctext) 4187640410dSJohn Snow 4197640410dSJohn Snow rstlist = ViewList() 4207640410dSJohn Snow for line in doctext.splitlines(): 4217640410dSJohn Snow # The reported line number will always be that of the start line 4227640410dSJohn Snow # of the doc comment, rather than the actual location of the error. 4237640410dSJohn Snow # Being more precise would require overhaul of the QAPIDoc class 4247640410dSJohn Snow # to track lines more exactly within all the sub-parts of the doc 4257640410dSJohn Snow # comment, as well as counting lines here. 4267640410dSJohn Snow rstlist.append(line, self._cur_doc.info.fname, 4277640410dSJohn Snow self._cur_doc.info.line) 4287640410dSJohn Snow # Append a blank line -- in some cases rST syntax errors get 4297640410dSJohn Snow # attributed to the line after one with actual text, and if there 4307640410dSJohn Snow # isn't anything in the ViewList corresponding to that then Sphinx 4317640410dSJohn Snow # 1.6's AutodocReporter will then misidentify the source/line location 4327640410dSJohn Snow # in the error message (usually attributing it to the top-level 4337640410dSJohn Snow # .rst file rather than the offending .json file). The extra blank 4347640410dSJohn Snow # line won't affect the rendered output. 4357640410dSJohn Snow rstlist.append("", self._cur_doc.info.fname, self._cur_doc.info.line) 4367640410dSJohn Snow self._sphinx_directive.do_parse(rstlist, node) 4377640410dSJohn Snow 4387640410dSJohn Snow def get_document_node(self): 4397640410dSJohn Snow """Return the root docutils node which makes up the document""" 4407640410dSJohn Snow return self._top_node 441