1abf6bedcSJohn Snow""" 2abf6bedcSJohn SnowSphinx cross-version compatibility goop 3abf6bedcSJohn Snow""" 4abf6bedcSJohn Snow 5a1fe2cd4SJohn Snowimport re 6a1fe2cd4SJohn Snowfrom typing import ( 7*707f2bbbSJohn Snow TYPE_CHECKING, 8a1fe2cd4SJohn Snow Any, 9a1fe2cd4SJohn Snow Callable, 10a1fe2cd4SJohn Snow Optional, 11a1fe2cd4SJohn Snow Type, 12a1fe2cd4SJohn Snow) 13abf6bedcSJohn Snow 14a1fe2cd4SJohn Snowfrom docutils import nodes 156d64a27cSJohn Snowfrom docutils.nodes import Element, Node, Text 16*707f2bbbSJohn Snowfrom docutils.statemachine import StringList 176d64a27cSJohn Snow 186d64a27cSJohn Snowimport sphinx 19a1fe2cd4SJohn Snowfrom sphinx import addnodes, util 20*707f2bbbSJohn Snowfrom sphinx.directives import ObjectDescription 21a1fe2cd4SJohn Snowfrom sphinx.environment import BuildEnvironment 22a1fe2cd4SJohn Snowfrom sphinx.roles import XRefRole 23a1fe2cd4SJohn Snowfrom sphinx.util import docfields 24a1fe2cd4SJohn Snowfrom sphinx.util.docutils import ( 25a1fe2cd4SJohn Snow ReferenceRole, 26a1fe2cd4SJohn Snow SphinxDirective, 27a1fe2cd4SJohn Snow switch_source_input, 28a1fe2cd4SJohn Snow) 29a1fe2cd4SJohn Snowfrom sphinx.util.typing import TextlikeNode 30a1fe2cd4SJohn Snow 31a1fe2cd4SJohn Snow 32a1fe2cd4SJohn SnowMAKE_XREF_WORKAROUND = sphinx.version_info[:3] < (4, 1, 0) 33abf6bedcSJohn Snow 34abf6bedcSJohn Snow 356d64a27cSJohn SnowSpaceNode: Callable[[str], Node] 366d64a27cSJohn SnowKeywordNode: Callable[[str, str], Node] 376d64a27cSJohn Snow 386d64a27cSJohn Snowif sphinx.version_info[:3] >= (4, 0, 0): 396d64a27cSJohn Snow SpaceNode = addnodes.desc_sig_space 406d64a27cSJohn Snow KeywordNode = addnodes.desc_sig_keyword 416d64a27cSJohn Snowelse: 426d64a27cSJohn Snow SpaceNode = Text 436d64a27cSJohn Snow KeywordNode = addnodes.desc_annotation 446d64a27cSJohn Snow 456d64a27cSJohn Snow 46abf6bedcSJohn Snowdef nested_parse_with_titles( 47abf6bedcSJohn Snow directive: SphinxDirective, content_node: Element 48abf6bedcSJohn Snow) -> None: 49abf6bedcSJohn Snow """ 50abf6bedcSJohn Snow This helper preserves error parsing context across sphinx versions. 51abf6bedcSJohn Snow """ 52abf6bedcSJohn Snow 53abf6bedcSJohn Snow # necessary so that the child nodes get the right source/line set 54abf6bedcSJohn Snow content_node.document = directive.state.document 55abf6bedcSJohn Snow 56abf6bedcSJohn Snow try: 57abf6bedcSJohn Snow # Modern sphinx (6.2.0+) supports proper offsetting for 58abf6bedcSJohn Snow # nested parse error context management 59a1fe2cd4SJohn Snow util.nodes.nested_parse_with_titles( 60abf6bedcSJohn Snow directive.state, 61abf6bedcSJohn Snow directive.content, 62abf6bedcSJohn Snow content_node, 63abf6bedcSJohn Snow content_offset=directive.content_offset, 64abf6bedcSJohn Snow ) 65abf6bedcSJohn Snow except TypeError: 66abf6bedcSJohn Snow # No content_offset argument. Fall back to SSI method. 67abf6bedcSJohn Snow with switch_source_input(directive.state, directive.content): 68a1fe2cd4SJohn Snow util.nodes.nested_parse_with_titles( 69abf6bedcSJohn Snow directive.state, directive.content, content_node 70abf6bedcSJohn Snow ) 71a1fe2cd4SJohn Snow 72a1fe2cd4SJohn Snow 73a1fe2cd4SJohn Snow# ########################################### 74a1fe2cd4SJohn Snow# xref compatibility hacks for Sphinx < 4.1 # 75a1fe2cd4SJohn Snow# ########################################### 76a1fe2cd4SJohn Snow 77a1fe2cd4SJohn Snow# When we require >= Sphinx 4.1, the following function and the 78a1fe2cd4SJohn Snow# subsequent 3 compatibility classes can be removed. Anywhere in 79a1fe2cd4SJohn Snow# qapi_domain that uses one of these Compat* types can be switched to 80a1fe2cd4SJohn Snow# using the garden-variety lib-provided classes with no trickery. 81a1fe2cd4SJohn Snow 82a1fe2cd4SJohn Snow 83a1fe2cd4SJohn Snowdef _compat_make_xref( # pylint: disable=unused-argument 84a1fe2cd4SJohn Snow self: sphinx.util.docfields.Field, 85a1fe2cd4SJohn Snow rolename: str, 86a1fe2cd4SJohn Snow domain: str, 87a1fe2cd4SJohn Snow target: str, 88a1fe2cd4SJohn Snow innernode: Type[TextlikeNode] = addnodes.literal_emphasis, 89a1fe2cd4SJohn Snow contnode: Optional[Node] = None, 90a1fe2cd4SJohn Snow env: Optional[BuildEnvironment] = None, 91a1fe2cd4SJohn Snow inliner: Any = None, 92a1fe2cd4SJohn Snow location: Any = None, 93a1fe2cd4SJohn Snow) -> Node: 94a1fe2cd4SJohn Snow """ 95a1fe2cd4SJohn Snow Compatibility workaround for Sphinx versions prior to 4.1.0. 96a1fe2cd4SJohn Snow 97a1fe2cd4SJohn Snow Older sphinx versions do not use the domain's XRefRole for parsing 98a1fe2cd4SJohn Snow and formatting cross-references, so we need to perform this magick 99a1fe2cd4SJohn Snow ourselves to avoid needing to write the parser/formatter in two 100a1fe2cd4SJohn Snow separate places. 101a1fe2cd4SJohn Snow 102a1fe2cd4SJohn Snow This workaround isn't brick-for-brick compatible with modern Sphinx 103a1fe2cd4SJohn Snow versions, because we do not have access to the parent directive's 104a1fe2cd4SJohn Snow state during this parsing like we do in more modern versions. 105a1fe2cd4SJohn Snow 106a1fe2cd4SJohn Snow It's no worse than what pre-Sphinx 4.1.0 does, so... oh well! 107a1fe2cd4SJohn Snow """ 108a1fe2cd4SJohn Snow 109a1fe2cd4SJohn Snow # Yes, this function is gross. Pre-4.1 support is a miracle. 110a1fe2cd4SJohn Snow # pylint: disable=too-many-locals 111a1fe2cd4SJohn Snow 112a1fe2cd4SJohn Snow assert env 113a1fe2cd4SJohn Snow # Note: Sphinx's own code ignores the type warning here, too. 114a1fe2cd4SJohn Snow if not rolename: 115a1fe2cd4SJohn Snow return contnode or innernode(target, target) # type: ignore[call-arg] 116a1fe2cd4SJohn Snow 117a1fe2cd4SJohn Snow # Get the role instance, but don't *execute it* - we lack the 118a1fe2cd4SJohn Snow # correct state to do so. Instead, we'll just use its public 119a1fe2cd4SJohn Snow # methods to do our reference formatting, and emulate the rest. 120a1fe2cd4SJohn Snow role = env.get_domain(domain).roles[rolename] 121a1fe2cd4SJohn Snow assert isinstance(role, XRefRole) 122a1fe2cd4SJohn Snow 123a1fe2cd4SJohn Snow # XRefRole features not supported by this compatibility shim; 124a1fe2cd4SJohn Snow # these were not supported in Sphinx 3.x either, so nothing of 125a1fe2cd4SJohn Snow # value is really lost. 126a1fe2cd4SJohn Snow assert not target.startswith("!") 127a1fe2cd4SJohn Snow assert not re.match(ReferenceRole.explicit_title_re, target) 128a1fe2cd4SJohn Snow assert not role.lowercase 129a1fe2cd4SJohn Snow assert not role.fix_parens 130a1fe2cd4SJohn Snow 131a1fe2cd4SJohn Snow # Code below based mostly on sphinx.roles.XRefRole; run() and 132a1fe2cd4SJohn Snow # create_xref_node() 133a1fe2cd4SJohn Snow options = { 134a1fe2cd4SJohn Snow "refdoc": env.docname, 135a1fe2cd4SJohn Snow "refdomain": domain, 136a1fe2cd4SJohn Snow "reftype": rolename, 137a1fe2cd4SJohn Snow "refexplicit": False, 138a1fe2cd4SJohn Snow "refwarn": role.warn_dangling, 139a1fe2cd4SJohn Snow } 140a1fe2cd4SJohn Snow refnode = role.nodeclass(target, **options) 141a1fe2cd4SJohn Snow title, target = role.process_link(env, refnode, False, target, target) 142a1fe2cd4SJohn Snow refnode["reftarget"] = target 143a1fe2cd4SJohn Snow classes = ["xref", domain, f"{domain}-{rolename}"] 144a1fe2cd4SJohn Snow refnode += role.innernodeclass(target, title, classes=classes) 145a1fe2cd4SJohn Snow 146a1fe2cd4SJohn Snow # This is the very gross part of the hack. Normally, 147a1fe2cd4SJohn Snow # result_nodes takes a document object to which we would pass 148a1fe2cd4SJohn Snow # self.inliner.document. Prior to Sphinx 4.1, we don't *have* an 149a1fe2cd4SJohn Snow # inliner to pass, so we have nothing to pass here. However, the 150a1fe2cd4SJohn Snow # actual implementation of role.result_nodes in this case 151a1fe2cd4SJohn Snow # doesn't actually use that argument, so this winds up being 152a1fe2cd4SJohn Snow # ... fine. Rest easy at night knowing this code only runs under 153a1fe2cd4SJohn Snow # old versions of Sphinx, so at least it won't change in the 154a1fe2cd4SJohn Snow # future on us and lead to surprising new failures. 155a1fe2cd4SJohn Snow # Gross, I know. 156a1fe2cd4SJohn Snow result_nodes, _messages = role.result_nodes( 157a1fe2cd4SJohn Snow None, # type: ignore 158a1fe2cd4SJohn Snow env, 159a1fe2cd4SJohn Snow refnode, 160a1fe2cd4SJohn Snow is_ref=True, 161a1fe2cd4SJohn Snow ) 162a1fe2cd4SJohn Snow return nodes.inline(target, "", *result_nodes) 163a1fe2cd4SJohn Snow 164a1fe2cd4SJohn Snow 165a1fe2cd4SJohn Snowclass CompatField(docfields.Field): 166a1fe2cd4SJohn Snow if MAKE_XREF_WORKAROUND: 167a1fe2cd4SJohn Snow make_xref = _compat_make_xref 168a1fe2cd4SJohn Snow 169a1fe2cd4SJohn Snow 170a1fe2cd4SJohn Snowclass CompatGroupedField(docfields.GroupedField): 171a1fe2cd4SJohn Snow if MAKE_XREF_WORKAROUND: 172a1fe2cd4SJohn Snow make_xref = _compat_make_xref 173a1fe2cd4SJohn Snow 174a1fe2cd4SJohn Snow 175a1fe2cd4SJohn Snowclass CompatTypedField(docfields.TypedField): 176a1fe2cd4SJohn Snow if MAKE_XREF_WORKAROUND: 177a1fe2cd4SJohn Snow make_xref = _compat_make_xref 178*707f2bbbSJohn Snow 179*707f2bbbSJohn Snow 180*707f2bbbSJohn Snow# ################################################################ 181*707f2bbbSJohn Snow# Nested parsing error location fix for Sphinx 5.3.0 < x < 6.2.0 # 182*707f2bbbSJohn Snow# ################################################################ 183*707f2bbbSJohn Snow 184*707f2bbbSJohn Snow# When we require Sphinx 4.x, the TYPE_CHECKING hack where we avoid 185*707f2bbbSJohn Snow# subscripting ObjectDescription at runtime can be removed in favor of 186*707f2bbbSJohn Snow# just always subscripting the class. 187*707f2bbbSJohn Snow 188*707f2bbbSJohn Snow# When we require Sphinx > 6.2.0, the rest of this compatibility hack 189*707f2bbbSJohn Snow# can be dropped and QAPIObject can just inherit directly from 190*707f2bbbSJohn Snow# ObjectDescription[Signature]. 191*707f2bbbSJohn Snow 192*707f2bbbSJohn SnowSOURCE_LOCATION_FIX = (5, 3, 0) <= sphinx.version_info[:3] < (6, 2, 0) 193*707f2bbbSJohn Snow 194*707f2bbbSJohn SnowSignature = str 195*707f2bbbSJohn Snow 196*707f2bbbSJohn Snow 197*707f2bbbSJohn Snowif TYPE_CHECKING: 198*707f2bbbSJohn Snow _BaseClass = ObjectDescription[Signature] 199*707f2bbbSJohn Snowelse: 200*707f2bbbSJohn Snow _BaseClass = ObjectDescription 201*707f2bbbSJohn Snow 202*707f2bbbSJohn Snow 203*707f2bbbSJohn Snowclass ParserFix(_BaseClass): 204*707f2bbbSJohn Snow 205*707f2bbbSJohn Snow _temp_content: StringList 206*707f2bbbSJohn Snow _temp_offset: int 207*707f2bbbSJohn Snow _temp_node: Optional[addnodes.desc_content] 208*707f2bbbSJohn Snow 209*707f2bbbSJohn Snow def before_content(self) -> None: 210*707f2bbbSJohn Snow # Work around a sphinx bug and parse the content ourselves. 211*707f2bbbSJohn Snow self._temp_content = self.content 212*707f2bbbSJohn Snow self._temp_offset = self.content_offset 213*707f2bbbSJohn Snow self._temp_node = None 214*707f2bbbSJohn Snow 215*707f2bbbSJohn Snow if SOURCE_LOCATION_FIX: 216*707f2bbbSJohn Snow self._temp_node = addnodes.desc_content() 217*707f2bbbSJohn Snow self.state.nested_parse( 218*707f2bbbSJohn Snow self.content, self.content_offset, self._temp_node 219*707f2bbbSJohn Snow ) 220*707f2bbbSJohn Snow # Sphinx will try to parse the content block itself, 221*707f2bbbSJohn Snow # Give it nothingness to parse instead. 222*707f2bbbSJohn Snow self.content = StringList() 223*707f2bbbSJohn Snow self.content_offset = 0 224*707f2bbbSJohn Snow 225*707f2bbbSJohn Snow def transform_content(self, content_node: addnodes.desc_content) -> None: 226*707f2bbbSJohn Snow # Sphinx workaround: Inject our parsed content and restore state. 227*707f2bbbSJohn Snow if self._temp_node: 228*707f2bbbSJohn Snow content_node += self._temp_node.children 229*707f2bbbSJohn Snow self.content = self._temp_content 230*707f2bbbSJohn Snow self.content_offset = self._temp_offset 231