@@ -2,14 +2,32 @@
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.docutils import SphinxDirective, switch_source_input
+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.nodes import nested_parse_with_titles
+from sphinx.util.typing import TextlikeNode
+
+
+MAKE_XREF_WORKAROUND = sphinx.version_info[:3] < (4, 1, 0)
space_node: Callable[[str], Node]
@@ -46,3 +64,95 @@ def nested_parse(directive: SphinxDirective, content_node: Element) -> None:
nested_parse_with_titles(
directive.state, directive.content, content_node
)
+
+
+class CompatFieldMixin:
+ """
+ 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!
+ """
+
+ def make_xref(
+ self,
+ rolename: str,
+ domain: str,
+ target: str,
+ innernode: Type[TextlikeNode] = addnodes.literal_emphasis,
+ contnode: Optional[Node] = None,
+ env: Optional[BuildEnvironment] = None,
+ *args: Any,
+ **kwargs: Any,
+ ) -> Node:
+ print("Using compat make_xref")
+
+ assert env
+ if not rolename:
+ return contnode or innernode(target, target)
+
+ # 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)
+ result_nodes, messages = role.result_nodes(
+ None, # FIXME - normally self.inliner.document ...
+ env,
+ refnode,
+ is_ref=True,
+ )
+ return nodes.inline(target, "", *result_nodes)
+
+
+class CompatField(CompatFieldMixin, docfields.Field):
+ pass
+
+
+class CompatGroupedField(CompatFieldMixin, docfields.GroupedField):
+ pass
+
+
+class CompatTypedField(CompatFieldMixin, docfields.TypedField):
+ pass
+
+
+if not MAKE_XREF_WORKAROUND:
+ Field = docfields.Field
+ GroupedField = docfields.GroupedField
+ TypedField = docfields.TypedField
+else:
+ Field = CompatField
+ GroupedField = CompatGroupedField
+ TypedField = CompatTypedField
@@ -24,7 +24,14 @@
from docutils.statemachine import StringList
from collapse import CollapseNode
-from compat import keyword_node, nested_parse, space_node
+from compat import (
+ Field,
+ GroupedField,
+ TypedField,
+ keyword_node,
+ nested_parse,
+ space_node,
+)
import sphinx
from sphinx import addnodes
from sphinx.addnodes import desc_signature, pending_xref
@@ -38,24 +45,18 @@
from sphinx.locale import _, __
from sphinx.roles import XRefRole
from sphinx.util import logging
-from sphinx.util.docfields import (
- DocFieldTransformer,
- Field,
- GroupedField,
- TypedField,
-)
+from sphinx.util.docfields import DocFieldTransformer
from sphinx.util.docutils import SphinxDirective
from sphinx.util.nodes import make_id, make_refnode
if TYPE_CHECKING:
from docutils.nodes import Element, Node
- from docutils.parsers.rst.states import Inliner
from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.environment import BuildEnvironment
- from sphinx.util.typing import OptionSpec, TextlikeNode
+ from sphinx.util.typing import OptionSpec
logger = logging.getLogger(__name__)
@@ -82,111 +83,8 @@ class ObjectEntry(NamedTuple):
aliased: bool
-class QAPIXrefMixin:
- def make_xref(
- self,
- rolename: str,
- domain: str,
- target: str,
- innernode: type[TextlikeNode] = nodes.literal,
- contnode: Optional[Node] = None,
- env: Optional[BuildEnvironment] = None,
- inliner: Optional[Inliner] = None,
- location: Optional[Node] = None,
- ) -> Node:
- # make_xref apparently has a mode of operation where the inliner
- # class argument is passed to the role object
- # (e.g. QAPIXRefRole) to construct the final result; passing
- # inliner = location = None forces it into its legacy mode where
- # it returns a pending_xref node instead.
- # (This is how the built-in Python domain behaves.)
- result = super().make_xref( # type: ignore[misc]
- rolename,
- domain,
- target,
- innernode=innernode,
- contnode=contnode,
- env=env,
- inliner=None,
- location=None,
- )
- if isinstance(result, pending_xref):
- assert env is not None
- # Add domain-specific context information to the pending reference.
- result["refspecific"] = True
- result["qapi:module"] = env.ref_context.get("qapi:module")
-
- assert isinstance(result, nodes.Node)
- return result
-
- def make_xrefs(
- self,
- rolename: str,
- domain: str,
- target: str,
- innernode: type[TextlikeNode] = nodes.literal,
- contnode: Optional[Node] = None,
- env: Optional[BuildEnvironment] = None,
- inliner: Optional[Inliner] = None,
- location: Optional[Node] = None,
- ) -> list[Node]:
- # Note: this function is called on up to three fields of text:
- # (1) The field name argument (e.g. member/arg name)
- # (2) The field name type (e.g. member/arg type)
- # (3) The field *body* text, for Fields that do not take arguments.
-
- list_type = False
- optional = False
-
- # If the rolename is qapi:type, we know we are processing a type
- # and not an arg/memb name or field body text.
- if rolename == "type":
- # force the innernode class to be a literal.
- innernode = nodes.literal
-
- # 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("?"):
- 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("]"):
- list_type = True
- target = target[1:-1]
-
- # When processing Fields with bodyrolename="type", contnode
- # will be present, which indicates that the body has already
- # been parsed into nodes. We don't want that, actually:
- # we'll re-create our own nodes for it.
- contnode = None
-
- results = []
- result = self.make_xref(
- rolename,
- domain,
- target,
- innernode,
- contnode,
- env,
- inliner,
- location,
- )
- results.append(result)
-
- if list_type:
- results.insert(0, nodes.literal("[", "["))
- results.append(nodes.literal("]", "]"))
- if optional:
- results.append(nodes.Text(", "))
- results.append(nodes.emphasis("?", "optional"))
-
- return results
-
-
class QAPIXRefRole(XRefRole):
+
def process_link(
self,
env: BuildEnvironment,
@@ -196,34 +94,63 @@ def process_link(
target: str,
) -> tuple[str, str]:
refnode["qapi:module"] = env.ref_context.get("qapi:module")
- if not has_explicit_title:
- title = title.lstrip(".") # only has a meaning for the target
- target = target.lstrip("~") # only has a meaning for the title
- # if the first character is a tilde, don't display the module
- # parts of the contents
- if title[0:1] == "~":
- title = title[1:]
- dot = title.rfind(".")
- if dot != -1:
- title = title[dot + 1 :]
- # if the first character is a dot, search more specific namespaces first
- # else search builtins first
- if target[0:1] == ".":
+
+ # 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:]
- refnode["refspecific"] = True
+
+ # 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]]:
-class QAPIGroupedField(QAPIXrefMixin, GroupedField):
- pass
+ # 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("]", "]"))
-class QAPITypedField(QAPIXrefMixin, TypedField):
- pass
+ if node.get("qapi:optional"):
+ results.append(nodes.Text(", "))
+ results.append(nodes.emphasis("?", "optional"))
-
-class QAPIField(QAPIXrefMixin, Field):
- pass
+ return results, []
def since_validator(param: str) -> str:
@@ -633,7 +560,7 @@ class QAPICommand(QAPIObject):
doc_field_types.extend(
[
# :arg TypeName ArgName: descr
- QAPITypedField(
+ TypedField(
"argument",
label=_("Arguments"),
names=("arg",),
@@ -648,7 +575,7 @@ class QAPICommand(QAPIObject):
has_arg=False,
),
# :returns TypeName: descr
- QAPIGroupedField(
+ GroupedField(
"returnvalue",
label=_("Returns"),
rolename="type",
@@ -656,7 +583,7 @@ class QAPICommand(QAPIObject):
can_collapse=True,
),
# :returns-nodesc: TypeName
- QAPIField(
+ Field(
"returnvalue",
label=_("Returns"),
names=("returns-nodesc",),
@@ -691,7 +618,7 @@ class QAPIAlternate(QAPIObject):
doc_field_types.extend(
[
# :choice type name: descr
- QAPITypedField(
+ TypedField(
"choice",
label=_("Choices"),
names=("choice",),
@@ -709,7 +636,7 @@ class QAPIObjectWithMembers(QAPIObject):
doc_field_types.extend(
[
# :member type name: descr
- QAPITypedField(
+ TypedField(
"member",
label=_("Members"),
names=("memb",),
@@ -842,7 +769,7 @@ class Branch(SphinxDirective):
doc_field_types: List[Field] = [
# :arg type name: descr
# :memb type name: descr
- QAPITypedField(
+ TypedField(
"branch-arg-or-memb",
label=":BRANCH:",
names=("arg", "memb"),
Signed-off-by: John Snow <jsnow@redhat.com> --- docs/sphinx/compat.py | 114 +++++++++++++++++++- docs/sphinx/qapi-domain.py | 207 ++++++++++++------------------------- 2 files changed, 179 insertions(+), 142 deletions(-)