compat.py 5.6 KB

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