* [PATCH v3 00/63] docs: Add new QAPI transmogrifier
@ 2025-03-11 3:41 John Snow
2025-03-11 3:41 ` [PATCH v3 01/63] do-not-merge John Snow
` (63 more replies)
0 siblings, 64 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:41 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This series is a "minimum viable" version of the new QAPI documentation
system. It does the bare minimum under the new framework, saving the
fancy features like the inliner for later. This version does add
cross-references for all QAPI definitions and a shiny new QAPI Index.
Patches 3-31 implement the qapi_domain extension.
Patches 32-59 implement the qapidoc "Transmogrifier".
Known shortcomings in this series:
- Some ifcond information (features, members) is still not displayed.
- No QAPI namespace support ... yet. So we can't enable it for QMP, QGA
and QSD simultaneously just yet. It's nearly complete.
v3:
- Moved a few more methods from QAPIObject to QAPIDescription
- Implemented most of the rest of Markus' feedback.
v2:
- Did you know that my computer just shut off the moment I started
sending the patchset? Just a hard off. Wow!
- Refactored QAPIObject class so that QAPIModule uses more of
Sphinx's ObjectDescription class, which means less fooling around with
re-parsing Sphinx standard options.
- Removed test document, flipped switch on the real QMP manual.
- Undocumented members have been re-added. I think that's new for this version!
- Removed some inliner branch code that snuck into the "validate info
fields" patch.
- Lots of Markus' feedback, and misc changes.
"v1":
- @foo is processed into ``foo`
- "The members of ..." messages have been temporarily re-added until we
can smooth over the inliner.
- This series runs under Sphinx 3.4.3 to Sphinx 8.2.0 inclusive. It
truly is a Christmas miracle. (please clap)
- This series now fully type checks and lint checks under Sphinx 8.2.0,
but may not type check under earlier Sphinx versions. Achieving this
alone, nevermind in conjunction with the above, was a literal
herculean labor.
John Snow (63):
do-not-merge
qapi: shush pylint up
docs/sphinx: create QAPI domain extension stub
docs/sphinx: add compat.py module and nested_parse helper
docs/qapi-domain: add QAPI domain object registry
docs/qapi-domain: add QAPI index
docs/qapi-domain: add resolve_any_xref()
docs/qapi-domain: add QAPI xref roles
docs/qapi-domain: add compatibility node classes
docs/qapi-domain: Add QAPIDescription abstract class
docs/qapi-domain: add qapi:module directive
docs/qapi-domain: add QAPIObject class
docs/qapi-domain: add qapi:command directive
docs/qapi-domain: add :since: directive option
docs/qapi-domain: add "Arguments:" field lists
docs/qapi-domain: add "Features:" field lists
docs/qapi-domain: add "Errors:" field lists
docs/qapi-domain: add "Return:" field lists
docs/qapi-domain: add qapi:enum directive
docs/qapi-domain: add qapi:alternate directive
docs/qapi-domain: add qapi:event directive
docs/qapi-domain: add qapi:object directive
docs/qapi-domain: add :deprecated: directive option
docs/qapi-domain: add :unstable: directive option
docs/qapi-domain: add :ifcond: directive option
docs/qapi-domain: add warnings for malformed field lists
docs/qapi-domain: add type cross-refs to field lists
docs/qapi-domain: add CSS styling
docs/qapi-domain: add XREF compatibility goop for Sphinx < 4.1
docs/qapi-domain: warn when QAPI domain xrefs fail to resolve
docs/qapi-domain: Fix error context reporting in Sphinx 5.x and 6.x
qapi/parser: adjust info location for doc body section
qapi: clean up encoding of section kinds
qapi/schema: add __repr__ to QAPIDoc.Section
docs/qapidoc: add transmogrifier stub
docs/qapidoc: split old implementation into qapidoc_legacy.py
docs/qapidoc: Fix static typing on qapidoc.py
do-not-merge
docs/qapidoc: add transmogrifier class stub
docs/qapidoc: add visit_module() method
qapi/source: allow multi-line QAPISourceInfo advancing
docs/qapidoc: add visit_freeform() method
docs/qapidoc: add preamble() method
docs/qapidoc: add visit_paragraph() method
docs/qapidoc: add visit_errors() method
docs/qapidoc: add format_type() method
docs/qapidoc: add add_field() and generate_field() helper methods
docs/qapidoc: add visit_feature() method
docs/qapidoc: prepare to record entity being transmogrified
docs/qapidoc: add visit_returns() method
docs/qapidoc: add visit_member() method
docs/qapidoc: add visit_sections() method
docs/qapidoc: add visit_entity()
docs/qapidoc: implement transmogrify() method
docs/qapidoc: process @foo into ``foo``
docs/qapidoc: add intermediate output debugger
docs/qapidoc: Add "the members of" pointers
docs/qapidoc: generate entries for undocumented members
qapi/parser: add undocumented stub members to all_sections
docs: disambiguate cross-references
docs: enable qapidoc transmogrifier for QEMU QMP Reference
docs: add qapi-domain syntax documentation
MAINTAINERS: Add jsnow as maintainer for Sphinx documentation
MAINTAINERS | 1 +
docs/conf.py | 18 +-
docs/devel/codebase.rst | 6 +-
docs/devel/index-build.rst | 1 +
docs/devel/qapi-domain.rst | 670 ++++++++++++++++++
docs/glossary.rst | 10 +-
docs/interop/qemu-qmp-ref.rst | 1 +
docs/sphinx-static/theme_overrides.css | 98 ++-
docs/sphinx/compat.py | 230 ++++++
docs/sphinx/qapi_domain.py | 931 ++++++++++++++++++++++++
docs/sphinx/qapidoc.py | 941 +++++++++++++------------
docs/sphinx/qapidoc_legacy.py | 440 ++++++++++++
qapi/qapi-schema.json | 2 +
scripts/qapi-lint.sh | 57 ++
scripts/qapi/backend.py | 2 +
scripts/qapi/main.py | 8 +-
scripts/qapi/parser.py | 123 +++-
scripts/qapi/source.py | 4 +-
tests/qapi-schema/doc-good.out | 10 +-
tests/qapi-schema/test-qapi.py | 2 +-
20 files changed, 3070 insertions(+), 485 deletions(-)
create mode 100644 docs/devel/qapi-domain.rst
create mode 100644 docs/sphinx/compat.py
create mode 100644 docs/sphinx/qapi_domain.py
create mode 100644 docs/sphinx/qapidoc_legacy.py
create mode 100755 scripts/qapi-lint.sh
--
2.48.1
^ permalink raw reply [flat|nested] 74+ messages in thread
* [PATCH v3 01/63] do-not-merge
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
@ 2025-03-11 3:41 ` John Snow
2025-03-11 3:42 ` [PATCH v3 02/63] qapi: shush pylint up John Snow
` (62 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:41 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Ad-hoc linting scripts to scrub down the new docs/sphinx files. Should
work with a reasonably modern mypy/pylint/etc, and Sphinx 8.2.0. Older
versions of Sphinx ought to still work at runtime, but may not type
check correctly.
Signed-off-by: John Snow <jsnow@redhat.com>
---
scripts/qapi-lint.sh | 57 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 57 insertions(+)
create mode 100755 scripts/qapi-lint.sh
diff --git a/scripts/qapi-lint.sh b/scripts/qapi-lint.sh
new file mode 100755
index 00000000000..7534ab0df98
--- /dev/null
+++ b/scripts/qapi-lint.sh
@@ -0,0 +1,57 @@
+#!/usr/bin/env bash
+set -e
+
+if [[ -f qapi/.flake8 ]]; then
+ echo "flake8 --config=qapi/.flake8 qapi/"
+ flake8 --config=qapi/.flake8 qapi/
+fi
+if [[ -f qapi/pylintrc ]]; then
+ echo "pylint --rcfile=qapi/pylintrc qapi/"
+ pylint --rcfile=qapi/pylintrc qapi/
+fi
+if [[ -f qapi/mypy.ini ]]; then
+ echo "mypy --config-file=qapi/mypy.ini qapi/"
+ mypy --config-file=qapi/mypy.ini qapi/
+fi
+
+if [[ -f qapi/.isort.cfg ]]; then
+ pushd qapi
+ echo "isort -c ."
+ isort -c .
+ popd
+fi
+
+if [[ -f ../docs/sphinx/qapi_domain.py ]]; then
+ files="qapi_domain.py"
+fi
+if [[ -f ../docs/sphinx/compat.py ]]; then
+ files="${files} compat.py"
+fi
+if [[ -f ../docs/sphinx/collapse.py ]]; then
+ files="${files} collapse.py"
+fi
+
+if [[ -f ../docs/sphinx/qapi_domain.py ]]; then
+ pushd ../docs/sphinx
+
+ set -x
+ mypy --strict $files
+ flake8 --max-line-length=79 $files qapidoc.py
+ isort -c $files qapidoc.py
+ black --line-length 79 --check $files qapidoc.py
+ PYTHONPATH=../../scripts/ pylint \
+ --rc-file ../../scripts/qapi/pylintrc \
+ $files qapidoc.py
+ set +x
+
+ popd
+fi
+
+pushd ../build
+#make -j13
+make check-qapi-schema
+rm -rf docs/
+#make docs
+#make sphinxdocs
+time pyvenv/bin/sphinx-build -v -j 8 -b html -d docs/manual.p/ ../docs/ docs/manual/;
+popd
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 02/63] qapi: shush pylint up
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
2025-03-11 3:41 ` [PATCH v3 01/63] do-not-merge John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 6:59 ` Markus Armbruster
2025-03-11 3:42 ` [PATCH v3 03/63] docs/sphinx: create QAPI domain extension stub John Snow
` (61 subsequent siblings)
63 siblings, 1 reply; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Shhhhh!
This patch can be dropped from the PR and I'll clean it up later. It's
just here to help me establish a linting baseline. It isn't really
needed for the series itself.
Signed-off-by: John Snow <jsnow@redhat.com>
---
scripts/qapi/backend.py | 2 ++
scripts/qapi/main.py | 8 +++-----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/scripts/qapi/backend.py b/scripts/qapi/backend.py
index 14e60aa67af..49ae6ecdd33 100644
--- a/scripts/qapi/backend.py
+++ b/scripts/qapi/backend.py
@@ -13,6 +13,7 @@
class QAPIBackend(ABC):
+ # pylint: disable=too-few-public-methods
@abstractmethod
def generate(self,
@@ -36,6 +37,7 @@ def generate(self,
class QAPICBackend(QAPIBackend):
+ # pylint: disable=too-few-public-methods
def generate(self,
schema: QAPISchema,
diff --git a/scripts/qapi/main.py b/scripts/qapi/main.py
index 5b4679abcf1..01155373bd0 100644
--- a/scripts/qapi/main.py
+++ b/scripts/qapi/main.py
@@ -38,8 +38,7 @@ def create_backend(path: str) -> QAPIBackend:
try:
mod = import_module(module_path)
except Exception as ex:
- print(f"unable to import '{module_path}': {ex}", file=sys.stderr)
- sys.exit(1)
+ raise QAPIError(f"unable to import '{module_path}': {ex}") from ex
try:
klass = getattr(mod, class_name)
@@ -51,9 +50,8 @@ def create_backend(path: str) -> QAPIBackend:
try:
backend = klass()
except Exception as ex:
- print(f"backend '{path}' cannot be instantiated: {ex}",
- file=sys.stderr)
- sys.exit(1)
+ raise QAPIError(
+ f"backend '{path}' cannot be instantiated: {ex}") from ex
if not isinstance(backend, QAPIBackend):
print(f"backend '{path}' must be an instance of QAPIBackend",
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 03/63] docs/sphinx: create QAPI domain extension stub
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
2025-03-11 3:41 ` [PATCH v3 01/63] do-not-merge John Snow
2025-03-11 3:42 ` [PATCH v3 02/63] qapi: shush pylint up John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 04/63] docs/sphinx: add compat.py module and nested_parse helper John Snow
` (60 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
A Sphinx domain is a collection of directive and role extensions meant
to facilitate the documentation of a specific language. For instance,
Sphinx ships with "python" and "cpp" domains. This patch introduces a
stub for the "qapi" language domain.
Please see https://www.sphinx-doc.org/en/master/usage/domains/index.html
for more information.
This stub doesn't really do anything yet, we'll get to it brick-by-brick
in the forthcoming commits to keep the series breezy and the git history
informative.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/conf.py | 9 +++++-
docs/sphinx/qapi_domain.py | 56 ++++++++++++++++++++++++++++++++++++++
2 files changed, 64 insertions(+), 1 deletion(-)
create mode 100644 docs/sphinx/qapi_domain.py
diff --git a/docs/conf.py b/docs/conf.py
index 31bb9a37893..49d9de894c0 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -60,7 +60,14 @@
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
-extensions = ['kerneldoc', 'qmp_lexer', 'hxtool', 'depfile', 'qapidoc']
+extensions = [
+ 'depfile',
+ 'hxtool',
+ 'kerneldoc',
+ 'qapi_domain',
+ 'qapidoc',
+ 'qmp_lexer',
+]
if sphinx.version_info[:3] > (4, 0, 0):
tags.add('sphinx4')
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
new file mode 100644
index 00000000000..a1983d94440
--- /dev/null
+++ b/docs/sphinx/qapi_domain.py
@@ -0,0 +1,56 @@
+"""
+QAPI domain extension.
+"""
+
+from __future__ import annotations
+
+from typing import (
+ TYPE_CHECKING,
+ AbstractSet,
+ Any,
+ Dict,
+ Tuple,
+)
+
+from sphinx.domains import Domain, ObjType
+from sphinx.util import logging
+
+
+if TYPE_CHECKING:
+ from sphinx.application import Sphinx
+
+logger = logging.getLogger(__name__)
+
+
+class QAPIDomain(Domain):
+ """QAPI language domain."""
+
+ name = "qapi"
+ label = "QAPI"
+
+ object_types: Dict[str, ObjType] = {}
+ directives = {}
+ roles = {}
+ initial_data: Dict[str, Dict[str, Tuple[Any]]] = {}
+ indices = []
+
+ def merge_domaindata(
+ self, docnames: AbstractSet[str], otherdata: Dict[str, Any]
+ ) -> None:
+ pass
+
+ def resolve_any_xref(self, *args: Any, **kwargs: Any) -> Any:
+ # pylint: disable=unused-argument
+ return []
+
+
+def setup(app: Sphinx) -> Dict[str, Any]:
+ app.setup_extension("sphinx.directives")
+ app.add_domain(QAPIDomain)
+
+ return {
+ "version": "1.0",
+ "env_version": 1,
+ "parallel_read_safe": True,
+ "parallel_write_safe": True,
+ }
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 04/63] docs/sphinx: add compat.py module and nested_parse helper
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (2 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 03/63] docs/sphinx: create QAPI domain extension stub John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 05/63] docs/qapi-domain: add QAPI domain object registry John Snow
` (59 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Create a compat module that handles sphinx cross-version compatibility
issues. For the inaugural function, add a nested_parse_with_titles()
helper that handles differences in line number tracking for nested
directive body parsing.
Spoilers: there are more cross-version hacks to come throughout the
series.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/compat.py | 35 +++++++++++++++++++++++++++++++++++
1 file changed, 35 insertions(+)
create mode 100644 docs/sphinx/compat.py
diff --git a/docs/sphinx/compat.py b/docs/sphinx/compat.py
new file mode 100644
index 00000000000..39b859a25e3
--- /dev/null
+++ b/docs/sphinx/compat.py
@@ -0,0 +1,35 @@
+"""
+Sphinx cross-version compatibility goop
+"""
+
+from docutils.nodes import Element
+
+from sphinx.util import nodes
+from sphinx.util.docutils import SphinxDirective, switch_source_input
+
+
+def nested_parse_with_titles(
+ directive: SphinxDirective, content_node: Element
+) -> None:
+ """
+ This helper preserves error parsing context across sphinx versions.
+ """
+
+ # necessary so that the child nodes get the right source/line set
+ content_node.document = directive.state.document
+
+ try:
+ # Modern sphinx (6.2.0+) supports proper offsetting for
+ # nested parse error context management
+ nodes.nested_parse_with_titles(
+ directive.state,
+ directive.content,
+ content_node,
+ content_offset=directive.content_offset,
+ )
+ except TypeError:
+ # No content_offset argument. Fall back to SSI method.
+ with switch_source_input(directive.state, directive.content):
+ nodes.nested_parse_with_titles(
+ directive.state, directive.content, content_node
+ )
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 05/63] docs/qapi-domain: add QAPI domain object registry
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (3 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 04/63] docs/sphinx: add compat.py module and nested_parse helper John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 06/63] docs/qapi-domain: add QAPI index John Snow
` (58 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This is the first step towards QAPI domain cross-references and a QAPI
reference index.
This patch just creates the object registry, and updates the
merge_domaindata stub method now that we have actual data we may need to
merge.
Note that how to handle merge conflict resolution is unhandled, as the
Sphinx python domain itself does not handle it either. I do not know how
to intentionally trigger it, so I've left an assertion instead if it
should ever come up ...
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 77 +++++++++++++++++++++++++++++++++++++-
1 file changed, 75 insertions(+), 2 deletions(-)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index a1983d94440..f3ece42bc2a 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -9,10 +9,12 @@
AbstractSet,
Any,
Dict,
+ NamedTuple,
Tuple,
)
from sphinx.domains import Domain, ObjType
+from sphinx.locale import __
from sphinx.util import logging
@@ -22,22 +24,93 @@
logger = logging.getLogger(__name__)
+class ObjectEntry(NamedTuple):
+ docname: str
+ node_id: str
+ objtype: str
+ aliased: bool
+
+
class QAPIDomain(Domain):
"""QAPI language domain."""
name = "qapi"
label = "QAPI"
+ # This table associates cross-reference object types (key) with an
+ # ObjType instance, which defines the valid cross-reference roles
+ # for each object type.
+
+ # Actual table entries for module, command, event, etc will come in
+ # forthcoming commits.
object_types: Dict[str, ObjType] = {}
+
directives = {}
roles = {}
- initial_data: Dict[str, Dict[str, Tuple[Any]]] = {}
+
+ # Moved into the data property at runtime;
+ # this is the internal index of reference-able objects.
+ initial_data: Dict[str, Dict[str, Tuple[Any]]] = {
+ "objects": {}, # fullname -> ObjectEntry
+ }
+
indices = []
+ @property
+ def objects(self) -> Dict[str, ObjectEntry]:
+ ret = self.data.setdefault("objects", {})
+ return ret # type: ignore[no-any-return]
+
+ def note_object(
+ self,
+ name: str,
+ objtype: str,
+ node_id: str,
+ aliased: bool = False,
+ location: Any = None,
+ ) -> None:
+ """Note a QAPI object for cross reference."""
+ if name in self.objects:
+ other = self.objects[name]
+ if other.aliased and aliased is False:
+ # The original definition found. Override it!
+ pass
+ elif other.aliased is False and aliased:
+ # The original definition is already registered.
+ return
+ else:
+ # duplicated
+ logger.warning(
+ __(
+ "duplicate object description of %s, "
+ "other instance in %s, use :no-index: for one of them"
+ ),
+ name,
+ other.docname,
+ location=location,
+ )
+ self.objects[name] = ObjectEntry(
+ self.env.docname, node_id, objtype, aliased
+ )
+
+ def clear_doc(self, docname: str) -> None:
+ for fullname, obj in list(self.objects.items()):
+ if obj.docname == docname:
+ del self.objects[fullname]
+
def merge_domaindata(
self, docnames: AbstractSet[str], otherdata: Dict[str, Any]
) -> None:
- pass
+ for fullname, obj in otherdata["objects"].items():
+ if obj.docname in docnames:
+ # Sphinx's own python domain doesn't appear to bother to
+ # check for collisions. Assert they don't happen and
+ # we'll fix it if/when the case arises.
+ assert fullname not in self.objects, (
+ "bug - collision on merge?"
+ f" {fullname=} {obj=} {self.objects[fullname]=}"
+ )
+ self.objects[fullname] = obj
def resolve_any_xref(self, *args: Any, **kwargs: Any) -> Any:
# pylint: disable=unused-argument
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 06/63] docs/qapi-domain: add QAPI index
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (4 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 05/63] docs/qapi-domain: add QAPI domain object registry John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 07/63] docs/qapi-domain: add resolve_any_xref() John Snow
` (57 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Use the QAPI object registry to generate a special index just for QAPI
definitions. The index can show entries both by definition type and all
together, alphabetically.
The index can be linked from anywhere in the QEMU manual by using the
reference `qapi-index`.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 73 ++++++++++++++++++++++++++++++++++++--
1 file changed, 70 insertions(+), 3 deletions(-)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index f3ece42bc2a..3e7718d32d1 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -9,12 +9,20 @@
AbstractSet,
Any,
Dict,
+ Iterable,
+ List,
NamedTuple,
+ Optional,
Tuple,
)
-from sphinx.domains import Domain, ObjType
-from sphinx.locale import __
+from sphinx.domains import (
+ Domain,
+ Index,
+ IndexEntry,
+ ObjType,
+)
+from sphinx.locale import _, __
from sphinx.util import logging
@@ -31,6 +39,62 @@ class ObjectEntry(NamedTuple):
aliased: bool
+class QAPIIndex(Index):
+ """
+ Index subclass to provide the QAPI definition index.
+ """
+
+ # pylint: disable=too-few-public-methods
+
+ name = "index"
+ localname = _("QAPI Index")
+ shortname = _("QAPI Index")
+
+ def generate(
+ self,
+ docnames: Optional[Iterable[str]] = None,
+ ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:
+ assert isinstance(self.domain, QAPIDomain)
+ content: Dict[str, List[IndexEntry]] = {}
+ collapse = False
+
+ # list of all object (name, ObjectEntry) pairs, sorted by name
+ # (ignoring the module)
+ objects = sorted(
+ self.domain.objects.items(),
+ key=lambda x: x[0].split(".")[-1].lower(),
+ )
+
+ for objname, obj in objects:
+ if docnames and obj.docname not in docnames:
+ continue
+
+ # Strip the module name out:
+ objname = objname.split(".")[-1]
+
+ # Add an alphabetical entry:
+ entries = content.setdefault(objname[0].upper(), [])
+ entries.append(
+ IndexEntry(
+ objname, 0, obj.docname, obj.node_id, obj.objtype, "", ""
+ )
+ )
+
+ # Add a categorical entry:
+ category = obj.objtype.title() + "s"
+ entries = content.setdefault(category, [])
+ entries.append(
+ IndexEntry(objname, 0, obj.docname, obj.node_id, "", "", "")
+ )
+
+ # alphabetically sort categories; type names first, ABC entries last.
+ sorted_content = sorted(
+ content.items(),
+ key=lambda x: (len(x[0]) == 1, x[0]),
+ )
+ return sorted_content, collapse
+
+
class QAPIDomain(Domain):
"""QAPI language domain."""
@@ -54,7 +118,10 @@ class QAPIDomain(Domain):
"objects": {}, # fullname -> ObjectEntry
}
- indices = []
+ # Index pages to generate; each entry is an Index class.
+ indices = [
+ QAPIIndex,
+ ]
@property
def objects(self) -> Dict[str, ObjectEntry]:
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 07/63] docs/qapi-domain: add resolve_any_xref()
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (5 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 06/63] docs/qapi-domain: add QAPI index John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 08/63] docs/qapi-domain: add QAPI xref roles John Snow
` (56 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Add the ability to resolve cross-references using the `any`
cross-reference syntax. Adding QAPI-specific cross-reference roles will
be added in a forthcoming commit, and will share the same find_obj()
helper.
(There's less code needed for the generic cross-reference resolver, so
it comes first in this series.)
Once again, this code is based very heavily on sphinx.domains.python.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 96 ++++++++++++++++++++++++++++++++++++--
1 file changed, 93 insertions(+), 3 deletions(-)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index 3e7718d32d1..f05c2cadf06 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -16,6 +16,9 @@
Tuple,
)
+from docutils import nodes
+
+from sphinx.addnodes import pending_xref
from sphinx.domains import (
Domain,
Index,
@@ -24,10 +27,15 @@
)
from sphinx.locale import _, __
from sphinx.util import logging
+from sphinx.util.nodes import make_refnode
if TYPE_CHECKING:
+ from docutils.nodes import Element
+
from sphinx.application import Sphinx
+ from sphinx.builders import Builder
+ from sphinx.environment import BuildEnvironment
logger = logging.getLogger(__name__)
@@ -179,9 +187,91 @@ def merge_domaindata(
)
self.objects[fullname] = obj
- def resolve_any_xref(self, *args: Any, **kwargs: Any) -> Any:
- # pylint: disable=unused-argument
- return []
+ def find_obj(
+ self, modname: str, name: str, typ: Optional[str]
+ ) -> list[tuple[str, ObjectEntry]]:
+ """
+ Find a QAPI object for "name", perhaps using the given module.
+
+ Returns a list of (name, object entry) tuples.
+
+ :param modname: The current module context (if any!)
+ under which we are searching.
+ :param name: The name of the x-ref to resolve;
+ may or may not include a leading module.
+ :param type: The role name of the x-ref we're resolving, if provided.
+ (This is absent for "any" lookups.)
+ """
+ if not name:
+ return []
+
+ names: list[str] = []
+ matches: list[tuple[str, ObjectEntry]] = []
+
+ fullname = name
+ if "." in fullname:
+ # We're searching for a fully qualified reference;
+ # ignore the contextual module.
+ pass
+ elif modname:
+ # We're searching for something from somewhere;
+ # try searching the current module first.
+ # e.g. :qapi:cmd:`query-block` or `query-block` is being searched.
+ fullname = f"{modname}.{name}"
+
+ if typ is None:
+ # type isn't specified, this is a generic xref.
+ # search *all* qapi-specific object types.
+ objtypes: List[str] = list(self.object_types)
+ else:
+ # type is specified and will be a role (e.g. obj, mod, cmd)
+ # convert this to eligible object types (e.g. command, module)
+ # using the QAPIDomain.object_types table.
+ objtypes = self.objtypes_for_role(typ, [])
+
+ if name in self.objects and self.objects[name].objtype in objtypes:
+ names = [name]
+ elif (
+ fullname in self.objects
+ and self.objects[fullname].objtype in objtypes
+ ):
+ names = [fullname]
+ else:
+ # exact match wasn't found; e.g. we are searching for
+ # `query-block` from a different (or no) module.
+ searchname = "." + name
+ names = [
+ oname
+ for oname in self.objects
+ if oname.endswith(searchname)
+ and self.objects[oname].objtype in objtypes
+ ]
+
+ matches = [(oname, self.objects[oname]) for oname in names]
+ if len(matches) > 1:
+ matches = [m for m in matches if not m[1].aliased]
+ return matches
+
+ def resolve_any_xref(
+ self,
+ env: BuildEnvironment,
+ fromdocname: str,
+ builder: Builder,
+ target: str,
+ node: pending_xref,
+ contnode: Element,
+ ) -> List[Tuple[str, nodes.reference]]:
+ results: List[Tuple[str, nodes.reference]] = []
+ matches = self.find_obj(node.get("qapi:module"), target, None)
+ for name, obj in matches:
+ rolename = self.role_for_objtype(obj.objtype)
+ assert rolename is not None
+ role = f"qapi:{rolename}"
+ refnode = make_refnode(
+ builder, fromdocname, obj.docname, obj.node_id, contnode, name
+ )
+ results.append((role, refnode))
+ return results
def setup(app: Sphinx) -> Dict[str, Any]:
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 08/63] docs/qapi-domain: add QAPI xref roles
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (6 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 07/63] docs/qapi-domain: add resolve_any_xref() John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 09/63] docs/qapi-domain: add compatibility node classes John Snow
` (55 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Add domain-specific cross-reference syntax. As of this commit, that
means new :qapi:any:`block-core` referencing syntax.
The :any: role will find anything registered to the QAPI domain,
including modules, commands, events, etc.
Creating the cross-references is powered by the QAPIXRefRole class;
resolving them is handled by QAPIDomain.resolve_xref().
QAPIXrefRole is based heavily on Sphinx's own PyXrefRole, with
modifications necessary for QAPI features.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 88 +++++++++++++++++++++++++++++++++++++-
1 file changed, 87 insertions(+), 1 deletion(-)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index f05c2cadf06..49d42c0921c 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -26,6 +26,7 @@
ObjType,
)
from sphinx.locale import _, __
+from sphinx.roles import XRefRole
from sphinx.util import logging
from sphinx.util.nodes import make_refnode
@@ -47,6 +48,54 @@ class ObjectEntry(NamedTuple):
aliased: bool
+class QAPIXRefRole(XRefRole):
+
+ def process_link(
+ self,
+ env: BuildEnvironment,
+ refnode: Element,
+ has_explicit_title: bool,
+ title: str,
+ target: str,
+ ) -> tuple[str, str]:
+ refnode["qapi:module"] = env.ref_context.get("qapi:module")
+
+ # Cross-references that begin with a tilde adjust the title to
+ # only show the reference without a leading module, even if one
+ # was provided. This is a Sphinx-standard syntax; give it
+ # priority over QAPI-specific type markup below.
+ hide_module = False
+ if target.startswith("~"):
+ hide_module = True
+ target = target[1:]
+
+ # Type names that end with "?" are considered optional
+ # arguments and should be documented as such, but it's not
+ # part of the xref itself.
+ if target.endswith("?"):
+ refnode["qapi:optional"] = True
+ target = target[:-1]
+
+ # Type names wrapped in brackets denote lists. strip the
+ # brackets and remember to add them back later.
+ if target.startswith("[") and target.endswith("]"):
+ refnode["qapi:array"] = True
+ target = target[1:-1]
+
+ if has_explicit_title:
+ # Don't mess with the title at all if it was explicitly set.
+ # Explicit title syntax for references is e.g.
+ # :qapi:type:`target <explicit title>`
+ # and this explicit title overrides everything else here.
+ return title, target
+
+ title = target
+ if hide_module:
+ title = target.split(".")[-1]
+
+ return title, target
+
+
class QAPIIndex(Index):
"""
Index subclass to provide the QAPI definition index.
@@ -118,7 +167,13 @@ class QAPIDomain(Domain):
object_types: Dict[str, ObjType] = {}
directives = {}
- roles = {}
+
+ # These are all cross-reference roles; e.g.
+ # :qapi:cmd:`query-block`. The keys correlate to the names used in
+ # the object_types table values above.
+ roles = {
+ "any": QAPIXRefRole(), # reference *any* type of QAPI object.
+ }
# Moved into the data property at runtime;
# this is the internal index of reference-able objects.
@@ -252,6 +307,37 @@ def find_obj(
matches = [m for m in matches if not m[1].aliased]
return matches
+ def resolve_xref(
+ self,
+ env: BuildEnvironment,
+ fromdocname: str,
+ builder: Builder,
+ typ: str,
+ target: str,
+ node: pending_xref,
+ contnode: Element,
+ ) -> nodes.reference | None:
+ modname = node.get("qapi:module")
+ matches = self.find_obj(modname, target, typ)
+
+ if not matches:
+ return None
+
+ if len(matches) > 1:
+ logger.warning(
+ __("more than one target found for cross-reference %r: %s"),
+ target,
+ ", ".join(match[0] for match in matches),
+ type="ref",
+ subtype="qapi",
+ location=node,
+ )
+
+ name, obj = matches[0]
+ return make_refnode(
+ builder, fromdocname, obj.docname, obj.node_id, contnode, name
+ )
+
def resolve_any_xref(
self,
env: BuildEnvironment,
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 09/63] docs/qapi-domain: add compatibility node classes
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (7 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 08/63] docs/qapi-domain: add QAPI xref roles John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 10/63] docs/qapi-domain: Add QAPIDescription abstract class John Snow
` (54 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Sphinx prior to v4.0 uses different classes for rendering elements of
documentation objects; add some compatibility classes to use the right
node classes conditionally.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/compat.py | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
diff --git a/docs/sphinx/compat.py b/docs/sphinx/compat.py
index 39b859a25e3..6bc698c5ada 100644
--- a/docs/sphinx/compat.py
+++ b/docs/sphinx/compat.py
@@ -2,12 +2,27 @@
Sphinx cross-version compatibility goop
"""
-from docutils.nodes import Element
+from typing import Callable
+from docutils.nodes import Element, Node, Text
+
+import sphinx
+from sphinx import addnodes
from sphinx.util import nodes
from sphinx.util.docutils import SphinxDirective, switch_source_input
+SpaceNode: Callable[[str], Node]
+KeywordNode: Callable[[str, str], Node]
+
+if sphinx.version_info[:3] >= (4, 0, 0):
+ SpaceNode = addnodes.desc_sig_space
+ KeywordNode = addnodes.desc_sig_keyword
+else:
+ SpaceNode = Text
+ KeywordNode = addnodes.desc_annotation
+
+
def nested_parse_with_titles(
directive: SphinxDirective, content_node: Element
) -> None:
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 10/63] docs/qapi-domain: Add QAPIDescription abstract class
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (8 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 09/63] docs/qapi-domain: add compatibility node classes John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 11/63] docs/qapi-domain: add qapi:module directive John Snow
` (53 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This class is a generic, top-level directive for documenting some kind
of QAPI thingamajig that we expect to go into the Index. This class
doesn't do much by itself, and it isn't yet associated with any
particular directive.
handle_signature(), _object_hierarchy_parts() and _toc_entry_name() are
defined in the base class. get_index_text() and add_target_and_index()
are new methods defined here; they are based heavily on the layout and
format of the Python domain's general object class.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 101 ++++++++++++++++++++++++++++++++++++-
1 file changed, 99 insertions(+), 2 deletions(-)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index 49d42c0921c..0ee36b46448 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -14,11 +14,13 @@
NamedTuple,
Optional,
Tuple,
+ cast,
)
from docutils import nodes
-from sphinx.addnodes import pending_xref
+from sphinx.addnodes import desc_signature, pending_xref
+from sphinx.directives import ObjectDescription
from sphinx.domains import (
Domain,
Index,
@@ -28,7 +30,7 @@
from sphinx.locale import _, __
from sphinx.roles import XRefRole
from sphinx.util import logging
-from sphinx.util.nodes import make_refnode
+from sphinx.util.nodes import make_id, make_refnode
if TYPE_CHECKING:
@@ -96,6 +98,101 @@ def process_link(
return title, target
+Signature = str
+
+
+class QAPIDescription(ObjectDescription[Signature]):
+ """
+ Generic QAPI description.
+
+ This is meant to be an abstract class, not instantiated
+ directly. This class handles the abstract details of indexing, the
+ TOC, and reference targets for QAPI descriptions.
+ """
+
+ def handle_signature(self, sig: str, signode: desc_signature) -> Signature:
+ # Do nothing. The return value here is the "name" of the entity
+ # being documented; for QAPI, this is the same as the
+ # "signature", which is just a name.
+
+ # Normally this method must also populate signode with nodes to
+ # render the signature; here we do nothing instead - the
+ # subclasses will handle this.
+ return sig
+
+ def get_index_text(self, name: Signature) -> Tuple[str, str]:
+ """Return the text for the index entry of the object."""
+
+ # NB: this is used for the global index, not the QAPI index.
+ return ("single", f"{name} (QMP {self.objtype})")
+
+ def add_target_and_index(
+ self, name: Signature, sig: str, signode: desc_signature
+ ) -> None:
+ # name is the return value of handle_signature.
+ # sig is the original, raw text argument to handle_signature.
+ # For QAPI, these are identical, currently.
+
+ assert self.objtype
+
+ # If we're documenting a module, don't include the module as
+ # part of the FQN.
+ modname = ""
+ if self.objtype != "module":
+ modname = self.options.get(
+ "module", self.env.ref_context.get("qapi:module")
+ )
+ fullname = (modname + "." if modname else "") + name
+
+ node_id = make_id(
+ self.env, self.state.document, self.objtype, fullname
+ )
+ signode["ids"].append(node_id)
+
+ self.state.document.note_explicit_target(signode)
+ domain = cast(QAPIDomain, self.env.get_domain("qapi"))
+ domain.note_object(fullname, self.objtype, node_id, location=signode)
+
+ if "no-index-entry" not in self.options:
+ arity, indextext = self.get_index_text(name)
+ assert self.indexnode is not None
+ if indextext:
+ self.indexnode["entries"].append(
+ (arity, indextext, node_id, "", None)
+ )
+
+ def _object_hierarchy_parts(
+ self, sig_node: desc_signature
+ ) -> Tuple[str, ...]:
+ if "fullname" not in sig_node:
+ return ()
+ modname = sig_node.get("module")
+ fullname = sig_node["fullname"]
+
+ if modname:
+ return (modname, *fullname.split("."))
+
+ return tuple(fullname.split("."))
+
+ def _toc_entry_name(self, sig_node: desc_signature) -> str:
+ # This controls the name in the TOC and on the sidebar.
+
+ # This is the return type of _object_hierarchy_parts().
+ toc_parts = cast(Tuple[str, ...], sig_node.get("_toc_parts", ()))
+ if not toc_parts:
+ return ""
+
+ config = self.env.app.config
+ *parents, name = toc_parts
+ if config.toc_object_entries_show_parents == "domain":
+ return sig_node.get("fullname", name)
+ if config.toc_object_entries_show_parents == "hide":
+ return name
+ if config.toc_object_entries_show_parents == "all":
+ return ".".join(parents + [name])
+ return ""
+
+
class QAPIIndex(Index):
"""
Index subclass to provide the QAPI definition index.
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 11/63] docs/qapi-domain: add qapi:module directive
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (9 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 10/63] docs/qapi-domain: Add QAPIDescription abstract class John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 12/63] docs/qapi-domain: add QAPIObject class John Snow
` (52 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This adds the qapi:module directive, which just notes the current module
being documented and performs a nested parse of the content block, if
present.
This code is based pretty heavily on Sphinx's PyModule directive, but
with unnecessary features excised.
For example:
.. qapi:module:: block-core
Hello, and welcome to block-core!
=================================
lorem ipsum, dolor sit amet ...
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 71 ++++++++++++++++++++++++++++++++++----
1 file changed, 65 insertions(+), 6 deletions(-)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index 0ee36b46448..e623d1f8678 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -19,6 +19,7 @@
from docutils import nodes
+from sphinx import addnodes
from sphinx.addnodes import desc_signature, pending_xref
from sphinx.directives import ObjectDescription
from sphinx.domains import (
@@ -34,7 +35,7 @@
if TYPE_CHECKING:
- from docutils.nodes import Element
+ from docutils.nodes import Element, Node
from sphinx.application import Sphinx
from sphinx.builders import Builder
@@ -193,6 +194,60 @@ def _toc_entry_name(self, sig_node: desc_signature) -> str:
return ""
+class QAPIModule(QAPIDescription):
+ """
+ Directive to mark description of a new module.
+
+ This directive doesn't generate any special formatting, and is just
+ a pass-through for the content body. Named section titles are
+ allowed in the content body.
+
+ Use this directive to create entries for the QAPI module in the
+ global index and the QAPI index; as well as to associate subsequent
+ definitions with the module they are defined in for purposes of
+ search and QAPI index organization.
+
+ :arg: The name of the module.
+ :opt no-index: Don't add cross-reference targets or index entries.
+ :opt no-typesetting: Don't render the content body (but preserve any
+ cross-reference target IDs in the squelched output.)
+
+ Example::
+
+ .. qapi:module:: block-core
+ :no-index:
+ :no-typesetting:
+
+ Lorem ipsum, dolor sit amet ...
+ """
+
+ def run(self) -> List[Node]:
+ modname = self.arguments[0].strip()
+ self.env.ref_context["qapi:module"] = modname
+ ret = super().run()
+
+ # ObjectDescription always creates a visible signature bar. We
+ # want module items to be "invisible", however.
+
+ # Extract the content body of the directive:
+ assert isinstance(ret[-1], addnodes.desc)
+ desc_node = ret.pop(-1)
+ assert isinstance(desc_node.children[1], addnodes.desc_content)
+ ret.extend(desc_node.children[1].children)
+
+ # Re-home node_ids so anchor refs still work:
+ node_ids: List[str]
+ if node_ids := [
+ node_id
+ for el in desc_node.children[0].traverse(nodes.Element)
+ for node_id in cast(List[str], el.get("ids", ()))
+ ]:
+ target_node = nodes.target(ids=node_ids)
+ ret.insert(1, target_node)
+
+ return ret
+
+
class QAPIIndex(Index):
"""
Index subclass to provide the QAPI definition index.
@@ -258,17 +313,21 @@ class QAPIDomain(Domain):
# This table associates cross-reference object types (key) with an
# ObjType instance, which defines the valid cross-reference roles
# for each object type.
+ object_types: Dict[str, ObjType] = {
+ "module": ObjType(_("module"), "mod", "any"),
+ }
- # Actual table entries for module, command, event, etc will come in
- # forthcoming commits.
- object_types: Dict[str, ObjType] = {}
-
- directives = {}
+ # Each of these provides a rST directive,
+ # e.g. .. qapi:module:: block-core
+ directives = {
+ "module": QAPIModule,
+ }
# These are all cross-reference roles; e.g.
# :qapi:cmd:`query-block`. The keys correlate to the names used in
# the object_types table values above.
roles = {
+ "mod": QAPIXRefRole(),
"any": QAPIXRefRole(), # reference *any* type of QAPI object.
}
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 12/63] docs/qapi-domain: add QAPIObject class
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (10 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 11/63] docs/qapi-domain: add qapi:module directive John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 13/63] docs/qapi-domain: add qapi:command directive John Snow
` (51 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This patch adds another abstract class that describes "a QAPI
thingie". The main difference here is that this class will be generating
visible documentation, unlike the QAPIDescription class.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 64 ++++++++++++++++++++++++++++++++++++++
1 file changed, 64 insertions(+)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index e623d1f8678..3109c0cb90a 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -18,7 +18,9 @@
)
from docutils import nodes
+from docutils.parsers.rst import directives
+from compat import KeywordNode, SpaceNode
from sphinx import addnodes
from sphinx.addnodes import desc_signature, pending_xref
from sphinx.directives import ObjectDescription
@@ -40,6 +42,7 @@
from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.environment import BuildEnvironment
+ from sphinx.util.typing import OptionSpec
logger = logging.getLogger(__name__)
@@ -99,6 +102,8 @@ def process_link(
return title, target
+# Alias for the return of handle_signature(), which is used in several places.
+# (In the Python domain, this is Tuple[str, str] instead.)
Signature = str
@@ -194,6 +199,65 @@ def _toc_entry_name(self, sig_node: desc_signature) -> str:
return ""
+class QAPIObject(QAPIDescription):
+ """
+ Description of a generic QAPI object.
+
+ It's not used directly, but is instead subclassed by specific directives.
+ """
+
+ # Inherit some standard options from Sphinx's ObjectDescription
+ option_spec: OptionSpec = ( # type:ignore[misc]
+ ObjectDescription.option_spec.copy()
+ )
+ option_spec.update(
+ {
+ # Borrowed from the Python domain:
+ "module": directives.unchanged, # Override contextual module name
+ }
+ )
+
+ def get_signature_prefix(self) -> List[nodes.Node]:
+ """Return a prefix to put before the object name in the signature."""
+ assert self.objtype
+ return [
+ KeywordNode("", self.objtype.title()),
+ SpaceNode(" "),
+ ]
+
+ def get_signature_suffix(self) -> List[nodes.Node]:
+ """Return a suffix to put after the object name in the signature."""
+ return []
+
+ def handle_signature(self, sig: str, signode: desc_signature) -> Signature:
+ """
+ Transform a QAPI definition name into RST nodes.
+
+ This method was originally intended for handling function
+ signatures. In the QAPI domain, however, we only pass the
+ definition name as the directive argument and handle everything
+ else in the content body with field lists.
+
+ As such, the only argument here is "sig", which is just the QAPI
+ definition name.
+ """
+ modname = self.options.get(
+ "module", self.env.ref_context.get("qapi:module")
+ )
+
+ signode["fullname"] = sig
+ signode["module"] = modname
+ sig_prefix = self.get_signature_prefix()
+ if sig_prefix:
+ signode += addnodes.desc_annotation(
+ str(sig_prefix), "", *sig_prefix
+ )
+ signode += addnodes.desc_name(sig, sig)
+ signode += self.get_signature_suffix()
+
+ return sig
+
+
class QAPIModule(QAPIDescription):
"""
Directive to mark description of a new module.
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 13/63] docs/qapi-domain: add qapi:command directive
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (11 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 12/63] docs/qapi-domain: add QAPIObject class John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 14/63] docs/qapi-domain: add :since: directive option John Snow
` (50 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This commit adds a stubbed version of QAPICommand that utilizes the
QAPIObject class, the qapi:command directive, the :qapi:cmd:
cross-reference role, and the "command" object type in the QAPI object
registry.
They don't do anything *particularly* interesting yet, but that will
come in forthcoming commits.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index 3109c0cb90a..547040f75a7 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -258,6 +258,12 @@ def handle_signature(self, sig: str, signode: desc_signature) -> Signature:
return sig
+class QAPICommand(QAPIObject):
+ """Description of a QAPI Command."""
+
+ # Nothing unique for now! Changed in later commits O:-)
+
+
class QAPIModule(QAPIDescription):
"""
Directive to mark description of a new module.
@@ -379,12 +385,14 @@ class QAPIDomain(Domain):
# for each object type.
object_types: Dict[str, ObjType] = {
"module": ObjType(_("module"), "mod", "any"),
+ "command": ObjType(_("command"), "cmd", "any"),
}
# Each of these provides a rST directive,
# e.g. .. qapi:module:: block-core
directives = {
"module": QAPIModule,
+ "command": QAPICommand,
}
# These are all cross-reference roles; e.g.
@@ -392,6 +400,7 @@ class QAPIDomain(Domain):
# the object_types table values above.
roles = {
"mod": QAPIXRefRole(),
+ "cmd": QAPIXRefRole(),
"any": QAPIXRefRole(), # reference *any* type of QAPI object.
}
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 14/63] docs/qapi-domain: add :since: directive option
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (12 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 13/63] docs/qapi-domain: add qapi:command directive John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 15/63] docs/qapi-domain: add "Arguments:" field lists John Snow
` (49 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Add a little special markup for registering "Since:" information. Adding
it as an option instead of generic content lets us hoist the information
into the Signature bar, optionally put it in the index, etc.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index 547040f75a7..222b420d2a7 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -214,6 +214,8 @@ class QAPIObject(QAPIDescription):
{
# Borrowed from the Python domain:
"module": directives.unchanged, # Override contextual module name
+ # These are QAPI originals:
+ "since": directives.unchanged,
}
)
@@ -227,7 +229,17 @@ def get_signature_prefix(self) -> List[nodes.Node]:
def get_signature_suffix(self) -> List[nodes.Node]:
"""Return a suffix to put after the object name in the signature."""
- return []
+ ret: List[nodes.Node] = []
+
+ if "since" in self.options:
+ ret += [
+ SpaceNode(" "),
+ addnodes.desc_sig_element(
+ "", f"(Since: {self.options['since']})"
+ ),
+ ]
+
+ return ret
def handle_signature(self, sig: str, signode: desc_signature) -> Signature:
"""
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 15/63] docs/qapi-domain: add "Arguments:" field lists
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (13 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 14/63] docs/qapi-domain: add :since: directive option John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 16/63] docs/qapi-domain: add "Features:" " John Snow
` (48 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This adds special rendering for Sphinx's typed info field lists.
This patch does not add any QAPI-aware markup, rendering, or
cross-referencing for the type names, yet. That feature requires a
subclass to TypedField which will happen in its own commit quite a bit
later in this series; after all the basic fields and objects have been
established first.
The syntax for this field is:
:arg type name: description
description cont'd
You can omit the type or the description, You should not omit the name;
if you do so, it degenerates into a "normal field list" entry, and
probably isn't what you want.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index 222b420d2a7..b4289db6d81 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -33,6 +33,7 @@
from sphinx.locale import _, __
from sphinx.roles import XRefRole
from sphinx.util import logging
+from sphinx.util.docfields import TypedField
from sphinx.util.nodes import make_id, make_refnode
@@ -273,7 +274,18 @@ def handle_signature(self, sig: str, signode: desc_signature) -> Signature:
class QAPICommand(QAPIObject):
"""Description of a QAPI Command."""
- # Nothing unique for now! Changed in later commits O:-)
+ doc_field_types = QAPIObject.doc_field_types.copy()
+ doc_field_types.extend(
+ [
+ # :arg TypeName ArgName: descr
+ TypedField(
+ "argument",
+ label=_("Arguments"),
+ names=("arg",),
+ can_collapse=False,
+ ),
+ ]
+ )
class QAPIModule(QAPIDescription):
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 16/63] docs/qapi-domain: add "Features:" field lists
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (14 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 15/63] docs/qapi-domain: add "Arguments:" field lists John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 17/63] docs/qapi-domain: add "Errors:" " John Snow
` (47 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Add support for Features field lists. There is no QAPI-specific
functionality here, but this could be changed if desired (if we wanted
the feature names to link somewhere, for instance.)
This feature list doesn't have any restrictions, so it can be used to
document object-wide features or per-member features as deemed
appropriate. It's essentially free-form text.
The syntax for this field is:
:feat name: description
description cont'd
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index b4289db6d81..8ec4482b291 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -33,7 +33,7 @@
from sphinx.locale import _, __
from sphinx.roles import XRefRole
from sphinx.util import logging
-from sphinx.util.docfields import TypedField
+from sphinx.util.docfields import GroupedField, TypedField
from sphinx.util.nodes import make_id, make_refnode
@@ -220,6 +220,16 @@ class QAPIObject(QAPIDescription):
}
)
+ doc_field_types = [
+ # :feat name: descr
+ GroupedField(
+ "feature",
+ label=_("Features"),
+ names=("feat",),
+ can_collapse=False,
+ ),
+ ]
+
def get_signature_prefix(self) -> List[nodes.Node]:
"""Return a prefix to put before the object name in the signature."""
assert self.objtype
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 17/63] docs/qapi-domain: add "Errors:" field lists
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (15 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 16/63] docs/qapi-domain: add "Features:" " John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 18/63] docs/qapi-domain: add "Return:" " John Snow
` (46 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
``:error: descr`` can now be used to document error conditions. The
format of the description is not defined here; so the ability to name
specific types is left to the document writer.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index 8ec4482b291..75350090783 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -33,7 +33,7 @@
from sphinx.locale import _, __
from sphinx.roles import XRefRole
from sphinx.util import logging
-from sphinx.util.docfields import GroupedField, TypedField
+from sphinx.util.docfields import Field, GroupedField, TypedField
from sphinx.util.nodes import make_id, make_refnode
@@ -294,6 +294,13 @@ class QAPICommand(QAPIObject):
names=("arg",),
can_collapse=False,
),
+ # :error: descr
+ Field(
+ "error",
+ label=_("Errors"),
+ names=("error", "errors"),
+ has_arg=False,
+ ),
]
)
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 18/63] docs/qapi-domain: add "Return:" field lists
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (16 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 17/63] docs/qapi-domain: add "Errors:" " John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 19/63] docs/qapi-domain: add qapi:enum directive John Snow
` (45 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Add "Return:" field list syntax to QAPI Commands.
Like "Arguments:" and "Errors:", the type name isn't currently processed
for cross-referencing, but this will be addressed in a forthcoming
commit.
The syntax of the new field is:
:return TypeName: description
description cont'd
This patch adds "Return" as a GroupedField, which means that multiple
return values can be annotated - this is only done because Sphinx does
not support mandatory type arguments to Ungrouped fields. Because we
want to cross-reference this type information later, we want to make the
type argument mandatory. As a result, you can technically add multiple
:return: fields, though I'm not aware of any circumstance in which you'd
need or want to. Recommendation: "Don't do that, then." The forthcoming
QAPIDoc transmogrifier does not, in fact, ever "do that".
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index 75350090783..45e69689d1e 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -301,6 +301,13 @@ class QAPICommand(QAPIObject):
names=("error", "errors"),
has_arg=False,
),
+ # :return TypeName: descr
+ GroupedField(
+ "returnvalue",
+ label=_("Return"),
+ names=("return",),
+ can_collapse=True,
+ ),
]
)
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 19/63] docs/qapi-domain: add qapi:enum directive
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (17 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 18/63] docs/qapi-domain: add "Return:" " John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 20/63] docs/qapi-domain: add qapi:alternate directive John Snow
` (44 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Add the .. qapi:enum:: directive, object, and :qapi:enum:`name`
cross-reference role.
Add the :value name: field list for documenting Enum values.
Of note, also introduce a new "type" role that is intended to be used by
other QAPI object directives to cross-reference arbitrary QAPI type
names, but will exclude commands, events, and modules from
consideration.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index 45e69689d1e..e399474dc5b 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -312,6 +312,23 @@ class QAPICommand(QAPIObject):
)
+class QAPIEnum(QAPIObject):
+ """Description of a QAPI Enum."""
+
+ doc_field_types = QAPIObject.doc_field_types.copy()
+ doc_field_types.extend(
+ [
+ # :value name: descr
+ GroupedField(
+ "value",
+ label=_("Values"),
+ names=("value",),
+ can_collapse=False,
+ )
+ ]
+ )
+
+
class QAPIModule(QAPIDescription):
"""
Directive to mark description of a new module.
@@ -431,9 +448,14 @@ class QAPIDomain(Domain):
# This table associates cross-reference object types (key) with an
# ObjType instance, which defines the valid cross-reference roles
# for each object type.
+ #
+ # e.g., the :qapi:type: cross-reference role can refer to enum,
+ # struct, union, or alternate objects; but :qapi:obj: can refer to
+ # anything. Each object also gets its own targeted cross-reference role.
object_types: Dict[str, ObjType] = {
"module": ObjType(_("module"), "mod", "any"),
"command": ObjType(_("command"), "cmd", "any"),
+ "enum": ObjType(_("enum"), "enum", "type", "any"),
}
# Each of these provides a rST directive,
@@ -441,6 +463,7 @@ class QAPIDomain(Domain):
directives = {
"module": QAPIModule,
"command": QAPICommand,
+ "enum": QAPIEnum,
}
# These are all cross-reference roles; e.g.
@@ -449,6 +472,9 @@ class QAPIDomain(Domain):
roles = {
"mod": QAPIXRefRole(),
"cmd": QAPIXRefRole(),
+ "enum": QAPIXRefRole(),
+ # reference any data type (excludes modules, commands, events)
+ "type": QAPIXRefRole(),
"any": QAPIXRefRole(), # reference *any* type of QAPI object.
}
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 20/63] docs/qapi-domain: add qapi:alternate directive
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (18 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 19/63] docs/qapi-domain: add qapi:enum directive John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 21/63] docs/qapi-domain: add qapi:event directive John Snow
` (43 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Add the .. qapi:alternate:: directive, object, and qapi:alt:`name`
cross-reference role.
Add the "Alternatives:" field list for describing alternate choices. Like
other field lists that reference QAPI types, a forthcoming commit will
add cross-referencing support to this field.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index e399474dc5b..506ed92700d 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -329,6 +329,23 @@ class QAPIEnum(QAPIObject):
)
+class QAPIAlternate(QAPIObject):
+ """Description of a QAPI Alternate."""
+
+ doc_field_types = QAPIObject.doc_field_types.copy()
+ doc_field_types.extend(
+ [
+ # :alt type name: descr
+ TypedField(
+ "alternative",
+ label=_("Alternatives"),
+ names=("alt",),
+ can_collapse=False,
+ ),
+ ]
+ )
+
+
class QAPIModule(QAPIDescription):
"""
Directive to mark description of a new module.
@@ -456,6 +473,7 @@ class QAPIDomain(Domain):
"module": ObjType(_("module"), "mod", "any"),
"command": ObjType(_("command"), "cmd", "any"),
"enum": ObjType(_("enum"), "enum", "type", "any"),
+ "alternate": ObjType(_("alternate"), "alt", "type", "any"),
}
# Each of these provides a rST directive,
@@ -464,6 +482,7 @@ class QAPIDomain(Domain):
"module": QAPIModule,
"command": QAPICommand,
"enum": QAPIEnum,
+ "alternate": QAPIAlternate,
}
# These are all cross-reference roles; e.g.
@@ -473,6 +492,7 @@ class QAPIDomain(Domain):
"mod": QAPIXRefRole(),
"cmd": QAPIXRefRole(),
"enum": QAPIXRefRole(),
+ "alt": QAPIXRefRole(),
# reference any data type (excludes modules, commands, events)
"type": QAPIXRefRole(),
"any": QAPIXRefRole(), # reference *any* type of QAPI object.
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 21/63] docs/qapi-domain: add qapi:event directive
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (19 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 20/63] docs/qapi-domain: add qapi:alternate directive John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 22/63] docs/qapi-domain: add qapi:object directive John Snow
` (42 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Adds the .. qapi:event:: directive, object, and :qapi:event:`name`
cross-referencing role.
Adds the :memb type name: field list syntax for documenting event data
members. As this syntax and phrasing will be shared with Structs and
Unions as well, add the field list definition to a shared abstract
class.
As per usual, QAPI cross-referencing for types in the member field list
will be added in a forthcoming commit.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index 506ed92700d..3ffb3eb72d1 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -346,6 +346,27 @@ class QAPIAlternate(QAPIObject):
)
+class QAPIObjectWithMembers(QAPIObject):
+ """Base class for Events/Structs/Unions"""
+
+ doc_field_types = QAPIObject.doc_field_types.copy()
+ doc_field_types.extend(
+ [
+ # :member type name: descr
+ TypedField(
+ "member",
+ label=_("Members"),
+ names=("memb",),
+ can_collapse=False,
+ ),
+ ]
+ )
+
+
+class QAPIEvent(QAPIObjectWithMembers):
+ """Description of a QAPI Event."""
+
+
class QAPIModule(QAPIDescription):
"""
Directive to mark description of a new module.
@@ -472,6 +493,7 @@ class QAPIDomain(Domain):
object_types: Dict[str, ObjType] = {
"module": ObjType(_("module"), "mod", "any"),
"command": ObjType(_("command"), "cmd", "any"),
+ "event": ObjType(_("event"), "event", "any"),
"enum": ObjType(_("enum"), "enum", "type", "any"),
"alternate": ObjType(_("alternate"), "alt", "type", "any"),
}
@@ -481,6 +503,7 @@ class QAPIDomain(Domain):
directives = {
"module": QAPIModule,
"command": QAPICommand,
+ "event": QAPIEvent,
"enum": QAPIEnum,
"alternate": QAPIAlternate,
}
@@ -491,6 +514,7 @@ class QAPIDomain(Domain):
roles = {
"mod": QAPIXRefRole(),
"cmd": QAPIXRefRole(),
+ "event": QAPIXRefRole(),
"enum": QAPIXRefRole(),
"alt": QAPIXRefRole(),
# reference any data type (excludes modules, commands, events)
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 22/63] docs/qapi-domain: add qapi:object directive
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (20 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 21/63] docs/qapi-domain: add qapi:event directive John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 23/63] docs/qapi-domain: add :deprecated: directive option John Snow
` (41 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Adds the .. qapi:object:: directive, object, and :qapi:obj:`name`
cross-referencing role. This directive is meant to document both structs
and unions.
As per usual, QAPI cross-referencing for types in the member field list
will be added in a forthcoming commit.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index 3ffb3eb72d1..b11300bc85d 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -367,6 +367,10 @@ class QAPIEvent(QAPIObjectWithMembers):
"""Description of a QAPI Event."""
+class QAPIJSONObject(QAPIObjectWithMembers):
+ """Description of a QAPI Object: structs and unions."""
+
+
class QAPIModule(QAPIDescription):
"""
Directive to mark description of a new module.
@@ -495,6 +499,7 @@ class QAPIDomain(Domain):
"command": ObjType(_("command"), "cmd", "any"),
"event": ObjType(_("event"), "event", "any"),
"enum": ObjType(_("enum"), "enum", "type", "any"),
+ "object": ObjType(_("object"), "obj", "type", "any"),
"alternate": ObjType(_("alternate"), "alt", "type", "any"),
}
@@ -505,6 +510,7 @@ class QAPIDomain(Domain):
"command": QAPICommand,
"event": QAPIEvent,
"enum": QAPIEnum,
+ "object": QAPIJSONObject,
"alternate": QAPIAlternate,
}
@@ -516,6 +522,7 @@ class QAPIDomain(Domain):
"cmd": QAPIXRefRole(),
"event": QAPIXRefRole(),
"enum": QAPIXRefRole(),
+ "obj": QAPIXRefRole(), # specifically structs and unions.
"alt": QAPIXRefRole(),
# reference any data type (excludes modules, commands, events)
"type": QAPIXRefRole(),
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 23/63] docs/qapi-domain: add :deprecated: directive option
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (21 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 22/63] docs/qapi-domain: add qapi:object directive John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 24/63] docs/qapi-domain: add :unstable: " John Snow
` (40 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster, Harmonie Snow
Although "deprecated" is a feature (and *will* appear in the features
list), add a special :deprecated: option to generate an eye-catch that
makes this information very hard to miss.
The forthcoming Transmogrifier in qapidoc.py will add this option
whenever it detects that the features list attached to a definition
contains the "deprecated" entry.
P.S., I outsourced the CSS ;)
Signed-off-by: Harmonie Snow <harmonie@gmail.com>
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx-static/theme_overrides.css | 25 +++++++++++++++++++++++++
docs/sphinx/qapi_domain.py | 26 ++++++++++++++++++++++++++
2 files changed, 51 insertions(+)
diff --git a/docs/sphinx-static/theme_overrides.css b/docs/sphinx-static/theme_overrides.css
index 965ecac54fd..3765cab1b20 100644
--- a/docs/sphinx-static/theme_overrides.css
+++ b/docs/sphinx-static/theme_overrides.css
@@ -208,3 +208,28 @@ div[class^="highlight"] pre {
color: inherit;
}
}
+
+/* QAPI domain theming */
+
+.qapi-infopips {
+ margin-bottom: 1em;
+}
+
+.qapi-infopip {
+ display: inline-block;
+ padding: 0em 0.5em 0em 0.5em;
+ margin: 0.25em;
+}
+
+.qapi-deprecated {
+ background-color: #fffef5;
+ border: solid #fff176 6px;
+ font-weight: bold;
+ padding: 8px;
+ border-radius: 15px;
+ margin: 5px;
+}
+
+.qapi-deprecated::before {
+ content: '⚠️ ';
+}
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index b11300bc85d..b672ae6c504 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -217,6 +217,7 @@ class QAPIObject(QAPIDescription):
"module": directives.unchanged, # Override contextual module name
# These are QAPI originals:
"since": directives.unchanged,
+ "deprecated": directives.flag,
}
)
@@ -280,6 +281,31 @@ def handle_signature(self, sig: str, signode: desc_signature) -> Signature:
return sig
+ def _add_infopips(self, contentnode: addnodes.desc_content) -> None:
+ # Add various eye-catches and things that go below the signature
+ # bar, but precede the user-defined content.
+ infopips = nodes.container()
+ infopips.attributes["classes"].append("qapi-infopips")
+
+ def _add_pip(source: str, content: str, classname: str) -> None:
+ node = nodes.container(source)
+ node.append(nodes.Text(content))
+ node.attributes["classes"].extend(["qapi-infopip", classname])
+ infopips.append(node)
+
+ if "deprecated" in self.options:
+ _add_pip(
+ ":deprecated:",
+ f"This {self.objtype} is deprecated.",
+ "qapi-deprecated",
+ )
+
+ if infopips.children:
+ contentnode.insert(0, infopips)
+
+ def transform_content(self, content_node: addnodes.desc_content) -> None:
+ self._add_infopips(content_node)
+
class QAPICommand(QAPIObject):
"""Description of a QAPI Command."""
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 24/63] docs/qapi-domain: add :unstable: directive option
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (22 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 23/63] docs/qapi-domain: add :deprecated: directive option John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 25/63] docs/qapi-domain: add :ifcond: " John Snow
` (39 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster, Harmonie Snow
Although "unstable" is a feature (and *will* appear in the features
list), add a special :unstable: option to generate an eye-catch that
makes this information very hard to miss.
The forthcoming Transmogrifier in qapidoc.py will add this option
whenever it detects that the features list attached to a definition
contains the "unstable" entry.
Signed-off-by: Harmonie Snow <harmonie@gmail.com>
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx-static/theme_overrides.css | 6 +++++-
docs/sphinx/qapi_domain.py | 8 ++++++++
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/docs/sphinx-static/theme_overrides.css b/docs/sphinx-static/theme_overrides.css
index 3765cab1b20..5f58f1d5246 100644
--- a/docs/sphinx-static/theme_overrides.css
+++ b/docs/sphinx-static/theme_overrides.css
@@ -221,7 +221,7 @@ div[class^="highlight"] pre {
margin: 0.25em;
}
-.qapi-deprecated {
+.qapi-deprecated,.qapi-unstable {
background-color: #fffef5;
border: solid #fff176 6px;
font-weight: bold;
@@ -230,6 +230,10 @@ div[class^="highlight"] pre {
margin: 5px;
}
+.qapi-unstable::before {
+ content: '🚧 ';
+}
+
.qapi-deprecated::before {
content: '⚠️ ';
}
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index b672ae6c504..00fd11ebf79 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -218,6 +218,7 @@ class QAPIObject(QAPIDescription):
# These are QAPI originals:
"since": directives.unchanged,
"deprecated": directives.flag,
+ "unstable": directives.flag,
}
)
@@ -300,6 +301,13 @@ def _add_pip(source: str, content: str, classname: str) -> None:
"qapi-deprecated",
)
+ if "unstable" in self.options:
+ _add_pip(
+ ":unstable:",
+ f"This {self.objtype} is unstable/experimental.",
+ "qapi-unstable",
+ )
+
if infopips.children:
contentnode.insert(0, infopips)
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 25/63] docs/qapi-domain: add :ifcond: directive option
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (23 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 24/63] docs/qapi-domain: add :unstable: " John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 26/63] docs/qapi-domain: add warnings for malformed field lists John Snow
` (38 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster, Harmonie Snow
Add a special :ifcond: option that allows us to annotate the
definition-level conditionals.
The syntax of the argument is currently undefined, but it's possible we
can apply better formatting in the future. Currently, we just display
the ifcond string as preformatted text.
Signed-off-by: Harmonie Snow <harmonie@gmail.com>
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx-static/theme_overrides.css | 13 +++++++++++++
docs/sphinx/qapi_domain.py | 23 +++++++++++++++++++++--
2 files changed, 34 insertions(+), 2 deletions(-)
diff --git a/docs/sphinx-static/theme_overrides.css b/docs/sphinx-static/theme_overrides.css
index 5f58f1d5246..3fd326613d9 100644
--- a/docs/sphinx-static/theme_overrides.css
+++ b/docs/sphinx-static/theme_overrides.css
@@ -237,3 +237,16 @@ div[class^="highlight"] pre {
.qapi-deprecated::before {
content: '⚠️ ';
}
+
+.qapi-ifcond::before {
+ /* gaze ye into the crystal ball to determine feature availability */
+ content: '🔮 ';
+}
+
+.qapi-ifcond {
+ background-color: #f9f5ff;
+ border: solid #dac2ff 6px;
+ padding: 8px;
+ border-radius: 15px;
+ margin: 5px;
+}
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index 00fd11ebf79..4531b5d8574 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -14,6 +14,7 @@
NamedTuple,
Optional,
Tuple,
+ Union,
cast,
)
@@ -217,6 +218,7 @@ class QAPIObject(QAPIDescription):
"module": directives.unchanged, # Override contextual module name
# These are QAPI originals:
"since": directives.unchanged,
+ "ifcond": directives.unchanged,
"deprecated": directives.flag,
"unstable": directives.flag,
}
@@ -288,9 +290,14 @@ def _add_infopips(self, contentnode: addnodes.desc_content) -> None:
infopips = nodes.container()
infopips.attributes["classes"].append("qapi-infopips")
- def _add_pip(source: str, content: str, classname: str) -> None:
+ def _add_pip(
+ source: str, content: Union[str, List[nodes.Node]], classname: str
+ ) -> None:
node = nodes.container(source)
- node.append(nodes.Text(content))
+ if isinstance(content, str):
+ node.append(nodes.Text(content))
+ else:
+ node.extend(content)
node.attributes["classes"].extend(["qapi-infopip", classname])
infopips.append(node)
@@ -308,6 +315,18 @@ def _add_pip(source: str, content: str, classname: str) -> None:
"qapi-unstable",
)
+ if self.options.get("ifcond", ""):
+ ifcond = self.options["ifcond"]
+ _add_pip(
+ f":ifcond: {ifcond}",
+ [
+ nodes.emphasis("", "Availability"),
+ nodes.Text(": "),
+ nodes.literal(ifcond, ifcond),
+ ],
+ "qapi-ifcond",
+ )
+
if infopips.children:
contentnode.insert(0, infopips)
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 26/63] docs/qapi-domain: add warnings for malformed field lists
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (24 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 25/63] docs/qapi-domain: add :ifcond: " John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 27/63] docs/qapi-domain: add type cross-refs to " John Snow
` (37 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Normally, Sphinx will silently fall back to its standard field list
processing if it doesn't match one of your defined fields. A lot of the
time, that's not what we want - we want to be warned if we goof
something up.
For instance, the canonical argument field list form is:
:arg type name: descr
This form is captured by Sphinx and transformed so that the field label
will become "Arguments:". It's possible to omit the type name and descr
and still have it be processed correctly. However, if you omit the type
name, Sphinx no longer recognizes it:
:arg: this is not recognized.
This will turn into an arbitrary field list entry whose label is "Arg:",
and it otherwise silently fails. You may also see failures for doing
things like using :values: instead of :value:, or :errors: instead of
:error:, and so on. It's also case sensitive, and easy to trip up.
Add a validator that guarantees all field list entries that are the
direct child of an ObjectDescription use only recognized forms of field
lists, and emit a warning (treated as error by default in most build
configurations) whenever we detect one that is goofed up.
However, there's still benefit to allowing arbitrary fields -- they are
after all not a Sphinx invention, but perfectly normal docutils
syntax. Create an allow list for known spellings we don't mind letting
through, but warn against anything else.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/conf.py | 9 +++++
docs/sphinx/qapi_domain.py | 74 ++++++++++++++++++++++++++++++++++++++
2 files changed, 83 insertions(+)
diff --git a/docs/conf.py b/docs/conf.py
index 49d9de894c0..a3f9fa63d94 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -153,6 +153,15 @@
with open(os.path.join(qemu_docdir, 'defs.rst.inc')) as f:
rst_epilog += f.read()
+
+# Normally, the QAPI domain is picky about what field lists you use to
+# describe a QAPI entity. If you'd like to use arbitrary additional
+# fields in source documentation, add them here.
+qapi_allowed_fields = {
+ "see also",
+}
+
+
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index 4531b5d8574..9fe006eef3e 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -49,6 +49,19 @@
logger = logging.getLogger(__name__)
+def _unpack_field(
+ field: nodes.Node,
+) -> Tuple[nodes.field_name, nodes.field_body]:
+ """
+ docutils helper: unpack a field node in a type-safe manner.
+ """
+ assert isinstance(field, nodes.field)
+ assert len(field.children) == 2
+ assert isinstance(field.children[0], nodes.field_name)
+ assert isinstance(field.children[1], nodes.field_body)
+ return (field.children[0], field.children[1])
+
+
class ObjectEntry(NamedTuple):
docname: str
node_id: str
@@ -330,9 +343,64 @@ def _add_pip(
if infopips.children:
contentnode.insert(0, infopips)
+ def _validate_field(self, field: nodes.field) -> None:
+ """Validate field lists in this QAPI Object Description."""
+ name, _ = _unpack_field(field)
+ allowed_fields = set(self.env.app.config.qapi_allowed_fields)
+
+ field_label = name.astext()
+ if field_label in allowed_fields:
+ # Explicitly allowed field list name, OK.
+ return
+
+ try:
+ # split into field type and argument (if provided)
+ # e.g. `:arg type name: descr` is
+ # field_type = "arg", field_arg = "type name".
+ field_type, field_arg = field_label.split(None, 1)
+ except ValueError:
+ # No arguments provided
+ field_type = field_label
+ field_arg = ""
+
+ typemap = self.get_field_type_map()
+ if field_type in typemap:
+ # This is a special docfield, yet-to-be-processed. Catch
+ # correct names, but incorrect arguments. This mismatch WILL
+ # cause Sphinx to render this field incorrectly (without a
+ # warning), which is never what we want.
+ typedesc = typemap[field_type][0]
+ if typedesc.has_arg != bool(field_arg):
+ msg = f"docfield field list type {field_type!r} "
+ if typedesc.has_arg:
+ msg += "requires an argument."
+ else:
+ msg += "takes no arguments."
+ logger.warning(msg, location=field)
+ else:
+ # This is unrecognized entirely. It's valid rST to use
+ # arbitrary fields, but let's ensure the documentation
+ # writer has done this intentionally.
+ valid = ", ".join(sorted(set(typemap) | allowed_fields))
+ msg = (
+ f"Unrecognized field list name {field_label!r}.\n"
+ f"Valid fields for qapi:{self.objtype} are: {valid}\n"
+ "\n"
+ "If this usage is intentional, please add it to "
+ "'qapi_allowed_fields' in docs/conf.py."
+ )
+ logger.warning(msg, location=field)
+
def transform_content(self, content_node: addnodes.desc_content) -> None:
self._add_infopips(content_node)
+ # Validate field lists.
+ for child in content_node:
+ if isinstance(child, nodes.field_list):
+ for field in child.children:
+ assert isinstance(field, nodes.field)
+ self._validate_field(field)
+
class QAPICommand(QAPIObject):
"""Description of a QAPI Command."""
@@ -769,6 +837,12 @@ def resolve_any_xref(
def setup(app: Sphinx) -> Dict[str, Any]:
app.setup_extension("sphinx.directives")
+ app.add_config_value(
+ "qapi_allowed_fields",
+ set(),
+ "env", # Setting impacts parsing phase
+ types=set,
+ )
app.add_domain(QAPIDomain)
return {
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 27/63] docs/qapi-domain: add type cross-refs to field lists
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (25 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 26/63] docs/qapi-domain: add warnings for malformed field lists John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 28/63] docs/qapi-domain: add CSS styling John Snow
` (36 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This commit, finally, adds cross-referencing support to various field
lists; modeled tightly after Sphinx's own Python domain code.
Cross-referencing support is added to type names provided to :arg:,
:memb:, :returns: and :choice:.
:feat:, :error: and :value:, which do not take type names, do not
support this syntax.
The general syntax is simple:
:arg TypeName ArgName: Lorem Ipsum ...
The domain will transform TypeName into :qapi:type:`TypeName` in this
basic case, and also apply the ``literal`` decoration to indicate that
this is a type cross-reference.
For optional arguments, the special "?" suffix is used. Because "*" has
special meaning in rST that would cause parsing errors, we elect to use
"?" instead. The special syntax processing strips this character from
the end of any type name argument and will append ", optional" to the
rendered output, applying the cross-reference only to the actual type
name.
The intent here is that the actual syntax in doc-blocks need not change;
but e.g. qapidoc.py will need to process and transform "@arg foo lorem
ipsum" into ":arg type? foo: lorem ipsum" based on the schema
information. Therefore, nobody should ever actually witness this
intermediate syntax unless they are writing manual documentation or the
doc transmogrifier breaks.
For array arguments, type names can similarly be surrounded by "[]",
which are stripped off and then re-appended outside of the
cross-reference.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index 9fe006eef3e..06fe78ce0bc 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -2,6 +2,9 @@
QAPI domain extension.
"""
+# The best laid plans of mice and men, ...
+# pylint: disable=too-many-lines
+
from __future__ import annotations
from typing import (
@@ -116,6 +119,28 @@ def process_link(
return title, target
+ def result_nodes(
+ self,
+ document: nodes.document,
+ env: BuildEnvironment,
+ node: Element,
+ is_ref: bool,
+ ) -> Tuple[List[nodes.Node], List[nodes.system_message]]:
+
+ # node here is the pending_xref node (or whatever nodeclass was
+ # configured at XRefRole class instantiation time).
+ results: List[nodes.Node] = [node]
+
+ if node.get("qapi:array"):
+ results.insert(0, nodes.literal("[", "["))
+ results.append(nodes.literal("]", "]"))
+
+ if node.get("qapi:optional"):
+ results.append(nodes.Text(", "))
+ results.append(nodes.emphasis("?", "optional"))
+
+ return results, []
+
# Alias for the return of handle_signature(), which is used in several places.
# (In the Python domain, this is Tuple[str, str] instead.)
@@ -413,6 +438,7 @@ class QAPICommand(QAPIObject):
"argument",
label=_("Arguments"),
names=("arg",),
+ typerolename="type",
can_collapse=False,
),
# :error: descr
@@ -426,6 +452,7 @@ class QAPICommand(QAPIObject):
GroupedField(
"returnvalue",
label=_("Return"),
+ rolename="type",
names=("return",),
can_collapse=True,
),
@@ -461,6 +488,7 @@ class QAPIAlternate(QAPIObject):
"alternative",
label=_("Alternatives"),
names=("alt",),
+ typerolename="type",
can_collapse=False,
),
]
@@ -478,6 +506,7 @@ class QAPIObjectWithMembers(QAPIObject):
"member",
label=_("Members"),
names=("memb",),
+ typerolename="type",
can_collapse=False,
),
]
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 28/63] docs/qapi-domain: add CSS styling
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (26 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 27/63] docs/qapi-domain: add type cross-refs to " John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 29/63] docs/qapi-domain: add XREF compatibility goop for Sphinx < 4.1 John Snow
` (35 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster, Harmonie Snow
Improve the general look and feel of generated QAPI docs.
Attempt to limit line lengths to offer a more comfortable measure on
maximized windows, and improve some margin and spacing for field lists.
Signed-off-by: Harmonie Snow <harmonie@gmail.com>
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx-static/theme_overrides.css | 56 +++++++++++++++++++++++++-
1 file changed, 54 insertions(+), 2 deletions(-)
diff --git a/docs/sphinx-static/theme_overrides.css b/docs/sphinx-static/theme_overrides.css
index 3fd326613d9..b225bf706f5 100644
--- a/docs/sphinx-static/theme_overrides.css
+++ b/docs/sphinx-static/theme_overrides.css
@@ -18,8 +18,8 @@ h1, h2, .rst-content .toctree-wrapper p.caption, h3, h4, h5, h6, legend {
.rst-content dl:not(.docutils) dt {
border-top: none;
- border-left: solid 3px #ccc;
- background-color: #f0f0f0;
+ border-left: solid 5px #bcc6d2;
+ background-color: #eaedf1;
color: black;
}
@@ -211,6 +211,18 @@ div[class^="highlight"] pre {
/* QAPI domain theming */
+/* most content in a QAPI object definition should not eclipse about
+ 80ch, but nested field lists are explicitly exempt due to their
+ two-column nature */
+.qapi dd *:not(dl) {
+ max-width: 80ch;
+}
+
+/* but the content column itself should still be less than ~80ch. */
+.qapi .field-list dd {
+ max-width: 80ch;
+}
+
.qapi-infopips {
margin-bottom: 1em;
}
@@ -250,3 +262,43 @@ div[class^="highlight"] pre {
border-radius: 15px;
margin: 5px;
}
+
+/* code blocks */
+.qapi div[class^="highlight"] {
+ width: fit-content;
+ background-color: #fffafd;
+ border: 2px solid #ffe1f3;
+}
+
+/* note, warning, etc. */
+.qapi .admonition {
+ width: fit-content;
+}
+
+/* pad the top of the field-list so the text doesn't start directly at
+ the top border; primarily for the field list labels, but adjust the
+ field bodies as well for parity. */
+dl.field-list > dt:first-of-type, dl.field-list > dd:first-of-type {
+ padding-top: 0.3em;
+}
+
+dl.field-list > dt:last-of-type, dl.field-list > dd:last-of-type {
+ padding-bottom: 0.3em;
+}
+
+/* pad the field list labels so they don't crash into the border */
+dl.field-list > dt {
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+}
+
+/* Add a little padding between field list sections */
+dl.field-list > dd:not(:last-child) {
+ padding-bottom: 1em;
+}
+
+/* Sphinx 3.x: unresolved xrefs */
+.rst-content *:not(a) > code.xref {
+ font-weight: 400;
+ color: #333333;
+}
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 29/63] docs/qapi-domain: add XREF compatibility goop for Sphinx < 4.1
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (27 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 28/63] docs/qapi-domain: add CSS styling John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 30/63] docs/qapi-domain: warn when QAPI domain xrefs fail to resolve John Snow
` (34 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Sphinx < 4.1 handles cross-references ... differently. Factor out and
isolate the compatibility goop we need to make cross references work
properly in old versions of Sphinx.
Yes, it's ugly. Yes, it works. No, I don't want to talk about
it.
Understand that this patch exists because of the overflowing love in my
heart.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/compat.py | 136 +++++++++++++++++++++++++++++++++++--
docs/sphinx/qapi_domain.py | 23 ++++---
2 files changed, 144 insertions(+), 15 deletions(-)
diff --git a/docs/sphinx/compat.py b/docs/sphinx/compat.py
index 6bc698c5ada..f068d70388d 100644
--- a/docs/sphinx/compat.py
+++ b/docs/sphinx/compat.py
@@ -2,14 +2,31 @@
Sphinx cross-version compatibility goop
"""
-from typing import Callable
+import re
+from typing import (
+ Any,
+ Callable,
+ Optional,
+ Type,
+)
+from docutils import nodes
from docutils.nodes import Element, Node, Text
import sphinx
-from sphinx import addnodes
-from sphinx.util import nodes
-from sphinx.util.docutils import SphinxDirective, switch_source_input
+from sphinx import addnodes, util
+from sphinx.environment import BuildEnvironment
+from sphinx.roles import XRefRole
+from sphinx.util import docfields
+from sphinx.util.docutils import (
+ ReferenceRole,
+ SphinxDirective,
+ switch_source_input,
+)
+from sphinx.util.typing import TextlikeNode
+
+
+MAKE_XREF_WORKAROUND = sphinx.version_info[:3] < (4, 1, 0)
SpaceNode: Callable[[str], Node]
@@ -36,7 +53,7 @@ def nested_parse_with_titles(
try:
# Modern sphinx (6.2.0+) supports proper offsetting for
# nested parse error context management
- nodes.nested_parse_with_titles(
+ util.nodes.nested_parse_with_titles(
directive.state,
directive.content,
content_node,
@@ -45,6 +62,113 @@ def nested_parse_with_titles(
except TypeError:
# No content_offset argument. Fall back to SSI method.
with switch_source_input(directive.state, directive.content):
- nodes.nested_parse_with_titles(
+ util.nodes.nested_parse_with_titles(
directive.state, directive.content, content_node
)
+
+
+# ###########################################
+# xref compatibility hacks for Sphinx < 4.1 #
+# ###########################################
+
+# When we require >= Sphinx 4.1, the following function and the
+# subsequent 3 compatibility classes can be removed. Anywhere in
+# qapi_domain that uses one of these Compat* types can be switched to
+# using the garden-variety lib-provided classes with no trickery.
+
+
+def _compat_make_xref( # pylint: disable=unused-argument
+ self: sphinx.util.docfields.Field,
+ rolename: str,
+ domain: str,
+ target: str,
+ innernode: Type[TextlikeNode] = addnodes.literal_emphasis,
+ contnode: Optional[Node] = None,
+ env: Optional[BuildEnvironment] = None,
+ inliner: Any = None,
+ location: Any = None,
+) -> Node:
+ """
+ Compatibility workaround for Sphinx versions prior to 4.1.0.
+
+ Older sphinx versions do not use the domain's XRefRole for parsing
+ and formatting cross-references, so we need to perform this magick
+ ourselves to avoid needing to write the parser/formatter in two
+ separate places.
+
+ This workaround isn't brick-for-brick compatible with modern Sphinx
+ versions, because we do not have access to the parent directive's
+ state during this parsing like we do in more modern versions.
+
+ It's no worse than what pre-Sphinx 4.1.0 does, so... oh well!
+ """
+
+ # Yes, this function is gross. Pre-4.1 support is a miracle.
+ # pylint: disable=too-many-locals
+
+ assert env
+ # Note: Sphinx's own code ignores the type warning here, too.
+ if not rolename:
+ return contnode or innernode(target, target) # type: ignore[call-arg]
+
+ # Get the role instance, but don't *execute it* - we lack the
+ # correct state to do so. Instead, we'll just use its public
+ # methods to do our reference formatting, and emulate the rest.
+ role = env.get_domain(domain).roles[rolename]
+ assert isinstance(role, XRefRole)
+
+ # XRefRole features not supported by this compatibility shim;
+ # these were not supported in Sphinx 3.x either, so nothing of
+ # value is really lost.
+ assert not target.startswith("!")
+ assert not re.match(ReferenceRole.explicit_title_re, target)
+ assert not role.lowercase
+ assert not role.fix_parens
+
+ # Code below based mostly on sphinx.roles.XRefRole; run() and
+ # create_xref_node()
+ options = {
+ "refdoc": env.docname,
+ "refdomain": domain,
+ "reftype": rolename,
+ "refexplicit": False,
+ "refwarn": role.warn_dangling,
+ }
+ refnode = role.nodeclass(target, **options)
+ title, target = role.process_link(env, refnode, False, target, target)
+ refnode["reftarget"] = target
+ classes = ["xref", domain, f"{domain}-{rolename}"]
+ refnode += role.innernodeclass(target, title, classes=classes)
+
+ # This is the very gross part of the hack. Normally,
+ # result_nodes takes a document object to which we would pass
+ # self.inliner.document. Prior to Sphinx 4.1, we don't *have* an
+ # inliner to pass, so we have nothing to pass here. However, the
+ # actual implementation of role.result_nodes in this case
+ # doesn't actually use that argument, so this winds up being
+ # ... fine. Rest easy at night knowing this code only runs under
+ # old versions of Sphinx, so at least it won't change in the
+ # future on us and lead to surprising new failures.
+ # Gross, I know.
+ result_nodes, _messages = role.result_nodes(
+ None, # type: ignore
+ env,
+ refnode,
+ is_ref=True,
+ )
+ return nodes.inline(target, "", *result_nodes)
+
+
+class CompatField(docfields.Field):
+ if MAKE_XREF_WORKAROUND:
+ make_xref = _compat_make_xref
+
+
+class CompatGroupedField(docfields.GroupedField):
+ if MAKE_XREF_WORKAROUND:
+ make_xref = _compat_make_xref
+
+
+class CompatTypedField(docfields.TypedField):
+ if MAKE_XREF_WORKAROUND:
+ make_xref = _compat_make_xref
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index 06fe78ce0bc..3b1490e29a1 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -24,7 +24,13 @@
from docutils import nodes
from docutils.parsers.rst import directives
-from compat import KeywordNode, SpaceNode
+from compat import (
+ CompatField,
+ CompatGroupedField,
+ CompatTypedField,
+ KeywordNode,
+ SpaceNode,
+)
from sphinx import addnodes
from sphinx.addnodes import desc_signature, pending_xref
from sphinx.directives import ObjectDescription
@@ -37,7 +43,6 @@
from sphinx.locale import _, __
from sphinx.roles import XRefRole
from sphinx.util import logging
-from sphinx.util.docfields import Field, GroupedField, TypedField
from sphinx.util.nodes import make_id, make_refnode
@@ -264,7 +269,7 @@ class QAPIObject(QAPIDescription):
doc_field_types = [
# :feat name: descr
- GroupedField(
+ CompatGroupedField(
"feature",
label=_("Features"),
names=("feat",),
@@ -434,7 +439,7 @@ class QAPICommand(QAPIObject):
doc_field_types.extend(
[
# :arg TypeName ArgName: descr
- TypedField(
+ CompatTypedField(
"argument",
label=_("Arguments"),
names=("arg",),
@@ -442,14 +447,14 @@ class QAPICommand(QAPIObject):
can_collapse=False,
),
# :error: descr
- Field(
+ CompatField(
"error",
label=_("Errors"),
names=("error", "errors"),
has_arg=False,
),
# :return TypeName: descr
- GroupedField(
+ CompatGroupedField(
"returnvalue",
label=_("Return"),
rolename="type",
@@ -467,7 +472,7 @@ class QAPIEnum(QAPIObject):
doc_field_types.extend(
[
# :value name: descr
- GroupedField(
+ CompatGroupedField(
"value",
label=_("Values"),
names=("value",),
@@ -484,7 +489,7 @@ class QAPIAlternate(QAPIObject):
doc_field_types.extend(
[
# :alt type name: descr
- TypedField(
+ CompatTypedField(
"alternative",
label=_("Alternatives"),
names=("alt",),
@@ -502,7 +507,7 @@ class QAPIObjectWithMembers(QAPIObject):
doc_field_types.extend(
[
# :member type name: descr
- TypedField(
+ CompatTypedField(
"member",
label=_("Members"),
names=("memb",),
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 30/63] docs/qapi-domain: warn when QAPI domain xrefs fail to resolve
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (28 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 29/63] docs/qapi-domain: add XREF compatibility goop for Sphinx < 4.1 John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 31/63] docs/qapi-domain: Fix error context reporting in Sphinx 5.x and 6.x John Snow
` (33 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This patch adds a warning (which is a build failure under our current
build settings) whenever a QAPI cross-reference fails to resolve.
This applies to any cross-references of the form :qapi:{role}:`foo`,
which covers all of the automatically generated references by the qapi
domain, and any such references that are manually written into the
documentation rst files.
Cross-references of the form `foo` do not use this system, but are
already configured to issue a warning (Again, a build failure) if the
cross-reference isn't found anywhere.
Adds warnings that look like the following:
docs/qapi/index.rst:48: WARNING: qapi:type reference target not found: 'footype' [ref.qapi]
docs/qapi/index.rst:50: WARNING: qapi:mod reference target not found: 'foomod' [ref.qapi]
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index 3b1490e29a1..b23db1eba26 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -830,6 +830,29 @@ def resolve_xref(
matches = self.find_obj(modname, target, typ)
if not matches:
+ # Normally, we could pass warn_dangling=True to QAPIXRefRole(),
+ # but that will trigger on references to these built-in types,
+ # which we'd like to ignore instead.
+
+ # Take care of that warning here instead, so long as the
+ # reference isn't to one of our built-in core types.
+ if target not in (
+ "string",
+ "number",
+ "int",
+ "boolean",
+ "null",
+ "value",
+ "q_empty",
+ ):
+ logger.warning(
+ __("qapi:%s reference target not found: %r"),
+ typ,
+ target,
+ type="ref",
+ subtype="qapi",
+ location=node,
+ )
return None
if len(matches) > 1:
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 31/63] docs/qapi-domain: Fix error context reporting in Sphinx 5.x and 6.x
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (29 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 30/63] docs/qapi-domain: warn when QAPI domain xrefs fail to resolve John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 32/63] qapi/parser: adjust info location for doc body section John Snow
` (32 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Sphinx 5.3.0 to Sphinx 6.2.0 has a bug where nested content in an
ObjectDescription content block has its error position reported
incorrectly due to an oversight when they added nested section support
to this directive.
(This bug is present in Sphinx's own Python and C domains; test it
yourself by creating a py:func directive and creating a syntax error in
the directive's content block. The reporting will be incorrect.)
To avoid overriding and re-implementing the entirety of the run()
method, a workaround is employed where we parse the content block
ourselves in before_content(), then null the content block to make
Sphinx's own parsing a no-op. Then, in transform_content (which occurs
after Sphinx's nested parse), we simply swap our own parsed content tree
back in for Sphinx's.
It appears a little tricky, but it's the nicest solution I can find.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/compat.py | 56 ++++++++++++++++++++++++++++++++++++++
docs/sphinx/qapi_domain.py | 15 ++++++----
2 files changed, 65 insertions(+), 6 deletions(-)
diff --git a/docs/sphinx/compat.py b/docs/sphinx/compat.py
index f068d70388d..9cf7fe006e4 100644
--- a/docs/sphinx/compat.py
+++ b/docs/sphinx/compat.py
@@ -4,6 +4,7 @@
import re
from typing import (
+ TYPE_CHECKING,
Any,
Callable,
Optional,
@@ -12,9 +13,11 @@
from docutils import nodes
from docutils.nodes import Element, Node, Text
+from docutils.statemachine import StringList
import sphinx
from sphinx import addnodes, util
+from sphinx.directives import ObjectDescription
from sphinx.environment import BuildEnvironment
from sphinx.roles import XRefRole
from sphinx.util import docfields
@@ -172,3 +175,56 @@ class CompatGroupedField(docfields.GroupedField):
class CompatTypedField(docfields.TypedField):
if MAKE_XREF_WORKAROUND:
make_xref = _compat_make_xref
+
+
+# ################################################################
+# Nested parsing error location fix for Sphinx 5.3.0 < x < 6.2.0 #
+# ################################################################
+
+# When we require Sphinx 4.x, the TYPE_CHECKING hack where we avoid
+# subscripting ObjectDescription at runtime can be removed in favor of
+# just always subscripting the class.
+
+# When we require Sphinx > 6.2.0, the rest of this compatibility hack
+# can be dropped and QAPIObject can just inherit directly from
+# ObjectDescription[Signature].
+
+SOURCE_LOCATION_FIX = (5, 3, 0) <= sphinx.version_info[:3] < (6, 2, 0)
+
+Signature = str
+
+
+if TYPE_CHECKING:
+ _BaseClass = ObjectDescription[Signature]
+else:
+ _BaseClass = ObjectDescription
+
+
+class ParserFix(_BaseClass):
+
+ _temp_content: StringList
+ _temp_offset: int
+ _temp_node: Optional[addnodes.desc_content]
+
+ def before_content(self) -> None:
+ # Work around a sphinx bug and parse the content ourselves.
+ self._temp_content = self.content
+ self._temp_offset = self.content_offset
+ self._temp_node = None
+
+ if SOURCE_LOCATION_FIX:
+ self._temp_node = addnodes.desc_content()
+ self.state.nested_parse(
+ self.content, self.content_offset, self._temp_node
+ )
+ # Sphinx will try to parse the content block itself,
+ # Give it nothingness to parse instead.
+ self.content = StringList()
+ self.content_offset = 0
+
+ def transform_content(self, content_node: addnodes.desc_content) -> None:
+ # Sphinx workaround: Inject our parsed content and restore state.
+ if self._temp_node:
+ content_node += self._temp_node.children
+ self.content = self._temp_content
+ self.content_offset = self._temp_offset
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index b23db1eba26..ca3f3a7e2d5 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -29,6 +29,8 @@
CompatGroupedField,
CompatTypedField,
KeywordNode,
+ ParserFix,
+ Signature,
SpaceNode,
)
from sphinx import addnodes
@@ -147,12 +149,7 @@ def result_nodes(
return results, []
-# Alias for the return of handle_signature(), which is used in several places.
-# (In the Python domain, this is Tuple[str, str] instead.)
-Signature = str
-
-
-class QAPIDescription(ObjectDescription[Signature]):
+class QAPIDescription(ParserFix):
"""
Generic QAPI description.
@@ -422,6 +419,10 @@ def _validate_field(self, field: nodes.field) -> None:
logger.warning(msg, location=field)
def transform_content(self, content_node: addnodes.desc_content) -> None:
+ # This hook runs after before_content and the nested parse, but
+ # before the DocFieldTransformer is executed.
+ super().transform_content(content_node)
+
self._add_infopips(content_node)
# Validate field lists.
@@ -519,10 +520,12 @@ class QAPIObjectWithMembers(QAPIObject):
class QAPIEvent(QAPIObjectWithMembers):
+ # pylint: disable=too-many-ancestors
"""Description of a QAPI Event."""
class QAPIJSONObject(QAPIObjectWithMembers):
+ # pylint: disable=too-many-ancestors
"""Description of a QAPI Object: structs and unions."""
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 32/63] qapi/parser: adjust info location for doc body section
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (30 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 31/63] docs/qapi-domain: Fix error context reporting in Sphinx 5.x and 6.x John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 7:01 ` Markus Armbruster
2025-03-11 3:42 ` [PATCH v3 33/63] qapi: clean up encoding of section kinds John Snow
` (31 subsequent siblings)
63 siblings, 1 reply; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Instead of using the info object for the doc block as a whole (which
always points to the very first line of the block), update the info
pointer for each call to ensure_untagged_section when the existing
section is otherwise empty. This way, Sphinx error information will
match precisely to where the text actually starts.
For example, this patch will move the info pointer for the "Hello!"
untagged section ...
> ## <-- from here ...
> # Hello! <-- ... to here.
> ##
This doesn't seem to improve error reporting now. It will with the
forthcoming QAPI doc transmogrifier.
If I stick bad rST into qapi/block-core.json like this:
> ##
> # @SnapshotInfo:
> #
> +# rST syntax error: *ahh!
> +#
> # @id: unique shapshot id
> #
> # @name: user chosen name
The existing code's error message will point to the beginning of the doc
comment, which is less than helpful. The transmogrifier's message will
point to the erroneous line, but to accomplish this, it needs this
patch.
Signed-off-by: John Snow <jsnow@redhat.com>
---
scripts/qapi/parser.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index 64f0bb824ae..97def9f0e4f 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -686,7 +686,11 @@ def end(self) -> None:
def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
if self.all_sections and not self.all_sections[-1].tag:
# extend current section
- self.all_sections[-1].text += '\n'
+ section = self.all_sections[-1]
+ if not section.text:
+ # Section is empty so far; update info to start *here*.
+ section.info = info
+ section.text += '\n'
return
# start new section
section = self.Section(info)
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 33/63] qapi: clean up encoding of section kinds
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (31 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 32/63] qapi/parser: adjust info location for doc body section John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 7:02 ` Markus Armbruster
2025-03-11 3:42 ` [PATCH v3 34/63] qapi/schema: add __repr__ to QAPIDoc.Section John Snow
` (30 subsequent siblings)
63 siblings, 1 reply; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
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>
---
docs/sphinx/qapidoc.py | 7 +--
scripts/qapi/parser.py | 97 ++++++++++++++++++++++++----------
tests/qapi-schema/doc-good.out | 10 ++--
tests/qapi-schema/test-qapi.py | 2 +-
4 files changed, 80 insertions(+), 36 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 97def9f0e4f..94d5322f8af 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -14,6 +14,7 @@
# This work is licensed under the terms of the GNU GPL, version 2.
# See the COPYING file in the top-level directory.
+import enum
import os
import re
from typing import (
@@ -574,7 +575,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)
@@ -585,7 +589,7 @@ def get_doc(self) -> 'QAPIDoc':
self,
"unexpected '=' markup in definition documentation")
else:
- # tag-less paragraph
+ # plain paragraph
doc.ensure_untagged_section(self.info)
doc.append_line(line)
line = self.get_doc_paragraph(doc)
@@ -634,14 +638,33 @@ 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':
+ return QAPIDoc.Kind[kind.upper()]
+
+ def __str__(self) -> str:
+ return self.name.title()
+
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 = ''
@@ -649,8 +672,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
def connect(self, member: 'QAPISchemaMember') -> None:
@@ -662,7 +691,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
@@ -679,12 +710,14 @@ 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 != QAPIDoc.Kind.PLAIN and section.text == '':
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:
+ kind = QAPIDoc.Kind.PLAIN
+
+ if self.all_sections and self.all_sections[-1].kind == kind:
# extend current section
section = self.all_sections[-1]
if not section.text:
@@ -692,46 +725,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)
def append_line(self, line: str) -> None:
self.all_sections[-1].append_line(line)
@@ -744,7 +787,7 @@ def connect_member(self, member: 'QAPISchemaMember') -> None:
"%s '%s' lacks documentation"
% (member.role, member.name))
self.args[member.name] = QAPIDoc.ArgSection(
- self.info, '@' + member.name)
+ self.info, QAPIDoc.Kind.MEMBER, member.name)
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
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 8fe951c8803..4be930228cc 100755
--- a/tests/qapi-schema/test-qapi.py
+++ b/tests/qapi-schema/test-qapi.py
@@ -122,7 +122,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):
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 34/63] qapi/schema: add __repr__ to QAPIDoc.Section
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (32 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 33/63] qapi: clean up encoding of section kinds John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 7:03 ` Markus Armbruster
2025-03-11 3:42 ` [PATCH v3 35/63] docs/qapidoc: add transmogrifier stub John Snow
` (29 subsequent siblings)
63 siblings, 1 reply; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Makes debugging far more pleasant when you can just print(section) and
get something reasonable to display.
Signed-off-by: John Snow <jsnow@redhat.com>
---
scripts/qapi/parser.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index 94d5322f8af..11c11bb09e5 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -668,6 +668,9 @@ def __init__(
# section text without tag
self.text = ''
+ def __repr__(self) -> str:
+ return f"<QAPIDoc.Section kind={self.kind!r} text={self.text!r}>"
+
def append_line(self, line: str) -> None:
self.text += line + '\n'
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 35/63] docs/qapidoc: add transmogrifier stub
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (33 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 34/63] qapi/schema: add __repr__ to QAPIDoc.Section John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 36/63] docs/qapidoc: split old implementation into qapidoc_legacy.py John Snow
` (28 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This commit adds a stubbed option to the qapi-doc directive that opts-in
to the new rST generator; the implementation of which will follow in
subsequent commits.
Once all QAPI documents have been converted, this option and the old
qapidoc implementation can be dropped.
Note that moving code outside of the try...except block has no impact
because the code moved outside of that block does not ever raise a
QAPIError.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 41 ++++++++++++++++++++++++++++-------------
1 file changed, 28 insertions(+), 13 deletions(-)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index d622398f1da..dc72f3fd3f3 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -452,9 +452,9 @@ def _parse_text_into_node(self, doctext, node):
rstlist.append("", self._cur_doc.info.fname, self._cur_doc.info.line)
self._sphinx_directive.do_parse(rstlist, node)
- def get_document_nodes(self):
- """Return the list of docutils nodes which make up the document"""
- return self._top_node.children
+ def get_document_node(self):
+ """Return the root docutils node which makes up the document"""
+ return self._top_node
# Turn the black formatter on for the rest of the file.
@@ -503,7 +503,10 @@ class QAPIDocDirective(NestedDirective):
required_argument = 1
optional_arguments = 1
- option_spec = {"qapifile": directives.unchanged_required}
+ option_spec = {
+ "qapifile": directives.unchanged_required,
+ "transmogrify": directives.flag,
+ }
has_content = False
def new_serialno(self):
@@ -511,10 +514,24 @@ def new_serialno(self):
env = self.state.document.settings.env
return "qapidoc-%d" % env.new_serialno("qapidoc")
+ def transmogrify(self, schema) -> nodes.Element:
+ raise NotImplementedError
+
+ def legacy(self, schema) -> nodes.Element:
+ vis = QAPISchemaGenRSTVisitor(self)
+ vis.visit_begin(schema)
+ for doc in schema.docs:
+ if doc.symbol:
+ vis.symbol(doc, schema.lookup_entity(doc.symbol))
+ else:
+ vis.freeform(doc)
+ return vis.get_document_node()
+
def run(self):
env = self.state.document.settings.env
qapifile = env.config.qapidoc_srctree + "/" + self.arguments[0]
qapidir = os.path.dirname(qapifile)
+ transmogrify = "transmogrify" in self.options
try:
schema = QAPISchema(qapifile)
@@ -522,20 +539,18 @@ def run(self):
# First tell Sphinx about all the schema files that the
# output documentation depends on (including 'qapifile' itself)
schema.visit(QAPISchemaGenDepVisitor(env, qapidir))
-
- vis = QAPISchemaGenRSTVisitor(self)
- vis.visit_begin(schema)
- for doc in schema.docs:
- if doc.symbol:
- vis.symbol(doc, schema.lookup_entity(doc.symbol))
- else:
- vis.freeform(doc)
- return vis.get_document_nodes()
except QAPIError as err:
# Launder QAPI parse errors into Sphinx extension errors
# so they are displayed nicely to the user
raise ExtensionError(str(err)) from err
+ if transmogrify:
+ contentnode = self.transmogrify(schema)
+ else:
+ contentnode = self.legacy(schema)
+
+ return contentnode.children
+
class QMPExample(CodeBlock, NestedDirective):
"""
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 36/63] docs/qapidoc: split old implementation into qapidoc_legacy.py
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (34 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 35/63] docs/qapidoc: add transmogrifier stub John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 37/63] docs/qapidoc: Fix static typing on qapidoc.py John Snow
` (27 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This is being done primarily to be able to type check and delint the new
implementation without needing to worry about fixing up the old
implementation.
I'm adding the new implementation into the existing file instead of into
a new file so that when the dust settles, qapidoc.py will contain the
full history of development on this generative module.
This patch *should* be pure motion, give or take the import statements.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 420 +-------------------------------
docs/sphinx/qapidoc_legacy.py | 439 ++++++++++++++++++++++++++++++++++
2 files changed, 441 insertions(+), 418 deletions(-)
create mode 100644 docs/sphinx/qapidoc_legacy.py
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index dc72f3fd3f3..f4abf42e7bf 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -25,19 +25,16 @@
"""
import os
-import re
import sys
-import textwrap
from typing import List
from docutils import nodes
from docutils.parsers.rst import Directive, directives
-from docutils.statemachine import ViewList
-from qapi.error import QAPIError, QAPISemError
+from qapi.error import QAPIError
from qapi.gen import QAPISchemaVisitor
-from qapi.parser import QAPIDoc
from qapi.schema import QAPISchema
+from qapidoc_legacy import QAPISchemaGenRSTVisitor
from sphinx import addnodes
from sphinx.directives.code import CodeBlock
from sphinx.errors import ExtensionError
@@ -48,419 +45,6 @@
__version__ = "1.0"
-def dedent(text: str) -> str:
- # Adjust indentation to make description text parse as paragraph.
-
- lines = text.splitlines(True)
- if re.match(r"\s+", lines[0]):
- # First line is indented; description started on the line after
- # the name. dedent the whole block.
- return textwrap.dedent(text)
-
- # Descr started on same line. Dedent line 2+.
- return lines[0] + textwrap.dedent("".join(lines[1:]))
-
-
-# Disable black auto-formatter until re-enabled:
-# fmt: off
-
-
-class QAPISchemaGenRSTVisitor(QAPISchemaVisitor):
- """A QAPI schema visitor which generates docutils/Sphinx nodes
-
- This class builds up a tree of docutils/Sphinx nodes corresponding
- to documentation for the various QAPI objects. To use it, first
- create a QAPISchemaGenRSTVisitor object, and call its
- visit_begin() method. Then you can call one of the two methods
- 'freeform' (to add documentation for a freeform documentation
- chunk) or 'symbol' (to add documentation for a QAPI symbol). These
- will cause the visitor to build up the tree of document
- nodes. Once you've added all the documentation via 'freeform' and
- 'symbol' method calls, you can call 'get_document_nodes' to get
- the final list of document nodes (in a form suitable for returning
- from a Sphinx directive's 'run' method).
- """
- def __init__(self, sphinx_directive):
- self._cur_doc = None
- self._sphinx_directive = sphinx_directive
- self._top_node = nodes.section()
- self._active_headings = [self._top_node]
-
- def _make_dlitem(self, term, defn):
- """Return a dlitem node with the specified term and definition.
-
- term should be a list of Text and literal nodes.
- defn should be one of:
- - a string, which will be handed to _parse_text_into_node
- - a list of Text and literal nodes, which will be put into
- a paragraph node
- """
- dlitem = nodes.definition_list_item()
- dlterm = nodes.term('', '', *term)
- dlitem += dlterm
- if defn:
- dldef = nodes.definition()
- if isinstance(defn, list):
- dldef += nodes.paragraph('', '', *defn)
- else:
- self._parse_text_into_node(defn, dldef)
- dlitem += dldef
- return dlitem
-
- def _make_section(self, title):
- """Return a section node with optional title"""
- section = nodes.section(ids=[self._sphinx_directive.new_serialno()])
- if title:
- section += nodes.title(title, title)
- return section
-
- def _nodes_for_ifcond(self, ifcond, with_if=True):
- """Return list of Text, literal nodes for the ifcond
-
- Return a list which gives text like ' (If: condition)'.
- If with_if is False, we don't return the "(If: " and ")".
- """
-
- doc = ifcond.docgen()
- if not doc:
- return []
- doc = nodes.literal('', doc)
- if not with_if:
- return [doc]
-
- nodelist = [nodes.Text(' ('), nodes.strong('', 'If: ')]
- nodelist.append(doc)
- nodelist.append(nodes.Text(')'))
- return nodelist
-
- def _nodes_for_one_member(self, member):
- """Return list of Text, literal nodes for this member
-
- Return a list of doctree nodes which give text like
- 'name: type (optional) (If: ...)' suitable for use as the
- 'term' part of a definition list item.
- """
- term = [nodes.literal('', member.name)]
- if member.type.doc_type():
- term.append(nodes.Text(': '))
- term.append(nodes.literal('', member.type.doc_type()))
- if member.optional:
- term.append(nodes.Text(' (optional)'))
- if member.ifcond.is_present():
- term.extend(self._nodes_for_ifcond(member.ifcond))
- return term
-
- def _nodes_for_variant_when(self, branches, variant):
- """Return list of Text, literal nodes for variant 'when' clause
-
- Return a list of doctree nodes which give text like
- 'when tagname is variant (If: ...)' suitable for use in
- the 'branches' part of a definition list.
- """
- term = [nodes.Text(' when '),
- nodes.literal('', branches.tag_member.name),
- nodes.Text(' is '),
- nodes.literal('', '"%s"' % variant.name)]
- if variant.ifcond.is_present():
- term.extend(self._nodes_for_ifcond(variant.ifcond))
- return term
-
- def _nodes_for_members(self, doc, what, base=None, branches=None):
- """Return list of doctree nodes for the table of members"""
- dlnode = nodes.definition_list()
- for section in doc.args.values():
- term = self._nodes_for_one_member(section.member)
- # TODO drop fallbacks when undocumented members are outlawed
- if section.text:
- defn = dedent(section.text)
- else:
- defn = [nodes.Text('Not documented')]
-
- dlnode += self._make_dlitem(term, defn)
-
- if base:
- dlnode += self._make_dlitem([nodes.Text('The members of '),
- nodes.literal('', base.doc_type())],
- None)
-
- if branches:
- for v in branches.variants:
- if v.type.name == 'q_empty':
- continue
- assert not v.type.is_implicit()
- term = [nodes.Text('The members of '),
- nodes.literal('', v.type.doc_type())]
- term.extend(self._nodes_for_variant_when(branches, v))
- dlnode += self._make_dlitem(term, None)
-
- if not dlnode.children:
- return []
-
- section = self._make_section(what)
- section += dlnode
- return [section]
-
- def _nodes_for_enum_values(self, doc):
- """Return list of doctree nodes for the table of enum values"""
- seen_item = False
- dlnode = nodes.definition_list()
- for section in doc.args.values():
- termtext = [nodes.literal('', section.member.name)]
- if section.member.ifcond.is_present():
- termtext.extend(self._nodes_for_ifcond(section.member.ifcond))
- # TODO drop fallbacks when undocumented members are outlawed
- if section.text:
- defn = dedent(section.text)
- else:
- defn = [nodes.Text('Not documented')]
-
- dlnode += self._make_dlitem(termtext, defn)
- seen_item = True
-
- if not seen_item:
- return []
-
- section = self._make_section('Values')
- section += dlnode
- return [section]
-
- def _nodes_for_arguments(self, doc, arg_type):
- """Return list of doctree nodes for the arguments section"""
- if arg_type and not arg_type.is_implicit():
- assert not doc.args
- section = self._make_section('Arguments')
- dlnode = nodes.definition_list()
- dlnode += self._make_dlitem(
- [nodes.Text('The members of '),
- nodes.literal('', arg_type.name)],
- None)
- section += dlnode
- return [section]
-
- return self._nodes_for_members(doc, 'Arguments')
-
- def _nodes_for_features(self, doc):
- """Return list of doctree nodes for the table of features"""
- seen_item = False
- dlnode = nodes.definition_list()
- for section in doc.features.values():
- dlnode += self._make_dlitem(
- [nodes.literal('', section.member.name)], dedent(section.text))
- seen_item = True
-
- if not seen_item:
- return []
-
- section = self._make_section('Features')
- section += dlnode
- return [section]
-
- def _nodes_for_sections(self, doc):
- """Return list of doctree nodes for additional sections"""
- nodelist = []
- for section in doc.sections:
- if section.kind == QAPIDoc.Kind.TODO:
- # Hide TODO: sections
- continue
-
- if section.kind == QAPIDoc.Kind.PLAIN:
- # Sphinx cannot handle sectionless titles;
- # Instead, just append the results to the prior section.
- container = nodes.container()
- self._parse_text_into_node(section.text, container)
- nodelist += container.children
- continue
-
- snode = self._make_section(section.kind.name.title())
- self._parse_text_into_node(dedent(section.text), snode)
- nodelist.append(snode)
- return nodelist
-
- def _nodes_for_if_section(self, ifcond):
- """Return list of doctree nodes for the "If" section"""
- nodelist = []
- if ifcond.is_present():
- snode = self._make_section('If')
- snode += nodes.paragraph(
- '', '', *self._nodes_for_ifcond(ifcond, with_if=False)
- )
- nodelist.append(snode)
- return nodelist
-
- def _add_doc(self, typ, sections):
- """Add documentation for a command/object/enum...
-
- We assume we're documenting the thing defined in self._cur_doc.
- typ is the type of thing being added ("Command", "Object", etc)
-
- sections is a list of nodes for sections to add to the definition.
- """
-
- doc = self._cur_doc
- snode = nodes.section(ids=[self._sphinx_directive.new_serialno()])
- snode += nodes.title('', '', *[nodes.literal(doc.symbol, doc.symbol),
- nodes.Text(' (' + typ + ')')])
- self._parse_text_into_node(doc.body.text, snode)
- for s in sections:
- if s is not None:
- snode += s
- self._add_node_to_current_heading(snode)
-
- def visit_enum_type(self, name, info, ifcond, features, members, prefix):
- doc = self._cur_doc
- self._add_doc('Enum',
- self._nodes_for_enum_values(doc)
- + self._nodes_for_features(doc)
- + self._nodes_for_sections(doc)
- + self._nodes_for_if_section(ifcond))
-
- def visit_object_type(self, name, info, ifcond, features,
- base, members, branches):
- doc = self._cur_doc
- if base and base.is_implicit():
- base = None
- self._add_doc('Object',
- self._nodes_for_members(doc, 'Members', base, branches)
- + self._nodes_for_features(doc)
- + self._nodes_for_sections(doc)
- + self._nodes_for_if_section(ifcond))
-
- def visit_alternate_type(self, name, info, ifcond, features,
- alternatives):
- doc = self._cur_doc
- self._add_doc('Alternate',
- self._nodes_for_members(doc, 'Members')
- + self._nodes_for_features(doc)
- + self._nodes_for_sections(doc)
- + self._nodes_for_if_section(ifcond))
-
- def visit_command(self, name, info, ifcond, features, arg_type,
- ret_type, gen, success_response, boxed, allow_oob,
- allow_preconfig, coroutine):
- doc = self._cur_doc
- self._add_doc('Command',
- self._nodes_for_arguments(doc, arg_type)
- + self._nodes_for_features(doc)
- + self._nodes_for_sections(doc)
- + self._nodes_for_if_section(ifcond))
-
- def visit_event(self, name, info, ifcond, features, arg_type, boxed):
- doc = self._cur_doc
- self._add_doc('Event',
- self._nodes_for_arguments(doc, arg_type)
- + self._nodes_for_features(doc)
- + self._nodes_for_sections(doc)
- + self._nodes_for_if_section(ifcond))
-
- def symbol(self, doc, entity):
- """Add documentation for one symbol to the document tree
-
- This is the main entry point which causes us to add documentation
- nodes for a symbol (which could be a 'command', 'object', 'event',
- etc). We do this by calling 'visit' on the schema entity, which
- will then call back into one of our visit_* methods, depending
- on what kind of thing this symbol is.
- """
- self._cur_doc = doc
- entity.visit(self)
- self._cur_doc = None
-
- def _start_new_heading(self, heading, level):
- """Start a new heading at the specified heading level
-
- Create a new section whose title is 'heading' and which is placed
- in the docutils node tree as a child of the most recent level-1
- heading. Subsequent document sections (commands, freeform doc chunks,
- etc) will be placed as children of this new heading section.
- """
- if len(self._active_headings) < level:
- raise QAPISemError(self._cur_doc.info,
- 'Level %d subheading found outside a '
- 'level %d heading'
- % (level, level - 1))
- snode = self._make_section(heading)
- self._active_headings[level - 1] += snode
- self._active_headings = self._active_headings[:level]
- self._active_headings.append(snode)
- return snode
-
- def _add_node_to_current_heading(self, node):
- """Add the node to whatever the current active heading is"""
- self._active_headings[-1] += node
-
- def freeform(self, doc):
- """Add a piece of 'freeform' documentation to the document tree
-
- A 'freeform' document chunk doesn't relate to any particular
- symbol (for instance, it could be an introduction).
-
- If the freeform document starts with a line of the form
- '= Heading text', this is a section or subsection heading, with
- the heading level indicated by the number of '=' signs.
- """
-
- # QAPIDoc documentation says free-form documentation blocks
- # must have only a body section, nothing else.
- assert not doc.sections
- assert not doc.args
- assert not doc.features
- self._cur_doc = doc
-
- text = doc.body.text
- if re.match(r'=+ ', text):
- # Section/subsection heading (if present, will always be
- # the first line of the block)
- (heading, _, text) = text.partition('\n')
- (leader, _, heading) = heading.partition(' ')
- node = self._start_new_heading(heading, len(leader))
- if text == '':
- return
- else:
- node = nodes.container()
-
- self._parse_text_into_node(text, node)
- self._cur_doc = None
-
- def _parse_text_into_node(self, doctext, node):
- """Parse a chunk of QAPI-doc-format text into the node
-
- The doc comment can contain most inline rST markup, including
- bulleted and enumerated lists.
- As an extra permitted piece of markup, @var will be turned
- into ``var``.
- """
-
- # Handle the "@var means ``var`` case
- doctext = re.sub(r'@([\w-]+)', r'``\1``', doctext)
-
- rstlist = ViewList()
- for line in doctext.splitlines():
- # The reported line number will always be that of the start line
- # of the doc comment, rather than the actual location of the error.
- # Being more precise would require overhaul of the QAPIDoc class
- # to track lines more exactly within all the sub-parts of the doc
- # comment, as well as counting lines here.
- rstlist.append(line, self._cur_doc.info.fname,
- self._cur_doc.info.line)
- # Append a blank line -- in some cases rST syntax errors get
- # attributed to the line after one with actual text, and if there
- # isn't anything in the ViewList corresponding to that then Sphinx
- # 1.6's AutodocReporter will then misidentify the source/line location
- # in the error message (usually attributing it to the top-level
- # .rst file rather than the offending .json file). The extra blank
- # line won't affect the rendered output.
- rstlist.append("", self._cur_doc.info.fname, self._cur_doc.info.line)
- self._sphinx_directive.do_parse(rstlist, node)
-
- def get_document_node(self):
- """Return the root docutils node which makes up the document"""
- return self._top_node
-
-
-# Turn the black formatter on for the rest of the file.
-# fmt: on
-
-
class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
"""A QAPI schema visitor which adds Sphinx dependencies each module
diff --git a/docs/sphinx/qapidoc_legacy.py b/docs/sphinx/qapidoc_legacy.py
new file mode 100644
index 00000000000..679f38356b1
--- /dev/null
+++ b/docs/sphinx/qapidoc_legacy.py
@@ -0,0 +1,439 @@
+# coding=utf-8
+#
+# QEMU qapidoc QAPI file parsing extension
+#
+# Copyright (c) 2020 Linaro
+#
+# This work is licensed under the terms of the GNU GPLv2 or later.
+# See the COPYING file in the top-level directory.
+
+"""
+qapidoc is a Sphinx extension that implements the qapi-doc directive
+
+The purpose of this extension is to read the documentation comments
+in QAPI schema files, and insert them all into the current document.
+
+It implements one new rST directive, "qapi-doc::".
+Each qapi-doc:: directive takes one argument, which is the
+pathname of the schema file to process, relative to the source tree.
+
+The docs/conf.py file must set the qapidoc_srctree config value to
+the root of the QEMU source tree.
+
+The Sphinx documentation on writing extensions is at:
+https://www.sphinx-doc.org/en/master/development/index.html
+"""
+
+import re
+import textwrap
+
+from docutils import nodes
+from docutils.statemachine import ViewList
+from qapi.error import QAPISemError
+from qapi.gen import QAPISchemaVisitor
+from qapi.parser import QAPIDoc
+
+
+def dedent(text: str) -> str:
+ # Adjust indentation to make description text parse as paragraph.
+
+ lines = text.splitlines(True)
+ if re.match(r"\s+", lines[0]):
+ # First line is indented; description started on the line after
+ # the name. dedent the whole block.
+ return textwrap.dedent(text)
+
+ # Descr started on same line. Dedent line 2+.
+ return lines[0] + textwrap.dedent("".join(lines[1:]))
+
+
+class QAPISchemaGenRSTVisitor(QAPISchemaVisitor):
+ """A QAPI schema visitor which generates docutils/Sphinx nodes
+
+ This class builds up a tree of docutils/Sphinx nodes corresponding
+ to documentation for the various QAPI objects. To use it, first
+ create a QAPISchemaGenRSTVisitor object, and call its
+ visit_begin() method. Then you can call one of the two methods
+ 'freeform' (to add documentation for a freeform documentation
+ chunk) or 'symbol' (to add documentation for a QAPI symbol). These
+ will cause the visitor to build up the tree of document
+ nodes. Once you've added all the documentation via 'freeform' and
+ 'symbol' method calls, you can call 'get_document_nodes' to get
+ the final list of document nodes (in a form suitable for returning
+ from a Sphinx directive's 'run' method).
+ """
+ def __init__(self, sphinx_directive):
+ self._cur_doc = None
+ self._sphinx_directive = sphinx_directive
+ self._top_node = nodes.section()
+ self._active_headings = [self._top_node]
+
+ def _make_dlitem(self, term, defn):
+ """Return a dlitem node with the specified term and definition.
+
+ term should be a list of Text and literal nodes.
+ defn should be one of:
+ - a string, which will be handed to _parse_text_into_node
+ - a list of Text and literal nodes, which will be put into
+ a paragraph node
+ """
+ dlitem = nodes.definition_list_item()
+ dlterm = nodes.term('', '', *term)
+ dlitem += dlterm
+ if defn:
+ dldef = nodes.definition()
+ if isinstance(defn, list):
+ dldef += nodes.paragraph('', '', *defn)
+ else:
+ self._parse_text_into_node(defn, dldef)
+ dlitem += dldef
+ return dlitem
+
+ def _make_section(self, title):
+ """Return a section node with optional title"""
+ section = nodes.section(ids=[self._sphinx_directive.new_serialno()])
+ if title:
+ section += nodes.title(title, title)
+ return section
+
+ def _nodes_for_ifcond(self, ifcond, with_if=True):
+ """Return list of Text, literal nodes for the ifcond
+
+ Return a list which gives text like ' (If: condition)'.
+ If with_if is False, we don't return the "(If: " and ")".
+ """
+
+ doc = ifcond.docgen()
+ if not doc:
+ return []
+ doc = nodes.literal('', doc)
+ if not with_if:
+ return [doc]
+
+ nodelist = [nodes.Text(' ('), nodes.strong('', 'If: ')]
+ nodelist.append(doc)
+ nodelist.append(nodes.Text(')'))
+ return nodelist
+
+ def _nodes_for_one_member(self, member):
+ """Return list of Text, literal nodes for this member
+
+ Return a list of doctree nodes which give text like
+ 'name: type (optional) (If: ...)' suitable for use as the
+ 'term' part of a definition list item.
+ """
+ term = [nodes.literal('', member.name)]
+ if member.type.doc_type():
+ term.append(nodes.Text(': '))
+ term.append(nodes.literal('', member.type.doc_type()))
+ if member.optional:
+ term.append(nodes.Text(' (optional)'))
+ if member.ifcond.is_present():
+ term.extend(self._nodes_for_ifcond(member.ifcond))
+ return term
+
+ def _nodes_for_variant_when(self, branches, variant):
+ """Return list of Text, literal nodes for variant 'when' clause
+
+ Return a list of doctree nodes which give text like
+ 'when tagname is variant (If: ...)' suitable for use in
+ the 'branches' part of a definition list.
+ """
+ term = [nodes.Text(' when '),
+ nodes.literal('', branches.tag_member.name),
+ nodes.Text(' is '),
+ nodes.literal('', '"%s"' % variant.name)]
+ if variant.ifcond.is_present():
+ term.extend(self._nodes_for_ifcond(variant.ifcond))
+ return term
+
+ def _nodes_for_members(self, doc, what, base=None, branches=None):
+ """Return list of doctree nodes for the table of members"""
+ dlnode = nodes.definition_list()
+ for section in doc.args.values():
+ term = self._nodes_for_one_member(section.member)
+ # TODO drop fallbacks when undocumented members are outlawed
+ if section.text:
+ defn = dedent(section.text)
+ else:
+ defn = [nodes.Text('Not documented')]
+
+ dlnode += self._make_dlitem(term, defn)
+
+ if base:
+ dlnode += self._make_dlitem([nodes.Text('The members of '),
+ nodes.literal('', base.doc_type())],
+ None)
+
+ if branches:
+ for v in branches.variants:
+ if v.type.name == 'q_empty':
+ continue
+ assert not v.type.is_implicit()
+ term = [nodes.Text('The members of '),
+ nodes.literal('', v.type.doc_type())]
+ term.extend(self._nodes_for_variant_when(branches, v))
+ dlnode += self._make_dlitem(term, None)
+
+ if not dlnode.children:
+ return []
+
+ section = self._make_section(what)
+ section += dlnode
+ return [section]
+
+ def _nodes_for_enum_values(self, doc):
+ """Return list of doctree nodes for the table of enum values"""
+ seen_item = False
+ dlnode = nodes.definition_list()
+ for section in doc.args.values():
+ termtext = [nodes.literal('', section.member.name)]
+ if section.member.ifcond.is_present():
+ termtext.extend(self._nodes_for_ifcond(section.member.ifcond))
+ # TODO drop fallbacks when undocumented members are outlawed
+ if section.text:
+ defn = dedent(section.text)
+ else:
+ defn = [nodes.Text('Not documented')]
+
+ dlnode += self._make_dlitem(termtext, defn)
+ seen_item = True
+
+ if not seen_item:
+ return []
+
+ section = self._make_section('Values')
+ section += dlnode
+ return [section]
+
+ def _nodes_for_arguments(self, doc, arg_type):
+ """Return list of doctree nodes for the arguments section"""
+ if arg_type and not arg_type.is_implicit():
+ assert not doc.args
+ section = self._make_section('Arguments')
+ dlnode = nodes.definition_list()
+ dlnode += self._make_dlitem(
+ [nodes.Text('The members of '),
+ nodes.literal('', arg_type.name)],
+ None)
+ section += dlnode
+ return [section]
+
+ return self._nodes_for_members(doc, 'Arguments')
+
+ def _nodes_for_features(self, doc):
+ """Return list of doctree nodes for the table of features"""
+ seen_item = False
+ dlnode = nodes.definition_list()
+ for section in doc.features.values():
+ dlnode += self._make_dlitem(
+ [nodes.literal('', section.member.name)], dedent(section.text))
+ seen_item = True
+
+ if not seen_item:
+ return []
+
+ section = self._make_section('Features')
+ section += dlnode
+ return [section]
+
+ def _nodes_for_sections(self, doc):
+ """Return list of doctree nodes for additional sections"""
+ nodelist = []
+ for section in doc.sections:
+ if section.kind == QAPIDoc.Kind.TODO:
+ # Hide TODO: sections
+ continue
+
+ if section.kind == QAPIDoc.Kind.PLAIN:
+ # Sphinx cannot handle sectionless titles;
+ # Instead, just append the results to the prior section.
+ container = nodes.container()
+ self._parse_text_into_node(section.text, container)
+ nodelist += container.children
+ continue
+
+ snode = self._make_section(section.kind.name.title())
+ self._parse_text_into_node(dedent(section.text), snode)
+ nodelist.append(snode)
+ return nodelist
+
+ def _nodes_for_if_section(self, ifcond):
+ """Return list of doctree nodes for the "If" section"""
+ nodelist = []
+ if ifcond.is_present():
+ snode = self._make_section('If')
+ snode += nodes.paragraph(
+ '', '', *self._nodes_for_ifcond(ifcond, with_if=False)
+ )
+ nodelist.append(snode)
+ return nodelist
+
+ def _add_doc(self, typ, sections):
+ """Add documentation for a command/object/enum...
+
+ We assume we're documenting the thing defined in self._cur_doc.
+ typ is the type of thing being added ("Command", "Object", etc)
+
+ sections is a list of nodes for sections to add to the definition.
+ """
+
+ doc = self._cur_doc
+ snode = nodes.section(ids=[self._sphinx_directive.new_serialno()])
+ snode += nodes.title('', '', *[nodes.literal(doc.symbol, doc.symbol),
+ nodes.Text(' (' + typ + ')')])
+ self._parse_text_into_node(doc.body.text, snode)
+ for s in sections:
+ if s is not None:
+ snode += s
+ self._add_node_to_current_heading(snode)
+
+ def visit_enum_type(self, name, info, ifcond, features, members, prefix):
+ doc = self._cur_doc
+ self._add_doc('Enum',
+ self._nodes_for_enum_values(doc)
+ + self._nodes_for_features(doc)
+ + self._nodes_for_sections(doc)
+ + self._nodes_for_if_section(ifcond))
+
+ def visit_object_type(self, name, info, ifcond, features,
+ base, members, branches):
+ doc = self._cur_doc
+ if base and base.is_implicit():
+ base = None
+ self._add_doc('Object',
+ self._nodes_for_members(doc, 'Members', base, branches)
+ + self._nodes_for_features(doc)
+ + self._nodes_for_sections(doc)
+ + self._nodes_for_if_section(ifcond))
+
+ def visit_alternate_type(self, name, info, ifcond, features,
+ alternatives):
+ doc = self._cur_doc
+ self._add_doc('Alternate',
+ self._nodes_for_members(doc, 'Members')
+ + self._nodes_for_features(doc)
+ + self._nodes_for_sections(doc)
+ + self._nodes_for_if_section(ifcond))
+
+ def visit_command(self, name, info, ifcond, features, arg_type,
+ ret_type, gen, success_response, boxed, allow_oob,
+ allow_preconfig, coroutine):
+ doc = self._cur_doc
+ self._add_doc('Command',
+ self._nodes_for_arguments(doc, arg_type)
+ + self._nodes_for_features(doc)
+ + self._nodes_for_sections(doc)
+ + self._nodes_for_if_section(ifcond))
+
+ def visit_event(self, name, info, ifcond, features, arg_type, boxed):
+ doc = self._cur_doc
+ self._add_doc('Event',
+ self._nodes_for_arguments(doc, arg_type)
+ + self._nodes_for_features(doc)
+ + self._nodes_for_sections(doc)
+ + self._nodes_for_if_section(ifcond))
+
+ def symbol(self, doc, entity):
+ """Add documentation for one symbol to the document tree
+
+ This is the main entry point which causes us to add documentation
+ nodes for a symbol (which could be a 'command', 'object', 'event',
+ etc). We do this by calling 'visit' on the schema entity, which
+ will then call back into one of our visit_* methods, depending
+ on what kind of thing this symbol is.
+ """
+ self._cur_doc = doc
+ entity.visit(self)
+ self._cur_doc = None
+
+ def _start_new_heading(self, heading, level):
+ """Start a new heading at the specified heading level
+
+ Create a new section whose title is 'heading' and which is placed
+ in the docutils node tree as a child of the most recent level-1
+ heading. Subsequent document sections (commands, freeform doc chunks,
+ etc) will be placed as children of this new heading section.
+ """
+ if len(self._active_headings) < level:
+ raise QAPISemError(self._cur_doc.info,
+ 'Level %d subheading found outside a '
+ 'level %d heading'
+ % (level, level - 1))
+ snode = self._make_section(heading)
+ self._active_headings[level - 1] += snode
+ self._active_headings = self._active_headings[:level]
+ self._active_headings.append(snode)
+ return snode
+
+ def _add_node_to_current_heading(self, node):
+ """Add the node to whatever the current active heading is"""
+ self._active_headings[-1] += node
+
+ def freeform(self, doc):
+ """Add a piece of 'freeform' documentation to the document tree
+
+ A 'freeform' document chunk doesn't relate to any particular
+ symbol (for instance, it could be an introduction).
+
+ If the freeform document starts with a line of the form
+ '= Heading text', this is a section or subsection heading, with
+ the heading level indicated by the number of '=' signs.
+ """
+
+ # QAPIDoc documentation says free-form documentation blocks
+ # must have only a body section, nothing else.
+ assert not doc.sections
+ assert not doc.args
+ assert not doc.features
+ self._cur_doc = doc
+
+ text = doc.body.text
+ if re.match(r'=+ ', text):
+ # Section/subsection heading (if present, will always be
+ # the first line of the block)
+ (heading, _, text) = text.partition('\n')
+ (leader, _, heading) = heading.partition(' ')
+ node = self._start_new_heading(heading, len(leader))
+ if text == '':
+ return
+ else:
+ node = nodes.container()
+
+ self._parse_text_into_node(text, node)
+ self._cur_doc = None
+
+ def _parse_text_into_node(self, doctext, node):
+ """Parse a chunk of QAPI-doc-format text into the node
+
+ The doc comment can contain most inline rST markup, including
+ bulleted and enumerated lists.
+ As an extra permitted piece of markup, @var will be turned
+ into ``var``.
+ """
+
+ # Handle the "@var means ``var`` case
+ doctext = re.sub(r'@([\w-]+)', r'``\1``', doctext)
+
+ rstlist = ViewList()
+ for line in doctext.splitlines():
+ # The reported line number will always be that of the start line
+ # of the doc comment, rather than the actual location of the error.
+ # Being more precise would require overhaul of the QAPIDoc class
+ # to track lines more exactly within all the sub-parts of the doc
+ # comment, as well as counting lines here.
+ rstlist.append(line, self._cur_doc.info.fname,
+ self._cur_doc.info.line)
+ # Append a blank line -- in some cases rST syntax errors get
+ # attributed to the line after one with actual text, and if there
+ # isn't anything in the ViewList corresponding to that then Sphinx
+ # 1.6's AutodocReporter will then misidentify the source/line location
+ # in the error message (usually attributing it to the top-level
+ # .rst file rather than the offending .json file). The extra blank
+ # line won't affect the rendered output.
+ rstlist.append("", self._cur_doc.info.fname, self._cur_doc.info.line)
+ self._sphinx_directive.do_parse(rstlist, node)
+
+ def get_document_node(self):
+ """Return the root docutils node which makes up the document"""
+ return self._top_node
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 37/63] docs/qapidoc: Fix static typing on qapidoc.py
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (35 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 36/63] docs/qapidoc: split old implementation into qapidoc_legacy.py John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 38/63] do-not-merge John Snow
` (26 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Now that the legacy code is factored out, fix up the typing on the
remaining code in qapidoc.py. Add a type ignore to qapi_legacy.py to
prevent the errors there from bleeding out into qapidoc.py.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 40 ++++++++++++++++++++++-------------
docs/sphinx/qapidoc_legacy.py | 1 +
2 files changed, 26 insertions(+), 15 deletions(-)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index f4abf42e7bf..5246832b68c 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -24,17 +24,18 @@
https://www.sphinx-doc.org/en/master/development/index.html
"""
+from __future__ import annotations
+
import os
import sys
-from typing import List
+from typing import TYPE_CHECKING
from docutils import nodes
from docutils.parsers.rst import Directive, directives
from qapi.error import QAPIError
-from qapi.gen import QAPISchemaVisitor
-from qapi.schema import QAPISchema
+from qapi.schema import QAPISchema, QAPISchemaVisitor
-from qapidoc_legacy import QAPISchemaGenRSTVisitor
+from qapidoc_legacy import QAPISchemaGenRSTVisitor # type: ignore
from sphinx import addnodes
from sphinx.directives.code import CodeBlock
from sphinx.errors import ExtensionError
@@ -42,6 +43,15 @@
from sphinx.util.nodes import nested_parse_with_titles
+if TYPE_CHECKING:
+ from typing import Any, List, Sequence
+
+ from docutils.statemachine import StringList
+
+ from sphinx.application import Sphinx
+ from sphinx.util.typing import ExtensionMetadata
+
+
__version__ = "1.0"
@@ -53,11 +63,11 @@ class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
schema file associated with each module in the QAPI input.
"""
- def __init__(self, env, qapidir):
+ def __init__(self, env: Any, qapidir: str) -> None:
self._env = env
self._qapidir = qapidir
- def visit_module(self, name):
+ def visit_module(self, name: str) -> None:
if name != "./builtin":
qapifile = self._qapidir + "/" + name
self._env.note_dependency(os.path.abspath(qapifile))
@@ -65,10 +75,10 @@ def visit_module(self, name):
class NestedDirective(Directive):
- def run(self):
+ def run(self) -> Sequence[nodes.Node]:
raise NotImplementedError
- def do_parse(self, rstlist, node):
+ def do_parse(self, rstlist: StringList, node: nodes.Node) -> None:
"""
Parse rST source lines and add them to the specified node
@@ -93,15 +103,15 @@ class QAPIDocDirective(NestedDirective):
}
has_content = False
- def new_serialno(self):
+ def new_serialno(self) -> str:
"""Return a unique new ID string suitable for use as a node's ID"""
env = self.state.document.settings.env
return "qapidoc-%d" % env.new_serialno("qapidoc")
- def transmogrify(self, schema) -> nodes.Element:
+ def transmogrify(self, schema: QAPISchema) -> nodes.Element:
raise NotImplementedError
- def legacy(self, schema) -> nodes.Element:
+ def legacy(self, schema: QAPISchema) -> nodes.Element:
vis = QAPISchemaGenRSTVisitor(self)
vis.visit_begin(schema)
for doc in schema.docs:
@@ -109,9 +119,9 @@ def legacy(self, schema) -> nodes.Element:
vis.symbol(doc, schema.lookup_entity(doc.symbol))
else:
vis.freeform(doc)
- return vis.get_document_node()
+ return vis.get_document_node() # type: ignore
- def run(self):
+ def run(self) -> Sequence[nodes.Node]:
env = self.state.document.settings.env
qapifile = env.config.qapidoc_srctree + "/" + self.arguments[0]
qapidir = os.path.dirname(qapifile)
@@ -185,7 +195,7 @@ def _highlightlang(self) -> addnodes.highlightlang:
)
return node
- def admonition_wrap(self, *content) -> List[nodes.Node]:
+ def admonition_wrap(self, *content: nodes.Node) -> List[nodes.Node]:
title = "Example:"
if "title" in self.options:
title = f"{title} {self.options['title']}"
@@ -231,7 +241,7 @@ def run(self) -> List[nodes.Node]:
return self.admonition_wrap(*content_nodes)
-def setup(app):
+def setup(app: Sphinx) -> ExtensionMetadata:
"""Register qapi-doc directive with Sphinx"""
app.add_config_value("qapidoc_srctree", None, "env")
app.add_directive("qapi-doc", QAPIDocDirective)
diff --git a/docs/sphinx/qapidoc_legacy.py b/docs/sphinx/qapidoc_legacy.py
index 679f38356b1..13520f4c26b 100644
--- a/docs/sphinx/qapidoc_legacy.py
+++ b/docs/sphinx/qapidoc_legacy.py
@@ -1,4 +1,5 @@
# coding=utf-8
+# type: ignore
#
# QEMU qapidoc QAPI file parsing extension
#
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 38/63] do-not-merge
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (36 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 37/63] docs/qapidoc: Fix static typing on qapidoc.py John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 39/63] docs/qapidoc: add transmogrifier class stub John Snow
` (25 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Add strict typing to qapidoc.py for the remainder of this series.
Signed-off-by: John Snow <jsnow@redhat.com>
---
scripts/qapi-lint.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/qapi-lint.sh b/scripts/qapi-lint.sh
index 7534ab0df98..cbad92c15b4 100755
--- a/scripts/qapi-lint.sh
+++ b/scripts/qapi-lint.sh
@@ -35,7 +35,7 @@ if [[ -f ../docs/sphinx/qapi_domain.py ]]; then
pushd ../docs/sphinx
set -x
- mypy --strict $files
+ PYTHONPATH=../../scripts/ mypy --follow-untyped-imports --strict $files qapidoc.py
flake8 --max-line-length=79 $files qapidoc.py
isort -c $files qapidoc.py
black --line-length 79 --check $files qapidoc.py
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 39/63] docs/qapidoc: add transmogrifier class stub
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (37 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 38/63] do-not-merge John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 40/63] docs/qapidoc: add visit_module() method John Snow
` (24 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Add the beginnings of the Transmogrifier class by adding the rST
conversion helpers that will be used to build the virtual rST document.
This version of the class does not actually "do anything" yet; each
individual feature is added one-at-a-time in the forthcoming commits.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 73 ++++++++++++++++++++++++++++++++++++++++--
1 file changed, 70 insertions(+), 3 deletions(-)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index 5246832b68c..c243bb6faaa 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -26,14 +26,17 @@
from __future__ import annotations
+from contextlib import contextmanager
import os
import sys
from typing import TYPE_CHECKING
from docutils import nodes
from docutils.parsers.rst import Directive, directives
+from docutils.statemachine import StringList
from qapi.error import QAPIError
from qapi.schema import QAPISchema, QAPISchemaVisitor
+from qapi.source import QAPISourceInfo
from qapidoc_legacy import QAPISchemaGenRSTVisitor # type: ignore
from sphinx import addnodes
@@ -44,9 +47,12 @@
if TYPE_CHECKING:
- from typing import Any, List, Sequence
-
- from docutils.statemachine import StringList
+ from typing import (
+ Any,
+ Generator,
+ List,
+ Sequence,
+ )
from sphinx.application import Sphinx
from sphinx.util.typing import ExtensionMetadata
@@ -55,6 +61,67 @@
__version__ = "1.0"
+class Transmogrifier:
+ def __init__(self) -> None:
+ self._result = StringList()
+ self.indent = 0
+
+ # General-purpose rST generation functions
+
+ def get_indent(self) -> str:
+ return " " * self.indent
+
+ @contextmanager
+ def indented(self) -> Generator[None]:
+ self.indent += 1
+ try:
+ yield
+ finally:
+ self.indent -= 1
+
+ def add_line_raw(self, line: str, source: str, *lineno: int) -> None:
+ """Append one line of generated reST to the output."""
+
+ # NB: Sphinx uses zero-indexed lines; subtract one.
+ lineno = tuple((n - 1 for n in lineno))
+
+ if line.strip():
+ # not a blank line
+ self._result.append(
+ self.get_indent() + line.rstrip("\n"), source, *lineno
+ )
+ else:
+ self._result.append("", source, *lineno)
+
+ def add_line(self, content: str, info: QAPISourceInfo) -> None:
+ # NB: We *require* an info object; this works out OK because we
+ # don't document built-in objects that don't have
+ # one. Everything else should.
+ self.add_line_raw(content, info.fname, info.line)
+
+ def add_lines(
+ self,
+ content: str,
+ info: QAPISourceInfo,
+ ) -> None:
+ lines = content.splitlines(True)
+ for i, line in enumerate(lines):
+ self.add_line_raw(line, info.fname, info.line + i)
+
+ def ensure_blank_line(self) -> None:
+ # Empty document -- no blank line required.
+ if not self._result:
+ return
+
+ # Last line isn't blank, add one.
+ if self._result[-1].strip(): # pylint: disable=no-member
+ fname, line = self._result.info(-1)
+ assert isinstance(line, int)
+ # New blank line is credited to one-after the current last line.
+ # +2: correct for zero/one index, then increment by one.
+ self.add_line_raw("", fname, line + 2)
+
+
class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
"""A QAPI schema visitor which adds Sphinx dependencies each module
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 40/63] docs/qapidoc: add visit_module() method
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (38 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 39/63] docs/qapidoc: add transmogrifier class stub John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 41/63] qapi/source: allow multi-line QAPISourceInfo advancing John Snow
` (23 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This method annotates the start of a new module, crediting the source
location to the first line of the module file.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index c243bb6faaa..6de8c900543 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -28,6 +28,7 @@
from contextlib import contextmanager
import os
+from pathlib import Path
import sys
from typing import TYPE_CHECKING
@@ -121,6 +122,14 @@ def ensure_blank_line(self) -> None:
# +2: correct for zero/one index, then increment by one.
self.add_line_raw("", fname, line + 2)
+ # Transmogrification core methods
+
+ def visit_module(self, path: str) -> None:
+ name = Path(path).stem
+ # module directives are credited to the first line of a module file.
+ self.add_line_raw(f".. qapi:module:: {name}", path, 1)
+ self.ensure_blank_line()
+
class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
"""A QAPI schema visitor which adds Sphinx dependencies each module
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 41/63] qapi/source: allow multi-line QAPISourceInfo advancing
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (39 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 40/63] docs/qapidoc: add visit_module() method John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 42/63] docs/qapidoc: add visit_freeform() method John Snow
` (22 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This is for the sake of the new rST generator (the "transmogrifier") so
we can advance multiple lines on occasion while keeping the
generated<-->source mappings accurate.
next_line now simply takes an optional n parameter which chooses the
number of lines to advance.
The next patch will use this when converting section syntax in free-form
documentation to more traditional rST section header syntax, which does
not always line up 1:1 for line counts.
For example:
```
##
# = Section <-- Info is pointing here, "L1"
#
# Lorem Ipsum
##
```
would be transformed to rST as:
```
======= <-- L1
Section <-- L1
======= <-- L1
<-- L2
Lorem Ipsum <-- L3
```
After consuming the single "Section" line from the source, we want to
advance the source pointer to the next non-empty line which requires
jumping by more than one line.
Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
---
scripts/qapi/source.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/scripts/qapi/source.py b/scripts/qapi/source.py
index 7b379fdc925..ffdc3f482ac 100644
--- a/scripts/qapi/source.py
+++ b/scripts/qapi/source.py
@@ -47,9 +47,9 @@ def set_defn(self, meta: str, name: str) -> None:
self.defn_meta = meta
self.defn_name = name
- def next_line(self: T) -> T:
+ def next_line(self: T, n: int = 1) -> T:
info = copy.copy(self)
- info.line += 1
+ info.line += n
return info
def loc(self) -> str:
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 42/63] docs/qapidoc: add visit_freeform() method
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (40 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 41/63] qapi/source: allow multi-line QAPISourceInfo advancing John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 43/63] docs/qapidoc: add preamble() method John Snow
` (21 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Add the transmogrifier implementation for converting freeform doc blocks
to rST.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 44 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 44 insertions(+)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index 6de8c900543..ddad6041455 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -29,6 +29,7 @@
from contextlib import contextmanager
import os
from pathlib import Path
+import re
import sys
from typing import TYPE_CHECKING
@@ -55,6 +56,8 @@
Sequence,
)
+ from qapi.parser import QAPIDoc
+
from sphinx.application import Sphinx
from sphinx.util.typing import ExtensionMetadata
@@ -130,6 +133,47 @@ def visit_module(self, path: str) -> None:
self.add_line_raw(f".. qapi:module:: {name}", path, 1)
self.ensure_blank_line()
+ def visit_freeform(self, doc: QAPIDoc) -> None:
+ # TODO: Once the old qapidoc transformer is deprecated, freeform
+ # sections can be updated to pure rST, and this transformed removed.
+ #
+ # For now, translate our micro-format into rST. Code adapted
+ # from Peter Maydell's freeform().
+
+ assert len(doc.all_sections) == 1, doc.all_sections
+ body = doc.all_sections[0]
+ text = body.text
+ info = doc.info
+
+ if re.match(r"=+ ", text):
+ # Section/subsection heading (if present, will always be the
+ # first line of the block)
+ (heading, _, text) = text.partition("\n")
+ (leader, _, heading) = heading.partition(" ")
+ # Implicit +1 for heading in the containing .rst doc
+ level = len(leader) + 1
+
+ # https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#sections
+ markers = ' #*=_^"'
+ overline = level <= 2
+ marker = markers[level]
+
+ self.ensure_blank_line()
+ # This credits all 2 or 3 lines to the single source line.
+ if overline:
+ self.add_line(marker * len(heading), info)
+ self.add_line(heading, info)
+ self.add_line(marker * len(heading), info)
+ self.ensure_blank_line()
+
+ # Eat blank line(s) and advance info
+ trimmed = text.lstrip("\n")
+ text = trimmed
+ info = info.next_line(len(text) - len(trimmed) + 1)
+
+ self.add_lines(text, info)
+ self.ensure_blank_line()
+
class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
"""A QAPI schema visitor which adds Sphinx dependencies each module
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 43/63] docs/qapidoc: add preamble() method
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (41 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 42/63] docs/qapidoc: add visit_freeform() method John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 44/63] docs/qapidoc: add visit_paragraph() method John Snow
` (20 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This method adds the options/preamble to each definition block. Notably,
:since: and :ifcond: are added, as are any "special features" such as
:deprecated: and :unstable:.
If conditionals, if attached to special features, are currently
unhandled in this patch and will be addressed at a future date. We
currently do not have any if conditionals attached to special features.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 41 ++++++++++++++++++++++++++++++++++++++---
1 file changed, 38 insertions(+), 3 deletions(-)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index ddad6041455..f56aa6d1fd7 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -37,7 +37,12 @@
from docutils.parsers.rst import Directive, directives
from docutils.statemachine import StringList
from qapi.error import QAPIError
-from qapi.schema import QAPISchema, QAPISchemaVisitor
+from qapi.parser import QAPIDoc
+from qapi.schema import (
+ QAPISchema,
+ QAPISchemaDefinition,
+ QAPISchemaVisitor,
+)
from qapi.source import QAPISourceInfo
from qapidoc_legacy import QAPISchemaGenRSTVisitor # type: ignore
@@ -56,8 +61,6 @@
Sequence,
)
- from qapi.parser import QAPIDoc
-
from sphinx.application import Sphinx
from sphinx.util.typing import ExtensionMetadata
@@ -125,6 +128,38 @@ def ensure_blank_line(self) -> None:
# +2: correct for zero/one index, then increment by one.
self.add_line_raw("", fname, line + 2)
+ # Transmogrification helpers
+
+ def preamble(self, ent: QAPISchemaDefinition) -> None:
+ """
+ Generate option lines for QAPI entity directives.
+ """
+ if ent.doc and ent.doc.since:
+ assert ent.doc.since.kind == QAPIDoc.Kind.SINCE
+ # Generated from the entity's docblock; info location is exact.
+ self.add_line(f":since: {ent.doc.since.text}", ent.doc.since.info)
+
+ if ent.ifcond.is_present():
+ doc = ent.ifcond.docgen()
+ assert ent.info
+ # Generated from entity definition; info location is approximate.
+ self.add_line(f":ifcond: {doc}", ent.info)
+
+ # Hoist special features such as :deprecated: and :unstable:
+ # into the options block for the entity. If, in the future, new
+ # special features are added, qapi-domain will chirp about
+ # unrecognized options and fail until they are handled in
+ # qapi-domain.
+ for feat in ent.features:
+ if feat.is_special():
+ # FIXME: handle ifcond if present. How to display that
+ # information is TBD.
+ # Generated from entity def; info location is approximate.
+ assert feat.info
+ self.add_line(f":{feat.name}:", feat.info)
+
+ self.ensure_blank_line()
+
# Transmogrification core methods
def visit_module(self, path: str) -> None:
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 44/63] docs/qapidoc: add visit_paragraph() method
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (42 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 43/63] docs/qapidoc: add preamble() method John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 45/63] docs/qapidoc: add visit_errors() method John Snow
` (19 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This transforms "formerly known as untagged sections" into our pure
intermediate rST format. These sections are already pure rST, so this
method doesn't do a whole lot except ensure appropriate newlines.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index f56aa6d1fd7..a9f98d46571 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -130,6 +130,15 @@ def ensure_blank_line(self) -> None:
# Transmogrification helpers
+ def visit_paragraph(self, section: QAPIDoc.Section) -> None:
+ # Squelch empty paragraphs.
+ if not section.text:
+ return
+
+ self.ensure_blank_line()
+ self.add_lines(section.text, section.info)
+ self.ensure_blank_line()
+
def preamble(self, ent: QAPISchemaDefinition) -> None:
"""
Generate option lines for QAPI entity directives.
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 45/63] docs/qapidoc: add visit_errors() method
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (43 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 44/63] docs/qapidoc: add visit_paragraph() method John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 46/63] docs/qapidoc: add format_type() method John Snow
` (18 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Notably, this method does not currently address the formatting issues
present with the "errors" section in QAPIDoc and just vomits the text
verbatim into the rST doc, with somewhat inconsistent results.
To be addressed in a future patch.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index a9f98d46571..c17cb9f9b16 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -139,6 +139,12 @@ def visit_paragraph(self, section: QAPIDoc.Section) -> None:
self.add_lines(section.text, section.info)
self.ensure_blank_line()
+ def visit_errors(self, section: QAPIDoc.Section) -> None:
+ # FIXME: the formatting for errors may be inconsistent and may
+ # or may not require different newline placement to ensure
+ # proper rendering as a nested list.
+ self.add_lines(f":error:\n{section.text}", section.info)
+
def preamble(self, ent: QAPISchemaDefinition) -> None:
"""
Generate option lines for QAPI entity directives.
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 46/63] docs/qapidoc: add format_type() method
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (44 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 45/63] docs/qapidoc: add visit_errors() method John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 47/63] docs/qapidoc: add add_field() and generate_field() helper methods John Snow
` (17 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This method is responsible for generating a type name for a given member
with the correct annotations for the QAPI domain. Features and enums do
not *have* types, so they return None. Everything else returns the type
name with a "?" suffix if that type is optional, and ensconced in
[brackets] if it's an array type.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 32 ++++++++++++++++++++++++++++++++
1 file changed, 32 insertions(+)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index c17cb9f9b16..5144bb965af 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -40,7 +40,13 @@
from qapi.parser import QAPIDoc
from qapi.schema import (
QAPISchema,
+ QAPISchemaArrayType,
QAPISchemaDefinition,
+ QAPISchemaEnumMember,
+ QAPISchemaFeature,
+ QAPISchemaMember,
+ QAPISchemaObjectTypeMember,
+ QAPISchemaType,
QAPISchemaVisitor,
)
from qapi.source import QAPISourceInfo
@@ -58,7 +64,9 @@
Any,
Generator,
List,
+ Optional,
Sequence,
+ Union,
)
from sphinx.application import Sphinx
@@ -128,6 +136,30 @@ def ensure_blank_line(self) -> None:
# +2: correct for zero/one index, then increment by one.
self.add_line_raw("", fname, line + 2)
+ def format_type(
+ self, ent: Union[QAPISchemaDefinition | QAPISchemaMember]
+ ) -> Optional[str]:
+ if isinstance(ent, (QAPISchemaEnumMember, QAPISchemaFeature)):
+ return None
+
+ qapi_type = ent
+ optional = False
+ if isinstance(ent, QAPISchemaObjectTypeMember):
+ qapi_type = ent.type
+ optional = ent.optional
+
+ if isinstance(qapi_type, QAPISchemaArrayType):
+ ret = f"[{qapi_type.element_type.doc_type()}]"
+ else:
+ assert isinstance(qapi_type, QAPISchemaType)
+ tmp = qapi_type.doc_type()
+ assert tmp
+ ret = tmp
+ if optional:
+ ret += "?"
+
+ return ret
+
# Transmogrification helpers
def visit_paragraph(self, section: QAPIDoc.Section) -> None:
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 47/63] docs/qapidoc: add add_field() and generate_field() helper methods
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (45 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 46/63] docs/qapidoc: add format_type() method John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 48/63] docs/qapidoc: add visit_feature() method John Snow
` (16 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
These are simple rST generation methods that assist in getting the types
and formatting correct for a field list entry. add_field() is a more
raw, direct call while generate_field() is intended to be used for
generating the correct field from a member object.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index 5144bb965af..2f85fe0bc3e 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -136,6 +136,20 @@ def ensure_blank_line(self) -> None:
# +2: correct for zero/one index, then increment by one.
self.add_line_raw("", fname, line + 2)
+ def add_field(
+ self,
+ kind: str,
+ name: str,
+ body: str,
+ info: QAPISourceInfo,
+ typ: Optional[str] = None,
+ ) -> None:
+ if typ:
+ text = f":{kind} {typ} {name}: {body}"
+ else:
+ text = f":{kind} {name}: {body}"
+ self.add_lines(text, info)
+
def format_type(
self, ent: Union[QAPISchemaDefinition | QAPISchemaMember]
) -> Optional[str]:
@@ -160,6 +174,16 @@ def format_type(
return ret
+ def generate_field(
+ self,
+ kind: str,
+ member: QAPISchemaMember,
+ body: str,
+ info: QAPISourceInfo,
+ ) -> None:
+ typ = self.format_type(member)
+ self.add_field(kind, member.name, body, info, typ)
+
# Transmogrification helpers
def visit_paragraph(self, section: QAPIDoc.Section) -> None:
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 48/63] docs/qapidoc: add visit_feature() method
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (46 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 47/63] docs/qapidoc: add add_field() and generate_field() helper methods John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 49/63] docs/qapidoc: prepare to record entity being transmogrified John Snow
` (15 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This adds a simple ":feat name: lorem ipsum ..." line to the generated
rST document, so at the moment it's only for "top level" features.
Features not attached directly to a QAPI definition are not currently
handled! This is a small regression over the prior documentation
generator that will be addressed in a future patch.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index 2f85fe0bc3e..208d7ca1446 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -195,6 +195,15 @@ def visit_paragraph(self, section: QAPIDoc.Section) -> None:
self.add_lines(section.text, section.info)
self.ensure_blank_line()
+ def visit_feature(self, section: QAPIDoc.ArgSection) -> None:
+ # FIXME - ifcond for features is not handled at all yet!
+ # Proposal: decorate the right-hand column with some graphical
+ # element to indicate conditional availability?
+ assert section.text # Guaranteed by parser.py
+ assert section.member
+
+ self.generate_field("feat", section.member, section.text, section.info)
+
def visit_errors(self, section: QAPIDoc.Section) -> None:
# FIXME: the formatting for errors may be inconsistent and may
# or may not require different newline placement to ensure
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 49/63] docs/qapidoc: prepare to record entity being transmogrified
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (47 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 48/63] docs/qapidoc: add visit_feature() method John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 50/63] docs/qapidoc: add visit_returns() method John Snow
` (14 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Prepare to keep a record of which entity we're working on documenting
for the purposes of being able to change certain generative features
conditionally and create stronger assertions.
If you find yourself asking: "Wait, but where does the current entity
actually get recorded?!", you're right! That part comes with the
visit_entity() implementation, which gets added later.
This patch is front-loaded for the sake of type checking in the
forthcoming commits before visit_entity() is ready to be added.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index 208d7ca1446..47c2eeef871 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -78,9 +78,15 @@
class Transmogrifier:
def __init__(self) -> None:
+ self._curr_ent: Optional[QAPISchemaDefinition] = None
self._result = StringList()
self.indent = 0
+ @property
+ def entity(self) -> QAPISchemaDefinition:
+ assert self._curr_ent is not None
+ return self._curr_ent
+
# General-purpose rST generation functions
def get_indent(self) -> str:
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 50/63] docs/qapidoc: add visit_returns() method
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (48 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 49/63] docs/qapidoc: prepare to record entity being transmogrified John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 51/63] docs/qapidoc: add visit_member() method John Snow
` (13 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Generates :return: fields for explicit returns statements. Note that
this does not presently handle undocumented returns, which is handled in
a later commit.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index 47c2eeef871..eb8841099c7 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -41,6 +41,7 @@
from qapi.schema import (
QAPISchema,
QAPISchemaArrayType,
+ QAPISchemaCommand,
QAPISchemaDefinition,
QAPISchemaEnumMember,
QAPISchemaFeature,
@@ -210,6 +211,20 @@ def visit_feature(self, section: QAPIDoc.ArgSection) -> None:
self.generate_field("feat", section.member, section.text, section.info)
+ def visit_returns(self, section: QAPIDoc.Section) -> None:
+ assert isinstance(self.entity, QAPISchemaCommand)
+ rtype = self.entity.ret_type
+ # q_empty can produce None, but we won't be documenting anything
+ # without an explicit return statement in the doc block, and we
+ # should not have any such explicit statements when there is no
+ # return value.
+ assert rtype
+
+ typ = self.format_type(rtype)
+ assert typ
+ assert section.text
+ self.add_field("return", typ, section.text, section.info)
+
def visit_errors(self, section: QAPIDoc.Section) -> None:
# FIXME: the formatting for errors may be inconsistent and may
# or may not require different newline placement to ensure
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 51/63] docs/qapidoc: add visit_member() method
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (49 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 50/63] docs/qapidoc: add visit_returns() method John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 52/63] docs/qapidoc: add visit_sections() method John Snow
` (12 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This method is used for generating the "members" of a wide variety of
things, including structs, unions, enums, alternates, etc. The field
name it uses to do so is dependent on the type of entity the "member"
belongs to.
Currently, IF conditionals for individual members are not handled or
rendered, a small regression from the prior documentation
generator. This will be fixed in a future patch.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index eb8841099c7..a8e19487d0a 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -78,6 +78,16 @@
class Transmogrifier:
+ # Field names used for different entity types:
+ field_types = {
+ "enum": "value",
+ "struct": "memb",
+ "union": "memb",
+ "event": "memb",
+ "command": "arg",
+ "alternate": "alt",
+ }
+
def __init__(self) -> None:
self._curr_ent: Optional[QAPISchemaDefinition] = None
self._result = StringList()
@@ -88,6 +98,10 @@ def entity(self) -> QAPISchemaDefinition:
assert self._curr_ent is not None
return self._curr_ent
+ @property
+ def member_field_type(self) -> str:
+ return self.field_types[self.entity.meta]
+
# General-purpose rST generation functions
def get_indent(self) -> str:
@@ -202,6 +216,19 @@ def visit_paragraph(self, section: QAPIDoc.Section) -> None:
self.add_lines(section.text, section.info)
self.ensure_blank_line()
+ def visit_member(self, section: QAPIDoc.ArgSection) -> None:
+ # FIXME: ifcond for members
+ # TODO: features for members (documented at entity-level,
+ # but sometimes defined per-member. Should we add such
+ # information to member descriptions when we can?)
+ assert section.text and section.member
+ self.generate_field(
+ self.member_field_type,
+ section.member,
+ section.text,
+ section.info,
+ )
+
def visit_feature(self, section: QAPIDoc.ArgSection) -> None:
# FIXME - ifcond for features is not handled at all yet!
# Proposal: decorate the right-hand column with some graphical
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 52/63] docs/qapidoc: add visit_sections() method
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (50 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 51/63] docs/qapidoc: add visit_member() method John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 8:34 ` Markus Armbruster
2025-03-11 3:42 ` [PATCH v3 53/63] docs/qapidoc: add visit_entity() John Snow
` (11 subsequent siblings)
63 siblings, 1 reply; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Implement the actual main dispatch method that processes and handles the
list of doc sections for a given QAPI entity.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 25 +++++++++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index a8e19487d0a..83022b15ca2 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -288,6 +288,31 @@ def preamble(self, ent: QAPISchemaDefinition) -> None:
self.ensure_blank_line()
+ def visit_sections(self, ent: QAPISchemaDefinition) -> None:
+ sections = ent.doc.all_sections if ent.doc else []
+
+ # Add sections in source order:
+ for section in sections:
+ if section.kind == QAPIDoc.Kind.PLAIN:
+ self.visit_paragraph(section)
+ elif section.kind == QAPIDoc.Kind.MEMBER:
+ assert isinstance(section, QAPIDoc.ArgSection)
+ self.visit_member(section)
+ elif section.kind == QAPIDoc.Kind.FEATURE:
+ assert isinstance(section, QAPIDoc.ArgSection)
+ self.visit_feature(section)
+ elif section.kind in (QAPIDoc.Kind.SINCE, QAPIDoc.Kind.TODO):
+ # Since is handled in preamble, TODO is skipped intentionally.
+ pass
+ elif section.kind == QAPIDoc.Kind.RETURNS:
+ self.visit_returns(section)
+ elif section.kind == QAPIDoc.Kind.ERRORS:
+ self.visit_errors(section)
+ else:
+ assert False
+
+ self.ensure_blank_line()
+
# Transmogrification core methods
def visit_module(self, path: str) -> None:
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 53/63] docs/qapidoc: add visit_entity()
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (51 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 52/63] docs/qapidoc: add visit_sections() method John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 54/63] docs/qapidoc: implement transmogrify() method John Snow
` (10 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Finally, the core entry method for a qapi entity.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index 83022b15ca2..aaf5b6e22bc 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -78,6 +78,8 @@
class Transmogrifier:
+ # pylint: disable=too-many-public-methods
+
# Field names used for different entity types:
field_types = {
"enum": "value",
@@ -362,6 +364,25 @@ def visit_freeform(self, doc: QAPIDoc) -> None:
self.add_lines(text, info)
self.ensure_blank_line()
+ def visit_entity(self, ent: QAPISchemaDefinition) -> None:
+ assert ent.info
+
+ try:
+ self._curr_ent = ent
+
+ # Squish structs and unions together into an "object" directive.
+ meta = ent.meta
+ if meta in ("struct", "union"):
+ meta = "object"
+
+ # This line gets credited to the start of the /definition/.
+ self.add_line(f".. qapi:{meta}:: {ent.name}", ent.info)
+ with self.indented():
+ self.preamble(ent)
+ self.visit_sections(ent)
+ finally:
+ self._curr_ent = None
+
class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
"""A QAPI schema visitor which adds Sphinx dependencies each module
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 54/63] docs/qapidoc: implement transmogrify() method
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (52 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 53/63] docs/qapidoc: add visit_entity() John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 8:52 ` Markus Armbruster
2025-03-11 3:42 ` [PATCH v3 55/63] docs/qapidoc: process @foo into ``foo`` John Snow
` (9 subsequent siblings)
63 siblings, 1 reply; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This is the true top-level processor for the new transmogrifier;
responsible both for generating the intermediate rST and then running
the nested parse on that generated document to produce the final
docutils tree that is then - very finally - postprocessed by sphinx for
final rendering to HTML &c.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 49 ++++++++++++++++++++++++++++++++++++++++--
1 file changed, 47 insertions(+), 2 deletions(-)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index aaf5b6e22bc..32baf66a390 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -2,6 +2,7 @@
#
# QEMU qapidoc QAPI file parsing extension
#
+# Copyright (c) 2024-2025 Red Hat
# Copyright (c) 2020 Linaro
#
# This work is licensed under the terms of the GNU GPLv2 or later.
@@ -56,6 +57,7 @@
from sphinx import addnodes
from sphinx.directives.code import CodeBlock
from sphinx.errors import ExtensionError
+from sphinx.util import logging
from sphinx.util.docutils import switch_source_input
from sphinx.util.nodes import nested_parse_with_titles
@@ -74,7 +76,9 @@
from sphinx.util.typing import ExtensionMetadata
-__version__ = "1.0"
+__version__ = "2.0"
+
+logger = logging.getLogger(__name__)
class Transmogrifier:
@@ -95,6 +99,10 @@ def __init__(self) -> None:
self._result = StringList()
self.indent = 0
+ @property
+ def result(self) -> StringList:
+ return self._result
+
@property
def entity(self) -> QAPISchemaDefinition:
assert self._curr_ent is not None
@@ -438,7 +446,43 @@ def new_serialno(self) -> str:
return "qapidoc-%d" % env.new_serialno("qapidoc")
def transmogrify(self, schema: QAPISchema) -> nodes.Element:
- raise NotImplementedError
+ logger.info("Transmogrifying QAPI to rST ...")
+ vis = Transmogrifier()
+ modules = set()
+
+ for doc in schema.docs:
+ module_source = doc.info.fname
+ if module_source not in modules:
+ vis.visit_module(module_source)
+ modules.add(module_source)
+
+ if doc.symbol:
+ ent = schema.lookup_entity(doc.symbol)
+ assert isinstance(ent, QAPISchemaDefinition)
+ vis.visit_entity(ent)
+ else:
+ vis.visit_freeform(doc)
+
+ logger.info("Transmogrification complete.")
+
+ contentnode = nodes.section()
+ content = vis.result
+ titles_allowed = True
+
+ logger.info("Transmogrifier running nested parse ...")
+ with switch_source_input(self.state, content):
+ if titles_allowed:
+ node: nodes.Element = nodes.section()
+ node.document = self.state.document
+ nested_parse_with_titles(self.state, content, contentnode)
+ else:
+ node = nodes.paragraph()
+ node.document = self.state.document
+ self.state.nested_parse(content, 0, contentnode)
+ logger.info("Transmogrifier's nested parse completed.")
+ sys.stdout.flush()
+
+ return contentnode
def legacy(self, schema: QAPISchema) -> nodes.Element:
vis = QAPISchemaGenRSTVisitor(self)
@@ -572,6 +616,7 @@ def run(self) -> List[nodes.Node]:
def setup(app: Sphinx) -> ExtensionMetadata:
"""Register qapi-doc directive with Sphinx"""
+ app.setup_extension("qapi_domain")
app.add_config_value("qapidoc_srctree", None, "env")
app.add_directive("qapi-doc", QAPIDocDirective)
app.add_directive("qmp-example", QMPExample)
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 55/63] docs/qapidoc: process @foo into ``foo``
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (53 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 54/63] docs/qapidoc: implement transmogrify() method John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 56/63] docs/qapidoc: add intermediate output debugger John Snow
` (8 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Add support for the special QAPI doc syntax to process @references as
``preformatted text``. At the moment, there are no actual
cross-references for individual members, so there is nothing to link
against. For now, process it identically to how we did in the old
qapidoc system.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index 32baf66a390..f98f75cb2f8 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -303,6 +303,9 @@ def visit_sections(self, ent: QAPISchemaDefinition) -> None:
# Add sections in source order:
for section in sections:
+ # @var is translated to ``var``:
+ section.text = re.sub(r"@([\w-]+)", r"``\1``", section.text)
+
if section.kind == QAPIDoc.Kind.PLAIN:
self.visit_paragraph(section)
elif section.kind == QAPIDoc.Kind.MEMBER:
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 56/63] docs/qapidoc: add intermediate output debugger
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (54 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 55/63] docs/qapidoc: process @foo into ``foo`` John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 57/63] docs/qapidoc: Add "the members of" pointers John Snow
` (7 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Add debugging output for the qapidoc transmogrifier - setting DEBUG=1
will produce .ir files (one for each qapidoc directive) that write the
generated rst file to disk to allow for easy debugging and verification
of the generated document.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 41 +++++++++++++++++++++++++++++++++++++----
1 file changed, 37 insertions(+), 4 deletions(-)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index f98f75cb2f8..89a63d18448 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -35,7 +35,7 @@
from typing import TYPE_CHECKING
from docutils import nodes
-from docutils.parsers.rst import Directive, directives
+from docutils.parsers.rst import directives
from docutils.statemachine import StringList
from qapi.error import QAPIError
from qapi.parser import QAPIDoc
@@ -58,7 +58,7 @@
from sphinx.directives.code import CodeBlock
from sphinx.errors import ExtensionError
from sphinx.util import logging
-from sphinx.util.docutils import switch_source_input
+from sphinx.util.docutils import SphinxDirective, switch_source_input
from sphinx.util.nodes import nested_parse_with_titles
@@ -414,7 +414,7 @@ def visit_module(self, name: str) -> None:
super().visit_module(name)
-class NestedDirective(Directive):
+class NestedDirective(SphinxDirective):
def run(self) -> Sequence[nodes.Node]:
raise NotImplementedError
@@ -483,10 +483,43 @@ def transmogrify(self, schema: QAPISchema) -> nodes.Element:
node.document = self.state.document
self.state.nested_parse(content, 0, contentnode)
logger.info("Transmogrifier's nested parse completed.")
+
+ if self.env.app.verbosity >= 2 or os.environ.get("DEBUG"):
+ argname = "_".join(Path(self.arguments[0]).parts)
+ name = Path(argname).stem + ".ir"
+ self.write_intermediate(content, name)
+
sys.stdout.flush()
-
return contentnode
+ def write_intermediate(self, content: StringList, filename: str) -> None:
+ logger.info(
+ "writing intermediate rST for '%s' to '%s'",
+ self.arguments[0],
+ filename,
+ )
+
+ srctree = Path(self.env.app.config.qapidoc_srctree).resolve()
+ outlines = []
+ lcol_width = 0
+
+ for i, line in enumerate(content):
+ src, lineno = content.info(i)
+ srcpath = Path(src).resolve()
+ srcpath = srcpath.relative_to(srctree)
+
+ lcol = f"{srcpath}:{lineno:04d}"
+ lcol_width = max(lcol_width, len(lcol))
+ outlines.append((lcol, line))
+
+ with open(filename, "w", encoding="UTF-8") as outfile:
+ for lcol, rcol in outlines:
+ outfile.write(lcol.rjust(lcol_width))
+ outfile.write(" |")
+ if rcol:
+ outfile.write(f" {rcol}")
+ outfile.write("\n")
+
def legacy(self, schema: QAPISchema) -> nodes.Element:
vis = QAPISchemaGenRSTVisitor(self)
vis.visit_begin(schema)
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 57/63] docs/qapidoc: Add "the members of" pointers
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (55 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 56/63] docs/qapidoc: add intermediate output debugger John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 58/63] docs/qapidoc: generate entries for undocumented members John Snow
` (6 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Add "the members of ..." pointers to Members and Arguments lists where
appropriate, with clickable cross-references - so it's a slight
improvement over the old system :)
This patch is meant to be a temporary solution until we can review and
merge the inliner.
The implementation of this patch is a little bit of a hack: Sphinx is
not designed to allow you to mix fields of different "type"; i.e. mixing
member descriptions and free-form text under the same heading. To
accomplish this with a minimum of hackery, we technically document a
"dummy field" and then just strip off the documentation for that dummy
field in a post-processing step. We use the "q_dummy" variable for this
purpose, then strip it back out before final processing. If this
processing step should fail, you'll see warnings for a bad
cross-reference. (So if you don't see any, it must be working!)
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapi_domain.py | 22 +++++++++++++--
docs/sphinx/qapidoc.py | 58 +++++++++++++++++++++++++++++++++++++-
2 files changed, 77 insertions(+), 3 deletions(-)
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
index ca3f3a7e2d5..7ff618d8cda 100644
--- a/docs/sphinx/qapi_domain.py
+++ b/docs/sphinx/qapi_domain.py
@@ -433,6 +433,24 @@ def transform_content(self, content_node: addnodes.desc_content) -> None:
self._validate_field(field)
+class SpecialTypedField(CompatTypedField):
+ def make_field(self, *args: Any, **kwargs: Any) -> nodes.field:
+ ret = super().make_field(*args, **kwargs)
+
+ # Look for the characteristic " -- " text node that Sphinx
+ # inserts for each TypedField entry ...
+ for node in ret.traverse(lambda n: str(n) == " -- "):
+ par = node.parent
+ if par.children[0].astext() != "q_dummy":
+ continue
+
+ # If the first node's text is q_dummy, this is a dummy
+ # field we want to strip down to just its contents.
+ del par.children[:-1]
+
+ return ret
+
+
class QAPICommand(QAPIObject):
"""Description of a QAPI Command."""
@@ -440,7 +458,7 @@ class QAPICommand(QAPIObject):
doc_field_types.extend(
[
# :arg TypeName ArgName: descr
- CompatTypedField(
+ SpecialTypedField(
"argument",
label=_("Arguments"),
names=("arg",),
@@ -508,7 +526,7 @@ class QAPIObjectWithMembers(QAPIObject):
doc_field_types.extend(
[
# :member type name: descr
- CompatTypedField(
+ SpecialTypedField(
"member",
label=_("Members"),
names=("memb",),
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index 89a63d18448..7c5a08958d5 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -45,8 +45,10 @@
QAPISchemaCommand,
QAPISchemaDefinition,
QAPISchemaEnumMember,
+ QAPISchemaEvent,
QAPISchemaFeature,
QAPISchemaMember,
+ QAPISchemaObjectType,
QAPISchemaObjectTypeMember,
QAPISchemaType,
QAPISchemaVisitor,
@@ -298,11 +300,61 @@ def preamble(self, ent: QAPISchemaDefinition) -> None:
self.ensure_blank_line()
+ def _insert_member_pointer(self, ent: QAPISchemaDefinition) -> None:
+
+ def _get_target(
+ ent: QAPISchemaDefinition,
+ ) -> Optional[QAPISchemaDefinition]:
+ if isinstance(ent, (QAPISchemaCommand, QAPISchemaEvent)):
+ return ent.arg_type
+ if isinstance(ent, QAPISchemaObjectType):
+ return ent.base
+ return None
+
+ target = _get_target(ent)
+ if target is not None and not target.is_implicit():
+ assert ent.info
+ self.add_field(
+ self.member_field_type,
+ "q_dummy",
+ f"The members of :qapi:type:`{target.name}`.",
+ ent.info,
+ "q_dummy",
+ )
+
+ if isinstance(ent, QAPISchemaObjectType) and ent.branches is not None:
+ for variant in ent.branches.variants:
+ if variant.type.name == "q_empty":
+ continue
+ assert ent.info
+ self.add_field(
+ self.member_field_type,
+ "q_dummy",
+ f" When ``{ent.branches.tag_member.name}`` is "
+ f"``{variant.name}``: "
+ f"The members of :qapi:type:`{variant.type.name}`.",
+ ent.info,
+ "q_dummy",
+ )
+
def visit_sections(self, ent: QAPISchemaDefinition) -> None:
sections = ent.doc.all_sections if ent.doc else []
+ # Determine the index location at which we should generate
+ # documentation for "The members of ..." pointers. This should
+ # go at the end of the members section(s) if any. Note that
+ # index 0 is assumed to be a plain intro section, even if it is
+ # empty; and that a members section if present will always
+ # immediately follow the opening PLAIN section.
+ gen_index = 1
+ if len(sections) > 1:
+ while sections[gen_index].kind == QAPIDoc.Kind.MEMBER:
+ gen_index += 1
+ if gen_index >= len(sections):
+ break
+
# Add sections in source order:
- for section in sections:
+ for i, section in enumerate(sections):
# @var is translated to ``var``:
section.text = re.sub(r"@([\w-]+)", r"``\1``", section.text)
@@ -324,6 +376,10 @@ def visit_sections(self, ent: QAPISchemaDefinition) -> None:
else:
assert False
+ # Generate "The members of ..." entries if necessary:
+ if i == gen_index - 1:
+ self._insert_member_pointer(ent)
+
self.ensure_blank_line()
# Transmogrification core methods
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 58/63] docs/qapidoc: generate entries for undocumented members
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (56 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 57/63] docs/qapidoc: Add "the members of" pointers John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 9:22 ` Markus Armbruster
2025-03-11 3:42 ` [PATCH v3 59/63] qapi/parser: add undocumented stub members to all_sections John Snow
` (5 subsequent siblings)
63 siblings, 1 reply; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Presently, we never have any empty text entries for members. The next
patch will explicitly generate such sections, so enable support for it
in advance.
The parser will generate placeholder sections to indicate undocumented
members, but it's the qapidoc generator that's responsible for deciding
what to do with that stub section.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/sphinx/qapidoc.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
index 7c5a08958d5..604ab109a19 100644
--- a/docs/sphinx/qapidoc.py
+++ b/docs/sphinx/qapidoc.py
@@ -233,11 +233,12 @@ def visit_member(self, section: QAPIDoc.ArgSection) -> None:
# TODO: features for members (documented at entity-level,
# but sometimes defined per-member. Should we add such
# information to member descriptions when we can?)
- assert section.text and section.member
+ assert section.member
self.generate_field(
self.member_field_type,
section.member,
- section.text,
+ # TODO drop fallbacks when undocumented members are outlawed
+ section.text if section.text else "Not Documented.",
section.info,
)
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 59/63] qapi/parser: add undocumented stub members to all_sections
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (57 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 58/63] docs/qapidoc: generate entries for undocumented members John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 7:04 ` Markus Armbruster
2025-03-11 3:42 ` [PATCH v3 60/63] docs: disambiguate cross-references John Snow
` (4 subsequent siblings)
63 siblings, 1 reply; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
This helps simplify the new doc generator if it doesn't have to check
for undocumented members, it can just blindly operate on a sequence of
QAPIDoc.Section instances.
NB: If there is no existing 'member' section, these undocumented stub
members will be inserted directly after the leading section.
Signed-off-by: John Snow <jsnow@redhat.com>
---
scripts/qapi/parser.py | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index 11c11bb09e5..52bc44facf2 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -789,8 +789,23 @@ 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(
+ section = QAPIDoc.ArgSection(
self.info, QAPIDoc.Kind.MEMBER, member.name)
+ self.args[member.name] = section
+
+ # Insert stub documentation section for missing member docs.
+ # TODO: drop when undocumented members are outlawed
+
+ # Determine where to insert stub doc - it should go at the
+ # end of the members section(s), if any. Note that index 0
+ # is assumed to be an untagged intro section, even if it is
+ # empty.
+ index = 1
+ if len(self.all_sections) > 1:
+ while self.all_sections[index].kind == QAPIDoc.Kind.MEMBER:
+ index += 1
+ self.all_sections.insert(index, section)
+
self.args[member.name].connect(member)
def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 60/63] docs: disambiguate cross-references
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (58 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 59/63] qapi/parser: add undocumented stub members to all_sections John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:42 ` [PATCH v3 61/63] docs: enable qapidoc transmogrifier for QEMU QMP Reference John Snow
` (3 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
The next patch will engage the qapidoc transmogrifier, which creates a
lot of cross-reference targets. Some of the existing targets
("migration", "qom", "replay") will become ambiguous as a result. Nail
them down more explicitly to prevent ambiguous cross-reference warnings.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/devel/codebase.rst | 6 +++---
docs/glossary.rst | 10 +++++-----
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/docs/devel/codebase.rst b/docs/devel/codebase.rst
index 4039875ee04..1b09953197b 100644
--- a/docs/devel/codebase.rst
+++ b/docs/devel/codebase.rst
@@ -23,7 +23,7 @@ Some of the main QEMU subsystems are:
- `Devices<device-emulation>` & Board models
- `Documentation <documentation-root>`
- `GDB support<GDB usage>`
-- `Migration<migration>`
+- :ref:`Migration<migration>`
- `Monitor<QEMU monitor>`
- :ref:`QOM (QEMU Object Model)<qom>`
- `System mode<System emulation>`
@@ -112,7 +112,7 @@ yet, so sometimes the source code is all you have.
* `libdecnumber <https://gitlab.com/qemu-project/qemu/-/tree/master/libdecnumber>`_:
Import of gcc library, used to implement decimal number arithmetic.
* `migration <https://gitlab.com/qemu-project/qemu/-/tree/master/migration>`__:
- `Migration framework <migration>`.
+ :ref:`Migration framework <migration>`.
* `monitor <https://gitlab.com/qemu-project/qemu/-/tree/master/monitor>`_:
`Monitor <QEMU monitor>` implementation (HMP & QMP).
* `nbd <https://gitlab.com/qemu-project/qemu/-/tree/master/nbd>`_:
@@ -193,7 +193,7 @@ yet, so sometimes the source code is all you have.
- `lcitool <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/lcitool>`_:
Generate dockerfiles for CI containers.
- `migration <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/migration>`_:
- Test scripts and data for `Migration framework <migration>`.
+ Test scripts and data for :ref:`Migration framework <migration>`.
- `multiboot <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/multiboot>`_:
Test multiboot functionality for x86_64/i386.
- `qapi-schema <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/qapi-schema>`_:
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 693d9855dd1..4fa044bfb6e 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -120,7 +120,7 @@ Migration
---------
QEMU can save and restore the execution of a virtual machine between different
-host systems. This is provided by the `Migration framework<migration>`.
+host systems. This is provided by the :ref:`Migration framework<migration>`.
NBD
---
@@ -212,14 +212,14 @@ machine emulator and virtualizer.
QOM
---
-`QEMU Object Model <qom>` is an object oriented API used to define various
-devices and hardware in the QEMU codebase.
+:ref:`QEMU Object Model <qom>` is an object oriented API used to define
+various devices and hardware in the QEMU codebase.
Record/replay
-------------
-`Record/replay <replay>` is a feature of QEMU allowing to have a deterministic
-and reproducible execution of a virtual machine.
+:ref:`Record/replay <replay>` is a feature of QEMU allowing to have a
+deterministic and reproducible execution of a virtual machine.
Rust
----
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 61/63] docs: enable qapidoc transmogrifier for QEMU QMP Reference
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (59 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 60/63] docs: disambiguate cross-references John Snow
@ 2025-03-11 3:42 ` John Snow
2025-03-11 3:43 ` [PATCH v3 62/63] docs: add qapi-domain syntax documentation John Snow
` (2 subsequent siblings)
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:42 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
We are not enabling the transmogrifier for QSD or QGA yet because we
don't (yet) have a way to create separate indices, and all of the
definitions will bleed together, which isn't so nice.
For now, QMP is better than nothing at all!
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/interop/qemu-qmp-ref.rst | 1 +
qapi/qapi-schema.json | 2 ++
2 files changed, 3 insertions(+)
diff --git a/docs/interop/qemu-qmp-ref.rst b/docs/interop/qemu-qmp-ref.rst
index f94614a0b2f..e95eeac45e2 100644
--- a/docs/interop/qemu-qmp-ref.rst
+++ b/docs/interop/qemu-qmp-ref.rst
@@ -7,3 +7,4 @@ QEMU QMP Reference Manual
:depth: 3
.. qapi-doc:: qapi/qapi-schema.json
+ :transmogrify:
diff --git a/qapi/qapi-schema.json b/qapi/qapi-schema.json
index 2877aff73d0..4475e81cc3e 100644
--- a/qapi/qapi-schema.json
+++ b/qapi/qapi-schema.json
@@ -5,6 +5,8 @@
#
# This document describes all commands currently supported by QMP.
#
+# For locating a particular item, please see the `qapi-index`.
+#
# Most of the time their usage is exactly the same as in the user
# Monitor, this means that any other document which also describe
# commands (the manpage, QEMU's manual, etc) can and should be
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 62/63] docs: add qapi-domain syntax documentation
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (60 preceding siblings ...)
2025-03-11 3:42 ` [PATCH v3 61/63] docs: enable qapidoc transmogrifier for QEMU QMP Reference John Snow
@ 2025-03-11 3:43 ` John Snow
2025-03-11 3:43 ` [PATCH v3 63/63] MAINTAINERS: Add jsnow as maintainer for Sphinx documentation John Snow
2025-03-11 8:52 ` [PATCH v3 00/63] docs: Add new QAPI transmogrifier Markus Armbruster
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:43 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Who documents the documentation?
Me, I guess.
Signed-off-by: John Snow <jsnow@redhat.com>
---
docs/devel/index-build.rst | 1 +
docs/devel/qapi-domain.rst | 670 +++++++++++++++++++++++++++++++++++++
2 files changed, 671 insertions(+)
create mode 100644 docs/devel/qapi-domain.rst
diff --git a/docs/devel/index-build.rst b/docs/devel/index-build.rst
index 0745c81a264..3f3cb21b9b4 100644
--- a/docs/devel/index-build.rst
+++ b/docs/devel/index-build.rst
@@ -12,4 +12,5 @@ some of the basics if you are adding new files and targets to the build.
kconfig
docs
qapi-code-gen
+ qapi-domain
control-flow-integrity
diff --git a/docs/devel/qapi-domain.rst b/docs/devel/qapi-domain.rst
new file mode 100644
index 00000000000..1475870ca6c
--- /dev/null
+++ b/docs/devel/qapi-domain.rst
@@ -0,0 +1,670 @@
+======================
+The Sphinx QAPI Domain
+======================
+
+An extension to the `rST syntax
+<https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html>`_
+in Sphinx is provided by the QAPI Domain, located in
+``docs/sphinx/qapi_domain.py``. This extension is analogous to the
+`Python Domain
+<https://www.sphinx-doc.org/en/master/usage/domains/python.html>`_
+included with Sphinx, but provides special directives and roles
+speciically for annotating and documenting QAPI definitions
+specifically.
+
+A `Domain
+<https://www.sphinx-doc.org/en/master/usage/domains/index.html>`_
+provides a set of special rST directives and cross-referencing roles to
+Sphinx for understanding rST markup written to document a specific
+language. By itself, this QAPI extension is only sufficient to parse rST
+markup written by hand; the `autodoc
+<https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html>`_
+functionality is provided elsewhere, in ``docs/sphinx/qapidoc.py``, by
+the "Transmogrifier".
+
+It is not expected that any developer nor documentation writer would
+never need to write *nor* read these special rST forms. However, in the
+event that something needs to be debugged, knowing the syntax of the
+domain is quite handy. This reference may also be useful as a guide for
+understanding the QAPI Domain extension code itself. Although most of
+these forms will not be needed for documentation writing purposes,
+understanding the cross-referencing syntax *will* be helpful when
+writing rST documentation elsewhere, or for enriching the body of
+QAPIDoc blocks themselves.
+
+
+Concepts
+========
+
+The QAPI Domain itself provides no mechanisms for reading the QAPI
+Schema or generating documentation from code that exists. It is merely
+the rST syntax used to describe things. For instance, the Sphinx Python
+domain adds syntax like ``:py:func:`` for describing Python functions in
+documentation, but it's the autodoc module that is responsible for
+reading python code and generating such syntax. QAPI is analagous here:
+qapidoc.py is responsible for reading the QAPI Schema and generating rST
+syntax, and qapi_domain.py is responsible for translating that special
+syntax and providing APIs for Sphinx internals.
+
+In other words:
+
+qapi_domain.py adds syntax like ``.. qapi:command::`` to Sphinx, and
+qapidoc.py transforms the documentation in ``qapi/*.json`` into rST
+using directives defined by the domain.
+
+Or even shorter:
+
+``:py:`` is to ``:qapi:`` as *autodoc* is to *qapidoc*.
+
+
+Info Field Lists
+================
+
+`Field lists
+<https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#field-lists>`_
+are a standard syntax in reStructuredText. Sphinx `extends that syntax
+<https://www.sphinx-doc.org/en/master/usage/domains/python.html#info-field-lists>`_
+to give certain field list entries special meaning and parsing to, for
+example, add cross-references. The QAPI Domain takes advantage of this
+field list extension to document things like Arguments, Members, Values,
+and so on.
+
+The special parsing and handling of info field lists in Sphinx is provided by
+three main classes; Field, GroupedField, and TypedField. The behavior
+and formatting for each configured field list entry in the domain
+changes depending on which class is used.
+
+Field:
+ * Creates an ungrouped field: i.e., each entry will create its own
+ section and they will not be combined.
+ * May *optionally* support an argument.
+ * May apply cross-reference roles to *either* the argument *or* the
+ content body, both, or neither.
+
+This is used primarily for entries which are not expected to be
+repeated, i.e., items that may only show up at most once. The QAPI
+domain uses this class for "Errors" section.
+
+GroupedField:
+ * Creates a grouped field: i.e. multiple adjacent entries will be
+ merged into one section, and the content will form a bulleted list.
+ * *Must* take an argument.
+ * May optionally apply a cross-reference role to the argument, but not
+ the body.
+ * Can be configured to remove the bulleted list if there is only a
+ single entry.
+ * All items will be generated with the form: "argument -- body"
+
+This is used for entries which are expected to be repeated, but aren't
+expected to have two arguments, i.e. types without names, or names
+without types. The QAPI domain uses this class for features, returns,
+and enum values.
+
+TypedField:
+ * Creates a grouped, typed field. Multiple adjacent entres will be
+ merged into one section, and the content will form a bulleted list.
+ * *Must* take at least one argument, but supports up to two -
+ nominally, a name and a type.
+ * May optionally apply a cross-reference role to the type or the name
+ argument, but not the body.
+ * Can be configured to remove the bulleted list if there is only a
+ single entry.
+ * All items will be generated with the form "name (type) -- body"
+
+This is used for entries that are expected to be repeated and will have
+a name, a type, and a description. The QAPI domain uses this class for
+arguments, alternatives, and members. Wherever type names are referenced
+below, They must be a valid, documented type that will be
+cross-referenced in the HTML output; or one of the built-in JSON types
+(string, number, int, boolean, null, value, q_empty).
+
+
+``:feat:``
+----------
+
+Document a feature attached to a QAPI definition.
+
+:availability: This field list is available in the body of Command,
+ Event, Enum, Object and Alternate directives.
+:syntax: ``:feat name: Lorem ipsum, dolor sit amet...``
+:type: `sphinx.util.docfields.GroupedField
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.GroupedField.html?private=1>`_
+
+Example::
+
+ .. qapi:object:: BlockdevOptionsVirtioBlkVhostVdpa
+ :since: 7.2
+ :ifcond: CONFIG_BLKIO
+
+ Driver specific block device options for the virtio-blk-vhost-vdpa
+ backend.
+
+ :memb string path: path to the vhost-vdpa character device.
+ :feat fdset: Member ``path`` supports the special "/dev/fdset/N" path
+ (since 8.1)
+
+
+``:arg:``
+---------
+
+Document an argument to a QAPI command.
+
+:availability: This field list is only available in the body of the
+ Command directive.
+:syntax: ``:arg type name: description``
+:type: `sphinx.util.docfields.TypedField
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.TypedField.html?private=1>`_
+
+
+Example::
+
+ .. qapi:command:: job-pause
+ :since: 3.0
+
+ Pause an active job.
+
+ This command returns immediately after marking the active job for
+ pausing. Pausing an already paused job is an error.
+
+ The job will pause as soon as possible, which means transitioning
+ into the PAUSED state if it was RUNNING, or into STANDBY if it was
+ READY. The corresponding JOB_STATUS_CHANGE event will be emitted.
+
+ Cancelling a paused job automatically resumes it.
+
+ :arg string id: The job identifier.
+
+
+``:error:``
+-----------
+
+Document the error condition(s) of a QAPI command.
+
+:availability: This field list is only available in the body of the
+ Command directive.
+:syntax: ``:error: Lorem ipsum dolor sit amet ...``
+:type: `sphinx.util.docfields.Field
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.Field.html?private=1>`_
+
+The format of the :errors: field list description is free-form rST. The
+alternative spelling ":errors:" is also permitted, but strictly
+analogous.
+
+Example::
+
+ .. qapi:command:: block-job-set-speed
+ :since: 1.1
+
+ Set maximum speed for a background block operation.
+
+ This command can only be issued when there is an active block job.
+
+ Throttling can be disabled by setting the speed to 0.
+
+ :arg string device: The job identifier. This used to be a device
+ name (hence the name of the parameter), but since QEMU 2.7 it
+ can have other values.
+ :arg int speed: the maximum speed, in bytes per second, or 0 for
+ unlimited. Defaults to 0.
+ :error:
+ - If no background operation is active on this device,
+ DeviceNotActive
+
+
+``:return:``
+-------------
+
+Document the return type(s) and value(s) of a QAPI command.
+
+:availability: This field list is only available in the body of the
+ Command directive.
+:syntax: ``:return type: Lorem ipsum dolor sit amet ...``
+:type: `sphinx.util.docfields.GroupedField
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.GroupedField.html?private=1>`_
+
+
+Example::
+
+ .. qapi:command:: query-replay
+ :since: 5.2
+
+ Retrieve the record/replay information. It includes current
+ instruction count which may be used for ``replay-break`` and
+ ``replay-seek`` commands.
+
+ :return ReplayInfo: record/replay information.
+
+ .. qmp-example::
+
+ -> { "execute": "query-replay" }
+ <- { "return": {
+ "mode": "play", "filename": "log.rr", "icount": 220414 }
+ }
+
+
+``:value:``
+-----------
+
+Document a possible value for a QAPI enum.
+
+:availability: This field list is only available in the body of the Enum
+ directive.
+:syntax: ``:value name: Lorem ipsum, dolor sit amet ...``
+:type: `sphinx.util.docfields.GroupedField
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.GroupedField.html?private=1>`_
+
+Example::
+
+ .. qapi:enum:: QapiErrorClass
+ :since: 1.2
+
+ QEMU error classes
+
+ :value GenericError: this is used for errors that don't require a specific
+ error class. This should be the default case for most errors
+ :value CommandNotFound: the requested command has not been found
+ :value DeviceNotActive: a device has failed to be become active
+ :value DeviceNotFound: the requested device has not been found
+ :value KVMMissingCap: the requested operation can't be fulfilled because a
+ required KVM capability is missing
+
+
+``:alt:``
+------------
+
+Document a possible branch for a QAPI alternate.
+
+:availability: This field list is only available in the body of the
+ Alternate directive.
+:syntax: ``:alt type name: Lorem ipsum, dolor sit amet ...``
+:type: `sphinx.util.docfields.TypedField
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.TypedField.html?private=1>`_
+
+As a limitation of Sphinx, we must document the "name" of the branch in
+addition to the type, even though this information is not visible on the
+wire in the QMP protocol format. This limitation *may* be lifted at a
+future date.
+
+Example::
+
+ .. qapi:alternate:: StrOrNull
+ :since: 2.10
+
+ This is a string value or the explicit lack of a string (null
+ pointer in C). Intended for cases when 'optional absent' already
+ has a different meaning.
+
+ :alt string s: the string value
+ :alt null n: no string value
+
+
+``:memb:``
+----------
+
+Document a member of an Event or Object.
+
+:availability: This field list is available in the body of Event or
+ Object directives.
+:syntax: ``:memb type name: Lorem ipsum, dolor sit amet ...``
+:type: `sphinx.util.docfields.TypedField
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.TypedField.html?private=1>`_
+
+This is fundamentally the same as ``:arg:`` and ``:alt:``, but uses the
+"Members" phrasing for Events and Objects (Structs and Unions).
+
+Example::
+
+ .. qapi:event:: JOB_STATUS_CHANGE
+ :since: 3.0
+
+ Emitted when a job transitions to a different status.
+
+ :memb string id: The job identifier
+ :memb JobStatus status: The new job status
+
+
+Arbitrary field lists
+---------------------
+
+Other field list names, while valid rST syntax, are prohibited inside of
+QAPI directives to help prevent accidental misspellings of info field
+list names. If you want to add a new arbitrary "non-value-added" field
+list to QAPI documentation, you must add the field name to the allow
+list in ``docs/conf.py``
+
+For example::
+
+ qapi_allowed_fields = {
+ "see also",
+ }
+
+Will allow you to add arbitrary field lists in QAPI directives::
+
+ .. qapi:command:: x-fake-command
+
+ :see also: Lorem ipsum, dolor sit amet ...
+
+
+Cross-references
+================
+
+Cross-reference `roles
+<https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html>`_
+in the QAPI domain are modeled closely after the `Python
+cross-referencing syntax
+<https://www.sphinx-doc.org/en/master/usage/domains/python.html#cross-referencing-python-objects>`_.
+
+QAPI definitions can be referenced using the standard `any
+<https://www.sphinx-doc.org/en/master/usage/referencing.html#role-any>`_
+role cross-reference syntax, such as with ```query-blockstats```. In
+the event that disambiguation is needed, cross-references can also be
+written using a number of explicit cross-reference roles:
+
+* ``:qapi:mod:`block-core``` -- Reference a QAPI module. The link will
+ take you to the beginning of that section in the documentation.
+* ``:qapi:cmd:`query-block``` -- Reference a QAPI command.
+* ``:qapi:event:`JOB_STATUS_CHANGE``` -- Reference a QAPI event.
+* ``:qapi:enum:`QapiErrorClass``` -- Reference a QAPI enum.
+* ``:qapi:obj:`BlockdevOptionsVirtioBlkVhostVdpa`` -- Reference a QAPI
+ object (struct or union)
+* ``:qapi:alt:`StrOrNull``` -- Reference a QAPI alternate.
+* ``:qapi:type:`BlockDirtyInfo``` -- Reference *any* QAPI type; this
+ excludes modules, commands, and events.
+* ``:qapi:any:`block-job-set-speed``` -- Reference absolutely any QAPI entity.
+
+Type arguments in info field lists are converted into references as if
+you had used the ``:qapi:type:`` role. All of the special syntax below
+applies to both info field lists and standalone explicit
+cross-references.
+
+
+Type decorations
+----------------
+
+Type names in references can be surrounded by brackets, like
+``[typename]``, to indicate an array of that type. The cross-reference
+will apply only to the type name between the brackets. For example;
+``:qapi:type:`[Qcow2BitmapInfoFlags]``` renders to:
+:qapi:type:`[Qcow2BitmapInfoFlags]`
+
+To indicate an optional argument/member in a field list, the type name
+can be suffixed with ``?``. The cross-reference will be transformed to
+"type, Optional" with the link applying only to the type name. For
+example; ``:qapi:type:`BitmapSyncMode?``` renders to:
+:qapi:type:`BitmapSyncMode?`
+
+
+Namespaces
+----------
+
+Mimicking the `Python domain target specification syntax
+<https://www.sphinx-doc.org/en/master/usage/domains/python.html#target-specification>`_,
+QAPI allows you to specify the fully qualified path for a data
+type. QAPI enforces globally unique names, so it's unlikely you'll need
+this specific feature, but it may be extended in the near future to
+allow referencing identically named commands and data types from
+different utilities; i.e. QEMU Storage Daemon vs QMP.
+
+* A module can be explicitly provided;
+ ``:qapi:type:`block-core.BitmapSyncMode``` will render to:
+ :qapi:type:`block-core.BitmapSyncMode`
+* If you don't want to display the "fully qualified" name, it can be
+ prefixed with a tilde; ``:qapi:type:`~block-core.BitmapSyncMode```
+ will render to: :qapi:type:`~block-core.BitmapSyncMode`
+
+
+Custom link text
+----------------
+
+The name of a cross-reference link can be explicitly overridden like
+`most stock Sphinx references
+<https://www.sphinx-doc.org/en/master/usage/referencing.html#syntax>`_
+using the ``custom text <target>`` syntax.
+
+For example, ``:qapi:cmd:`Merge dirty bitmaps
+<block-dirty-bitmap-merge>``` will render as: :qapi:cmd:`Merge dirty
+bitmaps <block-dirty-bitmap-merge>`
+
+
+Directives
+==========
+
+The QAPI domain adds a number of custom directives for documenting
+various QAPI/QMP entities. The syntax is plain rST, and follows this
+general format::
+
+ .. qapi:directive:: argument
+ :option:
+ :another-option: with an argument
+
+ Content body, arbitrary rST is allowed here.
+
+
+Sphinx standard options
+-----------------------
+
+All QAPI directives inherit a number of `standard options
+<https://www.sphinx-doc.org/en/master/usage/domains/index.html#basic-markup>`_
+from Sphinx's ObjectDescription class.
+
+The dashed spellings of the below options were added in Sphinx 7.2, the
+undashed spellings are currently retained as aliases, but will be
+removed in a future version.
+
+* ``:no-index:`` and ``:noindex:`` -- Do not add this item into the
+ Index, and do not make it available for cross-referencing.
+* ``no-index-entry:`` and ``:noindexentry:`` -- Do not add this item
+ into the Index, but allow it to be cross-referenced.
+* ``no-contents-entry`` and ``:nocontentsentry:`` -- Exclude this item
+ from the Table of Contents.
+* ``no-typesetting`` -- Create TOC, Index and cross-referencing
+ entities, but don't actually display the content.
+
+
+QAPI standard options
+---------------------
+
+All QAPI directives -- *except* for module -- support these common options.
+
+* ``:module: modname`` -- Borrowed from the Python domain, this option allows
+ you to override the module association of a given definition.
+* ``:since: x.y`` -- Allows the documenting of "Since" information, which is
+ displayed in the signature bar.
+* ``:ifcond: CONDITION`` -- Allows the documenting of conditional availability
+ information, which is displayed in an eyecatch just below the
+ signature bar.
+* ``:deprecated:`` -- Adds an eyecatch just below the signature bar that
+ advertises that this definition is deprecated and should be avoided.
+* ``:unstable:`` -- Adds an eyecatch just below the signature bar that
+ advertises that this definition is unstable and should not be used in
+ production code.
+
+
+qapi:module
+-----------
+
+The ``qapi:module`` directive marks the start of a QAPI module. It may have
+a content body, but it can be omitted. All subsequent QAPI directives
+are associated with the most recent module; this effects their "fully
+qualified" name, but has no other effect.
+
+Example::
+
+ .. qapi:module:: block-core
+
+ Welcome to the block-core module!
+
+Will be rendered as:
+
+.. qapi:module:: block-core
+ :noindex:
+
+ Welcome to the block-core module!
+
+
+qapi:command
+------------
+
+This directive documents a QMP command. It may use any of the standard
+Sphinx or QAPI options, and the documentation body may contain
+``:arg:``, ``:feat:``, ``:error:``, or ``:return:`` info field list
+entries.
+
+Example::
+
+ .. qapi:command:: x-fake-command
+ :since: 42.0
+ :unstable:
+
+ This command is fake, so it can't hurt you!
+
+ :arg int foo: Your favorite number.
+ :arg string? bar: Your favorite season.
+ :return [string]: A lovely computer-written poem for you.
+
+
+Will be rendered as:
+
+ .. qapi:command:: x-fake-command
+ :noindex:
+ :since: 42.0
+ :unstable:
+
+ This command is fake, so it can't hurt you!
+
+ :arg int foo: Your favorite number.
+ :arg string? bar: Your favorite season.
+ :return [string]: A lovely computer-written poem for you.
+
+
+qapi:event
+----------
+
+This directive documents a QMP event. It may use any of the standard
+Sphinx or QAPI options, and the documentation body may contain
+``:memb:`` or ``:feat:`` info field list entries.
+
+Example::
+
+ .. qapi:event:: COMPUTER_IS_RUINED
+ :since: 0.1
+ :deprecated:
+
+ This event is emitted when your computer is *extremely* ruined.
+
+ :memb string reason: Diagnostics as to what caused your computer to
+ be ruined.
+ :feat sadness: When present, the diagnostic message will also
+ explain how sad the computer is as a result of your wrongdoings.
+
+Will be rendered as:
+
+.. qapi:event:: COMPUTER_IS_RUINED
+ :noindex:
+ :since: 0.1
+ :deprecated:
+
+ This event is emitted when your computer is *extremely* ruined.
+
+ :memb string reason: Diagnostics as to what caused your computer to
+ be ruined.
+ :feat sadness: When present, the diagnostic message will also explain
+ how sad the computer is as a result of your wrongdoings.
+
+
+qapi:enum
+---------
+
+This directive documents a QAPI enum. It may use any of the standard
+Sphinx or QAPI options, and the documentation body may contain
+``:value:`` or ``:feat:`` info field list entries.
+
+Example::
+
+ .. qapi:enum:: Mood
+ :ifcond: LIB_PERSONALITY
+
+ This enum represents your virtual machine's current mood!
+
+ :value Happy: Your VM is content and well-fed.
+ :value Hungry: Your VM needs food.
+ :value Melancholic: Your VM is experiencing existential angst.
+ :value Petulant: Your VM is throwing a temper tantrum.
+
+Will be rendered as:
+
+.. qapi:enum:: Mood
+ :noindex:
+ :ifcond: LIB_PERSONALITY
+
+ This enum represents your virtual machine's current mood!
+
+ :value Happy: Your VM is content and well-fed.
+ :value Hungry: Your VM needs food.
+ :value Melancholic: Your VM is experiencing existential angst.
+ :value Petulant: Your VM is throwing a temper tantrum.
+
+
+qapi:object
+-----------
+
+This directive documents a QAPI structure or union and represents a QMP
+object. It may use any of the standard Sphinx or QAPI options, and the
+documentation body may contain ``:memb:`` or ``:feat:`` info field list
+entries.
+
+Example::
+
+ .. qapi:object:: BigBlobOfStuff
+
+ This object has a bunch of disparate and unrelated things in it.
+
+ :memb int Birthday: Your birthday, represented in seconds since the
+ UNIX epoch.
+ :memb [string] Fav-Foods: A list of your favorite foods.
+ :memb boolean? Bizarre-Docs: True if the documentation reference
+ should be strange.
+
+Will be rendered as:
+
+.. qapi:object:: BigBlobOfStuff
+ :noindex:
+
+ This object has a bunch of disparate and unrelated things in it.
+
+ :memb int Birthday: Your birthday, represented in seconds since the
+ UNIX epoch.
+ :memb [string] Fav-Foods: A list of your favorite foods.
+ :memb boolean? Bizarre-Docs: True if the documentation reference
+ should be strange.
+
+
+qapi:alternate
+--------------
+
+This directive documents a QAPI alternate. It may use any of the
+standard Sphinx or QAPI options, and the documentation body may contain
+``:alt:`` or ``:feat:`` info field list entries.
+
+Example::
+
+ .. qapi:alternate:: ErrorCode
+
+ This alternate represents an Error Code from the VM.
+
+ :alt int ec: An error code, like the type you're used to.
+ :alt string em: An expletive-laced error message, if your
+ computer is feeling particularly cranky and tired of your
+ antics.
+
+Will be rendered as:
+
+.. qapi:alternate:: ErrorCode
+ :noindex:
+
+ This alternate represents an Error Code from the VM.
+
+ :alt int ec: An error code, like the type you're used to.
+ :alt string em: An expletive-laced error message, if your
+ computer is feeling particularly cranky and tired of your
+ antics.
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* [PATCH v3 63/63] MAINTAINERS: Add jsnow as maintainer for Sphinx documentation
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (61 preceding siblings ...)
2025-03-11 3:43 ` [PATCH v3 62/63] docs: add qapi-domain syntax documentation John Snow
@ 2025-03-11 3:43 ` John Snow
2025-03-11 8:52 ` [PATCH v3 00/63] docs: Add new QAPI transmogrifier Markus Armbruster
63 siblings, 0 replies; 74+ messages in thread
From: John Snow @ 2025-03-11 3:43 UTC (permalink / raw)
To: qemu-devel
Cc: Peter Maydell, John Snow, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée, Markus Armbruster
Since I've just about rewritten the entirety of the QAPI documentation
system, it's probably fair that I be the contact point for if it goes
awry.
Signed-off-by: John Snow <jsnow@redhat.com>
---
MAINTAINERS | 1 +
1 file changed, 1 insertion(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 7ac04f35201..4b83a436d8f 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4325,6 +4325,7 @@ S: Orphan
F: po/*.po
Sphinx documentation configuration and build machinery
+M: John Snow <jsnow@redhat.com>
M: Peter Maydell <peter.maydell@linaro.org>
S: Maintained
F: docs/conf.py
--
2.48.1
^ permalink raw reply related [flat|nested] 74+ messages in thread
* Re: [PATCH v3 02/63] qapi: shush pylint up
2025-03-11 3:42 ` [PATCH v3 02/63] qapi: shush pylint up John Snow
@ 2025-03-11 6:59 ` Markus Armbruster
0 siblings, 0 replies; 74+ messages in thread
From: Markus Armbruster @ 2025-03-11 6:59 UTC (permalink / raw)
To: John Snow
Cc: qemu-devel, Peter Maydell, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée
John Snow <jsnow@redhat.com> writes:
> Shhhhh!
>
> This patch can be dropped from the PR and I'll clean it up later. It's
> just here to help me establish a linting baseline. It isn't really
> needed for the series itself.
>
> Signed-off-by: John Snow <jsnow@redhat.com>
Okay, I'll drop this patch.
Note that I just posted "[PATCH] scripts/qapi/backend: Clean up
create_backend()'s failure mode", and ...
> ---
> scripts/qapi/backend.py | 2 ++
> scripts/qapi/main.py | 8 +++-----
> 2 files changed, 5 insertions(+), 5 deletions(-)
>
> diff --git a/scripts/qapi/backend.py b/scripts/qapi/backend.py
> index 14e60aa67af..49ae6ecdd33 100644
> --- a/scripts/qapi/backend.py
> +++ b/scripts/qapi/backend.py
> @@ -13,6 +13,7 @@
>
>
> class QAPIBackend(ABC):
> + # pylint: disable=too-few-public-methods
>
> @abstractmethod
> def generate(self,
> @@ -36,6 +37,7 @@ def generate(self,
>
>
> class QAPICBackend(QAPIBackend):
> + # pylint: disable=too-few-public-methods
>
> def generate(self,
> schema: QAPISchema,
... when you rebase on top of my patch, the hunk above stays, and the
hunk below goes.
> diff --git a/scripts/qapi/main.py b/scripts/qapi/main.py
> index 5b4679abcf1..01155373bd0 100644
> --- a/scripts/qapi/main.py
> +++ b/scripts/qapi/main.py
> @@ -38,8 +38,7 @@ def create_backend(path: str) -> QAPIBackend:
> try:
> mod = import_module(module_path)
> except Exception as ex:
> - print(f"unable to import '{module_path}': {ex}", file=sys.stderr)
> - sys.exit(1)
> + raise QAPIError(f"unable to import '{module_path}': {ex}") from ex
>
> try:
> klass = getattr(mod, class_name)
> @@ -51,9 +50,8 @@ def create_backend(path: str) -> QAPIBackend:
> try:
> backend = klass()
> except Exception as ex:
> - print(f"backend '{path}' cannot be instantiated: {ex}",
> - file=sys.stderr)
> - sys.exit(1)
> + raise QAPIError(
> + f"backend '{path}' cannot be instantiated: {ex}") from ex
>
> if not isinstance(backend, QAPIBackend):
> print(f"backend '{path}' must be an instance of QAPIBackend",
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v3 32/63] qapi/parser: adjust info location for doc body section
2025-03-11 3:42 ` [PATCH v3 32/63] qapi/parser: adjust info location for doc body section John Snow
@ 2025-03-11 7:01 ` Markus Armbruster
0 siblings, 0 replies; 74+ messages in thread
From: Markus Armbruster @ 2025-03-11 7:01 UTC (permalink / raw)
To: John Snow
Cc: qemu-devel, Peter Maydell, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée
John Snow <jsnow@redhat.com> writes:
> Instead of using the info object for the doc block as a whole (which
> always points to the very first line of the block), update the info
> pointer for each call to ensure_untagged_section when the existing
> section is otherwise empty. This way, Sphinx error information will
> match precisely to where the text actually starts.
>
> For example, this patch will move the info pointer for the "Hello!"
> untagged section ...
>
>> ## <-- from here ...
>> # Hello! <-- ... to here.
>> ##
>
> This doesn't seem to improve error reporting now. It will with the
> forthcoming QAPI doc transmogrifier.
>
> If I stick bad rST into qapi/block-core.json like this:
>
>> ##
>> # @SnapshotInfo:
>> #
>> +# rST syntax error: *ahh!
>> +#
>> # @id: unique shapshot id
>> #
>> # @name: user chosen name
>
> The existing code's error message will point to the beginning of the doc
> comment, which is less than helpful. The transmogrifier's message will
> point to the erroneous line, but to accomplish this, it needs this
> patch.
>
> Signed-off-by: John Snow <jsnow@redhat.com>
> ---
> scripts/qapi/parser.py | 6 +++++-
> 1 file changed, 5 insertions(+), 1 deletion(-)
>
> diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
> index 64f0bb824ae..97def9f0e4f 100644
> --- a/scripts/qapi/parser.py
> +++ b/scripts/qapi/parser.py
> @@ -686,7 +686,11 @@ def end(self) -> None:
> def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
> if self.all_sections and not self.all_sections[-1].tag:
> # extend current section
> - self.all_sections[-1].text += '\n'
> + section = self.all_sections[-1]
> + if not section.text:
> + # Section is empty so far; update info to start *here*.
> + section.info = info
> + section.text += '\n'
> return
> # start new section
> section = self.Section(info)
Reviewed-by: Markus Armbruster <armbru@redhat.com>
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v3 33/63] qapi: clean up encoding of section kinds
2025-03-11 3:42 ` [PATCH v3 33/63] qapi: clean up encoding of section kinds John Snow
@ 2025-03-11 7:02 ` Markus Armbruster
0 siblings, 0 replies; 74+ messages in thread
From: Markus Armbruster @ 2025-03-11 7:02 UTC (permalink / raw)
To: John Snow
Cc: qemu-devel, Peter Maydell, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée
John Snow <jsnow@redhat.com> writes:
> 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>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v3 34/63] qapi/schema: add __repr__ to QAPIDoc.Section
2025-03-11 3:42 ` [PATCH v3 34/63] qapi/schema: add __repr__ to QAPIDoc.Section John Snow
@ 2025-03-11 7:03 ` Markus Armbruster
0 siblings, 0 replies; 74+ messages in thread
From: Markus Armbruster @ 2025-03-11 7:03 UTC (permalink / raw)
To: John Snow
Cc: qemu-devel, Peter Maydell, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée
John Snow <jsnow@redhat.com> writes:
> Makes debugging far more pleasant when you can just print(section) and
> get something reasonable to display.
>
> Signed-off-by: John Snow <jsnow@redhat.com>
> ---
> scripts/qapi/parser.py | 3 +++
> 1 file changed, 3 insertions(+)
>
> diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
> index 94d5322f8af..11c11bb09e5 100644
> --- a/scripts/qapi/parser.py
> +++ b/scripts/qapi/parser.py
> @@ -668,6 +668,9 @@ def __init__(
> # section text without tag
> self.text = ''
>
> + def __repr__(self) -> str:
> + return f"<QAPIDoc.Section kind={self.kind!r} text={self.text!r}>"
> +
> def append_line(self, line: str) -> None:
> self.text += line + '\n'
Reviewed-by: Markus Armbruster <armbru@redhat.com>
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v3 59/63] qapi/parser: add undocumented stub members to all_sections
2025-03-11 3:42 ` [PATCH v3 59/63] qapi/parser: add undocumented stub members to all_sections John Snow
@ 2025-03-11 7:04 ` Markus Armbruster
2025-03-11 8:14 ` Markus Armbruster
0 siblings, 1 reply; 74+ messages in thread
From: Markus Armbruster @ 2025-03-11 7:04 UTC (permalink / raw)
To: John Snow
Cc: qemu-devel, Peter Maydell, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée
John Snow <jsnow@redhat.com> writes:
> This helps simplify the new doc generator if it doesn't have to check
> for undocumented members, it can just blindly operate on a sequence of
> QAPIDoc.Section instances.
>
> NB: If there is no existing 'member' section, these undocumented stub
> members will be inserted directly after the leading section.
>
> Signed-off-by: John Snow <jsnow@redhat.com>
> ---
> scripts/qapi/parser.py | 17 ++++++++++++++++-
> 1 file changed, 16 insertions(+), 1 deletion(-)
>
> diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
> index 11c11bb09e5..52bc44facf2 100644
> --- a/scripts/qapi/parser.py
> +++ b/scripts/qapi/parser.py
> @@ -789,8 +789,23 @@ 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(
> + section = QAPIDoc.ArgSection(
> self.info, QAPIDoc.Kind.MEMBER, member.name)
> + self.args[member.name] = section
> +
> + # Insert stub documentation section for missing member docs.
> + # TODO: drop when undocumented members are outlawed
> +
> + # Determine where to insert stub doc - it should go at the
> + # end of the members section(s), if any. Note that index 0
> + # is assumed to be an untagged intro section, even if it is
> + # empty.
> + index = 1
> + if len(self.all_sections) > 1:
> + while self.all_sections[index].kind == QAPIDoc.Kind.MEMBER:
> + index += 1
> + self.all_sections.insert(index, section)
> +
> self.args[member.name].connect(member)
>
> def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
Reviewed-by: Markus Armbruster <armbru@redhat.com>
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v3 59/63] qapi/parser: add undocumented stub members to all_sections
2025-03-11 7:04 ` Markus Armbruster
@ 2025-03-11 8:14 ` Markus Armbruster
0 siblings, 0 replies; 74+ messages in thread
From: Markus Armbruster @ 2025-03-11 8:14 UTC (permalink / raw)
To: John Snow
Cc: qemu-devel, Peter Maydell, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée
Markus Armbruster <armbru@redhat.com> writes:
> John Snow <jsnow@redhat.com> writes:
>
>> This helps simplify the new doc generator if it doesn't have to check
>> for undocumented members, it can just blindly operate on a sequence of
>> QAPIDoc.Section instances.
>>
>> NB: If there is no existing 'member' section, these undocumented stub
>> members will be inserted directly after the leading section.
>>
>> Signed-off-by: John Snow <jsnow@redhat.com>
This doesn't affect the output. It's not obvious why.
The old doc generator processes doc.body, .args, .features, .sections.
.sections has all sections not in the other three. It does not use
.all_sections.
The new doc generator processes doc.all_sections. It does not use
.body, .args, .features, .sections.
Parser and doc generator cooperate on generating stub documentation for
undocumented members. The parser makes up an ArgSection with an empty
description, and the doc generator makes up a description.
The old doc generator needs the made-up ArgSections in .args.
The new one needs it in .all_sections.
>> ---
>> scripts/qapi/parser.py | 17 ++++++++++++++++-
>> 1 file changed, 16 insertions(+), 1 deletion(-)
>>
>> diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
>> index 11c11bb09e5..52bc44facf2 100644
>> --- a/scripts/qapi/parser.py
>> +++ b/scripts/qapi/parser.py
>> @@ -789,8 +789,23 @@ def connect_member(self, member: 'QAPISchemaMember') -> None:
if self.symbol not in member.info.pragma.documentation_exceptions:
>> raise QAPISemError(member.info,
>> "%s '%s' lacks documentation"
>> % (member.role, member.name))
The code above rejects missing documentation unless permitted by a
pragma.
The code below makes up for missing documentation.
[*]
>> - self.args[member.name] = QAPIDoc.ArgSection(
>> + section = QAPIDoc.ArgSection(
>> self.info, QAPIDoc.Kind.MEMBER, member.name)
>> + self.args[member.name] = section
This puts the aforementioned made-up ArgSection into .args. The patch
doesn't change behavior.
>> +
>> + # Insert stub documentation section for missing member docs.
>> + # TODO: drop when undocumented members are outlawed
I'm going to move this comment up to [*].
>> +
>> + # Determine where to insert stub doc - it should go at the
>> + # end of the members section(s), if any. Note that index 0
>> + # is assumed to be an untagged intro section, even if it is
>> + # empty.
>> + index = 1
>> + if len(self.all_sections) > 1:
>> + while self.all_sections[index].kind == QAPIDoc.Kind.MEMBER:
>> + index += 1
>> + self.all_sections.insert(index, section)
This puts it into .all_sections.
>> +
>> self.args[member.name].connect(member)
>>
>> def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
I'm going to clarify the commit message as follows:
Parser and doc generator cooperate on generating stub documentation for
undocumented members. The parser makes up an ArgSection with an empty
description, and the doc generator makes up a description.
Right now, the made-up ArgSections go into doc.args. However, the new
doc generator uses .all_sections, not .args. So put them into
.all_sections, too.
Insert them right after existing 'member' sections. If there are none,
insert directly after the leading section.
Doesn't affect the old generator, because that one doesn't use
.all_sections.
> Reviewed-by: Markus Armbruster <armbru@redhat.com>
With that, my R-by stands.
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v3 52/63] docs/qapidoc: add visit_sections() method
2025-03-11 3:42 ` [PATCH v3 52/63] docs/qapidoc: add visit_sections() method John Snow
@ 2025-03-11 8:34 ` Markus Armbruster
0 siblings, 0 replies; 74+ messages in thread
From: Markus Armbruster @ 2025-03-11 8:34 UTC (permalink / raw)
To: John Snow
Cc: qemu-devel, Peter Maydell, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée
John Snow <jsnow@redhat.com> writes:
> Implement the actual main dispatch method that processes and handles the
> list of doc sections for a given QAPI entity.
Your PATCH 59 mentions in passing that the new doc generator "blindly
operates on a sequence of QAPIDoc.Section instances." I think that's
worth noting here. I'm going to insert into the commit message:
Process doc sections in strict source order. This is good; reordering
doc text is undesirable. Improvement over the old doc generator, which
can reorder doc comments that don't adhere to (largely unspoken)
conventions.
> Signed-off-by: John Snow <jsnow@redhat.com>
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v3 00/63] docs: Add new QAPI transmogrifier
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
` (62 preceding siblings ...)
2025-03-11 3:43 ` [PATCH v3 63/63] MAINTAINERS: Add jsnow as maintainer for Sphinx documentation John Snow
@ 2025-03-11 8:52 ` Markus Armbruster
63 siblings, 0 replies; 74+ messages in thread
From: Markus Armbruster @ 2025-03-11 8:52 UTC (permalink / raw)
To: John Snow
Cc: qemu-devel, Peter Maydell, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée
John Snow <jsnow@redhat.com> writes:
> This series is a "minimum viable" version of the new QAPI documentation
> system. It does the bare minimum under the new framework, saving the
> fancy features like the inliner for later. This version does add
> cross-references for all QAPI definitions and a shiny new QAPI Index.
PATCH 01,02 are not for merge.
> Patches 3-31 implement the qapi_domain extension.
These patches
Acked-by: Markus Armbruster <armbru@redhat.com>
> Patches 32-59 implement the qapidoc "Transmogrifier".
These patches
Acked-by: Markus Armbruster <armbru@redhat.com>
except for PATCH 32-34,41,59, which are R-by instead, and PATCH 38,
which is not for merge.
Queued except for 01,02,38. Expect a pull request later today.
[...]
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v3 54/63] docs/qapidoc: implement transmogrify() method
2025-03-11 3:42 ` [PATCH v3 54/63] docs/qapidoc: implement transmogrify() method John Snow
@ 2025-03-11 8:52 ` Markus Armbruster
0 siblings, 0 replies; 74+ messages in thread
From: Markus Armbruster @ 2025-03-11 8:52 UTC (permalink / raw)
To: John Snow
Cc: qemu-devel, Peter Maydell, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée
John Snow <jsnow@redhat.com> writes:
> This is the true top-level processor for the new transmogrifier;
> responsible both for generating the intermediate rST and then running
> the nested parse on that generated document to produce the final
> docutils tree that is then - very finally - postprocessed by sphinx for
> final rendering to HTML &c.
>
> Signed-off-by: John Snow <jsnow@redhat.com>
> ---
> docs/sphinx/qapidoc.py | 49 ++++++++++++++++++++++++++++++++++++++++--
> 1 file changed, 47 insertions(+), 2 deletions(-)
>
> diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
> index aaf5b6e22bc..32baf66a390 100644
> --- a/docs/sphinx/qapidoc.py
> +++ b/docs/sphinx/qapidoc.py
> @@ -2,6 +2,7 @@
> #
> # QEMU qapidoc QAPI file parsing extension
> #
> +# Copyright (c) 2024-2025 Red Hat
> # Copyright (c) 2020 Linaro
> #
> # This work is licensed under the terms of the GNU GPLv2 or later.
> @@ -56,6 +57,7 @@
> from sphinx import addnodes
> from sphinx.directives.code import CodeBlock
> from sphinx.errors import ExtensionError
> +from sphinx.util import logging
> from sphinx.util.docutils import switch_source_input
> from sphinx.util.nodes import nested_parse_with_titles
>
> @@ -74,7 +76,9 @@
> from sphinx.util.typing import ExtensionMetadata
>
>
> -__version__ = "1.0"
> +__version__ = "2.0"
PEP 8:
Module level “dunders” (i.e. names with two leading and two trailing
underscores) such as __all__, __author__, __version__, etc. should
be placed after the module docstring but before any import
statements except from __future__ imports.
Will move it in my tree.
> +
> +logger = logging.getLogger(__name__)
>
>
> class Transmogrifier:
> @@ -95,6 +99,10 @@ def __init__(self) -> None:
> self._result = StringList()
> self.indent = 0
>
> + @property
> + def result(self) -> StringList:
> + return self._result
> +
> @property
> def entity(self) -> QAPISchemaDefinition:
> assert self._curr_ent is not None
> @@ -438,7 +446,43 @@ def new_serialno(self) -> str:
> return "qapidoc-%d" % env.new_serialno("qapidoc")
>
> def transmogrify(self, schema: QAPISchema) -> nodes.Element:
> - raise NotImplementedError
> + logger.info("Transmogrifying QAPI to rST ...")
> + vis = Transmogrifier()
> + modules = set()
> +
> + for doc in schema.docs:
> + module_source = doc.info.fname
> + if module_source not in modules:
> + vis.visit_module(module_source)
> + modules.add(module_source)
> +
> + if doc.symbol:
> + ent = schema.lookup_entity(doc.symbol)
> + assert isinstance(ent, QAPISchemaDefinition)
> + vis.visit_entity(ent)
> + else:
> + vis.visit_freeform(doc)
> +
> + logger.info("Transmogrification complete.")
> +
> + contentnode = nodes.section()
> + content = vis.result
> + titles_allowed = True
> +
> + logger.info("Transmogrifier running nested parse ...")
> + with switch_source_input(self.state, content):
> + if titles_allowed:
> + node: nodes.Element = nodes.section()
> + node.document = self.state.document
> + nested_parse_with_titles(self.state, content, contentnode)
> + else:
> + node = nodes.paragraph()
> + node.document = self.state.document
> + self.state.nested_parse(content, 0, contentnode)
> + logger.info("Transmogrifier's nested parse completed.")
> + sys.stdout.flush()
> +
> + return contentnode
>
> def legacy(self, schema: QAPISchema) -> nodes.Element:
> vis = QAPISchemaGenRSTVisitor(self)
> @@ -572,6 +616,7 @@ def run(self) -> List[nodes.Node]:
>
> def setup(app: Sphinx) -> ExtensionMetadata:
> """Register qapi-doc directive with Sphinx"""
> + app.setup_extension("qapi_domain")
> app.add_config_value("qapidoc_srctree", None, "env")
> app.add_directive("qapi-doc", QAPIDocDirective)
> app.add_directive("qmp-example", QMPExample)
^ permalink raw reply [flat|nested] 74+ messages in thread
* Re: [PATCH v3 58/63] docs/qapidoc: generate entries for undocumented members
2025-03-11 3:42 ` [PATCH v3 58/63] docs/qapidoc: generate entries for undocumented members John Snow
@ 2025-03-11 9:22 ` Markus Armbruster
0 siblings, 0 replies; 74+ messages in thread
From: Markus Armbruster @ 2025-03-11 9:22 UTC (permalink / raw)
To: John Snow
Cc: qemu-devel, Peter Maydell, Daniel P. Berrangé, Eric Blake,
Michael Roth, Thomas Huth, Philippe Mathieu-Daudé,
Alex Bennée
John Snow <jsnow@redhat.com> writes:
> Presently, we never have any empty text entries for members. The next
> patch will explicitly generate such sections, so enable support for it
> in advance.
>
> The parser will generate placeholder sections to indicate undocumented
> members, but it's the qapidoc generator that's responsible for deciding
> what to do with that stub section.
>
> Signed-off-by: John Snow <jsnow@redhat.com>
> ---
> docs/sphinx/qapidoc.py | 5 +++--
> 1 file changed, 3 insertions(+), 2 deletions(-)
>
> diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
> index 7c5a08958d5..604ab109a19 100644
> --- a/docs/sphinx/qapidoc.py
> +++ b/docs/sphinx/qapidoc.py
> @@ -233,11 +233,12 @@ def visit_member(self, section: QAPIDoc.ArgSection) -> None:
> # TODO: features for members (documented at entity-level,
> # but sometimes defined per-member. Should we add such
> # information to member descriptions when we can?)
> - assert section.text and section.member
> + assert section.member
> self.generate_field(
> self.member_field_type,
> section.member,
> - section.text,
> + # TODO drop fallbacks when undocumented members are outlawed
> + section.text if section.text else "Not Documented.",
I'm changing this to "Not documented" to obey English capitalization
rules.
> section.info,
> )
^ permalink raw reply [flat|nested] 74+ messages in thread
end of thread, other threads:[~2025-03-11 9:22 UTC | newest]
Thread overview: 74+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-03-11 3:41 [PATCH v3 00/63] docs: Add new QAPI transmogrifier John Snow
2025-03-11 3:41 ` [PATCH v3 01/63] do-not-merge John Snow
2025-03-11 3:42 ` [PATCH v3 02/63] qapi: shush pylint up John Snow
2025-03-11 6:59 ` Markus Armbruster
2025-03-11 3:42 ` [PATCH v3 03/63] docs/sphinx: create QAPI domain extension stub John Snow
2025-03-11 3:42 ` [PATCH v3 04/63] docs/sphinx: add compat.py module and nested_parse helper John Snow
2025-03-11 3:42 ` [PATCH v3 05/63] docs/qapi-domain: add QAPI domain object registry John Snow
2025-03-11 3:42 ` [PATCH v3 06/63] docs/qapi-domain: add QAPI index John Snow
2025-03-11 3:42 ` [PATCH v3 07/63] docs/qapi-domain: add resolve_any_xref() John Snow
2025-03-11 3:42 ` [PATCH v3 08/63] docs/qapi-domain: add QAPI xref roles John Snow
2025-03-11 3:42 ` [PATCH v3 09/63] docs/qapi-domain: add compatibility node classes John Snow
2025-03-11 3:42 ` [PATCH v3 10/63] docs/qapi-domain: Add QAPIDescription abstract class John Snow
2025-03-11 3:42 ` [PATCH v3 11/63] docs/qapi-domain: add qapi:module directive John Snow
2025-03-11 3:42 ` [PATCH v3 12/63] docs/qapi-domain: add QAPIObject class John Snow
2025-03-11 3:42 ` [PATCH v3 13/63] docs/qapi-domain: add qapi:command directive John Snow
2025-03-11 3:42 ` [PATCH v3 14/63] docs/qapi-domain: add :since: directive option John Snow
2025-03-11 3:42 ` [PATCH v3 15/63] docs/qapi-domain: add "Arguments:" field lists John Snow
2025-03-11 3:42 ` [PATCH v3 16/63] docs/qapi-domain: add "Features:" " John Snow
2025-03-11 3:42 ` [PATCH v3 17/63] docs/qapi-domain: add "Errors:" " John Snow
2025-03-11 3:42 ` [PATCH v3 18/63] docs/qapi-domain: add "Return:" " John Snow
2025-03-11 3:42 ` [PATCH v3 19/63] docs/qapi-domain: add qapi:enum directive John Snow
2025-03-11 3:42 ` [PATCH v3 20/63] docs/qapi-domain: add qapi:alternate directive John Snow
2025-03-11 3:42 ` [PATCH v3 21/63] docs/qapi-domain: add qapi:event directive John Snow
2025-03-11 3:42 ` [PATCH v3 22/63] docs/qapi-domain: add qapi:object directive John Snow
2025-03-11 3:42 ` [PATCH v3 23/63] docs/qapi-domain: add :deprecated: directive option John Snow
2025-03-11 3:42 ` [PATCH v3 24/63] docs/qapi-domain: add :unstable: " John Snow
2025-03-11 3:42 ` [PATCH v3 25/63] docs/qapi-domain: add :ifcond: " John Snow
2025-03-11 3:42 ` [PATCH v3 26/63] docs/qapi-domain: add warnings for malformed field lists John Snow
2025-03-11 3:42 ` [PATCH v3 27/63] docs/qapi-domain: add type cross-refs to " John Snow
2025-03-11 3:42 ` [PATCH v3 28/63] docs/qapi-domain: add CSS styling John Snow
2025-03-11 3:42 ` [PATCH v3 29/63] docs/qapi-domain: add XREF compatibility goop for Sphinx < 4.1 John Snow
2025-03-11 3:42 ` [PATCH v3 30/63] docs/qapi-domain: warn when QAPI domain xrefs fail to resolve John Snow
2025-03-11 3:42 ` [PATCH v3 31/63] docs/qapi-domain: Fix error context reporting in Sphinx 5.x and 6.x John Snow
2025-03-11 3:42 ` [PATCH v3 32/63] qapi/parser: adjust info location for doc body section John Snow
2025-03-11 7:01 ` Markus Armbruster
2025-03-11 3:42 ` [PATCH v3 33/63] qapi: clean up encoding of section kinds John Snow
2025-03-11 7:02 ` Markus Armbruster
2025-03-11 3:42 ` [PATCH v3 34/63] qapi/schema: add __repr__ to QAPIDoc.Section John Snow
2025-03-11 7:03 ` Markus Armbruster
2025-03-11 3:42 ` [PATCH v3 35/63] docs/qapidoc: add transmogrifier stub John Snow
2025-03-11 3:42 ` [PATCH v3 36/63] docs/qapidoc: split old implementation into qapidoc_legacy.py John Snow
2025-03-11 3:42 ` [PATCH v3 37/63] docs/qapidoc: Fix static typing on qapidoc.py John Snow
2025-03-11 3:42 ` [PATCH v3 38/63] do-not-merge John Snow
2025-03-11 3:42 ` [PATCH v3 39/63] docs/qapidoc: add transmogrifier class stub John Snow
2025-03-11 3:42 ` [PATCH v3 40/63] docs/qapidoc: add visit_module() method John Snow
2025-03-11 3:42 ` [PATCH v3 41/63] qapi/source: allow multi-line QAPISourceInfo advancing John Snow
2025-03-11 3:42 ` [PATCH v3 42/63] docs/qapidoc: add visit_freeform() method John Snow
2025-03-11 3:42 ` [PATCH v3 43/63] docs/qapidoc: add preamble() method John Snow
2025-03-11 3:42 ` [PATCH v3 44/63] docs/qapidoc: add visit_paragraph() method John Snow
2025-03-11 3:42 ` [PATCH v3 45/63] docs/qapidoc: add visit_errors() method John Snow
2025-03-11 3:42 ` [PATCH v3 46/63] docs/qapidoc: add format_type() method John Snow
2025-03-11 3:42 ` [PATCH v3 47/63] docs/qapidoc: add add_field() and generate_field() helper methods John Snow
2025-03-11 3:42 ` [PATCH v3 48/63] docs/qapidoc: add visit_feature() method John Snow
2025-03-11 3:42 ` [PATCH v3 49/63] docs/qapidoc: prepare to record entity being transmogrified John Snow
2025-03-11 3:42 ` [PATCH v3 50/63] docs/qapidoc: add visit_returns() method John Snow
2025-03-11 3:42 ` [PATCH v3 51/63] docs/qapidoc: add visit_member() method John Snow
2025-03-11 3:42 ` [PATCH v3 52/63] docs/qapidoc: add visit_sections() method John Snow
2025-03-11 8:34 ` Markus Armbruster
2025-03-11 3:42 ` [PATCH v3 53/63] docs/qapidoc: add visit_entity() John Snow
2025-03-11 3:42 ` [PATCH v3 54/63] docs/qapidoc: implement transmogrify() method John Snow
2025-03-11 8:52 ` Markus Armbruster
2025-03-11 3:42 ` [PATCH v3 55/63] docs/qapidoc: process @foo into ``foo`` John Snow
2025-03-11 3:42 ` [PATCH v3 56/63] docs/qapidoc: add intermediate output debugger John Snow
2025-03-11 3:42 ` [PATCH v3 57/63] docs/qapidoc: Add "the members of" pointers John Snow
2025-03-11 3:42 ` [PATCH v3 58/63] docs/qapidoc: generate entries for undocumented members John Snow
2025-03-11 9:22 ` Markus Armbruster
2025-03-11 3:42 ` [PATCH v3 59/63] qapi/parser: add undocumented stub members to all_sections John Snow
2025-03-11 7:04 ` Markus Armbruster
2025-03-11 8:14 ` Markus Armbruster
2025-03-11 3:42 ` [PATCH v3 60/63] docs: disambiguate cross-references John Snow
2025-03-11 3:42 ` [PATCH v3 61/63] docs: enable qapidoc transmogrifier for QEMU QMP Reference John Snow
2025-03-11 3:43 ` [PATCH v3 62/63] docs: add qapi-domain syntax documentation John Snow
2025-03-11 3:43 ` [PATCH v3 63/63] MAINTAINERS: Add jsnow as maintainer for Sphinx documentation John Snow
2025-03-11 8:52 ` [PATCH v3 00/63] docs: Add new QAPI transmogrifier Markus Armbruster
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).