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
|
||||
|
||||
Sphinx documentation configuration and build machinery
|
||||
M: John Snow <jsnow@redhat.com>
|
||||
M: Peter Maydell <peter.maydell@linaro.org>
|
||||
S: Maintained
|
||||
F: docs/conf.py
|
||||
|
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
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ['kerneldoc', 'qmp_lexer', 'hxtool', 'depfile', 'qapidoc']
|
||||
extensions = [
|
||||
'depfile',
|
||||
'hxtool',
|
||||
'kerneldoc',
|
||||
'qapi_domain',
|
||||
'qapidoc',
|
||||
'qmp_lexer',
|
||||
]
|
||||
|
||||
if sphinx.version_info[:3] > (4, 0, 0):
|
||||
tags.add('sphinx4')
|
||||
@ -146,6 +153,15 @@ rst_epilog = ".. |CONFDIR| replace:: ``" + confdir + "``\n"
|
||||
with open(os.path.join(qemu_docdir, 'defs.rst.inc')) as f:
|
||||
rst_epilog += f.read()
|
||||
|
||||
|
||||
# Normally, the QAPI domain is picky about what field lists you use to
|
||||
# describe a QAPI entity. If you'd like to use arbitrary additional
|
||||
# fields in source documentation, add them here.
|
||||
qapi_allowed_fields = {
|
||||
"see also",
|
||||
}
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
|
@ -23,7 +23,7 @@ Some of the main QEMU subsystems are:
|
||||
- `Devices<device-emulation>` & Board models
|
||||
- `Documentation <documentation-root>`
|
||||
- `GDB support<GDB usage>`
|
||||
- `Migration<migration>`
|
||||
- :ref:`Migration<migration>`
|
||||
- `Monitor<QEMU monitor>`
|
||||
- :ref:`QOM (QEMU Object Model)<qom>`
|
||||
- `System mode<System emulation>`
|
||||
@ -112,7 +112,7 @@ yet, so sometimes the source code is all you have.
|
||||
* `libdecnumber <https://gitlab.com/qemu-project/qemu/-/tree/master/libdecnumber>`_:
|
||||
Import of gcc library, used to implement decimal number arithmetic.
|
||||
* `migration <https://gitlab.com/qemu-project/qemu/-/tree/master/migration>`__:
|
||||
`Migration framework <migration>`.
|
||||
:ref:`Migration framework <migration>`.
|
||||
* `monitor <https://gitlab.com/qemu-project/qemu/-/tree/master/monitor>`_:
|
||||
`Monitor <QEMU monitor>` implementation (HMP & QMP).
|
||||
* `nbd <https://gitlab.com/qemu-project/qemu/-/tree/master/nbd>`_:
|
||||
@ -193,7 +193,7 @@ yet, so sometimes the source code is all you have.
|
||||
- `lcitool <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/lcitool>`_:
|
||||
Generate dockerfiles for CI containers.
|
||||
- `migration <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/migration>`_:
|
||||
Test scripts and data for `Migration framework <migration>`.
|
||||
Test scripts and data for :ref:`Migration framework <migration>`.
|
||||
- `multiboot <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/multiboot>`_:
|
||||
Test multiboot functionality for x86_64/i386.
|
||||
- `qapi-schema <https://gitlab.com/qemu-project/qemu/-/tree/master/tests/qapi-schema>`_:
|
||||
|
@ -12,4 +12,5 @@ some of the basics if you are adding new files and targets to the build.
|
||||
kconfig
|
||||
docs
|
||||
qapi-code-gen
|
||||
qapi-domain
|
||||
control-flow-integrity
|
||||
|
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
|
||||
host systems. This is provided by the `Migration framework<migration>`.
|
||||
host systems. This is provided by the :ref:`Migration framework<migration>`.
|
||||
|
||||
NBD
|
||||
---
|
||||
@ -212,14 +212,14 @@ machine emulator and virtualizer.
|
||||
QOM
|
||||
---
|
||||
|
||||
`QEMU Object Model <qom>` is an object oriented API used to define various
|
||||
devices and hardware in the QEMU codebase.
|
||||
:ref:`QEMU Object Model <qom>` is an object oriented API used to define
|
||||
various devices and hardware in the QEMU codebase.
|
||||
|
||||
Record/replay
|
||||
-------------
|
||||
|
||||
`Record/replay <replay>` is a feature of QEMU allowing to have a deterministic
|
||||
and reproducible execution of a virtual machine.
|
||||
:ref:`Record/replay <replay>` is a feature of QEMU allowing to have a
|
||||
deterministic and reproducible execution of a virtual machine.
|
||||
|
||||
Rust
|
||||
----
|
||||
|
@ -7,3 +7,4 @@ QEMU QMP Reference Manual
|
||||
:depth: 3
|
||||
|
||||
.. 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 {
|
||||
border-top: none;
|
||||
border-left: solid 3px #ccc;
|
||||
background-color: #f0f0f0;
|
||||
border-left: solid 5px #bcc6d2;
|
||||
background-color: #eaedf1;
|
||||
color: black;
|
||||
}
|
||||
|
||||
@ -208,3 +208,97 @@ div[class^="highlight"] pre {
|
||||
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.
|
||||
#
|
||||
# For locating a particular item, please see the `qapi-index`.
|
||||
#
|
||||
# Most of the time their usage is exactly the same as in the user
|
||||
# Monitor, this means that any other document which also describe
|
||||
# commands (the manpage, QEMU's manual, etc) can and should be
|
||||
|
@ -31,34 +31,28 @@ def create_backend(path: str) -> QAPIBackend:
|
||||
|
||||
module_path, dot, class_name = path.rpartition('.')
|
||||
if not dot:
|
||||
print("argument of -B must be of the form MODULE.CLASS",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
raise QAPIError("argument of -B must be of the form MODULE.CLASS")
|
||||
|
||||
try:
|
||||
mod = import_module(module_path)
|
||||
except Exception as ex:
|
||||
print(f"unable to import '{module_path}': {ex}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
raise QAPIError(f"unable to import '{module_path}': {ex}") from ex
|
||||
|
||||
try:
|
||||
klass = getattr(mod, class_name)
|
||||
except AttributeError:
|
||||
print(f"module '{module_path}' has no class '{class_name}'",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except AttributeError as ex:
|
||||
raise QAPIError(
|
||||
f"module '{module_path}' has no class '{class_name}'") from ex
|
||||
|
||||
try:
|
||||
backend = klass()
|
||||
except Exception as ex:
|
||||
print(f"backend '{path}' cannot be instantiated: {ex}",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
raise QAPIError(
|
||||
f"backend '{path}' cannot be instantiated: {ex}") from ex
|
||||
|
||||
if not isinstance(backend, QAPIBackend):
|
||||
print(f"backend '{path}' must be an instance of QAPIBackend",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
raise QAPIError(
|
||||
f"backend '{path}' must be an instance of QAPIBackend")
|
||||
|
||||
return backend
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
# This work is licensed under the terms of the GNU GPL, version 2.
|
||||
# See the COPYING file in the top-level directory.
|
||||
|
||||
import enum
|
||||
import os
|
||||
import re
|
||||
from typing import (
|
||||
@ -574,7 +575,10 @@ class QAPISchemaParser:
|
||||
)
|
||||
raise QAPIParseError(self, emsg)
|
||||
|
||||
doc.new_tagged_section(self.info, match.group(1))
|
||||
doc.new_tagged_section(
|
||||
self.info,
|
||||
QAPIDoc.Kind.from_string(match.group(1))
|
||||
)
|
||||
text = line[match.end():]
|
||||
if text:
|
||||
doc.append_line(text)
|
||||
@ -585,7 +589,7 @@ class QAPISchemaParser:
|
||||
self,
|
||||
"unexpected '=' markup in definition documentation")
|
||||
else:
|
||||
# tag-less paragraph
|
||||
# plain paragraph
|
||||
doc.ensure_untagged_section(self.info)
|
||||
doc.append_line(line)
|
||||
line = self.get_doc_paragraph(doc)
|
||||
@ -634,23 +638,51 @@ class QAPIDoc:
|
||||
Free-form documentation blocks consist only of a body section.
|
||||
"""
|
||||
|
||||
class Kind(enum.Enum):
|
||||
PLAIN = 0
|
||||
MEMBER = 1
|
||||
FEATURE = 2
|
||||
RETURNS = 3
|
||||
ERRORS = 4
|
||||
SINCE = 5
|
||||
TODO = 6
|
||||
|
||||
@staticmethod
|
||||
def from_string(kind: str) -> 'QAPIDoc.Kind':
|
||||
return QAPIDoc.Kind[kind.upper()]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name.title()
|
||||
|
||||
class Section:
|
||||
# pylint: disable=too-few-public-methods
|
||||
def __init__(self, info: QAPISourceInfo,
|
||||
tag: Optional[str] = None):
|
||||
def __init__(
|
||||
self,
|
||||
info: QAPISourceInfo,
|
||||
kind: 'QAPIDoc.Kind',
|
||||
):
|
||||
# section source info, i.e. where it begins
|
||||
self.info = info
|
||||
# section tag, if any ('Returns', '@name', ...)
|
||||
self.tag = tag
|
||||
# section kind
|
||||
self.kind = kind
|
||||
# section text without tag
|
||||
self.text = ''
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<QAPIDoc.Section kind={self.kind!r} text={self.text!r}>"
|
||||
|
||||
def append_line(self, line: str) -> None:
|
||||
self.text += line + '\n'
|
||||
|
||||
class ArgSection(Section):
|
||||
def __init__(self, info: QAPISourceInfo, tag: str):
|
||||
super().__init__(info, tag)
|
||||
def __init__(
|
||||
self,
|
||||
info: QAPISourceInfo,
|
||||
kind: 'QAPIDoc.Kind',
|
||||
name: str
|
||||
):
|
||||
super().__init__(info, kind)
|
||||
self.name = name
|
||||
self.member: Optional['QAPISchemaMember'] = None
|
||||
|
||||
def connect(self, member: 'QAPISchemaMember') -> None:
|
||||
@ -662,7 +694,9 @@ class QAPIDoc:
|
||||
# definition doc's symbol, None for free-form doc
|
||||
self.symbol: Optional[str] = symbol
|
||||
# the sections in textual order
|
||||
self.all_sections: List[QAPIDoc.Section] = [QAPIDoc.Section(info)]
|
||||
self.all_sections: List[QAPIDoc.Section] = [
|
||||
QAPIDoc.Section(info, QAPIDoc.Kind.PLAIN)
|
||||
]
|
||||
# the body section
|
||||
self.body: Optional[QAPIDoc.Section] = self.all_sections[0]
|
||||
# dicts mapping parameter/feature names to their description
|
||||
@ -679,55 +713,71 @@ class QAPIDoc:
|
||||
def end(self) -> None:
|
||||
for section in self.all_sections:
|
||||
section.text = section.text.strip('\n')
|
||||
if section.tag is not None and section.text == '':
|
||||
if section.kind != QAPIDoc.Kind.PLAIN and section.text == '':
|
||||
raise QAPISemError(
|
||||
section.info, "text required after '%s:'" % section.tag)
|
||||
section.info, "text required after '%s:'" % section.kind)
|
||||
|
||||
def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
|
||||
if self.all_sections and not self.all_sections[-1].tag:
|
||||
kind = QAPIDoc.Kind.PLAIN
|
||||
|
||||
if self.all_sections and self.all_sections[-1].kind == kind:
|
||||
# extend current section
|
||||
self.all_sections[-1].text += '\n'
|
||||
section = self.all_sections[-1]
|
||||
if not section.text:
|
||||
# Section is empty so far; update info to start *here*.
|
||||
section.info = info
|
||||
section.text += '\n'
|
||||
return
|
||||
|
||||
# start new section
|
||||
section = self.Section(info)
|
||||
section = self.Section(info, kind)
|
||||
self.sections.append(section)
|
||||
self.all_sections.append(section)
|
||||
|
||||
def new_tagged_section(self, info: QAPISourceInfo, tag: str) -> None:
|
||||
section = self.Section(info, tag)
|
||||
if tag == 'Returns':
|
||||
def new_tagged_section(
|
||||
self,
|
||||
info: QAPISourceInfo,
|
||||
kind: 'QAPIDoc.Kind',
|
||||
) -> None:
|
||||
section = self.Section(info, kind)
|
||||
if kind == QAPIDoc.Kind.RETURNS:
|
||||
if self.returns:
|
||||
raise QAPISemError(
|
||||
info, "duplicated '%s' section" % tag)
|
||||
info, "duplicated '%s' section" % kind)
|
||||
self.returns = section
|
||||
elif tag == 'Errors':
|
||||
elif kind == QAPIDoc.Kind.ERRORS:
|
||||
if self.errors:
|
||||
raise QAPISemError(
|
||||
info, "duplicated '%s' section" % tag)
|
||||
info, "duplicated '%s' section" % kind)
|
||||
self.errors = section
|
||||
elif tag == 'Since':
|
||||
elif kind == QAPIDoc.Kind.SINCE:
|
||||
if self.since:
|
||||
raise QAPISemError(
|
||||
info, "duplicated '%s' section" % tag)
|
||||
info, "duplicated '%s' section" % kind)
|
||||
self.since = section
|
||||
self.sections.append(section)
|
||||
self.all_sections.append(section)
|
||||
|
||||
def _new_description(self, info: QAPISourceInfo, name: str,
|
||||
desc: Dict[str, ArgSection]) -> None:
|
||||
def _new_description(
|
||||
self,
|
||||
info: QAPISourceInfo,
|
||||
name: str,
|
||||
kind: 'QAPIDoc.Kind',
|
||||
desc: Dict[str, ArgSection]
|
||||
) -> None:
|
||||
if not name:
|
||||
raise QAPISemError(info, "invalid parameter name")
|
||||
if name in desc:
|
||||
raise QAPISemError(info, "'%s' parameter name duplicated" % name)
|
||||
section = self.ArgSection(info, '@' + name)
|
||||
section = self.ArgSection(info, kind, name)
|
||||
self.all_sections.append(section)
|
||||
desc[name] = section
|
||||
|
||||
def new_argument(self, info: QAPISourceInfo, name: str) -> None:
|
||||
self._new_description(info, name, self.args)
|
||||
self._new_description(info, name, QAPIDoc.Kind.MEMBER, self.args)
|
||||
|
||||
def new_feature(self, info: QAPISourceInfo, name: str) -> None:
|
||||
self._new_description(info, name, self.features)
|
||||
self._new_description(info, name, QAPIDoc.Kind.FEATURE, self.features)
|
||||
|
||||
def append_line(self, line: str) -> None:
|
||||
self.all_sections[-1].append_line(line)
|
||||
@ -739,8 +789,23 @@ class QAPIDoc:
|
||||
raise QAPISemError(member.info,
|
||||
"%s '%s' lacks documentation"
|
||||
% (member.role, member.name))
|
||||
self.args[member.name] = QAPIDoc.ArgSection(
|
||||
self.info, '@' + member.name)
|
||||
# Insert stub documentation section for missing member docs.
|
||||
# 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)
|
||||
|
||||
def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
|
||||
|
@ -47,9 +47,9 @@ class QAPISourceInfo:
|
||||
self.defn_meta = meta
|
||||
self.defn_name = name
|
||||
|
||||
def next_line(self: T) -> T:
|
||||
def next_line(self: T, n: int = 1) -> T:
|
||||
info = copy.copy(self)
|
||||
info.line += 1
|
||||
info.line += n
|
||||
return info
|
||||
|
||||
def loc(self) -> str:
|
||||
|
@ -113,7 +113,7 @@ The _one_ {and only}, description on the same line
|
||||
Also _one_ {and only}
|
||||
feature=enum-member-feat
|
||||
a member feature
|
||||
section=None
|
||||
section=Plain
|
||||
@two is undocumented
|
||||
doc symbol=Base
|
||||
body=
|
||||
@ -171,15 +171,15 @@ description starts on the same line
|
||||
a feature
|
||||
feature=cmd-feat2
|
||||
another feature
|
||||
section=None
|
||||
section=Plain
|
||||
.. note:: @arg3 is undocumented
|
||||
section=Returns
|
||||
@Object
|
||||
section=Errors
|
||||
some
|
||||
section=TODO
|
||||
section=Todo
|
||||
frobnicate
|
||||
section=None
|
||||
section=Plain
|
||||
.. admonition:: Notes
|
||||
|
||||
- Lorem ipsum dolor sit amet
|
||||
@ -212,7 +212,7 @@ If you're bored enough to read this, go see a video of boxed cats
|
||||
a feature
|
||||
feature=cmd-feat2
|
||||
another feature
|
||||
section=None
|
||||
section=Plain
|
||||
.. qmp-example::
|
||||
|
||||
-> "this example"
|
||||
|
@ -122,7 +122,7 @@ def test_frontend(fname):
|
||||
for feat, section in doc.features.items():
|
||||
print(' feature=%s\n%s' % (feat, section.text))
|
||||
for section in doc.sections:
|
||||
print(' section=%s\n%s' % (section.tag, section.text))
|
||||
print(' section=%s\n%s' % (section.kind, section.text))
|
||||
|
||||
|
||||
def open_test_result(dir_name, file_name, update):
|
||||
|
Loading…
x
Reference in New Issue
Block a user