 a1fe2cd443
			
		
	
	
		a1fe2cd443
		
	
	
	
	
		
			
			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> Message-ID: <20250311034303.75779-30-jsnow@redhat.com> Acked-by: Markus Armbruster <armbru@redhat.com> Signed-off-by: Markus Armbruster <armbru@redhat.com>
		
			
				
	
	
		
			175 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			175 lines
		
	
	
		
			5.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """
 | |
| Sphinx cross-version compatibility goop
 | |
| """
 | |
| 
 | |
| 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, 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]
 | |
| 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
 |