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:
Stefan Hajnoczi 2025-03-12 07:49:54 +08:00
commit 94d689d0c6
18 changed files with 3000 additions and 478 deletions

View File

@ -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

View File

@ -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

View File

@ -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>`_:

View File

@ -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
View 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.

View File

@ -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
---- ----

View File

@ -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:

View File

@ -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
View 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
View 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

View 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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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"

View File

@ -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):