xref: /openbmc/qemu/docs/sphinx/compat.py (revision 94d689d0c6f23dc3129e8432c496ccb866788dbf)
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