diff mbox series

[v2,15/21] docs/qapidoc: create qmp-example directive

Message ID 20240626222128.406106-16-jsnow@redhat.com (mailing list archive)
State New
Headers show
Series qapi: convert "Note" and "Example" sections to rST | expand

Commit Message

John Snow June 26, 2024, 10:21 p.m. UTC
This is a directive that creates a syntactic sugar for creating
"Example" boxes very similar to the ones already used in the bitmaps.rst
document, please see e.g.
https://www.qemu.org/docs/master/interop/bitmaps.html#creation-block-dirty-bitmap-add

In its simplest form, when a custom title is not needed or wanted, and
the example body is *solely* a QMP example:

```
.. qmp-example::

   {body}
```

is syntactic sugar for:

```
.. admonition:: Example:

   .. code-block:: QMP

      {body}
```

When a custom, plaintext title that describes the example is desired,
this form:

```
.. qmp-example::
   :title: Defrobnification

   {body}
```

Is syntactic sugar for:

```
.. admonition:: Example: Defrobnification

   .. code-block:: QMP

      {body}
```

Lastly, when Examples are multi-step processes that require non-QMP
exposition, have lengthy titles, or otherwise involve prose with rST
markup (lists, cross-references, etc), the most complex form:

```
.. qmp-example::
   :annotated:

   This example shows how to use `foo-command`::

     {body}
```

Is desugared to:

```
.. admonition:: Example:

   This example shows how to use `foo-command`::

     {body}

   For more information, please see `frobnozz`.
```

The primary benefit here being documentation source consistently using
the same directive for all forms of examples to ensure consistent visual
styling, and ensuring all relevant prose is visually grouped alongside
the code literal block.

Note that as of this commit, the code-block rST syntax "::" does not
apply QMP highlighting; you would need to use ".. code-block:: QMP". The
very next commit changes this behavior to assume all "::" code blocks
within this directive are QMP blocks.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 docs/sphinx/qapidoc.py | 60 ++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 58 insertions(+), 2 deletions(-)

Comments

Markus Armbruster June 28, 2024, 1:24 p.m. UTC | #1
John Snow <jsnow@redhat.com> writes:

> This is a directive that creates a syntactic sugar for creating
> "Example" boxes very similar to the ones already used in the bitmaps.rst
> document, please see e.g.
> https://www.qemu.org/docs/master/interop/bitmaps.html#creation-block-dirty-bitmap-add
>
> In its simplest form, when a custom title is not needed or wanted, and
> the example body is *solely* a QMP example:
>
> ```
> .. qmp-example::
>
>    {body}
> ```
>
> is syntactic sugar for:
>
> ```
> .. admonition:: Example:
>
>    .. code-block:: QMP
>
>       {body}
> ```
>
> When a custom, plaintext title that describes the example is desired,
> this form:
>
> ```
> .. qmp-example::
>    :title: Defrobnification
>
>    {body}
> ```
>
> Is syntactic sugar for:
>
> ```
> .. admonition:: Example: Defrobnification
>
>    .. code-block:: QMP
>
>       {body}
> ```
>
> Lastly, when Examples are multi-step processes that require non-QMP
> exposition, have lengthy titles, or otherwise involve prose with rST
> markup (lists, cross-references, etc), the most complex form:
>
> ```
> .. qmp-example::
>    :annotated:
>
>    This example shows how to use `foo-command`::
>
>      {body}
> ```
>
> Is desugared to:
>
> ```
> .. admonition:: Example:
>
>    This example shows how to use `foo-command`::
>
>      {body}
>
>    For more information, please see `frobnozz`.
> ```

Can we combine the latter two?  Like this:

  .. qmp-example::
     :title: Defrobnification
     :annotated:

     This example shows how to use `foo-command`::

       {body}

> The primary benefit here being documentation source consistently using
> the same directive for all forms of examples to ensure consistent visual
> styling, and ensuring all relevant prose is visually grouped alongside
> the code literal block.
>
> Note that as of this commit, the code-block rST syntax "::" does not
> apply QMP highlighting; you would need to use ".. code-block:: QMP". The
> very next commit changes this behavior to assume all "::" code blocks
> within this directive are QMP blocks.
>
> Signed-off-by: John Snow <jsnow@redhat.com>
> ---
>  docs/sphinx/qapidoc.py | 60 ++++++++++++++++++++++++++++++++++++++++--
>  1 file changed, 58 insertions(+), 2 deletions(-)

