qapi: clean up encoding of section kinds

We have several kinds of sections, and to tell them apart, we use
Section attribute @tag and also the section object's Python type:

              type        @tag
    untagged  Section     None
    @foo:     ArgSection  'foo'
    Returns:  Section     'Returns'
    Errors:   Section     'Errors'
    Since:    Section     'Since'
    TODO:     Section     'TODO'

Note:

* @foo can be a member or a feature description, depending on context.

* tag == 'Since' can be a Since: section or a member or feature
  description.  If it's a Section, it's the former, and if it's an
  ArgSection, it's the latter.

Clean this up as follows.  Move the member or feature name to new
ArgSection attribute @name, and replace @tag by enum @kind like this:

              type         kind     name
    untagged  Section      PLAIN
    @foo:     ArgSection   MEMBER   'foo'   if member or argument
              ArgSection   FEATURE  'foo'   if feature
    Returns:  Section      RETURNS
    Errors:   Section      ERRORS
    Since:    Section      SINCE
    TODO:     Section      TODO

The qapi-schema tests are updated to account for the new section names;
"TODO" becomes "Todo" and `None` becomes "Plain" there.

Signed-off-by: John Snow <jsnow@redhat.com>
Message-ID: <20250311034303.75779-34-jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
This commit is contained in:
John Snow 2025-03-10 23:42:31 -04:00 committed by Markus Armbruster
parent faeacf858b
commit 323c668934
4 changed files with 80 additions and 36 deletions

View File

@ -35,6 +35,7 @@ from docutils.parsers.rst import Directive, directives
from docutils.statemachine import ViewList from docutils.statemachine import ViewList
from qapi.error import QAPIError, QAPISemError from qapi.error import QAPIError, QAPISemError
from qapi.gen import QAPISchemaVisitor from qapi.gen import QAPISchemaVisitor
from qapi.parser import QAPIDoc
from qapi.schema import QAPISchema from qapi.schema import QAPISchema
from sphinx import addnodes from sphinx import addnodes
@ -258,11 +259,11 @@ class QAPISchemaGenRSTVisitor(QAPISchemaVisitor):
"""Return list of doctree nodes for additional sections""" """Return list of doctree nodes for additional sections"""
nodelist = [] nodelist = []
for section in doc.sections: for section in doc.sections:
if section.tag and section.tag == 'TODO': if section.kind == QAPIDoc.Kind.TODO:
# Hide TODO: sections # Hide TODO: sections
continue continue
if not section.tag: if section.kind == QAPIDoc.Kind.PLAIN:
# Sphinx cannot handle sectionless titles; # Sphinx cannot handle sectionless titles;
# Instead, just append the results to the prior section. # Instead, just append the results to the prior section.
container = nodes.container() container = nodes.container()
@ -270,7 +271,7 @@ class QAPISchemaGenRSTVisitor(QAPISchemaVisitor):
nodelist += container.children nodelist += container.children
continue continue
snode = self._make_section(section.tag) snode = self._make_section(section.kind.name.title())
self._parse_text_into_node(dedent(section.text), snode) self._parse_text_into_node(dedent(section.text), snode)
nodelist.append(snode) nodelist.append(snode)
return nodelist return nodelist

View File

