[PATCH 08/20] qapi/parser: differentiate intro and outro paragraphs

John Snow posted 20 patches 6 months, 2 weeks ago
Maintainers: Markus Armbruster <armbru@redhat.com>, Michael Roth <michael.roth@amd.com>, Peter Maydell <peter.maydell@linaro.org>, Eric Blake <eblake@redhat.com>, "Michael S. Tsirkin" <mst@redhat.com>, Igor Mammedov <imammedo@redhat.com>, Ani Sinha <anisinha@redhat.com>, Gerd Hoffmann <kraxel@redhat.com>, "Marc-André Lureau" <marcandre.lureau@redhat.com>, Kevin Wolf <kwolf@redhat.com>, Hanna Reitz <hreitz@redhat.com>, Paolo Bonzini <pbonzini@redhat.com>, "Daniel P. Berrangé" <berrange@redhat.com>, Eduardo Habkost <eduardo@habkost.net>, Marcel Apfelbaum <marcel.apfelbaum@gmail.com>, "Philippe Mathieu-Daudé" <philmd@linaro.org>, Yanan Wang <wangyanan55@huawei.com>, Peter Xu <peterx@redhat.com>, Fabiano Rosas <farosas@suse.de>, Jason Wang <jasowang@redhat.com>, Pavel Dovgalyuk <pavel.dovgaluk@ispras.ru>, Jiri Pirko <jiri@resnulli.us>, Stefan Berger <stefanb@linux.vnet.ibm.com>, Stefan Hajnoczi <stefanha@redhat.com>, Mads Ynddal <mads@ynddal.dk>, Lukas Straub <lukasstraub2@web.de>, Konstantin Kostiuk <kkostiuk@redhat.com>
[PATCH 08/20] qapi/parser: differentiate intro and outro paragraphs
Posted by John Snow 6 months, 2 weeks ago
Add a semantic tag to paragraphs that appear *before* tagged
sections/members/features and those that appear after. This will control
how they are inlined when doc sections are merged and flattened.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/parser.py | 22 +++++++++++++++++-----
 1 file changed, 17 insertions(+), 5 deletions(-)

diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index cf4cbca1c1f..b1794f71e12 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -503,6 +503,10 @@ def get_doc(self) -> 'QAPIDoc':
             self.accept(False)
             line = self.get_doc_line()
             no_more_args = False
+            # Paragraphs before members/features/tagged are "intro" paragraphs.
+            # Any appearing subsequently are "outro" paragraphs.
+            # This is only semantic metadata for the doc generator.
+            intro = True
 
             while line is not None:
                 # Blank lines
@@ -532,6 +536,7 @@ def get_doc(self) -> 'QAPIDoc':
                         raise QAPIParseError(
                             self, 'feature descriptions expected')
                     no_more_args = True
+                    intro = False
                 elif match := self._match_at_name_colon(line):
                     # description
                     if no_more_args:
@@ -547,6 +552,7 @@ def get_doc(self) -> 'QAPIDoc':
                             doc.append_line(text)
                         line = self.get_doc_indented(doc)
                     no_more_args = True
+                    intro = False
                 elif match := re.match(
                         r'(Returns|Errors|Since|Notes?|Examples?|TODO): *',
                         line):
@@ -557,13 +563,14 @@ def get_doc(self) -> 'QAPIDoc':
                         doc.append_line(text)
                     line = self.get_doc_indented(doc)
                     no_more_args = True
+                    intro = False
                 elif line.startswith('='):
                     raise QAPIParseError(
                         self,
                         "unexpected '=' markup in definition documentation")
                 else:
                     # tag-less paragraph
-                    doc.ensure_untagged_section(self.info)
+                    doc.ensure_untagged_section(self.info, intro)
                     doc.append_line(line)
                     line = self.get_doc_paragraph(doc)
         else:
