@@ -2,14 +2,31 @@
Sphinx cross-version compatibility goop
"""
-from typing import Callable
+import re
+from typing import (
+ Any,
+ Callable,
+ Optional,
+ Type,
+)
+from docutils import nodes
from docutils.nodes import Element, Node, Text
import sphinx
-from sphinx import addnodes
-from sphinx.util import nodes
-from sphinx.util.docutils import SphinxDirective, switch_source_input
+from sphinx import addnodes, util
+from sphinx.environment import BuildEnvironment
+from sphinx.roles import XRefRole
+from sphinx.util import docfields
+from sphinx.util.docutils import (
+ ReferenceRole,
+ SphinxDirective,
+ switch_source_input,
+)
+from sphinx.util.typing import TextlikeNode
+
+
+MAKE_XREF_WORKAROUND = sphinx.version_info[:3] < (4, 1, 0)
SpaceNode: Callable[[str], Node]
@@ -36,7 +53,7 @@ def nested_parse_with_titles(
try:
# Modern sphinx (6.2.0+) supports proper offsetting for
# nested parse error context management
- nodes.nested_parse_with_titles(
+ util.nodes.nested_parse_with_titles(
directive.state,
directive.content,
content_node,
@@ -45,6 +62,113 @@ def nested_parse_with_titles(
except TypeError:
# No content_offset argument. Fall back to SSI method.
with switch_source_input(directive.state, directive.content):
- nodes.nested_parse_with_titles(
+ util.nodes.nested_parse_with_titles(
directive.state, directive.content, content_node
)
+
+
+# ###########################################
+# xref compatibility hacks for Sphinx < 4.1 #
+# ###########################################
+
+# When we require >= Sphinx 4.1, the following function and the
+# subsequent 3 compatibility classes can be removed. Anywhere in
+# qapi_domain that uses one of these Compat* types can be switched to
+# using the garden-variety lib-provided classes with no trickery.
+
+
+def _compat_make_xref( # pylint: disable=unused-argument
+ self: sphinx.util.docfields.Field,
+ rolename: str,
+ domain: str,
+ target: str,
+ innernode: Type[TextlikeNode] = addnodes.literal_emphasis,
+ contnode: Optional[Node] = None,
+ env: Optional[BuildEnvironment] = None,
+ inliner: Any = None,
+ location: Any = None,
+) -> Node:
+ """
+ Compatibility workaround for Sphinx versions prior to 4.1.0.
+
+ Older sphinx versions do not use the domain's XRefRole for parsing
+ and formatting cross-references, so we need to perform this magick
+ ourselves to avoid needing to write the parser/formatter in two
+ separate places.
+
+ This workaround isn't brick-for-brick compatible with modern Sphinx
+ versions, because we do not have access to the parent directive's
+ state during this parsing like we do in more modern versions.
+
+ It's no worse than what pre-Sphinx 4.1.0 does, so... oh well!
+ """
+
+ # Yes, this function is gross. Pre-4.1 support is a miracle.
+ # pylint: disable=too-many-locals
+
+ assert env
+ # Note: Sphinx's own code ignores the type warning here, too.
+ if not rolename:
+ return contnode or innernode(target, target) # type: ignore[call-arg]
+
+ # Get the role instance, but don't *execute it* - we lack the
+ # correct state to do so. Instead, we'll just use its public
+ # methods to do our reference formatting, and emulate the rest.
+ role = env.get_domain(domain).roles[rolename]
+ assert isinstance(role, XRefRole)
+
+ # XRefRole features not supported by this compatibility shim;
+ # these were not supported in Sphinx 3.x either, so nothing of
+ # value is really lost.
+ assert not target.startswith("!")
+ assert not re.match(ReferenceRole.explicit_title_re, target)
+ assert not role.lowercase
+ assert not role.fix_parens
+
+ # Code below based mostly on sphinx.roles.XRefRole; run() and
+ # create_xref_node()
+ options = {
+ "refdoc": env.docname,
+ "refdomain": domain,
+ "reftype": rolename,
+ "refexplicit": False,
+ "refwarn": role.warn_dangling,
+ }
+ refnode = role.nodeclass(target, **options)
+ title, target = role.process_link(env, refnode, False, target, target)
+ refnode["reftarget"] = target
+ classes = ["xref", domain, f"{domain}-{rolename}"]
+ refnode += role.innernodeclass(target, title, classes=classes)
+
+ # This is the very gross part of the hack. Normally,
+ # result_nodes takes a document object to which we would pass
+ # self.inliner.document. Prior to Sphinx 4.1, we don't *have* an
+ # inliner to pass, so we have nothing to pass here. However, the
+ # actual implementation of role.result_nodes in this case
+ # doesn't actually use that argument, so this winds up being
+ # ... fine. Rest easy at night knowing this code only runs under
+ # old versions of Sphinx, so at least it won't change in the
+ # future on us and lead to surprising new failures.
+ # Gross, I know.
+ result_nodes, _messages = role.result_nodes(
+ None, # type: ignore
+ env,
+ refnode,
+ is_ref=True,
+ )
+ return nodes.inline(target, "", *result_nodes)
+
+
+class CompatField(docfields.Field):
+ if MAKE_XREF_WORKAROUND:
+ make_xref = _compat_make_xref
+
+
+class CompatGroupedField(docfields.GroupedField):
+ if MAKE_XREF_WORKAROUND:
+ make_xref = _compat_make_xref
+
+
+class CompatTypedField(docfields.TypedField):
+ if MAKE_XREF_WORKAROUND:
+ make_xref = _compat_make_xref
@@ -24,7 +24,13 @@
from docutils import nodes
from docutils.parsers.rst import directives
-from compat import KeywordNode, SpaceNode
+from compat import (
+ CompatField,
+ CompatGroupedField,
+ CompatTypedField,
+ KeywordNode,
+ SpaceNode,
+)
from sphinx import addnodes
from sphinx.addnodes import desc_signature, pending_xref
from sphinx.directives import ObjectDescription
@@ -37,7 +43,6 @@
from sphinx.locale import _, __
from sphinx.roles import XRefRole
from sphinx.util import logging
-from sphinx.util.docfields import Field, GroupedField, TypedField
from sphinx.util.nodes import make_id, make_refnode
@@ -228,7 +233,7 @@ class QAPIObject(QAPIDescription):
doc_field_types = [
# :feat name: descr
- GroupedField(
+ CompatGroupedField(
"feature",
label=_("Features"),
names=("feat",),
@@ -429,7 +434,7 @@ class QAPICommand(QAPIObject):
doc_field_types.extend(
[
# :arg TypeName ArgName: descr
- TypedField(
+ CompatTypedField(
"argument",
label=_("Arguments"),
names=("arg",),
@@ -437,14 +442,14 @@ class QAPICommand(QAPIObject):
can_collapse=False,
),
# :error: descr
- Field(
+ CompatField(
"error",
label=_("Errors"),
names=("error", "errors"),
has_arg=False,
),
# :returns TypeName: descr
- GroupedField(
+ CompatGroupedField(
"returnvalue",
label=_("Return"),
rolename="type",
@@ -462,7 +467,7 @@ class QAPIEnum(QAPIObject):
doc_field_types.extend(
[
# :value name: descr
- GroupedField(
+ CompatGroupedField(
"value",
label=_("Values"),
names=("value",),
@@ -479,7 +484,7 @@ class QAPIAlternate(QAPIObject):
doc_field_types.extend(
[
# :alt type name: descr
- TypedField(
+ CompatTypedField(
"alternative",
label=_("Alternatives"),
names=("alt",),
@@ -497,7 +502,7 @@ class QAPIObjectWithMembers(QAPIObject):
doc_field_types.extend(
[
# :member type name: descr
- TypedField(
+ CompatTypedField(
"member",
label=_("Members"),
names=("memb",),
Sphinx < 4.1 handles cross-references ... differently. Factor out and isolate the compatibility goop we need to make cross references work properly in old versions of Sphinx. Yes, it's ugly. Yes, it works. No, I don't want to talk about it. Understand that this patch exists because of the overflowing love in my heart. Signed-off-by: John Snow <jsnow@redhat.com> --- docs/sphinx/compat.py | 136 +++++++++++++++++++++++++++++++++++-- docs/sphinx/qapi_domain.py | 23 ++++--- 2 files changed, 144 insertions(+), 15 deletions(-)