@ -14,6 +14,7 @@
# This work is licensed under the terms of the GNU GPL, version 2. # This work is licensed under the terms of the GNU GPL, version 2.
# See the COPYING file in the top-level directory. # See the COPYING file in the top-level directory.
import enum
import os import os
import re import re
from typing import ( from typing import (
@ -574,7 +575,10 @@ class QAPISchemaParser:
) )
raise QAPIParseError(self, emsg) raise QAPIParseError(self, emsg)
doc.new_tagged_section(self.info, match.group(1)) doc.new_tagged_section(
self.info,
QAPIDoc.Kind.from_string(match.group(1))
)
text = line[match.end():] text = line[match.end():]
if text: if text:
doc.append_line(text) doc.append_line(text)
@ -585,7 +589,7 @@ class QAPISchemaParser:
self, self,
"unexpected '=' markup in definition documentation") "unexpected '=' markup in definition documentation")
else: else:
# tag-less paragraph # plain paragraph
doc.ensure_untagged_section(self.info) doc.ensure_untagged_section(self.info)
doc.append_line(line) doc.append_line(line)
line = self.get_doc_paragraph(doc) line = self.get_doc_paragraph(doc)
@ -634,14 +638,33 @@ class QAPIDoc:
Free-form documentation blocks consist only of a body section. Free-form documentation blocks consist only of a body section.
""" """
class Kind(enum.Enum):
PLAIN = 0
MEMBER = 1
FEATURE = 2
RETURNS = 3
ERRORS = 4
SINCE = 5
TODO = 6
@staticmethod
def from_string(kind: str) -> 'QAPIDoc.Kind':
return QAPIDoc.Kind[kind.upper()]
def __str__(self) -> str:
return self.name.title()
class Section: class Section:
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
def __init__(self, info: QAPISourceInfo, def __init__(
tag: Optional[str] = None): self,
info: QAPISourceInfo,
kind: 'QAPIDoc.Kind',
):
# section source info, i.e. where it begins # section source info, i.e. where it begins
self.info = info self.info = info
# section tag, if any ('Returns', '@name', ...) # section kind
self.tag = tag self.kind = kind
# section text without tag # section text without tag
self.text = '' self.text = ''
@ -649,8 +672,14 @@ class QAPIDoc:
self.text += line + '\n' self.text += line + '\n'
class ArgSection(Section): class ArgSection(Section):
def __init__(self, info: QAPISourceInfo, tag: str): def __init__(
super().__init__(info, tag) self,
info: QAPISourceInfo,
kind: 'QAPIDoc.Kind',
name: str
):
super().__init__(info, kind)
self.name = name
self.member: Optional['QAPISchemaMember'] = None self.member: Optional['QAPISchemaMember'] = None
def connect(self, member: 'QAPISchemaMember') -> None: def connect(self, member: 'QAPISchemaMember') -> None:
@ -662,7 +691,9 @@ class QAPIDoc:
# definition doc's symbol, None for free-form doc # definition doc's symbol, None for free-form doc
self.symbol: Optional[str] = symbol self.symbol: Optional[str] = symbol
# the sections in textual order # the sections in textual order
self.all_sections: List[QAPIDoc.Section] = [QAPIDoc.Section(info)] self.all_sections: List[QAPIDoc.Section] = [
QAPIDoc.Section(info, QAPIDoc.Kind.PLAIN)
]
# the body section # the body section
self.body: Optional[QAPIDoc.Section] = self.all_sections[0] self.body: Optional[QAPIDoc.Section] = self.all_sections[0]
# dicts mapping parameter/feature names to their description # dicts mapping parameter/feature names to their description
@ -679,12 +710,14 @@ class QAPIDoc:
def end(self) -> None: def end(self) -> None:
for section in self.all_sections: for section in self.all_sections:
section.text = section.text.strip('\n') section.text = section.text.strip('\n')
if section.tag is not None and section.text == '': if section.kind != QAPIDoc.Kind.PLAIN and section.text == '':
raise QAPISemError( raise QAPISemError(
section.info, "text required after '%s:'" % section.tag) section.info, "text required after '%s:'" % section.kind)
def ensure_untagged_section(self, info: QAPISourceInfo) -> None: def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
if self.all_sections and not self.all_sections[-1].tag: kind = QAPIDoc.Kind.PLAIN
if self.all_sections and self.all_sections[-1].kind == kind:
# extend current section # extend current section
section = self.all_sections[-1] section = self.all_sections[-1]
if not section.text: if not section.text:
@ -692,46 +725,56 @@ class QAPIDoc:
section.info = info section.info = info
section.text += '\n' section.text += '\n'
return return
# start new section # start new section
section = self.Section(info) section = self.Section(info, kind)
self.sections.append(section) self.sections.append(section)
self.all_sections.append(section) self.all_sections.append(section)
def new_tagged_section(self, info: QAPISourceInfo, tag: str) -> None: def new_tagged_section(
section = self.Section(info, tag) self,
if tag == 'Returns': info: QAPISourceInfo,
kind: 'QAPIDoc.Kind',
) -> None:
section = self.Section(info, kind)
if kind == QAPIDoc.Kind.RETURNS:
if self.returns: if self.returns:
raise QAPISemError( raise QAPISemError(
info, "duplicated '%s' section" % tag) info, "duplicated '%s' section" % kind)
self.returns = section self.returns = section
elif tag == 'Errors': elif kind == QAPIDoc.Kind.ERRORS:
if self.errors: if self.errors:
raise QAPISemError( raise QAPISemError(
info, "duplicated '%s' section" % tag) info, "duplicated '%s' section" % kind)
self.errors = section self.errors = section
elif tag == 'Since': elif kind == QAPIDoc.Kind.SINCE:
if self.since: if self.since:
raise QAPISemError( raise QAPISemError(
info, "duplicated '%s' section" % tag) info, "duplicated '%s' section" % kind)
self.since = section self.since = section
self.sections.append(section) self.sections.append(section)
self.all_sections.append(section) self.all_sections.append(section)
def _new_description(self, info: QAPISourceInfo, name: str, def _new_description(
desc: Dict[str, ArgSection]) -> None: self,
info: QAPISourceInfo,
name: str,
kind: 'QAPIDoc.Kind',
desc: Dict[str, ArgSection]
) -> None:
if not name: if not name:
raise QAPISemError(info, "invalid parameter name") raise QAPISemError(info, "invalid parameter name")
if name in desc: if name in desc:
raise QAPISemError(info, "'%s' parameter name duplicated" % name) raise QAPISemError(info, "'%s' parameter name duplicated" % name)
section = self.ArgSection(info, '@' + name) section = self.ArgSection(info, kind, name)
self.all_sections.append(section) self.all_sections.append(section)
desc[name] = section desc[name] = section
def new_argument(self, info: QAPISourceInfo, name: str) -> None: def new_argument(self, info: QAPISourceInfo, name: str) -> None:
self._new_description(info, name, self.args) self._new_description(info, name, QAPIDoc.Kind.MEMBER, self.args)
def new_feature(self, info: QAPISourceInfo, name: str) -> None: def new_feature(self, info: QAPISourceInfo, name: str) -> None:
self._new_description(info, name, self.features) self._new_description(info, name, QAPIDoc.Kind.FEATURE, self.features)
def append_line(self, line: str) -> None: def append_line(self, line: str) -> None:
self.all_sections[-1].append_line(line) self.all_sections[-1].append_line(line)
@ -744,7 +787,7 @@ class QAPIDoc:
"%s '%s' lacks documentation" "%s '%s' lacks documentation"
% (member.role, member.name)) % (member.role, member.name))
self.args[member.name] = QAPIDoc.ArgSection( self.args[member.name] = QAPIDoc.ArgSection(
self.info, '@' + member.name) self.info, QAPIDoc.Kind.MEMBER, member.name)
self.args[member.name].connect(member) self.args[member.name].connect(member)
def connect_feature(self, feature: 'QAPISchemaFeature') -> None: def connect_feature(self, feature: 'QAPISchemaFeature') -> None:

