2
0

compat.py 7.5 KB

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