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