View File

@ -113,7 +113,7 @@ The _one_ {and only}, description on the same line
Also _one_ {and only} Also _one_ {and only}
feature=enum-member-feat feature=enum-member-feat
a member feature a member feature
section=None section=Plain
@two is undocumented @two is undocumented
doc symbol=Base doc symbol=Base
body= body=
@ -171,15 +171,15 @@ description starts on the same line
a feature a feature
feature=cmd-feat2 feature=cmd-feat2
another feature another feature
section=None section=Plain
.. note:: @arg3 is undocumented .. note:: @arg3 is undocumented
section=Returns section=Returns
@Object @Object
section=Errors section=Errors
some some
section=TODO section=Todo
frobnicate frobnicate
section=None section=Plain
.. admonition:: Notes .. admonition:: Notes
- Lorem ipsum dolor sit amet - Lorem ipsum dolor sit amet
@ -212,7 +212,7 @@ If you're bored enough to read this, go see a video of boxed cats
a feature a feature
feature=cmd-feat2 feature=cmd-feat2
another feature another feature
section=None section=Plain
.. qmp-example:: .. qmp-example::
-> "this example" -> "this example"

View File

@ -122,7 +122,7 @@ def test_frontend(fname):
for feat, section in doc.features.items(): for feat, section in doc.features.items():
print(' feature=%s\n%s' % (feat, section.text)) print(' feature=%s\n%s' % (feat, section.text))
for section in doc.sections: for section in doc.sections:
print(' section=%s\n%s' % (section.tag, section.text)) print(' section=%s\n%s' % (section.kind, section.text))
def open_test_result(dir_name, file_name, update): def open_test_result(dir_name, file_name, update):