@@ -617,7 +624,7 @@ def __init__(
             self,
             info: QAPISourceInfo,
             tag: Optional[str] = None,
-            kind: str = 'paragraph',
+            kind: str = 'intro-paragraph',
         ):
             # section source info, i.e. where it begins
             self.info = info
@@ -625,7 +632,7 @@ def __init__(
             self.tag = tag
             # section text without tag
             self.text = ''
-            # section type - {paragraph, feature, member, tagged}
+            # section type - {<intro|outro>-paragraph, feature, member, tagged}
             self.kind = kind
 
         def append_line(self, line: str) -> None:
@@ -666,7 +673,11 @@ def end(self) -> None:
                 raise QAPISemError(
                     section.info, "text required after '%s:'" % section.tag)
 
-    def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
+    def ensure_untagged_section(
+        self,
+        info: QAPISourceInfo,
+        intro: bool = True,
+    ) -> None:
         if self.all_sections and not self.all_sections[-1].tag:
             section = self.all_sections[-1]
             # Section is empty so far; update info to start *here*.
@@ -677,7 +688,8 @@ def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
                 self.all_sections[-1].text += '\n'
             return
         # start new section
-        section = self.Section(info)
+        kind = ("intro" if intro else "outro") + "-paragraph"
+        section = self.Section(info, kind=kind)
         self.sections.append(section)
         self.all_sections.append(section)
 
-- 
2.44.0
Re: [PATCH 08/20] qapi/parser: differentiate intro and outro paragraphs
Posted by Markus Armbruster 6 months, 1 week ago
John Snow <jsnow@redhat.com> writes:

> Add a semantic tag to paragraphs that appear *before* tagged
> sections/members/features and those that appear after. This will control
> how they are inlined when doc sections are merged and flattened.

This future use is not obvious to me now.  I guess the effective way to
help me see it is actual patches, which will come in due time.

> Signed-off-by: John Snow <jsnow@redhat.com>
> ---
>  scripts/qapi/parser.py | 22 +++++++++++++++++-----
>  1 file changed, 17 insertions(+), 5 deletions(-)
>
> diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
> index cf4cbca1c1f..b1794f71e12 100644
> --- a/scripts/qapi/parser.py
> +++ b/scripts/qapi/parser.py
> @@ -503,6 +503,10 @@ def get_doc(self) -> 'QAPIDoc':
>              self.accept(False)
>              line = self.get_doc_line()
>              no_more_args = False
> +            # Paragraphs before members/features/tagged are "intro" paragraphs.
> +            # Any appearing subsequently are "outro" paragraphs.
> +            # This is only semantic metadata for the doc generator.

Not sure about the last sentence.  Isn't it true for almost everything
around here?

Also, long line.  

> +            intro = True
>  
>              while line is not None:
>                  # Blank lines
> @@ -532,6 +536,7 @@ def get_doc(self) -> 'QAPIDoc':
>                          raise QAPIParseError(
>                              self, 'feature descriptions expected')
>                      no_more_args = True
> +                    intro = False

After feature descriptions.

>                  elif match := self._match_at_name_colon(line):
>                      # description
>                      if no_more_args:
> @@ -547,6 +552,7 @@ def get_doc(self) -> 'QAPIDoc':
>                              doc.append_line(text)
>                          line = self.get_doc_indented(doc)
>                      no_more_args = True
> +                    intro = False

Or after member descriptions.

>                  elif match := re.match(
>                          r'(Returns|Errors|Since|Notes?|Examples?|TODO): *',
>                          line):
> @@ -557,13 +563,14 @@ def get_doc(self) -> 'QAPIDoc':
>                          doc.append_line(text)
>                      line = self.get_doc_indented(doc)
>                      no_more_args = True
> +                    intro = False

Or after the first tagged section.

Okay, it does what it says on the tin.

>                  elif line.startswith('='):
>                      raise QAPIParseError(
>                          self,
>                          "unexpected '=' markup in definition documentation")
>                  else:
>                      # tag-less paragraph
> -                    doc.ensure_untagged_section(self.info)
> +                    doc.ensure_untagged_section(self.info, intro)
>                      doc.append_line(line)
>                      line = self.get_doc_paragraph(doc)
>          else:
> @@ -617,7 +624,7 @@ def __init__(
>              self,
>              info: QAPISourceInfo,
>              tag: Optional[str] = None,
> -            kind: str = 'paragraph',
> +            kind: str = 'intro-paragraph',

The question "why is this optional?" crossed my mind when reviewing the
previous patch.  I left it unasked, because I felt challenging the
overlap between @kind and @tag was more useful.  However, the new
default value 'intro-paragraph' feels more arbitrary to me than the old
one 'paragraph', and that makes the question pop right back into my
mind.

Unless I'm mistaken, all calls but one @tag and @kind.  Making that one
pass it too feels simpler to me.

Moot if we fuse @tag and @kind, of course.

>          ):
>              # section source info, i.e. where it begins
>              self.info = info
> @@ -625,7 +632,7 @@ def __init__(
>              self.tag = tag
>              # section text without tag
>              self.text = ''
> -            # section type - {paragraph, feature, member, tagged}
> +            # section type - {<intro|outro>-paragraph, feature, member, tagged}

Long line.

>              self.kind = kind
>  
>          def append_line(self, line: str) -> None:
> @@ -666,7 +673,11 @@ def end(self) -> None:
>                  raise QAPISemError(
>                      section.info, "text required after '%s:'" % section.tag)
>  
> -    def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
> +    def ensure_untagged_section(
> +        self,
> +        info: QAPISourceInfo,
> +        intro: bool = True,

Two callers, one passes @info, one doesn't.  Passing it always might be
simpler.

> +    ) -> None:
>          if self.all_sections and not self.all_sections[-1].tag:
>              section = self.all_sections[-1]
>              # Section is empty so far; update info to start *here*.
> @@ -677,7 +688,8 @@ def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
>                  self.all_sections[-1].text += '\n'
>              return
>          # start new section
> -        section = self.Section(info)
> +        kind = ("intro" if intro else "outro") + "-paragraph"
> +        section = self.Section(info, kind=kind)
>          self.sections.append(section)
>          self.all_sections.append(section)
Re: [PATCH 08/20] qapi/parser: differentiate intro and outro paragraphs
Posted by John Snow 6 months, 1 week ago
On Thu, May 16, 2024, 5:34 AM Markus Armbruster <armbru@redhat.com> wrote:

> John Snow <jsnow@redhat.com> writes:
>
> > Add a semantic tag to paragraphs that appear *before* tagged
> > sections/members/features and those that appear after. This will control
> > how they are inlined when doc sections are merged and flattened.
>
> This future use is not obvious to me now.  I guess the effective way to
> help me see it is actual patches, which will come in due time.
>

Head recursion and tail recursion, respectively :)

* intro
* inherited intro
* members [ancestor-descendent]
* features [ancestor-descendent]
* inherited outro
* outro

Child gets the first and final words. Inherited stuff goes in the sandwich
fillings.

It feels like a simple rule that's easy to internalize. As a bonus, you can
explain it by analogy to Americans as a burger, which is the only metaphor
we understand.


> > Signed-off-by: John Snow <jsnow@redhat.com>
> > ---
> >  scripts/qapi/parser.py | 22 +++++++++++++++++-----
> >  1 file changed, 17 insertions(+), 5 deletions(-)
> >
> > diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
> > index cf4cbca1c1f..b1794f71e12 100644
> > --- a/scripts/qapi/parser.py
> > +++ b/scripts/qapi/parser.py
> > @@ -503,6 +503,10 @@ def get_doc(self) -> 'QAPIDoc':
> >              self.accept(False)
> >              line = self.get_doc_line()
> >              no_more_args = False
> > +            # Paragraphs before members/features/tagged are "intro"
> paragraphs.
> > +            # Any appearing subsequently are "outro" paragraphs.
> > +            # This is only semantic metadata for the doc generator.
>
> Not sure about the last sentence.  Isn't it true for almost everything
> around here?
>

I guess I was trying to say "There's no real difference between the two
mechanically, it's purely based on where it appears in the doc block, which
offers only a heuristic for its semantic value- introductory statements or
additional detail."

In my mind: the other "kind" values have some more mechanical difference to
them, but intro/outro don't.


> Also, long line.
>
> > +            intro = True
> >
> >              while line is not None:
> >                  # Blank lines
> > @@ -532,6 +536,7 @@ def get_doc(self) -> 'QAPIDoc':
> >                          raise QAPIParseError(
> >                              self, 'feature descriptions expected')
> >                      no_more_args = True
> > +                    intro = False
>
> After feature descriptions.
>
> >                  elif match := self._match_at_name_colon(line):
> >                      # description
> >                      if no_more_args:
> > @@ -547,6 +552,7 @@ def get_doc(self) -> 'QAPIDoc':
> >                              doc.append_line(text)
> >                          line = self.get_doc_indented(doc)
> >                      no_more_args = True
> > +                    intro = False
>
> Or after member descriptions.
>
> >                  elif match := re.match(
> >                          r'(Returns|Errors|Since|Notes?|Examples?|TODO):
> *',
> >                          line):
> > @@ -557,13 +563,14 @@ def get_doc(self) -> 'QAPIDoc':
> >                          doc.append_line(text)
> >                      line = self.get_doc_indented(doc)
> >                      no_more_args = True
> > +                    intro = False
>
> Or after the first tagged section.
>
> Okay, it does what it says on the tin.
>
> >                  elif line.startswith('='):
> >                      raise QAPIParseError(
> >                          self,
> >                          "unexpected '=' markup in definition
> documentation")
> >                  else:
> >                      # tag-less paragraph
> > -                    doc.ensure_untagged_section(self.info)
> > +                    doc.ensure_untagged_section(self.info, intro)
> >                      doc.append_line(line)
> >                      line = self.get_doc_paragraph(doc)
> >          else:
> > @@ -617,7 +624,7 @@ def __init__(
> >              self,
> >              info: QAPISourceInfo,
> >              tag: Optional[str] = None,
> > -            kind: str = 'paragraph',
> > +            kind: str = 'intro-paragraph',
>
> The question "why is this optional?" crossed my mind when reviewing the
> previous patch.  I left it unasked, because I felt challenging the
> overlap between @kind and @tag was more useful.  However, the new
> default value 'intro-paragraph' feels more arbitrary to me than the old
> one 'paragraph', and that makes the question pop right back into my
> mind.
>

Just "don't break API" habit, nothing more. I can make it mandatory.


> Unless I'm mistaken, all calls but one @tag and @kind.  Making that one
> pass it too feels simpler to me.
>
> Moot if we fuse @tag and @kind, of course.


> >          ):
> >              # section source info, i.e. where it begins
> >              self.info = info
> > @@ -625,7 +632,7 @@ def __init__(
> >              self.tag = tag
> >              # section text without tag
> >              self.text = ''
> > -            # section type - {paragraph, feature, member, tagged}
> > +            # section type - {<intro|outro>-paragraph, feature, member,
> tagged}
>
> Long line.


Oops, default for black is longer. Forgot to enable the "I still use email
patches as part of my penance" setting


> >              self.kind = kind
> >
> >          def append_line(self, line: str) -> None:
> > @@ -666,7 +673,11 @@ def end(self) -> None:
> >                  raise QAPISemError(
> >                      section.info, "text required after '%s:'" %
> section.tag)
> >
> > -    def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
> > +    def ensure_untagged_section(
> > +        self,
> > +        info: QAPISourceInfo,
> > +        intro: bool = True,
>
> Two callers, one passes @info, one doesn't.  Passing it always might be
> simpler.
>

Okeydokey.


> > +    ) -> None:
> >          if self.all_sections and not self.all_sections[-1].tag:
> >              section = self.all_sections[-1]
> >              # Section is empty so far; update info to start *here*.
> > @@ -677,7 +688,8 @@ def ensure_untagged_section(self, info:
> QAPISourceInfo) -> None:
> >                  self.all_sections[-1].text += '\n'
> >              return
> >          # start new section
> > -        section = self.Section(info)
> > +        kind = ("intro" if intro else "outro") + "-paragraph"
> > +        section = self.Section(info, kind=kind)
> >          self.sections.append(section)
> >          self.all_sections.append(section)
>
>
Re: [PATCH 08/20] qapi/parser: differentiate intro and outro paragraphs
Posted by John Snow 6 months, 1 week ago
On Thu, May 16, 2024 at 11:06 AM John Snow <jsnow@redhat.com> wrote:

>
>
> On Thu, May 16, 2024, 5:34 AM Markus Armbruster <armbru@redhat.com> wrote:
>
>> John Snow <jsnow@redhat.com> writes:
>>
>> > Add a semantic tag to paragraphs that appear *before* tagged
>> > sections/members/features and those that appear after. This will control
>> > how they are inlined when doc sections are merged and flattened.
>>
>> This future use is not obvious to me now.  I guess the effective way to
>> help me see it is actual patches, which will come in due time.
>>
>
> Head recursion and tail recursion, respectively :)
>
> * intro
> * inherited intro
> * members [ancestor-descendent]
> * features [ancestor-descendent]
> * inherited outro
> * outro
>
> Child gets the first and final words. Inherited stuff goes in the sandwich
> fillings.
>
> It feels like a simple rule that's easy to internalize. As a bonus, you
> can explain it by analogy to Americans as a burger, which is the only
> metaphor we understand.
>
>
>> > Signed-off-by: John Snow <jsnow@redhat.com>
>> > ---
>> >  scripts/qapi/parser.py | 22 +++++++++++++++++-----
>> >  1 file changed, 17 insertions(+), 5 deletions(-)
>> >
>> > diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
>> > index cf4cbca1c1f..b1794f71e12 100644
>> > --- a/scripts/qapi/parser.py
>> > +++ b/scripts/qapi/parser.py
>> > @@ -503,6 +503,10 @@ def get_doc(self) -> 'QAPIDoc':
>> >              self.accept(False)
>> >              line = self.get_doc_line()
>> >              no_more_args = False
>> > +            # Paragraphs before members/features/tagged are "intro"
>> paragraphs.
>> > +            # Any appearing subsequently are "outro" paragraphs.
>> > +            # This is only semantic metadata for the doc generator.
>>
>> Not sure about the last sentence.  Isn't it true for almost everything
>> around here?
>>
>
> I guess I was trying to say "There's no real difference between the two
> mechanically, it's purely based on where it appears in the doc block, which
> offers only a heuristic for its semantic value- introductory statements or
> additional detail."
>
> In my mind: the other "kind" values have some more mechanical difference
> to them, but intro/outro don't.
>
>
>> Also, long line.
>>
>> > +            intro = True
>> >
>> >              while line is not None:
>> >                  # Blank lines
>> > @@ -532,6 +536,7 @@ def get_doc(self) -> 'QAPIDoc':
>> >                          raise QAPIParseError(
>> >                              self, 'feature descriptions expected')
>> >                      no_more_args = True
>> > +                    intro = False
>>
>> After feature descriptions.
>>
>> >                  elif match := self._match_at_name_colon(line):
>> >                      # description
>> >                      if no_more_args:
>> > @@ -547,6 +552,7 @@ def get_doc(self) -> 'QAPIDoc':
>> >                              doc.append_line(text)
>> >                          line = self.get_doc_indented(doc)
>> >                      no_more_args = True
>> > +                    intro = False
>>
>> Or after member descriptions.
>>
>> >                  elif match := re.match(
>> >
>> r'(Returns|Errors|Since|Notes?|Examples?|TODO): *',
>> >                          line):
>> > @@ -557,13 +563,14 @@ def get_doc(self) -> 'QAPIDoc':
>> >                          doc.append_line(text)
>> >                      line = self.get_doc_indented(doc)
>> >                      no_more_args = True
>> > +                    intro = False
>>
>> Or after the first tagged section.
>>
>> Okay, it does what it says on the tin.
>>
>> >                  elif line.startswith('='):
>> >                      raise QAPIParseError(
>> >                          self,
>> >                          "unexpected '=' markup in definition
>> documentation")
>> >                  else:
>> >                      # tag-less paragraph
>> > -                    doc.ensure_untagged_section(self.info)
>> > +                    doc.ensure_untagged_section(self.info, intro)
>> >                      doc.append_line(line)
>> >                      line = self.get_doc_paragraph(doc)
>> >          else:
>> > @@ -617,7 +624,7 @@ def __init__(
>> >              self,
>> >              info: QAPISourceInfo,
>> >              tag: Optional[str] = None,
>> > -            kind: str = 'paragraph',
>> > +            kind: str = 'intro-paragraph',
>>
>> The question "why is this optional?" crossed my mind when reviewing the
>> previous patch.  I left it unasked, because I felt challenging the
>> overlap between @kind and @tag was more useful.  However, the new
>> default value 'intro-paragraph' feels more arbitrary to me than the old
>> one 'paragraph', and that makes the question pop right back into my
>> mind.
>>
>
> Just "don't break API" habit, nothing more. I can make it mandatory.
>
>
>> Unless I'm mistaken, all calls but one @tag and @kind.  Making that one
>> pass it too feels simpler to me.
>>
>> Moot if we fuse @tag and @kind, of course.
>
>
>> >          ):
>> >              # section source info, i.e. where it begins
>> >              self.info = info
>> > @@ -625,7 +632,7 @@ def __init__(
>> >              self.tag = tag
>> >              # section text without tag
>> >              self.text = ''
>> > -            # section type - {paragraph, feature, member, tagged}
>> > +            # section type - {<intro|outro>-paragraph, feature,
>> member, tagged}
>>
>> Long line.
>
>
> Oops, default for black is longer. Forgot to enable the "I still use email
> patches as part of my penance" setting
>

Oh, no, this is actually 79 columns on the button. Not picked up by *any*
of our linters. Ehm... can you make the tools enforce them to your
preference instead, please...? What's a "long line"?


>
>
>> >              self.kind = kind
>> >
>> >          def append_line(self, line: str) -> None:
>> > @@ -666,7 +673,11 @@ def end(self) -> None:
>> >                  raise QAPISemError(
>> >                      section.info, "text required after '%s:'" %
>> section.tag)
>> >
>> > -    def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
>> > +    def ensure_untagged_section(
>> > +        self,
>> > +        info: QAPISourceInfo,
>> > +        intro: bool = True,
>>
>> Two callers, one passes @info, one doesn't.  Passing it always might be
>> simpler.
>>
>
> Okeydokey.
>
>
>> > +    ) -> None:
>> >          if self.all_sections and not self.all_sections[-1].tag:
>> >              section = self.all_sections[-1]
>> >              # Section is empty so far; update info to start *here*.
>> > @@ -677,7 +688,8 @@ def ensure_untagged_section(self, info:
>> QAPISourceInfo) -> None:
>> >                  self.all_sections[-1].text += '\n'
>> >              return
>> >          # start new section
>> > -        section = self.Section(info)
>> > +        kind = ("intro" if intro else "outro") + "-paragraph"
>> > +        section = self.Section(info, kind=kind)
>> >          self.sections.append(section)
>> >          self.all_sections.append(section)
>>
>>