QAPI patches patches for 2025-03-11
-----BEGIN PGP SIGNATURE----- iQJGBAABCAAwFiEENUvIs9frKmtoZ05fOHC0AOuRhlMFAmfQCnkSHGFybWJydUBy ZWRoYXQuY29tAAoJEDhwtADrkYZTsJ0P/jcXiyFxjcbXN/3a6+iuPPqlviiWPAKG db2aHn2divceFEf7hUrwqjiJIPLDxaq6iJy71bjPUDkE8wAEdsf2zD7ryHo+sGcO rWaSaHmonn0QHvqcvkGGrbmTH+Ezl1RpP8XVGfG2lmHbjPQ3+EYnRwML6jC8dnvR C7qkyQ+qxmdV2lWb4MalgABKZToZ2aqnI9lr9KzHmN+55i2OxJrhECUKDHcgtG2i Pqc1GLGmmQ4Wj+4z0PyvKYZS4LP/90eH8bNyeA6TVsPHxgG79pencct7DOHxhc8q hHQ1TaqcBeWFQ7tndLMNDnHjm9XpAzMuew87xMTo6R450JxiSn+AkioTE0L563hy SjeXmIQ8COZbHsuSKlFJcV1OS1c/mJbwpkxptyaMLjTt2Lp9geFs39WKWHcs8pCN EmWSdvoqmP7D4bp1hXAVSPIIvJ7L2NwnM8ONH0KmRD5uMQrjiHsfvyWHAVnT10yu 8822hjlJp7l3B1QCi19mTlkiztCFScjb3Se8A+jScP5iX0q9C4H4t+tAw2m4UY1V pvn4xFxV82CvR3uQI0OMTKhp0/eEfvBioA1PEXOegPH5cS/L7YFF59mta1dCnaL7 0JRRCsTAnwAAAXoEteGqF1/6tXBdOnroL0OvHXJQVb2HH5c5YTnuxMiQywcP6Jty wt1vl42jfTj1 =Gt4B -----END PGP SIGNATURE----- Merge tag 'pull-qapi-2025-03-11' of https://repo.or.cz/qemu/armbru into staging QAPI patches patches for 2025-03-11 # -----BEGIN PGP SIGNATURE----- # # iQJGBAABCAAwFiEENUvIs9frKmtoZ05fOHC0AOuRhlMFAmfQCnkSHGFybWJydUBy # ZWRoYXQuY29tAAoJEDhwtADrkYZTsJ0P/jcXiyFxjcbXN/3a6+iuPPqlviiWPAKG # db2aHn2divceFEf7hUrwqjiJIPLDxaq6iJy71bjPUDkE8wAEdsf2zD7ryHo+sGcO # rWaSaHmonn0QHvqcvkGGrbmTH+Ezl1RpP8XVGfG2lmHbjPQ3+EYnRwML6jC8dnvR # C7qkyQ+qxmdV2lWb4MalgABKZToZ2aqnI9lr9KzHmN+55i2OxJrhECUKDHcgtG2i # Pqc1GLGmmQ4Wj+4z0PyvKYZS4LP/90eH8bNyeA6TVsPHxgG79pencct7DOHxhc8q # hHQ1TaqcBeWFQ7tndLMNDnHjm9XpAzMuew87xMTo6R450JxiSn+AkioTE0L563hy # SjeXmIQ8COZbHsuSKlFJcV1OS1c/mJbwpkxptyaMLjTt2Lp9geFs39WKWHcs8pCN # EmWSdvoqmP7D4bp1hXAVSPIIvJ7L2NwnM8ONH0KmRD5uMQrjiHsfvyWHAVnT10yu # 8822hjlJp7l3B1QCi19mTlkiztCFScjb3Se8A+jScP5iX0q9C4H4t+tAw2m4UY1V # pvn4xFxV82CvR3uQI0OMTKhp0/eEfvBioA1PEXOegPH5cS/L7YFF59mta1dCnaL7 # 0JRRCsTAnwAAAXoEteGqF1/6tXBdOnroL0OvHXJQVb2HH5c5YTnuxMiQywcP6Jty # wt1vl42jfTj1 # =Gt4B # -----END PGP SIGNATURE----- # gpg: Signature made Tue 11 Mar 2025 18:03:37 HKT # gpg: using RSA key 354BC8B3D7EB2A6B68674E5F3870B400EB918653 # gpg: issuer "armbru@redhat.com" # gpg: Good signature from "Markus Armbruster <armbru@redhat.com>" [full] # gpg: aka "Markus Armbruster <armbru@pond.sub.org>" [full] # Primary key fingerprint: 354B C8B3 D7EB 2A6B 6867 4E5F 3870 B400 EB91 8653 * tag 'pull-qapi-2025-03-11' of https://repo.or.cz/qemu/armbru: (61 commits) scripts/qapi/backend: Clean up create_backend()'s failure mode MAINTAINERS: Add jsnow as maintainer for Sphinx documentation docs: add qapi-domain syntax documentation docs: enable qapidoc transmogrifier for QEMU QMP Reference docs: disambiguate cross-references qapi/parser: add undocumented stub members to all_sections docs/qapidoc: generate entries for undocumented members docs/qapidoc: Add "the members of" pointers docs/qapidoc: add intermediate output debugger docs/qapidoc: process @foo into ``foo`` docs/qapidoc: implement transmogrify() method docs/qapidoc: add visit_entity() docs/qapidoc: add visit_sections() method docs/qapidoc: add visit_member() method docs/qapidoc: add visit_returns() method docs/qapidoc: prepare to record entity being transmogrified docs/qapidoc: add visit_feature() method docs/qapidoc: add add_field() and generate_field() helper methods docs/qapidoc: add format_type() method docs/qapidoc: add visit_errors() method ... Signed-off-by: Stefan Hajnoczi <stefanha@redhat.com>
This commit is contained in:
commit
94d689d0c6
@ -4325,6 +4325,7 @@ S: Orphan
|
|||||||
F: po/*.po
|
F: po/*.po
|
||||||
|
|
||||||
Sphinx documentation configuration and build machinery
|
Sphinx documentation configuration and build machinery
|
||||||
|
M: John Snow <jsnow@redhat.com>
|
||||||
M: Peter Maydell <peter.maydell@linaro.org>
|
M: Peter Maydell <peter.maydell@linaro.org>
|
||||||
S: Maintained
|
S: Maintained
|
||||||
F: docs/conf.py
|
F: docs/conf.py
|
||||||
|
18
docs/conf.py
18
docs/conf.py
@ -60,7 +60,14 @@ needs_sphinx = '3.4.3'
|
|||||||
# Add any Sphinx extension module names here, as strings. They can be
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
# ones.
|
# ones.
|
||||||
extensions = ['kerneldoc', 'qmp_lexer', 'hxtool', 'depfile', 'qapidoc']
|
extensions = [
|
||||||
|
'depfile',
|
||||||
|
'hxtool',
|
||||||
|
'kerneldoc',
|
||||||
|
'qapi_domain',
|
||||||
|
'qapidoc',
|
||||||
|
'qmp_lexer',
|
||||||
|
]
|
||||||
|
|
||||||
if sphinx.version_info[:3] > (4, 0, 0):
|
if sphinx.version_info[:3] > (4, 0, 0):
|
||||||
tags.add('sphinx4')
|
tags.add('sphinx4')
|
||||||
@ -146,6 +153,15 @@ rst_epilog = ".. |CONFDIR| replace:: ``" + confdir + "``\n"
|
|||||||
with open(os.path.join(qemu_docdir, 'defs.rst.inc')) as f:
|
with open(os.path.join(qemu_docdir, 'defs.rst.inc')) as f:
|
||||||
rst_epilog += f.read()
|
rst_epilog += f.read()
|
||||||
|
|
||||||
|
|
||||||
|
# Normally, the QAPI domain is picky about what field lists you use to
|
||||||
|
# describe a QAPI entity. If you'd like to use arbitrary additional
|
||||||
|
# fields in source documentation, add them here.
|
||||||
|
qapi_allowed_fields = {
|
||||||
|
"see also",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output ----------------------------------------------
|
# -- Options for HTML output ----------------------------------------------
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
@ -23,7 +23,7 @@ Some of the main QEMU subsystems are:
|
|||||||
- `Devices<device-emulation>` & Board models
|
- `Devices<device-emulation>` & Board models
|
||||||
- `Documentation <documentation-root>`
|
- `Documentation <documentation-root>`
|
||||||
- `GDB support<GDB usage>`
|
- `GDB support<GDB usage>`
|
||||||
- `Migration<migration>`
|
- :ref:`Migration<migration>`
|
||||||
- `Monitor<QEMU monitor>`
|
- `Monitor<QEMU monitor>`
|
||||||
- :ref:`QOM (QEMU Object Model)<qom>`
|
- :ref:`QOM (QEMU Object Model)<qom>`
|
||||||
- `System mode<System emulation>`
|
- `System mode<System emulation>`
|
||||||
@ -112,7 +112,7 @@ yet, so sometimes the source code is all you have.
|
|||||||
* `libdecnumber <https://gitlab.com/qemu-project/qemu/-/tree/master/libdecnumber>`_:
|
* `libdecnumber <https://gitlab.com/qemu-project/qemu/-/tree/master/libdecnumber>`_:
|
||||||
Import of gcc library, used to implement decimal number arithmetic.
|
Import of gcc library, used to implement decimal number arithmetic.
|
||||||
* `migration <https://gitlab.com/qemu-project/qemu/-/tree/master/migration>`__:
|
* `migration <https://gitlab.com/qemu-project/qemu/-/tree/master/migration>`__:
|
||||||
`Migration framework <migration>`.
|
:ref:`Migration framework <migration>`.
|
||||||
* `monitor <https://gitlab.com/qemu-project/qemu/-/tree/master/monitor>`_:
|
* `monitor <https://gitlab.com/qemu-project/qemu/-/tree/master/monitor>`_:
|
||||||
`Monitor <QEMU monitor>` implementation (HMP & QMP).
|
`Monitor <QEMU monitor>` implementation (HMP & QMP).
|
||||||
* `nbd <https://gitlab.com/qemu-project/qemu/-/tree/master/nbd>`_:
|
* `nbd <https://gitlab.com/qemu-project/qemu/-/tree/master/nbd>`_:
|
||||||
@ -193,7 +193,7 @@ yet, so sometimes the source code is all you have.
|
|||||||
- `lcitool <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/lcitool>`_:
|
- `lcitool <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/lcitool>`_:
|
||||||
Generate dockerfiles for CI containers.
|
Generate dockerfiles for CI containers.
|
||||||
- `migration <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/migration>`_:
|
- `migration <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/migration>`_:
|
||||||
Test scripts and data for `Migration framework <migration>`.
|
Test scripts and data for :ref:`Migration framework <migration>`.
|
||||||
- `multiboot <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/multiboot>`_:
|
- `multiboot <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/multiboot>`_:
|
||||||
Test multiboot functionality for x86_64/i386.
|
Test multiboot functionality for x86_64/i386.
|
||||||
- `qapi-schema <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/qapi-schema>`_:
|
- `qapi-schema <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/qapi-schema>`_:
|
||||||
|
@ -12,4 +12,5 @@ some of the basics if you are adding new files and targets to the build.
|
|||||||
kconfig
|
kconfig
|
||||||
docs
|
docs
|
||||||
qapi-code-gen
|
qapi-code-gen
|
||||||
|
qapi-domain
|
||||||
control-flow-integrity
|
control-flow-integrity
|
||||||
|
670
docs/devel/qapi-domain.rst
Normal file
670
docs/devel/qapi-domain.rst
Normal file
@ -0,0 +1,670 @@
|
|||||||
|
======================
|
||||||
|
The Sphinx QAPI Domain
|
||||||
|
======================
|
||||||
|
|
||||||
|
An extension to the `rST syntax
|
||||||
|
<https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html>`_
|
||||||
|
in Sphinx is provided by the QAPI Domain, located in
|
||||||
|
``docs/sphinx/qapi_domain.py``. This extension is analogous to the
|
||||||
|
`Python Domain
|
||||||
|
<https://www.sphinx-doc.org/en/master/usage/domains/python.html>`_
|
||||||
|
included with Sphinx, but provides special directives and roles
|
||||||
|
speciically for annotating and documenting QAPI definitions
|
||||||
|
specifically.
|
||||||
|
|
||||||
|
A `Domain
|
||||||
|
<https://www.sphinx-doc.org/en/master/usage/domains/index.html>`_
|
||||||
|
provides a set of special rST directives and cross-referencing roles to
|
||||||
|
Sphinx for understanding rST markup written to document a specific
|
||||||
|
language. By itself, this QAPI extension is only sufficient to parse rST
|
||||||
|
markup written by hand; the `autodoc
|
||||||
|
<https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html>`_
|
||||||
|
functionality is provided elsewhere, in ``docs/sphinx/qapidoc.py``, by
|
||||||
|
the "Transmogrifier".
|
||||||
|
|
||||||
|
It is not expected that any developer nor documentation writer would
|
||||||
|
never need to write *nor* read these special rST forms. However, in the
|
||||||
|
event that something needs to be debugged, knowing the syntax of the
|
||||||
|
domain is quite handy. This reference may also be useful as a guide for
|
||||||
|
understanding the QAPI Domain extension code itself. Although most of
|
||||||
|
these forms will not be needed for documentation writing purposes,
|
||||||
|
understanding the cross-referencing syntax *will* be helpful when
|
||||||
|
writing rST documentation elsewhere, or for enriching the body of
|
||||||
|
QAPIDoc blocks themselves.
|
||||||
|
|
||||||
|
|
||||||
|
Concepts
|
||||||
|
========
|
||||||
|
|
||||||
|
The QAPI Domain itself provides no mechanisms for reading the QAPI
|
||||||
|
Schema or generating documentation from code that exists. It is merely
|
||||||
|
the rST syntax used to describe things. For instance, the Sphinx Python
|
||||||
|
domain adds syntax like ``:py:func:`` for describing Python functions in
|
||||||
|
documentation, but it's the autodoc module that is responsible for
|
||||||
|
reading python code and generating such syntax. QAPI is analagous here:
|
||||||
|
qapidoc.py is responsible for reading the QAPI Schema and generating rST
|
||||||
|
syntax, and qapi_domain.py is responsible for translating that special
|
||||||
|
syntax and providing APIs for Sphinx internals.
|
||||||
|
|
||||||
|
In other words:
|
||||||
|
|
||||||
|
qapi_domain.py adds syntax like ``.. qapi:command::`` to Sphinx, and
|
||||||
|
qapidoc.py transforms the documentation in ``qapi/*.json`` into rST
|
||||||
|
using directives defined by the domain.
|
||||||
|
|
||||||
|
Or even shorter:
|
||||||
|
|
||||||
|
``:py:`` is to ``:qapi:`` as *autodoc* is to *qapidoc*.
|
||||||
|
|
||||||
|
|
||||||
|
Info Field Lists
|
||||||
|
================
|
||||||
|
|
||||||
|
`Field lists
|
||||||
|
<https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#field-lists>`_
|
||||||
|
are a standard syntax in reStructuredText. Sphinx `extends that syntax
|
||||||
|
<https://www.sphinx-doc.org/en/master/usage/domains/python.html#info-field-lists>`_
|
||||||
|
to give certain field list entries special meaning and parsing to, for
|
||||||
|
example, add cross-references. The QAPI Domain takes advantage of this
|
||||||
|
field list extension to document things like Arguments, Members, Values,
|
||||||
|
and so on.
|
||||||
|
|
||||||
|
The special parsing and handling of info field lists in Sphinx is provided by
|
||||||
|
three main classes; Field, GroupedField, and TypedField. The behavior
|
||||||
|
and formatting for each configured field list entry in the domain
|
||||||
|
changes depending on which class is used.
|
||||||
|
|
||||||
|
Field:
|
||||||
|
* Creates an ungrouped field: i.e., each entry will create its own
|
||||||
|
section and they will not be combined.
|
||||||
|
* May *optionally* support an argument.
|
||||||
|
* May apply cross-reference roles to *either* the argument *or* the
|
||||||
|
content body, both, or neither.
|
||||||
|
|
||||||
|
This is used primarily for entries which are not expected to be
|
||||||
|
repeated, i.e., items that may only show up at most once. The QAPI
|
||||||
|
domain uses this class for "Errors" section.
|
||||||
|
|
||||||
|
GroupedField:
|
||||||
|
* Creates a grouped field: i.e. multiple adjacent entries will be
|
||||||
|
merged into one section, and the content will form a bulleted list.
|
||||||
|
* *Must* take an argument.
|
||||||
|
* May optionally apply a cross-reference role to the argument, but not
|
||||||
|
the body.
|
||||||
|
* Can be configured to remove the bulleted list if there is only a
|
||||||
|
single entry.
|
||||||
|
* All items will be generated with the form: "argument -- body"
|
||||||
|
|
||||||
|
This is used for entries which are expected to be repeated, but aren't
|
||||||
|
expected to have two arguments, i.e. types without names, or names
|
||||||
|
without types. The QAPI domain uses this class for features, returns,
|
||||||
|
and enum values.
|
||||||
|
|
||||||
|
TypedField:
|
||||||
|
* Creates a grouped, typed field. Multiple adjacent entres will be
|
||||||
|
merged into one section, and the content will form a bulleted list.
|
||||||
|
* *Must* take at least one argument, but supports up to two -
|
||||||
|
nominally, a name and a type.
|
||||||
|
* May optionally apply a cross-reference role to the type or the name
|
||||||
|
argument, but not the body.
|
||||||
|
* Can be configured to remove the bulleted list if there is only a
|
||||||
|
single entry.
|
||||||
|
* All items will be generated with the form "name (type) -- body"
|
||||||
|
|
||||||
|
This is used for entries that are expected to be repeated and will have
|
||||||
|
a name, a type, and a description. The QAPI domain uses this class for
|
||||||
|
arguments, alternatives, and members. Wherever type names are referenced
|
||||||
|
below, They must be a valid, documented type that will be
|
||||||
|
cross-referenced in the HTML output; or one of the built-in JSON types
|
||||||
|
(string, number, int, boolean, null, value, q_empty).
|
||||||
|
|
||||||
|
|
||||||
|
``:feat:``
|
||||||
|
----------
|
||||||
|
|
||||||
|
Document a feature attached to a QAPI definition.
|
||||||
|
|
||||||
|
:availability: This field list is available in the body of Command,
|
||||||
|
Event, Enum, Object and Alternate directives.
|
||||||
|
:syntax: ``:feat name: Lorem ipsum, dolor sit amet...``
|
||||||
|
:type: `sphinx.util.docfields.GroupedField
|
||||||
|
<https://pydoc.dev/sphinx/latest/sphinx.util.docfields.GroupedField.html?private=1>`_
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
.. qapi:object:: BlockdevOptionsVirtioBlkVhostVdpa
|
||||||
|
:since: 7.2
|
||||||
|
:ifcond: CONFIG_BLKIO
|
||||||
|
|
||||||
|
Driver specific block device options for the virtio-blk-vhost-vdpa
|
||||||
|
backend.
|
||||||
|
|
||||||
|
:memb string path: path to the vhost-vdpa character device.
|
||||||
|
:feat fdset: Member ``path`` supports the special "/dev/fdset/N" path
|
||||||
|
(since 8.1)
|
||||||
|
|
||||||
|
|
||||||
|
``:arg:``
|
||||||
|
---------
|
||||||
|
|
||||||
|
Document an argument to a QAPI command.
|
||||||
|
|
||||||
|
:availability: This field list is only available in the body of the
|
||||||
|
Command directive.
|
||||||
|
:syntax: ``:arg type name: description``
|
||||||
|
:type: `sphinx.util.docfields.TypedField
|
||||||
|
<https://pydoc.dev/sphinx/latest/sphinx.util.docfields.TypedField.html?private=1>`_
|
||||||
|
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
.. qapi:command:: job-pause
|
||||||
|
:since: 3.0
|
||||||
|
|
||||||
|
Pause an active job.
|
||||||
|
|
||||||
|
This command returns immediately after marking the active job for
|
||||||
|
pausing. Pausing an already paused job is an error.
|
||||||
|
|
||||||
|
The job will pause as soon as possible, which means transitioning
|
||||||
|
into the PAUSED state if it was RUNNING, or into STANDBY if it was
|
||||||
|
READY. The corresponding JOB_STATUS_CHANGE event will be emitted.
|
||||||
|
|
||||||
|
Cancelling a paused job automatically resumes it.
|
||||||
|
|
||||||
|
:arg string id: The job identifier.
|
||||||
|
|
||||||
|
|
||||||
|
``:error:``
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Document the error condition(s) of a QAPI command.
|
||||||
|
|
||||||
|
:availability: This field list is only available in the body of the
|
||||||
|
Command directive.
|
||||||
|
:syntax: ``:error: Lorem ipsum dolor sit amet ...``
|
||||||
|
:type: `sphinx.util.docfields.Field
|
||||||
|
<https://pydoc.dev/sphinx/latest/sphinx.util.docfields.Field.html?private=1>`_
|
||||||
|
|
||||||
|
The format of the :errors: field list description is free-form rST. The
|
||||||
|
alternative spelling ":errors:" is also permitted, but strictly
|
||||||
|
analogous.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
.. qapi:command:: block-job-set-speed
|
||||||
|
:since: 1.1
|
||||||
|
|
||||||
|
Set maximum speed for a background block operation.
|
||||||
|
|
||||||
|
This command can only be issued when there is an active block job.
|
||||||
|
|
||||||
|
Throttling can be disabled by setting the speed to 0.
|
||||||
|
|
||||||
|
:arg string device: The job identifier. This used to be a device
|
||||||
|
name (hence the name of the parameter), but since QEMU 2.7 it
|
||||||
|
can have other values.
|
||||||
|
:arg int speed: the maximum speed, in bytes per second, or 0 for
|
||||||
|
unlimited. Defaults to 0.
|
||||||
|
:error:
|
||||||
|
- If no background operation is active on this device,
|
||||||
|
DeviceNotActive
|
||||||
|
|
||||||
|
|
||||||
|
``:return:``
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Document the return type(s) and value(s) of a QAPI command.
|
||||||
|
|
||||||
|
:availability: This field list is only available in the body of the
|
||||||
|
Command directive.
|
||||||
|
:syntax: ``:return type: Lorem ipsum dolor sit amet ...``
|
||||||
|
:type: `sphinx.util.docfields.GroupedField
|
||||||
|
<https://pydoc.dev/sphinx/latest/sphinx.util.docfields.GroupedField.html?private=1>`_
|
||||||
|
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
.. qapi:command:: query-replay
|
||||||
|
:since: 5.2
|
||||||
|
|
||||||
|
Retrieve the record/replay information. It includes current
|
||||||
|
instruction count which may be used for ``replay-break`` and
|
||||||
|
``replay-seek`` commands.
|
||||||
|
|
||||||
|
:return ReplayInfo: record/replay information.
|
||||||
|
|
||||||
|
.. qmp-example::
|
||||||
|
|
||||||
|
-> { "execute": "query-replay" }
|
||||||
|
<- { "return": {
|
||||||
|
"mode": "play", "filename": "log.rr", "icount": 220414 }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
``:value:``
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Document a possible value for a QAPI enum.
|
||||||
|
|
||||||
|
:availability: This field list is only available in the body of the Enum
|
||||||
|
directive.
|
||||||
|
:syntax: ``:value name: Lorem ipsum, dolor sit amet ...``
|
||||||
|
:type: `sphinx.util.docfields.GroupedField
|
||||||
|
<https://pydoc.dev/sphinx/latest/sphinx.util.docfields.GroupedField.html?private=1>`_
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
.. qapi:enum:: QapiErrorClass
|
||||||
|
:since: 1.2
|
||||||
|
|
||||||
|
QEMU error classes
|
||||||
|
|
||||||
|
:value GenericError: this is used for errors that don't require a specific
|
||||||
|
error class. This should be the default case for most errors
|
||||||
|
:value CommandNotFound: the requested command has not been found
|
||||||
|
:value DeviceNotActive: a device has failed to be become active
|
||||||
|
:value DeviceNotFound: the requested device has not been found
|
||||||
|
:value KVMMissingCap: the requested operation can't be fulfilled because a
|
||||||
|
required KVM capability is missing
|
||||||
|
|
||||||
|
|
||||||
|
``:alt:``
|
||||||
|
------------
|
||||||
|
|
||||||
|
Document a possible branch for a QAPI alternate.
|
||||||
|
|
||||||
|
:availability: This field list is only available in the body of the
|
||||||
|
Alternate directive.
|
||||||
|
:syntax: ``:alt type name: Lorem ipsum, dolor sit amet ...``
|
||||||
|
:type: `sphinx.util.docfields.TypedField
|
||||||
|
<https://pydoc.dev/sphinx/latest/sphinx.util.docfields.TypedField.html?private=1>`_
|
||||||
|
|
||||||
|
As a limitation of Sphinx, we must document the "name" of the branch in
|
||||||
|
addition to the type, even though this information is not visible on the
|
||||||
|
wire in the QMP protocol format. This limitation *may* be lifted at a
|
||||||
|
future date.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
.. qapi:alternate:: StrOrNull
|
||||||
|
:since: 2.10
|
||||||
|
|
||||||
|
This is a string value or the explicit lack of a string (null
|
||||||
|
pointer in C). Intended for cases when 'optional absent' already
|
||||||
|
has a different meaning.
|
||||||
|
|
||||||
|
:alt string s: the string value
|
||||||
|
:alt null n: no string value
|
||||||
|
|
||||||
|
|
||||||
|
``:memb:``
|
||||||
|
----------
|
||||||
|
|
||||||
|
Document a member of an Event or Object.
|
||||||
|
|
||||||
|
:availability: This field list is available in the body of Event or
|
||||||
|
Object directives.
|
||||||
|
:syntax: ``:memb type name: Lorem ipsum, dolor sit amet ...``
|
||||||
|
:type: `sphinx.util.docfields.TypedField
|
||||||
|
<https://pydoc.dev/sphinx/latest/sphinx.util.docfields.TypedField.html?private=1>`_
|
||||||
|
|
||||||
|
This is fundamentally the same as ``:arg:`` and ``:alt:``, but uses the
|
||||||
|
"Members" phrasing for Events and Objects (Structs and Unions).
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
.. qapi:event:: JOB_STATUS_CHANGE
|
||||||
|
:since: 3.0
|
||||||
|
|
||||||
|
Emitted when a job transitions to a different status.
|
||||||
|
|
||||||
|
:memb string id: The job identifier
|
||||||
|
:memb JobStatus status: The new job status
|
||||||
|
|
||||||
|
|
||||||
|
Arbitrary field lists
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Other field list names, while valid rST syntax, are prohibited inside of
|
||||||
|
QAPI directives to help prevent accidental misspellings of info field
|
||||||
|
list names. If you want to add a new arbitrary "non-value-added" field
|
||||||
|
list to QAPI documentation, you must add the field name to the allow
|
||||||
|
list in ``docs/conf.py``
|
||||||
|
|
||||||
|
For example::
|
||||||
|
|
||||||
|
qapi_allowed_fields = {
|
||||||
|
"see also",
|
||||||
|
}
|
||||||
|
|
||||||
|
Will allow you to add arbitrary field lists in QAPI directives::
|
||||||
|
|
||||||
|
.. qapi:command:: x-fake-command
|
||||||
|
|
||||||
|
:see also: Lorem ipsum, dolor sit amet ...
|
||||||
|
|
||||||
|
|
||||||
|
Cross-references
|
||||||
|
================
|
||||||
|
|
||||||
|
Cross-reference `roles
|
||||||
|
<https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html>`_
|
||||||
|
in the QAPI domain are modeled closely after the `Python
|
||||||
|
cross-referencing syntax
|
||||||
|
<https://www.sphinx-doc.org/en/master/usage/domains/python.html#cross-referencing-python-objects>`_.
|
||||||
|
|
||||||
|
QAPI definitions can be referenced using the standard `any
|
||||||
|
<https://www.sphinx-doc.org/en/master/usage/referencing.html#role-any>`_
|
||||||
|
role cross-reference syntax, such as with ```query-blockstats```. In
|
||||||
|
the event that disambiguation is needed, cross-references can also be
|
||||||
|
written using a number of explicit cross-reference roles:
|
||||||
|
|
||||||
|
* ``:qapi:mod:`block-core``` -- Reference a QAPI module. The link will
|
||||||
|
take you to the beginning of that section in the documentation.
|
||||||
|
* ``:qapi:cmd:`query-block``` -- Reference a QAPI command.
|
||||||
|
* ``:qapi:event:`JOB_STATUS_CHANGE``` -- Reference a QAPI event.
|
||||||
|
* ``:qapi:enum:`QapiErrorClass``` -- Reference a QAPI enum.
|
||||||
|
* ``:qapi:obj:`BlockdevOptionsVirtioBlkVhostVdpa`` -- Reference a QAPI
|
||||||
|
object (struct or union)
|
||||||
|
* ``:qapi:alt:`StrOrNull``` -- Reference a QAPI alternate.
|
||||||
|
* ``:qapi:type:`BlockDirtyInfo``` -- Reference *any* QAPI type; this
|
||||||
|
excludes modules, commands, and events.
|
||||||
|
* ``:qapi:any:`block-job-set-speed``` -- Reference absolutely any QAPI entity.
|
||||||
|
|
||||||
|
Type arguments in info field lists are converted into references as if
|
||||||
|
you had used the ``:qapi:type:`` role. All of the special syntax below
|
||||||
|
applies to both info field lists and standalone explicit
|
||||||
|
cross-references.
|
||||||
|
|
||||||
|
|
||||||
|
Type decorations
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Type names in references can be surrounded by brackets, like
|
||||||
|
``[typename]``, to indicate an array of that type. The cross-reference
|
||||||
|
will apply only to the type name between the brackets. For example;
|
||||||
|
``:qapi:type:`[Qcow2BitmapInfoFlags]``` renders to:
|
||||||
|
:qapi:type:`[Qcow2BitmapInfoFlags]`
|
||||||
|
|
||||||
|
To indicate an optional argument/member in a field list, the type name
|
||||||
|
can be suffixed with ``?``. The cross-reference will be transformed to
|
||||||
|
"type, Optional" with the link applying only to the type name. For
|
||||||
|
example; ``:qapi:type:`BitmapSyncMode?``` renders to:
|
||||||
|
:qapi:type:`BitmapSyncMode?`
|
||||||
|
|
||||||
|
|
||||||
|
Namespaces
|
||||||
|
----------
|
||||||
|
|
||||||
|
Mimicking the `Python domain target specification syntax
|
||||||
|
<https://www.sphinx-doc.org/en/master/usage/domains/python.html#target-specification>`_,
|
||||||
|
QAPI allows you to specify the fully qualified path for a data
|
||||||
|
type. QAPI enforces globally unique names, so it's unlikely you'll need
|
||||||
|
this specific feature, but it may be extended in the near future to
|
||||||
|
allow referencing identically named commands and data types from
|
||||||
|
different utilities; i.e. QEMU Storage Daemon vs QMP.
|
||||||
|
|
||||||
|
* A module can be explicitly provided;
|
||||||
|
``:qapi:type:`block-core.BitmapSyncMode``` will render to:
|
||||||
|
:qapi:type:`block-core.BitmapSyncMode`
|
||||||
|
* If you don't want to display the "fully qualified" name, it can be
|
||||||
|
prefixed with a tilde; ``:qapi:type:`~block-core.BitmapSyncMode```
|
||||||
|
will render to: :qapi:type:`~block-core.BitmapSyncMode`
|
||||||
|
|
||||||
|
|
||||||
|
Custom link text
|
||||||
|
----------------
|
||||||
|
|
||||||
|
The name of a cross-reference link can be explicitly overridden like
|
||||||
|
`most stock Sphinx references
|
||||||
|
<https://www.sphinx-doc.org/en/master/usage/referencing.html#syntax>`_
|
||||||
|
using the ``custom text <target>`` syntax.
|
||||||
|
|
||||||
|
For example, ``:qapi:cmd:`Merge dirty bitmaps
|
||||||
|
<block-dirty-bitmap-merge>``` will render as: :qapi:cmd:`Merge dirty
|
||||||
|
bitmaps <block-dirty-bitmap-merge>`
|
||||||
|
|
||||||
|
|
||||||
|
Directives
|
||||||
|
==========
|
||||||
|
|
||||||
|
The QAPI domain adds a number of custom directives for documenting
|
||||||
|
various QAPI/QMP entities. The syntax is plain rST, and follows this
|
||||||
|
general format::
|
||||||
|
|
||||||
|
.. qapi:directive:: argument
|
||||||
|
:option:
|
||||||
|
:another-option: with an argument
|
||||||
|
|
||||||
|
Content body, arbitrary rST is allowed here.
|
||||||
|
|
||||||
|
|
||||||
|
Sphinx standard options
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
All QAPI directives inherit a number of `standard options
|
||||||
|
<https://www.sphinx-doc.org/en/master/usage/domains/index.html#basic-markup>`_
|
||||||
|
from Sphinx's ObjectDescription class.
|
||||||
|
|
||||||
|
The dashed spellings of the below options were added in Sphinx 7.2, the
|
||||||
|
undashed spellings are currently retained as aliases, but will be
|
||||||
|
removed in a future version.
|
||||||
|
|
||||||
|
* ``:no-index:`` and ``:noindex:`` -- Do not add this item into the
|
||||||
|
Index, and do not make it available for cross-referencing.
|
||||||
|
* ``no-index-entry:`` and ``:noindexentry:`` -- Do not add this item
|
||||||
|
into the Index, but allow it to be cross-referenced.
|
||||||
|
* ``no-contents-entry`` and ``:nocontentsentry:`` -- Exclude this item
|
||||||
|
from the Table of Contents.
|
||||||
|
* ``no-typesetting`` -- Create TOC, Index and cross-referencing
|
||||||
|
entities, but don't actually display the content.
|
||||||
|
|
||||||
|
|
||||||
|
QAPI standard options
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
All QAPI directives -- *except* for module -- support these common options.
|
||||||
|
|
||||||
|
* ``:module: modname`` -- Borrowed from the Python domain, this option allows
|
||||||
|
you to override the module association of a given definition.
|
||||||
|
* ``:since: x.y`` -- Allows the documenting of "Since" information, which is
|
||||||
|
displayed in the signature bar.
|
||||||
|
* ``:ifcond: CONDITION`` -- Allows the documenting of conditional availability
|
||||||
|
information, which is displayed in an eyecatch just below the
|
||||||
|
signature bar.
|
||||||
|
* ``:deprecated:`` -- Adds an eyecatch just below the signature bar that
|
||||||
|
advertises that this definition is deprecated and should be avoided.
|
||||||
|
* ``:unstable:`` -- Adds an eyecatch just below the signature bar that
|
||||||
|
advertises that this definition is unstable and should not be used in
|
||||||
|
production code.
|
||||||
|
|
||||||
|
|
||||||
|
qapi:module
|
||||||
|
-----------
|
||||||
|
|
||||||
|
The ``qapi:module`` directive marks the start of a QAPI module. It may have
|
||||||
|
a content body, but it can be omitted. All subsequent QAPI directives
|
||||||
|
are associated with the most recent module; this effects their "fully
|
||||||
|
qualified" name, but has no other effect.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
.. qapi:module:: block-core
|
||||||
|
|
||||||
|
Welcome to the block-core module!
|
||||||
|
|
||||||
|
Will be rendered as:
|
||||||
|
|
||||||
|
.. qapi:module:: block-core
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
Welcome to the block-core module!
|
||||||
|
|
||||||
|
|
||||||
|
qapi:command
|
||||||
|
------------
|
||||||
|
|
||||||
|
This directive documents a QMP command. It may use any of the standard
|
||||||
|
Sphinx or QAPI options, and the documentation body may contain
|
||||||
|
``:arg:``, ``:feat:``, ``:error:``, or ``:return:`` info field list
|
||||||
|
entries.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
.. qapi:command:: x-fake-command
|
||||||
|
:since: 42.0
|
||||||
|
:unstable:
|
||||||
|
|
||||||
|
This command is fake, so it can't hurt you!
|
||||||
|
|
||||||
|
:arg int foo: Your favorite number.
|
||||||
|
:arg string? bar: Your favorite season.
|
||||||
|
:return [string]: A lovely computer-written poem for you.
|
||||||
|
|
||||||
|
|
||||||
|
Will be rendered as:
|
||||||
|
|
||||||
|
.. qapi:command:: x-fake-command
|
||||||
|
:noindex:
|
||||||
|
:since: 42.0
|
||||||
|
:unstable:
|
||||||
|
|
||||||
|
This command is fake, so it can't hurt you!
|
||||||
|
|
||||||
|
:arg int foo: Your favorite number.
|
||||||
|
:arg string? bar: Your favorite season.
|
||||||
|
:return [string]: A lovely computer-written poem for you.
|
||||||
|
|
||||||
|
|
||||||
|
qapi:event
|
||||||
|
----------
|
||||||
|
|
||||||
|
This directive documents a QMP event. It may use any of the standard
|
||||||
|
Sphinx or QAPI options, and the documentation body may contain
|
||||||
|
``:memb:`` or ``:feat:`` info field list entries.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
.. qapi:event:: COMPUTER_IS_RUINED
|
||||||
|
:since: 0.1
|
||||||
|
:deprecated:
|
||||||
|
|
||||||
|
This event is emitted when your computer is *extremely* ruined.
|
||||||
|
|
||||||
|
:memb string reason: Diagnostics as to what caused your computer to
|
||||||
|
be ruined.
|
||||||
|
:feat sadness: When present, the diagnostic message will also
|
||||||
|
explain how sad the computer is as a result of your wrongdoings.
|
||||||
|
|
||||||
|
Will be rendered as:
|
||||||
|
|
||||||
|
.. qapi:event:: COMPUTER_IS_RUINED
|
||||||
|
:noindex:
|
||||||
|
:since: 0.1
|
||||||
|
:deprecated:
|
||||||
|
|
||||||
|
This event is emitted when your computer is *extremely* ruined.
|
||||||
|
|
||||||
|
:memb string reason: Diagnostics as to what caused your computer to
|
||||||
|
be ruined.
|
||||||
|
:feat sadness: When present, the diagnostic message will also explain
|
||||||
|
how sad the computer is as a result of your wrongdoings.
|
||||||
|
|
||||||
|
|
||||||
|
qapi:enum
|
||||||
|
---------
|
||||||
|
|
||||||
|
This directive documents a QAPI enum. It may use any of the standard
|
||||||
|
Sphinx or QAPI options, and the documentation body may contain
|
||||||
|
``:value:`` or ``:feat:`` info field list entries.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
.. qapi:enum:: Mood
|
||||||
|
:ifcond: LIB_PERSONALITY
|
||||||
|
|
||||||
|
This enum represents your virtual machine's current mood!
|
||||||
|
|
||||||
|
:value Happy: Your VM is content and well-fed.
|
||||||
|
:value Hungry: Your VM needs food.
|
||||||
|
:value Melancholic: Your VM is experiencing existential angst.
|
||||||
|
:value Petulant: Your VM is throwing a temper tantrum.
|
||||||
|
|
||||||
|
Will be rendered as:
|
||||||
|
|
||||||
|
.. qapi:enum:: Mood
|
||||||
|
:noindex:
|
||||||
|
:ifcond: LIB_PERSONALITY
|
||||||
|
|
||||||
|
This enum represents your virtual machine's current mood!
|
||||||
|
|
||||||
|
:value Happy: Your VM is content and well-fed.
|
||||||
|
:value Hungry: Your VM needs food.
|
||||||
|
:value Melancholic: Your VM is experiencing existential angst.
|
||||||
|
:value Petulant: Your VM is throwing a temper tantrum.
|
||||||
|
|
||||||
|
|
||||||
|
qapi:object
|
||||||
|
-----------
|
||||||
|
|
||||||
|
This directive documents a QAPI structure or union and represents a QMP
|
||||||
|
object. It may use any of the standard Sphinx or QAPI options, and the
|
||||||
|
documentation body may contain ``:memb:`` or ``:feat:`` info field list
|
||||||
|
entries.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
.. qapi:object:: BigBlobOfStuff
|
||||||
|
|
||||||
|
This object has a bunch of disparate and unrelated things in it.
|
||||||
|
|
||||||
|
:memb int Birthday: Your birthday, represented in seconds since the
|
||||||
|
UNIX epoch.
|
||||||
|
:memb [string] Fav-Foods: A list of your favorite foods.
|
||||||
|
:memb boolean? Bizarre-Docs: True if the documentation reference
|
||||||
|
should be strange.
|
||||||
|
|
||||||
|
Will be rendered as:
|
||||||
|
|
||||||
|
.. qapi:object:: BigBlobOfStuff
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
This object has a bunch of disparate and unrelated things in it.
|
||||||
|
|
||||||
|
:memb int Birthday: Your birthday, represented in seconds since the
|
||||||
|
UNIX epoch.
|
||||||
|
:memb [string] Fav-Foods: A list of your favorite foods.
|
||||||
|
:memb boolean? Bizarre-Docs: True if the documentation reference
|
||||||
|
should be strange.
|
||||||
|
|
||||||
|
|
||||||
|
qapi:alternate
|
||||||
|
--------------
|
||||||
|
|
||||||
|
This directive documents a QAPI alternate. It may use any of the
|
||||||
|
standard Sphinx or QAPI options, and the documentation body may contain
|
||||||
|
``:alt:`` or ``:feat:`` info field list entries.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
.. qapi:alternate:: ErrorCode
|
||||||
|
|
||||||
|
This alternate represents an Error Code from the VM.
|
||||||
|
|
||||||
|
:alt int ec: An error code, like the type you're used to.
|
||||||
|
:alt string em: An expletive-laced error message, if your
|
||||||
|
computer is feeling particularly cranky and tired of your
|
||||||
|
antics.
|
||||||
|
|
||||||
|
Will be rendered as:
|
||||||
|
|
||||||
|
.. qapi:alternate:: ErrorCode
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
This alternate represents an Error Code from the VM.
|
||||||
|
|
||||||
|
:alt int ec: An error code, like the type you're used to.
|
||||||
|
:alt string em: An expletive-laced error message, if your
|
||||||
|
computer is feeling particularly cranky and tired of your
|
||||||
|
antics.
|
@ -120,7 +120,7 @@ Migration
|
|||||||
---------
|
---------
|
||||||
|
|
||||||
QEMU can save and restore the execution of a virtual machine between different
|
QEMU can save and restore the execution of a virtual machine between different
|
||||||
host systems. This is provided by the `Migration framework<migration>`.
|
host systems. This is provided by the :ref:`Migration framework<migration>`.
|
||||||
|
|
||||||
NBD
|
NBD
|
||||||
---
|
---
|
||||||
@ -212,14 +212,14 @@ machine emulator and virtualizer.
|
|||||||
QOM
|
QOM
|
||||||
---
|
---
|
||||||
|
|
||||||
`QEMU Object Model <qom>` is an object oriented API used to define various
|
:ref:`QEMU Object Model <qom>` is an object oriented API used to define
|
||||||
devices and hardware in the QEMU codebase.
|
various devices and hardware in the QEMU codebase.
|
||||||
|
|
||||||
Record/replay
|
Record/replay
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
`Record/replay <replay>` is a feature of QEMU allowing to have a deterministic
|
:ref:`Record/replay <replay>` is a feature of QEMU allowing to have a
|
||||||
and reproducible execution of a virtual machine.
|
deterministic and reproducible execution of a virtual machine.
|
||||||
|
|
||||||
Rust
|
Rust
|
||||||
----
|
----
|
||||||
|
@ -7,3 +7,4 @@ QEMU QMP Reference Manual
|
|||||||
:depth: 3
|
:depth: 3
|
||||||
|
|
||||||
.. qapi-doc:: qapi/qapi-schema.json
|
.. qapi-doc:: qapi/qapi-schema.json
|
||||||
|
:transmogrify:
|
||||||
|
@ -18,8 +18,8 @@ h1, h2, .rst-content .toctree-wrapper p.caption, h3, h4, h5, h6, legend {
|
|||||||
|
|
||||||
.rst-content dl:not(.docutils) dt {
|
.rst-content dl:not(.docutils) dt {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
border-left: solid 3px #ccc;
|
border-left: solid 5px #bcc6d2;
|
||||||
background-color: #f0f0f0;
|
background-color: #eaedf1;
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,3 +208,97 @@ div[class^="highlight"] pre {
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* QAPI domain theming */
|
||||||
|
|
||||||
|
/* most content in a QAPI object definition should not eclipse about
|
||||||
|
80ch, but nested field lists are explicitly exempt due to their
|
||||||
|
two-column nature */
|
||||||
|
.qapi dd *:not(dl) {
|
||||||
|
max-width: 80ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* but the content column itself should still be less than ~80ch. */
|
||||||
|
.qapi .field-list dd {
|
||||||
|
max-width: 80ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qapi-infopips {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qapi-infopip {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0em 0.5em 0em 0.5em;
|
||||||
|
margin: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qapi-deprecated,.qapi-unstable {
|
||||||
|
background-color: #fffef5;
|
||||||
|
border: solid #fff176 6px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 15px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qapi-unstable::before {
|
||||||
|
content: '🚧 ';
|
||||||
|
}
|
||||||
|
|
||||||
|
.qapi-deprecated::before {
|
||||||
|
content: '⚠️ ';
|
||||||
|
}
|
||||||
|
|
||||||
|
.qapi-ifcond::before {
|
||||||
|
/* gaze ye into the crystal ball to determine feature availability */
|
||||||
|
content: '🔮 ';
|
||||||
|
}
|
||||||
|
|
||||||
|
.qapi-ifcond {
|
||||||
|
background-color: #f9f5ff;
|
||||||
|
border: solid #dac2ff 6px;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 15px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* code blocks */
|
||||||
|
.qapi div[class^="highlight"] {
|
||||||
|
width: fit-content;
|
||||||
|
background-color: #fffafd;
|
||||||
|
border: 2px solid #ffe1f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* note, warning, etc. */
|
||||||
|
.qapi .admonition {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* pad the top of the field-list so the text doesn't start directly at
|
||||||
|
the top border; primarily for the field list labels, but adjust the
|
||||||
|
field bodies as well for parity. */
|
||||||
|
dl.field-list > dt:first-of-type, dl.field-list > dd:first-of-type {
|
||||||
|
padding-top: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl.field-list > dt:last-of-type, dl.field-list > dd:last-of-type {
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* pad the field list labels so they don't crash into the border */
|
||||||
|
dl.field-list > dt {
|
||||||
|
padding-left: 0.5em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add a little padding between field list sections */
|
||||||
|
dl.field-list > dd:not(:last-child) {
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sphinx 3.x: unresolved xrefs */
|
||||||
|
.rst-content *:not(a) > code.xref {
|
||||||
|
font-weight: 400;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
230
docs/sphinx/compat.py
Normal file
230
docs/sphinx/compat.py
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
"""
|
||||||
|
Sphinx cross-version compatibility goop
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
Optional,
|
||||||
|
Type,
|
||||||
|
)
|
||||||
|
|
||||||
|
from docutils import nodes
|
||||||
|
from docutils.nodes import Element, Node, Text
|
||||||
|
from docutils.statemachine import StringList
|
||||||
|
|
||||||
|
import sphinx
|
||||||
|
from sphinx import addnodes, util
|
||||||
|
from sphinx.directives import ObjectDescription
|
||||||
|
from sphinx.environment import BuildEnvironment
|
||||||
|
from sphinx.roles import XRefRole
|
||||||
|
from sphinx.util import docfields
|
||||||
|
from sphinx.util.docutils import (
|
||||||
|
ReferenceRole,
|
||||||
|
SphinxDirective,
|
||||||
|
switch_source_input,
|
||||||
|
)
|
||||||
|
from sphinx.util.typing import TextlikeNode
|
||||||
|
|
||||||
|
|
||||||
|
MAKE_XREF_WORKAROUND = sphinx.version_info[:3] < (4, 1, 0)
|
||||||
|
|
||||||
|
|
||||||
|
SpaceNode: Callable[[str], Node]
|
||||||
|
KeywordNode: Callable[[str, str], Node]
|
||||||
|
|
||||||
|
if sphinx.version_info[:3] >= (4, 0, 0):
|
||||||
|
SpaceNode = addnodes.desc_sig_space
|
||||||
|
KeywordNode = addnodes.desc_sig_keyword
|
||||||
|
else:
|
||||||
|
SpaceNode = Text
|
||||||
|
KeywordNode = addnodes.desc_annotation
|
||||||
|
|
||||||
|
|
||||||
|
def nested_parse_with_titles(
|
||||||
|
directive: SphinxDirective, content_node: Element
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
This helper preserves error parsing context across sphinx versions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# necessary so that the child nodes get the right source/line set
|
||||||
|
content_node.document = directive.state.document
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Modern sphinx (6.2.0+) supports proper offsetting for
|
||||||
|
# nested parse error context management
|
||||||
|
util.nodes.nested_parse_with_titles(
|
||||||
|
directive.state,
|
||||||
|
directive.content,
|
||||||
|
content_node,
|
||||||
|
content_offset=directive.content_offset,
|
||||||
|
)
|
||||||
|
except TypeError:
|
||||||
|
# No content_offset argument. Fall back to SSI method.
|
||||||
|
with switch_source_input(directive.state, directive.content):
|
||||||
|
util.nodes.nested_parse_with_titles(
|
||||||
|
directive.state, directive.content, content_node
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ###########################################
|
||||||
|
# xref compatibility hacks for Sphinx < 4.1 #
|
||||||
|
# ###########################################
|
||||||
|
|
||||||
|
# When we require >= Sphinx 4.1, the following function and the
|
||||||
|
# subsequent 3 compatibility classes can be removed. Anywhere in
|
||||||
|
# qapi_domain that uses one of these Compat* types can be switched to
|
||||||
|
# using the garden-variety lib-provided classes with no trickery.
|
||||||
|
|
||||||
|
|
||||||
|
def _compat_make_xref( # pylint: disable=unused-argument
|
||||||
|
self: sphinx.util.docfields.Field,
|
||||||
|
rolename: str,
|
||||||
|
domain: str,
|
||||||
|
target: str,
|
||||||
|
innernode: Type[TextlikeNode] = addnodes.literal_emphasis,
|
||||||
|
contnode: Optional[Node] = None,
|
||||||
|
env: Optional[BuildEnvironment] = None,
|
||||||
|
inliner: Any = None,
|
||||||
|
location: Any = None,
|
||||||
|
) -> Node:
|
||||||
|
"""
|
||||||
|
Compatibility workaround for Sphinx versions prior to 4.1.0.
|
||||||
|
|
||||||
|
Older sphinx versions do not use the domain's XRefRole for parsing
|
||||||
|
and formatting cross-references, so we need to perform this magick
|
||||||
|
ourselves to avoid needing to write the parser/formatter in two
|
||||||
|
separate places.
|
||||||
|
|
||||||
|
This workaround isn't brick-for-brick compatible with modern Sphinx
|
||||||
|
versions, because we do not have access to the parent directive's
|
||||||
|
state during this parsing like we do in more modern versions.
|
||||||
|
|
||||||
|
It's no worse than what pre-Sphinx 4.1.0 does, so... oh well!
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Yes, this function is gross. Pre-4.1 support is a miracle.
|
||||||
|
# pylint: disable=too-many-locals
|
||||||
|
|
||||||
|
assert env
|
||||||
|
# Note: Sphinx's own code ignores the type warning here, too.
|
||||||
|
if not rolename:
|
||||||
|
return contnode or innernode(target, target) # type: ignore[call-arg]
|
||||||
|
|
||||||
|
# Get the role instance, but don't *execute it* - we lack the
|
||||||
|
# correct state to do so. Instead, we'll just use its public
|
||||||
|
# methods to do our reference formatting, and emulate the rest.
|
||||||
|
role = env.get_domain(domain).roles[rolename]
|
||||||
|
assert isinstance(role, XRefRole)
|
||||||
|
|
||||||
|
# XRefRole features not supported by this compatibility shim;
|
||||||
|
# these were not supported in Sphinx 3.x either, so nothing of
|
||||||
|
# value is really lost.
|
||||||
|
assert not target.startswith("!")
|
||||||
|
assert not re.match(ReferenceRole.explicit_title_re, target)
|
||||||
|
assert not role.lowercase
|
||||||
|
assert not role.fix_parens
|
||||||
|
|
||||||
|
# Code below based mostly on sphinx.roles.XRefRole; run() and
|
||||||
|
# create_xref_node()
|
||||||
|
options = {
|
||||||
|
"refdoc": env.docname,
|
||||||
|
"refdomain": domain,
|
||||||
|
"reftype": rolename,
|
||||||
|
"refexplicit": False,
|
||||||
|
"refwarn": role.warn_dangling,
|
||||||
|
}
|
||||||
|
refnode = role.nodeclass(target, **options)
|
||||||
|
title, target = role.process_link(env, refnode, False, target, target)
|
||||||
|
refnode["reftarget"] = target
|
||||||
|
classes = ["xref", domain, f"{domain}-{rolename}"]
|
||||||
|
refnode += role.innernodeclass(target, title, classes=classes)
|
||||||
|
|
||||||
|
# This is the very gross part of the hack. Normally,
|
||||||
|
# result_nodes takes a document object to which we would pass
|
||||||
|
# self.inliner.document. Prior to Sphinx 4.1, we don't *have* an
|
||||||
|
# inliner to pass, so we have nothing to pass here. However, the
|
||||||
|
# actual implementation of role.result_nodes in this case
|
||||||
|
# doesn't actually use that argument, so this winds up being
|
||||||
|
# ... fine. Rest easy at night knowing this code only runs under
|
||||||
|
# old versions of Sphinx, so at least it won't change in the
|
||||||
|
# future on us and lead to surprising new failures.
|
||||||
|
# Gross, I know.
|
||||||
|
result_nodes, _messages = role.result_nodes(
|
||||||
|
None, # type: ignore
|
||||||
|
env,
|
||||||
|
refnode,
|
||||||
|
is_ref=True,
|
||||||
|
)
|
||||||
|
return nodes.inline(target, "", *result_nodes)
|
||||||
|
|
||||||
|
|
||||||
|
class CompatField(docfields.Field):
|
||||||
|
if MAKE_XREF_WORKAROUND:
|
||||||
|
make_xref = _compat_make_xref
|
||||||
|
|
||||||
|
|
||||||
|
class CompatGroupedField(docfields.GroupedField):
|
||||||
|
if MAKE_XREF_WORKAROUND:
|
||||||
|
make_xref = _compat_make_xref
|
||||||
|
|
||||||
|
|
||||||
|
class CompatTypedField(docfields.TypedField):
|
||||||
|
if MAKE_XREF_WORKAROUND:
|
||||||
|
make_xref = _compat_make_xref
|
||||||
|
|
||||||
|
|
||||||
|
# ################################################################
|
||||||
|
# Nested parsing error location fix for Sphinx 5.3.0 < x < 6.2.0 #
|
||||||
|
# ################################################################
|
||||||
|
|
||||||
|
# When we require Sphinx 4.x, the TYPE_CHECKING hack where we avoid
|
||||||
|
# subscripting ObjectDescription at runtime can be removed in favor of
|
||||||
|
# just always subscripting the class.
|
||||||
|
|
||||||
|
# When we require Sphinx > 6.2.0, the rest of this compatibility hack
|
||||||
|
# can be dropped and QAPIObject can just inherit directly from
|
||||||
|
# ObjectDescription[Signature].
|
||||||
|
|
||||||
|
SOURCE_LOCATION_FIX = (5, 3, 0) <= sphinx.version_info[:3] < (6, 2, 0)
|
||||||
|
|
||||||
|
Signature = str
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
_BaseClass = ObjectDescription[Signature]
|
||||||
|
else:
|
||||||
|
_BaseClass = ObjectDescription
|
||||||
|
|
||||||
|
|
||||||
|
class ParserFix(_BaseClass):
|
||||||
|
|
||||||
|
_temp_content: StringList
|
||||||
|
_temp_offset: int
|
||||||
|
_temp_node: Optional[addnodes.desc_content]
|
||||||
|
|
||||||
|
def before_content(self) -> None:
|
||||||
|
# Work around a sphinx bug and parse the content ourselves.
|
||||||
|
self._temp_content = self.content
|
||||||
|
self._temp_offset = self.content_offset
|
||||||
|
self._temp_node = None
|
||||||
|
|
||||||
|
if SOURCE_LOCATION_FIX:
|
||||||
|
self._temp_node = addnodes.desc_content()
|
||||||
|
self.state.nested_parse(
|
||||||
|
self.content, self.content_offset, self._temp_node
|
||||||
|
)
|
||||||
|
# Sphinx will try to parse the content block itself,
|
||||||
|
# Give it nothingness to parse instead.
|
||||||
|
self.content = StringList()
|
||||||
|
self.content_offset = 0
|
||||||
|
|
||||||
|
def transform_content(self, content_node: addnodes.desc_content) -> None:
|
||||||
|
# Sphinx workaround: Inject our parsed content and restore state.
|
||||||
|
if self._temp_node:
|
||||||
|
content_node += self._temp_node.children
|
||||||
|
self.content = self._temp_content
|
||||||
|
self.content_offset = self._temp_offset
|
931
docs/sphinx/qapi_domain.py
Normal file
931
docs/sphinx/qapi_domain.py
Normal file
@ -0,0 +1,931 @@
|
|||||||
|
"""
|
||||||
|
QAPI domain extension.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The best laid plans of mice and men, ...
|
||||||
|
# pylint: disable=too-many-lines
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
AbstractSet,
|
||||||
|
Any,
|
||||||
|
Dict,
|
||||||
|
Iterable,
|
||||||
|
List,
|
||||||
|
NamedTuple,
|
||||||
|
Optional,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
|
|
||||||
|
from docutils import nodes
|
||||||
|
from docutils.parsers.rst import directives
|
||||||
|
|
||||||
|
from compat import (
|
||||||
|
CompatField,
|
||||||
|
CompatGroupedField,
|
||||||
|
CompatTypedField,
|
||||||
|
KeywordNode,
|
||||||
|
ParserFix,
|
||||||
|
Signature,
|
||||||
|
SpaceNode,
|
||||||
|
)
|
||||||
|
from sphinx import addnodes
|
||||||
|
from sphinx.addnodes import desc_signature, pending_xref
|
||||||
|
from sphinx.directives import ObjectDescription
|
||||||
|
from sphinx.domains import (
|
||||||
|
Domain,
|
||||||
|
Index,
|
||||||
|
IndexEntry,
|
||||||
|
ObjType,
|
||||||
|
)
|
||||||
|
from sphinx.locale import _, __
|
||||||
|
from sphinx.roles import XRefRole
|
||||||
|
from sphinx.util import logging
|
||||||
|
from sphinx.util.nodes import make_id, make_refnode
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from docutils.nodes import Element, Node
|
||||||
|
|
||||||
|
from sphinx.application import Sphinx
|
||||||
|
from sphinx.builders import Builder
|
||||||
|
from sphinx.environment import BuildEnvironment
|
||||||
|
from sphinx.util.typing import OptionSpec
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _unpack_field(
|
||||||
|
field: nodes.Node,
|
||||||
|
) -> Tuple[nodes.field_name, nodes.field_body]:
|
||||||
|
"""
|
||||||
|
docutils helper: unpack a field node in a type-safe manner.
|
||||||
|
"""
|
||||||
|
assert isinstance(field, nodes.field)
|
||||||
|
assert len(field.children) == 2
|
||||||
|
assert isinstance(field.children[0], nodes.field_name)
|
||||||
|
assert isinstance(field.children[1], nodes.field_body)
|
||||||
|
return (field.children[0], field.children[1])
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectEntry(NamedTuple):
|
||||||
|
docname: str
|
||||||
|
node_id: str
|
||||||
|
objtype: str
|
||||||
|
aliased: bool
|
||||||
|
|
||||||
|
|
||||||
|
class QAPIXRefRole(XRefRole):
|
||||||
|
|
||||||
|
def process_link(
|
||||||
|
self,
|
||||||
|
env: BuildEnvironment,
|
||||||
|
refnode: Element,
|
||||||
|
has_explicit_title: bool,
|
||||||
|
title: str,
|
||||||
|
target: str,
|
||||||
|
) -> tuple[str, str]:
|
||||||
|
refnode["qapi:module"] = env.ref_context.get("qapi:module")
|
||||||
|
|
||||||
|
# Cross-references that begin with a tilde adjust the title to
|
||||||
|
# only show the reference without a leading module, even if one
|
||||||
|
# was provided. This is a Sphinx-standard syntax; give it
|
||||||
|
# priority over QAPI-specific type markup below.
|
||||||
|
hide_module = False
|
||||||
|
if target.startswith("~"):
|
||||||
|
hide_module = True
|
||||||
|
target = target[1:]
|
||||||
|
|
||||||
|
# Type names that end with "?" are considered optional
|
||||||
|
# arguments and should be documented as such, but it's not
|
||||||
|
# part of the xref itself.
|
||||||
|
if target.endswith("?"):
|
||||||
|
refnode["qapi:optional"] = True
|
||||||
|
target = target[:-1]
|
||||||
|
|
||||||
|
# Type names wrapped in brackets denote lists. strip the
|
||||||
|
# brackets and remember to add them back later.
|
||||||
|
if target.startswith("[") and target.endswith("]"):
|
||||||
|
refnode["qapi:array"] = True
|
||||||
|
target = target[1:-1]
|
||||||
|
|
||||||
|
if has_explicit_title:
|
||||||
|
# Don't mess with the title at all if it was explicitly set.
|
||||||
|
# Explicit title syntax for references is e.g.
|
||||||
|
# :qapi:type:`target <explicit title>`
|
||||||
|
# and this explicit title overrides everything else here.
|
||||||
|
return title, target
|
||||||
|
|
||||||
|
title = target
|
||||||
|
if hide_module:
|
||||||
|
title = target.split(".")[-1]
|
||||||
|
|
||||||
|
return title, target
|
||||||
|
|
||||||
|
def result_nodes(
|
||||||
|
self,
|
||||||
|
document: nodes.document,
|
||||||
|
env: BuildEnvironment,
|
||||||
|
node: Element,
|
||||||
|
is_ref: bool,
|
||||||
|
) -> Tuple[List[nodes.Node], List[nodes.system_message]]:
|
||||||
|
|
||||||
|
# node here is the pending_xref node (or whatever nodeclass was
|
||||||
|
# configured at XRefRole class instantiation time).
|
||||||
|
results: List[nodes.Node] = [node]
|
||||||
|
|
||||||
|
if node.get("qapi:array"):
|
||||||
|
results.insert(0, nodes.literal("[", "["))
|
||||||
|
results.append(nodes.literal("]", "]"))
|
||||||
|
|
||||||
|
if node.get("qapi:optional"):
|
||||||
|
results.append(nodes.Text(", "))
|
||||||
|
results.append(nodes.emphasis("?", "optional"))
|
||||||
|
|
||||||
|
return results, []
|
||||||
|
|
||||||
|
|
||||||
|
class QAPIDescription(ParserFix):
|
||||||
|
"""
|
||||||
|
Generic QAPI description.
|
||||||
|
|
||||||
|
This is meant to be an abstract class, not instantiated
|
||||||
|
directly. This class handles the abstract details of indexing, the
|
||||||
|
TOC, and reference targets for QAPI descriptions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def handle_signature(self, sig: str, signode: desc_signature) -> Signature:
|
||||||
|
# Do nothing. The return value here is the "name" of the entity
|
||||||
|
# being documented; for QAPI, this is the same as the
|
||||||
|
# "signature", which is just a name.
|
||||||
|
|
||||||
|
# Normally this method must also populate signode with nodes to
|
||||||
|
# render the signature; here we do nothing instead - the
|
||||||
|
# subclasses will handle this.
|
||||||
|
return sig
|
||||||
|
|
||||||
|
def get_index_text(self, name: Signature) -> Tuple[str, str]:
|
||||||
|
"""Return the text for the index entry of the object."""
|
||||||
|
|
||||||
|
# NB: this is used for the global index, not the QAPI index.
|
||||||
|
return ("single", f"{name} (QMP {self.objtype})")
|
||||||
|
|
||||||
|
def add_target_and_index(
|
||||||
|
self, name: Signature, sig: str, signode: desc_signature
|
||||||
|
) -> None:
|
||||||
|
# name is the return value of handle_signature.
|
||||||
|
# sig is the original, raw text argument to handle_signature.
|
||||||
|
# For QAPI, these are identical, currently.
|
||||||
|
|
||||||
|
assert self.objtype
|
||||||
|
|
||||||
|
# If we're documenting a module, don't include the module as
|
||||||
|
# part of the FQN.
|
||||||
|
modname = ""
|
||||||
|
if self.objtype != "module":
|
||||||
|
modname = self.options.get(
|
||||||
|
"module", self.env.ref_context.get("qapi:module")
|
||||||
|
)
|
||||||
|
fullname = (modname + "." if modname else "") + name
|
||||||
|
|
||||||
|
node_id = make_id(
|
||||||
|
self.env, self.state.document, self.objtype, fullname
|
||||||
|
)
|
||||||
|
signode["ids"].append(node_id)
|
||||||
|
|
||||||
|
self.state.document.note_explicit_target(signode)
|
||||||
|
domain = cast(QAPIDomain, self.env.get_domain("qapi"))
|
||||||
|
domain.note_object(fullname, self.objtype, node_id, location=signode)
|
||||||
|
|
||||||
|
if "no-index-entry" not in self.options:
|
||||||
|
arity, indextext = self.get_index_text(name)
|
||||||
|
assert self.indexnode is not None
|
||||||
|
if indextext:
|
||||||
|
self.indexnode["entries"].append(
|
||||||
|
(arity, indextext, node_id, "", None)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _object_hierarchy_parts(
|
||||||
|
self, sig_node: desc_signature
|
||||||
|
) -> Tuple[str, ...]:
|
||||||
|
if "fullname" not in sig_node:
|
||||||
|
return ()
|
||||||
|
modname = sig_node.get("module")
|
||||||
|
fullname = sig_node["fullname"]
|
||||||
|
|
||||||
|
if modname:
|
||||||
|
return (modname, *fullname.split("."))
|
||||||
|
|
||||||
|
return tuple(fullname.split("."))
|
||||||
|
|
||||||
|
def _toc_entry_name(self, sig_node: desc_signature) -> str:
|
||||||
|
# This controls the name in the TOC and on the sidebar.
|
||||||
|
|
||||||
|
# This is the return type of _object_hierarchy_parts().
|
||||||
|
toc_parts = cast(Tuple[str, ...], sig_node.get("_toc_parts", ()))
|
||||||
|
if not toc_parts:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
config = self.env.app.config
|
||||||
|
*parents, name = toc_parts
|
||||||
|
if config.toc_object_entries_show_parents == "domain":
|
||||||
|
return sig_node.get("fullname", name)
|
||||||
|
if config.toc_object_entries_show_parents == "hide":
|
||||||
|
return name
|
||||||
|
if config.toc_object_entries_show_parents == "all":
|
||||||
|
return ".".join(parents + [name])
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
class QAPIObject(QAPIDescription):
|
||||||
|
"""
|
||||||
|
Description of a generic QAPI object.
|
||||||
|
|
||||||
|
It's not used directly, but is instead subclassed by specific directives.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Inherit some standard options from Sphinx's ObjectDescription
|
||||||
|
option_spec: OptionSpec = ( # type:ignore[misc]
|
||||||
|
ObjectDescription.option_spec.copy()
|
||||||
|
)
|
||||||
|
option_spec.update(
|
||||||
|
{
|
||||||
|
# Borrowed from the Python domain:
|
||||||
|
"module": directives.unchanged, # Override contextual module name
|
||||||
|
# These are QAPI originals:
|
||||||
|
"since": directives.unchanged,
|
||||||
|
"ifcond": directives.unchanged,
|
||||||
|
"deprecated": directives.flag,
|
||||||
|
"unstable": directives.flag,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
doc_field_types = [
|
||||||
|
# :feat name: descr
|
||||||
|
CompatGroupedField(
|
||||||
|
"feature",
|
||||||
|
label=_("Features"),
|
||||||
|
names=("feat",),
|
||||||
|
can_collapse=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_signature_prefix(self) -> List[nodes.Node]:
|
||||||
|
"""Return a prefix to put before the object name in the signature."""
|
||||||
|
assert self.objtype
|
||||||
|
return [
|
||||||
|
KeywordNode("", self.objtype.title()),
|
||||||
|
SpaceNode(" "),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_signature_suffix(self) -> List[nodes.Node]:
|
||||||
|
"""Return a suffix to put after the object name in the signature."""
|
||||||
|
ret: List[nodes.Node] = []
|
||||||
|
|
||||||
|
if "since" in self.options:
|
||||||
|
ret += [
|
||||||
|
SpaceNode(" "),
|
||||||
|
addnodes.desc_sig_element(
|
||||||
|
"", f"(Since: {self.options['since']})"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def handle_signature(self, sig: str, signode: desc_signature) -> Signature:
|
||||||
|
"""
|
||||||
|
Transform a QAPI definition name into RST nodes.
|
||||||
|
|
||||||
|
This method was originally intended for handling function
|
||||||
|
signatures. In the QAPI domain, however, we only pass the
|
||||||
|
definition name as the directive argument and handle everything
|
||||||
|
else in the content body with field lists.
|
||||||
|
|
||||||
|
As such, the only argument here is "sig", which is just the QAPI
|
||||||
|
definition name.
|
||||||
|
"""
|
||||||
|
modname = self.options.get(
|
||||||
|
"module", self.env.ref_context.get("qapi:module")
|
||||||
|
)
|
||||||
|
|
||||||
|
signode["fullname"] = sig
|
||||||
|
signode["module"] = modname
|
||||||
|
sig_prefix = self.get_signature_prefix()
|
||||||
|
if sig_prefix:
|
||||||
|
signode += addnodes.desc_annotation(
|
||||||
|
str(sig_prefix), "", *sig_prefix
|
||||||
|
)
|
||||||
|
signode += addnodes.desc_name(sig, sig)
|
||||||
|
signode += self.get_signature_suffix()
|
||||||
|
|
||||||
|
return sig
|
||||||
|
|
||||||
|
def _add_infopips(self, contentnode: addnodes.desc_content) -> None:
|
||||||
|
# Add various eye-catches and things that go below the signature
|
||||||
|
# bar, but precede the user-defined content.
|
||||||
|
infopips = nodes.container()
|
||||||
|
infopips.attributes["classes"].append("qapi-infopips")
|
||||||
|
|
||||||
|
def _add_pip(
|
||||||
|
source: str, content: Union[str, List[nodes.Node]], classname: str
|
||||||
|
) -> None:
|
||||||
|
node = nodes.container(source)
|
||||||
|
if isinstance(content, str):
|
||||||
|
node.append(nodes.Text(content))
|
||||||
|
else:
|
||||||
|
node.extend(content)
|
||||||
|
node.attributes["classes"].extend(["qapi-infopip", classname])
|
||||||
|
infopips.append(node)
|
||||||
|
|
||||||
|
if "deprecated" in self.options:
|
||||||
|
_add_pip(
|
||||||
|
":deprecated:",
|
||||||
|
f"This {self.objtype} is deprecated.",
|
||||||
|
"qapi-deprecated",
|
||||||
|
)
|
||||||
|
|
||||||
|
if "unstable" in self.options:
|
||||||
|
_add_pip(
|
||||||
|
":unstable:",
|
||||||
|
f"This {self.objtype} is unstable/experimental.",
|
||||||
|
"qapi-unstable",
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.options.get("ifcond", ""):
|
||||||
|
ifcond = self.options["ifcond"]
|
||||||
|
_add_pip(
|
||||||
|
f":ifcond: {ifcond}",
|
||||||
|
[
|
||||||
|
nodes.emphasis("", "Availability"),
|
||||||
|
nodes.Text(": "),
|
||||||
|
nodes.literal(ifcond, ifcond),
|
||||||
|
],
|
||||||
|
"qapi-ifcond",
|
||||||
|
)
|
||||||
|
|
||||||
|
if infopips.children:
|
||||||
|
contentnode.insert(0, infopips)
|
||||||
|
|
||||||
|
def _validate_field(self, field: nodes.field) -> None:
|
||||||
|
"""Validate field lists in this QAPI Object Description."""
|
||||||
|
name, _ = _unpack_field(field)
|
||||||
|
allowed_fields = set(self.env.app.config.qapi_allowed_fields)
|
||||||
|
|
||||||
|
field_label = name.astext()
|
||||||
|
if field_label in allowed_fields:
|
||||||
|
# Explicitly allowed field list name, OK.
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# split into field type and argument (if provided)
|
||||||
|
# e.g. `:arg type name: descr` is
|
||||||
|
# field_type = "arg", field_arg = "type name".
|
||||||
|
field_type, field_arg = field_label.split(None, 1)
|
||||||
|
except ValueError:
|
||||||
|
# No arguments provided
|
||||||
|
field_type = field_label
|
||||||
|
field_arg = ""
|
||||||
|
|
||||||
|
typemap = self.get_field_type_map()
|
||||||
|
if field_type in typemap:
|
||||||
|
# This is a special docfield, yet-to-be-processed. Catch
|
||||||
|
# correct names, but incorrect arguments. This mismatch WILL
|
||||||
|
# cause Sphinx to render this field incorrectly (without a
|
||||||
|
# warning), which is never what we want.
|
||||||
|
typedesc = typemap[field_type][0]
|
||||||
|
if typedesc.has_arg != bool(field_arg):
|
||||||
|
msg = f"docfield field list type {field_type!r} "
|
||||||
|
if typedesc.has_arg:
|
||||||
|
msg += "requires an argument."
|
||||||
|
else:
|
||||||
|
msg += "takes no arguments."
|
||||||
|
logger.warning(msg, location=field)
|
||||||
|
else:
|
||||||
|
# This is unrecognized entirely. It's valid rST to use
|
||||||
|
# arbitrary fields, but let's ensure the documentation
|
||||||
|
# writer has done this intentionally.
|
||||||
|
valid = ", ".join(sorted(set(typemap) | allowed_fields))
|
||||||
|
msg = (
|
||||||
|
f"Unrecognized field list name {field_label!r}.\n"
|
||||||
|
f"Valid fields for qapi:{self.objtype} are: {valid}\n"
|
||||||
|
"\n"
|
||||||
|
"If this usage is intentional, please add it to "
|
||||||
|
"'qapi_allowed_fields' in docs/conf.py."
|
||||||
|
)
|
||||||
|
logger.warning(msg, location=field)
|
||||||
|
|
||||||
|
def transform_content(self, content_node: addnodes.desc_content) -> None:
|
||||||
|
# This hook runs after before_content and the nested parse, but
|
||||||
|
# before the DocFieldTransformer is executed.
|
||||||
|
super().transform_content(content_node)
|
||||||
|
|
||||||
|
self._add_infopips(content_node)
|
||||||
|
|
||||||
|
# Validate field lists.
|
||||||
|
for child in content_node:
|
||||||
|
if isinstance(child, nodes.field_list):
|
||||||
|
for field in child.children:
|
||||||
|
assert isinstance(field, nodes.field)
|
||||||
|
self._validate_field(field)
|
||||||
|
|
||||||
|
|
||||||
|
class SpecialTypedField(CompatTypedField):
|
||||||
|
def make_field(self, *args: Any, **kwargs: Any) -> nodes.field:
|
||||||
|
ret = super().make_field(*args, **kwargs)
|
||||||
|
|
||||||
|
# Look for the characteristic " -- " text node that Sphinx
|
||||||
|
# inserts for each TypedField entry ...
|
||||||
|
for node in ret.traverse(lambda n: str(n) == " -- "):
|
||||||
|
par = node.parent
|
||||||
|
if par.children[0].astext() != "q_dummy":
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If the first node's text is q_dummy, this is a dummy
|
||||||
|
# field we want to strip down to just its contents.
|
||||||
|
del par.children[:-1]
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class QAPICommand(QAPIObject):
|
||||||
|
"""Description of a QAPI Command."""
|
||||||
|
|
||||||
|
doc_field_types = QAPIObject.doc_field_types.copy()
|
||||||
|
doc_field_types.extend(
|
||||||
|
[
|
||||||
|
# :arg TypeName ArgName: descr
|
||||||
|
SpecialTypedField(
|
||||||
|
"argument",
|
||||||
|
label=_("Arguments"),
|
||||||
|
names=("arg",),
|
||||||
|
typerolename="type",
|
||||||
|
can_collapse=False,
|
||||||
|
),
|
||||||
|
# :error: descr
|
||||||
|
CompatField(
|
||||||
|
"error",
|
||||||
|
label=_("Errors"),
|
||||||
|
names=("error", "errors"),
|
||||||
|
has_arg=False,
|
||||||
|
),
|
||||||
|
# :return TypeName: descr
|
||||||
|
CompatGroupedField(
|
||||||
|
"returnvalue",
|
||||||
|
label=_("Return"),
|
||||||
|
rolename="type",
|
||||||
|
names=("return",),
|
||||||
|
can_collapse=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class QAPIEnum(QAPIObject):
|
||||||
|
"""Description of a QAPI Enum."""
|
||||||
|
|
||||||
|
doc_field_types = QAPIObject.doc_field_types.copy()
|
||||||
|
doc_field_types.extend(
|
||||||
|
[
|
||||||
|
# :value name: descr
|
||||||
|
CompatGroupedField(
|
||||||
|
"value",
|
||||||
|
label=_("Values"),
|
||||||
|
names=("value",),
|
||||||
|
can_collapse=False,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class QAPIAlternate(QAPIObject):
|
||||||
|
"""Description of a QAPI Alternate."""
|
||||||
|
|
||||||
|
doc_field_types = QAPIObject.doc_field_types.copy()
|
||||||
|
doc_field_types.extend(
|
||||||
|
[
|
||||||
|
# :alt type name: descr
|
||||||
|
CompatTypedField(
|
||||||
|
"alternative",
|
||||||
|
label=_("Alternatives"),
|
||||||
|
names=("alt",),
|
||||||
|
typerolename="type",
|
||||||
|
can_collapse=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class QAPIObjectWithMembers(QAPIObject):
|
||||||
|
"""Base class for Events/Structs/Unions"""
|
||||||
|
|
||||||
|
doc_field_types = QAPIObject.doc_field_types.copy()
|
||||||
|
doc_field_types.extend(
|
||||||
|
[
|
||||||
|
# :member type name: descr
|
||||||
|
SpecialTypedField(
|
||||||
|
"member",
|
||||||
|
label=_("Members"),
|
||||||
|
names=("memb",),
|
||||||
|
typerolename="type",
|
||||||
|
can_collapse=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class QAPIEvent(QAPIObjectWithMembers):
|
||||||
|
# pylint: disable=too-many-ancestors
|
||||||
|
"""Description of a QAPI Event."""
|
||||||
|
|
||||||
|
|
||||||
|
class QAPIJSONObject(QAPIObjectWithMembers):
|
||||||
|
# pylint: disable=too-many-ancestors
|
||||||
|
"""Description of a QAPI Object: structs and unions."""
|
||||||
|
|
||||||
|
|
||||||
|
class QAPIModule(QAPIDescription):
|
||||||
|
"""
|
||||||
|
Directive to mark description of a new module.
|
||||||
|
|
||||||
|
This directive doesn't generate any special formatting, and is just
|
||||||
|
a pass-through for the content body. Named section titles are
|
||||||
|
allowed in the content body.
|
||||||
|
|
||||||
|
Use this directive to create entries for the QAPI module in the
|
||||||
|
global index and the QAPI index; as well as to associate subsequent
|
||||||
|
definitions with the module they are defined in for purposes of
|
||||||
|
search and QAPI index organization.
|
||||||
|
|
||||||
|
:arg: The name of the module.
|
||||||
|
:opt no-index: Don't add cross-reference targets or index entries.
|
||||||
|
:opt no-typesetting: Don't render the content body (but preserve any
|
||||||
|
cross-reference target IDs in the squelched output.)
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
.. qapi:module:: block-core
|
||||||
|
:no-index:
|
||||||
|
:no-typesetting:
|
||||||
|
|
||||||
|
Lorem ipsum, dolor sit amet ...
|
||||||
|
"""
|
||||||
|
|
||||||
|
def run(self) -> List[Node]:
|
||||||
|
modname = self.arguments[0].strip()
|
||||||
|
self.env.ref_context["qapi:module"] = modname
|
||||||
|
ret = super().run()
|
||||||
|
|
||||||
|
# ObjectDescription always creates a visible signature bar. We
|
||||||
|
# want module items to be "invisible", however.
|
||||||
|
|
||||||
|
# Extract the content body of the directive:
|
||||||
|
assert isinstance(ret[-1], addnodes.desc)
|
||||||
|
desc_node = ret.pop(-1)
|
||||||
|
assert isinstance(desc_node.children[1], addnodes.desc_content)
|
||||||
|
ret.extend(desc_node.children[1].children)
|
||||||
|
|
||||||
|
# Re-home node_ids so anchor refs still work:
|
||||||
|
node_ids: List[str]
|
||||||
|
if node_ids := [
|
||||||
|
node_id
|
||||||
|
for el in desc_node.children[0].traverse(nodes.Element)
|
||||||
|
for node_id in cast(List[str], el.get("ids", ()))
|
||||||
|
]:
|
||||||
|
target_node = nodes.target(ids=node_ids)
|
||||||
|
ret.insert(1, target_node)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class QAPIIndex(Index):
|
||||||
|
"""
|
||||||
|
Index subclass to provide the QAPI definition index.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# pylint: disable=too-few-public-methods
|
||||||
|
|
||||||
|
name = "index"
|
||||||
|
localname = _("QAPI Index")
|
||||||
|
shortname = _("QAPI Index")
|
||||||
|
|
||||||
|
def generate(
|
||||||
|
self,
|
||||||
|
docnames: Optional[Iterable[str]] = None,
|
||||||
|
) -> Tuple[List[Tuple[str, List[IndexEntry]]], bool]:
|
||||||
|
assert isinstance(self.domain, QAPIDomain)
|
||||||
|
content: Dict[str, List[IndexEntry]] = {}
|
||||||
|
collapse = False
|
||||||
|
|
||||||
|
# list of all object (name, ObjectEntry) pairs, sorted by name
|
||||||
|
# (ignoring the module)
|
||||||
|
objects = sorted(
|
||||||
|
self.domain.objects.items(),
|
||||||
|
key=lambda x: x[0].split(".")[-1].lower(),
|
||||||
|
)
|
||||||
|
|
||||||
|
for objname, obj in objects:
|
||||||
|
if docnames and obj.docname not in docnames:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Strip the module name out:
|
||||||
|
objname = objname.split(".")[-1]
|
||||||
|
|
||||||
|
# Add an alphabetical entry:
|
||||||
|
entries = content.setdefault(objname[0].upper(), [])
|
||||||
|
entries.append(
|
||||||
|
IndexEntry(
|
||||||
|
objname, 0, obj.docname, obj.node_id, obj.objtype, "", ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add a categorical entry:
|
||||||
|
category = obj.objtype.title() + "s"
|
||||||
|
entries = content.setdefault(category, [])
|
||||||
|
entries.append(
|
||||||
|
IndexEntry(objname, 0, obj.docname, obj.node_id, "", "", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
# alphabetically sort categories; type names first, ABC entries last.
|
||||||
|
sorted_content = sorted(
|
||||||
|
content.items(),
|
||||||
|
key=lambda x: (len(x[0]) == 1, x[0]),
|
||||||
|
)
|
||||||
|
return sorted_content, collapse
|
||||||
|
|
||||||
|
|
||||||
|
class QAPIDomain(Domain):
|
||||||
|
"""QAPI language domain."""
|
||||||
|
|
||||||
|
name = "qapi"
|
||||||
|
label = "QAPI"
|
||||||
|
|
||||||
|
# This table associates cross-reference object types (key) with an
|
||||||
|
# ObjType instance, which defines the valid cross-reference roles
|
||||||
|
# for each object type.
|
||||||
|
#
|
||||||
|
# e.g., the :qapi:type: cross-reference role can refer to enum,
|
||||||
|
# struct, union, or alternate objects; but :qapi:obj: can refer to
|
||||||
|
# anything. Each object also gets its own targeted cross-reference role.
|
||||||
|
object_types: Dict[str, ObjType] = {
|
||||||
|
"module": ObjType(_("module"), "mod", "any"),
|
||||||
|
"command": ObjType(_("command"), "cmd", "any"),
|
||||||
|
"event": ObjType(_("event"), "event", "any"),
|
||||||
|
"enum": ObjType(_("enum"), "enum", "type", "any"),
|
||||||
|
"object": ObjType(_("object"), "obj", "type", "any"),
|
||||||
|
"alternate": ObjType(_("alternate"), "alt", "type", "any"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Each of these provides a rST directive,
|
||||||
|
# e.g. .. qapi:module:: block-core
|
||||||
|
directives = {
|
||||||
|
"module": QAPIModule,
|
||||||
|
"command": QAPICommand,
|
||||||
|
"event": QAPIEvent,
|
||||||
|
"enum": QAPIEnum,
|
||||||
|
"object": QAPIJSONObject,
|
||||||
|
"alternate": QAPIAlternate,
|
||||||
|
}
|
||||||
|
|
||||||
|
# These are all cross-reference roles; e.g.
|
||||||
|
# :qapi:cmd:`query-block`. The keys correlate to the names used in
|
||||||
|
# the object_types table values above.
|
||||||
|
roles = {
|
||||||
|
"mod": QAPIXRefRole(),
|
||||||
|
"cmd": QAPIXRefRole(),
|
||||||
|
"event": QAPIXRefRole(),
|
||||||
|
"enum": QAPIXRefRole(),
|
||||||
|
"obj": QAPIXRefRole(), # specifically structs and unions.
|
||||||
|
"alt": QAPIXRefRole(),
|
||||||
|
# reference any data type (excludes modules, commands, events)
|
||||||
|
"type": QAPIXRefRole(),
|
||||||
|
"any": QAPIXRefRole(), # reference *any* type of QAPI object.
|
||||||
|
}
|
||||||
|
|
||||||
|
# Moved into the data property at runtime;
|
||||||
|
# this is the internal index of reference-able objects.
|
||||||
|
initial_data: Dict[str, Dict[str, Tuple[Any]]] = {
|
||||||
|
"objects": {}, # fullname -> ObjectEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
# Index pages to generate; each entry is an Index class.
|
||||||
|
indices = [
|
||||||
|
QAPIIndex,
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def objects(self) -> Dict[str, ObjectEntry]:
|
||||||
|
ret = self.data.setdefault("objects", {})
|
||||||
|
return ret # type: ignore[no-any-return]
|
||||||
|
|
||||||
|
def note_object(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
objtype: str,
|
||||||
|
node_id: str,
|
||||||
|
aliased: bool = False,
|
||||||
|
location: Any = None,
|
||||||
|
) -> None:
|
||||||
|
"""Note a QAPI object for cross reference."""
|
||||||
|
if name in self.objects:
|
||||||
|
other = self.objects[name]
|
||||||
|
if other.aliased and aliased is False:
|
||||||
|
# The original definition found. Override it!
|
||||||
|
pass
|
||||||
|
elif other.aliased is False and aliased:
|
||||||
|
# The original definition is already registered.
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# duplicated
|
||||||
|
logger.warning(
|
||||||
|
__(
|
||||||
|
"duplicate object description of %s, "
|
||||||
|
"other instance in %s, use :no-index: for one of them"
|
||||||
|
),
|
||||||
|
name,
|
||||||
|
other.docname,
|
||||||
|
location=location,
|
||||||
|
)
|
||||||
|
self.objects[name] = ObjectEntry(
|
||||||
|
self.env.docname, node_id, objtype, aliased
|
||||||
|
)
|
||||||
|
|
||||||
|
def clear_doc(self, docname: str) -> None:
|
||||||
|
for fullname, obj in list(self.objects.items()):
|
||||||
|
if obj.docname == docname:
|
||||||
|
del self.objects[fullname]
|
||||||
|
|
||||||
|
def merge_domaindata(
|
||||||
|
self, docnames: AbstractSet[str], otherdata: Dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
for fullname, obj in otherdata["objects"].items():
|
||||||
|
if obj.docname in docnames:
|
||||||
|
# Sphinx's own python domain doesn't appear to bother to
|
||||||
|
# check for collisions. Assert they don't happen and
|
||||||
|
# we'll fix it if/when the case arises.
|
||||||
|
assert fullname not in self.objects, (
|
||||||
|
"bug - collision on merge?"
|
||||||
|
f" {fullname=} {obj=} {self.objects[fullname]=}"
|
||||||
|
)
|
||||||
|
self.objects[fullname] = obj
|
||||||
|
|
||||||
|
def find_obj(
|
||||||
|
self, modname: str, name: str, typ: Optional[str]
|
||||||
|
) -> list[tuple[str, ObjectEntry]]:
|
||||||
|
"""
|
||||||
|
Find a QAPI object for "name", perhaps using the given module.
|
||||||
|
|
||||||
|
Returns a list of (name, object entry) tuples.
|
||||||
|
|
||||||
|
:param modname: The current module context (if any!)
|
||||||
|
under which we are searching.
|
||||||
|
:param name: The name of the x-ref to resolve;
|
||||||
|
may or may not include a leading module.
|
||||||
|
:param type: The role name of the x-ref we're resolving, if provided.
|
||||||
|
(This is absent for "any" lookups.)
|
||||||
|
"""
|
||||||
|
if not name:
|
||||||
|
return []
|
||||||
|
|
||||||
|
names: list[str] = []
|
||||||
|
matches: list[tuple[str, ObjectEntry]] = []
|
||||||
|
|
||||||
|
fullname = name
|
||||||
|
if "." in fullname:
|
||||||
|
# We're searching for a fully qualified reference;
|
||||||
|
# ignore the contextual module.
|
||||||
|
pass
|
||||||
|
elif modname:
|
||||||
|
# We're searching for something from somewhere;
|
||||||
|
# try searching the current module first.
|
||||||
|
# e.g. :qapi:cmd:`query-block` or `query-block` is being searched.
|
||||||
|
fullname = f"{modname}.{name}"
|
||||||
|
|
||||||
|
if typ is None:
|
||||||
|
# type isn't specified, this is a generic xref.
|
||||||
|
# search *all* qapi-specific object types.
|
||||||
|
objtypes: List[str] = list(self.object_types)
|
||||||
|
else:
|
||||||
|
# type is specified and will be a role (e.g. obj, mod, cmd)
|
||||||
|
# convert this to eligible object types (e.g. command, module)
|
||||||
|
# using the QAPIDomain.object_types table.
|
||||||
|
objtypes = self.objtypes_for_role(typ, [])
|
||||||
|
|
||||||
|
if name in self.objects and self.objects[name].objtype in objtypes:
|
||||||
|
names = [name]
|
||||||
|
elif (
|
||||||
|
fullname in self.objects
|
||||||
|
and self.objects[fullname].objtype in objtypes
|
||||||
|
):
|
||||||
|
names = [fullname]
|
||||||
|
else:
|
||||||
|
# exact match wasn't found; e.g. we are searching for
|
||||||
|
# `query-block` from a different (or no) module.
|
||||||
|
searchname = "." + name
|
||||||
|
names = [
|
||||||
|
oname
|
||||||
|
for oname in self.objects
|
||||||
|
if oname.endswith(searchname)
|
||||||
|
and self.objects[oname].objtype in objtypes
|
||||||
|
]
|
||||||
|
|
||||||
|
matches = [(oname, self.objects[oname]) for oname in names]
|
||||||
|
if len(matches) > 1:
|
||||||
|
matches = [m for m in matches if not m[1].aliased]
|
||||||
|
return matches
|
||||||
|
|
||||||
|
def resolve_xref(
|
||||||
|
self,
|
||||||
|
env: BuildEnvironment,
|
||||||
|
fromdocname: str,
|
||||||
|
builder: Builder,
|
||||||
|
typ: str,
|
||||||
|
target: str,
|
||||||
|
node: pending_xref,
|
||||||
|
contnode: Element,
|
||||||
|
) -> nodes.reference | None:
|
||||||
|
modname = node.get("qapi:module")
|
||||||
|
matches = self.find_obj(modname, target, typ)
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
# Normally, we could pass warn_dangling=True to QAPIXRefRole(),
|
||||||
|
# but that will trigger on references to these built-in types,
|
||||||
|
# which we'd like to ignore instead.
|
||||||
|
|
||||||
|
# Take care of that warning here instead, so long as the
|
||||||
|
# reference isn't to one of our built-in core types.
|
||||||
|
if target not in (
|
||||||
|
"string",
|
||||||
|
"number",
|
||||||
|
"int",
|
||||||
|
"boolean",
|
||||||
|
"null",
|
||||||
|
"value",
|
||||||
|
"q_empty",
|
||||||
|
):
|
||||||
|
logger.warning(
|
||||||
|
__("qapi:%s reference target not found: %r"),
|
||||||
|
typ,
|
||||||
|
target,
|
||||||
|
type="ref",
|
||||||
|
subtype="qapi",
|
||||||
|
location=node,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if len(matches) > 1:
|
||||||
|
logger.warning(
|
||||||
|
__("more than one target found for cross-reference %r: %s"),
|
||||||
|
target,
|
||||||
|
", ".join(match[0] for match in matches),
|
||||||
|
type="ref",
|
||||||
|
subtype="qapi",
|
||||||
|
location=node,
|
||||||
|
)
|
||||||
|
|
||||||
|
name, obj = matches[0]
|
||||||
|
return make_refnode(
|
||||||
|
builder, fromdocname, obj.docname, obj.node_id, contnode, name
|
||||||
|
)
|
||||||
|
|
||||||
|
def resolve_any_xref(
|
||||||
|
self,
|
||||||
|
env: BuildEnvironment,
|
||||||
|
fromdocname: str,
|
||||||
|
builder: Builder,
|
||||||
|
target: str,
|
||||||
|
node: pending_xref,
|
||||||
|
contnode: Element,
|
||||||
|
) -> List[Tuple[str, nodes.reference]]:
|
||||||
|
results: List[Tuple[str, nodes.reference]] = []
|
||||||
|
matches = self.find_obj(node.get("qapi:module"), target, None)
|
||||||
|
for name, obj in matches:
|
||||||
|
rolename = self.role_for_objtype(obj.objtype)
|
||||||
|
assert rolename is not None
|
||||||
|
role = f"qapi:{rolename}"
|
||||||
|
refnode = make_refnode(
|
||||||
|
builder, fromdocname, obj.docname, obj.node_id, contnode, name
|
||||||
|
)
|
||||||
|
results.append((role, refnode))
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def setup(app: Sphinx) -> Dict[str, Any]:
|
||||||
|
app.setup_extension("sphinx.directives")
|
||||||
|
app.add_config_value(
|
||||||
|
"qapi_allowed_fields",
|
||||||
|
set(),
|
||||||
|
"env", # Setting impacts parsing phase
|
||||||
|
types=set,
|
||||||
|
)
|
||||||
|
app.add_domain(QAPIDomain)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"version": "1.0",
|
||||||
|
"env_version": 1,
|
||||||
|
"parallel_read_safe": True,
|
||||||
|
"parallel_write_safe": True,
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
440
docs/sphinx/qapidoc_legacy.py
Normal file
440
docs/sphinx/qapidoc_legacy.py
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
# coding=utf-8
|
||||||
|
# type: ignore
|
||||||
|
#
|
||||||
|
# QEMU qapidoc QAPI file parsing extension
|
||||||
|
#
|
||||||
|
# Copyright (c) 2020 Linaro
|
||||||
|
#
|
||||||
|
# This work is licensed under the terms of the GNU GPLv2 or later.
|
||||||
|
# See the COPYING file in the top-level directory.
|
||||||
|
|
||||||
|
"""
|
||||||
|
qapidoc is a Sphinx extension that implements the qapi-doc directive
|
||||||
|
|
||||||
|
The purpose of this extension is to read the documentation comments
|
||||||
|
in QAPI schema files, and insert them all into the current document.
|
||||||
|
|
||||||
|
It implements one new rST directive, "qapi-doc::".
|
||||||
|
Each qapi-doc:: directive takes one argument, which is the
|
||||||
|
pathname of the schema file to process, relative to the source tree.
|
||||||
|
|
||||||
|
The docs/conf.py file must set the qapidoc_srctree config value to
|
||||||
|
the root of the QEMU source tree.
|
||||||
|
|
||||||
|
The Sphinx documentation on writing extensions is at:
|
||||||
|
https://www.sphinx-doc.org/en/master/development/index.html
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
from docutils import nodes
|
||||||
|
from docutils.statemachine import ViewList
|
||||||
|
from qapi.error import QAPISemError
|
||||||
|
from qapi.gen import QAPISchemaVisitor
|
||||||
|
from qapi.parser import QAPIDoc
|
||||||
|
|
||||||
|
|
||||||
|
def dedent(text: str) -> str:
|
||||||
|
# Adjust indentation to make description text parse as paragraph.
|
||||||
|
|
||||||
|
lines = text.splitlines(True)
|
||||||
|
if re.match(r"\s+", lines[0]):
|
||||||
|
# First line is indented; description started on the line after
|
||||||
|
# the name. dedent the whole block.
|
||||||
|
return textwrap.dedent(text)
|
||||||
|
|
||||||
|
# Descr started on same line. Dedent line 2+.
|
||||||
|
return lines[0] + textwrap.dedent("".join(lines[1:]))
|
||||||
|
|
||||||
|
|
||||||
|
class QAPISchemaGenRSTVisitor(QAPISchemaVisitor):
|
||||||
|
"""A QAPI schema visitor which generates docutils/Sphinx nodes
|
||||||
|
|
||||||
|
This class builds up a tree of docutils/Sphinx nodes corresponding
|
||||||
|
to documentation for the various QAPI objects. To use it, first
|
||||||
|
create a QAPISchemaGenRSTVisitor object, and call its
|
||||||
|
visit_begin() method. Then you can call one of the two methods
|
||||||
|
'freeform' (to add documentation for a freeform documentation
|
||||||
|
chunk) or 'symbol' (to add documentation for a QAPI symbol). These
|
||||||
|
will cause the visitor to build up the tree of document
|
||||||
|
nodes. Once you've added all the documentation via 'freeform' and
|
||||||
|
'symbol' method calls, you can call 'get_document_nodes' to get
|
||||||
|
the final list of document nodes (in a form suitable for returning
|
||||||
|
from a Sphinx directive's 'run' method).
|
||||||
|
"""
|
||||||
|
def __init__(self, sphinx_directive):
|
||||||
|
self._cur_doc = None
|
||||||
|
self._sphinx_directive = sphinx_directive
|
||||||
|
self._top_node = nodes.section()
|
||||||
|
self._active_headings = [self._top_node]
|
||||||
|
|
||||||
|
def _make_dlitem(self, term, defn):
|
||||||
|
"""Return a dlitem node with the specified term and definition.
|
||||||
|
|
||||||
|
term should be a list of Text and literal nodes.
|
||||||
|
defn should be one of:
|
||||||
|
- a string, which will be handed to _parse_text_into_node
|
||||||
|
- a list of Text and literal nodes, which will be put into
|
||||||
|
a paragraph node
|
||||||
|
"""
|
||||||
|
dlitem = nodes.definition_list_item()
|
||||||
|
dlterm = nodes.term('', '', *term)
|
||||||
|
dlitem += dlterm
|
||||||
|
if defn:
|
||||||
|
dldef = nodes.definition()
|
||||||
|
if isinstance(defn, list):
|
||||||
|
dldef += nodes.paragraph('', '', *defn)
|
||||||
|
else:
|
||||||
|
self._parse_text_into_node(defn, dldef)
|
||||||
|
dlitem += dldef
|
||||||
|
return dlitem
|
||||||
|
|
||||||
|
def _make_section(self, title):
|
||||||
|
"""Return a section node with optional title"""
|
||||||
|
section = nodes.section(ids=[self._sphinx_directive.new_serialno()])
|
||||||
|
if title:
|
||||||
|
section += nodes.title(title, title)
|
||||||
|
return section
|
||||||
|
|
||||||
|
def _nodes_for_ifcond(self, ifcond, with_if=True):
|
||||||
|
"""Return list of Text, literal nodes for the ifcond
|
||||||
|
|
||||||
|
Return a list which gives text like ' (If: condition)'.
|
||||||
|
If with_if is False, we don't return the "(If: " and ")".
|
||||||
|
"""
|
||||||
|
|
||||||
|
doc = ifcond.docgen()
|
||||||
|
if not doc:
|
||||||
|
return []
|
||||||
|
doc = nodes.literal('', doc)
|
||||||
|
if not with_if:
|
||||||
|
return [doc]
|
||||||
|
|
||||||
|
nodelist = [nodes.Text(' ('), nodes.strong('', 'If: ')]
|
||||||
|
nodelist.append(doc)
|
||||||
|
nodelist.append(nodes.Text(')'))
|
||||||
|
return nodelist
|
||||||
|
|
||||||
|
def _nodes_for_one_member(self, member):
|
||||||
|
"""Return list of Text, literal nodes for this member
|
||||||
|
|
||||||
|
Return a list of doctree nodes which give text like
|
||||||
|
'name: type (optional) (If: ...)' suitable for use as the
|
||||||
|
'term' part of a definition list item.
|
||||||
|
"""
|
||||||
|
term = [nodes.literal('', member.name)]
|
||||||
|
if member.type.doc_type():
|
||||||
|
term.append(nodes.Text(': '))
|
||||||
|
term.append(nodes.literal('', member.type.doc_type()))
|
||||||
|
if member.optional:
|
||||||
|
term.append(nodes.Text(' (optional)'))
|
||||||
|
if member.ifcond.is_present():
|
||||||
|
term.extend(self._nodes_for_ifcond(member.ifcond))
|
||||||
|
return term
|
||||||
|
|
||||||
|
def _nodes_for_variant_when(self, branches, variant):
|
||||||
|
"""Return list of Text, literal nodes for variant 'when' clause
|
||||||
|
|
||||||
|
Return a list of doctree nodes which give text like
|
||||||
|
'when tagname is variant (If: ...)' suitable for use in
|
||||||
|
the 'branches' part of a definition list.
|
||||||
|
"""
|
||||||
|
term = [nodes.Text(' when '),
|
||||||
|
nodes.literal('', branches.tag_member.name),
|
||||||
|
nodes.Text(' is '),
|
||||||
|
nodes.literal('', '"%s"' % variant.name)]
|
||||||
|
if variant.ifcond.is_present():
|
||||||
|
term.extend(self._nodes_for_ifcond(variant.ifcond))
|
||||||
|
return term
|
||||||
|
|
||||||
|
def _nodes_for_members(self, doc, what, base=None, branches=None):
|
||||||
|
"""Return list of doctree nodes for the table of members"""
|
||||||
|
dlnode = nodes.definition_list()
|
||||||
|
for section in doc.args.values():
|
||||||
|
term = self._nodes_for_one_member(section.member)
|
||||||
|
# TODO drop fallbacks when undocumented members are outlawed
|
||||||
|
if section.text:
|
||||||
|
defn = dedent(section.text)
|
||||||
|
else:
|
||||||
|
defn = [nodes.Text('Not documented')]
|
||||||
|
|
||||||
|
dlnode += self._make_dlitem(term, defn)
|
||||||
|
|
||||||
|
if base:
|
||||||
|
dlnode += self._make_dlitem([nodes.Text('The members of '),
|
||||||
|
nodes.literal('', base.doc_type())],
|
||||||
|
None)
|
||||||
|
|
||||||
|
if branches:
|
||||||
|
for v in branches.variants:
|
||||||
|
if v.type.name == 'q_empty':
|
||||||
|
continue
|
||||||
|
assert not v.type.is_implicit()
|
||||||
|
term = [nodes.Text('The members of '),
|
||||||
|
nodes.literal('', v.type.doc_type())]
|
||||||
|
term.extend(self._nodes_for_variant_when(branches, v))
|
||||||
|
dlnode += self._make_dlitem(term, None)
|
||||||
|
|
||||||
|
if not dlnode.children:
|
||||||
|
return []
|
||||||
|
|
||||||
|
section = self._make_section(what)
|
||||||
|
section += dlnode
|
||||||
|
return [section]
|
||||||
|
|
||||||
|
def _nodes_for_enum_values(self, doc):
|
||||||
|
"""Return list of doctree nodes for the table of enum values"""
|
||||||
|
seen_item = False
|
||||||
|
dlnode = nodes.definition_list()
|
||||||
|
for section in doc.args.values():
|
||||||
|
termtext = [nodes.literal('', section.member.name)]
|
||||||
|
if section.member.ifcond.is_present():
|
||||||
|
termtext.extend(self._nodes_for_ifcond(section.member.ifcond))
|
||||||
|
# TODO drop fallbacks when undocumented members are outlawed
|
||||||
|
if section.text:
|
||||||
|
defn = dedent(section.text)
|
||||||
|
else:
|
||||||
|
defn = [nodes.Text('Not documented')]
|
||||||
|
|
||||||
|
dlnode += self._make_dlitem(termtext, defn)
|
||||||
|
seen_item = True
|
||||||
|
|
||||||
|
if not seen_item:
|
||||||
|
return []
|
||||||
|
|
||||||
|
section = self._make_section('Values')
|
||||||
|
section += dlnode
|
||||||
|
return [section]
|
||||||
|
|
||||||
|
def _nodes_for_arguments(self, doc, arg_type):
|
||||||
|
"""Return list of doctree nodes for the arguments section"""
|
||||||
|
if arg_type and not arg_type.is_implicit():
|
||||||
|
assert not doc.args
|
||||||
|
section = self._make_section('Arguments')
|
||||||
|
dlnode = nodes.definition_list()
|
||||||
|
dlnode += self._make_dlitem(
|
||||||
|
[nodes.Text('The members of '),
|
||||||
|
nodes.literal('', arg_type.name)],
|
||||||
|
None)
|
||||||
|
section += dlnode
|
||||||
|
return [section]
|
||||||
|
|
||||||
|
return self._nodes_for_members(doc, 'Arguments')
|
||||||
|
|
||||||
|
def _nodes_for_features(self, doc):
|
||||||
|
"""Return list of doctree nodes for the table of features"""
|
||||||
|
seen_item = False
|
||||||
|
dlnode = nodes.definition_list()
|
||||||
|
for section in doc.features.values():
|
||||||
|
dlnode += self._make_dlitem(
|
||||||
|
[nodes.literal('', section.member.name)], dedent(section.text))
|
||||||
|
seen_item = True
|
||||||
|
|
||||||
|
if not seen_item:
|
||||||
|
return []
|
||||||
|
|
||||||
|
section = self._make_section('Features')
|
||||||
|
section += dlnode
|
||||||
|
return [section]
|
||||||
|
|
||||||
|
def _nodes_for_sections(self, doc):
|
||||||
|
"""Return list of doctree nodes for additional sections"""
|
||||||
|
nodelist = []
|
||||||
|
for section in doc.sections:
|
||||||
|
if section.kind == QAPIDoc.Kind.TODO:
|
||||||
|
# Hide TODO: sections
|
||||||
|
continue
|
||||||
|
|
||||||
|
if section.kind == QAPIDoc.Kind.PLAIN:
|
||||||
|
# Sphinx cannot handle sectionless titles;
|
||||||
|
# Instead, just append the results to the prior section.
|
||||||
|
container = nodes.container()
|
||||||
|
self._parse_text_into_node(section.text, container)
|
||||||
|
nodelist += container.children
|
||||||
|
continue
|
||||||
|
|
||||||
|
snode = self._make_section(section.kind.name.title())
|
||||||
|
self._parse_text_into_node(dedent(section.text), snode)
|
||||||
|
nodelist.append(snode)
|
||||||
|
return nodelist
|
||||||
|
|
||||||
|
def _nodes_for_if_section(self, ifcond):
|
||||||
|
"""Return list of doctree nodes for the "If" section"""
|
||||||
|
nodelist = []
|
||||||
|
if ifcond.is_present():
|
||||||
|
snode = self._make_section('If')
|
||||||
|
snode += nodes.paragraph(
|
||||||
|
'', '', *self._nodes_for_ifcond(ifcond, with_if=False)
|
||||||
|
)
|
||||||
|
nodelist.append(snode)
|
||||||
|
return nodelist
|
||||||
|
|
||||||
|
def _add_doc(self, typ, sections):
|
||||||
|
"""Add documentation for a command/object/enum...
|
||||||
|
|
||||||
|
We assume we're documenting the thing defined in self._cur_doc.
|
||||||
|
typ is the type of thing being added ("Command", "Object", etc)
|
||||||
|
|
||||||
|
sections is a list of nodes for sections to add to the definition.
|
||||||
|
"""
|
||||||
|
|
||||||
|
doc = self._cur_doc
|
||||||
|
snode = nodes.section(ids=[self._sphinx_directive.new_serialno()])
|
||||||
|
snode += nodes.title('', '', *[nodes.literal(doc.symbol, doc.symbol),
|
||||||
|
nodes.Text(' (' + typ + ')')])
|
||||||
|
self._parse_text_into_node(doc.body.text, snode)
|
||||||
|
for s in sections:
|
||||||
|
if s is not None:
|
||||||
|
snode += s
|
||||||
|
self._add_node_to_current_heading(snode)
|
||||||
|
|
||||||
|
def visit_enum_type(self, name, info, ifcond, features, members, prefix):
|
||||||
|
doc = self._cur_doc
|
||||||
|
self._add_doc('Enum',
|
||||||
|
self._nodes_for_enum_values(doc)
|
||||||
|
+ self._nodes_for_features(doc)
|
||||||
|
+ self._nodes_for_sections(doc)
|
||||||
|
+ self._nodes_for_if_section(ifcond))
|
||||||
|
|
||||||
|
def visit_object_type(self, name, info, ifcond, features,
|
||||||
|
base, members, branches):
|
||||||
|
doc = self._cur_doc
|
||||||
|
if base and base.is_implicit():
|
||||||
|
base = None
|
||||||
|
self._add_doc('Object',
|
||||||
|
self._nodes_for_members(doc, 'Members', base, branches)
|
||||||
|
+ self._nodes_for_features(doc)
|
||||||
|
+ self._nodes_for_sections(doc)
|
||||||
|
+ self._nodes_for_if_section(ifcond))
|
||||||
|
|
||||||
|
def visit_alternate_type(self, name, info, ifcond, features,
|
||||||
|
alternatives):
|
||||||
|
doc = self._cur_doc
|
||||||
|
self._add_doc('Alternate',
|
||||||
|
self._nodes_for_members(doc, 'Members')
|
||||||
|
+ self._nodes_for_features(doc)
|
||||||
|
+ self._nodes_for_sections(doc)
|
||||||
|
+ self._nodes_for_if_section(ifcond))
|
||||||
|
|
||||||
|
def visit_command(self, name, info, ifcond, features, arg_type,
|
||||||
|
ret_type, gen, success_response, boxed, allow_oob,
|
||||||
|
allow_preconfig, coroutine):
|
||||||
|
doc = self._cur_doc
|
||||||
|
self._add_doc('Command',
|
||||||
|
self._nodes_for_arguments(doc, arg_type)
|
||||||
|
+ self._nodes_for_features(doc)
|
||||||
|
+ self._nodes_for_sections(doc)
|
||||||
|
+ self._nodes_for_if_section(ifcond))
|
||||||
|
|
||||||
|
def visit_event(self, name, info, ifcond, features, arg_type, boxed):
|
||||||
|
doc = self._cur_doc
|
||||||
|
self._add_doc('Event',
|
||||||
|
self._nodes_for_arguments(doc, arg_type)
|
||||||
|
+ self._nodes_for_features(doc)
|
||||||
|
+ self._nodes_for_sections(doc)
|
||||||
|
+ self._nodes_for_if_section(ifcond))
|
||||||
|
|
||||||
|
def symbol(self, doc, entity):
|
||||||
|
"""Add documentation for one symbol to the document tree
|
||||||
|
|
||||||
|
This is the main entry point which causes us to add documentation
|
||||||
|
nodes for a symbol (which could be a 'command', 'object', 'event',
|
||||||
|
etc). We do this by calling 'visit' on the schema entity, which
|
||||||
|
will then call back into one of our visit_* methods, depending
|
||||||
|
on what kind of thing this symbol is.
|
||||||
|
"""
|
||||||
|
self._cur_doc = doc
|
||||||
|
entity.visit(self)
|
||||||
|
self._cur_doc = None
|
||||||
|
|
||||||
|
def _start_new_heading(self, heading, level):
|
||||||
|
"""Start a new heading at the specified heading level
|
||||||
|
|
||||||
|
Create a new section whose title is 'heading' and which is placed
|
||||||
|
in the docutils node tree as a child of the most recent level-1
|
||||||
|
heading. Subsequent document sections (commands, freeform doc chunks,
|
||||||
|
etc) will be placed as children of this new heading section.
|
||||||
|
"""
|
||||||
|
if len(self._active_headings) < level:
|
||||||
|
raise QAPISemError(self._cur_doc.info,
|
||||||
|
'Level %d subheading found outside a '
|
||||||
|
'level %d heading'
|
||||||
|
% (level, level - 1))
|
||||||
|
snode = self._make_section(heading)
|
||||||
|
self._active_headings[level - 1] += snode
|
||||||
|
self._active_headings = self._active_headings[:level]
|
||||||
|
self._active_headings.append(snode)
|
||||||
|
return snode
|
||||||
|
|
||||||
|
def _add_node_to_current_heading(self, node):
|
||||||
|
"""Add the node to whatever the current active heading is"""
|
||||||
|
self._active_headings[-1] += node
|
||||||
|
|
||||||
|
def freeform(self, doc):
|
||||||
|
"""Add a piece of 'freeform' documentation to the document tree
|
||||||
|
|
||||||
|
A 'freeform' document chunk doesn't relate to any particular
|
||||||
|
symbol (for instance, it could be an introduction).
|
||||||
|
|
||||||
|
If the freeform document starts with a line of the form
|
||||||
|
'= Heading text', this is a section or subsection heading, with
|
||||||
|
the heading level indicated by the number of '=' signs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# QAPIDoc documentation says free-form documentation blocks
|
||||||
|
# must have only a body section, nothing else.
|
||||||
|
assert not doc.sections
|
||||||
|
assert not doc.args
|
||||||
|
assert not doc.features
|
||||||
|
self._cur_doc = doc
|
||||||
|
|
||||||
|
text = doc.body.text
|
||||||
|
if re.match(r'=+ ', text):
|
||||||
|
# Section/subsection heading (if present, will always be
|
||||||
|
# the first line of the block)
|
||||||
|
(heading, _, text) = text.partition('\n')
|
||||||
|
(leader, _, heading) = heading.partition(' ')
|
||||||
|
node = self._start_new_heading(heading, len(leader))
|
||||||
|
if text == '':
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
node = nodes.container()
|
||||||
|
|
||||||
|
self._parse_text_into_node(text, node)
|
||||||
|
self._cur_doc = None
|
||||||
|
|
||||||
|
def _parse_text_into_node(self, doctext, node):
|
||||||
|
"""Parse a chunk of QAPI-doc-format text into the node
|
||||||
|
|
||||||
|
The doc comment can contain most inline rST markup, including
|
||||||
|
bulleted and enumerated lists.
|
||||||
|
As an extra permitted piece of markup, @var will be turned
|
||||||
|
into ``var``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Handle the "@var means ``var`` case
|
||||||
|
doctext = re.sub(r'@([\w-]+)', r'``\1``', doctext)
|
||||||
|
|
||||||
|
rstlist = ViewList()
|
||||||
|
for line in doctext.splitlines():
|
||||||
|
# The reported line number will always be that of the start line
|
||||||
|
# of the doc comment, rather than the actual location of the error.
|
||||||
|
# Being more precise would require overhaul of the QAPIDoc class
|
||||||
|
# to track lines more exactly within all the sub-parts of the doc
|
||||||
|
# comment, as well as counting lines here.
|
||||||
|
rstlist.append(line, self._cur_doc.info.fname,
|
||||||
|
self._cur_doc.info.line)
|
||||||
|
# Append a blank line -- in some cases rST syntax errors get
|
||||||
|
# attributed to the line after one with actual text, and if there
|
||||||
|
# isn't anything in the ViewList corresponding to that then Sphinx
|
||||||
|
# 1.6's AutodocReporter will then misidentify the source/line location
|
||||||
|
# in the error message (usually attributing it to the top-level
|
||||||
|
# .rst file rather than the offending .json file). The extra blank
|
||||||
|
# line won't affect the rendered output.
|
||||||
|
rstlist.append("", self._cur_doc.info.fname, self._cur_doc.info.line)
|
||||||
|
self._sphinx_directive.do_parse(rstlist, node)
|
||||||
|
|
||||||
|
def get_document_node(self):
|
||||||
|
"""Return the root docutils node which makes up the document"""
|
||||||
|
return self._top_node
|
@ -5,6 +5,8 @@
|
|||||||
#
|
#
|
||||||
# This document describes all commands currently supported by QMP.
|
# This document describes all commands currently supported by QMP.
|
||||||
#
|
#
|
||||||
|
# For locating a particular item, please see the `qapi-index`.
|
||||||
|
#
|
||||||
# Most of the time their usage is exactly the same as in the user
|
# Most of the time their usage is exactly the same as in the user
|
||||||
# Monitor, this means that any other document which also describe
|
# Monitor, this means that any other document which also describe
|
||||||
# commands (the manpage, QEMU's manual, etc) can and should be
|
# commands (the manpage, QEMU's manual, etc) can and should be
|
||||||
|
@ -31,34 +31,28 @@ def create_backend(path: str) -> QAPIBackend:
|
|||||||
|
|
||||||
module_path, dot, class_name = path.rpartition('.')
|
module_path, dot, class_name = path.rpartition('.')
|
||||||
if not dot:
|
if not dot:
|
||||||
print("argument of -B must be of the form MODULE.CLASS",
|
raise QAPIError("argument of -B must be of the form MODULE.CLASS")
|
||||||
file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mod = import_module(module_path)
|
mod = import_module(module_path)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(f"unable to import '{module_path}': {ex}", file=sys.stderr)
|
raise QAPIError(f"unable to import '{module_path}': {ex}") from ex
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
klass = getattr(mod, class_name)
|
klass = getattr(mod, class_name)
|
||||||
except AttributeError:
|
except AttributeError as ex:
|
||||||
print(f"module '{module_path}' has no class '{class_name}'",
|
raise QAPIError(
|
||||||
file=sys.stderr)
|
f"module '{module_path}' has no class '{class_name}'") from ex
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
backend = klass()
|
backend = klass()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(f"backend '{path}' cannot be instantiated: {ex}",
|
raise QAPIError(
|
||||||
file=sys.stderr)
|
f"backend '{path}' cannot be instantiated: {ex}") from ex
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if not isinstance(backend, QAPIBackend):
|
if not isinstance(backend, QAPIBackend):
|
||||||
print(f"backend '{path}' must be an instance of QAPIBackend",
|
raise QAPIError(
|
||||||
file=sys.stderr)
|
f"backend '{path}' must be an instance of QAPIBackend")
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
return backend
|
return backend
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
# This work is licensed under the terms of the GNU GPL, version 2.
|
# This work is licensed under the terms of the GNU GPL, version 2.
|
||||||
# See the COPYING file in the top-level directory.
|
# See the COPYING file in the top-level directory.
|
||||||
|
|
||||||
|
import enum
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from typing import (
|
from typing import (
|
||||||
@ -574,7 +575,10 @@ class QAPISchemaParser:
|
|||||||
)
|
)
|
||||||
raise QAPIParseError(self, emsg)
|
raise QAPIParseError(self, emsg)
|
||||||
|
|
||||||
doc.new_tagged_section(self.info, match.group(1))
|
doc.new_tagged_section(
|
||||||
|
self.info,
|
||||||
|
QAPIDoc.Kind.from_string(match.group(1))
|
||||||
|
)
|
||||||
text = line[match.end():]
|
text = line[match.end():]
|
||||||
if text:
|
if text:
|
||||||
doc.append_line(text)
|
doc.append_line(text)
|
||||||
@ -585,7 +589,7 @@ class QAPISchemaParser:
|
|||||||
self,
|
self,
|
||||||
"unexpected '=' markup in definition documentation")
|
"unexpected '=' markup in definition documentation")
|
||||||
else:
|
else:
|
||||||
# tag-less paragraph
|
# plain paragraph
|
||||||
doc.ensure_untagged_section(self.info)
|
doc.ensure_untagged_section(self.info)
|
||||||
doc.append_line(line)
|
doc.append_line(line)
|
||||||
line = self.get_doc_paragraph(doc)
|
line = self.get_doc_paragraph(doc)
|
||||||
@ -634,23 +638,51 @@ class QAPIDoc:
|
|||||||
Free-form documentation blocks consist only of a body section.
|
Free-form documentation blocks consist only of a body section.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Kind(enum.Enum):
|
||||||
|
PLAIN = 0
|
||||||
|
MEMBER = 1
|
||||||
|
FEATURE = 2
|
||||||
|
RETURNS = 3
|
||||||
|
ERRORS = 4
|
||||||
|
SINCE = 5
|
||||||
|
TODO = 6
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_string(kind: str) -> 'QAPIDoc.Kind':
|
||||||
|
return QAPIDoc.Kind[kind.upper()]
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return self.name.title()
|
||||||
|
|
||||||
class Section:
|
class Section:
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
def __init__(self, info: QAPISourceInfo,
|
def __init__(
|
||||||
tag: Optional[str] = None):
|
self,
|
||||||
|
info: QAPISourceInfo,
|
||||||
|
kind: 'QAPIDoc.Kind',
|
||||||
|
):
|
||||||
# section source info, i.e. where it begins
|
# section source info, i.e. where it begins
|
||||||
self.info = info
|
self.info = info
|
||||||
# section tag, if any ('Returns', '@name', ...)
|
# section kind
|
||||||
self.tag = tag
|
self.kind = kind
|
||||||
# section text without tag
|
# section text without tag
|
||||||
self.text = ''
|
self.text = ''
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<QAPIDoc.Section kind={self.kind!r} text={self.text!r}>"
|
||||||
|
|
||||||
def append_line(self, line: str) -> None:
|
def append_line(self, line: str) -> None:
|
||||||
self.text += line + '\n'
|
self.text += line + '\n'
|
||||||
|
|
||||||
class ArgSection(Section):
|
class ArgSection(Section):
|
||||||
def __init__(self, info: QAPISourceInfo, tag: str):
|
def __init__(
|
||||||
super().__init__(info, tag)
|
self,
|
||||||
|
info: QAPISourceInfo,
|
||||||
|
kind: 'QAPIDoc.Kind',
|
||||||
|
name: str
|
||||||
|
):
|
||||||
|
super().__init__(info, kind)
|
||||||
|
self.name = name
|
||||||
self.member: Optional['QAPISchemaMember'] = None
|
self.member: Optional['QAPISchemaMember'] = None
|
||||||
|
|
||||||
def connect(self, member: 'QAPISchemaMember') -> None:
|
def connect(self, member: 'QAPISchemaMember') -> None:
|
||||||
@ -662,7 +694,9 @@ class QAPIDoc:
|
|||||||
# definition doc's symbol, None for free-form doc
|
# definition doc's symbol, None for free-form doc
|
||||||
self.symbol: Optional[str] = symbol
|
self.symbol: Optional[str] = symbol
|
||||||
# the sections in textual order
|
# the sections in textual order
|
||||||
self.all_sections: List[QAPIDoc.Section] = [QAPIDoc.Section(info)]
|
self.all_sections: List[QAPIDoc.Section] = [
|
||||||
|
QAPIDoc.Section(info, QAPIDoc.Kind.PLAIN)
|
||||||
|
]
|
||||||
# the body section
|
# the body section
|
||||||
self.body: Optional[QAPIDoc.Section] = self.all_sections[0]
|
self.body: Optional[QAPIDoc.Section] = self.all_sections[0]
|
||||||
# dicts mapping parameter/feature names to their description
|
# dicts mapping parameter/feature names to their description
|
||||||
@ -679,55 +713,71 @@ class QAPIDoc:
|
|||||||
def end(self) -> None:
|
def end(self) -> None:
|
||||||
for section in self.all_sections:
|
for section in self.all_sections:
|
||||||
section.text = section.text.strip('\n')
|
section.text = section.text.strip('\n')
|
||||||
if section.tag is not None and section.text == '':
|
if section.kind != QAPIDoc.Kind.PLAIN and section.text == '':
|
||||||
raise QAPISemError(
|
raise QAPISemError(
|
||||||
section.info, "text required after '%s:'" % section.tag)
|
section.info, "text required after '%s:'" % section.kind)
|
||||||
|
|
||||||
def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
|
def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
|
||||||
if self.all_sections and not self.all_sections[-1].tag:
|
kind = QAPIDoc.Kind.PLAIN
|
||||||
|
|
||||||
|
if self.all_sections and self.all_sections[-1].kind == kind:
|
||||||
# extend current section
|
# extend current section
|
||||||
self.all_sections[-1].text += '\n'
|
section = self.all_sections[-1]
|
||||||
|
if not section.text:
|
||||||
|
# Section is empty so far; update info to start *here*.
|
||||||
|
section.info = info
|
||||||
|
section.text += '\n'
|
||||||
return
|
return
|
||||||
|
|
||||||
# start new section
|
# start new section
|
||||||
section = self.Section(info)
|
section = self.Section(info, kind)
|
||||||
self.sections.append(section)
|
self.sections.append(section)
|
||||||
self.all_sections.append(section)
|
self.all_sections.append(section)
|
||||||
|
|
||||||
def new_tagged_section(self, info: QAPISourceInfo, tag: str) -> None:
|
def new_tagged_section(
|
||||||
section = self.Section(info, tag)
|
self,
|
||||||
if tag == 'Returns':
|
info: QAPISourceInfo,
|
||||||
|
kind: 'QAPIDoc.Kind',
|
||||||
|
) -> None:
|
||||||
|
section = self.Section(info, kind)
|
||||||
|
if kind == QAPIDoc.Kind.RETURNS:
|
||||||
if self.returns:
|
if self.returns:
|
||||||
raise QAPISemError(
|
raise QAPISemError(
|
||||||
info, "duplicated '%s' section" % tag)
|
info, "duplicated '%s' section" % kind)
|
||||||
self.returns = section
|
self.returns = section
|
||||||
elif tag == 'Errors':
|
elif kind == QAPIDoc.Kind.ERRORS:
|
||||||
if self.errors:
|
if self.errors:
|
||||||
raise QAPISemError(
|
raise QAPISemError(
|
||||||
info, "duplicated '%s' section" % tag)
|
info, "duplicated '%s' section" % kind)
|
||||||
self.errors = section
|
self.errors = section
|
||||||
elif tag == 'Since':
|
elif kind == QAPIDoc.Kind.SINCE:
|
||||||
if self.since:
|
if self.since:
|
||||||
raise QAPISemError(
|
raise QAPISemError(
|
||||||
info, "duplicated '%s' section" % tag)
|
info, "duplicated '%s' section" % kind)
|
||||||
self.since = section
|
self.since = section
|
||||||
self.sections.append(section)
|
self.sections.append(section)
|
||||||
self.all_sections.append(section)
|
self.all_sections.append(section)
|
||||||
|
|
||||||
def _new_description(self, info: QAPISourceInfo, name: str,
|
def _new_description(
|
||||||
desc: Dict[str, ArgSection]) -> None:
|
self,
|
||||||
|
info: QAPISourceInfo,
|
||||||
|
name: str,
|
||||||
|
kind: 'QAPIDoc.Kind',
|
||||||
|
desc: Dict[str, ArgSection]
|
||||||
|
) -> None:
|
||||||
if not name:
|
if not name:
|
||||||
raise QAPISemError(info, "invalid parameter name")
|
raise QAPISemError(info, "invalid parameter name")
|
||||||
if name in desc:
|
if name in desc:
|
||||||
raise QAPISemError(info, "'%s' parameter name duplicated" % name)
|
raise QAPISemError(info, "'%s' parameter name duplicated" % name)
|
||||||
section = self.ArgSection(info, '@' + name)
|
section = self.ArgSection(info, kind, name)
|
||||||
self.all_sections.append(section)
|
self.all_sections.append(section)
|
||||||
desc[name] = section
|
desc[name] = section
|
||||||
|
|
||||||
def new_argument(self, info: QAPISourceInfo, name: str) -> None:
|
def new_argument(self, info: QAPISourceInfo, name: str) -> None:
|
||||||
self._new_description(info, name, self.args)
|
self._new_description(info, name, QAPIDoc.Kind.MEMBER, self.args)
|
||||||
|
|
||||||
def new_feature(self, info: QAPISourceInfo, name: str) -> None:
|
def new_feature(self, info: QAPISourceInfo, name: str) -> None:
|
||||||
self._new_description(info, name, self.features)
|
self._new_description(info, name, QAPIDoc.Kind.FEATURE, self.features)
|
||||||
|
|
||||||
def append_line(self, line: str) -> None:
|
def append_line(self, line: str) -> None:
|
||||||
self.all_sections[-1].append_line(line)
|
self.all_sections[-1].append_line(line)
|
||||||
@ -739,8 +789,23 @@ class QAPIDoc:
|
|||||||
raise QAPISemError(member.info,
|
raise QAPISemError(member.info,
|
||||||
"%s '%s' lacks documentation"
|
"%s '%s' lacks documentation"
|
||||||
% (member.role, member.name))
|
% (member.role, member.name))
|
||||||
self.args[member.name] = QAPIDoc.ArgSection(
|
# Insert stub documentation section for missing member docs.
|
||||||
self.info, '@' + member.name)
|
# TODO: drop when undocumented members are outlawed
|
||||||
|
|
||||||
|
section = QAPIDoc.ArgSection(
|
||||||
|
self.info, QAPIDoc.Kind.MEMBER, member.name)
|
||||||
|
self.args[member.name] = section
|
||||||
|
|
||||||
|
# Determine where to insert stub doc - it should go at the
|
||||||
|
# end of the members section(s), if any. Note that index 0
|
||||||
|
# is assumed to be an untagged intro section, even if it is
|
||||||
|
# empty.
|
||||||
|
index = 1
|
||||||
|
if len(self.all_sections) > 1:
|
||||||
|
while self.all_sections[index].kind == QAPIDoc.Kind.MEMBER:
|
||||||
|
index += 1
|
||||||
|
self.all_sections.insert(index, section)
|
||||||
|
|
||||||
self.args[member.name].connect(member)
|
self.args[member.name].connect(member)
|
||||||
|
|
||||||
def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
|
def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
|
||||||
|
@ -47,9 +47,9 @@ class QAPISourceInfo:
|
|||||||
self.defn_meta = meta
|
self.defn_meta = meta
|
||||||
self.defn_name = name
|
self.defn_name = name
|
||||||
|
|
||||||
def next_line(self: T) -> T:
|
def next_line(self: T, n: int = 1) -> T:
|
||||||
info = copy.copy(self)
|
info = copy.copy(self)
|
||||||
info.line += 1
|
info.line += n
|
||||||
return info
|
return info
|
||||||
|
|
||||||
def loc(self) -> str:
|
def loc(self) -> str:
|
||||||
|
@ -113,7 +113,7 @@ The _one_ {and only}, description on the same line
|
|||||||
Also _one_ {and only}
|
Also _one_ {and only}
|
||||||
feature=enum-member-feat
|
feature=enum-member-feat
|
||||||
a member feature
|
a member feature
|
||||||
section=None
|
section=Plain
|
||||||
@two is undocumented
|
@two is undocumented
|
||||||
doc symbol=Base
|
doc symbol=Base
|
||||||
body=
|
body=
|
||||||
@ -171,15 +171,15 @@ description starts on the same line
|
|||||||
a feature
|
a feature
|
||||||
feature=cmd-feat2
|
feature=cmd-feat2
|
||||||
another feature
|
another feature
|
||||||
section=None
|
section=Plain
|
||||||
.. note:: @arg3 is undocumented
|
.. note:: @arg3 is undocumented
|
||||||
section=Returns
|
section=Returns
|
||||||
@Object
|
@Object
|
||||||
section=Errors
|
section=Errors
|
||||||
some
|
some
|
||||||
section=TODO
|
section=Todo
|
||||||
frobnicate
|
frobnicate
|
||||||
section=None
|
section=Plain
|
||||||
.. admonition:: Notes
|
.. admonition:: Notes
|
||||||
|
|
||||||
- Lorem ipsum dolor sit amet
|
- Lorem ipsum dolor sit amet
|
||||||
@ -212,7 +212,7 @@ If you're bored enough to read this, go see a video of boxed cats
|
|||||||
a feature
|
a feature
|
||||||
feature=cmd-feat2
|
feature=cmd-feat2
|
||||||
another feature
|
another feature
|
||||||
section=None
|
section=Plain
|
||||||
.. qmp-example::
|
.. qmp-example::
|
||||||
|
|
||||||
-> "this example"
|
-> "this example"
|
||||||
|
@ -122,7 +122,7 @@ def test_frontend(fname):
|
|||||||
for feat, section in doc.features.items():
|
for feat, section in doc.features.items():
|
||||||
print(' feature=%s\n%s' % (feat, section.text))
|
print(' feature=%s\n%s' % (feat, section.text))
|
||||||
for section in doc.sections:
|
for section in doc.sections:
|
||||||
print(' section=%s\n%s' % (section.tag, section.text))
|
print(' section=%s\n%s' % (section.kind, section.text))
|
||||||
|
|
||||||
|
|
||||||
def open_test_result(dir_name, file_name, update):
|
def open_test_result(dir_name, file_name, update):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user