No tests?  Hmm, I see you convert existing tests in PATCH 19-21.  While
that works, test coverage now would make it easier to see how each patch
affects doc generator output.

> diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
> index 43dd99e21e6..a2fa05fc491 100644
> --- a/docs/sphinx/qapidoc.py
> +++ b/docs/sphinx/qapidoc.py
> @@ -27,16 +27,19 @@
>  import os
>  import re
>  import textwrap
> +from typing import List
>  
>  from docutils import nodes
> -from docutils.parsers.rst import Directive, directives
> +from docutils.parsers.rst import directives
>  from docutils.statemachine import ViewList
>  from qapi.error import QAPIError, QAPISemError
>  from qapi.gen import QAPISchemaVisitor
>  from qapi.schema import QAPISchema
>  
>  import sphinx
> +from sphinx.directives.code import CodeBlock
>  from sphinx.errors import ExtensionError
> +from sphinx.util.docutils import SphinxDirective
>  from sphinx.util.nodes import nested_parse_with_titles
>  
>  
> @@ -494,7 +497,7 @@ def visit_module(self, name):
>          super().visit_module(name)
>  
>  
> -class NestedDirective(Directive):
> +class NestedDirective(SphinxDirective):

What is this about?

>      def run(self):
>          raise NotImplementedError
>  
> @@ -567,10 +570,63 @@ def run(self):
>              raise ExtensionError(str(err)) from err
>  
>  
> +class QMPExample(CodeBlock, NestedDirective):
> +    """
> +    Custom admonition for QMP code examples.
> +
> +    When the :annotated: option is present, the body of this directive
> +    is parsed as normal rST instead. Code blocks must be explicitly
> +    written by the user, but this allows for intermingling explanatory
> +    paragraphs with arbitrary rST syntax and code blocks for more
> +    involved examples.
> +
> +    When :annotated: is absent, the directive body is treated as a
> +    simple standalone QMP code block literal.
> +    """
> +
> +    required_argument = 0
> +    optional_arguments = 0
> +    has_content = True
> +    option_spec = {
> +        "annotated": directives.flag,
> +        "title": directives.unchanged,
> +    }
> +
> +    def admonition_wrap(self, *content) -> List[nodes.Node]:
> +        title = "Example:"
> +        if "title" in self.options:
> +            title = f"{title} {self.options['title']}"
> +
> +        admon = nodes.admonition(
> +            "",
> +            nodes.title("", title),
> +            *content,
> +            classes=["admonition", "admonition-example"],
> +        )
> +        return [admon]
> +
> +    def run_annotated(self) -> List[nodes.Node]:
> +        content_node: nodes.Element = nodes.section()
> +        self.do_parse(self.content, content_node)
> +        return content_node.children
> +
> +    def run(self) -> List[nodes.Node]:
> +        annotated = "annotated" in self.options
> +
> +        if annotated:
> +            content_nodes = self.run_annotated()
> +        else:
> +            self.arguments = ["QMP"]
> +            content_nodes = super().run()
> +
> +        return self.admonition_wrap(*content_nodes)
> +
> +
>  def setup(app):
>      """Register qapi-doc directive with Sphinx"""
>      app.add_config_value("qapidoc_srctree", None, "env")
>      app.add_directive("qapi-doc", QAPIDocDirective)
> +    app.add_directive("qmp-example", QMPExample)
>  
>      return {
>          "version": __version__,
John Snow June 28, 2024, 3:24 p.m. UTC | #2
On Fri, Jun 28, 2024, 9:24 AM Markus Armbruster <armbru@redhat.com> wrote:

> John Snow <jsnow@redhat.com> writes:
>
> > This is a directive that creates a syntactic sugar for creating
> > "Example" boxes very similar to the ones already used in the bitmaps.rst
> > document, please see e.g.
> >
> https://www.qemu.org/docs/master/interop/bitmaps.html#creation-block-dirty-bitmap-add
> >
> > In its simplest form, when a custom title is not needed or wanted, and
> > the example body is *solely* a QMP example:
> >
> > ```
> > .. qmp-example::
> >
> >    {body}
> > ```
> >
> > is syntactic sugar for:
> >
> > ```
> > .. admonition:: Example:
> >
> >    .. code-block:: QMP
> >
> >       {body}
> > ```
> >
> > When a custom, plaintext title that describes the example is desired,
> > this form:
> >
> > ```
> > .. qmp-example::
> >    :title: Defrobnification
> >
> >    {body}
> > ```
> >
> > Is syntactic sugar for:
> >
> > ```
> > .. admonition:: Example: Defrobnification
> >
> >    .. code-block:: QMP
> >
> >       {body}
> > ```
> >
> > Lastly, when Examples are multi-step processes that require non-QMP
> > exposition, have lengthy titles, or otherwise involve prose with rST
> > markup (lists, cross-references, etc), the most complex form:
> >
> > ```
> > .. qmp-example::
> >    :annotated:
> >
> >    This example shows how to use `foo-command`::
> >
> >      {body}
> > ```
> >
> > Is desugared to:
> >
> > ```
> > .. admonition:: Example:
> >
> >    This example shows how to use `foo-command`::
> >
> >      {body}
> >
> >    For more information, please see `frobnozz`.
> > ```
>

^ Whoops, added prose in the desugar block without modifying the original.


> Can we combine the latter two?  Like this:
>
>   .. qmp-example::
>      :title: Defrobnification
>      :annotated:
>
>      This example shows how to use `foo-command`::
>
>        {body}
>

Yes! I only didn't use that form in the series because splitting longer
Examples into title and prose felt like an editorial decision, but
absolutely you can use both.


> > The primary benefit here being documentation source consistently using
> > the same directive for all forms of examples to ensure consistent visual
> > styling, and ensuring all relevant prose is visually grouped alongside
> > the code literal block.
> >
> > Note that as of this commit, the code-block rST syntax "::" does not
> > apply QMP highlighting; you would need to use ".. code-block:: QMP". The
> > very next commit changes this behavior to assume all "::" code blocks
> > within this directive are QMP blocks.
> >
> > Signed-off-by: John Snow <jsnow@redhat.com>
> > ---
> >  docs/sphinx/qapidoc.py | 60 ++++++++++++++++++++++++++++++++++++++++--
> >  1 file changed, 58 insertions(+), 2 deletions(-)
>
> No tests?  Hmm, I see you convert existing tests in PATCH 19-21.  While
> that works, test coverage now would make it easier to see how each patch
> affects doc generator output.
>

Mmm. Do you want me to move the test changes up to this patch ... ?


> > diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
> > index 43dd99e21e6..a2fa05fc491 100644
> > --- a/docs/sphinx/qapidoc.py
> > +++ b/docs/sphinx/qapidoc.py
> > @@ -27,16 +27,19 @@
> >  import os
> >  import re
> >  import textwrap
> > +from typing import List
> >
> >  from docutils import nodes
> > -from docutils.parsers.rst import Directive, directives
> > +from docutils.parsers.rst import directives
> >  from docutils.statemachine import ViewList
> >  from qapi.error import QAPIError, QAPISemError
> >  from qapi.gen import QAPISchemaVisitor
> >  from qapi.schema import QAPISchema
> >
> >  import sphinx
> > +from sphinx.directives.code import CodeBlock
> >  from sphinx.errors import ExtensionError
> > +from sphinx.util.docutils import SphinxDirective
> >  from sphinx.util.nodes import nested_parse_with_titles
> >
> >
> > @@ -494,7 +497,7 @@ def visit_module(self, name):
> >          super().visit_module(name)
> >
> >
> > -class NestedDirective(Directive):
> > +class NestedDirective(SphinxDirective):
>
> What is this about?
>

Hmm. Strictly it's for access to sphinx configuration which I use only in
the next patch, but practically I suspect if I don't change it *here* that
the multiple inheritance from CodeBlock (which is a SphinxDirective) would
possibly be stranger.

I can try delaying that change by a patch and see if it hurts anything ...


> >      def run(self):
> >          raise NotImplementedError
> >
> > @@ -567,10 +570,63 @@ def run(self):
> >              raise ExtensionError(str(err)) from err
> >
> >
> > +class QMPExample(CodeBlock, NestedDirective):
> > +    """
> > +    Custom admonition for QMP code examples.
> > +
> > +    When the :annotated: option is present, the body of this directive
> > +    is parsed as normal rST instead. Code blocks must be explicitly
> > +    written by the user, but this allows for intermingling explanatory
> > +    paragraphs with arbitrary rST syntax and code blocks for more
> > +    involved examples.
> > +
> > +    When :annotated: is absent, the directive body is treated as a
> > +    simple standalone QMP code block literal.
> > +    """
> > +
> > +    required_argument = 0
> > +    optional_arguments = 0
> > +    has_content = True
> > +    option_spec = {
> > +        "annotated": directives.flag,
> > +        "title": directives.unchanged,
> > +    }
> > +
> > +    def admonition_wrap(self, *content) -> List[nodes.Node]:
> > +        title = "Example:"
> > +        if "title" in self.options:
> > +            title = f"{title} {self.options['title']}"
> > +
> > +        admon = nodes.admonition(
> > +            "",
> > +            nodes.title("", title),
> > +            *content,
> > +            classes=["admonition", "admonition-example"],
> > +        )
> > +        return [admon]
> > +
> > +    def run_annotated(self) -> List[nodes.Node]:
> > +        content_node: nodes.Element = nodes.section()
> > +        self.do_parse(self.content, content_node)
> > +        return content_node.children
> > +
> > +    def run(self) -> List[nodes.Node]:
> > +        annotated = "annotated" in self.options
> > +
> > +        if annotated:
> > +            content_nodes = self.run_annotated()
> > +        else:
> > +            self.arguments = ["QMP"]
> > +            content_nodes = super().run()
> > +
> > +        return self.admonition_wrap(*content_nodes)
> > +
> > +
> >  def setup(app):
> >      """Register qapi-doc directive with Sphinx"""
> >      app.add_config_value("qapidoc_srctree", None, "env")
> >      app.add_directive("qapi-doc", QAPIDocDirective)
> > +    app.add_directive("qmp-example", QMPExample)
> >
> >      return {
> >          "version": __version__,
>
>
diff mbox series

Patch

diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index 43dd99e21e6..a2fa05fc491 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -27,16 +27,19 @@ 
 import os
 import re
 import textwrap
+from typing import List
 
 from docutils import nodes
-from docutils.parsers.rst import Directive, directives
+from docutils.parsers.rst import directives
 from docutils.statemachine import ViewList
 from qapi.error import QAPIError, QAPISemError
 from qapi.gen import QAPISchemaVisitor
 from qapi.schema import QAPISchema
 
 import sphinx
+from sphinx.directives.code import CodeBlock
 from sphinx.errors import ExtensionError
+from sphinx.util.docutils import SphinxDirective
 from sphinx.util.nodes import nested_parse_with_titles
 
 
@@ -494,7 +497,7 @@  def visit_module(self, name):
         super().visit_module(name)
 
 
-class NestedDirective(Directive):
+class NestedDirective(SphinxDirective):
     def run(self):
         raise NotImplementedError
 
@@ -567,10 +570,63 @@  def run(self):
             raise ExtensionError(str(err)) from err
 
 
+class QMPExample(CodeBlock, NestedDirective):
+    """
+    Custom admonition for QMP code examples.
+
+    When the :annotated: option is present, the body of this directive
+    is parsed as normal rST instead. Code blocks must be explicitly
+    written by the user, but this allows for intermingling explanatory
+    paragraphs with arbitrary rST syntax and code blocks for more
+    involved examples.
+
+    When :annotated: is absent, the directive body is treated as a
+    simple standalone QMP code block literal.
+    """
+
+    required_argument = 0
+    optional_arguments = 0
+    has_content = True
+    option_spec = {
+        "annotated": directives.flag,
+        "title": directives.unchanged,
+    }
+
+    def admonition_wrap(self, *content) -> List[nodes.Node]:
+        title = "Example:"
+        if "title" in self.options:
+            title = f"{title} {self.options['title']}"
+
+        admon = nodes.admonition(
+            "",
+            nodes.title("", title),
+            *content,
+            classes=["admonition", "admonition-example"],
+        )
+        return [admon]
+
+    def run_annotated(self) -> List[nodes.Node]:
+        content_node: nodes.Element = nodes.section()
+        self.do_parse(self.content, content_node)
+        return content_node.children
+
+    def run(self) -> List[nodes.Node]:
+        annotated = "annotated" in self.options
+
+        if annotated:
+            content_nodes = self.run_annotated()
+        else:
+            self.arguments = ["QMP"]
+            content_nodes = super().run()
+
+        return self.admonition_wrap(*content_nodes)
+
+
 def setup(app):
     """Register qapi-doc directive with Sphinx"""
     app.add_config_value("qapidoc_srctree", None, "env")
     app.add_directive("qapi-doc", QAPIDocDirective)
+    app.add_directive("qmp-example", QMPExample)
 
     return {
         "version": __version__,