On Wed, Mar 5, 2025 at 5:16 AM Markus Armbruster <arm...@redhat.com> wrote:
> Replaying review of a previous posting for your convenience... > > John Snow <js...@redhat.com> writes: > > > This patch adds an explicit section "kind" to all QAPIDoc > > sections. Members/Features are now explicitly marked as such, with the > > name now being stored in a dedicated "name" field (which qapidoc.py was > > not actually using anyway.) > > I'm not sure what the parenthesis is trying to convey. > The old qapidoc.py doesn't actually use the name field, so there's nothing to adjust for old callers. > > Before the patch, we have: > > type tag > untagged Section None > @foo: ArgSection 'foo' > Returns: Section 'Returns' > Errors: Section 'Errors' > Since: Section 'Since' > TODO: Section 'TODO' > > Afterwards, I believe: > > 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 > > So, .tag is replaced by .kind and .name, member vs. feature vs. other > tags is now obvious from .kind alone, i.e. there's no need to account > for context or type. > > Fine print: why do we need to account for type before the patch? > Consider @Since: ... > I'm not sure I follow... > > > The qapi-schema tests are updated to account for the new section names; > > mostly "TODO" becomes "Todo" and `None` becomes "Plain". > > > > Signed-off-by: John Snow <js...@redhat.com> > > --- > > docs/sphinx/qapidoc.py | 7 ++- > > scripts/qapi/parser.py | 109 ++++++++++++++++++++++++--------- > > tests/qapi-schema/doc-good.out | 10 +-- > > tests/qapi-schema/test-qapi.py | 2 +- > > 4 files changed, 90 insertions(+), 38 deletions(-) > > > > diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py > > index 61997fd21af..d622398f1da 100644 > > --- a/docs/sphinx/qapidoc.py > > +++ b/docs/sphinx/qapidoc.py > > @@ -35,6 +35,7 @@ > > from docutils.statemachine import ViewList > > from qapi.error import QAPIError, QAPISemError > > from qapi.gen import QAPISchemaVisitor > > +from qapi.parser import QAPIDoc > > from qapi.schema import QAPISchema > > > > from sphinx import addnodes > > @@ -258,11 +259,11 @@ def _nodes_for_sections(self, doc): > > """Return list of doctree nodes for additional sections""" > > nodelist = [] > > for section in doc.sections: > > - if section.tag and section.tag == 'TODO': > > + if section.kind == QAPIDoc.Kind.TODO: > > # Hide TODO: sections > > continue > > > > - if not section.tag: > > + if section.kind == QAPIDoc.Kind.PLAIN: > > # Sphinx cannot handle sectionless titles; > > # Instead, just append the results to the prior section. > > container = nodes.container() > > @@ -270,7 +271,7 @@ def _nodes_for_sections(self, doc): > > nodelist += container.children > > continue > > > > - snode = self._make_section(section.tag) > > + snode = self._make_section(section.kind.name.title()) > > self._parse_text_into_node(dedent(section.text), snode) > > nodelist.append(snode) > > return nodelist > > diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py > > index 36cb64a677a..c3004aa70c6 100644 > > --- a/scripts/qapi/parser.py > > +++ b/scripts/qapi/parser.py > > @@ -15,6 +15,7 @@ > > # See the COPYING file in the top-level directory. > > > > from collections import OrderedDict > > +import enum > > import os > > import re > > from typing import ( > > @@ -575,7 +576,10 @@ def get_doc(self) -> 'QAPIDoc': > > ) > > 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():] > > if text: > > doc.append_line(text) > > @@ -586,7 +590,7 @@ def get_doc(self) -> 'QAPIDoc': > > self, > > "unexpected '=' markup in definition > documentation") > > else: > > - # tag-less paragraph > > + # plain paragraph(s) > > We're parsing a single pargraph here. The plain section we add it to > may have any number of paragraphs. But for me, the comment is about > what's being parsed. Mind to drop (s)? > Anguish. I can't keep this straight in my head. OK. It wasn't obvious at a glance where the break is if we get an empty newline ... > > > doc.ensure_untagged_section(self.info) > > doc.append_line(line) > > line = self.get_doc_paragraph(doc) > > @@ -635,14 +639,37 @@ class QAPIDoc: > > 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': > > Remind me, why do we need to quote the type here? > It doesn't exist yet; it's a forward reference, basically. While we are in the context of defining the class, we don't have access to variables scoped within the class :) > > > + return QAPIDoc.Kind[kind.upper()] > > + > > + def text_required(self) -> bool: > > + # Only "plain" sections can be empty > > + return self.value not in (0,) > > Rather roundabout way to check for PLAIN, isn't it? > Vestigial from intro/details split. It can be removed here for now. > > There's just one caller (see below). I doubt the method is worth its > keep. > Vestigial again. Whether it's worth it once there are multiple such sections, who knows. I thought it made the code read nicer in context. We don't need it right now anyway... > > > + > > + def __str__(self) -> str: > > + return self.name.title() > > + > > I wonder whether a simple StrEnum without methods would do. Oh, StrEnum > is new in 3.11. Nevermind. > > Hmm. > > >>> Kind = Enum('Kind', [('PLAIN', 'Plain'), ('TODO, 'TODO)]) > >>> kind=Kind('Plain') > >>> kind.value > 'Plain' > > What do you think? > Maybe, lemme play with it and see if it makes something else worse, I don't know right away. > > > class Section: > > # pylint: disable=too-few-public-methods > > - def __init__(self, info: QAPISourceInfo, > > - tag: Optional[str] = None): > > + def __init__( > > + self, > > + info: QAPISourceInfo, > > + kind: 'QAPIDoc.Kind', > > + ): > > # section source info, i.e. where it begins > > self.info = info > > - # section tag, if any ('Returns', '@name', ...) > > - self.tag = tag > > + # section kind > > + self.kind = kind > > # section text without tag > > self.text = '' > > > > @@ -650,8 +677,14 @@ def append_line(self, line: str) -> None: > > self.text += line + '\n' > > > > class ArgSection(Section): > > - def __init__(self, info: QAPISourceInfo, tag: str): > > - super().__init__(info, tag) > > + def __init__( > > + self, > > + info: QAPISourceInfo, > > + kind: 'QAPIDoc.Kind', > > + name: str > > + ): > > + super().__init__(info, kind) > > + self.name = name > > self.member: Optional['QAPISchemaMember'] = None > > Before the patch, use of a separate type for members, arguments and > features was necessary to distinguish between '@TAG:' and 'TAG:' for the > various TAGs. This is no longer the case. Fold ArgSection into > Section? Not sure. If yes, separate patch to keep this one as > mechanical as possible. > Possibly the case. I'll play with it. Do you want it in this series? (It's pretty long as is...!) > > > > > def connect(self, member: 'QAPISchemaMember') -> None: > > @@ -663,7 +696,9 @@ def __init__(self, info: QAPISourceInfo, symbol: > Optional[str] = None): > > # definition doc's symbol, None for free-form doc > > self.symbol: Optional[str] = symbol > > # 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 > > self.body: Optional[QAPIDoc.Section] = self.all_sections[0] > > # dicts mapping parameter/feature names to their description > > @@ -680,12 +715,17 @@ def __init__(self, info: QAPISourceInfo, symbol: > Optional[str] = None): > > def end(self) -> None: > > for section in self.all_sections: > > section.text = section.text.strip('\n') > > - if section.tag is not None and section.text == '': > > + if section.kind.text_required() and section.text == '': > > This is the only use of .text_required(). I believe checking for PLAIN > would be clearer. > > > 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: > > - if self.all_sections and not self.all_sections[-1].tag: > > + def ensure_untagged_section( > > + self, > > + info: QAPISourceInfo, > > + ) -> None: > > Accidental line breaking? > Something something something black autoformatter. I wonder why it did this, though... I'll see if I can undo it. > > > + kind = QAPIDoc.Kind.PLAIN > > + > > + if self.all_sections and self.all_sections[-1].kind == kind: > > I'd prefer not to hide PLAIN behind a variable, but I'd also prefer > the condition to fit on a line. Hmm. > Also somewhat vestigial from when I had intro/details. When that split comes, kind = becomes a conditional ternary. > > > # extend current section > > section = self.all_sections[-1] > > if not section.text: > > Maybe > > section = self.all_sections[-1] if self.all_sections else None > > if second and section.kind = QAPIDoc.Kind.Plain: > # extend current section > if not section.text: > Could, but it's going to get rewritten when the inliner comes anyway, ... > > > @@ -693,46 +733,56 @@ def ensure_untagged_section(self, info: > QAPISourceInfo) -> None: > > section.info = info > > section.text += '\n' > > return > > + > > # start new section > > - section = self.Section(info) > > + section = self.Section(info, kind) > > self.sections.append(section) > > self.all_sections.append(section) > > > > - def new_tagged_section(self, info: QAPISourceInfo, tag: str) -> > None: > > - section = self.Section(info, tag) > > - if tag == 'Returns': > > + def new_tagged_section( > > + self, > > + info: QAPISourceInfo, > > + kind: 'QAPIDoc.Kind', > > + ) -> None: > > + section = self.Section(info, kind) > > + if kind == QAPIDoc.Kind.RETURNS: > > if self.returns: > > raise QAPISemError( > > - info, "duplicated '%s' section" % tag) > > + info, "duplicated '%s' section" % kind) > > self.returns = section > > - elif tag == 'Errors': > > + elif kind == QAPIDoc.Kind.ERRORS: > > if self.errors: > > raise QAPISemError( > > - info, "duplicated '%s' section" % tag) > > + info, "duplicated '%s' section" % kind) > > self.errors = section > > - elif tag == 'Since': > > + elif kind == QAPIDoc.Kind.SINCE: > > if self.since: > > raise QAPISemError( > > - info, "duplicated '%s' section" % tag) > > + info, "duplicated '%s' section" % kind) > > self.since = section > > self.sections.append(section) > > self.all_sections.append(section) > > > > - def _new_description(self, info: QAPISourceInfo, name: str, > > - desc: Dict[str, ArgSection]) -> None: > > + def _new_description( > > + self, > > + info: QAPISourceInfo, > > + name: str, > > + kind: 'QAPIDoc.Kind', > > + desc: Dict[str, ArgSection] > > + ) -> None: > > if not name: > > raise QAPISemError(info, "invalid parameter name") > > if name in desc: > > raise QAPISemError(info, "'%s' parameter name duplicated" % > name) > > - section = self.ArgSection(info, '@' + name) > > + section = self.ArgSection(info, kind, name) > > self.all_sections.append(section) > > desc[name] = section > > > > 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: > > - self._new_description(info, name, self.features) > > + self._new_description(info, name, QAPIDoc.Kind.FEATURE, > self.features) > > QAPIDoc.Kind.FOO is a mouthful, and it tends to result in long lines, > like here. Can't see an easy and clean way to reduce the verbosity. > Yeah... > > > > > def append_line(self, line: str) -> None: > > self.all_sections[-1].append_line(line) > > @@ -744,8 +794,9 @@ def connect_member(self, member: 'QAPISchemaMember') > -> None: > > raise QAPISemError(member.info, > > "%s '%s' lacks documentation" > > % (member.role, member.name)) > > - self.args[member.name] = QAPIDoc.ArgSection( > > - self.info, '@' + member.name) > > + section = QAPIDoc.ArgSection( > > + self.info, QAPIDoc.Kind.MEMBER, member.name) > > + self.args[member.name] = section > > Why the extra variable? > Wish I remembered. I can undo it and see if anything barks. > > > self.args[member.name].connect(member) > > > > def connect_feature(self, feature: 'QAPISchemaFeature') -> None: > > diff --git a/tests/qapi-schema/doc-good.out > b/tests/qapi-schema/doc-good.out > > index 0a9da3efdeb..5773f1dd6d6 100644 > > --- a/tests/qapi-schema/doc-good.out > > +++ b/tests/qapi-schema/doc-good.out > > @@ -113,7 +113,7 @@ The _one_ {and only}, description on the same line > > Also _one_ {and only} > > feature=enum-member-feat > > a member feature > > - section=None > > + section=Plain > > @two is undocumented > > doc symbol=Base > > body= > > @@ -171,15 +171,15 @@ description starts on the same line > > a feature > > feature=cmd-feat2 > > another feature > > - section=None > > + section=Plain > > .. note:: @arg3 is undocumented > > section=Returns > > @Object > > section=Errors > > some > > - section=TODO > > + section=Todo > > With the method-less Enum I suggested, this hunk would go away. Not > that it matters :) > > > frobnicate > > - section=None > > + section=Plain > > .. admonition:: Notes > > > > - 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 > > feature=cmd-feat2 > > another feature > > - section=None > > + section=Plain > > .. qmp-example:: > > > > -> "this example" > > diff --git a/tests/qapi-schema/test-qapi.py > b/tests/qapi-schema/test-qapi.py > > index 7e3f9f4aa1f..bca924309be 100755 > > --- a/tests/qapi-schema/test-qapi.py > > +++ b/tests/qapi-schema/test-qapi.py > > @@ -131,7 +131,7 @@ def test_frontend(fname): > > for feat, section in doc.features.items(): > > print(' feature=%s\n%s' % (feat, section.text)) > > 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): > >