...
...
13
- Didn't finish addressing all of Markus' feedback, but needed to get
13
- Didn't finish addressing all of Markus' feedback, but needed to get
14
another spin out on the list ASAP.
14
another spin out on the list ASAP.
15
15
16
v2:
16
v2:
17
17
18
- Did you know that my computer just shut off the moment I started
19
sending the patchset? Just a hard off. Wow!
18
- Refactored QAPIObject class so that QAPIModule uses more of
20
- Refactored QAPIObject class so that QAPIModule uses more of
19
Sphinx's ObjectDescription class, which means less fooling around with
21
Sphinx's ObjectDescription class, which means less fooling around with
20
re-parsing Sphinx standard options.
22
re-parsing Sphinx standard options.
21
- Removed test document, flipped switch on the real QMP manual.
23
- Removed test document, flipped switch on the real QMP manual.
22
- Undocumented members have been re-added. I think that's new for this version!
24
- Undocumented members have been re-added. I think that's new for this version!
...
...
diff view generated by jsdifflib
1
Ad-hoc linting scripts to scrub down the new docs/sphinx files. Should
1
Ad-hoc linting scripts to scrub down the new docs/sphinx files. Should
2
work with a reasonably modern mypy/pylint/etc, and Sphinx 8.2.0. Older
2
work with a reasonably modern mypy/pylint/etc, and Sphinx 8.2.0. Older
3
versions of Sphinx ought to still work at runtime, but may not type
3
versions of Sphinx ought to still work at runtime, but may not type
4
check correctly.
4
check correctly.
5
5
6
Signed-off-by: John Snow <jsnow@redhat.com>
6
Signed-off-by: John Snow <jsnow@redhat.com>
7
---
7
---
8
scripts/qapi-lint.sh | 57 ++++++++++++++++++++++++++++++++++++++++++++
8
scripts/qapi-lint.sh | 57 ++++++++++++++++++++++++++++++++++++++++++++
9
1 file changed, 57 insertions(+)
9
1 file changed, 57 insertions(+)
10
create mode 100755 scripts/qapi-lint.sh
10
create mode 100755 scripts/qapi-lint.sh
11
11
12
diff --git a/scripts/qapi-lint.sh b/scripts/qapi-lint.sh
12
diff --git a/scripts/qapi-lint.sh b/scripts/qapi-lint.sh
13
new file mode 100755
13
new file mode 100755
14
index XXXXXXX..XXXXXXX
14
index XXXXXXX..XXXXXXX
15
--- /dev/null
15
--- /dev/null
16
+++ b/scripts/qapi-lint.sh
16
+++ b/scripts/qapi-lint.sh
17
@@ -XXX,XX +XXX,XX @@
17
@@ -XXX,XX +XXX,XX @@
18
+#!/usr/bin/env bash
18
+#!/usr/bin/env bash
19
+set -e
19
+set -e
20
+
20
+
21
+if [[ -f qapi/.flake8 ]]; then
21
+if [[ -f qapi/.flake8 ]]; then
22
+ echo "flake8 --config=qapi/.flake8 qapi/"
22
+ echo "flake8 --config=qapi/.flake8 qapi/"
23
+ flake8 --config=qapi/.flake8 qapi/
23
+ flake8 --config=qapi/.flake8 qapi/
24
+fi
24
+fi
25
+if [[ -f qapi/pylintrc ]]; then
25
+if [[ -f qapi/pylintrc ]]; then
26
+ echo "pylint --rcfile=qapi/pylintrc qapi/"
26
+ echo "pylint --rcfile=qapi/pylintrc qapi/"
27
+ pylint --rcfile=qapi/pylintrc qapi/
27
+ pylint --rcfile=qapi/pylintrc qapi/
28
+fi
28
+fi
29
+if [[ -f qapi/mypy.ini ]]; then
29
+if [[ -f qapi/mypy.ini ]]; then
30
+ echo "mypy --config-file=qapi/mypy.ini qapi/"
30
+ echo "mypy --config-file=qapi/mypy.ini qapi/"
31
+ mypy --config-file=qapi/mypy.ini qapi/
31
+ mypy --config-file=qapi/mypy.ini qapi/
32
+fi
32
+fi
33
+
33
+
34
+if [[ -f qapi/.isort.cfg ]]; then
34
+if [[ -f qapi/.isort.cfg ]]; then
35
+ pushd qapi
35
+ pushd qapi
36
+ echo "isort -c ."
36
+ echo "isort -c ."
37
+ isort -c .
37
+ isort -c .
38
+ popd
38
+ popd
39
+fi
39
+fi
40
+
40
+
41
+if [[ -f ../docs/sphinx/qapi_domain.py ]]; then
41
+if [[ -f ../docs/sphinx/qapi_domain.py ]]; then
42
+ files="qapi_domain.py"
42
+ files="qapi_domain.py"
43
+fi
43
+fi
44
+if [[ -f ../docs/sphinx/compat.py ]]; then
44
+if [[ -f ../docs/sphinx/compat.py ]]; then
45
+ files="${files} compat.py"
45
+ files="${files} compat.py"
46
+fi
46
+fi
47
+if [[ -f ../docs/sphinx/collapse.py ]]; then
47
+if [[ -f ../docs/sphinx/collapse.py ]]; then
48
+ files="${files} collapse.py"
48
+ files="${files} collapse.py"
49
+fi
49
+fi
50
+
50
+
51
+if [[ -f ../docs/sphinx/qapi_domain.py ]]; then
51
+if [[ -f ../docs/sphinx/qapi_domain.py ]]; then
52
+ pushd ../docs/sphinx
52
+ pushd ../docs/sphinx
53
+
53
+
54
+ set -x
54
+ set -x
55
+ mypy --strict $files
55
+ mypy --strict $files
56
+ flake8 --max-line-length=80 $files qapidoc.py
56
+ flake8 --max-line-length=80 $files qapidoc.py
57
+ isort -c $files qapidoc.py
57
+ isort -c $files qapidoc.py
58
+ black --line-length 80 --check $files qapidoc.py
58
+ black --line-length 80 --check $files qapidoc.py
59
+ PYTHONPATH=../../scripts/ pylint \
59
+ PYTHONPATH=../../scripts/ pylint \
60
+ --rc-file ../../scripts/qapi/pylintrc \
60
+ --rc-file ../../scripts/qapi/pylintrc \
61
+ $files qapidoc.py
61
+ $files qapidoc.py
62
+ set +x
62
+ set +x
63
+
63
+
64
+ popd
64
+ popd
65
+fi
65
+fi
66
+
66
+
67
+pushd ../build
67
+pushd ../build
68
+#make -j13
68
+#make -j13
69
+make check-qapi-schema
69
+make check-qapi-schema
70
+rm -rf docs/
70
+rm -rf docs/
71
+#make docs
71
+#make docs
72
+#make sphinxdocs
72
+#make sphinxdocs
73
+time pyvenv/bin/sphinx-build -v -j 8 -b html -d docs/manual.p/ ../docs/ docs/manual/;
73
+time pyvenv/bin/sphinx-build -v -j 8 -b html -d docs/manual.p/ ../docs/ docs/manual/;
74
+popd
74
+popd
75
--
75
--
76
2.48.1
76
2.48.1
diff view generated by jsdifflib
New patch
1
Shhhhh!
1
2
3
This patch is RFC quality, I wasn't in the mood to actually solve
4
problems so much as I was in the mood to continue working on the Sphinx
5
rework. Plus, I don't think the code I am patching has hit origin/master
6
yet ...
7
8
Signed-off-by: John Snow <jsnow@redhat.com>
9
---
10
scripts/qapi/backend.py | 2 ++
11
scripts/qapi/main.py | 8 +++-----
12
2 files changed, 5 insertions(+), 5 deletions(-)
13
14
diff --git a/scripts/qapi/backend.py b/scripts/qapi/backend.py
15
index XXXXXXX..XXXXXXX 100644
16
--- a/scripts/qapi/backend.py
17
+++ b/scripts/qapi/backend.py
18
@@ -XXX,XX +XXX,XX @@
19
20
21
class QAPIBackend(ABC):
22
+ # pylint: disable=too-few-public-methods
23
24
@abstractmethod
25
def generate(self,
26
@@ -XXX,XX +XXX,XX @@ def generate(self,
27
28
29
class QAPICBackend(QAPIBackend):
30
+ # pylint: disable=too-few-public-methods
31
32
def generate(self,
33
schema: QAPISchema,
34
diff --git a/scripts/qapi/main.py b/scripts/qapi/main.py
35
index XXXXXXX..XXXXXXX 100644
36
--- a/scripts/qapi/main.py
37
+++ b/scripts/qapi/main.py
38
@@ -XXX,XX +XXX,XX @@ def create_backend(path: str) -> QAPIBackend:
39
try:
40
mod = import_module(module_path)
41
except Exception as ex:
42
- print(f"unable to import '{module_path}': {ex}", file=sys.stderr)
43
- sys.exit(1)
44
+ raise QAPIError(f"unable to import '{module_path}': {ex}") from ex
45
46
try:
47
klass = getattr(mod, class_name)
48
@@ -XXX,XX +XXX,XX @@ def create_backend(path: str) -> QAPIBackend:
49
try:
50
backend = klass()
51
except Exception as ex:
52
- print(f"backend '{path}' cannot be instantiated: {ex}",
53
- file=sys.stderr)
54
- sys.exit(1)
55
+ raise QAPIError(
56
+ f"backend '{path}' cannot be instantiated: {ex}") from ex
57
58
if not isinstance(backend, QAPIBackend):
59
print(f"backend '{path}' must be an instance of QAPIBackend",
60
--
61
2.48.1
diff view generated by jsdifflib
New patch
1
A Sphinx domain is a collection of directive and role extensions meant
2
to facilitate the documentation of a specific language. For instance,
3
Sphinx ships with "python" and "cpp" domains. This patch introduces a
4
stub for the "qapi" language domain.
1
5
6
Please see https://www.sphinx-doc.org/en/master/usage/domains/index.html
7
for more information.
8
9
This stub doesn't really do anything yet, we'll get to it brick-by-brick
10
in the forthcoming commits to keep the series breezy and the git history
11
informative.
12
13
Signed-off-by: John Snow <jsnow@redhat.com>
14
---
15
docs/conf.py | 9 +++++-
16
docs/sphinx/qapi_domain.py | 56 ++++++++++++++++++++++++++++++++++++++
17
2 files changed, 64 insertions(+), 1 deletion(-)
18
create mode 100644 docs/sphinx/qapi_domain.py
19
20
diff --git a/docs/conf.py b/docs/conf.py
21
index XXXXXXX..XXXXXXX 100644
22
--- a/docs/conf.py
23
+++ b/docs/conf.py
24
@@ -XXX,XX +XXX,XX @@
25
# Add any Sphinx extension module names here, as strings. They can be
26
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
27
# ones.
28
-extensions = ['kerneldoc', 'qmp_lexer', 'hxtool', 'depfile', 'qapidoc']
29
+extensions = [
30
+ 'depfile',
31
+ 'hxtool',
32
+ 'kerneldoc',
33
+ 'qapi_domain',
34
+ 'qapidoc',
35
+ 'qmp_lexer',
36
+]
37
38
if sphinx.version_info[:3] > (4, 0, 0):
39
tags.add('sphinx4')
40
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
41
new file mode 100644
42
index XXXXXXX..XXXXXXX
43
--- /dev/null
44
+++ b/docs/sphinx/qapi_domain.py
45
@@ -XXX,XX +XXX,XX @@
46
+"""
47
+QAPI domain extension.
48
+"""
49
+
50
+from __future__ import annotations
51
+
52
+from typing import (
53
+ TYPE_CHECKING,
54
+ AbstractSet,
55
+ Any,
56
+ Dict,
57
+ Tuple,
58
+)
59
+
60
+from sphinx.domains import Domain, ObjType
61
+from sphinx.util import logging
62
+
63
+
64
+if TYPE_CHECKING:
65
+ from sphinx.application import Sphinx
66
+
67
+logger = logging.getLogger(__name__)
68
+
69
+
70
+class QAPIDomain(Domain):
71
+ """QAPI language domain."""
72
+
73
+ name = "qapi"
74
+ label = "QAPI"
75
+
76
+ object_types: Dict[str, ObjType] = {}
77
+ directives = {}
78
+ roles = {}
79
+ initial_data: Dict[str, Dict[str, Tuple[Any]]] = {}
80
+ indices = []
81
+
82
+ def merge_domaindata(
83
+ self, docnames: AbstractSet[str], otherdata: Dict[str, Any]
84
+ ) -> None:
85
+ pass
86
+
87
+ def resolve_any_xref(self, *args: Any, **kwargs: Any) -> Any:
88
+ # pylint: disable=unused-argument
89
+ return []
90
+
91
+
92
+def setup(app: Sphinx) -> Dict[str, Any]:
93
+ app.setup_extension("sphinx.directives")
94
+ app.add_domain(QAPIDomain)
95
+
96
+ return {
97
+ "version": "1.0",
98
+ "env_version": 1,
99
+ "parallel_read_safe": True,
100
+ "parallel_write_safe": True,
101
+ }
102
--
103
2.48.1
diff view generated by jsdifflib
New patch
1
Create a compat module that handles sphinx cross-version compatibility
2
issues. For the inaugural function, add a nested_parse() helper that
3
handles differences in line number tracking for nested directive body
4
parsing.
1
5
6
Spoilers: there are more cross-version hacks to come throughout the
7
series.
8
9
Signed-off-by: John Snow <jsnow@redhat.com>
10
---
11
docs/sphinx/compat.py | 35 +++++++++++++++++++++++++++++++++++
12
1 file changed, 35 insertions(+)
13
create mode 100644 docs/sphinx/compat.py
14
15
diff --git a/docs/sphinx/compat.py b/docs/sphinx/compat.py
16
new file mode 100644
17
index XXXXXXX..XXXXXXX
18
--- /dev/null
19
+++ b/docs/sphinx/compat.py
20
@@ -XXX,XX +XXX,XX @@
21
+"""
22
+Sphinx cross-version compatibility goop
23
+"""
24
+
25
+from docutils.nodes import Element
26
+
27
+from sphinx.util import nodes
28
+from sphinx.util.docutils import SphinxDirective, switch_source_input
29
+
30
+
31
+def nested_parse_with_titles(
32
+ directive: SphinxDirective, content_node: Element
33
+) -> None:
34
+ """
35
+ This helper preserves error parsing context across sphinx versions.
36
+ """
37
+
38
+ # necessary so that the child nodes get the right source/line set
39
+ content_node.document = directive.state.document
40
+
41
+ try:
42
+ # Modern sphinx (6.2.0+) supports proper offsetting for
43
+ # nested parse error context management
44
+ nodes.nested_parse_with_titles(
45
+ directive.state,
46
+ directive.content,
47
+ content_node,
48
+ content_offset=directive.content_offset,
49
+ )
50
+ except TypeError:
51
+ # No content_offset argument. Fall back to SSI method.
52
+ with switch_source_input(directive.state, directive.content):
53
+ nodes.nested_parse_with_titles(
54
+ directive.state, directive.content, content_node
55
+ )
56
--
57
2.48.1
diff view generated by jsdifflib
New patch
1
This is the first step towards QAPI domain cross-references and a QAPI
2
reference index.
1
3
4
This patch just creates the object registry, and updates the
5
merge_domaindata stub method now that we have actual data we may need to
6
merge.
7
8
Note that how to handle merge conflict resolution is unhandled, as the
9
Sphinx python domain itself does not handle it either. I do not know how
10
to intentionally trigger it, so I've left an assertion instead if it
11
should ever come up ...
12
13
Signed-off-by: John Snow <jsnow@redhat.com>
14
---
15
docs/sphinx/qapi_domain.py | 77 +++++++++++++++++++++++++++++++++++++-
16
1 file changed, 75 insertions(+), 2 deletions(-)
17
18
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
19
index XXXXXXX..XXXXXXX 100644
20
--- a/docs/sphinx/qapi_domain.py
21
+++ b/docs/sphinx/qapi_domain.py
22
@@ -XXX,XX +XXX,XX @@
23
AbstractSet,
24
Any,
25
Dict,
26
+ NamedTuple,
27
Tuple,
28
)
29
30
from sphinx.domains import Domain, ObjType
31
+from sphinx.locale import __
32
from sphinx.util import logging
33
34
35
@@ -XXX,XX +XXX,XX @@
36
logger = logging.getLogger(__name__)
37
38
39
+class ObjectEntry(NamedTuple):
40
+ docname: str
41
+ node_id: str
42
+ objtype: str
43
+ aliased: bool
44
+
45
+
46
class QAPIDomain(Domain):
47
"""QAPI language domain."""
48
49
name = "qapi"
50
label = "QAPI"
51
52
+ # This table associates cross-reference object types (key) with an
53
+ # ObjType instance, which defines the valid cross-reference roles
54
+ # for each object type.
55
+
56
+ # Actual table entries for module, command, event, etc will come in
57
+ # forthcoming commits.
58
object_types: Dict[str, ObjType] = {}
59
+
60
directives = {}
61
roles = {}
62
- initial_data: Dict[str, Dict[str, Tuple[Any]]] = {}
63
+
64
+ # Moved into the data property at runtime;
65
+ # this is the internal index of reference-able objects.
66
+ initial_data: Dict[str, Dict[str, Tuple[Any]]] = {
67
+ "objects": {}, # fullname -> ObjectEntry
68
+ }
69
+
70
indices = []
71
72
+ @property
73
+ def objects(self) -> Dict[str, ObjectEntry]:
74
+ ret = self.data.setdefault("objects", {})
75
+ return ret # type: ignore[no-any-return]
76
+
77
+ def note_object(
78
+ self,
79
+ name: str,
80
+ objtype: str,
81
+ node_id: str,
82
+ aliased: bool = False,
83
+ location: Any = None,
84
+ ) -> None:
85
+ """Note a QAPI object for cross reference."""
86
+ if name in self.objects:
87
+ other = self.objects[name]
88
+ if other.aliased and aliased is False:
89
+ # The original definition found. Override it!
90
+ pass
91
+ elif other.aliased is False and aliased:
92
+ # The original definition is already registered.
93
+ return
94
+ else:
95
+ # duplicated
96
+ logger.warning(
97
+ __(
98
+ "duplicate object description of %s, "
99
+ "other instance in %s, use :no-index: for one of them"
100
+ ),
101
+ name,
102
+ other.docname,
103
+ location=location,
104
+ )
105
+ self.objects[name] = ObjectEntry(
106
+ self.env.docname, node_id, objtype, aliased
107
+ )
108
+
109
+ def clear_doc(self, docname: str) -> None:
110
+ for fullname, obj in list(self.objects.items()):
111
+ if obj.docname == docname:
112
+ del self.objects[fullname]
113
+
114
def merge_domaindata(
115
self, docnames: AbstractSet[str], otherdata: Dict[str, Any]
116
) -> None:
117
- pass
118
+ for fullname, obj in otherdata["objects"].items():
119
+ if obj.docname in docnames:
120
+ # Sphinx's own python domain doesn't appear to bother to
121
+ # check for collisions. Assert they don't happen and
122
+ # we'll fix it if/when the case arises.
123
+ assert fullname not in self.objects, (
124
+ "bug - collision on merge?"
125
+ f" {fullname=} {obj=} {self.objects[fullname]=}"
126
+ )
127
+ self.objects[fullname] = obj
128
129
def resolve_any_xref(self, *args: Any, **kwargs: Any) -> Any:
130
# pylint: disable=unused-argument
131
--
132
2.48.1
diff view generated by jsdifflib
New patch
1
Use the QAPI object registry to generate a special index just for QAPI
2
definitions. The index can show entries both by definition type and all
3
together, alphabetically.
1
4
5
The index can be linked from anywhere in the QEMU manual by using the
6
reference `qapi-index`.
7
8
Signed-off-by: John Snow <jsnow@redhat.com>
9
---
10
docs/sphinx/qapi_domain.py | 73 ++++++++++++++++++++++++++++++++++++--
11
1 file changed, 70 insertions(+), 3 deletions(-)
12
13
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
14
index XXXXXXX..XXXXXXX 100644
15
--- a/docs/sphinx/qapi_domain.py
16
+++ b/docs/sphinx/qapi_domain.py
17
@@ -XXX,XX +XXX,XX @@
18
AbstractSet,
19
Any,
20
Dict,
21
+ Iterable,
22
+ List,
23
NamedTuple,
24
+ Optional,
25
Tuple,
26
)
27
28
-from sphinx.domains import Domain, ObjType
29
-from sphinx.locale import __
30
+from sphinx.domains import (
31
+ Domain,
32
+ Index,
33
+ IndexEntry,
34
+ ObjType,
35
+)
36
+from sphinx.locale import _, __
37
from sphinx.util import logging
38
39
40
@@ -XXX,XX +XXX,XX @@ class ObjectEntry(NamedTuple):
41
aliased: bool
42
43
44
+class QAPIIndex(Index):
45
+ """
46
+ Index subclass to provide the QAPI definition index.
47
+ """
48
+
49
+ # pylint: disable=too-few-public-methods
50
+
51
+ name = "index"
52
+ localname = _("QAPI Index")
53
+ shortname = _("QAPI Index")
54
+
55
+ def generate(
56
+ self,
57
+ docnames: Optional[Iterable[str]] = None,
58
+ ) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:
59
+ assert isinstance(self.domain, QAPIDomain)
60
+ content: Dict[str, List[IndexEntry]] = {}
61
+ collapse = False
62
+
63
+ # list of all object (name, ObjectEntry) pairs, sorted by name
64
+ # (ignoring the module)
65
+ objects = sorted(
66
+ self.domain.objects.items(),
67
+ key=lambda x: x[0].split(".")[-1].lower(),
68
+ )
69
+
70
+ for objname, obj in objects:
71
+ if docnames and obj.docname not in docnames:
72
+ continue
73
+
74
+ # Strip the module name out:
75
+ objname = objname.split(".")[-1]
76
+
77
+ # Add an alphabetical entry:
78
+ entries = content.setdefault(objname[0].upper(), [])
79
+ entries.append(
80
+ IndexEntry(
81
+ objname, 0, obj.docname, obj.node_id, obj.objtype, "", ""
82
+ )
83
+ )
84
+
85
+ # Add a categorical entry:
86
+ category = obj.objtype.title() + "s"
87
+ entries = content.setdefault(category, [])
88
+ entries.append(
89
+ IndexEntry(objname, 0, obj.docname, obj.node_id, "", "", "")
90
+ )
91
+
92
+ # alphabetically sort categories; type names first, ABC entries last.
93
+ sorted_content = sorted(
94
+ content.items(),
95
+ key=lambda x: (len(x[0]) == 1, x[0]),
96
+ )
97
+ return sorted_content, collapse
98
+
99
+
100
class QAPIDomain(Domain):
101
"""QAPI language domain."""
102
103
@@ -XXX,XX +XXX,XX @@ class QAPIDomain(Domain):
104
"objects": {}, # fullname -> ObjectEntry
105
}
106
107
- indices = []
108
+ # Index pages to generate; each entry is an Index class.
109
+ indices = [
110
+ QAPIIndex,
111
+ ]
112
113
@property
114
def objects(self) -> Dict[str, ObjectEntry]:
115
--
116
2.48.1
diff view generated by jsdifflib
New patch
1
Add the ability to resolve cross-references using the `any`
2
cross-reference syntax. Adding QAPI-specific cross-reference roles will
3
be added in a forthcoming commit, and will share the same find_obj()
4
helper.
1
5
6
(There's less code needed for the generic cross-reference resolver, so
7
it comes first in this series.)
8
9
Once again, this code is based very heavily on sphinx.domains.python.
10
11
Signed-off-by: John Snow <jsnow@redhat.com>
12
---
13
docs/sphinx/qapi_domain.py | 96 ++++++++++++++++++++++++++++++++++++--
14
1 file changed, 93 insertions(+), 3 deletions(-)
15
16
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
17
index XXXXXXX..XXXXXXX 100644
18
--- a/docs/sphinx/qapi_domain.py
19
+++ b/docs/sphinx/qapi_domain.py
20
@@ -XXX,XX +XXX,XX @@
21
Tuple,
22
)
23
24
+from docutils import nodes
25
+
26
+from sphinx.addnodes import pending_xref
27
from sphinx.domains import (
28
Domain,
29
Index,
30
@@ -XXX,XX +XXX,XX @@
31
)
32
from sphinx.locale import _, __
33
from sphinx.util import logging
34
+from sphinx.util.nodes import make_refnode
35
36
37
if TYPE_CHECKING:
38
+ from docutils.nodes import Element
39
+
40
from sphinx.application import Sphinx
41
+ from sphinx.builders import Builder
42
+ from sphinx.environment import BuildEnvironment
43
44
logger = logging.getLogger(__name__)
45
46
@@ -XXX,XX +XXX,XX @@ def merge_domaindata(
47
)
48
self.objects[fullname] = obj
49
50
- def resolve_any_xref(self, *args: Any, **kwargs: Any) -> Any:
51
- # pylint: disable=unused-argument
52
- return []
53
+ def find_obj(
54
+ self, modname: str, name: str, typ: Optional[str]
55
+ ) -> list[tuple[str, ObjectEntry]]:
56
+ """
57
+ Find a QAPI object for "name", perhaps using the given module.
58
+
59
+ Returns a list of (name, object entry) tuples.
60
+
61
+ :param modname: The current module context (if any!)
62
+ under which we are searching.
63
+ :param name: The name of the x-ref to resolve;
64
+ may or may not include a leading module.
65
+ :param type: The role name of the x-ref we're resolving, if provided.
66
+ (This is absent for "any" lookups.)
67
+ """
68
+ if not name:
69
+ return []
70
+
71
+ names: list[str] = []
72
+ matches: list[tuple[str, ObjectEntry]] = []
73
+
74
+ fullname = name
75
+ if "." in fullname:
76
+ # We're searching for a fully qualified reference;
77
+ # ignore the contextual module.
78
+ pass
79
+ elif modname:
80
+ # We're searching for something from somewhere;
81
+ # try searching the current module first.
82
+ # e.g. :qapi:cmd:`query-block` or `query-block` is being searched.
83
+ fullname = f"{modname}.{name}"
84
+
85
+ if typ is None:
86
+ # type isn't specified, this is a generic xref.
87
+ # search *all* qapi-specific object types.
88
+ objtypes: List[str] = list(self.object_types)
89
+ else:
90
+ # type is specified and will be a role (e.g. obj, mod, cmd)
91
+ # convert this to eligible object types (e.g. command, module)
92
+ # using the QAPIDomain.object_types table.
93
+ objtypes = self.objtypes_for_role(typ, [])
94
+
95
+ if name in self.objects and self.objects[name].objtype in objtypes:
96
+ names = [name]
97
+ elif (
98
+ fullname in self.objects
99
+ and self.objects[fullname].objtype in objtypes
100
+ ):
101
+ names = [fullname]
102
+ else:
103
+ # exact match wasn't found; e.g. we are searching for
104
+ # `query-block` from a different (or no) module.
105
+ searchname = "." + name
106
+ names = [
107
+ oname
108
+ for oname in self.objects
109
+ if oname.endswith(searchname)
110
+ and self.objects[oname].objtype in objtypes
111
+ ]
112
+
113
+ matches = [(oname, self.objects[oname]) for oname in names]
114
+ if len(matches) > 1:
115
+ matches = [m for m in matches if not m[1].aliased]
116
+ return matches
117
+
118
+ def resolve_any_xref(
119
+ self,
120
+ env: BuildEnvironment,
121
+ fromdocname: str,
122
+ builder: Builder,
123
+ target: str,
124
+ node: pending_xref,
125
+ contnode: Element,
126
+ ) -> List[Tuple[str, nodes.reference]]:
127
+ results: List[Tuple[str, nodes.reference]] = []
128
+ matches = self.find_obj(node.get("qapi:module"), target, None)
129
+ for name, obj in matches:
130
+ rolename = self.role_for_objtype(obj.objtype)
131
+ assert rolename is not None
132
+ role = f"qapi:{rolename}"
133
+ refnode = make_refnode(
134
+ builder, fromdocname, obj.docname, obj.node_id, contnode, name
135
+ )
136
+ results.append((role, refnode))
137
+ return results
138
139
140
def setup(app: Sphinx) -> Dict[str, Any]:
141
--
142
2.48.1
diff view generated by jsdifflib
New patch
1
Add domain-specific cross-reference syntax. As of this commit, that
2
means new :qapi:any:`block-core` referencing syntax.
1
3
4
The :any: role will find anything registered to the QAPI domain,
5
including modules, commands, events, etc.
6
7
Creating the cross-references is powered by the QAPIXRefRole class;
8
resolving them is handled by QAPIDomain.resolve_xref().
9
10
QAPIXrefRole is based heavily on Sphinx's own PyXrefRole, with
11
modifications necessary for QAPI features.
12
13
Signed-off-by: John Snow <jsnow@redhat.com>
14
---
15
docs/sphinx/qapi_domain.py | 88 +++++++++++++++++++++++++++++++++++++-
16
1 file changed, 87 insertions(+), 1 deletion(-)
17
18
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
19
index XXXXXXX..XXXXXXX 100644
20
--- a/docs/sphinx/qapi_domain.py
21
+++ b/docs/sphinx/qapi_domain.py
22
@@ -XXX,XX +XXX,XX @@
23
ObjType,
24
)
25
from sphinx.locale import _, __
26
+from sphinx.roles import XRefRole
27
from sphinx.util import logging
28
from sphinx.util.nodes import make_refnode
29
30
@@ -XXX,XX +XXX,XX @@ class ObjectEntry(NamedTuple):
31
aliased: bool
32
33
34
+class QAPIXRefRole(XRefRole):
35
+
36
+ def process_link(
37
+ self,
38
+ env: BuildEnvironment,
39
+ refnode: Element,
40
+ has_explicit_title: bool,
41
+ title: str,
42
+ target: str,
43
+ ) -> tuple[str, str]:
44
+ refnode["qapi:module"] = env.ref_context.get("qapi:module")
45
+
46
+ # Cross-references that begin with a tilde adjust the title to
47
+ # only show the reference without a leading module, even if one
48
+ # was provided. This is a Sphinx-standard syntax; give it
49
+ # priority over QAPI-specific type markup below.
50
+ hide_module = False
51
+ if target.startswith("~"):
52
+ hide_module = True
53
+ target = target[1:]
54
+
55
+ # Type names that end with "?" are considered optional
56
+ # arguments and should be documented as such, but it's not
57
+ # part of the xref itself.
58
+ if target.endswith("?"):
59
+ refnode["qapi:optional"] = True
60
+ target = target[:-1]
61
+
62
+ # Type names wrapped in brackets denote lists. strip the
63
+ # brackets and remember to add them back later.
64
+ if target.startswith("[") and target.endswith("]"):
65
+ refnode["qapi:array"] = True
66
+ target = target[1:-1]
67
+
68
+ if has_explicit_title:
69
+ # Don't mess with the title at all if it was explicitly set.
70
+ # Explicit title syntax for references is e.g.
71
+ # :qapi:type:`target <explicit title>`
72
+ # and this explicit title overrides everything else here.
73
+ return title, target
74
+
75
+ title = target
76
+ if hide_module:
77
+ title = target.split(".")[-1]
78
+
79
+ return title, target
80
+
81
+
82
class QAPIIndex(Index):
83
"""
84
Index subclass to provide the QAPI definition index.
85
@@ -XXX,XX +XXX,XX @@ class QAPIDomain(Domain):
86
object_types: Dict[str, ObjType] = {}
87
88
directives = {}
89
- roles = {}
90
+
91
+ # These are all cross-reference roles; e.g.
92
+ # :qapi:cmd:`query-block`. The keys correlate to the names used in
93
+ # the object_types table values above.
94
+ roles = {
95
+ "any": QAPIXRefRole(), # reference *any* type of QAPI object.
96
+ }
97
98
# Moved into the data property at runtime;
99
# this is the internal index of reference-able objects.
100
@@ -XXX,XX +XXX,XX @@ def find_obj(
101
matches = [m for m in matches if not m[1].aliased]
102
return matches
103
104
+ def resolve_xref(
105
+ self,
106
+ env: BuildEnvironment,
107
+ fromdocname: str,
108
+ builder: Builder,
109
+ typ: str,
110
+ target: str,
111
+ node: pending_xref,
112
+ contnode: Element,
113
+ ) -> nodes.reference | None:
114
+ modname = node.get("qapi:module")
115
+ matches = self.find_obj(modname, target, typ)
116
+
117
+ if not matches:
118
+ return None
119
+
120
+ if len(matches) > 1:
121
+ logger.warning(
122
+ __("more than one target found for cross-reference %r: %s"),
123
+ target,
124
+ ", ".join(match[0] for match in matches),
125
+ type="ref",
126
+ subtype="qapi",
127
+ location=node,
128
+ )
129
+
130
+ name, obj = matches[0]
131
+ return make_refnode(
132
+ builder, fromdocname, obj.docname, obj.node_id, contnode, name
133
+ )
134
+
135
def resolve_any_xref(
136
self,
137
env: BuildEnvironment,
138
--
139
2.48.1
diff view generated by jsdifflib
New patch
1
Sphinx prior to v4.0 uses different classes for rendering elements of
2
documentation objects; add some compatibility classes to use the right
3
node classes conditionally.
1
4
5
Signed-off-by: John Snow <jsnow@redhat.com>
6
---
7
docs/sphinx/compat.py | 17 ++++++++++++++++-
8
1 file changed, 16 insertions(+), 1 deletion(-)
9
10
diff --git a/docs/sphinx/compat.py b/docs/sphinx/compat.py
11
index XXXXXXX..XXXXXXX 100644
12
--- a/docs/sphinx/compat.py
13
+++ b/docs/sphinx/compat.py
14
@@ -XXX,XX +XXX,XX @@
15
Sphinx cross-version compatibility goop
16
"""
17
18
-from docutils.nodes import Element
19
+from typing import Callable
20
21
+from docutils.nodes import Element, Node, Text
22
+
23
+import sphinx
24
+from sphinx import addnodes
25
from sphinx.util import nodes
26
from sphinx.util.docutils import SphinxDirective, switch_source_input
27
28
29
+SpaceNode: Callable[[str], Node]
30
+KeywordNode: Callable[[str, str], Node]
31
+
32
+if sphinx.version_info[:3] >= (4, 0, 0):
33
+ SpaceNode = addnodes.desc_sig_space
34
+ KeywordNode = addnodes.desc_sig_keyword
35
+else:
36
+ SpaceNode = Text
37
+ KeywordNode = addnodes.desc_annotation
38
+
39
+
40
def nested_parse_with_titles(
41
directive: SphinxDirective, content_node: Element
42
) -> None:
43
--
44
2.48.1
diff view generated by jsdifflib
New patch
1
This class is a generic, top-level directive for documenting some kind
2
of QAPI thingamajig that we expect to go into the Index. This class
3
doesn't do much by itself, and it isn't yet associated with any
4
particular directive.
1
5
6
Only handle_signature() is defined in the base class; get_index_text and
7
add_target_and_index are new methods defined here; they are based
8
heavily on the layout and format of the Python domain's general object
9
class.
10
11
Signed-off-by: John Snow <jsnow@redhat.com>
12
---
13
docs/sphinx/qapi_domain.py | 65 ++++++++++++++++++++++++++++++++++++--
14
1 file changed, 63 insertions(+), 2 deletions(-)
15
16
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
17
index XXXXXXX..XXXXXXX 100644
18
--- a/docs/sphinx/qapi_domain.py
19
+++ b/docs/sphinx/qapi_domain.py
20
@@ -XXX,XX +XXX,XX @@
21
NamedTuple,
22
Optional,
23
Tuple,
24
+ cast,
25
)
26
27
from docutils import nodes
28
29
-from sphinx.addnodes import pending_xref
30
+from sphinx.addnodes import desc_signature, pending_xref
31
+from sphinx.directives import ObjectDescription
32
from sphinx.domains import (
33
Domain,
34
Index,
35
@@ -XXX,XX +XXX,XX @@
36
from sphinx.locale import _, __
37
from sphinx.roles import XRefRole
38
from sphinx.util import logging
39
-from sphinx.util.nodes import make_refnode
40
+from sphinx.util.nodes import make_id, make_refnode
41
42
43
if TYPE_CHECKING:
44
@@ -XXX,XX +XXX,XX @@ def process_link(
45
return title, target
46
47
48
+Signature = str
49
+
50
+
51
+class QAPIDescription(ObjectDescription[Signature]):
52
+ """
53
+ Generic QAPI description.
54
+
55
+ Abstract class, not instantiated directly.
56
+ """
57
+
58
+ def handle_signature(self, sig: str, signode: desc_signature) -> Signature:
59
+ # Do nothing. The return value here is the "name" of the entity
60
+ # being documented; for QAPI, this is the same as the
61
+ # "signature", which is just a name.
62
+
63
+ # Normally this method must also populate signode with nodes to
64
+ # render the signature; here we do nothing instead.
65
+ return sig
66
+
67
+ def get_index_text(self, name: Signature) -> Tuple[str, str]:
68
+ """Return the text for the index entry of the object."""
69
+
70
+ # NB: this is used for the global index, not the QAPI index.
71
+ return ("single", f"{name} (QMP {self.objtype})")
72
+
73
+ def add_target_and_index(
74
+ self, name: Signature, sig: str, signode: desc_signature
75
+ ) -> None:
76
+ # name is the return value of handle_signature.
77
+ # sig is the original, raw text argument to handle_signature.
78
+ # For QAPI, these are identical, currently.
79
+
80
+ assert self.objtype
81
+
82
+ # If we're documenting a module, don't include the module as
83
+ # part of the FQN.
84
+ modname = ""
85
+ if self.objtype != "module":
86
+ modname = self.options.get(
87
+ "module", self.env.ref_context.get("qapi:module")
88
+ )
89
+ fullname = (modname + "." if modname else "") + name
90
+
91
+ node_id = make_id(self.env, self.state.document, self.objtype, fullname)
92
+ signode["ids"].append(node_id)
93
+
94
+ self.state.document.note_explicit_target(signode)
95
+ domain = cast(QAPIDomain, self.env.get_domain("qapi"))
96
+ domain.note_object(fullname, self.objtype, node_id, location=signode)
97
+
98
+ if "no-index-entry" not in self.options:
99
+ arity, indextext = self.get_index_text(name)
100
+ assert self.indexnode is not None
101
+ if indextext:
102
+ self.indexnode["entries"].append(
103
+ (arity, indextext, node_id, "", None)
104
+ )
105
+
106
+
107
class QAPIIndex(Index):
108
"""
109
Index subclass to provide the QAPI definition index.
110
--
111
2.48.1
diff view generated by jsdifflib
New patch
1
This adds the qapi:module directive, which just notes the current module
2
being documented and performs a nested parse of the content block, if
3
present.
1
4
5
This code is based pretty heavily on Sphinx's PyModule directive, but
6
with unnecessary features excised.
7
8
For example:
9
10
.. qapi:module:: block-core
11
12
Hello, and welcome to block-core!
13
=================================
14
15
lorem ipsum, dolor sit amet ...
16
17
Signed-off-by: John Snow <jsnow@redhat.com>
18
---
19
docs/sphinx/qapi_domain.py | 71 ++++++++++++++++++++++++++++++++++----
20
1 file changed, 65 insertions(+), 6 deletions(-)
21
22
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
23
index XXXXXXX..XXXXXXX 100644
24
--- a/docs/sphinx/qapi_domain.py
25
+++ b/docs/sphinx/qapi_domain.py
26
@@ -XXX,XX +XXX,XX @@
27
28
from docutils import nodes
29
30
+from sphinx import addnodes
31
from sphinx.addnodes import desc_signature, pending_xref
32
from sphinx.directives import ObjectDescription
33
from sphinx.domains import (
34
@@ -XXX,XX +XXX,XX @@
35
36
37
if TYPE_CHECKING:
38
- from docutils.nodes import Element
39
+ from docutils.nodes import Element, Node
40
41
from sphinx.application import Sphinx
42
from sphinx.builders import Builder
43
@@ -XXX,XX +XXX,XX @@ def add_target_and_index(
44
)
45
46
47
+class QAPIModule(QAPIDescription):
48
+ """
49
+ Directive to mark description of a new module.
50
+
51
+ This directive doesn't generate any special formatting, and is just
52
+ a pass-through for the content body. Named section titles are
53
+ allowed in the content body.
54
+
55
+ Use this directive to create entries for the QAPI module in the
56
+ global index and the qapi index; as well as to associate subsequent
57
+ definitions with the module they are defined in for purposes of
58
+ search and QAPI index organization.
59
+
60
+ :arg: The name of the module.
61
+ :opt no-index: Don't add cross-reference targets or index entries.
62
+ :opt no-typesetting: Don't render the content body (but preserve any
63
+ cross-reference target IDs in the squelched output.)
64
+
65
+ Example::
66
+
67
+ .. qapi:module:: block-core
68
+ :no-index:
69
+ :no-typesetting:
70
+
71
+ Lorem ipsum, dolor sit amet ...
72
+ """
73
+
74
+ def run(self) -> List[Node]:
75
+ modname = self.arguments[0].strip()
76
+ self.env.ref_context["qapi:module"] = modname
77
+ ret = super().run()
78
+
79
+ # ObjectDescription always creates a visible signature bar. We
80
+ # want module items to be "invisible", however.
81
+
82
+ # Extract the content body of the directive:
83
+ assert isinstance(ret[-1], addnodes.desc)
84
+ desc_node = ret.pop(-1)
85
+ assert isinstance(desc_node.children[1], addnodes.desc_content)
86
+ ret.extend(desc_node.children[1].children)
87
+
88
+ # Re-home node_ids so anchor refs still work:
89
+ node_ids: List[str]
90
+ if node_ids := [
91
+ node_id
92
+ for el in desc_node.children[0].traverse(nodes.Element)
93
+ for node_id in cast(List[str], el.get("ids", ()))
94
+ ]:
95
+ target_node = nodes.target(ids=node_ids)
96
+ ret.insert(1, target_node)
97
+
98
+ return ret
99
+
100
+
101
class QAPIIndex(Index):
102
"""
103
Index subclass to provide the QAPI definition index.
104
@@ -XXX,XX +XXX,XX @@ class QAPIDomain(Domain):
105
# This table associates cross-reference object types (key) with an
106
# ObjType instance, which defines the valid cross-reference roles
107
# for each object type.
108
+ object_types: Dict[str, ObjType] = {
109
+ "module": ObjType(_("module"), "mod", "any"),
110
+ }
111
112
- # Actual table entries for module, command, event, etc will come in
113
- # forthcoming commits.
114
- object_types: Dict[str, ObjType] = {}
115
-
116
- directives = {}
117
+ # Each of these provides a rST directive,
118
+ # e.g. .. qapi:module:: block-core
119
+ directives = {
120
+ "module": QAPIModule,
121
+ }
122
123
# These are all cross-reference roles; e.g.
124
# :qapi:cmd:`query-block`. The keys correlate to the names used in
125
# the object_types table values above.
126
roles = {
127
+ "mod": QAPIXRefRole(),
128
"any": QAPIXRefRole(), # reference *any* type of QAPI object.
129
}
130
131
--
132
2.48.1
diff view generated by jsdifflib
New patch
1
Signed-off-by: John Snow <jsnow@redhat.com>
2
---
3
docs/sphinx/qapi_domain.py | 95 ++++++++++++++++++++++++++++++++++++++
4
1 file changed, 95 insertions(+)
1
5
6
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
7
index XXXXXXX..XXXXXXX 100644
8
--- a/docs/sphinx/qapi_domain.py
9
+++ b/docs/sphinx/qapi_domain.py
10
@@ -XXX,XX +XXX,XX @@
11
)
12
13
from docutils import nodes
14
+from docutils.parsers.rst import directives
15
16
+from compat import KeywordNode, SpaceNode
17
from sphinx import addnodes
18
from sphinx.addnodes import desc_signature, pending_xref
19
from sphinx.directives import ObjectDescription
20
@@ -XXX,XX +XXX,XX @@
21
from sphinx.application import Sphinx
22
from sphinx.builders import Builder
23
from sphinx.environment import BuildEnvironment
24
+ from sphinx.util.typing import OptionSpec
25
26
logger = logging.getLogger(__name__)
27
28
@@ -XXX,XX +XXX,XX @@ def process_link(
29
return title, target
30
31
32
+# Alias for the return of handle_signature(), which is used in several places.
33
+# (In the Python domain, this is Tuple[str, str] instead.)
34
Signature = str
35
36
37
@@ -XXX,XX +XXX,XX @@ def add_target_and_index(
38
)
39
40
41
+class QAPIObject(QAPIDescription):
42
+ """
43
+ Description of a generic QAPI object.
44
+
45
+ It's not used directly, but is instead subclassed by specific directives.
46
+ """
47
+
48
+ # Inherit some standard options from Sphinx's ObjectDescription
49
+ option_spec: OptionSpec = ( # type:ignore[misc]
50
+ ObjectDescription.option_spec.copy()
51
+ )
52
+ option_spec.update(
53
+ {
54
+ # Borrowed from the Python domain:
55
+ "module": directives.unchanged, # Override contextual module name
56
+ }
57
+ )
58
+
59
+ def get_signature_prefix(self) -> List[nodes.Node]:
60
+ """Returns a prefix to put before the object name in the signature."""
61
+ assert self.objtype
62
+ return [
63
+ KeywordNode("", self.objtype.title()),
64
+ SpaceNode(" "),
65
+ ]
66
+
67
+ def get_signature_suffix(self) -> List[nodes.Node]:
68
+ """Returns a suffix to put after the object name in the signature."""
69
+ return []
70
+
71
+ def handle_signature(self, sig: str, signode: desc_signature) -> Signature:
72
+ """
73
+ Transform a QAPI definition name into RST nodes.
74
+
75
+ This method was originally intended for handling function
76
+ signatures. In the QAPI domain, however, we only pass the
77
+ definition name as the directive argument and handle everything
78
+ else in the content body with field lists.
79
+
80
+ As such, the only argument here is "sig", which is just the QAPI
81
+ definition name.
82
+ """
83
+ modname = self.options.get(
84
+ "module", self.env.ref_context.get("qapi:module")
85
+ )
86
+
87
+ signode["fullname"] = sig
88
+ signode["module"] = modname
89
+ sig_prefix = self.get_signature_prefix()
90
+ if sig_prefix:
91
+ signode += addnodes.desc_annotation(
92
+ str(sig_prefix), "", *sig_prefix
93
+ )
94
+ signode += addnodes.desc_name(sig, sig)
95
+ signode += self.get_signature_suffix()
96
+
97
+ return sig
98
+
99
+ def _object_hierarchy_parts(
100
+ self, sig_node: desc_signature
101
+ ) -> Tuple[str, ...]:
102
+ if "fullname" not in sig_node:
103
+ return ()
104
+ modname = sig_node.get("module")
105
+ fullname = sig_node["fullname"]
106
+
107
+ if modname:
108
+ return (modname, *fullname.split("."))
109
+
110
+ return tuple(fullname.split("."))
111
+
112
+ def _toc_entry_name(self, sig_node: desc_signature) -> str:
113
+ # This controls the name in the TOC and on the sidebar.
114
+
115
+ # This is the return type of _object_hierarchy_parts().
116
+ toc_parts = cast(Tuple[str, ...], sig_node.get("_toc_parts", ()))
117
+ if not toc_parts:
118
+ return ""
119
+
120
+ config = self.env.app.config
121
+ *parents, name = toc_parts
122
+ if config.toc_object_entries_show_parents == "domain":
123
+ return sig_node.get("fullname", name)
124
+ if config.toc_object_entries_show_parents == "hide":
125
+ return name
126
+ if config.toc_object_entries_show_parents == "all":
127
+ return ".".join(parents + [name])
128
+ return ""
129
+
130
+
131
class QAPIModule(QAPIDescription):
132
"""
133
Directive to mark description of a new module.
134
--
135
2.48.1
diff view generated by jsdifflib
New patch
1
This commit adds a stubbed version of QAPICommand that utilizes the
2
QAPIObject class, the qapi:command directive, the :qapi:cmd:
3
cross-reference role, and the "command" object type in the QAPI object
4
registry.
1
5
6
They don't do anything *particularly* interesting yet, but that will
7
come in forthcoming commits.
8
9
Signed-off-by: John Snow <jsnow@redhat.com>
10
---
11
docs/sphinx/qapi_domain.py | 9 +++++++++
12
1 file changed, 9 insertions(+)
13
14
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
15
index XXXXXXX..XXXXXXX 100644
16
--- a/docs/sphinx/qapi_domain.py
17
+++ b/docs/sphinx/qapi_domain.py
18
@@ -XXX,XX +XXX,XX @@ def _toc_entry_name(self, sig_node: desc_signature) -> str:
19
return ""
20
21
22
+class QAPICommand(QAPIObject):
23
+ """Description of a QAPI Command."""
24
+
25
+ # Nothing unique for now! Changed in later commits O:-)
26
+
27
+
28
class QAPIModule(QAPIDescription):
29
"""
30
Directive to mark description of a new module.
31
@@ -XXX,XX +XXX,XX @@ class QAPIDomain(Domain):
32
# for each object type.
33
object_types: Dict[str, ObjType] = {
34
"module": ObjType(_("module"), "mod", "any"),
35
+ "command": ObjType(_("command"), "cmd", "any"),
36
}
37
38
# Each of these provides a rST directive,
39
# e.g. .. qapi:module:: block-core
40
directives = {
41
"module": QAPIModule,
42
+ "command": QAPICommand,
43
}
44
45
# These are all cross-reference roles; e.g.
46
@@ -XXX,XX +XXX,XX @@ class QAPIDomain(Domain):
47
# the object_types table values above.
48
roles = {
49
"mod": QAPIXRefRole(),
50
+ "cmd": QAPIXRefRole(),
51
"any": QAPIXRefRole(), # reference *any* type of QAPI object.
52
}
53
54
--
55
2.48.1
diff view generated by jsdifflib
New patch
1
Add a little special markup for registering "Since:" information. Adding
2
it as an option instead of generic content lets us hoist the information
3
into the Signature bar, optionally put it in the index, etc.
1
4
5
Signed-off-by: John Snow <jsnow@redhat.com>
6
---
7
docs/sphinx/qapi_domain.py | 14 +++++++++++++-
8
1 file changed, 13 insertions(+), 1 deletion(-)
9
10
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
11
index XXXXXXX..XXXXXXX 100644
12
--- a/docs/sphinx/qapi_domain.py
13
+++ b/docs/sphinx/qapi_domain.py
14
@@ -XXX,XX +XXX,XX @@ class QAPIObject(QAPIDescription):
15
{
16
# Borrowed from the Python domain:
17
"module": directives.unchanged, # Override contextual module name
18
+ # These are QAPI originals:
19
+ "since": directives.unchanged,
20
}
21
)
22
23
@@ -XXX,XX +XXX,XX @@ def get_signature_prefix(self) -> List[nodes.Node]:
24
25
def get_signature_suffix(self) -> List[nodes.Node]:
26
"""Returns a suffix to put after the object name in the signature."""
27
- return []
28
+ ret: List[nodes.Node] = []
29
+
30
+ if "since" in self.options:
31
+ ret += [
32
+ SpaceNode(" "),
33
+ addnodes.desc_sig_element(
34
+ "", f"(Since: {self.options['since']})"
35
+ ),
36
+ ]
37
+
38
+ return ret
39
40
def handle_signature(self, sig: str, signode: desc_signature) -> Signature:
41
"""
42
--
43
2.48.1
diff view generated by jsdifflib
New patch
1
This adds special rendering for Sphinx's typed field lists.
1
2
3
This patch does not add any QAPI-aware markup, rendering, or
4
cross-referencing for the type names, yet. That feature requires a
5
subclass to TypedField which will happen in its own commit quite a bit
6
later in this series; after all the basic fields and objects have been
7
established first.
8
9
The syntax for this field is:
10
11
:arg type name: description
12
description cont'd
13
14
You can omit the type or the description, but you cannot omit the name
15
-- if you do so, it degenerates into a "normal field list" entry, and
16
probably isn't what you want.
17
18
Signed-off-by: John Snow <jsnow@redhat.com>
19
---
20
docs/sphinx/qapi_domain.py | 14 +++++++++++++-
21
1 file changed, 13 insertions(+), 1 deletion(-)
22
23
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
24
index XXXXXXX..XXXXXXX 100644
25
--- a/docs/sphinx/qapi_domain.py
26
+++ b/docs/sphinx/qapi_domain.py
27
@@ -XXX,XX +XXX,XX @@
28
from sphinx.locale import _, __
29
from sphinx.roles import XRefRole
30
from sphinx.util import logging
31
+from sphinx.util.docfields import TypedField
32
from sphinx.util.nodes import make_id, make_refnode
33
34
35
@@ -XXX,XX +XXX,XX @@ def _toc_entry_name(self, sig_node: desc_signature) -> str:
36
class QAPICommand(QAPIObject):
37
"""Description of a QAPI Command."""
38
39
- # Nothing unique for now! Changed in later commits O:-)
40
+ doc_field_types = QAPIObject.doc_field_types.copy()
41
+ doc_field_types.extend(
42
+ [
43
+ # :arg TypeName ArgName: descr
44
+ TypedField(
45
+ "argument",
46
+ label=_("Arguments"),
47
+ names=("arg",),
48
+ can_collapse=False,
49
+ ),
50
+ ]
51
+ )
52
53
54
class QAPIModule(QAPIDescription):
55
--
56
2.48.1
diff view generated by jsdifflib
New patch
1
Add support for Features field lists. There is no QAPI-specific
2
functionality here, but this could be changed if desired (if we wanted
3
the feature names to link somewhere, for instance.)
1
4
5
This feature list doesn't have any restrictions, so it can be used to
6
document object-wide features or per-member features as deemed
7
appropriate. It's essentially free-form text.
8
9
Signed-off-by: John Snow <jsnow@redhat.com>
10
---
11
docs/sphinx/qapi_domain.py | 12 +++++++++++-
12
1 file changed, 11 insertions(+), 1 deletion(-)
13
14
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
15
index XXXXXXX..XXXXXXX 100644
16
--- a/docs/sphinx/qapi_domain.py
17
+++ b/docs/sphinx/qapi_domain.py
18
@@ -XXX,XX +XXX,XX @@
19
from sphinx.locale import _, __
20
from sphinx.roles import XRefRole
21
from sphinx.util import logging
22
-from sphinx.util.docfields import TypedField
23
+from sphinx.util.docfields import GroupedField, TypedField
24
from sphinx.util.nodes import make_id, make_refnode
25
26
27
@@ -XXX,XX +XXX,XX @@ class QAPIObject(QAPIDescription):
28
}
29
)
30
31
+ doc_field_types = [
32
+ # :feat name: descr
33
+ GroupedField(
34
+ "feature",
35
+ label=_("Features"),
36
+ names=("feat",),
37
+ can_collapse=False,
38
+ ),
39
+ ]
40
+
41
def get_signature_prefix(self) -> List[nodes.Node]:
42
"""Returns a prefix to put before the object name in the signature."""
43
assert self.objtype
44
--
45
2.48.1
diff view generated by jsdifflib
New patch
1
``:error: descr`` can now be used to document error conditions. The
2
format of the description is not defined here; so the ability to name
3
specific types is left to the document writer.
1
4
5
Signed-off-by: John Snow <jsnow@redhat.com>
6
---
7
docs/sphinx/qapi_domain.py | 9 ++++++++-
8
1 file changed, 8 insertions(+), 1 deletion(-)
9
10
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
11
index XXXXXXX..XXXXXXX 100644
12
--- a/docs/sphinx/qapi_domain.py
13
+++ b/docs/sphinx/qapi_domain.py
14
@@ -XXX,XX +XXX,XX @@
15
from sphinx.locale import _, __
16
from sphinx.roles import XRefRole
17
from sphinx.util import logging
18
-from sphinx.util.docfields import GroupedField, TypedField
19
+from sphinx.util.docfields import Field, GroupedField, TypedField
20
from sphinx.util.nodes import make_id, make_refnode
21
22
23
@@ -XXX,XX +XXX,XX @@ class QAPICommand(QAPIObject):
24
names=("arg",),
25
can_collapse=False,
26
),
27
+ # :error: descr
28
+ Field(
29
+ "error",
30
+ label=_("Errors"),
31
+ names=("error", "errors"),
32
+ has_arg=False,
33
+ ),
34
]
35
)
36
37
--
38
2.48.1
diff view generated by jsdifflib
New patch
1
Add "Return:" field list syntax to QAPI Commands.
1
2
3
Like "Arguments:" and "Errors:", the type name isn't currently processed
4
for cross-referencing, but this will be addressed in a forthcoming
5
commit.
6
7
This patch adds "Return" as a GroupedField, which means that multiple
8
return values can be annotated - this is only done because Sphinx does
9
not seemingly (Maybe I missed it?) support mandatory type arguments to
10
Ungrouped fields. Because we want to cross-reference this type
11
information later, we want to make the type argument mandatory. As a
12
result, you can technically add multiple :return: fields, though I'm not
13
aware of any circumstance in which you'd need or want
14
to. Recommendation: "Don't do that, then."
15
16
Signed-off-by: John Snow <jsnow@redhat.com>
17
---
18
docs/sphinx/qapi_domain.py | 7 +++++++
19
1 file changed, 7 insertions(+)
20
21
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
22
index XXXXXXX..XXXXXXX 100644
23
--- a/docs/sphinx/qapi_domain.py
24
+++ b/docs/sphinx/qapi_domain.py
25
@@ -XXX,XX +XXX,XX @@ class QAPICommand(QAPIObject):
26
names=("error", "errors"),
27
has_arg=False,
28
),
29
+ # :returns TypeName: descr
30
+ GroupedField(
31
+ "returnvalue",
32
+ label=_("Return"),
33
+ names=("return",),
34
+ can_collapse=True,
35
+ ),
36
]
37
)
38
39
--
40
2.48.1
diff view generated by jsdifflib
New patch
1
Add the .. qapi:enum:: directive, object, and :qapi:enum:`name`
2
cross-reference role.
1
3
4
Add the :value name: field list for documenting Enum values.
5
6
Of note, also introduce a new "type" role that is intended to be used by
7
other QAPI object directives to cross-reference arbitrary QAPI type
8
names, but will exclude commands, events, and modules from
9
consideration.
10
11
Signed-off-by: John Snow <jsnow@redhat.com>
12
---
13
docs/sphinx/qapi_domain.py | 26 ++++++++++++++++++++++++++
14
1 file changed, 26 insertions(+)
15
16
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
17
index XXXXXXX..XXXXXXX 100644
18
--- a/docs/sphinx/qapi_domain.py
19
+++ b/docs/sphinx/qapi_domain.py
20
@@ -XXX,XX +XXX,XX @@ class QAPICommand(QAPIObject):
21
)
22
23
24
+class QAPIEnum(QAPIObject):
25
+ """Description of a QAPI Enum."""
26
+
27
+ doc_field_types = QAPIObject.doc_field_types.copy()
28
+ doc_field_types.extend(
29
+ [
30
+ # :value name: descr
31
+ GroupedField(
32
+ "value",
33
+ label=_("Values"),
34
+ names=("value",),
35
+ can_collapse=False,
36
+ )
37
+ ]
38
+ )
39
+
40
+
41
class QAPIModule(QAPIDescription):
42
"""
43
Directive to mark description of a new module.
44
@@ -XXX,XX +XXX,XX @@ class QAPIDomain(Domain):
45
# This table associates cross-reference object types (key) with an
46
# ObjType instance, which defines the valid cross-reference roles
47
# for each object type.
48
+ #
49
+ # e.g., the :qapi:type: cross-reference role can refer to enum,
50
+ # struct, union, or alternate objects; but :qapi:obj: can refer to
51
+ # anything. Each object also gets its own targeted cross-reference role.
52
object_types: Dict[str, ObjType] = {
53
"module": ObjType(_("module"), "mod", "any"),
54
"command": ObjType(_("command"), "cmd", "any"),
55
+ "enum": ObjType(_("enum"), "enum", "type", "any"),
56
}
57
58
# Each of these provides a rST directive,
59
@@ -XXX,XX +XXX,XX @@ class QAPIDomain(Domain):
60
directives = {
61
"module": QAPIModule,
62
"command": QAPICommand,
63
+ "enum": QAPIEnum,
64
}
65
66
# These are all cross-reference roles; e.g.
67
@@ -XXX,XX +XXX,XX @@ class QAPIDomain(Domain):
68
roles = {
69
"mod": QAPIXRefRole(),
70
"cmd": QAPIXRefRole(),
71
+ "enum": QAPIXRefRole(),
72
+ # reference any data type (excludes modules, commands, events)
73
+ "type": QAPIXRefRole(),
74
"any": QAPIXRefRole(), # reference *any* type of QAPI object.
75
}
76
77
--
78
2.48.1
diff view generated by jsdifflib
New patch
1
Add the .. qapi:alternate:: directive, object, and qapi:alt:`name`
2
cross-reference role.
1
3
4
Add the "Alternatives:" field list for describing alternate choices. Like
5
other field lists that reference QAPI types, a forthcoming commit will
6
add cross-referencing support to this field.
7
8
Signed-off-by: John Snow <jsnow@redhat.com>
9
---
10
docs/sphinx/qapi_domain.py | 20 ++++++++++++++++++++
11
1 file changed, 20 insertions(+)
12
13
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
14
index XXXXXXX..XXXXXXX 100644
15
--- a/docs/sphinx/qapi_domain.py
16
+++ b/docs/sphinx/qapi_domain.py
17
@@ -XXX,XX +XXX,XX @@ class QAPIEnum(QAPIObject):
18
)
19
20
21
+class QAPIAlternate(QAPIObject):
22
+ """Description of a QAPI Alternate."""
23
+
24
+ doc_field_types = QAPIObject.doc_field_types.copy()
25
+ doc_field_types.extend(
26
+ [
27
+ # :alt type name: descr
28
+ TypedField(
29
+ "alternative",
30
+ label=_("Alternatives"),
31
+ names=("alt",),
32
+ can_collapse=False,
33
+ ),
34
+ ]
35
+ )
36
+
37
+
38
class QAPIModule(QAPIDescription):
39
"""
40
Directive to mark description of a new module.
41
@@ -XXX,XX +XXX,XX @@ class QAPIDomain(Domain):
42
"module": ObjType(_("module"), "mod", "any"),
43
"command": ObjType(_("command"), "cmd", "any"),
44
"enum": ObjType(_("enum"), "enum", "type", "any"),
45
+ "alternate": ObjType(_("alternate"), "alt", "type", "any"),
46
}
47
48
# Each of these provides a rST directive,
49
@@ -XXX,XX +XXX,XX @@ class QAPIDomain(Domain):
50
"module": QAPIModule,
51
"command": QAPICommand,
52
"enum": QAPIEnum,
53
+ "alternate": QAPIAlternate,
54
}
55
56
# These are all cross-reference roles; e.g.
57
@@ -XXX,XX +XXX,XX @@ class QAPIDomain(Domain):
58
"mod": QAPIXRefRole(),
59
"cmd": QAPIXRefRole(),
60
"enum": QAPIXRefRole(),
61
+ "alt": QAPIXRefRole(),
62
# reference any data type (excludes modules, commands, events)
63
"type": QAPIXRefRole(),
64
"any": QAPIXRefRole(), # reference *any* type of QAPI object.
65
--
66
2.48.1
diff view generated by jsdifflib
New patch
1
Adds the .. qapi:event:: directive, object, and :qapi:event:`name`
2
cross-referencing role.
1
3
4
Adds the :memb type name: field list syntax for documenting event data
5
members. As this syntax and phrasing will be shared with Structs and
6
Unions as well, add the field list definition to a shared abstract
7
class.
8
9
As per usual, QAPI cross-referencing for types in the member field list
10
will be added in a forthcoming commit.
11
12
Signed-off-by: John Snow <jsnow@redhat.com>
13
---
14
docs/sphinx/qapi_domain.py | 24 ++++++++++++++++++++++++
15
1 file changed, 24 insertions(+)
16
17
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
18
index XXXXXXX..XXXXXXX 100644
19
--- a/docs/sphinx/qapi_domain.py
20
+++ b/docs/sphinx/qapi_domain.py
21
@@ -XXX,XX +XXX,XX @@ class QAPIAlternate(QAPIObject):
22
)
23
24
25
+class QAPIObjectWithMembers(QAPIObject):
26
+ """Base class for Events/Structs/Unions"""
27
+
28
+ doc_field_types = QAPIObject.doc_field_types.copy()
29
+ doc_field_types.extend(
30
+ [
31
+ # :member type name: descr
32
+ TypedField(
33
+ "member",
34
+ label=_("Members"),
35
+ names=("memb",),
36
+ can_collapse=False,
37
+ ),
38
+ ]
39
+ )
40
+
41
+
42
+class QAPIEvent(QAPIObjectWithMembers):
43
+ """Description of a QAPI Event."""
44
+
45
+
46
class QAPIModule(QAPIDescription):
47
"""
48
Directive to mark description of a new module.
49
@@ -XXX,XX +XXX,XX @@ class QAPIDomain(Domain):
50
object_types: Dict[str, ObjType] = {
51
"module": ObjType(_("module"), "mod", "any"),
52
"command": ObjType(_("command"), "cmd", "any"),
53
+ "event": ObjType(_("event"), "event", "any"),
54
"enum": ObjType(_("enum"), "enum", "type", "any"),
55
"alternate": ObjType(_("alternate"), "alt", "type", "any"),
56
}
57
@@ -XXX,XX +XXX,XX @@ class QAPIDomain(Domain):
58
directives = {
59
"module": QAPIModule,
60
"command": QAPICommand,
61
+ "event": QAPIEvent,
62
"enum": QAPIEnum,
63
"alternate": QAPIAlternate,
64
}
65
@@ -XXX,XX +XXX,XX @@ class QAPIDomain(Domain):
66
roles = {
67
"mod": QAPIXRefRole(),
68
"cmd": QAPIXRefRole(),
69
+ "event": QAPIXRefRole(),
70
"enum": QAPIXRefRole(),
71
"alt": QAPIXRefRole(),
72
# reference any data type (excludes modules, commands, events)
73
--
74
2.48.1
diff view generated by jsdifflib
New patch
1
Adds the .. qapi:object:: directive, object, and :qapi:obj:`name`
2
cross-referencing role. This directive is meant to document both structs
3
and unions.
1
4
5
As per usual, QAPI cross-referencing for types in the member field list
6
will be added in a forthcoming commit.
7
8
Signed-off-by: John Snow <jsnow@redhat.com>
9
---
10
docs/sphinx/qapi_domain.py | 7 +++++++
11
1 file changed, 7 insertions(+)
12
13
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
14
index XXXXXXX..XXXXXXX 100644
15
--- a/docs/sphinx/qapi_domain.py
16
+++ b/docs/sphinx/qapi_domain.py
17
@@ -XXX,XX +XXX,XX @@ class QAPIEvent(QAPIObjectWithMembers):
18
"""Description of a QAPI Event."""
19
20
21
+class QAPIJSONObject(QAPIObjectWithMembers):
22
+ """Description of a QAPI Object: structs and unions."""
23
+
24
+
25
class QAPIModule(QAPIDescription):
26
"""
27
Directive to mark description of a new module.
28
@@ -XXX,XX +XXX,XX @@ class QAPIDomain(Domain):
29
"command": ObjType(_("command"), "cmd", "any"),
30
"event": ObjType(_("event"), "event", "any"),
31
"enum": ObjType(_("enum"), "enum", "type", "any"),
32
+ "object": ObjType(_("object"), "obj", "type", "any"),
33
"alternate": ObjType(_("alternate"), "alt", "type", "any"),
34
}
35
36
@@ -XXX,XX +XXX,XX @@ class QAPIDomain(Domain):
37
"command": QAPICommand,
38
"event": QAPIEvent,
39
"enum": QAPIEnum,
40
+ "object": QAPIJSONObject,
41
"alternate": QAPIAlternate,
42
}
43
44
@@ -XXX,XX +XXX,XX @@ class QAPIDomain(Domain):
45
"cmd": QAPIXRefRole(),
46
"event": QAPIXRefRole(),
47
"enum": QAPIXRefRole(),
48
+ "obj": QAPIXRefRole(), # specifically structs and unions.
49
"alt": QAPIXRefRole(),
50
# reference any data type (excludes modules, commands, events)
51
"type": QAPIXRefRole(),
52
--
53
2.48.1
diff view generated by jsdifflib
New patch
1
Although "deprecated" is a feature (and *will* appear in the features
2
list), add a special :deprecated: option to generate an eye-catch that
3
makes this information very hard to miss.
1
4
5
(The intent is to modify qapidoc.py to add this option whenever it
6
detects that the features list attached to a definition contains the
7
"deprecated" entry.)
8
9
-
10
11
RFC: Technically, this object-level option is un-needed and could be
12
replaced with a standard content-level directive that e.g. qapidoc.py
13
could insert at the beginning of the content block. I've done it here as
14
an option to demonstrate how it would be possible to do.
15
16
It's a matter of taste for "where" we feel like implementing it.
17
18
One benefit of doing it this way is that we can create a single
19
containing box to set CSS style options controlling the flow of multiple
20
infoboxes. The other way to achieve that would be to create a directive
21
that allows us to set multiple options instead, e.g.:
22
23
.. qapi:infoboxes:: deprecated unstable
24
25
or possibly:
26
27
.. qapi:infoboxes::
28
:deprecated:
29
:unstable:
30
31
For now, I've left these as top-level QAPI object options. "Hey, it works."
32
33
P.S., I outsourced the CSS ;)
34
35
Signed-off-by: Harmonie Snow <harmonie@gmail.com>
36
Signed-off-by: John Snow <jsnow@redhat.com>
37
---
38
docs/sphinx-static/theme_overrides.css | 25 +++++++++++++++++++++++++
39
docs/sphinx/qapi_domain.py | 26 ++++++++++++++++++++++++++
40
2 files changed, 51 insertions(+)
41
42
diff --git a/docs/sphinx-static/theme_overrides.css b/docs/sphinx-static/theme_overrides.css
43
index XXXXXXX..XXXXXXX 100644
44
--- a/docs/sphinx-static/theme_overrides.css
45
+++ b/docs/sphinx-static/theme_overrides.css
46
@@ -XXX,XX +XXX,XX @@ div[class^="highlight"] pre {
47
color: inherit;
48
}
49
}
50
+
51
+/* QAPI domain theming */
52
+
53
+.qapi-infopips {
54
+ margin-bottom: 1em;
55
+}
56
+
57
+.qapi-infopip {
58
+ display: inline-block;
59
+ padding: 0em 0.5em 0em 0.5em;
60
+ margin: 0.25em;
61
+}
62
+
63
+.qapi-deprecated {
64
+ background-color: #fffef5;
65
+ border: solid #fff176 6px;
66
+ font-weight: bold;
67
+ padding: 8px;
68
+ border-radius: 15px;
69
+ margin: 5px;
70
+}
71
+
72
+.qapi-deprecated::before {
73
+ content: '⚠️ ';
74
+}
75
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
76
index XXXXXXX..XXXXXXX 100644
77
--- a/docs/sphinx/qapi_domain.py
78
+++ b/docs/sphinx/qapi_domain.py
79
@@ -XXX,XX +XXX,XX @@ class QAPIObject(QAPIDescription):
80
"module": directives.unchanged, # Override contextual module name
81
# These are QAPI originals:
82
"since": directives.unchanged,
83
+ "deprecated": directives.flag,
84
}
85
)
86
87
@@ -XXX,XX +XXX,XX @@ def _object_hierarchy_parts(
88
89
return tuple(fullname.split("."))
90
91
+ def _add_infopips(self, contentnode: addnodes.desc_content) -> None:
92
+ # Add various eye-catches and things that go below the signature
93
+ # bar, but precede the user-defined content.
94
+ infopips = nodes.container()
95
+ infopips.attributes["classes"].append("qapi-infopips")
96
+
97
+ def _add_pip(source: str, content: str, classname: str) -> None:
98
+ node = nodes.container(source)
99
+ node.append(nodes.Text(content))
100
+ node.attributes["classes"].extend(["qapi-infopip", classname])
101
+ infopips.append(node)
102
+
103
+ if "deprecated" in self.options:
104
+ _add_pip(
105
+ ":deprecated:",
106
+ f"This {self.objtype} is deprecated.",
107
+ "qapi-deprecated",
108
+ )
109
+
110
+ if infopips.children:
111
+ contentnode.insert(0, infopips)
112
+
113
+ def transform_content(self, content_node: addnodes.desc_content) -> None:
114
+ self._add_infopips(content_node)
115
+
116
def _toc_entry_name(self, sig_node: desc_signature) -> str:
117
# This controls the name in the TOC and on the sidebar.
118
119
--
120
2.48.1
121
122
diff view generated by jsdifflib
New patch
1
Although "unstable" is a feature (and *will* appear in the features
2
list), add a special :unstable: option to generate an eye-catch that
3
makes this information very hard to miss.
1
4
5
(The intent is to modify qapidoc.py to add this option whenever it
6
detects that the features list attached to a definition contains the
7
"unstable" entry.)
8
9
RFC: Same comments as last patch.
10
11
Signed-off-by: Harmonie Snow <harmonie@gmail.com>
12
Signed-off-by: John Snow <jsnow@redhat.com>
13
---
14
docs/sphinx-static/theme_overrides.css | 6 +++++-
15
docs/sphinx/qapi_domain.py | 8 ++++++++
16
2 files changed, 13 insertions(+), 1 deletion(-)
17
18
diff --git a/docs/sphinx-static/theme_overrides.css b/docs/sphinx-static/theme_overrides.css
19
index XXXXXXX..XXXXXXX 100644
20
--- a/docs/sphinx-static/theme_overrides.css
21
+++ b/docs/sphinx-static/theme_overrides.css
22
@@ -XXX,XX +XXX,XX @@ div[class^="highlight"] pre {
23
margin: 0.25em;
24
}
25
26
-.qapi-deprecated {
27
+.qapi-deprecated,.qapi-unstable {
28
background-color: #fffef5;
29
border: solid #fff176 6px;
30
font-weight: bold;
31
@@ -XXX,XX +XXX,XX @@ div[class^="highlight"] pre {
32
margin: 5px;
33
}
34
35
+.qapi-unstable::before {
36
+ content: '🚧 ';
37
+}
38
+
39
.qapi-deprecated::before {
40
content: '⚠️ ';
41
}
42
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
43
index XXXXXXX..XXXXXXX 100644
44
--- a/docs/sphinx/qapi_domain.py
45
+++ b/docs/sphinx/qapi_domain.py
46
@@ -XXX,XX +XXX,XX @@ class QAPIObject(QAPIDescription):
47
# These are QAPI originals:
48
"since": directives.unchanged,
49
"deprecated": directives.flag,
50
+ "unstable": directives.flag,
51
}
52
)
53
54
@@ -XXX,XX +XXX,XX @@ def _add_pip(source: str, content: str, classname: str) -> None:
55
"qapi-deprecated",
56
)
57
58
+ if "unstable" in self.options:
59
+ _add_pip(
60
+ ":unstable:",
61
+ f"This {self.objtype} is unstable/experimental.",
62
+ "qapi-unstable",
63
+ )
64
+
65
if infopips.children:
66
contentnode.insert(0, infopips)
67
68
--
69
2.48.1
70
71
diff view generated by jsdifflib
New patch
1
Add a special :ifcond: option that allows us to annotate the
2
definition-level conditionals.
1
3
4
The syntax of the argument is currently undefined, but it's possible we
5
can apply better formatting in the future. Currently, we just display
6
the ifcond string as preformatted text.
7
8
Signed-off-by: Harmonie Snow <harmonie@gmail.com>
9
Signed-off-by: John Snow <jsnow@redhat.com>
10
---
11
docs/sphinx-static/theme_overrides.css | 13 +++++++++++++
12
docs/sphinx/qapi_domain.py | 23 +++++++++++++++++++++--
13
2 files changed, 34 insertions(+), 2 deletions(-)
14
15
diff --git a/docs/sphinx-static/theme_overrides.css b/docs/sphinx-static/theme_overrides.css
16
index XXXXXXX..XXXXXXX 100644
17
--- a/docs/sphinx-static/theme_overrides.css
18
+++ b/docs/sphinx-static/theme_overrides.css
19
@@ -XXX,XX +XXX,XX @@ div[class^="highlight"] pre {
20
.qapi-deprecated::before {
21
content: '⚠️ ';
22
}
23
+
24
+.qapi-ifcond::before {
25
+ /* gaze ye into the crystal ball to determine feature availability */
26
+ content: '🔮 ';
27
+}
28
+
29
+.qapi-ifcond {
30
+ background-color: #f9f5ff;
31
+ border: solid #dac2ff 6px;
32
+ padding: 8px;
33
+ border-radius: 15px;
34
+ margin: 5px;
35
+}
36
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
37
index XXXXXXX..XXXXXXX 100644
38
--- a/docs/sphinx/qapi_domain.py
39
+++ b/docs/sphinx/qapi_domain.py
40
@@ -XXX,XX +XXX,XX @@
41
NamedTuple,
42
Optional,
43
Tuple,
44
+ Union,
45
cast,
46
)
47
48
@@ -XXX,XX +XXX,XX @@ class QAPIObject(QAPIDescription):
49
"module": directives.unchanged, # Override contextual module name
50
# These are QAPI originals:
51
"since": directives.unchanged,
52
+ "ifcond": directives.unchanged,
53
"deprecated": directives.flag,
54
"unstable": directives.flag,
55
}
56
@@ -XXX,XX +XXX,XX @@ def _add_infopips(self, contentnode: addnodes.desc_content) -> None:
57
infopips = nodes.container()
58
infopips.attributes["classes"].append("qapi-infopips")
59
60
- def _add_pip(source: str, content: str, classname: str) -> None:
61
+ def _add_pip(
62
+ source: str, content: Union[str, List[nodes.Node]], classname: str
63
+ ) -> None:
64
node = nodes.container(source)
65
- node.append(nodes.Text(content))
66
+ if isinstance(content, str):
67
+ node.append(nodes.Text(content))
68
+ else:
69
+ node.extend(content)
70
node.attributes["classes"].extend(["qapi-infopip", classname])
71
infopips.append(node)
72
73
@@ -XXX,XX +XXX,XX @@ def _add_pip(source: str, content: str, classname: str) -> None:
74
"qapi-unstable",
75
)
76
77
+ if self.options.get("ifcond", ""):
78
+ ifcond = self.options["ifcond"]
79
+ _add_pip(
80
+ f":ifcond: {ifcond}",
81
+ [
82
+ nodes.emphasis("", "Availability"),
83
+ nodes.Text(": "),
84
+ nodes.literal(ifcond, ifcond),
85
+ ],
86
+ "qapi-ifcond",
87
+ )
88
+
89
if infopips.children:
90
contentnode.insert(0, infopips)
91
92
--
93
2.48.1
94
95
diff view generated by jsdifflib
New patch
1
Normally, Sphinx will silently fall back to its standard field list
2
processing if it doesn't match one of your defined fields. A lot of the
3
time, that's not what we want - we want to be warned if we goof
4
something up.
1
5
6
For instance, the canonical argument field list form is:
7
8
:arg type name: descr
9
10
This form is captured by Sphinx and transformed so that the field label
11
will become "Arguments:". It's possible to omit the type name and descr
12
and still have it be processed correctly. However, if you omit the type
13
name, Sphinx no longer recognizes it:
14
15
:arg: this is not recognized.
16
17
This will turn into an arbitrary field list entry whose label is "Arg:",
18
and it otherwise silently fails. You may also see failures for doing
19
things like using :values: instead of :value:, or :errors: instead of
20
:error:, and so on. It's also case sensitive, and easy to trip up.
21
22
Add a validator that guarantees all field list entries that are the
23
direct child of an ObjectDescription use only recognized forms of field
24
lists, and emit a warning (treated as error by default in most build
25
configurations) whenever we detect one that is goofed up.
26
27
However, there's still benefit to allowing arbitrary fields -- they are
28
after all not a Sphinx invention, but perfectly normal docutils
29
syntax. Create an allow list for known spellings we don't mind letting
30
through, but warn against anything else.
31
32
Signed-off-by: John Snow <jsnow@redhat.com>
33
---
34
docs/conf.py | 9 +++++
35
docs/sphinx/qapi_domain.py | 74 ++++++++++++++++++++++++++++++++++++++
36
2 files changed, 83 insertions(+)
37
38
diff --git a/docs/conf.py b/docs/conf.py
39
index XXXXXXX..XXXXXXX 100644
40
--- a/docs/conf.py
41
+++ b/docs/conf.py
42
@@ -XXX,XX +XXX,XX @@
43
with open(os.path.join(qemu_docdir, 'defs.rst.inc')) as f:
44
rst_epilog += f.read()
45
46
+
47
+# Normally, the QAPI domain is picky about what field lists you use to
48
+# describe a QAPI entity. If you'd like to use arbitrary additional
49
+# fields in source documentation, add them here.
50
+qapi_allowed_fields = {
51
+ "see also",
52
+}
53
+
54
+
55
# -- Options for HTML output ----------------------------------------------
56
57
# The theme to use for HTML and HTML Help pages. See the documentation for
58
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
59
index XXXXXXX..XXXXXXX 100644
60
--- a/docs/sphinx/qapi_domain.py
61
+++ b/docs/sphinx/qapi_domain.py
62
@@ -XXX,XX +XXX,XX @@
63
logger = logging.getLogger(__name__)
64
65
66
+def _unpack_field(
67
+ field: nodes.Node,
68
+) -> Tuple[nodes.field_name, nodes.field_body]:
69
+ """
70
+ docutils helper: unpack a field node in a type-safe manner.
71
+ """
72
+ assert isinstance(field, nodes.field)
73
+ assert len(field.children) == 2
74
+ assert isinstance(field.children[0], nodes.field_name)
75
+ assert isinstance(field.children[1], nodes.field_body)
76
+ return (field.children[0], field.children[1])
77
+
78
+
79
class ObjectEntry(NamedTuple):
80
docname: str
81
node_id: str
82
@@ -XXX,XX +XXX,XX @@ def _add_pip(
83
if infopips.children:
84
contentnode.insert(0, infopips)
85
86
+ def _validate_field(self, field: nodes.field) -> None:
87
+ """Validate field lists in this QAPI Object Description."""
88
+ name, _ = _unpack_field(field)
89
+ allowed_fields = set(self.env.app.config.qapi_allowed_fields)
90
+
91
+ field_label = name.astext()
92
+ if field_label in allowed_fields:
93
+ # Explicitly allowed field list name, OK.
94
+ return
95
+
96
+ try:
97
+ # split into field type and argument (if provided)
98
+ # e.g. `:arg type name: descr` is
99
+ # field_type = "arg", field_arg = "type name".
100
+ field_type, field_arg = field_label.split(None, 1)
101
+ except ValueError:
102
+ # No arguments provided
103
+ field_type = field_label
104
+ field_arg = ""
105
+
106
+ typemap = self.get_field_type_map()
107
+ if field_type in typemap:
108
+ # This is a special docfield, yet-to-be-processed. Catch
109
+ # correct names, but incorrect arguments. This mismatch WILL
110
+ # cause Sphinx to render this field incorrectly (without a
111
+ # warning), which is never what we want.
112
+ typedesc = typemap[field_type][0]
113
+ if typedesc.has_arg != bool(field_arg):
114
+ msg = f"docfield field list type {field_type!r} "
115
+ if typedesc.has_arg:
116
+ msg += "requires an argument."
117
+ else:
118
+ msg += "takes no arguments."
119
+ logger.warning(msg, location=field)
120
+ else:
121
+ # This is unrecognized entirely. It's valid rST to use
122
+ # arbitrary fields, but let's ensure the documentation
123
+ # writer has done this intentionally.
124
+ valid = ", ".join(sorted(set(typemap) | allowed_fields))
125
+ msg = (
126
+ f"Unrecognized field list name {field_label!r}.\n"
127
+ f"Valid fields for qapi:{self.objtype} are: {valid}\n"
128
+ "\n"
129
+ "If this usage is intentional, please add it to "
130
+ "'qapi_allowed_fields' in docs/conf.py."
131
+ )
132
+ logger.warning(msg, location=field)
133
+
134
def transform_content(self, content_node: addnodes.desc_content) -> None:
135
self._add_infopips(content_node)
136
137
+ # Validate field lists.
138
+ for child in content_node:
139
+ if isinstance(child, nodes.field_list):
140
+ for field in child.children:
141
+ assert isinstance(field, nodes.field)
142
+ self._validate_field(field)
143
+
144
def _toc_entry_name(self, sig_node: desc_signature) -> str:
145
# This controls the name in the TOC and on the sidebar.
146
147
@@ -XXX,XX +XXX,XX @@ def resolve_any_xref(
148
149
def setup(app: Sphinx) -> Dict[str, Any]:
150
app.setup_extension("sphinx.directives")
151
+ app.add_config_value(
152
+ "qapi_allowed_fields",
153
+ set(),
154
+ "env", # Setting impacts parsing phase
155
+ types=set,
156
+ )
157
app.add_domain(QAPIDomain)
158
159
return {
160
--
161
2.48.1
diff view generated by jsdifflib
New patch
1
This commit, finally, adds cross-referencing support to various field
2
lists; modeled tightly after Sphinx's own Python domain code.
1
3
4
Cross-referencing support is added to type names provided to :arg:,
5
:memb:, :returns: and :choice:.
6
7
:feat:, :error: and :value:, which do not take type names, do not
8
support this syntax.
9
10
The general syntax is simple:
11
12
:arg TypeName ArgName: Lorem Ipsum ...
13
14
The domain will transform TypeName into :qapi:type:`TypeName` in this
15
basic case, and also apply the ``literal`` decoration to indicate that
16
this is a type cross-reference.
17
18
For optional arguments, the special "?" suffix is used. Because "*" has
19
special meaning in rST that would cause parsing errors, we elect to use
20
"?" instead. The special syntax processing strips this character from
21
the end of any type name argument and will append ", optional" to the
22
rendered output, applying the cross-reference only to the actual type
23
name.
24
25
The intent here is that the actual syntax in doc-blocks need not change;
26
but e.g. qapidoc.py will need to process and transform "@arg foo lorem
27
ipsum" into ":arg type? foo: lorem ipsum" based on the schema
28
information. Therefore, nobody should ever actually witness this
29
intermediate syntax unless they are writing manual documentation or the
30
doc transmogrifier breaks.
31
32
For array arguments, type names can similarly be surrounded by "[]",
33
which are stripped off and then re-appended outside of the
34
cross-reference.
35
36
Signed-off-by: John Snow <jsnow@redhat.com>
37
---
38
docs/sphinx/qapi_domain.py | 29 +++++++++++++++++++++++++++++
39
1 file changed, 29 insertions(+)
40
41
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
42
index XXXXXXX..XXXXXXX 100644
43
--- a/docs/sphinx/qapi_domain.py
44
+++ b/docs/sphinx/qapi_domain.py
45
@@ -XXX,XX +XXX,XX @@
46
QAPI domain extension.
47
"""
48
49
+# The best laid plans of mice and men, ...
50
+# pylint: disable=too-many-lines
51
+
52
from __future__ import annotations
53
54
from typing import (
55
@@ -XXX,XX +XXX,XX @@ def process_link(
56
57
return title, target
58
59
+ def result_nodes(
60
+ self,
61
+ document: nodes.document,
62
+ env: BuildEnvironment,
63
+ node: Element,
64
+ is_ref: bool,
65
+ ) -> Tuple[List[nodes.Node], List[nodes.system_message]]:
66
+
67
+ # node here is the pending_xref node (or whatever nodeclass was
68
+ # configured at XRefRole class instantiation time).
69
+ results: List[nodes.Node] = [node]
70
+
71
+ if node.get("qapi:array"):
72
+ results.insert(0, nodes.literal("[", "["))
73
+ results.append(nodes.literal("]", "]"))
74
+
75
+ if node.get("qapi:optional"):
76
+ results.append(nodes.Text(", "))
77
+ results.append(nodes.emphasis("?", "optional"))
78
+
79
+ return results, []
80
+
81
82
# Alias for the return of handle_signature(), which is used in several places.
83
# (In the Python domain, this is Tuple[str, str] instead.)
84
@@ -XXX,XX +XXX,XX @@ class QAPICommand(QAPIObject):
85
"argument",
86
label=_("Arguments"),
87
names=("arg",),
88
+ typerolename="type",
89
can_collapse=False,
90
),
91
# :error: descr
92
@@ -XXX,XX +XXX,XX @@ class QAPICommand(QAPIObject):
93
GroupedField(
94
"returnvalue",
95
label=_("Return"),
96
+ rolename="type",
97
names=("return",),
98
can_collapse=True,
99
),
100
@@ -XXX,XX +XXX,XX @@ class QAPIAlternate(QAPIObject):
101
"alternative",
102
label=_("Alternatives"),
103
names=("alt",),
104
+ typerolename="type",
105
can_collapse=False,
106
),
107
]
108
@@ -XXX,XX +XXX,XX @@ class QAPIObjectWithMembers(QAPIObject):
109
"member",
110
label=_("Members"),
111
names=("memb",),
112
+ typerolename="type",
113
can_collapse=False,
114
),
115
]
116
--
117
2.48.1
diff view generated by jsdifflib
New patch
1
Improve the general look and feel of generated QAPI docs.
1
2
3
Attempt to limit line lengths to offer a more comfortable measure on
4
maximized windows, and improve some margin and spacing for field lists.
5
6
Signed-off-by: Harmonie Snow <harmonie@gmail.com>
7
Signed-off-by: John Snow <jsnow@redhat.com>
8
---
9
docs/sphinx-static/theme_overrides.css | 56 +++++++++++++++++++++++++-
10
1 file changed, 54 insertions(+), 2 deletions(-)
11
12
diff --git a/docs/sphinx-static/theme_overrides.css b/docs/sphinx-static/theme_overrides.css
13
index XXXXXXX..XXXXXXX 100644
14
--- a/docs/sphinx-static/theme_overrides.css
15
+++ b/docs/sphinx-static/theme_overrides.css
16
@@ -XXX,XX +XXX,XX @@ h1, h2, .rst-content .toctree-wrapper p.caption, h3, h4, h5, h6, legend {
17
18
.rst-content dl:not(.docutils) dt {
19
border-top: none;
20
- border-left: solid 3px #ccc;
21
- background-color: #f0f0f0;
22
+ border-left: solid 5px #bcc6d2;
23
+ background-color: #eaedf1;
24
color: black;
25
}
26
27
@@ -XXX,XX +XXX,XX @@ div[class^="highlight"] pre {
28
29
/* QAPI domain theming */
30
31
+/* most content in a qapi object definition should not eclipse about
32
+ 80ch, but nested field lists are explicitly exempt due to their
33
+ two-column nature */
34
+.qapi dd *:not(dl) {
35
+ max-width: 80ch;
36
+}
37
+
38
+/* but the content column itself should still be less than ~80ch. */
39
+.qapi .field-list dd {
40
+ max-width: 80ch;
41
+}
42
+
43
.qapi-infopips {
44
margin-bottom: 1em;
45
}
46
@@ -XXX,XX +XXX,XX @@ div[class^="highlight"] pre {
47
border-radius: 15px;
48
margin: 5px;
49
}
50
+
51
+/* code blocks */
52
+.qapi div[class^="highlight"] {
53
+ width: fit-content;
54
+ background-color: #fffafd;
55
+ border: 2px solid #ffe1f3;
56
+}
57
+
58
+/* note, warning, etc. */
59
+.qapi .admonition {
60
+ width: fit-content;
61
+}
62
+
63
+/* pad the top of the field-list so the text doesn't start directly at
64
+ the top border; primarily for the field list labels, but adjust the
65
+ field bodies as well for parity. */
66
+dl.field-list > dt:first-of-type, dl.field-list > dd:first-of-type {
67
+ padding-top: 0.3em;
68
+}
69
+
70
+dl.field-list > dt:last-of-type, dl.field-list > dd:last-of-type {
71
+ padding-bottom: 0.3em;
72
+}
73
+
74
+/* pad the field list labels so they don't crash into the border */
75
+dl.field-list > dt {
76
+ padding-left: 0.5em;
77
+ padding-right: 0.5em;
78
+}
79
+
80
+/* Add a little padding between field list sections */
81
+dl.field-list > dd:not(:last-child) {
82
+ padding-bottom: 1em;
83
+}
84
+
85
+/* Sphinx 3.x: unresolved xrefs */
86
+.rst-content *:not(a) > code.xref {
87
+ font-weight: 400;
88
+ color: #333333;
89
+}
90
--
91
2.48.1
diff view generated by jsdifflib
New patch
1
1
Sphinx < 4.1 handles cross-references ... differently. Factor out and
2
isolate the compatibility goop we need to make cross references work
3
properly in old versions of Sphinx.
4
5
Yes, it's ugly. Yes, it works. No, I don't want to talk about
6
it.
7
8
Understand that this patch exists because of the overflowing love in my
9
heart.
10
11
Signed-off-by: John Snow <jsnow@redhat.com>
12
---
13
docs/sphinx/compat.py | 136 +++++++++++++++++++++++++++++++++++--
14
docs/sphinx/qapi_domain.py | 23 ++++---
15
2 files changed, 144 insertions(+), 15 deletions(-)
16
17
diff --git a/docs/sphinx/compat.py b/docs/sphinx/compat.py
18
index XXXXXXX..XXXXXXX 100644
19
--- a/docs/sphinx/compat.py
20
+++ b/docs/sphinx/compat.py
21
@@ -XXX,XX +XXX,XX @@
22
Sphinx cross-version compatibility goop
23
"""
24
25
-from typing import Callable
26
+import re
27
+from typing import (
28
+ Any,
29
+ Callable,
30
+ Optional,
31
+ Type,
32
+)
33
34
+from docutils import nodes
35
from docutils.nodes import Element, Node, Text
36
37
import sphinx
38
-from sphinx import addnodes
39
-from sphinx.util import nodes
40
-from sphinx.util.docutils import SphinxDirective, switch_source_input
41
+from sphinx import addnodes, util
42
+from sphinx.environment import BuildEnvironment
43
+from sphinx.roles import XRefRole
44
+from sphinx.util import docfields
45
+from sphinx.util.docutils import (
46
+ ReferenceRole,
47
+ SphinxDirective,
48
+ switch_source_input,
49
+)
50
+from sphinx.util.typing import TextlikeNode
51
+
52
+
53
+MAKE_XREF_WORKAROUND = sphinx.version_info[:3] < (4, 1, 0)
54
55
56
SpaceNode: Callable[[str], Node]
57
@@ -XXX,XX +XXX,XX @@ def nested_parse_with_titles(
58
try:
59
# Modern sphinx (6.2.0+) supports proper offsetting for
60
# nested parse error context management
61
- nodes.nested_parse_with_titles(
62
+ util.nodes.nested_parse_with_titles(
63
directive.state,
64
directive.content,
65
content_node,
66
@@ -XXX,XX +XXX,XX @@ def nested_parse_with_titles(
67
except TypeError:
68
# No content_offset argument. Fall back to SSI method.
69
with switch_source_input(directive.state, directive.content):
70
- nodes.nested_parse_with_titles(
71
+ util.nodes.nested_parse_with_titles(
72
directive.state, directive.content, content_node
73
)
74
+
75
+
76
+# ###########################################
77
+# xref compatibility hacks for Sphinx < 4.1 #
78
+# ###########################################
79
+
80
+# When we require >= Sphinx 4.1, the following function and the
81
+# subsequent 3 compatibility classes can be removed. Anywhere in
82
+# qapi_domain that uses one of these Compat* types can be switched to
83
+# using the garden-variety lib-provided classes with no trickery.
84
+
85
+
86
+def _compat_make_xref( # pylint: disable=unused-argument
87
+ self: sphinx.util.docfields.Field,
88
+ rolename: str,
89
+ domain: str,
90
+ target: str,
91
+ innernode: Type[TextlikeNode] = addnodes.literal_emphasis,
92
+ contnode: Optional[Node] = None,
93
+ env: Optional[BuildEnvironment] = None,
94
+ inliner: Any = None,
95
+ location: Any = None,
96
+) -> Node:
97
+ """
98
+ Compatibility workaround for Sphinx versions prior to 4.1.0.
99
+
100
+ Older sphinx versions do not use the domain's XRefRole for parsing
101
+ and formatting cross-references, so we need to perform this magick
102
+ ourselves to avoid needing to write the parser/formatter in two
103
+ separate places.
104
+
105
+ This workaround isn't brick-for-brick compatible with modern Sphinx
106
+ versions, because we do not have access to the parent directive's
107
+ state during this parsing like we do in more modern versions.
108
+
109
+ It's no worse than what pre-Sphinx 4.1.0 does, so... oh well!
110
+ """
111
+
112
+ # Yes, this function is gross. Pre-4.1 support is a miracle.
113
+ # pylint: disable=too-many-locals
114
+
115
+ assert env
116
+ # Note: Sphinx's own code ignores the type warning here, too.
117
+ if not rolename:
118
+ return contnode or innernode(target, target) # type: ignore[call-arg]
119
+
120
+ # Get the role instance, but don't *execute it* - we lack the
121
+ # correct state to do so. Instead, we'll just use its public
122
+ # methods to do our reference formatting, and emulate the rest.
123
+ role = env.get_domain(domain).roles[rolename]
124
+ assert isinstance(role, XRefRole)
125
+
126
+ # XRefRole features not supported by this compatibility shim;
127
+ # these were not supported in Sphinx 3.x either, so nothing of
128
+ # value is really lost.
129
+ assert not target.startswith("!")
130
+ assert not re.match(ReferenceRole.explicit_title_re, target)
131
+ assert not role.lowercase
132
+ assert not role.fix_parens
133
+
134
+ # Code below based mostly on sphinx.roles.XRefRole; run() and
135
+ # create_xref_node()
136
+ options = {
137
+ "refdoc": env.docname,
138
+ "refdomain": domain,
139
+ "reftype": rolename,
140
+ "refexplicit": False,
141
+ "refwarn": role.warn_dangling,
142
+ }
143
+ refnode = role.nodeclass(target, **options)
144
+ title, target = role.process_link(env, refnode, False, target, target)
145
+ refnode["reftarget"] = target
146
+ classes = ["xref", domain, f"{domain}-{rolename}"]
147
+ refnode += role.innernodeclass(target, title, classes=classes)
148
+
149
+ # This is the very gross part of the hack. Normally,
150
+ # result_nodes takes a document object to which we would pass
151
+ # self.inliner.document. Prior to Sphinx 4.1, we don't *have* an
152
+ # inliner to pass, so we have nothing to pass here. However, the
153
+ # actual implementation of role.result_nodes in this case
154
+ # doesn't actually use that argument, so this winds up being
155
+ # ... fine. Rest easy at night knowing this code only runs under
156
+ # old versions of Sphinx, so at least it won't change in the
157
+ # future on us and lead to surprising new failures.
158
+ # Gross, I know.
159
+ result_nodes, _messages = role.result_nodes(
160
+ None, # type: ignore
161
+ env,
162
+ refnode,
163
+ is_ref=True,
164
+ )
165
+ return nodes.inline(target, "", *result_nodes)
166
+
167
+
168
+class CompatField(docfields.Field):
169
+ if MAKE_XREF_WORKAROUND:
170
+ make_xref = _compat_make_xref
171
+
172
+
173
+class CompatGroupedField(docfields.GroupedField):
174
+ if MAKE_XREF_WORKAROUND:
175
+ make_xref = _compat_make_xref
176
+
177
+
178
+class CompatTypedField(docfields.TypedField):
179
+ if MAKE_XREF_WORKAROUND:
180
+ make_xref = _compat_make_xref
181
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
182
index XXXXXXX..XXXXXXX 100644
183
--- a/docs/sphinx/qapi_domain.py
184
+++ b/docs/sphinx/qapi_domain.py
185
@@ -XXX,XX +XXX,XX @@
186
from docutils import nodes
187
from docutils.parsers.rst import directives
188
189
-from compat import KeywordNode, SpaceNode
190
+from compat import (
191
+ CompatField,
192
+ CompatGroupedField,
193
+ CompatTypedField,
194
+ KeywordNode,
195
+ SpaceNode,
196
+)
197
from sphinx import addnodes
198
from sphinx.addnodes import desc_signature, pending_xref
199
from sphinx.directives import ObjectDescription
200
@@ -XXX,XX +XXX,XX @@
201
from sphinx.locale import _, __
202
from sphinx.roles import XRefRole
203
from sphinx.util import logging
204
-from sphinx.util.docfields import Field, GroupedField, TypedField
205
from sphinx.util.nodes import make_id, make_refnode
206
207
208
@@ -XXX,XX +XXX,XX @@ class QAPIObject(QAPIDescription):
209
210
doc_field_types = [
211
# :feat name: descr
212
- GroupedField(
213
+ CompatGroupedField(
214
"feature",
215
label=_("Features"),
216
names=("feat",),
217
@@ -XXX,XX +XXX,XX @@ class QAPICommand(QAPIObject):
218
doc_field_types.extend(
219
[
220
# :arg TypeName ArgName: descr
221
- TypedField(
222
+ CompatTypedField(
223
"argument",
224
label=_("Arguments"),
225
names=("arg",),
226
@@ -XXX,XX +XXX,XX @@ class QAPICommand(QAPIObject):
227
can_collapse=False,
228
),
229
# :error: descr
230
- Field(
231
+ CompatField(
232
"error",
233
label=_("Errors"),
234
names=("error", "errors"),
235
has_arg=False,
236
),
237
# :returns TypeName: descr
238
- GroupedField(
239
+ CompatGroupedField(
240
"returnvalue",
241
label=_("Return"),
242
rolename="type",
243
@@ -XXX,XX +XXX,XX @@ class QAPIEnum(QAPIObject):
244
doc_field_types.extend(
245
[
246
# :value name: descr
247
- GroupedField(
248
+ CompatGroupedField(
249
"value",
250
label=_("Values"),
251
names=("value",),
252
@@ -XXX,XX +XXX,XX @@ class QAPIAlternate(QAPIObject):
253
doc_field_types.extend(
254
[
255
# :alt type name: descr
256
- TypedField(
257
+ CompatTypedField(
258
"alternative",
259
label=_("Alternatives"),
260
names=("alt",),
261
@@ -XXX,XX +XXX,XX @@ class QAPIObjectWithMembers(QAPIObject):
262
doc_field_types.extend(
263
[
264
# :member type name: descr
265
- TypedField(
266
+ CompatTypedField(
267
"member",
268
label=_("Members"),
269
names=("memb",),
270
--
271
2.48.1
diff view generated by jsdifflib
New patch
1
This patch adds a warning (which is a build failure under our current
2
build settings) whenever a QAPI cross-reference fails to resolve.
1
3
4
This applies to any cross-references of the form :qapi:{role}:`foo`,
5
which covers all of the automatically generated references by the qapi
6
domain, and any such references that are manually written into the
7
documentation rst files.
8
9
Cross-references of the form `foo` do not use this system, but are
10
already configured to issue a warning (Again, a build failure) if the
11
cross-reference isn't found anywhere.
12
13
Adds warnings that look like the following:
14
15
docs/qapi/index.rst:48: WARNING: qapi:type reference target not found: 'footype' [ref.qapi]
16
docs/qapi/index.rst:50: WARNING: qapi:mod reference target not found: 'foomod' [ref.qapi]
17
18
Signed-off-by: John Snow <jsnow@redhat.com>
19
---
20
docs/sphinx/qapi_domain.py | 23 +++++++++++++++++++++++
21
1 file changed, 23 insertions(+)
22
23
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
24
index XXXXXXX..XXXXXXX 100644
25
--- a/docs/sphinx/qapi_domain.py
26
+++ b/docs/sphinx/qapi_domain.py
27
@@ -XXX,XX +XXX,XX @@ def resolve_xref(
28
matches = self.find_obj(modname, target, typ)
29
30
if not matches:
31
+ # Normally, we could pass warn_dangling=True to QAPIXRefRole(),
32
+ # but that will trigger on references to these built-in types,
33
+ # which we'd like to ignore instead.
34
+
35
+ # Take care of that warning here instead, so long as the
36
+ # reference isn't to one of our built-in core types.
37
+ if target not in (
38
+ "string",
39
+ "number",
40
+ "int",
41
+ "boolean",
42
+ "null",
43
+ "value",
44
+ "q_empty",
45
+ ):
46
+ logger.warning(
47
+ __("qapi:%s reference target not found: %r"),
48
+ typ,
49
+ target,
50
+ type="ref",
51
+ subtype="qapi",
52
+ location=node,
53
+ )
54
return None
55
56
if len(matches) > 1:
57
--
58
2.48.1
diff view generated by jsdifflib
New patch
1
Sphinx 5.3.0 to Sphinx 6.2.0 has a bug where nested content in an
2
ObjectDescription content block has its error position reported
3
incorrectly due to an oversight when they added nested section support
4
to this directive.
1
5
6
(This bug is present in Sphinx's own Python and C domains; test it
7
yourself by creating a py:func directive and creating a syntax error in
8
the directive's content block. The reporting will be incorrect.)
9
10
To avoid overriding and re-implementing the entirety of the run()
11
method, a workaround is employed where we parse the content block
12
ourselves in before_content(), then null the content block to make
13
Sphinx's own parsing a no-op. Then, in transform_content (which occurs
14
after Sphinx's nested parse), we simply swap our own parsed content tree
15
back in for Sphinx's.
16
17
It appears a little tricky, but it's the nicest solution I can find.
18
19
Signed-off-by: John Snow <jsnow@redhat.com>
20
---
21
docs/sphinx/compat.py | 56 ++++++++++++++++++++++++++++++++++++++
22
docs/sphinx/qapi_domain.py | 15 ++++++----
23
2 files changed, 65 insertions(+), 6 deletions(-)
24
25
diff --git a/docs/sphinx/compat.py b/docs/sphinx/compat.py
26
index XXXXXXX..XXXXXXX 100644
27
--- a/docs/sphinx/compat.py
28
+++ b/docs/sphinx/compat.py
29
@@ -XXX,XX +XXX,XX @@
30
31
import re
32
from typing import (
33
+ TYPE_CHECKING,
34
Any,
35
Callable,
36
Optional,
37
@@ -XXX,XX +XXX,XX @@
38
39
from docutils import nodes
40
from docutils.nodes import Element, Node, Text
41
+from docutils.statemachine import StringList
42
43
import sphinx
44
from sphinx import addnodes, util
45
+from sphinx.directives import ObjectDescription
46
from sphinx.environment import BuildEnvironment
47
from sphinx.roles import XRefRole
48
from sphinx.util import docfields
49
@@ -XXX,XX +XXX,XX @@ class CompatGroupedField(docfields.GroupedField):
50
class CompatTypedField(docfields.TypedField):
51
if MAKE_XREF_WORKAROUND:
52
make_xref = _compat_make_xref
53
+
54
+
55
+# ################################################################
56
+# Nested parsing error location fix for Sphinx 5.3.0 < x < 6.2.0 #
57
+# ################################################################
58
+
59
+# When we require Sphinx 4.x, the TYPE_CHECKING hack where we avoid
60
+# subscripting ObjectDescription at runtime can be removed in favor of
61
+# just always subscripting the class.
62
+
63
+# When we require Sphinx > 6.2.0, the rest of this compatibility hack
64
+# can be dropped and QAPIObject can just inherit directly from
65
+# ObjectDescription[Signature].
66
+
67
+SOURCE_LOCATION_FIX = (5, 3, 0) <= sphinx.version_info[:3] < (6, 2, 0)
68
+
69
+Signature = str
70
+
71
+
72
+if TYPE_CHECKING:
73
+ _BaseClass = ObjectDescription[Signature]
74
+else:
75
+ _BaseClass = ObjectDescription
76
+
77
+
78
+class ParserFix(_BaseClass):
79
+
80
+ _temp_content: StringList
81
+ _temp_offset: int
82
+ _temp_node: Optional[addnodes.desc_content]
83
+
84
+ def before_content(self) -> None:
85
+ # Work around a sphinx bug and parse the content ourselves.
86
+ self._temp_content = self.content
87
+ self._temp_offset = self.content_offset
88
+ self._temp_node = None
89
+
90
+ if SOURCE_LOCATION_FIX:
91
+ self._temp_node = addnodes.desc_content()
92
+ self.state.nested_parse(
93
+ self.content, self.content_offset, self._temp_node
94
+ )
95
+ # Sphinx will try to parse the content block itself,
96
+ # Give it nothingness to parse instead.
97
+ self.content = StringList()
98
+ self.content_offset = 0
99
+
100
+ def transform_content(self, content_node: addnodes.desc_content) -> None:
101
+ # Sphinx workaround: Inject our parsed content and restore state.
102
+ if self._temp_node:
103
+ content_node += self._temp_node.children
104
+ self.content = self._temp_content
105
+ self.content_offset = self._temp_offset
106
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
107
index XXXXXXX..XXXXXXX 100644
108
--- a/docs/sphinx/qapi_domain.py
109
+++ b/docs/sphinx/qapi_domain.py
110
@@ -XXX,XX +XXX,XX @@
111
CompatGroupedField,
112
CompatTypedField,
113
KeywordNode,
114
+ ParserFix,
115
+ Signature,
116
SpaceNode,
117
)
118
from sphinx import addnodes
119
@@ -XXX,XX +XXX,XX @@ def result_nodes(
120
return results, []
121
122
123
-# Alias for the return of handle_signature(), which is used in several places.
124
-# (In the Python domain, this is Tuple[str, str] instead.)
125
-Signature = str
126
-
127
-
128
-class QAPIDescription(ObjectDescription[Signature]):
129
+class QAPIDescription(ParserFix):
130
"""
131
Generic QAPI description.
132
133
@@ -XXX,XX +XXX,XX @@ def _validate_field(self, field: nodes.field) -> None:
134
logger.warning(msg, location=field)
135
136
def transform_content(self, content_node: addnodes.desc_content) -> None:
137
+ # This hook runs after before_content and the nested parse, but
138
+ # before the DocFieldTransformer is executed.
139
+ super().transform_content(content_node)
140
+
141
self._add_infopips(content_node)
142
143
# Validate field lists.
144
@@ -XXX,XX +XXX,XX @@ class QAPIObjectWithMembers(QAPIObject):
145
146
147
class QAPIEvent(QAPIObjectWithMembers):
148
+ # pylint: disable=too-many-ancestors
149
"""Description of a QAPI Event."""
150
151
152
class QAPIJSONObject(QAPIObjectWithMembers):
153
+ # pylint: disable=too-many-ancestors
154
"""Description of a QAPI Object: structs and unions."""
155
156
157
--
158
2.48.1
diff view generated by jsdifflib
New patch
1
Instead of using the info object for the doc block as a whole (which
2
always points to the very first line of the block), update the info
3
pointer for each call to ensure_untagged_section when the existing
4
section is otherwise empty. This way, Sphinx error information will
5
match precisely to where the text actually starts.
1
6
7
For example, this patch will move the info pointer for the "Hello!"
8
untagged section ...
9
10
> ## <-- from here ...
11
> # Hello! <-- ... to here.
12
> ##
13
14
Signed-off-by: John Snow <jsnow@redhat.com>
15
---
16
scripts/qapi/parser.py | 6 +++++-
17
1 file changed, 5 insertions(+), 1 deletion(-)
18
19
diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
20
index XXXXXXX..XXXXXXX 100644
21
--- a/scripts/qapi/parser.py
22
+++ b/scripts/qapi/parser.py
23
@@ -XXX,XX +XXX,XX @@ def end(self) -> None:
24
def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
25
if self.all_sections and not self.all_sections[-1].tag:
26
# extend current section
27
- self.all_sections[-1].text += '\n'
28
+ section = self.all_sections[-1]
29
+ if not section.text:
30
+ # Section is empty so far; update info to start *here*.
31
+ section.info = info
32
+ section.text += '\n'
33
return
34
# start new section
35
section = self.Section(info)
36
--
37
2.48.1
diff view generated by jsdifflib
New patch
1
This patch adds an explicit section "kind" to all QAPIDoc
2
sections. Members/Features are now explicitly marked as such, with the
3
name now being stored in a dedicated "name" field (which qapidoc.py was
4
not actually using anyway.)
1
5
6
The qapi-schema tests are updated to account for the new section names;
7
mostly "TODO" becomes "Todo" and `None` becomes "Plain".
8
9
Signed-off-by: John Snow <jsnow@redhat.com>
10
---
11
docs/sphinx/qapidoc.py | 7 +--
12
scripts/qapi/parser.py | 97 ++++++++++++++++++++++++----------
13
tests/qapi-schema/doc-good.out | 10 ++--
14
tests/qapi-schema/test-qapi.py | 2 +-
15
4 files changed, 80 insertions(+), 36 deletions(-)
16
17
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
18
index XXXXXXX..XXXXXXX 100644
19
--- a/docs/sphinx/qapidoc.py
20
+++ b/docs/sphinx/qapidoc.py
21
@@ -XXX,XX +XXX,XX @@
22
from docutils.statemachine import ViewList
23
from qapi.error import QAPIError, QAPISemError
24
from qapi.gen import QAPISchemaVisitor
25
+from qapi.parser import QAPIDoc
26
from qapi.schema import QAPISchema
27
28
from sphinx import addnodes
29
@@ -XXX,XX +XXX,XX @@ def _nodes_for_sections(self, doc):
30
"""Return list of doctree nodes for additional sections"""
31
nodelist = []
32
for section in doc.sections:
33
- if section.tag and section.tag == 'TODO':
34
+ if section.kind == QAPIDoc.Kind.TODO:
35
# Hide TODO: sections
36
continue
37
38
- if not section.tag:
39
+ if section.kind == QAPIDoc.Kind.PLAIN:
40
# Sphinx cannot handle sectionless titles;
41
# Instead, just append the results to the prior section.
42
container = nodes.container()
43
@@ -XXX,XX +XXX,XX @@ def _nodes_for_sections(self, doc):
44
nodelist += container.children
45
continue
46
47
- snode = self._make_section(section.tag)
48
+ snode = self._make_section(section.kind.name.title())
49
self._parse_text_into_node(dedent(section.text), snode)
50
nodelist.append(snode)
51
return nodelist
52
diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
53
index XXXXXXX..XXXXXXX 100644
54
--- a/scripts/qapi/parser.py
55
+++ b/scripts/qapi/parser.py
56
@@ -XXX,XX +XXX,XX @@
57
# This work is licensed under the terms of the GNU GPL, version 2.
58
# See the COPYING file in the top-level directory.
59
60
+import enum
61
import os
62
import re
63
from typing import (
64
@@ -XXX,XX +XXX,XX @@ def get_doc(self) -> 'QAPIDoc':
65
)
66
raise QAPIParseError(self, emsg)
67
68
- doc.new_tagged_section(self.info, match.group(1))
69
+ doc.new_tagged_section(
70
+ self.info,
71
+ QAPIDoc.Kind.from_string(match.group(1))
72
+ )
73
text = line[match.end():]
74
if text:
75
doc.append_line(text)
76
@@ -XXX,XX +XXX,XX @@ def get_doc(self) -> 'QAPIDoc':
77
self,
78
"unexpected '=' markup in definition documentation")
79
else:
80
- # tag-less paragraph
81
+ # plain paragraph
82
doc.ensure_untagged_section(self.info)
83
doc.append_line(line)
84
line = self.get_doc_paragraph(doc)
85
@@ -XXX,XX +XXX,XX @@ class QAPIDoc:
86
Free-form documentation blocks consist only of a body section.
87
"""
88
89
+ class Kind(enum.Enum):
90
+ PLAIN = 0
91
+ MEMBER = 1
92
+ FEATURE = 2
93
+ RETURNS = 3
94
+ ERRORS = 4
95
+ SINCE = 5
96
+ TODO = 6
97
+
98
+ @staticmethod
99
+ def from_string(kind: str) -> 'QAPIDoc.Kind':
100
+ return QAPIDoc.Kind[kind.upper()]
101
+
102
+ def __str__(self) -> str:
103
+ return self.name.title()
104
+
105
class Section:
106
# pylint: disable=too-few-public-methods
107
- def __init__(self, info: QAPISourceInfo,
108
- tag: Optional[str] = None):
109
+ def __init__(
110
+ self,
111
+ info: QAPISourceInfo,
112
+ kind: 'QAPIDoc.Kind',
113
+ ):
114
# section source info, i.e. where it begins
115
self.info = info
116
- # section tag, if any ('Returns', '@name', ...)
117
- self.tag = tag
118
+ # section kind
119
+ self.kind = kind
120
# section text without tag
121
self.text = ''
122
123
@@ -XXX,XX +XXX,XX @@ def append_line(self, line: str) -> None:
124
self.text += line + '\n'
125
126
class ArgSection(Section):
127
- def __init__(self, info: QAPISourceInfo, tag: str):
128
- super().__init__(info, tag)
129
+ def __init__(
130
+ self,
131
+ info: QAPISourceInfo,
132
+ kind: 'QAPIDoc.Kind',
133
+ name: str
134
+ ):
135
+ super().__init__(info, kind)
136
+ self.name = name
137
self.member: Optional['QAPISchemaMember'] = None
138
139
def connect(self, member: 'QAPISchemaMember') -> None:
140
@@ -XXX,XX +XXX,XX @@ def __init__(self, info: QAPISourceInfo, symbol: Optional[str] = None):
141
# definition doc's symbol, None for free-form doc
142
self.symbol: Optional[str] = symbol
143
# the sections in textual order
144
- self.all_sections: List[QAPIDoc.Section] = [QAPIDoc.Section(info)]
145
+ self.all_sections: List[QAPIDoc.Section] = [
146
+ QAPIDoc.Section(info, QAPIDoc.Kind.PLAIN)
147
+ ]
148
# the body section
149
self.body: Optional[QAPIDoc.Section] = self.all_sections[0]
150
# dicts mapping parameter/feature names to their description
151
@@ -XXX,XX +XXX,XX @@ def __init__(self, info: QAPISourceInfo, symbol: Optional[str] = None):
152
def end(self) -> None:
153
for section in self.all_sections:
154
section.text = section.text.strip('\n')
155
- if section.tag is not None and section.text == '':
156
+ if section.kind != QAPIDoc.Kind.PLAIN and section.text == '':
157
raise QAPISemError(
158
- section.info, "text required after '%s:'" % section.tag)
159
+ section.info, "text required after '%s:'" % section.kind)
160
161
def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
162
- if self.all_sections and not self.all_sections[-1].tag:
163
+ kind = QAPIDoc.Kind.PLAIN
164
+
165
+ if self.all_sections and self.all_sections[-1].kind == kind:
166
# extend current section
167
section = self.all_sections[-1]
168
if not section.text:
169
@@ -XXX,XX +XXX,XX @@ def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
170
section.info = info
171
section.text += '\n'
172
return
173
+
174
# start new section
175
- section = self.Section(info)
176
+ section = self.Section(info, kind)
177
self.sections.append(section)
178
self.all_sections.append(section)
179
180
- def new_tagged_section(self, info: QAPISourceInfo, tag: str) -> None:
181
- section = self.Section(info, tag)
182
- if tag == 'Returns':
183
+ def new_tagged_section(
184
+ self,
185
+ info: QAPISourceInfo,
186
+ kind: 'QAPIDoc.Kind',
187
+ ) -> None:
188
+ section = self.Section(info, kind)
189
+ if kind == QAPIDoc.Kind.RETURNS:
190
if self.returns:
191
raise QAPISemError(
192
- info, "duplicated '%s' section" % tag)
193
+ info, "duplicated '%s' section" % kind)
194
self.returns = section
195
- elif tag == 'Errors':
196
+ elif kind == QAPIDoc.Kind.ERRORS:
197
if self.errors:
198
raise QAPISemError(
199
- info, "duplicated '%s' section" % tag)
200
+ info, "duplicated '%s' section" % kind)
201
self.errors = section
202
- elif tag == 'Since':
203
+ elif kind == QAPIDoc.Kind.SINCE:
204
if self.since:
205
raise QAPISemError(
206
- info, "duplicated '%s' section" % tag)
207
+ info, "duplicated '%s' section" % kind)
208
self.since = section
209
self.sections.append(section)
210
self.all_sections.append(section)
211
212
- def _new_description(self, info: QAPISourceInfo, name: str,
213
- desc: Dict[str, ArgSection]) -> None:
214
+ def _new_description(
215
+ self,
216
+ info: QAPISourceInfo,
217
+ name: str,
218
+ kind: 'QAPIDoc.Kind',
219
+ desc: Dict[str, ArgSection]
220
+ ) -> None:
221
if not name:
222
raise QAPISemError(info, "invalid parameter name")
223
if name in desc:
224
raise QAPISemError(info, "'%s' parameter name duplicated" % name)
225
- section = self.ArgSection(info, '@' + name)
226
+ section = self.ArgSection(info, kind, name)
227
self.all_sections.append(section)
228
desc[name] = section
229
230
def new_argument(self, info: QAPISourceInfo, name: str) -> None:
231
- self._new_description(info, name, self.args)
232
+ self._new_description(info, name, QAPIDoc.Kind.MEMBER, self.args)
233
234
def new_feature(self, info: QAPISourceInfo, name: str) -> None:
235
- self._new_description(info, name, self.features)
236
+ self._new_description(info, name, QAPIDoc.Kind.FEATURE, self.features)
237
238
def append_line(self, line: str) -> None:
239
self.all_sections[-1].append_line(line)
240
@@ -XXX,XX +XXX,XX @@ def connect_member(self, member: 'QAPISchemaMember') -> None:
241
"%s '%s' lacks documentation"
242
% (member.role, member.name))
243
self.args[member.name] = QAPIDoc.ArgSection(
244
- self.info, '@' + member.name)
245
+ self.info, QAPIDoc.Kind.MEMBER, member.name)
246
self.args[member.name].connect(member)
247
248
def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
249
diff --git a/tests/qapi-schema/doc-good.out b/tests/qapi-schema/doc-good.out
250
index XXXXXXX..XXXXXXX 100644
251
--- a/tests/qapi-schema/doc-good.out
252
+++ b/tests/qapi-schema/doc-good.out
253
@@ -XXX,XX +XXX,XX @@ The _one_ {and only}, description on the same line
254
Also _one_ {and only}
255
feature=enum-member-feat
256
a member feature
257
- section=None
258
+ section=Plain
259
@two is undocumented
260
doc symbol=Base
261
body=
262
@@ -XXX,XX +XXX,XX @@ description starts on the same line
263
a feature
264
feature=cmd-feat2
265
another feature
266
- section=None
267
+ section=Plain
268
.. note:: @arg3 is undocumented
269
section=Returns
270
@Object
271
section=Errors
272
some
273
- section=TODO
274
+ section=Todo
275
frobnicate
276
- section=None
277
+ section=Plain
278
.. admonition:: Notes
279
280
- Lorem ipsum dolor sit amet
281
@@ -XXX,XX +XXX,XX @@ If you're bored enough to read this, go see a video of boxed cats
282
a feature
283
feature=cmd-feat2
284
another feature
285
- section=None
286
+ section=Plain
287
.. qmp-example::
288
289
-> "this example"
290
diff --git a/tests/qapi-schema/test-qapi.py b/tests/qapi-schema/test-qapi.py
291
index XXXXXXX..XXXXXXX 100755
292
--- a/tests/qapi-schema/test-qapi.py
293
+++ b/tests/qapi-schema/test-qapi.py
294
@@ -XXX,XX +XXX,XX @@ def test_frontend(fname):
295
for feat, section in doc.features.items():
296
print(' feature=%s\n%s' % (feat, section.text))
297
for section in doc.sections:
298
- print(' section=%s\n%s' % (section.tag, section.text))
299
+ print(' section=%s\n%s' % (section.kind, section.text))
300
301
302
def open_test_result(dir_name, file_name, update):
303
--
304
2.48.1
diff view generated by jsdifflib
New patch
1
Makes debugging far more pleasant when you can just print(section) and
2
get something reasonable to display.
1
3
4
Signed-off-by: John Snow <jsnow@redhat.com>
5
---
6
scripts/qapi/parser.py | 3 +++
7
1 file changed, 3 insertions(+)
8
9
diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
10
index XXXXXXX..XXXXXXX 100644
11
--- a/scripts/qapi/parser.py
12
+++ b/scripts/qapi/parser.py
13
@@ -XXX,XX +XXX,XX @@ def __init__(
14
# section text without tag
15
self.text = ''
16
17
+ def __repr__(self) -> str:
18
+ return f"<QAPIDoc.Section kind={self.kind!r} text={self.text!r}>"
19
+
20
def append_line(self, line: str) -> None:
21
self.text += line + '\n'
22
23
--
24
2.48.1
diff view generated by jsdifflib
New patch
1
This commit adds a stubbed option to the qapi-doc directive that opts-in
2
to the new rST generator; the implementation of which will follow in
3
subsequent commits.
1
4
5
Once all QAPI documents have been converted, this option and the old
6
qapidoc implementation can be dropped.
7
8
Note that moving code outside of the try...except block has no impact
9
because the code moved outside of that block does not ever raise a
10
QAPIError.
11
12
Signed-off-by: John Snow <jsnow@redhat.com>
13
---
14
docs/sphinx/qapidoc.py | 41 ++++++++++++++++++++++++++++-------------
15
1 file changed, 28 insertions(+), 13 deletions(-)
16
17
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
18
index XXXXXXX..XXXXXXX 100644
19
--- a/docs/sphinx/qapidoc.py
20
+++ b/docs/sphinx/qapidoc.py
21
@@ -XXX,XX +XXX,XX @@ def _parse_text_into_node(self, doctext, node):
22
rstlist.append("", self._cur_doc.info.fname, self._cur_doc.info.line)
23
self._sphinx_directive.do_parse(rstlist, node)
24
25
- def get_document_nodes(self):
26
- """Return the list of docutils nodes which make up the document"""
27
- return self._top_node.children
28
+ def get_document_node(self):
29
+ """Return the root docutils node which makes up the document"""
30
+ return self._top_node
31
32
33
# Turn the black formatter on for the rest of the file.
34
@@ -XXX,XX +XXX,XX @@ class QAPIDocDirective(NestedDirective):
35
36
required_argument = 1
37
optional_arguments = 1
38
- option_spec = {"qapifile": directives.unchanged_required}
39
+ option_spec = {
40
+ "qapifile": directives.unchanged_required,
41
+ "transmogrify": directives.flag,
42
+ }
43
has_content = False
44
45
def new_serialno(self):
46
@@ -XXX,XX +XXX,XX @@ def new_serialno(self):
47
env = self.state.document.settings.env
48
return "qapidoc-%d" % env.new_serialno("qapidoc")
49
50
+ def transmogrify(self, schema) -> nodes.Element:
51
+ raise NotImplementedError
52
+
53
+ def legacy(self, schema) -> nodes.Element:
54
+ vis = QAPISchemaGenRSTVisitor(self)
55
+ vis.visit_begin(schema)
56
+ for doc in schema.docs:
57
+ if doc.symbol:
58
+ vis.symbol(doc, schema.lookup_entity(doc.symbol))
59
+ else:
60
+ vis.freeform(doc)
61
+ return vis.get_document_node()
62
+
63
def run(self):
64
env = self.state.document.settings.env
65
qapifile = env.config.qapidoc_srctree + "/" + self.arguments[0]
66
qapidir = os.path.dirname(qapifile)
67
+ transmogrify = "transmogrify" in self.options
68
69
try:
70
schema = QAPISchema(qapifile)
71
@@ -XXX,XX +XXX,XX @@ def run(self):
72
# First tell Sphinx about all the schema files that the
73
# output documentation depends on (including 'qapifile' itself)
74
schema.visit(QAPISchemaGenDepVisitor(env, qapidir))
75
-
76
- vis = QAPISchemaGenRSTVisitor(self)
77
- vis.visit_begin(schema)
78
- for doc in schema.docs:
79
- if doc.symbol:
80
- vis.symbol(doc, schema.lookup_entity(doc.symbol))
81
- else:
82
- vis.freeform(doc)
83
- return vis.get_document_nodes()
84
except QAPIError as err:
85
# Launder QAPI parse errors into Sphinx extension errors
86
# so they are displayed nicely to the user
87
raise ExtensionError(str(err)) from err
88
89
+ if transmogrify:
90
+ contentnode = self.transmogrify(schema)
91
+ else:
92
+ contentnode = self.legacy(schema)
93
+
94
+ return contentnode.children
95
+
96
97
class QMPExample(CodeBlock, NestedDirective):
98
"""
99
--
100
2.48.1
diff view generated by jsdifflib
New patch
1
This is being done primarily to be able to type check and delint the new
2
implementation without needing to worry about fixing up the old
3
implementation.
1
4
5
I'm adding the new implementation into the existing file instead of into
6
a new file so that when the dust settles, qapidoc.py will contain the
7
full history of development on this generative module.
8
9
This patch *should* be pure motion, give or take the import statements.
10
11
Signed-off-by: John Snow <jsnow@redhat.com>
12
---
13
docs/sphinx/qapidoc.py | 420 +-------------------------------
14
docs/sphinx/qapidoc_legacy.py | 439 ++++++++++++++++++++++++++++++++++
15
2 files changed, 441 insertions(+), 418 deletions(-)
16
create mode 100644 docs/sphinx/qapidoc_legacy.py
17
18
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
19
index XXXXXXX..XXXXXXX 100644
20
--- a/docs/sphinx/qapidoc.py
21
+++ b/docs/sphinx/qapidoc.py
22
@@ -XXX,XX +XXX,XX @@
23
"""
24
25
import os
26
-import re
27
import sys
28
-import textwrap
29
from typing import List
30
31
from docutils import nodes
32
from docutils.parsers.rst import Directive, directives
33
-from docutils.statemachine import ViewList
34
-from qapi.error import QAPIError, QAPISemError
35
+from qapi.error import QAPIError
36
from qapi.gen import QAPISchemaVisitor
37
-from qapi.parser import QAPIDoc
38
from qapi.schema import QAPISchema
39
40
+from qapidoc_legacy import QAPISchemaGenRSTVisitor
41
from sphinx import addnodes
42
from sphinx.directives.code import CodeBlock
43
from sphinx.errors import ExtensionError
44
@@ -XXX,XX +XXX,XX @@
45
__version__ = "1.0"
46
47
48
-def dedent(text: str) -> str:
49
- # Adjust indentation to make description text parse as paragraph.
50
-
51
- lines = text.splitlines(True)
52
- if re.match(r"\s+", lines[0]):
53
- # First line is indented; description started on the line after
54
- # the name. dedent the whole block.
55
- return textwrap.dedent(text)
56
-
57
- # Descr started on same line. Dedent line 2+.
58
- return lines[0] + textwrap.dedent("".join(lines[1:]))
59
-
60
-
61
-# Disable black auto-formatter until re-enabled:
62
-# fmt: off
63
-
64
-
65
-class QAPISchemaGenRSTVisitor(QAPISchemaVisitor):
66
- """A QAPI schema visitor which generates docutils/Sphinx nodes
67
-
68
- This class builds up a tree of docutils/Sphinx nodes corresponding
69
- to documentation for the various QAPI objects. To use it, first
70
- create a QAPISchemaGenRSTVisitor object, and call its
71
- visit_begin() method. Then you can call one of the two methods
72
- 'freeform' (to add documentation for a freeform documentation
73
- chunk) or 'symbol' (to add documentation for a QAPI symbol). These
74
- will cause the visitor to build up the tree of document
75
- nodes. Once you've added all the documentation via 'freeform' and
76
- 'symbol' method calls, you can call 'get_document_nodes' to get
77
- the final list of document nodes (in a form suitable for returning
78
- from a Sphinx directive's 'run' method).
79
- """
80
- def __init__(self, sphinx_directive):
81
- self._cur_doc = None
82
- self._sphinx_directive = sphinx_directive
83
- self._top_node = nodes.section()
84
- self._active_headings = [self._top_node]
85
-
86
- def _make_dlitem(self, term, defn):
87
- """Return a dlitem node with the specified term and definition.
88
-
89
- term should be a list of Text and literal nodes.
90
- defn should be one of:
91
- - a string, which will be handed to _parse_text_into_node
92
- - a list of Text and literal nodes, which will be put into
93
- a paragraph node
94
- """
95
- dlitem = nodes.definition_list_item()
96
- dlterm = nodes.term('', '', *term)
97
- dlitem += dlterm
98
- if defn:
99
- dldef = nodes.definition()
100
- if isinstance(defn, list):
101
- dldef += nodes.paragraph('', '', *defn)
102
- else:
103
- self._parse_text_into_node(defn, dldef)
104
- dlitem += dldef
105
- return dlitem
106
-
107
- def _make_section(self, title):
108
- """Return a section node with optional title"""
109
- section = nodes.section(ids=[self._sphinx_directive.new_serialno()])
110
- if title:
111
- section += nodes.title(title, title)
112
- return section
113
-
114
- def _nodes_for_ifcond(self, ifcond, with_if=True):
115
- """Return list of Text, literal nodes for the ifcond
116
-
117
- Return a list which gives text like ' (If: condition)'.
118
- If with_if is False, we don't return the "(If: " and ")".
119
- """
120
-
121
- doc = ifcond.docgen()
122
- if not doc:
123
- return []
124
- doc = nodes.literal('', doc)
125
- if not with_if:
126
- return [doc]
127
-
128
- nodelist = [nodes.Text(' ('), nodes.strong('', 'If: ')]
129
- nodelist.append(doc)
130
- nodelist.append(nodes.Text(')'))
131
- return nodelist
132
-
133
- def _nodes_for_one_member(self, member):
134
- """Return list of Text, literal nodes for this member
135
-
136
- Return a list of doctree nodes which give text like
137
- 'name: type (optional) (If: ...)' suitable for use as the
138
- 'term' part of a definition list item.
139
- """
140
- term = [nodes.literal('', member.name)]
141
- if member.type.doc_type():
142
- term.append(nodes.Text(': '))
143
- term.append(nodes.literal('', member.type.doc_type()))
144
- if member.optional:
145
- term.append(nodes.Text(' (optional)'))
146
- if member.ifcond.is_present():
147
- term.extend(self._nodes_for_ifcond(member.ifcond))
148
- return term
149
-
150
- def _nodes_for_variant_when(self, branches, variant):
151
- """Return list of Text, literal nodes for variant 'when' clause
152
-
153
- Return a list of doctree nodes which give text like
154
- 'when tagname is variant (If: ...)' suitable for use in
155
- the 'branches' part of a definition list.
156
- """
157
- term = [nodes.Text(' when '),
158
- nodes.literal('', branches.tag_member.name),
159
- nodes.Text(' is '),
160
- nodes.literal('', '"%s"' % variant.name)]
161
- if variant.ifcond.is_present():
162
- term.extend(self._nodes_for_ifcond(variant.ifcond))
163
- return term
164
-
165
- def _nodes_for_members(self, doc, what, base=None, branches=None):
166
- """Return list of doctree nodes for the table of members"""
167
- dlnode = nodes.definition_list()
168
- for section in doc.args.values():
169
- term = self._nodes_for_one_member(section.member)
170
- # TODO drop fallbacks when undocumented members are outlawed
171
- if section.text:
172
- defn = dedent(section.text)
173
- else:
174
- defn = [nodes.Text('Not documented')]
175
-
176
- dlnode += self._make_dlitem(term, defn)
177
-
178
- if base:
179
- dlnode += self._make_dlitem([nodes.Text('The members of '),
180
- nodes.literal('', base.doc_type())],
181
- None)
182
-
183
- if branches:
184
- for v in branches.variants:
185
- if v.type.name == 'q_empty':
186
- continue
187
- assert not v.type.is_implicit()
188
- term = [nodes.Text('The members of '),
189
- nodes.literal('', v.type.doc_type())]
190
- term.extend(self._nodes_for_variant_when(branches, v))
191
- dlnode += self._make_dlitem(term, None)
192
-
193
- if not dlnode.children:
194
- return []
195
-
196
- section = self._make_section(what)
197
- section += dlnode
198
- return [section]
199
-
200
- def _nodes_for_enum_values(self, doc):
201
- """Return list of doctree nodes for the table of enum values"""
202
- seen_item = False
203
- dlnode = nodes.definition_list()
204
- for section in doc.args.values():
205
- termtext = [nodes.literal('', section.member.name)]
206
- if section.member.ifcond.is_present():
207
- termtext.extend(self._nodes_for_ifcond(section.member.ifcond))
208
- # TODO drop fallbacks when undocumented members are outlawed
209
- if section.text:
210
- defn = dedent(section.text)
211
- else:
212
- defn = [nodes.Text('Not documented')]
213
-
214
- dlnode += self._make_dlitem(termtext, defn)
215
- seen_item = True
216
-
217
- if not seen_item:
218
- return []
219
-
220
- section = self._make_section('Values')
221
- section += dlnode
222
- return [section]
223
-
224
- def _nodes_for_arguments(self, doc, arg_type):
225
- """Return list of doctree nodes for the arguments section"""
226
- if arg_type and not arg_type.is_implicit():
227
- assert not doc.args
228
- section = self._make_section('Arguments')
229
- dlnode = nodes.definition_list()
230
- dlnode += self._make_dlitem(
231
- [nodes.Text('The members of '),
232
- nodes.literal('', arg_type.name)],
233
- None)
234
- section += dlnode
235
- return [section]
236
-
237
- return self._nodes_for_members(doc, 'Arguments')
238
-
239
- def _nodes_for_features(self, doc):
240
- """Return list of doctree nodes for the table of features"""
241
- seen_item = False
242
- dlnode = nodes.definition_list()
243
- for section in doc.features.values():
244
- dlnode += self._make_dlitem(
245
- [nodes.literal('', section.member.name)], dedent(section.text))
246
- seen_item = True
247
-
248
- if not seen_item:
249
- return []
250
-
251
- section = self._make_section('Features')
252
- section += dlnode
253
- return [section]
254
-
255
- def _nodes_for_sections(self, doc):
256
- """Return list of doctree nodes for additional sections"""
257
- nodelist = []
258
- for section in doc.sections:
259
- if section.kind == QAPIDoc.Kind.TODO:
260
- # Hide TODO: sections
261
- continue
262
-
263
- if section.kind == QAPIDoc.Kind.PLAIN:
264
- # Sphinx cannot handle sectionless titles;
265
- # Instead, just append the results to the prior section.
266
- container = nodes.container()
267
- self._parse_text_into_node(section.text, container)
268
- nodelist += container.children
269
- continue
270
-
271
- snode = self._make_section(section.kind.name.title())
272
- self._parse_text_into_node(dedent(section.text), snode)
273
- nodelist.append(snode)
274
- return nodelist
275
-
276
- def _nodes_for_if_section(self, ifcond):
277
- """Return list of doctree nodes for the "If" section"""
278
- nodelist = []
279
- if ifcond.is_present():
280
- snode = self._make_section('If')
281
- snode += nodes.paragraph(
282
- '', '', *self._nodes_for_ifcond(ifcond, with_if=False)
283
- )
284
- nodelist.append(snode)
285
- return nodelist
286
-
287
- def _add_doc(self, typ, sections):
288
- """Add documentation for a command/object/enum...
289
-
290
- We assume we're documenting the thing defined in self._cur_doc.
291
- typ is the type of thing being added ("Command", "Object", etc)
292
-
293
- sections is a list of nodes for sections to add to the definition.
294
- """
295
-
296
- doc = self._cur_doc
297
- snode = nodes.section(ids=[self._sphinx_directive.new_serialno()])
298
- snode += nodes.title('', '', *[nodes.literal(doc.symbol, doc.symbol),
299
- nodes.Text(' (' + typ + ')')])
300
- self._parse_text_into_node(doc.body.text, snode)
301
- for s in sections:
302
- if s is not None:
303
- snode += s
304
- self._add_node_to_current_heading(snode)
305
-
306
- def visit_enum_type(self, name, info, ifcond, features, members, prefix):
307
- doc = self._cur_doc
308
- self._add_doc('Enum',
309
- self._nodes_for_enum_values(doc)
310
- + self._nodes_for_features(doc)
311
- + self._nodes_for_sections(doc)
312
- + self._nodes_for_if_section(ifcond))
313
-
314
- def visit_object_type(self, name, info, ifcond, features,
315
- base, members, branches):
316
- doc = self._cur_doc
317
- if base and base.is_implicit():
318
- base = None
319
- self._add_doc('Object',
320
- self._nodes_for_members(doc, 'Members', base, branches)
321
- + self._nodes_for_features(doc)
322
- + self._nodes_for_sections(doc)
323
- + self._nodes_for_if_section(ifcond))
324
-
325
- def visit_alternate_type(self, name, info, ifcond, features,
326
- alternatives):
327
- doc = self._cur_doc
328
- self._add_doc('Alternate',
329
- self._nodes_for_members(doc, 'Members')
330
- + self._nodes_for_features(doc)
331
- + self._nodes_for_sections(doc)
332
- + self._nodes_for_if_section(ifcond))
333
-
334
- def visit_command(self, name, info, ifcond, features, arg_type,
335
- ret_type, gen, success_response, boxed, allow_oob,
336
- allow_preconfig, coroutine):
337
- doc = self._cur_doc
338
- self._add_doc('Command',
339
- self._nodes_for_arguments(doc, arg_type)
340
- + self._nodes_for_features(doc)
341
- + self._nodes_for_sections(doc)
342
- + self._nodes_for_if_section(ifcond))
343
-
344
- def visit_event(self, name, info, ifcond, features, arg_type, boxed):
345
- doc = self._cur_doc
346
- self._add_doc('Event',
347
- self._nodes_for_arguments(doc, arg_type)
348
- + self._nodes_for_features(doc)
349
- + self._nodes_for_sections(doc)
350
- + self._nodes_for_if_section(ifcond))
351
-
352
- def symbol(self, doc, entity):
353
- """Add documentation for one symbol to the document tree
354
-
355
- This is the main entry point which causes us to add documentation
356
- nodes for a symbol (which could be a 'command', 'object', 'event',
357
- etc). We do this by calling 'visit' on the schema entity, which
358
- will then call back into one of our visit_* methods, depending
359
- on what kind of thing this symbol is.
360
- """
361
- self._cur_doc = doc
362
- entity.visit(self)
363
- self._cur_doc = None
364
-
365
- def _start_new_heading(self, heading, level):
366
- """Start a new heading at the specified heading level
367
-
368
- Create a new section whose title is 'heading' and which is placed
369
- in the docutils node tree as a child of the most recent level-1
370
- heading. Subsequent document sections (commands, freeform doc chunks,
371
- etc) will be placed as children of this new heading section.
372
- """
373
- if len(self._active_headings) < level:
374
- raise QAPISemError(self._cur_doc.info,
375
- 'Level %d subheading found outside a '
376
- 'level %d heading'
377
- % (level, level - 1))
378
- snode = self._make_section(heading)
379
- self._active_headings[level - 1] += snode
380
- self._active_headings = self._active_headings[:level]
381
- self._active_headings.append(snode)
382
- return snode
383
-
384
- def _add_node_to_current_heading(self, node):
385
- """Add the node to whatever the current active heading is"""
386
- self._active_headings[-1] += node
387
-
388
- def freeform(self, doc):
389
- """Add a piece of 'freeform' documentation to the document tree
390
-
391
- A 'freeform' document chunk doesn't relate to any particular
392
- symbol (for instance, it could be an introduction).
393
-
394
- If the freeform document starts with a line of the form
395
- '= Heading text', this is a section or subsection heading, with
396
- the heading level indicated by the number of '=' signs.
397
- """
398
-
399
- # QAPIDoc documentation says free-form documentation blocks
400
- # must have only a body section, nothing else.
401
- assert not doc.sections
402
- assert not doc.args
403
- assert not doc.features
404
- self._cur_doc = doc
405
-
406
- text = doc.body.text
407
- if re.match(r'=+ ', text):
408
- # Section/subsection heading (if present, will always be
409
- # the first line of the block)
410
- (heading, _, text) = text.partition('\n')
411
- (leader, _, heading) = heading.partition(' ')
412
- node = self._start_new_heading(heading, len(leader))
413
- if text == '':
414
- return
415
- else:
416
- node = nodes.container()
417
-
418
- self._parse_text_into_node(text, node)
419
- self._cur_doc = None
420
-
421
- def _parse_text_into_node(self, doctext, node):
422
- """Parse a chunk of QAPI-doc-format text into the node
423
-
424
- The doc comment can contain most inline rST markup, including
425
- bulleted and enumerated lists.
426
- As an extra permitted piece of markup, @var will be turned
427
- into ``var``.
428
- """
429
-
430
- # Handle the "@var means ``var`` case
431
- doctext = re.sub(r'@([\w-]+)', r'``\1``', doctext)
432
-
433
- rstlist = ViewList()
434
- for line in doctext.splitlines():
435
- # The reported line number will always be that of the start line
436
- # of the doc comment, rather than the actual location of the error.
437
- # Being more precise would require overhaul of the QAPIDoc class
438
- # to track lines more exactly within all the sub-parts of the doc
439
- # comment, as well as counting lines here.
440
- rstlist.append(line, self._cur_doc.info.fname,
441
- self._cur_doc.info.line)
442
- # Append a blank line -- in some cases rST syntax errors get
443
- # attributed to the line after one with actual text, and if there
444
- # isn't anything in the ViewList corresponding to that then Sphinx
445
- # 1.6's AutodocReporter will then misidentify the source/line location
446
- # in the error message (usually attributing it to the top-level
447
- # .rst file rather than the offending .json file). The extra blank
448
- # line won't affect the rendered output.
449
- rstlist.append("", self._cur_doc.info.fname, self._cur_doc.info.line)
450
- self._sphinx_directive.do_parse(rstlist, node)
451
-
452
- def get_document_node(self):
453
- """Return the root docutils node which makes up the document"""
454
- return self._top_node
455
-
456
-
457
-# Turn the black formatter on for the rest of the file.
458
-# fmt: on
459
-
460
-
461
class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
462
"""A QAPI schema visitor which adds Sphinx dependencies each module
463
464
diff --git a/docs/sphinx/qapidoc_legacy.py b/docs/sphinx/qapidoc_legacy.py
465
new file mode 100644
466
index XXXXXXX..XXXXXXX
467
--- /dev/null
468
+++ b/docs/sphinx/qapidoc_legacy.py
469
@@ -XXX,XX +XXX,XX @@
470
+# coding=utf-8
471
+#
472
+# QEMU qapidoc QAPI file parsing extension
473
+#
474
+# Copyright (c) 2020 Linaro
475
+#
476
+# This work is licensed under the terms of the GNU GPLv2 or later.
477
+# See the COPYING file in the top-level directory.
478
+
479
+"""
480
+qapidoc is a Sphinx extension that implements the qapi-doc directive
481
+
482
+The purpose of this extension is to read the documentation comments
483
+in QAPI schema files, and insert them all into the current document.
484
+
485
+It implements one new rST directive, "qapi-doc::".
486
+Each qapi-doc:: directive takes one argument, which is the
487
+pathname of the schema file to process, relative to the source tree.
488
+
489
+The docs/conf.py file must set the qapidoc_srctree config value to
490
+the root of the QEMU source tree.
491
+
492
+The Sphinx documentation on writing extensions is at:
493
+https://www.sphinx-doc.org/en/master/development/index.html
494
+"""
495
+
496
+import re
497
+import textwrap
498
+
499
+from docutils import nodes
500
+from docutils.statemachine import ViewList
501
+from qapi.error import QAPISemError
502
+from qapi.gen import QAPISchemaVisitor
503
+from qapi.parser import QAPIDoc
504
+
505
+
506
+def dedent(text: str) -> str:
507
+ # Adjust indentation to make description text parse as paragraph.
508
+
509
+ lines = text.splitlines(True)
510
+ if re.match(r"\s+", lines[0]):
511
+ # First line is indented; description started on the line after
512
+ # the name. dedent the whole block.
513
+ return textwrap.dedent(text)
514
+
515
+ # Descr started on same line. Dedent line 2+.
516
+ return lines[0] + textwrap.dedent("".join(lines[1:]))
517
+
518
+
519
+class QAPISchemaGenRSTVisitor(QAPISchemaVisitor):
520
+ """A QAPI schema visitor which generates docutils/Sphinx nodes
521
+
522
+ This class builds up a tree of docutils/Sphinx nodes corresponding
523
+ to documentation for the various QAPI objects. To use it, first
524
+ create a QAPISchemaGenRSTVisitor object, and call its
525
+ visit_begin() method. Then you can call one of the two methods
526
+ 'freeform' (to add documentation for a freeform documentation
527
+ chunk) or 'symbol' (to add documentation for a QAPI symbol). These
528
+ will cause the visitor to build up the tree of document
529
+ nodes. Once you've added all the documentation via 'freeform' and
530
+ 'symbol' method calls, you can call 'get_document_nodes' to get
531
+ the final list of document nodes (in a form suitable for returning
532
+ from a Sphinx directive's 'run' method).
533
+ """
534
+ def __init__(self, sphinx_directive):
535
+ self._cur_doc = None
536
+ self._sphinx_directive = sphinx_directive
537
+ self._top_node = nodes.section()
538
+ self._active_headings = [self._top_node]
539
+
540
+ def _make_dlitem(self, term, defn):
541
+ """Return a dlitem node with the specified term and definition.
542
+
543
+ term should be a list of Text and literal nodes.
544
+ defn should be one of:
545
+ - a string, which will be handed to _parse_text_into_node
546
+ - a list of Text and literal nodes, which will be put into
547
+ a paragraph node
548
+ """
549
+ dlitem = nodes.definition_list_item()
550
+ dlterm = nodes.term('', '', *term)
551
+ dlitem += dlterm
552
+ if defn:
553
+ dldef = nodes.definition()
554
+ if isinstance(defn, list):
555
+ dldef += nodes.paragraph('', '', *defn)
556
+ else:
557
+ self._parse_text_into_node(defn, dldef)
558
+ dlitem += dldef
559
+ return dlitem
560
+
561
+ def _make_section(self, title):
562
+ """Return a section node with optional title"""
563
+ section = nodes.section(ids=[self._sphinx_directive.new_serialno()])
564
+ if title:
565
+ section += nodes.title(title, title)
566
+ return section
567
+
568
+ def _nodes_for_ifcond(self, ifcond, with_if=True):
569
+ """Return list of Text, literal nodes for the ifcond
570
+
571
+ Return a list which gives text like ' (If: condition)'.
572
+ If with_if is False, we don't return the "(If: " and ")".
573
+ """
574
+
575
+ doc = ifcond.docgen()
576
+ if not doc:
577
+ return []
578
+ doc = nodes.literal('', doc)
579
+ if not with_if:
580
+ return [doc]
581
+
582
+ nodelist = [nodes.Text(' ('), nodes.strong('', 'If: ')]
583
+ nodelist.append(doc)
584
+ nodelist.append(nodes.Text(')'))
585
+ return nodelist
586
+
587
+ def _nodes_for_one_member(self, member):
588
+ """Return list of Text, literal nodes for this member
589
+
590
+ Return a list of doctree nodes which give text like
591
+ 'name: type (optional) (If: ...)' suitable for use as the
592
+ 'term' part of a definition list item.
593
+ """
594
+ term = [nodes.literal('', member.name)]
595
+ if member.type.doc_type():
596
+ term.append(nodes.Text(': '))
597
+ term.append(nodes.literal('', member.type.doc_type()))
598
+ if member.optional:
599
+ term.append(nodes.Text(' (optional)'))
600
+ if member.ifcond.is_present():
601
+ term.extend(self._nodes_for_ifcond(member.ifcond))
602
+ return term
603
+
604
+ def _nodes_for_variant_when(self, branches, variant):
605
+ """Return list of Text, literal nodes for variant 'when' clause
606
+
607
+ Return a list of doctree nodes which give text like
608
+ 'when tagname is variant (If: ...)' suitable for use in
609
+ the 'branches' part of a definition list.
610
+ """
611
+ term = [nodes.Text(' when '),
612
+ nodes.literal('', branches.tag_member.name),
613
+ nodes.Text(' is '),
614
+ nodes.literal('', '"%s"' % variant.name)]
615
+ if variant.ifcond.is_present():
616
+ term.extend(self._nodes_for_ifcond(variant.ifcond))
617
+ return term
618
+
619
+ def _nodes_for_members(self, doc, what, base=None, branches=None):
620
+ """Return list of doctree nodes for the table of members"""
621
+ dlnode = nodes.definition_list()
622
+ for section in doc.args.values():
623
+ term = self._nodes_for_one_member(section.member)
624
+ # TODO drop fallbacks when undocumented members are outlawed
625
+ if section.text:
626
+ defn = dedent(section.text)
627
+ else:
628
+ defn = [nodes.Text('Not documented')]
629
+
630
+ dlnode += self._make_dlitem(term, defn)
631
+
632
+ if base:
633
+ dlnode += self._make_dlitem([nodes.Text('The members of '),
634
+ nodes.literal('', base.doc_type())],
635
+ None)
636
+
637
+ if branches:
638
+ for v in branches.variants:
639
+ if v.type.name == 'q_empty':
640
+ continue
641
+ assert not v.type.is_implicit()
642
+ term = [nodes.Text('The members of '),
643
+ nodes.literal('', v.type.doc_type())]
644
+ term.extend(self._nodes_for_variant_when(branches, v))
645
+ dlnode += self._make_dlitem(term, None)
646
+
647
+ if not dlnode.children:
648
+ return []
649
+
650
+ section = self._make_section(what)
651
+ section += dlnode
652
+ return [section]
653
+
654
+ def _nodes_for_enum_values(self, doc):
655
+ """Return list of doctree nodes for the table of enum values"""
656
+ seen_item = False
657
+ dlnode = nodes.definition_list()
658
+ for section in doc.args.values():
659
+ termtext = [nodes.literal('', section.member.name)]
660
+ if section.member.ifcond.is_present():
661
+ termtext.extend(self._nodes_for_ifcond(section.member.ifcond))
662
+ # TODO drop fallbacks when undocumented members are outlawed
663
+ if section.text:
664
+ defn = dedent(section.text)
665
+ else:
666
+ defn = [nodes.Text('Not documented')]
667
+
668
+ dlnode += self._make_dlitem(termtext, defn)
669
+ seen_item = True
670
+
671
+ if not seen_item:
672
+ return []
673
+
674
+ section = self._make_section('Values')
675
+ section += dlnode
676
+ return [section]
677
+
678
+ def _nodes_for_arguments(self, doc, arg_type):
679
+ """Return list of doctree nodes for the arguments section"""
680
+ if arg_type and not arg_type.is_implicit():
681
+ assert not doc.args
682
+ section = self._make_section('Arguments')
683
+ dlnode = nodes.definition_list()
684
+ dlnode += self._make_dlitem(
685
+ [nodes.Text('The members of '),
686
+ nodes.literal('', arg_type.name)],
687
+ None)
688
+ section += dlnode
689
+ return [section]
690
+
691
+ return self._nodes_for_members(doc, 'Arguments')
692
+
693
+ def _nodes_for_features(self, doc):
694
+ """Return list of doctree nodes for the table of features"""
695
+ seen_item = False
696
+ dlnode = nodes.definition_list()
697
+ for section in doc.features.values():
698
+ dlnode += self._make_dlitem(
699
+ [nodes.literal('', section.member.name)], dedent(section.text))
700
+ seen_item = True
701
+
702
+ if not seen_item:
703
+ return []
704
+
705
+ section = self._make_section('Features')
706
+ section += dlnode
707
+ return [section]
708
+
709
+ def _nodes_for_sections(self, doc):
710
+ """Return list of doctree nodes for additional sections"""
711
+ nodelist = []
712
+ for section in doc.sections:
713
+ if section.kind == QAPIDoc.Kind.TODO:
714
+ # Hide TODO: sections
715
+ continue
716
+
717
+ if section.kind == QAPIDoc.Kind.PLAIN:
718
+ # Sphinx cannot handle sectionless titles;
719
+ # Instead, just append the results to the prior section.
720
+ container = nodes.container()
721
+ self._parse_text_into_node(section.text, container)
722
+ nodelist += container.children
723
+ continue
724
+
725
+ snode = self._make_section(section.kind.name.title())
726
+ self._parse_text_into_node(dedent(section.text), snode)
727
+ nodelist.append(snode)
728
+ return nodelist
729
+
730
+ def _nodes_for_if_section(self, ifcond):
731
+ """Return list of doctree nodes for the "If" section"""
732
+ nodelist = []
733
+ if ifcond.is_present():
734
+ snode = self._make_section('If')
735
+ snode += nodes.paragraph(
736
+ '', '', *self._nodes_for_ifcond(ifcond, with_if=False)
737
+ )
738
+ nodelist.append(snode)
739
+ return nodelist
740
+
741
+ def _add_doc(self, typ, sections):
742
+ """Add documentation for a command/object/enum...
743
+
744
+ We assume we're documenting the thing defined in self._cur_doc.
745
+ typ is the type of thing being added ("Command", "Object", etc)
746
+
747
+ sections is a list of nodes for sections to add to the definition.
748
+ """
749
+
750
+ doc = self._cur_doc
751
+ snode = nodes.section(ids=[self._sphinx_directive.new_serialno()])
752
+ snode += nodes.title('', '', *[nodes.literal(doc.symbol, doc.symbol),
753
+ nodes.Text(' (' + typ + ')')])
754
+ self._parse_text_into_node(doc.body.text, snode)
755
+ for s in sections:
756
+ if s is not None:
757
+ snode += s
758
+ self._add_node_to_current_heading(snode)
759
+
760
+ def visit_enum_type(self, name, info, ifcond, features, members, prefix):
761
+ doc = self._cur_doc
762
+ self._add_doc('Enum',
763
+ self._nodes_for_enum_values(doc)
764
+ + self._nodes_for_features(doc)
765
+ + self._nodes_for_sections(doc)
766
+ + self._nodes_for_if_section(ifcond))
767
+
768
+ def visit_object_type(self, name, info, ifcond, features,
769
+ base, members, branches):
770
+ doc = self._cur_doc
771
+ if base and base.is_implicit():
772
+ base = None
773
+ self._add_doc('Object',
774
+ self._nodes_for_members(doc, 'Members', base, branches)
775
+ + self._nodes_for_features(doc)
776
+ + self._nodes_for_sections(doc)
777
+ + self._nodes_for_if_section(ifcond))
778
+
779
+ def visit_alternate_type(self, name, info, ifcond, features,
780
+ alternatives):
781
+ doc = self._cur_doc
782
+ self._add_doc('Alternate',
783
+ self._nodes_for_members(doc, 'Members')
784
+ + self._nodes_for_features(doc)
785
+ + self._nodes_for_sections(doc)
786
+ + self._nodes_for_if_section(ifcond))
787
+
788
+ def visit_command(self, name, info, ifcond, features, arg_type,
789
+ ret_type, gen, success_response, boxed, allow_oob,
790
+ allow_preconfig, coroutine):
791
+ doc = self._cur_doc
792
+ self._add_doc('Command',
793
+ self._nodes_for_arguments(doc, arg_type)
794
+ + self._nodes_for_features(doc)
795
+ + self._nodes_for_sections(doc)
796
+ + self._nodes_for_if_section(ifcond))
797
+
798
+ def visit_event(self, name, info, ifcond, features, arg_type, boxed):
799
+ doc = self._cur_doc
800
+ self._add_doc('Event',
801
+ self._nodes_for_arguments(doc, arg_type)
802
+ + self._nodes_for_features(doc)
803
+ + self._nodes_for_sections(doc)
804
+ + self._nodes_for_if_section(ifcond))
805
+
806
+ def symbol(self, doc, entity):
807
+ """Add documentation for one symbol to the document tree
808
+
809
+ This is the main entry point which causes us to add documentation
810
+ nodes for a symbol (which could be a 'command', 'object', 'event',
811
+ etc). We do this by calling 'visit' on the schema entity, which
812
+ will then call back into one of our visit_* methods, depending
813
+ on what kind of thing this symbol is.
814
+ """
815
+ self._cur_doc = doc
816
+ entity.visit(self)
817
+ self._cur_doc = None
818
+
819
+ def _start_new_heading(self, heading, level):
820
+ """Start a new heading at the specified heading level
821
+
822
+ Create a new section whose title is 'heading' and which is placed
823
+ in the docutils node tree as a child of the most recent level-1
824
+ heading. Subsequent document sections (commands, freeform doc chunks,
825
+ etc) will be placed as children of this new heading section.
826
+ """
827
+ if len(self._active_headings) < level:
828
+ raise QAPISemError(self._cur_doc.info,
829
+ 'Level %d subheading found outside a '
830
+ 'level %d heading'
831
+ % (level, level - 1))
832
+ snode = self._make_section(heading)
833
+ self._active_headings[level - 1] += snode
834
+ self._active_headings = self._active_headings[:level]
835
+ self._active_headings.append(snode)
836
+ return snode
837
+
838
+ def _add_node_to_current_heading(self, node):
839
+ """Add the node to whatever the current active heading is"""
840
+ self._active_headings[-1] += node
841
+
842
+ def freeform(self, doc):
843
+ """Add a piece of 'freeform' documentation to the document tree
844
+
845
+ A 'freeform' document chunk doesn't relate to any particular
846
+ symbol (for instance, it could be an introduction).
847
+
848
+ If the freeform document starts with a line of the form
849
+ '= Heading text', this is a section or subsection heading, with
850
+ the heading level indicated by the number of '=' signs.
851
+ """
852
+
853
+ # QAPIDoc documentation says free-form documentation blocks
854
+ # must have only a body section, nothing else.
855
+ assert not doc.sections
856
+ assert not doc.args
857
+ assert not doc.features
858
+ self._cur_doc = doc
859
+
860
+ text = doc.body.text
861
+ if re.match(r'=+ ', text):
862
+ # Section/subsection heading (if present, will always be
863
+ # the first line of the block)
864
+ (heading, _, text) = text.partition('\n')
865
+ (leader, _, heading) = heading.partition(' ')
866
+ node = self._start_new_heading(heading, len(leader))
867
+ if text == '':
868
+ return
869
+ else:
870
+ node = nodes.container()
871
+
872
+ self._parse_text_into_node(text, node)
873
+ self._cur_doc = None
874
+
875
+ def _parse_text_into_node(self, doctext, node):
876
+ """Parse a chunk of QAPI-doc-format text into the node
877
+
878
+ The doc comment can contain most inline rST markup, including
879
+ bulleted and enumerated lists.
880
+ As an extra permitted piece of markup, @var will be turned
881
+ into ``var``.
882
+ """
883
+
884
+ # Handle the "@var means ``var`` case
885
+ doctext = re.sub(r'@([\w-]+)', r'``\1``', doctext)
886
+
887
+ rstlist = ViewList()
888
+ for line in doctext.splitlines():
889
+ # The reported line number will always be that of the start line
890
+ # of the doc comment, rather than the actual location of the error.
891
+ # Being more precise would require overhaul of the QAPIDoc class
892
+ # to track lines more exactly within all the sub-parts of the doc
893
+ # comment, as well as counting lines here.
894
+ rstlist.append(line, self._cur_doc.info.fname,
895
+ self._cur_doc.info.line)
896
+ # Append a blank line -- in some cases rST syntax errors get
897
+ # attributed to the line after one with actual text, and if there
898
+ # isn't anything in the ViewList corresponding to that then Sphinx
899
+ # 1.6's AutodocReporter will then misidentify the source/line location
900
+ # in the error message (usually attributing it to the top-level
901
+ # .rst file rather than the offending .json file). The extra blank
902
+ # line won't affect the rendered output.
903
+ rstlist.append("", self._cur_doc.info.fname, self._cur_doc.info.line)
904
+ self._sphinx_directive.do_parse(rstlist, node)
905
+
906
+ def get_document_node(self):
907
+ """Return the root docutils node which makes up the document"""
908
+ return self._top_node
909
--
910
2.48.1
diff view generated by jsdifflib
New patch
1
Now that the legacy code is factored out, fix up the typing on the
2
remaining code in qapidoc.py. Add a type ignore to qapi_legacy.py to
3
prevent the errors there from bleeding out into qapidoc.py.
1
4
5
Signed-off-by: John Snow <jsnow@redhat.com>
6
---
7
docs/sphinx/qapidoc.py | 40 ++++++++++++++++++++++-------------
8
docs/sphinx/qapidoc_legacy.py | 1 +
9
2 files changed, 26 insertions(+), 15 deletions(-)
10
11
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
12
index XXXXXXX..XXXXXXX 100644
13
--- a/docs/sphinx/qapidoc.py
14
+++ b/docs/sphinx/qapidoc.py
15
@@ -XXX,XX +XXX,XX @@
16
https://www.sphinx-doc.org/en/master/development/index.html
17
"""
18
19
+from __future__ import annotations
20
+
21
import os
22
import sys
23
-from typing import List
24
+from typing import TYPE_CHECKING
25
26
from docutils import nodes
27
from docutils.parsers.rst import Directive, directives
28
from qapi.error import QAPIError
29
-from qapi.gen import QAPISchemaVisitor
30
-from qapi.schema import QAPISchema
31
+from qapi.schema import QAPISchema, QAPISchemaVisitor
32
33
-from qapidoc_legacy import QAPISchemaGenRSTVisitor
34
+from qapidoc_legacy import QAPISchemaGenRSTVisitor # type: ignore
35
from sphinx import addnodes
36
from sphinx.directives.code import CodeBlock
37
from sphinx.errors import ExtensionError
38
@@ -XXX,XX +XXX,XX @@
39
from sphinx.util.nodes import nested_parse_with_titles
40
41
42
+if TYPE_CHECKING:
43
+ from typing import Any, List, Sequence
44
+
45
+ from docutils.statemachine import StringList
46
+
47
+ from sphinx.application import Sphinx
48
+ from sphinx.util.typing import ExtensionMetadata
49
+
50
+
51
__version__ = "1.0"
52
53
54
@@ -XXX,XX +XXX,XX @@ class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
55
schema file associated with each module in the QAPI input.
56
"""
57
58
- def __init__(self, env, qapidir):
59
+ def __init__(self, env: Any, qapidir: str) -> None:
60
self._env = env
61
self._qapidir = qapidir
62
63
- def visit_module(self, name):
64
+ def visit_module(self, name: str) -> None:
65
if name != "./builtin":
66
qapifile = self._qapidir + "/" + name
67
self._env.note_dependency(os.path.abspath(qapifile))
68
@@ -XXX,XX +XXX,XX @@ def visit_module(self, name):
69
70
71
class NestedDirective(Directive):
72
- def run(self):
73
+ def run(self) -> Sequence[nodes.Node]:
74
raise NotImplementedError
75
76
- def do_parse(self, rstlist, node):
77
+ def do_parse(self, rstlist: StringList, node: nodes.Node) -> None:
78
"""
79
Parse rST source lines and add them to the specified node
80
81
@@ -XXX,XX +XXX,XX @@ class QAPIDocDirective(NestedDirective):
82
}
83
has_content = False
84
85
- def new_serialno(self):
86
+ def new_serialno(self) -> str:
87
"""Return a unique new ID string suitable for use as a node's ID"""
88
env = self.state.document.settings.env
89
return "qapidoc-%d" % env.new_serialno("qapidoc")
90
91
- def transmogrify(self, schema) -> nodes.Element:
92
+ def transmogrify(self, schema: QAPISchema) -> nodes.Element:
93
raise NotImplementedError
94
95
- def legacy(self, schema) -> nodes.Element:
96
+ def legacy(self, schema: QAPISchema) -> nodes.Element:
97
vis = QAPISchemaGenRSTVisitor(self)
98
vis.visit_begin(schema)
99
for doc in schema.docs:
100
@@ -XXX,XX +XXX,XX @@ def legacy(self, schema) -> nodes.Element:
101
vis.symbol(doc, schema.lookup_entity(doc.symbol))
102
else:
103
vis.freeform(doc)
104
- return vis.get_document_node()
105
+ return vis.get_document_node() # type: ignore
106
107
- def run(self):
108
+ def run(self) -> Sequence[nodes.Node]:
109
env = self.state.document.settings.env
110
qapifile = env.config.qapidoc_srctree + "/" + self.arguments[0]
111
qapidir = os.path.dirname(qapifile)
112
@@ -XXX,XX +XXX,XX @@ def _highlightlang(self) -> addnodes.highlightlang:
113
)
114
return node
115
116
- def admonition_wrap(self, *content) -> List[nodes.Node]:
117
+ def admonition_wrap(self, *content: nodes.Node) -> List[nodes.Node]:
118
title = "Example:"
119
if "title" in self.options:
120
title = f"{title} {self.options['title']}"
121
@@ -XXX,XX +XXX,XX @@ def run(self) -> List[nodes.Node]:
122
return self.admonition_wrap(*content_nodes)
123
124
125
-def setup(app):
126
+def setup(app: Sphinx) -> ExtensionMetadata:
127
"""Register qapi-doc directive with Sphinx"""
128
app.add_config_value("qapidoc_srctree", None, "env")
129
app.add_directive("qapi-doc", QAPIDocDirective)
130
diff --git a/docs/sphinx/qapidoc_legacy.py b/docs/sphinx/qapidoc_legacy.py
131
index XXXXXXX..XXXXXXX 100644
132
--- a/docs/sphinx/qapidoc_legacy.py
133
+++ b/docs/sphinx/qapidoc_legacy.py
134
@@ -XXX,XX +XXX,XX @@
135
# coding=utf-8
136
+# type: ignore
137
#
138
# QEMU qapidoc QAPI file parsing extension
139
#
140
--
141
2.48.1
diff view generated by jsdifflib
New patch
1
Add strict typing to qapidoc.py for the remainder of this series.
1
2
3
Signed-off-by: John Snow <jsnow@redhat.com>
4
---
5
scripts/qapi-lint.sh | 2 +-
6
1 file changed, 1 insertion(+), 1 deletion(-)
7
8
diff --git a/scripts/qapi-lint.sh b/scripts/qapi-lint.sh
9
index XXXXXXX..XXXXXXX 100755
10
--- a/scripts/qapi-lint.sh
11
+++ b/scripts/qapi-lint.sh
12
@@ -XXX,XX +XXX,XX @@ if [[ -f ../docs/sphinx/qapi_domain.py ]]; then
13
pushd ../docs/sphinx
14
15
set -x
16
- mypy --strict $files
17
+ PYTHONPATH=../../scripts/ mypy --follow-untyped-imports --strict $files qapidoc.py
18
flake8 --max-line-length=80 $files qapidoc.py
19
isort -c $files qapidoc.py
20
black --line-length 80 --check $files qapidoc.py
21
--
22
2.48.1
diff view generated by jsdifflib
New patch
1
Add the beginnings of the Transmogrifier class by adding the rST
2
conversion helpers that will be used to build the virtual rST document.
1
3
4
This version of the class does not actually "do anything" yet; each
5
individual feature is added one-at-a-time in the forthcoming commits.
6
7
Signed-off-by: John Snow <jsnow@redhat.com>
8
---
9
docs/sphinx/qapidoc.py | 73 ++++++++++++++++++++++++++++++++++++++++--
10
1 file changed, 70 insertions(+), 3 deletions(-)
11
12
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
13
index XXXXXXX..XXXXXXX 100644
14
--- a/docs/sphinx/qapidoc.py
15
+++ b/docs/sphinx/qapidoc.py
16
@@ -XXX,XX +XXX,XX @@
17
18
from __future__ import annotations
19
20
+from contextlib import contextmanager
21
import os
22
import sys
23
from typing import TYPE_CHECKING
24
25
from docutils import nodes
26
from docutils.parsers.rst import Directive, directives
27
+from docutils.statemachine import StringList
28
from qapi.error import QAPIError
29
from qapi.schema import QAPISchema, QAPISchemaVisitor
30
+from qapi.source import QAPISourceInfo
31
32
from qapidoc_legacy import QAPISchemaGenRSTVisitor # type: ignore
33
from sphinx import addnodes
34
@@ -XXX,XX +XXX,XX @@
35
36
37
if TYPE_CHECKING:
38
- from typing import Any, List, Sequence
39
-
40
- from docutils.statemachine import StringList
41
+ from typing import (
42
+ Any,
43
+ Generator,
44
+ List,
45
+ Sequence,
46
+ )
47
48
from sphinx.application import Sphinx
49
from sphinx.util.typing import ExtensionMetadata
50
@@ -XXX,XX +XXX,XX @@
51
__version__ = "1.0"
52
53
54
+class Transmogrifier:
55
+ def __init__(self) -> None:
56
+ self._result = StringList()
57
+ self.indent = 0
58
+
59
+ # General-purpose rST generation functions
60
+
61
+ def get_indent(self) -> str:
62
+ return " " * self.indent
63
+
64
+ @contextmanager
65
+ def indented(self) -> Generator[None]:
66
+ self.indent += 1
67
+ try:
68
+ yield
69
+ finally:
70
+ self.indent -= 1
71
+
72
+ def add_line_raw(self, line: str, source: str, *lineno: int) -> None:
73
+ """Append one line of generated reST to the output."""
74
+
75
+ # NB: Sphinx uses zero-indexed lines; subtract one.
76
+ lineno = tuple((n - 1 for n in lineno))
77
+
78
+ if line.strip():
79
+ # not a blank line
80
+ self._result.append(
81
+ self.get_indent() + line.rstrip("\n"), source, *lineno
82
+ )
83
+ else:
84
+ self._result.append("", source, *lineno)
85
+
86
+ def add_line(self, content: str, info: QAPISourceInfo) -> None:
87
+ # NB: We *require* an info object; this works out OK because we
88
+ # don't document built-in objects that don't have
89
+ # one. Everything else should.
90
+ self.add_line_raw(content, info.fname, info.line)
91
+
92
+ def add_lines(
93
+ self,
94
+ content: str,
95
+ info: QAPISourceInfo,
96
+ ) -> None:
97
+ lines = content.splitlines(True)
98
+ for i, line in enumerate(lines):
99
+ self.add_line_raw(line, info.fname, info.line + i)
100
+
101
+ def ensure_blank_line(self) -> None:
102
+ # Empty document -- no blank line required.
103
+ if not self._result:
104
+ return
105
+
106
+ # Last line isn't blank, add one.
107
+ if self._result[-1].strip(): # pylint: disable=no-member
108
+ fname, line = self._result.info(-1)
109
+ assert isinstance(line, int)
110
+ # New blank line is credited to one-after the current last line.
111
+ # +2: correct for zero/one index, then increment by one.
112
+ self.add_line_raw("", fname, line + 2)
113
+
114
+
115
class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
116
"""A QAPI schema visitor which adds Sphinx dependencies each module
117
118
--
119
2.48.1
diff view generated by jsdifflib
New patch
1
This method annotates the start of a new module, crediting the source
2
location to the first line of the module file.
1
3
4
Signed-off-by: John Snow <jsnow@redhat.com>
5
---
6
docs/sphinx/qapidoc.py | 9 +++++++++
7
1 file changed, 9 insertions(+)
8
9
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
10
index XXXXXXX..XXXXXXX 100644
11
--- a/docs/sphinx/qapidoc.py
12
+++ b/docs/sphinx/qapidoc.py
13
@@ -XXX,XX +XXX,XX @@
14
15
from contextlib import contextmanager
16
import os
17
+from pathlib import Path
18
import sys
19
from typing import TYPE_CHECKING
20
21
@@ -XXX,XX +XXX,XX @@ def ensure_blank_line(self) -> None:
22
# +2: correct for zero/one index, then increment by one.
23
self.add_line_raw("", fname, line + 2)
24
25
+ # Transmogrification core methods
26
+
27
+ def visit_module(self, path: str) -> None:
28
+ name = Path(path).stem
29
+ # module directives are credited to the first line of a module file.
30
+ self.add_line_raw(f".. qapi:module:: {name}", path, 1)
31
+ self.ensure_blank_line()
32
+
33
34
class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
35
"""A QAPI schema visitor which adds Sphinx dependencies each module
36
--
37
2.48.1
diff view generated by jsdifflib
New patch
1
This is for the sake of the new rST generator (the "transmogrifier") so
2
we can advance multiple lines on occasion while keeping the
3
generated<-->source mappings accurate.
1
4
5
next_line now simply takes an optional n parameter which chooses the
6
number of lines to advance.
7
8
RFC: Here's the exorbitant detail on why I want this:
9
10
This is used mainly when converting section syntax in free-form
11
documentation to more traditional rST section header syntax, which
12
does not always line up 1:1 for line counts.
13
14
For example:
15
16
```
17
##
18
# = Section <-- Info is pointing here, "L1"
19
#
20
# Lorem Ipsum
21
##
22
```
23
24
would be transformed to rST as:
25
26
```
27
======= <-- L1
28
Section <-- L1
29
======= <-- L1
30
<-- L2
31
Lorem Ipsum <-- L3
32
```
33
34
After consuming the single "Section" line from the source, we want to
35
advance the source pointer to the next non-empty line which requires
36
jumping by more than one line.
37
38
Signed-off-by: John Snow <jsnow@redhat.com>
39
Reviewed-by: Markus Armbruster <armbru@redhat.com>
40
---
41
scripts/qapi/source.py | 4 ++--
42
1 file changed, 2 insertions(+), 2 deletions(-)
43
44
diff --git a/scripts/qapi/source.py b/scripts/qapi/source.py
45
index XXXXXXX..XXXXXXX 100644
46
--- a/scripts/qapi/source.py
47
+++ b/scripts/qapi/source.py
48
@@ -XXX,XX +XXX,XX @@ def set_defn(self, meta: str, name: str) -> None:
49
self.defn_meta = meta
50
self.defn_name = name
51
52
- def next_line(self: T) -> T:
53
+ def next_line(self: T, n: int = 1) -> T:
54
info = copy.copy(self)
55
- info.line += 1
56
+ info.line += n
57
return info
58
59
def loc(self) -> str:
60
--
61
2.48.1
diff view generated by jsdifflib
New patch
1
Signed-off-by: John Snow <jsnow@redhat.com>
2
---
3
docs/sphinx/qapidoc.py | 50 ++++++++++++++++++++++++++++++++++++++++++
4
1 file changed, 50 insertions(+)
1
5
6
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
7
index XXXXXXX..XXXXXXX 100644
8
--- a/docs/sphinx/qapidoc.py
9
+++ b/docs/sphinx/qapidoc.py
10
@@ -XXX,XX +XXX,XX @@
11
from contextlib import contextmanager
12
import os
13
from pathlib import Path
14
+import re
15
import sys
16
from typing import TYPE_CHECKING
17
18
@@ -XXX,XX +XXX,XX @@
19
Sequence,
20
)
21
22
+ from qapi.parser import QAPIDoc
23
+
24
from sphinx.application import Sphinx
25
from sphinx.util.typing import ExtensionMetadata
26
27
@@ -XXX,XX +XXX,XX @@ def visit_module(self, path: str) -> None:
28
self.add_line_raw(f".. qapi:module:: {name}", path, 1)
29
self.ensure_blank_line()
30
31
+ def visit_freeform(self, doc: QAPIDoc) -> None:
32
+ # TODO: Once the old qapidoc transformer is deprecated, freeform
33
+ # sections can be updated to pure rST, and this transformed removed.
34
+ #
35
+ # For now, translate our micro-format into rST. Code adapted
36
+ # from Peter Maydell's freeform().
37
+
38
+ assert len(doc.all_sections) == 1, doc.all_sections
39
+ body = doc.all_sections[0]
40
+ text = body.text
41
+ info = doc.info
42
+
43
+ if re.match(r"=+ ", text):
44
+ # Section/subsection heading (if present, will always be the
45
+ # first line of the block)
46
+ (heading, _, text) = text.partition("\n")
47
+ (leader, _, heading) = heading.partition(" ")
48
+ level = len(leader) + 1 # Implicit +1 for heading in .rST stub
49
+
50
+ # https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#sections
51
+ markers = {
52
+ 1: "#",
53
+ 2: "*",
54
+ 3: "=",
55
+ 4: "-",
56
+ 5: "^",
57
+ 6: '"',
58
+ }
59
+ overline = level <= 2
60
+ marker = markers[level]
61
+
62
+ self.ensure_blank_line()
63
+ # This credits all 2 or 3 lines to the single source line.
64
+ if overline:
65
+ self.add_line(marker * len(heading), info)
66
+ self.add_line(heading, info)
67
+ self.add_line(marker * len(heading), info)
68
+ self.ensure_blank_line()
69
+
70
+ # Eat blank line(s) and advance info
71
+ trimmed = text.lstrip("\n")
72
+ text = trimmed
73
+ info = info.next_line(len(text) - len(trimmed) + 1)
74
+
75
+ self.add_lines(text, info)
76
+ self.ensure_blank_line()
77
+
78
79
class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
80
"""A QAPI schema visitor which adds Sphinx dependencies each module
81
--
82
2.48.1
diff view generated by jsdifflib
New patch
1
This method adds the options/preamble to each definition block. Notably,
2
:since: and :ifcond: are added, as are any "special features" such as
3
:deprecated: and :unstable:.
1
4
5
Signed-off-by: John Snow <jsnow@redhat.com>
6
---
7
docs/sphinx/qapidoc.py | 41 ++++++++++++++++++++++++++++++++++++++---
8
1 file changed, 38 insertions(+), 3 deletions(-)
9
10
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
11
index XXXXXXX..XXXXXXX 100644
12
--- a/docs/sphinx/qapidoc.py
13
+++ b/docs/sphinx/qapidoc.py
14
@@ -XXX,XX +XXX,XX @@
15
from docutils.parsers.rst import Directive, directives
16
from docutils.statemachine import StringList
17
from qapi.error import QAPIError
18
-from qapi.schema import QAPISchema, QAPISchemaVisitor
19
+from qapi.parser import QAPIDoc
20
+from qapi.schema import (
21
+ QAPISchema,
22
+ QAPISchemaDefinition,
23
+ QAPISchemaVisitor,
24
+)
25
from qapi.source import QAPISourceInfo
26
27
from qapidoc_legacy import QAPISchemaGenRSTVisitor # type: ignore
28
@@ -XXX,XX +XXX,XX @@
29
Sequence,
30
)
31
32
- from qapi.parser import QAPIDoc
33
-
34
from sphinx.application import Sphinx
35
from sphinx.util.typing import ExtensionMetadata
36
37
@@ -XXX,XX +XXX,XX @@ def ensure_blank_line(self) -> None:
38
# +2: correct for zero/one index, then increment by one.
39
self.add_line_raw("", fname, line + 2)
40
41
+ # Transmogrification helpers
42
+
43
+ def preamble(self, ent: QAPISchemaDefinition) -> None:
44
+ """
45
+ Generate option lines for qapi entity directives.
46
+ """
47
+ if ent.doc and ent.doc.since:
48
+ assert ent.doc.since.kind == QAPIDoc.Kind.SINCE
49
+ # Generated from the entity's docblock; info location is exact.
50
+ self.add_line(f":since: {ent.doc.since.text}", ent.doc.since.info)
51
+
52
+ if ent.ifcond.is_present():
53
+ doc = ent.ifcond.docgen()
54
+ assert ent.info
55
+ # Generated from entity definition; info location is approximate.
56
+ self.add_line(f":ifcond: {doc}", ent.info)
57
+
58
+ # Hoist special features such as :deprecated: and :unstable:
59
+ # into the options block for the entity. If, in the future, new
60
+ # special features are added, qapi-domain will chirp about
61
+ # unrecognized options and fail until they are handled in
62
+ # qapi-domain.
63
+ for feat in ent.features:
64
+ if feat.is_special():
65
+ # FIXME: handle ifcond if present. How to display that
66
+ # information is TBD.
67
+ # Generated from entity def; info location is approximate.
68
+ assert feat.info
69
+ self.add_line(f":{feat.name}:", feat.info)
70
+
71
+ self.ensure_blank_line()
72
+
73
# Transmogrification core methods
74
75
def visit_module(self, path: str) -> None:
76
--
77
2.48.1
diff view generated by jsdifflib
New patch
1
This transforms "formerly known as untagged sections" into our pure
2
intermediate rST format. These sections are already pure rST, so this
3
method doesn't do a whole lot except ensure appropriate newlines.
1
4
5
Signed-off-by: John Snow <jsnow@redhat.com>
6
---
7
docs/sphinx/qapidoc.py | 9 +++++++++
8
1 file changed, 9 insertions(+)
9
10
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
11
index XXXXXXX..XXXXXXX 100644
12
--- a/docs/sphinx/qapidoc.py
13
+++ b/docs/sphinx/qapidoc.py
14
@@ -XXX,XX +XXX,XX @@ def ensure_blank_line(self) -> None:
15
16
# Transmogrification helpers
17
18
+ def visit_paragraph(self, section: QAPIDoc.Section) -> None:
19
+ # Squelch empty paragraphs.
20
+ if not section.text:
21
+ return
22
+
23
+ self.ensure_blank_line()
24
+ self.add_lines(section.text, section.info)
25
+ self.ensure_blank_line()
26
+
27
def preamble(self, ent: QAPISchemaDefinition) -> None:
28
"""
29
Generate option lines for qapi entity directives.
30
--
31
2.48.1
diff view generated by jsdifflib
New patch
1
Notably, this method does not currently address the formatting issues
2
present with the "errors" section in QAPIDoc and just vomits the text
3
verbatim into the rST doc, with somewhat inconsistent results.
1
4
5
To be addressed in a future revision.
6
7
Signed-off-by: John Snow <jsnow@redhat.com>
8
---
9
docs/sphinx/qapidoc.py | 6 ++++++
10
1 file changed, 6 insertions(+)
11
12
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
13
index XXXXXXX..XXXXXXX 100644
14
--- a/docs/sphinx/qapidoc.py
15
+++ b/docs/sphinx/qapidoc.py
16
@@ -XXX,XX +XXX,XX @@ def visit_paragraph(self, section: QAPIDoc.Section) -> None:
17
self.add_lines(section.text, section.info)
18
self.ensure_blank_line()
19
20
+ def visit_errors(self, section: QAPIDoc.Section) -> None:
21
+ # FIXME: the formatting for errors may be inconsistent and may
22
+ # or may not require different newline placement to ensure
23
+ # proper rendering as a nested list.
24
+ self.add_lines(f":error:\n{section.text}", section.info)
25
+
26
def preamble(self, ent: QAPISchemaDefinition) -> None:
27
"""
28
Generate option lines for qapi entity directives.
29
--
30
2.48.1
diff view generated by jsdifflib
New patch
1
This method is responsible for generating a type name for a given member
2
with the correct annotations for the QAPI domain. Features and enums do
3
not *have* types, so they return None. Everything else returns the type
4
name with a "?" suffix if that type is optional, and ensconced in
5
[brackets] if it's an array type.
1
6
7
Signed-off-by: John Snow <jsnow@redhat.com>
8
---
9
docs/sphinx/qapidoc.py | 32 ++++++++++++++++++++++++++++++++
10
1 file changed, 32 insertions(+)
11
12
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
13
index XXXXXXX..XXXXXXX 100644
14
--- a/docs/sphinx/qapidoc.py
15
+++ b/docs/sphinx/qapidoc.py
16
@@ -XXX,XX +XXX,XX @@
17
from qapi.parser import QAPIDoc
18
from qapi.schema import (
19
QAPISchema,
20
+ QAPISchemaArrayType,
21
QAPISchemaDefinition,
22
+ QAPISchemaEnumMember,
23
+ QAPISchemaFeature,
24
+ QAPISchemaMember,
25
+ QAPISchemaObjectTypeMember,
26
+ QAPISchemaType,
27
QAPISchemaVisitor,
28
)
29
from qapi.source import QAPISourceInfo
30
@@ -XXX,XX +XXX,XX @@
31
Any,
32
Generator,
33
List,
34
+ Optional,
35
Sequence,
36
+ Union,
37
)
38
39
from sphinx.application import Sphinx
40
@@ -XXX,XX +XXX,XX @@ def ensure_blank_line(self) -> None:
41
# +2: correct for zero/one index, then increment by one.
42
self.add_line_raw("", fname, line + 2)
43
44
+ def format_type(
45
+ self, ent: Union[QAPISchemaDefinition | QAPISchemaMember]
46
+ ) -> Optional[str]:
47
+ if isinstance(ent, (QAPISchemaEnumMember, QAPISchemaFeature)):
48
+ return None
49
+
50
+ qapi_type = ent
51
+ optional = False
52
+ if isinstance(ent, QAPISchemaObjectTypeMember):
53
+ qapi_type = ent.type
54
+ optional = ent.optional
55
+
56
+ if isinstance(qapi_type, QAPISchemaArrayType):
57
+ ret = f"[{qapi_type.element_type.doc_type()}]"
58
+ else:
59
+ assert isinstance(qapi_type, QAPISchemaType)
60
+ tmp = qapi_type.doc_type()
61
+ assert tmp
62
+ ret = tmp
63
+ if optional:
64
+ ret += "?"
65
+
66
+ return ret
67
+
68
# Transmogrification helpers
69
70
def visit_paragraph(self, section: QAPIDoc.Section) -> None:
71
--
72
2.48.1
diff view generated by jsdifflib
New patch
1
These are simple rST generation methods that assist in getting the types
2
and formatting correct for a field list entry. add_field() is a more
3
raw, direct call while generate_field() is intended to be used for
4
generating the correct field from a member object.
1
5
6
Signed-off-by: John Snow <jsnow@redhat.com>
7
---
8
docs/sphinx/qapidoc.py | 24 ++++++++++++++++++++++++
9
1 file changed, 24 insertions(+)
10
11
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
12
index XXXXXXX..XXXXXXX 100644
13
--- a/docs/sphinx/qapidoc.py
14
+++ b/docs/sphinx/qapidoc.py
15
@@ -XXX,XX +XXX,XX @@ def ensure_blank_line(self) -> None:
16
# +2: correct for zero/one index, then increment by one.
17
self.add_line_raw("", fname, line + 2)
18
19
+ def add_field(
20
+ self,
21
+ kind: str,
22
+ name: str,
23
+ body: str,
24
+ info: QAPISourceInfo,
25
+ typ: Optional[str] = None,
26
+ ) -> None:
27
+ if typ:
28
+ text = f":{kind} {typ} {name}: {body}"
29
+ else:
30
+ text = f":{kind} {name}: {body}"
31
+ self.add_lines(text, info)
32
+
33
def format_type(
34
self, ent: Union[QAPISchemaDefinition | QAPISchemaMember]
35
) -> Optional[str]:
36
@@ -XXX,XX +XXX,XX @@ def format_type(
37
38
return ret
39
40
+ def generate_field(
41
+ self,
42
+ kind: str,
43
+ member: QAPISchemaMember,
44
+ body: str,
45
+ info: QAPISourceInfo,
46
+ ) -> None:
47
+ typ = self.format_type(member)
48
+ self.add_field(kind, member.name, body, info, typ)
49
+
50
# Transmogrification helpers
51
52
def visit_paragraph(self, section: QAPIDoc.Section) -> None:
53
--
54
2.48.1
diff view generated by jsdifflib
New patch
1
This adds a simple ":feat name: lorem ipsum ..." line to the generated
2
rST document, so at the moment it's only for "top level" features.
1
3
4
Signed-off-by: John Snow <jsnow@redhat.com>
5
---
6
docs/sphinx/qapidoc.py | 9 +++++++++
7
1 file changed, 9 insertions(+)
8
9
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
10
index XXXXXXX..XXXXXXX 100644
11
--- a/docs/sphinx/qapidoc.py
12
+++ b/docs/sphinx/qapidoc.py
13
@@ -XXX,XX +XXX,XX @@ def visit_paragraph(self, section: QAPIDoc.Section) -> None:
14
self.add_lines(section.text, section.info)
15
self.ensure_blank_line()
16
17
+ def visit_feature(self, section: QAPIDoc.ArgSection) -> None:
18
+ # FIXME - ifcond for features is not handled at all yet!
19
+ # Proposal: decorate the right-hand column with some graphical
20
+ # element to indicate conditional availability?
21
+ assert section.text # Guaranteed by parser.py
22
+ assert section.member
23
+
24
+ self.generate_field("feat", section.member, section.text, section.info)
25
+
26
def visit_errors(self, section: QAPIDoc.Section) -> None:
27
# FIXME: the formatting for errors may be inconsistent and may
28
# or may not require different newline placement to ensure
29
--
30
2.48.1
diff view generated by jsdifflib
New patch
1
Prepare to keep a record of which entity we're working on documenting
2
for the purposes of being able to change certain generative features
3
conditionally and create stronger assertions.
1
4
5
If you find yourself asking: "Wait, but where does the current entity
6
actually get recorded?!", you're right! That part comes with the
7
visit_entity() implementation, which gets added later.
8
9
This patch is front-loaded for the sake of type checking in the
10
forthcoming commits before visit_entity() is ready to be added.
11
12
Signed-off-by: John Snow <jsnow@redhat.com>
13
---
14
docs/sphinx/qapidoc.py | 6 ++++++
15
1 file changed, 6 insertions(+)
16
17
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
18
index XXXXXXX..XXXXXXX 100644
19
--- a/docs/sphinx/qapidoc.py
20
+++ b/docs/sphinx/qapidoc.py
21
@@ -XXX,XX +XXX,XX @@
22
23
class Transmogrifier:
24
def __init__(self) -> None:
25
+ self._curr_ent: Optional[QAPISchemaDefinition] = None
26
self._result = StringList()
27
self.indent = 0
28
29
+ @property
30
+ def entity(self) -> QAPISchemaDefinition:
31
+ assert self._curr_ent is not None
32
+ return self._curr_ent
33
+
34
# General-purpose rST generation functions
35
36
def get_indent(self) -> str:
37
--
38
2.48.1
diff view generated by jsdifflib
New patch
1
Generates :returns: fields for explicit returns statements. Note that
2
this does not presently handle undocumented returns, which is handled in
3
a later commit.
1
4
5
Signed-off-by: John Snow <jsnow@redhat.com>
6
---
7
docs/sphinx/qapidoc.py | 15 +++++++++++++++
8
1 file changed, 15 insertions(+)
9
10
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
11
index XXXXXXX..XXXXXXX 100644
12
--- a/docs/sphinx/qapidoc.py
13
+++ b/docs/sphinx/qapidoc.py
14
@@ -XXX,XX +XXX,XX @@
15
from qapi.schema import (
16
QAPISchema,
17
QAPISchemaArrayType,
18
+ QAPISchemaCommand,
19
QAPISchemaDefinition,
20
QAPISchemaEnumMember,
21
QAPISchemaFeature,
22
@@ -XXX,XX +XXX,XX @@ def visit_feature(self, section: QAPIDoc.ArgSection) -> None:
23
24
self.generate_field("feat", section.member, section.text, section.info)
25
26
+ def visit_returns(self, section: QAPIDoc.Section) -> None:
27
+ assert isinstance(self.entity, QAPISchemaCommand)
28
+ rtype = self.entity.ret_type
29
+ # q_empty can produce None, but we won't be documenting anything
30
+ # without an explicit return statement in the doc block, and we
31
+ # should not have any such explicit statements when there is no
32
+ # return value.
33
+ assert rtype
34
+
35
+ typ = self.format_type(rtype)
36
+ assert typ
37
+ assert section.text # We don't expect empty returns sections.
38
+ self.add_field("return", typ, section.text, section.info)
39
+
40
def visit_errors(self, section: QAPIDoc.Section) -> None:
41
# FIXME: the formatting for errors may be inconsistent and may
42
# or may not require different newline placement to ensure
43
--
44
2.48.1
diff view generated by jsdifflib
New patch
1
This method is used for generating the "members" of a wide variety of
2
things, including structs, unions, enums, alternates, etc. The field
3
name it uses to do so is dependent on the type of entity the "member"
4
belongs to.
1
5
6
Signed-off-by: John Snow <jsnow@redhat.com>
7
---
8
docs/sphinx/qapidoc.py | 27 +++++++++++++++++++++++++++
9
1 file changed, 27 insertions(+)
10
11
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
12
index XXXXXXX..XXXXXXX 100644
13
--- a/docs/sphinx/qapidoc.py
14
+++ b/docs/sphinx/qapidoc.py
15
@@ -XXX,XX +XXX,XX @@
16
17
18
class Transmogrifier:
19
+ # Field names used for different entity types:
20
+ field_types = {
21
+ "enum": "value",
22
+ "struct": "memb",
23
+ "union": "memb",
24
+ "event": "memb",
25
+ "command": "arg",
26
+ "alternate": "alt",
27
+ }
28
+
29
def __init__(self) -> None:
30
self._curr_ent: Optional[QAPISchemaDefinition] = None
31
self._result = StringList()
32
@@ -XXX,XX +XXX,XX @@ def entity(self) -> QAPISchemaDefinition:
33
assert self._curr_ent is not None
34
return self._curr_ent
35
36
+ @property
37
+ def member_field_type(self) -> str:
38
+ return self.field_types[self.entity.meta]
39
+
40
# General-purpose rST generation functions
41
42
def get_indent(self) -> str:
43
@@ -XXX,XX +XXX,XX @@ def visit_paragraph(self, section: QAPIDoc.Section) -> None:
44
self.add_lines(section.text, section.info)
45
self.ensure_blank_line()
46
47
+ def visit_member(self, section: QAPIDoc.ArgSection) -> None:
48
+ # TODO: ifcond for members
49
+ # TODO?: features for members (documented at entity-level,
50
+ # but sometimes defined per-member. Should we add such
51
+ # information to member descriptions when we can?)
52
+ assert section.text and section.member
53
+ self.generate_field(
54
+ self.member_field_type,
55
+ section.member,
56
+ section.text,
57
+ section.info,
58
+ )
59
+
60
def visit_feature(self, section: QAPIDoc.ArgSection) -> None:
61
# FIXME - ifcond for features is not handled at all yet!
62
# Proposal: decorate the right-hand column with some graphical
63
--
64
2.48.1
diff view generated by jsdifflib
New patch
1
Implement the actual main dispatch method that processes and handles the
2
list of doc sections for a given QAPI entity.
1
3
4
Signed-off-by: John Snow <jsnow@redhat.com>
5
---
6
docs/sphinx/qapidoc.py | 25 +++++++++++++++++++++++++
7
1 file changed, 25 insertions(+)
8
9
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
10
index XXXXXXX..XXXXXXX 100644
11
--- a/docs/sphinx/qapidoc.py
12
+++ b/docs/sphinx/qapidoc.py
13
@@ -XXX,XX +XXX,XX @@ def preamble(self, ent: QAPISchemaDefinition) -> None:
14
15
self.ensure_blank_line()
16
17
+ def visit_sections(self, ent: QAPISchemaDefinition) -> None:
18
+ sections = ent.doc.all_sections if ent.doc else []
19
+
20
+ # Add sections *in the order they are documented*:
21
+ for section in sections:
22
+ if section.kind == QAPIDoc.Kind.PLAIN:
23
+ self.visit_paragraph(section)
24
+ elif section.kind == QAPIDoc.Kind.MEMBER:
25
+ assert isinstance(section, QAPIDoc.ArgSection)
26
+ self.visit_member(section)
27
+ elif section.kind == QAPIDoc.Kind.FEATURE:
28
+ assert isinstance(section, QAPIDoc.ArgSection)
29
+ self.visit_feature(section)
30
+ elif section.kind in (QAPIDoc.Kind.SINCE, QAPIDoc.Kind.TODO):
31
+ # Since is handled in preamble, TODO is skipped intentionally.
32
+ pass
33
+ elif section.kind == QAPIDoc.Kind.RETURNS:
34
+ self.visit_returns(section)
35
+ elif section.kind == QAPIDoc.Kind.ERRORS:
36
+ self.visit_errors(section)
37
+ else:
38
+ assert False
39
+
40
+ self.ensure_blank_line()
41
+
42
# Transmogrification core methods
43
44
def visit_module(self, path: str) -> None:
45
--
46
2.48.1
diff view generated by jsdifflib
New patch
1
Finally, the core entry method for a qapi entity.
1
2
3
Signed-off-by: John Snow <jsnow@redhat.com>
4
---
5
docs/sphinx/qapidoc.py | 21 +++++++++++++++++++++
6
1 file changed, 21 insertions(+)
7
8
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
9
index XXXXXXX..XXXXXXX 100644
10
--- a/docs/sphinx/qapidoc.py
11
+++ b/docs/sphinx/qapidoc.py
12
@@ -XXX,XX +XXX,XX @@
13
14
15
class Transmogrifier:
16
+ # pylint: disable=too-many-public-methods
17
+
18
# Field names used for different entity types:
19
field_types = {
20
"enum": "value",
21
@@ -XXX,XX +XXX,XX @@ def visit_freeform(self, doc: QAPIDoc) -> None:
22
self.add_lines(text, info)
23
self.ensure_blank_line()
24
25
+ def visit_entity(self, ent: QAPISchemaDefinition) -> None:
26
+ assert ent.info
27
+
28
+ try:
29
+ self._curr_ent = ent
30
+
31
+ # Squish structs and unions together into an "object" directive.
32
+ meta = ent.meta
33
+ if meta in ("struct", "union"):
34
+ meta = "object"
35
+
36
+ # This line gets credited to the start of the /definition/.
37
+ self.add_line(f".. qapi:{meta}:: {ent.name}", ent.info)
38
+ with self.indented():
39
+ self.preamble(ent)
40
+ self.visit_sections(ent)
41
+ finally:
42
+ self._curr_ent = None
43
+
44
45
class QAPISchemaGenDepVisitor(QAPISchemaVisitor):
46
"""A QAPI schema visitor which adds Sphinx dependencies each module
47
--
48
2.48.1
diff view generated by jsdifflib
New patch
1
This is the true top-level processor for the new transmogrifier;
2
responsible both for generating the intermediate rST and then running
3
the nested parse on that generated document to produce the final
4
docutils tree that is then - very finally - postprocessed by sphinx for
5
final rendering to HTML &c.
1
6
7
Signed-off-by: John Snow <jsnow@redhat.com>
8
---
9
docs/sphinx/qapidoc.py | 49 +++++++++++++++++++++++++++++++++++++++++-
10
1 file changed, 48 insertions(+), 1 deletion(-)
11
12
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
13
index XXXXXXX..XXXXXXX 100644
14
--- a/docs/sphinx/qapidoc.py
15
+++ b/docs/sphinx/qapidoc.py
16
@@ -XXX,XX +XXX,XX @@
17
#
18
# QEMU qapidoc QAPI file parsing extension
19
#
20
+# Copyright (c) 2024 Red Hat
21
# Copyright (c) 2020 Linaro
22
#
23
# This work is licensed under the terms of the GNU GPLv2 or later.
24
@@ -XXX,XX +XXX,XX @@
25
https://www.sphinx-doc.org/en/master/development/index.html
26
"""
27
28
+# pylint: disable=too-many-lines
29
+
30
from __future__ import annotations
31
32
from contextlib import contextmanager
33
@@ -XXX,XX +XXX,XX @@
34
from sphinx import addnodes
35
from sphinx.directives.code import CodeBlock
36
from sphinx.errors import ExtensionError
37
+from sphinx.util import logging
38
from sphinx.util.docutils import switch_source_input
39
from sphinx.util.nodes import nested_parse_with_titles
40
41
@@ -XXX,XX +XXX,XX @@
42
43
__version__ = "1.0"
44
45
+logger = logging.getLogger(__name__)
46
+
47
48
class Transmogrifier:
49
# pylint: disable=too-many-public-methods
50
@@ -XXX,XX +XXX,XX @@ def __init__(self) -> None:
51
self._result = StringList()
52
self.indent = 0
53
54
+ @property
55
+ def result(self) -> StringList:
56
+ return self._result
57
+
58
@property
59
def entity(self) -> QAPISchemaDefinition:
60
assert self._curr_ent is not None
61
@@ -XXX,XX +XXX,XX @@ def new_serialno(self) -> str:
62
return "qapidoc-%d" % env.new_serialno("qapidoc")
63
64
def transmogrify(self, schema: QAPISchema) -> nodes.Element:
65
- raise NotImplementedError
66
+ logger.info("Transmogrifying QAPI to rST ...")
67
+ vis = Transmogrifier()
68
+ modules = set()
69
+
70
+ for doc in schema.docs:
71
+ module_source = doc.info.fname
72
+ if module_source not in modules:
73
+ vis.visit_module(module_source)
74
+ modules.add(module_source)
75
+
76
+ if doc.symbol:
77
+ ent = schema.lookup_entity(doc.symbol)
78
+ assert isinstance(ent, QAPISchemaDefinition)
79
+ vis.visit_entity(ent)
80
+ else:
81
+ vis.visit_freeform(doc)
82
+
83
+ logger.info("Transmogrification complete.")
84
+
85
+ contentnode = nodes.section()
86
+ content = vis.result
87
+ titles_allowed = True
88
+
89
+ logger.info("Transmogrifier running nested parse ...")
90
+ with switch_source_input(self.state, content):
91
+ if titles_allowed:
92
+ node: nodes.Element = nodes.section()
93
+ node.document = self.state.document
94
+ nested_parse_with_titles(self.state, content, contentnode)
95
+ else:
96
+ node = nodes.paragraph()
97
+ node.document = self.state.document
98
+ self.state.nested_parse(content, 0, contentnode)
99
+ logger.info("Transmogrifier's nested parse completed.")
100
+ sys.stdout.flush()
101
+
102
+ return contentnode
103
104
def legacy(self, schema: QAPISchema) -> nodes.Element:
105
vis = QAPISchemaGenRSTVisitor(self)
106
@@ -XXX,XX +XXX,XX @@ def run(self) -> List[nodes.Node]:
107
108
def setup(app: Sphinx) -> ExtensionMetadata:
109
"""Register qapi-doc directive with Sphinx"""
110
+ app.setup_extension("qapi_domain")
111
app.add_config_value("qapidoc_srctree", None, "env")
112
app.add_directive("qapi-doc", QAPIDocDirective)
113
app.add_directive("qmp-example", QMPExample)
114
--
115
2.48.1
diff view generated by jsdifflib
New patch
1
Add support for the special QAPI doc syntax to process @references as
2
``preformatted text``. At the moment, there are no actual
3
cross-references for individual members, so there is nothing to link
4
against. For now, process it identically to how we did in the old
5
qapidoc system.
1
6
7
Signed-off-by: John Snow <jsnow@redhat.com>
8
---
9
docs/sphinx/qapidoc.py | 3 +++
10
1 file changed, 3 insertions(+)
11
12
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
13
index XXXXXXX..XXXXXXX 100644
14
--- a/docs/sphinx/qapidoc.py
15
+++ b/docs/sphinx/qapidoc.py
16
@@ -XXX,XX +XXX,XX @@ def visit_sections(self, ent: QAPISchemaDefinition) -> None:
17
18
# Add sections *in the order they are documented*:
19
for section in sections:
20
+ # @var is translated to ``var``:
21
+ section.text = re.sub(r"@([\w-]+)", r"``\1``", section.text)
22
+
23
if section.kind == QAPIDoc.Kind.PLAIN:
24
self.visit_paragraph(section)
25
elif section.kind == QAPIDoc.Kind.MEMBER:
26
--
27
2.48.1
diff view generated by jsdifflib
New patch
1
Add debugging output for the qapidoc transmogrifier - setting DEBUG=1
2
will produce .ir files (one for each qapidoc directive) that write the
3
generated rst file to disk to allow for easy debugging and verification
4
of the generated document.
1
5
6
Signed-off-by: John Snow <jsnow@redhat.com>
7
---
8
docs/sphinx/qapidoc.py | 41 +++++++++++++++++++++++++++++++++++++----
9
1 file changed, 37 insertions(+), 4 deletions(-)
10
11
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
12
index XXXXXXX..XXXXXXX 100644
13
--- a/docs/sphinx/qapidoc.py
14
+++ b/docs/sphinx/qapidoc.py
15
@@ -XXX,XX +XXX,XX @@
16
from typing import TYPE_CHECKING
17
18
from docutils import nodes
19
-from docutils.parsers.rst import Directive, directives
20
+from docutils.parsers.rst import directives
21
from docutils.statemachine import StringList
22
from qapi.error import QAPIError
23
from qapi.parser import QAPIDoc
24
@@ -XXX,XX +XXX,XX @@
25
from sphinx.directives.code import CodeBlock
26
from sphinx.errors import ExtensionError
27
from sphinx.util import logging
28
-from sphinx.util.docutils import switch_source_input
29
+from sphinx.util.docutils import SphinxDirective, switch_source_input
30
from sphinx.util.nodes import nested_parse_with_titles
31
32
33
@@ -XXX,XX +XXX,XX @@ def visit_module(self, name: str) -> None:
34
super().visit_module(name)
35
36
37
-class NestedDirective(Directive):
38
+class NestedDirective(SphinxDirective):
39
def run(self) -> Sequence[nodes.Node]:
40
raise NotImplementedError
41
42
@@ -XXX,XX +XXX,XX @@ def transmogrify(self, schema: QAPISchema) -> nodes.Element:
43
node.document = self.state.document
44
self.state.nested_parse(content, 0, contentnode)
45
logger.info("Transmogrifier's nested parse completed.")
46
+
47
+ if self.env.app.verbosity >= 2 or os.environ.get("DEBUG"):
48
+ argname = "_".join(Path(self.arguments[0]).parts)
49
+ name = Path(argname).stem + ".ir"
50
+ self.write_intermediate(content, name)
51
+
52
sys.stdout.flush()
53
-
54
return contentnode
55
56
+ def write_intermediate(self, content: StringList, filename: str) -> None:
57
+ logger.info(
58
+ "writing intermediate rST for '%s' to '%s'",
59
+ self.arguments[0],
60
+ filename,
61
+ )
62
+
63
+ srctree = Path(self.env.app.config.qapidoc_srctree).resolve()
64
+ outlines = []
65
+ lcol_width = 0
66
+
67
+ for i, line in enumerate(content):
68
+ src, lineno = content.info(i)
69
+ srcpath = Path(src).resolve()
70
+ srcpath = srcpath.relative_to(srctree)
71
+
72
+ lcol = f"{srcpath}:{lineno:04d}"
73
+ lcol_width = max(lcol_width, len(lcol))
74
+ outlines.append((lcol, line))
75
+
76
+ with open(filename, "w", encoding="UTF-8") as outfile:
77
+ for lcol, rcol in outlines:
78
+ outfile.write(lcol.rjust(lcol_width))
79
+ outfile.write(" |")
80
+ if rcol:
81
+ outfile.write(f" {rcol}")
82
+ outfile.write("\n")
83
+
84
def legacy(self, schema: QAPISchema) -> nodes.Element:
85
vis = QAPISchemaGenRSTVisitor(self)
86
vis.visit_begin(schema)
87
--
88
2.48.1
diff view generated by jsdifflib
New patch
1
Add "the members of ..." pointers to Members and Arguments lists where
2
appropriate, with clickable cross-references - so it's a slight
3
improvement over the old system :)
1
4
5
This patch is meant to be a temporary solution until we can review and
6
merge the inliner.
7
8
The implementation of this patch is a little bit of a hack: Sphinx is
9
not designed to allow you to mix fields of different "type"; i.e. mixing
10
member descriptions and free-form text under the same heading. To
11
accomplish this with a minimum of hackery, we technically document a
12
"dummy field" and then just strip off the documentation for that dummy
13
field in a post-processing step. We use the "q_dummy" variable for this
14
purpose, then strip it back out before final processing. If this
15
processing step should fail, you'll see warnings for a bad
16
cross-reference. (So if you don't see any, it must be working!)
17
18
Signed-off-by: John Snow <jsnow@redhat.com>
19
---
20
docs/sphinx/qapi_domain.py | 22 +++++++++++++--
21
docs/sphinx/qapidoc.py | 58 +++++++++++++++++++++++++++++++++++++-
22
2 files changed, 77 insertions(+), 3 deletions(-)
23
24
diff --git a/docs/sphinx/qapi_domain.py b/docs/sphinx/qapi_domain.py
25
index XXXXXXX..XXXXXXX 100644
26
--- a/docs/sphinx/qapi_domain.py
27
+++ b/docs/sphinx/qapi_domain.py
28
@@ -XXX,XX +XXX,XX @@ def _toc_entry_name(self, sig_node: desc_signature) -> str:
29
return ""
30
31
32
+class SpecialTypedField(CompatTypedField):
33
+ def make_field(self, *args: Any, **kwargs: Any) -> nodes.field:
34
+ ret = super().make_field(*args, **kwargs)
35
+
36
+ # Look for the characteristic " -- " text node that Sphinx
37
+ # inserts for each TypedField entry ...
38
+ for node in ret.traverse(lambda n: str(n) == " -- "):
39
+ par = node.parent
40
+ if par.children[0].astext() != "q_dummy":
41
+ continue
42
+
43
+ # If the first node's text is q_dummy, this is a dummy
44
+ # field we want to strip down to just its contents.
45
+ del par.children[:-1]
46
+
47
+ return ret
48
+
49
+
50
class QAPICommand(QAPIObject):
51
"""Description of a QAPI Command."""
52
53
@@ -XXX,XX +XXX,XX @@ class QAPICommand(QAPIObject):
54
doc_field_types.extend(
55
[
56
# :arg TypeName ArgName: descr
57
- CompatTypedField(
58
+ SpecialTypedField(
59
"argument",
60
label=_("Arguments"),
61
names=("arg",),
62
@@ -XXX,XX +XXX,XX @@ class QAPIObjectWithMembers(QAPIObject):
63
doc_field_types.extend(
64
[
65
# :member type name: descr
66
- CompatTypedField(
67
+ SpecialTypedField(
68
"member",
69
label=_("Members"),
70
names=("memb",),
71
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
72
index XXXXXXX..XXXXXXX 100644
73
--- a/docs/sphinx/qapidoc.py
74
+++ b/docs/sphinx/qapidoc.py
75
@@ -XXX,XX +XXX,XX @@
76
QAPISchemaCommand,
77
QAPISchemaDefinition,
78
QAPISchemaEnumMember,
79
+ QAPISchemaEvent,
80
QAPISchemaFeature,
81
QAPISchemaMember,
82
+ QAPISchemaObjectType,
83
QAPISchemaObjectTypeMember,
84
QAPISchemaType,
85
QAPISchemaVisitor,
86
@@ -XXX,XX +XXX,XX @@ def preamble(self, ent: QAPISchemaDefinition) -> None:
87
88
self.ensure_blank_line()
89
90
+ def _insert_member_pointer(self, ent: QAPISchemaDefinition) -> None:
91
+
92
+ def _get_target(
93
+ ent: QAPISchemaDefinition,
94
+ ) -> Optional[QAPISchemaDefinition]:
95
+ if isinstance(ent, (QAPISchemaCommand, QAPISchemaEvent)):
96
+ return ent.arg_type
97
+ if isinstance(ent, QAPISchemaObjectType):
98
+ return ent.base
99
+ return None
100
+
101
+ target = _get_target(ent)
102
+ if target is not None and not target.is_implicit():
103
+ assert ent.info
104
+ self.add_field(
105
+ self.member_field_type,
106
+ "q_dummy",
107
+ f"The members of :qapi:type:`{target.name}`.",
108
+ ent.info,
109
+ "q_dummy",
110
+ )
111
+
112
+ if isinstance(ent, QAPISchemaObjectType) and ent.branches is not None:
113
+ for variant in ent.branches.variants:
114
+ if variant.type.name == "q_empty":
115
+ continue
116
+ assert ent.info
117
+ self.add_field(
118
+ self.member_field_type,
119
+ "q_dummy",
120
+ f" When ``{ent.branches.tag_member.name}`` is "
121
+ f"``{variant.name}``: "
122
+ f"The members of :qapi:type:`{variant.type.name}`.",
123
+ ent.info,
124
+ "q_dummy",
125
+ )
126
+
127
def visit_sections(self, ent: QAPISchemaDefinition) -> None:
128
sections = ent.doc.all_sections if ent.doc else []
129
130
+ # Determine the index location at which we should generate
131
+ # documentation for "The members of ..." pointers. This should
132
+ # go at the end of the members section(s) if any. Note that
133
+ # index 0 is assumed to be a plain intro section, even if it is
134
+ # empty; and that a members section if present will always
135
+ # immediately follow the opening PLAIN section.
136
+ gen_index = 1
137
+ if len(sections) > 1:
138
+ while sections[gen_index].kind == QAPIDoc.Kind.MEMBER:
139
+ gen_index += 1
140
+ if gen_index >= len(sections):
141
+ break
142
+
143
# Add sections *in the order they are documented*:
144
- for section in sections:
145
+ for i, section in enumerate(sections):
146
# @var is translated to ``var``:
147
section.text = re.sub(r"@([\w-]+)", r"``\1``", section.text)
148
149
@@ -XXX,XX +XXX,XX @@ def visit_sections(self, ent: QAPISchemaDefinition) -> None:
150
else:
151
assert False
152
153
+ # Generate "The members of ..." entries if necessary:
154
+ if i == gen_index - 1:
155
+ self._insert_member_pointer(ent)
156
+
157
self.ensure_blank_line()
158
159
# Transmogrification core methods
160
--
161
2.48.1
diff view generated by jsdifflib
New patch
1
Presently, we never have any empty text entries for members. The next
2
patch will explicitly generate such sections, so enable support for it
3
in advance.
1
4
5
The parser will generate placeholder sections to indicate undocumented
6
members, but it's the qapidoc generator that's responsible for deciding
7
what to do with that stub section.
8
9
Signed-off-by: John Snow <jsnow@redhat.com>
10
---
11
docs/sphinx/qapidoc.py | 4 ++--
12
1 file changed, 2 insertions(+), 2 deletions(-)
13
14
diff --git a/docs/sphinx/qapidoc.py b/docs/sphinx/qapidoc.py
15
index XXXXXXX..XXXXXXX 100644
16
--- a/docs/sphinx/qapidoc.py
17
+++ b/docs/sphinx/qapidoc.py
18
@@ -XXX,XX +XXX,XX @@ def visit_member(self, section: QAPIDoc.ArgSection) -> None:
19
# TODO?: features for members (documented at entity-level,
20
# but sometimes defined per-member. Should we add such
21
# information to member descriptions when we can?)
22
- assert section.text and section.member
23
+ assert section.member
24
self.generate_field(
25
self.member_field_type,
26
section.member,
27
- section.text,
28
+ section.text if section.text else "(Not Documented.)",
29
section.info,
30
)
31
32
--
33
2.48.1
diff view generated by jsdifflib
New patch
1
This helps simplify the new doc generator if it doesn't have to check
2
for undocumented members, it can just blindly operate on a sequence of
3
QAPIDoc.Section instances.
1
4
5
NB: If there is no existing 'member' section, these undocumented stub
6
members will be inserted directly after the leading section.
7
8
Signed-off-by: John Snow <jsnow@redhat.com>
9
---
10
scripts/qapi/parser.py | 15 ++++++++++++++-
11
1 file changed, 14 insertions(+), 1 deletion(-)
12
13
diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
14
index XXXXXXX..XXXXXXX 100644
15
--- a/scripts/qapi/parser.py
16
+++ b/scripts/qapi/parser.py
17
@@ -XXX,XX +XXX,XX @@ def connect_member(self, member: 'QAPISchemaMember') -> None:
18
raise QAPISemError(member.info,
19
"%s '%s' lacks documentation"
20
% (member.role, member.name))
21
- self.args[member.name] = QAPIDoc.ArgSection(
22
+ section = QAPIDoc.ArgSection(
23
self.info, QAPIDoc.Kind.MEMBER, member.name)
24
+ self.args[member.name] = section
25
+
26
+ # Insert stub documentation section for missing member docs.
27
+ # Determine where to insert stub doc - it should go at the
28
+ # end of the members section(s), if any. Note that index 0
29
+ # is assumed to be an untagged intro section, even if it is
30
+ # empty.
31
+ index = 1
32
+ if len(self.all_sections) > 1:
33
+ while self.all_sections[index].kind == QAPIDoc.Kind.MEMBER:
34
+ index += 1
35
+ self.all_sections.insert(index, section)
36
+
37
self.args[member.name].connect(member)
38
39
def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
40
--
41
2.48.1
diff view generated by jsdifflib
New patch
1
The next patch will engage the qapidoc transmogrifier, which creates a
2
lot of cross-reference targets. Some of the existing targets
3
("migration", "qom", "replay") will become ambiguous as a result. Nail
4
them down more explicitly to prevent ambiguous cross-reference warnings.
1
5
6
Signed-off-by: John Snow <jsnow@redhat.com>
7
---
8
docs/devel/codebase.rst | 6 +++---
9
docs/glossary.rst | 10 +++++-----
10
2 files changed, 8 insertions(+), 8 deletions(-)
11
12
diff --git a/docs/devel/codebase.rst b/docs/devel/codebase.rst
13
index XXXXXXX..XXXXXXX 100644
14
--- a/docs/devel/codebase.rst
15
+++ b/docs/devel/codebase.rst
16
@@ -XXX,XX +XXX,XX @@ Some of the main QEMU subsystems are:
17
- `Devices<device-emulation>` & Board models
18
- `Documentation <documentation-root>`
19
- `GDB support<GDB usage>`
20
-- `Migration<migration>`
21
+- :ref:`Migration<migration>`
22
- `Monitor<QEMU monitor>`
23
- :ref:`QOM (QEMU Object Model)<qom>`
24
- `System mode<System emulation>`
25
@@ -XXX,XX +XXX,XX @@ yet, so sometimes the source code is all you have.
26
* `libdecnumber <https://gitlab.com/qemu-project/qemu/-/tree/master/libdecnumber>`_:
27
Import of gcc library, used to implement decimal number arithmetic.
28
* `migration <https://gitlab.com/qemu-project/qemu/-/tree/master/migration>`__:
29
- `Migration framework <migration>`.
30
+ :ref:`Migration framework <migration>`.
31
* `monitor <https://gitlab.com/qemu-project/qemu/-/tree/master/monitor>`_:
32
`Monitor <QEMU monitor>` implementation (HMP & QMP).
33
* `nbd <https://gitlab.com/qemu-project/qemu/-/tree/master/nbd>`_:
34
@@ -XXX,XX +XXX,XX @@ yet, so sometimes the source code is all you have.
35
- `lcitool <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/lcitool>`_:
36
Generate dockerfiles for CI containers.
37
- `migration <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/migration>`_:
38
- Test scripts and data for `Migration framework <migration>`.
39
+ Test scripts and data for :ref:`Migration framework <migration>`.
40
- `multiboot <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/multiboot>`_:
41
Test multiboot functionality for x86_64/i386.
42
- `qapi-schema <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/qapi-schema>`_:
43
diff --git a/docs/glossary.rst b/docs/glossary.rst
44
index XXXXXXX..XXXXXXX 100644
45
--- a/docs/glossary.rst
46
+++ b/docs/glossary.rst
47
@@ -XXX,XX +XXX,XX @@ Migration
48
---------
49
50
QEMU can save and restore the execution of a virtual machine between different
51
-host systems. This is provided by the `Migration framework<migration>`.
52
+host systems. This is provided by the :ref:`Migration framework<migration>`.
53
54
NBD
55
---
56
@@ -XXX,XX +XXX,XX @@ machine emulator and virtualizer.
57
QOM
58
---
59
60
-`QEMU Object Model <qom>` is an object oriented API used to define various
61
-devices and hardware in the QEMU codebase.
62
+:ref:`QEMU Object Model <qom>` is an object oriented API used to define
63
+various devices and hardware in the QEMU codebase.
64
65
Record/replay
66
-------------
67
68
-`Record/replay <replay>` is a feature of QEMU allowing to have a deterministic
69
-and reproducible execution of a virtual machine.
70
+:ref:`Record/replay <replay>` is a feature of QEMU allowing to have a
71
+deterministic and reproducible execution of a virtual machine.
72
73
Rust
74
----
75
--
76
2.48.1
diff view generated by jsdifflib
New patch
1
We are not enabling the transmogrifier for QSD or QGA yet because we
2
don't (yet) have a way to create separate indices, and all of the
3
definitions will bleed together, which isn't so nice.
1
4
5
For now, QMP is better than nothing at all!
6
7
Signed-off-by: John Snow <jsnow@redhat.com>
8
---
9
docs/interop/qemu-qmp-ref.rst | 1 +
10
qapi/qapi-schema.json | 2 ++
11
2 files changed, 3 insertions(+)
12
13
diff --git a/docs/interop/qemu-qmp-ref.rst b/docs/interop/qemu-qmp-ref.rst
14
index XXXXXXX..XXXXXXX 100644
15
--- a/docs/interop/qemu-qmp-ref.rst
16
+++ b/docs/interop/qemu-qmp-ref.rst
17
@@ -XXX,XX +XXX,XX @@ QEMU QMP Reference Manual
18
:depth: 3
19
20
.. qapi-doc:: qapi/qapi-schema.json
21
+ :transmogrify:
22
diff --git a/qapi/qapi-schema.json b/qapi/qapi-schema.json
23
index XXXXXXX..XXXXXXX 100644
24
--- a/qapi/qapi-schema.json
25
+++ b/qapi/qapi-schema.json
26
@@ -XXX,XX +XXX,XX @@
27
#
28
# This document describes all commands currently supported by QMP.
29
#
30
+# For locating a particular item, please see the `qapi-index`.
31
+#
32
# Most of the time their usage is exactly the same as in the user
33
# Monitor, this means that any other document which also describe
34
# commands (the manpage, QEMU's manual, etc) can and should be
35
--
36
2.48.1
diff view generated by jsdifflib
New patch
1
Who documents the documentation?
1
2
3
Me, I guess.
4
5
Signed-off-by: John Snow <jsnow@redhat.com>
6
---
7
docs/devel/index-build.rst | 1 +
8
docs/devel/qapi-domain.rst | 670 +++++++++++++++++++++++++++++++++++++
9
2 files changed, 671 insertions(+)
10
create mode 100644 docs/devel/qapi-domain.rst
11
12
diff --git a/docs/devel/index-build.rst b/docs/devel/index-build.rst
13
index XXXXXXX..XXXXXXX 100644
14
--- a/docs/devel/index-build.rst
15
+++ b/docs/devel/index-build.rst
16
@@ -XXX,XX +XXX,XX @@ some of the basics if you are adding new files and targets to the build.
17
kconfig
18
docs
19
qapi-code-gen
20
+ qapi-domain
21
control-flow-integrity
22
diff --git a/docs/devel/qapi-domain.rst b/docs/devel/qapi-domain.rst
23
new file mode 100644
24
index XXXXXXX..XXXXXXX
25
--- /dev/null
26
+++ b/docs/devel/qapi-domain.rst
27
@@ -XXX,XX +XXX,XX @@
28
+======================
29
+The Sphinx QAPI Domain
30
+======================
31
+
32
+An extension to the `rST syntax
33
+<https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html>`_
34
+in Sphinx is provided by the QAPI Domain, located in
35
+``docs/sphinx/qapi_domain.py``. This extension is analogous to the
36
+`Python Domain
37
+<https://www.sphinx-doc.org/en/master/usage/domains/python.html>`_
38
+included with Sphinx, but provides special directives and roles
39
+speciically for annotating and documenting QAPI definitions
40
+specifically.
41
+
42
+A `Domain
43
+<https://www.sphinx-doc.org/en/master/usage/domains/index.html>`_
44
+provides a set of special rST directives and cross-referencing roles to
45
+Sphinx for understanding rST markup written to document a specific
46
+language. By itself, this QAPI extension is only sufficient to parse rST
47
+markup written by hand; the `autodoc
48
+<https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html>`_
49
+functionality is provided elsewhere, in ``docs/sphinx/qapidoc.py``, by
50
+the "Transmogrifier".
51
+
52
+It is not expected that any developer nor documentation writer would
53
+never need to write *nor* read these special rST forms. However, in the
54
+event that something needs to be debugged, knowing the syntax of the
55
+domain is quite handy. This reference may also be useful as a guide for
56
+understanding the QAPI Domain extension code itself. Although most of
57
+these forms will not be needed for documentation writing purposes,
58
+understanding the cross-referencing syntax *will* be helpful when
59
+writing rST documentation elsewhere, or for enriching the body of
60
+QAPIDoc blocks themselves.
61
+
62
+
63
+Concepts
64
+========
65
+
66
+The QAPI Domain itself provides no mechanisms for reading the QAPI
67
+Schema or generating documentation from code that exists. It is merely
68
+the rST syntax used to describe things. For instance, the Sphinx Python
69
+domain adds syntax like ``:py:func:`` for describing Python functions in
70
+documentation, but it's the autodoc module that is responsible for
71
+reading python code and generating such syntax. QAPI is analagous here:
72
+qapidoc.py is responsible for reading the QAPI Schema and generating rST
73
+syntax, and qapi_domain.py is responsible for translating that special
74
+syntax and providing APIs for Sphinx internals.
75
+
76
+In other words:
77
+
78
+qapi_domain.py adds syntax like ``.. qapi:command::`` to Sphinx, and
79
+qapidoc.py transforms the documentation in ``qapi/*.json`` into rST
80
+using directives defined by the domain.
81
+
82
+Or even shorter:
83
+
84
+``:py:`` is to ``:qapi:`` as *autodoc* is to *qapidoc*.
85
+
86
+
87
+Info Field Lists
88
+================
89
+
90
+`Field lists
91
+<https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#field-lists>`_
92
+are a standard syntax in reStructuredText. Sphinx `extends that syntax
93
+<https://www.sphinx-doc.org/en/master/usage/domains/python.html#info-field-lists>`_
94
+to give certain field list entries special meaning and parsing to, for
95
+example, add cross-references. The QAPI Domain takes advantage of this
96
+field list extension to document things like Arguments, Members, Values,
97
+and so on.
98
+
99
+The special parsing and handling of info field lists in Sphinx is provided by
100
+three main classes; Field, GroupedField, and TypedField. The behavior
101
+and formatting for each configured field list entry in the domain
102
+changes depending on which class is used.
103
+
104
+Field:
105
+ * Creates an ungrouped field: i.e., each entry will create its own
106
+ section and they will not be combined.
107
+ * May *optionally* support an argument.
108
+ * May apply cross-reference roles to *either* the argument *or* the
109
+ content body, both, or neither.
110
+
111
+This is used primarily for entries which are not expected to be
112
+repeated, i.e., items that may only show up at most once. The QAPI
113
+domain uses this class for "Errors" section.
114
+
115
+GroupedField:
116
+ * Creates a grouped field: i.e. multiple adjacent entries will be
117
+ merged into one section, and the content will form a bulleted list.
118
+ * *Must* take an argument.
119
+ * May optionally apply a cross-reference role to the argument, but not
120
+ the body.
121
+ * Can be configured to remove the bulleted list if there is only a
122
+ single entry.
123
+ * All items will be generated with the form: "argument -- body"
124
+
125
+This is used for entries which are expected to be repeated, but aren't
126
+expected to have two arguments, i.e. types without names, or names
127
+without types. The QAPI domain uses this class for features, returns,
128
+and enum values.
129
+
130
+TypedField:
131
+ * Creates a grouped, typed field. Multiple adjacent entres will be
132
+ merged into one section, and the content will form a bulleted list.
133
+ * *Must* take at least one argument, but supports up to two -
134
+ nominally, a name and a type.
135
+ * May optionally apply a cross-reference role to the type or the name
136
+ argument, but not the body.
137
+ * Can be configured to remove the bulleted list if there is only a
138
+ single entry.
139
+ * All items will be generated with the form "name (type) -- body"
140
+
141
+This is used for entries that are expected to be repeated and will have
142
+a name, a type, and a description. The QAPI domain uses this class for
143
+arguments, alternatives, and members. Wherever type names are referenced
144
+below, They must be a valid, documented type that will be
145
+cross-referenced in the HTML output; or one of the built-in JSON types
146
+(string, number, int, boolean, null, value, q_empty).
147
+
148
+
149
+``:feat:``
150
+----------
151
+
152
+Document a feature attached to a QAPI definition.
153
+
154
+:availability: This field list is available in the body of Command,
155
+ Event, Enum, Object and Alternate directives.
156
+:syntax: ``:feat name: Lorem ipsum, dolor sit amet...``
157
+:type: `sphinx.util.docfields.GroupedField
158
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.GroupedField.html?private=1>`_
159
+
160
+Example::
161
+
162
+ .. qapi:object:: BlockdevOptionsVirtioBlkVhostVdpa
163
+ :since: 7.2
164
+ :ifcond: CONFIG_BLKIO
165
+
166
+ Driver specific block device options for the virtio-blk-vhost-vdpa
167
+ backend.
168
+
169
+ :memb string path: path to the vhost-vdpa character device.
170
+ :feat fdset: Member ``path`` supports the special "/dev/fdset/N" path
171
+ (since 8.1)
172
+
173
+
174
+``:arg:``
175
+---------
176
+
177
+Document an argument to a QAPI command.
178
+
179
+:availability: This field list is only available in the body of the
180
+ Command directive.
181
+:syntax: ``:arg type name: description``
182
+:type: `sphinx.util.docfields.TypedField
183
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.TypedField.html?private=1>`_
184
+
185
+
186
+Example::
187
+
188
+ .. qapi:command:: job-pause
189
+ :since: 3.0
190
+
191
+ Pause an active job.
192
+
193
+ This command returns immediately after marking the active job for
194
+ pausing. Pausing an already paused job is an error.
195
+
196
+ The job will pause as soon as possible, which means transitioning
197
+ into the PAUSED state if it was RUNNING, or into STANDBY if it was
198
+ READY. The corresponding JOB_STATUS_CHANGE event will be emitted.
199
+
200
+ Cancelling a paused job automatically resumes it.
201
+
202
+ :arg string id: The job identifier.
203
+
204
+
205
+``:error:``
206
+-----------
207
+
208
+Document the error condition(s) of a QAPI command.
209
+
210
+:availability: This field list is only available in the body of the
211
+ Command directive.
212
+:syntax: ``:error: Lorem ipsum dolor sit amet ...``
213
+:type: `sphinx.util.docfields.Field
214
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.Field.html?private=1>`_
215
+
216
+The format of the :errors: field list description is free-form rST. The
217
+alternative spelling ":errors:" is also permitted, but strictly
218
+analogous.
219
+
220
+Example::
221
+
222
+ .. qapi:command:: block-job-set-speed
223
+ :since: 1.1
224
+
225
+ Set maximum speed for a background block operation.
226
+
227
+ This command can only be issued when there is an active block job.
228
+
229
+ Throttling can be disabled by setting the speed to 0.
230
+
231
+ :arg string device: The job identifier. This used to be a device
232
+ name (hence the name of the parameter), but since QEMU 2.7 it
233
+ can have other values.
234
+ :arg int speed: the maximum speed, in bytes per second, or 0 for
235
+ unlimited. Defaults to 0.
236
+ :error:
237
+ - If no background operation is active on this device,
238
+ DeviceNotActive
239
+
240
+
241
+``:return:``
242
+-------------
243
+
244
+Document the return type(s) and value(s) of a QAPI command.
245
+
246
+:availability: This field list is only available in the body of the
247
+ Command directive.
248
+:syntax: ``:return type: Lorem ipsum dolor sit amet ...``
249
+:type: `sphinx.util.docfields.GroupedField
250
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.GroupedField.html?private=1>`_
251
+
252
+
253
+Example::
254
+
255
+ .. qapi:command:: query-replay
256
+ :since: 5.2
257
+
258
+ Retrieve the record/replay information. It includes current
259
+ instruction count which may be used for ``replay-break`` and
260
+ ``replay-seek`` commands.
261
+
262
+ :return ReplayInfo: record/replay information.
263
+
264
+ .. qmp-example::
265
+
266
+ -> { "execute": "query-replay" }
267
+ <- { "return": {
268
+ "mode": "play", "filename": "log.rr", "icount": 220414 }
269
+ }
270
+
271
+
272
+``:value:``
273
+-----------
274
+
275
+Document a possible value for a QAPI enum.
276
+
277
+:availability: This field list is only available in the body of the Enum
278
+ directive.
279
+:syntax: ``:value name: Lorem ipsum, dolor sit amet ...``
280
+:type: `sphinx.util.docfields.GroupedField
281
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.GroupedField.html?private=1>`_
282
+
283
+Example::
284
+
285
+ .. qapi:enum:: QapiErrorClass
286
+ :since: 1.2
287
+
288
+ QEMU error classes
289
+
290
+ :value GenericError: this is used for errors that don't require a specific
291
+ error class. This should be the default case for most errors
292
+ :value CommandNotFound: the requested command has not been found
293
+ :value DeviceNotActive: a device has failed to be become active
294
+ :value DeviceNotFound: the requested device has not been found
295
+ :value KVMMissingCap: the requested operation can't be fulfilled because a
296
+ required KVM capability is missing
297
+
298
+
299
+``:alt:``
300
+------------
301
+
302
+Document a possible branch for a QAPI alternate.
303
+
304
+:availability: This field list is only available in the body of the
305
+ Alternate directive.
306
+:syntax: ``:alt type name: Lorem ipsum, dolor sit amet ...``
307
+:type: `sphinx.util.docfields.TypedField
308
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.TypedField.html?private=1>`_
309
+
310
+As a limitation of Sphinx, we must document the "name" of the branch in
311
+addition to the type, even though this information is not visible on the
312
+wire in the QMP protocol format. This limitation *may* be lifted at a
313
+future date.
314
+
315
+Example::
316
+
317
+ .. qapi:alternate:: StrOrNull
318
+ :since: 2.10
319
+
320
+ This is a string value or the explicit lack of a string (null
321
+ pointer in C). Intended for cases when 'optional absent' already
322
+ has a different meaning.
323
+
324
+ :alt string s: the string value
325
+ :alt null n: no string value
326
+
327
+
328
+``:memb:``
329
+----------
330
+
331
+Document a member of an Event or Object.
332
+
333
+:availability: This field list is available in the body of Event or
334
+ Object directives.
335
+:syntax: ``:memb type name: Lorem ipsum, dolor sit amet ...``
336
+:type: `sphinx.util.docfields.TypedField
337
+ <https://pydoc.dev/sphinx/latest/sphinx.util.docfields.TypedField.html?private=1>`_
338
+
339
+This is fundamentally the same as ``:arg:`` and ``:alt:``, but uses the
340
+"Members" phrasing for Events and Objects (Structs and Unions).
341
+
342
+Example::
343
+
344
+ .. qapi:event:: JOB_STATUS_CHANGE
345
+ :since: 3.0
346
+
347
+ Emitted when a job transitions to a different status.
348
+
349
+ :memb string id: The job identifier
350
+ :memb JobStatus status: The new job status
351
+
352
+
353
+Arbitrary field lists
354
+---------------------
355
+
356
+Other field list names, while valid rST syntax, are prohibited inside of
357
+QAPI directives to help prevent accidental misspellings of info field
358
+list names. If you want to add a new arbitrary "non-value-added" field
359
+list to QAPI documentation, you must add the field name to the allow
360
+list in ``docs/conf.py``
361
+
362
+For example::
363
+
364
+ qapi_allowed_fields = {
365
+ "see also",
366
+ }
367
+
368
+Will allow you to add arbitrary field lists in QAPI directives::
369
+
370
+ .. qapi:command:: x-fake-command
371
+
372
+ :see also: Lorem ipsum, dolor sit amet ...
373
+
374
+
375
+Cross-references
376
+================
377
+
378
+Cross-reference `roles
379
+<https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html>`_
380
+in the QAPI domain are modeled closely after the `Python
381
+cross-referencing syntax
382
+<https://www.sphinx-doc.org/en/master/usage/domains/python.html#cross-referencing-python-objects>`_.
383
+
384
+QAPI definitions can be referenced using the standard `any
385
+<https://www.sphinx-doc.org/en/master/usage/referencing.html#role-any>`_
386
+role cross-reference syntax, such as with ```query-blockstats```. In
387
+the event that disambiguation is needed, cross-references can also be
388
+written using a number of explicit cross-reference roles:
389
+
390
+* ``:qapi:mod:`block-core``` -- Reference a QAPI module. The link will
391
+ take you to the beginning of that section in the documentation.
392
+* ``:qapi:cmd:`query-block``` -- Reference a QAPI command.
393
+* ``:qapi:event:`JOB_STATUS_CHANGE``` -- Reference a QAPI event.
394
+* ``:qapi:enum:`QapiErrorClass``` -- Reference a QAPI enum.
395
+* ``:qapi:obj:`BlockdevOptionsVirtioBlkVhostVdpa`` -- Reference a QAPI
396
+ object (struct or union)
397
+* ``:qapi:alt:`StrOrNull``` -- Reference a QAPI alternate.
398
+* ``:qapi:type:`BlockDirtyInfo``` -- Reference *any* QAPI type; this
399
+ excludes modules, commands, and events.
400
+* ``:qapi:any:`block-job-set-speed``` -- Reference absolutely any QAPI entity.
401
+
402
+Type arguments in info field lists are converted into references as if
403
+you had used the ``:qapi:type:`` role. All of the special syntax below
404
+applies to both info field lists and standalone explicit
405
+cross-references.
406
+
407
+
408
+Type decorations
409
+----------------
410
+
411
+Type names in references can be surrounded by brackets, like
412
+``[typename]``, to indicate an array of that type. The cross-reference
413
+will apply only to the type name between the brackets. For example;
414
+``:qapi:type:`[Qcow2BitmapInfoFlags]``` renders to:
415
+:qapi:type:`[Qcow2BitmapInfoFlags]`
416
+
417
+To indicate an optional argument/member in a field list, the type name
418
+can be suffixed with ``?``. The cross-reference will be transformed to
419
+"type, Optional" with the link applying only to the type name. For
420
+example; ``:qapi:type:`BitmapSyncMode?``` renders to:
421
+:qapi:type:`BitmapSyncMode?`
422
+
423
+
424
+Namespaces
425
+----------
426
+
427
+Mimicking the `Python domain target specification syntax
428
+<https://www.sphinx-doc.org/en/master/usage/domains/python.html#target-specification>`_,
429
+QAPI allows you to specify the fully qualified path for a data
430
+type. QAPI enforces globally unique names, so it's unlikely you'll need
431
+this specific feature, but it may be extended in the near future to
432
+allow referencing identically named commands and data types from
433
+different utilities; i.e. QEMU Storage Daemon vs QMP.
434
+
435
+* A module can be explicitly provided;
436
+ ``:qapi:type:`block-core.BitmapSyncMode``` will render to:
437
+ :qapi:type:`block-core.BitmapSyncMode`
438
+* If you don't want to display the "fully qualified" name, it can be
439
+ prefixed with a tilde; ``:qapi:type:`~block-core.BitmapSyncMode```
440
+ will render to: :qapi:type:`~block-core.BitmapSyncMode`
441
+
442
+
443
+Custom link text
444
+----------------
445
+
446
+The name of a cross-reference link can be explicitly overridden like
447
+`most stock Sphinx references
448
+<https://www.sphinx-doc.org/en/master/usage/referencing.html#syntax>`_
449
+using the ``custom text <target>`` syntax.
450
+
451
+For example, ``:qapi:cmd:`Merge dirty bitmaps
452
+<block-dirty-bitmap-merge>``` will render as: :qapi:cmd:`Merge dirty
453
+bitmaps <block-dirty-bitmap-merge>`
454
+
455
+
456
+Directives
457
+==========
458
+
459
+The QAPI domain adds a number of custom directives for documenting
460
+various QAPI/QMP entities. The syntax is plain rST, and follows this
461
+general format::
462
+
463
+ .. qapi:directive:: argument
464
+ :option:
465
+ :another-option: with an argument
466
+
467
+ Content body, arbitrary rST is allowed here.
468
+
469
+
470
+Sphinx standard options
471
+-----------------------
472
+
473
+All QAPI directives inherit a number of `standard options
474
+<https://www.sphinx-doc.org/en/master/usage/domains/index.html#basic-markup>`_
475
+from Sphinx's ObjectDescription class.
476
+
477
+The dashed spellings of the below options were added in Sphinx 7.2, the
478
+undashed spellings are currently retained as aliases, but will be
479
+removed in a future version.
480
+
481
+* ``:no-index:`` and ``:noindex:`` -- Do not add this item into the
482
+ Index, and do not make it available for cross-referencing.
483
+* ``no-index-entry:`` and ``:noindexentry:`` -- Do not add this item
484
+ into the Index, but allow it to be cross-referenced.
485
+* ``no-contents-entry`` and ``:nocontentsentry:`` -- Exclude this item
486
+ from the Table of Contents.
487
+* ``no-typesetting`` -- Create TOC, Index and cross-referencing
488
+ entities, but don't actually display the content.
489
+
490
+
491
+QAPI standard options
492
+---------------------
493
+
494
+All QAPI directives -- *except* for module -- support these common options.
495
+
496
+* ``:module: modname`` -- Borrowed from the Python domain, this option allows
497
+ you to override the module association of a given definition.
498
+* ``:since: x.y`` -- Allows the documenting of "Since" information, which is
499
+ displayed in the signature bar.
500
+* ``:ifcond: CONDITION`` -- Allows the documenting of conditional availability
501
+ information, which is displayed in an eyecatch just below the
502
+ signature bar.
503
+* ``:deprecated:`` -- Adds an eyecatch just below the signature bar that
504
+ advertises that this definition is deprecated and should be avoided.
505
+* ``:unstable:`` -- Adds an eyecatch just below the signature bar that
506
+ advertises that this definition is unstable and should not be used in
507
+ production code.
508
+
509
+
510
+qapi:module
511
+-----------
512
+
513
+The ``qapi:module`` directive marks the start of a QAPI module. It may have
514
+a content body, but it can be omitted. All subsequent QAPI directives
515
+are associated with the most recent module; this effects their "fully
516
+qualified" name, but has no other effect.
517
+
518
+Example::
519
+
520
+ .. qapi:module:: block-core
521
+
522
+ Welcome to the block-core module!
523
+
524
+Will be rendered as:
525
+
526
+.. qapi:module:: block-core
527
+ :noindex:
528
+
529
+ Welcome to the block-core module!
530
+
531
+
532
+qapi:command
533
+------------
534
+
535
+This directive documents a QMP command. It may use any of the standard
536
+Sphinx or QAPI options, and the documentation body may contain
537
+``:arg:``, ``:feat:``, ``:error:``, or ``:return:`` info field list
538
+entries.
539
+
540
+Example::
541
+
542
+ .. qapi:command:: x-fake-command
543
+ :since: 42.0
544
+ :unstable:
545
+
546
+ This command is fake, so it can't hurt you!
547
+
548
+ :arg int foo: Your favorite number.
549
+ :arg string? bar: Your favorite season.
550
+ :return [string]: A lovely computer-written poem for you.
551
+
552
+
553
+Will be rendered as:
554
+
555
+ .. qapi:command:: x-fake-command
556
+ :noindex:
557
+ :since: 42.0
558
+ :unstable:
559
+
560
+ This command is fake, so it can't hurt you!
561
+
562
+ :arg int foo: Your favorite number.
563
+ :arg string? bar: Your favorite season.
564
+ :return [string]: A lovely computer-written poem for you.
565
+
566
+
567
+qapi:event
568
+----------
569
+
570
+This directive documents a QMP event. It may use any of the standard
571
+Sphinx or QAPI options, and the documentation body may contain
572
+``:memb:`` or ``:feat:`` info field list entries.
573
+
574
+Example::
575
+
576
+ .. qapi:event:: COMPUTER_IS_RUINED
577
+ :since: 0.1
578
+ :deprecated:
579
+
580
+ This event is emitted when your computer is *extremely* ruined.
581
+
582
+ :memb string reason: Diagnostics as to what caused your computer to
583
+ be ruined.
584
+ :feat sadness: When present, the diagnostic message will also
585
+ explain how sad the computer is as a result of your wrongdoings.
586
+
587
+Will be rendered as:
588
+
589
+.. qapi:event:: COMPUTER_IS_RUINED
590
+ :noindex:
591
+ :since: 0.1
592
+ :deprecated:
593
+
594
+ This event is emitted when your computer is *extremely* ruined.
595
+
596
+ :memb string reason: Diagnostics as to what caused your computer to
597
+ be ruined.
598
+ :feat sadness: When present, the diagnostic message will also explain
599
+ how sad the computer is as a result of your wrongdoings.
600
+
601
+
602
+qapi:enum
603
+---------
604
+
605
+This directive documents a QAPI enum. It may use any of the standard
606
+Sphinx or QAPI options, and the documentation body may contain
607
+``:value:`` or ``:feat:`` info field list entries.
608
+
609
+Example::
610
+
611
+ .. qapi:enum:: Mood
612
+ :ifcond: LIB_PERSONALITY
613
+
614
+ This enum represents your virtual machine's current mood!
615
+
616
+ :value Happy: Your VM is content and well-fed.
617
+ :value Hungry: Your VM needs food.
618
+ :value Melancholic: Your VM is experiencing existential angst.
619
+ :value Petulant: Your VM is throwing a temper tantrum.
620
+
621
+Will be rendered as:
622
+
623
+.. qapi:enum:: Mood
624
+ :noindex:
625
+ :ifcond: LIB_PERSONALITY
626
+
627
+ This enum represents your virtual machine's current mood!
628
+
629
+ :value Happy: Your VM is content and well-fed.
630
+ :value Hungry: Your VM needs food.
631
+ :value Melancholic: Your VM is experiencing existential angst.
632
+ :value Petulant: Your VM is throwing a temper tantrum.
633
+
634
+
635
+qapi:object
636
+-----------
637
+
638
+This directive documents a QAPI structure or union and represents a QMP
639
+object. It may use any of the standard Sphinx or QAPI options, and the
640
+documentation body may contain ``:memb:`` or ``:feat:`` info field list
641
+entries.
642
+
643
+Example::
644
+
645
+ .. qapi:object:: BigBlobOfStuff
646
+
647
+ This object has a bunch of disparate and unrelated things in it.
648
+
649
+ :memb int Birthday: Your birthday, represented in seconds since the
650
+ UNIX epoch.
651
+ :memb [string] Fav-Foods: A list of your favorite foods.
652
+ :memb boolean? Bizarre-Docs: True if the documentation reference
653
+ should be strange.
654
+
655
+Will be rendered as:
656
+
657
+.. qapi:object:: BigBlobOfStuff
658
+ :noindex:
659
+
660
+ This object has a bunch of disparate and unrelated things in it.
661
+
662
+ :memb int Birthday: Your birthday, represented in seconds since the
663
+ UNIX epoch.
664
+ :memb [string] Fav-Foods: A list of your favorite foods.
665
+ :memb boolean? Bizarre-Docs: True if the documentation reference
666
+ should be strange.
667
+
668
+
669
+qapi:alternate
670
+--------------
671
+
672
+This directive documents a QAPI alternate. It may use any of the
673
+standard Sphinx or QAPI options, and the documentation body may contain
674
+``:alt:`` or ``:feat:`` info field list entries.
675
+
676
+Example::
677
+
678
+ .. qapi:alternate:: ErrorCode
679
+
680
+ This alternate represents an Error Code from the VM.
681
+
682
+ :alt int ec: An error code, like the type you're used to.
683
+ :alt string em: An expletive-laced error message, if your
684
+ computer is feeling particularly cranky and tired of your
685
+ antics.
686
+
687
+Will be rendered as:
688
+
689
+.. qapi:alternate:: ErrorCode
690
+ :noindex:
691
+
692
+ This alternate represents an Error Code from the VM.
693
+
694
+ :alt int ec: An error code, like the type you're used to.
695
+ :alt string em: An expletive-laced error message, if your
696
+ computer is feeling particularly cranky and tired of your
697
+ antics.
698
--
699
2.48.1
diff view generated by jsdifflib