Sphinx 8.1.2__py3-none-any.whl → 8.2.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of Sphinx might be problematic. Click here for more details.

Files changed (193) hide show
  1. sphinx/__init__.py +8 -4
  2. sphinx/__main__.py +2 -0
  3. sphinx/_cli/__init__.py +2 -5
  4. sphinx/_cli/util/colour.py +34 -11
  5. sphinx/_cli/util/errors.py +128 -61
  6. sphinx/addnodes.py +51 -35
  7. sphinx/application.py +362 -230
  8. sphinx/builders/__init__.py +87 -64
  9. sphinx/builders/_epub_base.py +65 -56
  10. sphinx/builders/changes.py +17 -23
  11. sphinx/builders/dirhtml.py +8 -13
  12. sphinx/builders/epub3.py +70 -38
  13. sphinx/builders/gettext.py +93 -73
  14. sphinx/builders/html/__init__.py +240 -186
  15. sphinx/builders/html/_assets.py +9 -2
  16. sphinx/builders/html/_build_info.py +3 -0
  17. sphinx/builders/latex/__init__.py +64 -54
  18. sphinx/builders/latex/constants.py +14 -11
  19. sphinx/builders/latex/nodes.py +2 -0
  20. sphinx/builders/latex/theming.py +8 -9
  21. sphinx/builders/latex/transforms.py +7 -5
  22. sphinx/builders/linkcheck.py +193 -149
  23. sphinx/builders/manpage.py +17 -17
  24. sphinx/builders/singlehtml.py +28 -16
  25. sphinx/builders/texinfo.py +28 -21
  26. sphinx/builders/text.py +10 -15
  27. sphinx/builders/xml.py +10 -19
  28. sphinx/cmd/build.py +49 -119
  29. sphinx/cmd/make_mode.py +35 -31
  30. sphinx/cmd/quickstart.py +78 -62
  31. sphinx/config.py +265 -163
  32. sphinx/directives/__init__.py +51 -54
  33. sphinx/directives/admonitions.py +107 -0
  34. sphinx/directives/code.py +24 -19
  35. sphinx/directives/other.py +21 -42
  36. sphinx/directives/patches.py +28 -16
  37. sphinx/domains/__init__.py +54 -31
  38. sphinx/domains/_domains_container.py +22 -17
  39. sphinx/domains/_index.py +5 -8
  40. sphinx/domains/c/__init__.py +366 -245
  41. sphinx/domains/c/_ast.py +378 -256
  42. sphinx/domains/c/_ids.py +89 -31
  43. sphinx/domains/c/_parser.py +283 -214
  44. sphinx/domains/c/_symbol.py +269 -198
  45. sphinx/domains/changeset.py +39 -24
  46. sphinx/domains/citation.py +54 -24
  47. sphinx/domains/cpp/__init__.py +517 -362
  48. sphinx/domains/cpp/_ast.py +999 -682
  49. sphinx/domains/cpp/_ids.py +133 -65
  50. sphinx/domains/cpp/_parser.py +746 -588
  51. sphinx/domains/cpp/_symbol.py +692 -489
  52. sphinx/domains/index.py +10 -8
  53. sphinx/domains/javascript.py +152 -74
  54. sphinx/domains/math.py +48 -40
  55. sphinx/domains/python/__init__.py +402 -211
  56. sphinx/domains/python/_annotations.py +114 -57
  57. sphinx/domains/python/_object.py +151 -67
  58. sphinx/domains/rst.py +94 -49
  59. sphinx/domains/std/__init__.py +510 -249
  60. sphinx/environment/__init__.py +345 -61
  61. sphinx/environment/adapters/asset.py +7 -1
  62. sphinx/environment/adapters/indexentries.py +15 -20
  63. sphinx/environment/adapters/toctree.py +19 -9
  64. sphinx/environment/collectors/__init__.py +3 -1
  65. sphinx/environment/collectors/asset.py +18 -15
  66. sphinx/environment/collectors/dependencies.py +8 -10
  67. sphinx/environment/collectors/metadata.py +6 -4
  68. sphinx/environment/collectors/title.py +3 -1
  69. sphinx/environment/collectors/toctree.py +4 -4
  70. sphinx/errors.py +1 -3
  71. sphinx/events.py +4 -4
  72. sphinx/ext/apidoc/__init__.py +21 -0
  73. sphinx/ext/apidoc/__main__.py +9 -0
  74. sphinx/ext/apidoc/_cli.py +356 -0
  75. sphinx/ext/apidoc/_generate.py +356 -0
  76. sphinx/ext/apidoc/_shared.py +66 -0
  77. sphinx/ext/autodoc/__init__.py +837 -483
  78. sphinx/ext/autodoc/directive.py +57 -21
  79. sphinx/ext/autodoc/importer.py +184 -67
  80. sphinx/ext/autodoc/mock.py +25 -10
  81. sphinx/ext/autodoc/preserve_defaults.py +17 -9
  82. sphinx/ext/autodoc/type_comment.py +56 -29
  83. sphinx/ext/autodoc/typehints.py +49 -26
  84. sphinx/ext/autosectionlabel.py +28 -11
  85. sphinx/ext/autosummary/__init__.py +271 -143
  86. sphinx/ext/autosummary/generate.py +121 -51
  87. sphinx/ext/coverage.py +152 -91
  88. sphinx/ext/doctest.py +169 -101
  89. sphinx/ext/duration.py +12 -6
  90. sphinx/ext/extlinks.py +33 -21
  91. sphinx/ext/githubpages.py +8 -8
  92. sphinx/ext/graphviz.py +175 -109
  93. sphinx/ext/ifconfig.py +11 -6
  94. sphinx/ext/imgconverter.py +48 -25
  95. sphinx/ext/imgmath.py +127 -97
  96. sphinx/ext/inheritance_diagram.py +177 -103
  97. sphinx/ext/intersphinx/__init__.py +22 -13
  98. sphinx/ext/intersphinx/__main__.py +3 -1
  99. sphinx/ext/intersphinx/_cli.py +18 -14
  100. sphinx/ext/intersphinx/_load.py +91 -82
  101. sphinx/ext/intersphinx/_resolve.py +108 -74
  102. sphinx/ext/intersphinx/_shared.py +2 -2
  103. sphinx/ext/linkcode.py +28 -12
  104. sphinx/ext/mathjax.py +60 -29
  105. sphinx/ext/napoleon/__init__.py +19 -7
  106. sphinx/ext/napoleon/docstring.py +229 -231
  107. sphinx/ext/todo.py +44 -49
  108. sphinx/ext/viewcode.py +105 -57
  109. sphinx/extension.py +3 -1
  110. sphinx/highlighting.py +13 -7
  111. sphinx/io.py +9 -13
  112. sphinx/jinja2glue.py +29 -26
  113. sphinx/locale/__init__.py +8 -9
  114. sphinx/parsers.py +8 -7
  115. sphinx/project.py +2 -2
  116. sphinx/pycode/__init__.py +31 -21
  117. sphinx/pycode/ast.py +6 -3
  118. sphinx/pycode/parser.py +14 -8
  119. sphinx/pygments_styles.py +4 -5
  120. sphinx/registry.py +192 -92
  121. sphinx/roles.py +58 -7
  122. sphinx/search/__init__.py +75 -54
  123. sphinx/search/en.py +11 -13
  124. sphinx/search/fi.py +1 -1
  125. sphinx/search/ja.py +8 -6
  126. sphinx/search/nl.py +1 -1
  127. sphinx/search/zh.py +19 -21
  128. sphinx/testing/fixtures.py +26 -29
  129. sphinx/testing/path.py +26 -62
  130. sphinx/testing/restructuredtext.py +14 -8
  131. sphinx/testing/util.py +21 -19
  132. sphinx/texinputs/make.bat.jinja +50 -50
  133. sphinx/texinputs/sphinx.sty +4 -3
  134. sphinx/texinputs/sphinxlatexadmonitions.sty +1 -1
  135. sphinx/texinputs/sphinxlatexobjects.sty +29 -10
  136. sphinx/themes/basic/static/searchtools.js +8 -5
  137. sphinx/theming.py +49 -61
  138. sphinx/transforms/__init__.py +17 -38
  139. sphinx/transforms/compact_bullet_list.py +5 -3
  140. sphinx/transforms/i18n.py +8 -21
  141. sphinx/transforms/post_transforms/__init__.py +142 -93
  142. sphinx/transforms/post_transforms/code.py +5 -5
  143. sphinx/transforms/post_transforms/images.py +28 -24
  144. sphinx/transforms/references.py +3 -1
  145. sphinx/util/__init__.py +109 -60
  146. sphinx/util/_files.py +39 -23
  147. sphinx/util/_importer.py +4 -1
  148. sphinx/util/_inventory_file_reader.py +76 -0
  149. sphinx/util/_io.py +2 -2
  150. sphinx/util/_lines.py +6 -3
  151. sphinx/util/_pathlib.py +40 -2
  152. sphinx/util/build_phase.py +2 -0
  153. sphinx/util/cfamily.py +19 -14
  154. sphinx/util/console.py +44 -179
  155. sphinx/util/display.py +9 -10
  156. sphinx/util/docfields.py +140 -122
  157. sphinx/util/docstrings.py +1 -1
  158. sphinx/util/docutils.py +118 -77
  159. sphinx/util/fileutil.py +25 -26
  160. sphinx/util/http_date.py +2 -0
  161. sphinx/util/i18n.py +77 -64
  162. sphinx/util/images.py +8 -6
  163. sphinx/util/inspect.py +147 -38
  164. sphinx/util/inventory.py +215 -116
  165. sphinx/util/logging.py +33 -33
  166. sphinx/util/matching.py +12 -4
  167. sphinx/util/nodes.py +18 -13
  168. sphinx/util/osutil.py +38 -39
  169. sphinx/util/parallel.py +22 -13
  170. sphinx/util/parsing.py +2 -1
  171. sphinx/util/png.py +6 -2
  172. sphinx/util/requests.py +33 -2
  173. sphinx/util/rst.py +3 -2
  174. sphinx/util/tags.py +1 -1
  175. sphinx/util/template.py +18 -10
  176. sphinx/util/texescape.py +8 -6
  177. sphinx/util/typing.py +148 -122
  178. sphinx/versioning.py +3 -3
  179. sphinx/writers/html.py +3 -1
  180. sphinx/writers/html5.py +61 -50
  181. sphinx/writers/latex.py +80 -65
  182. sphinx/writers/manpage.py +19 -38
  183. sphinx/writers/texinfo.py +44 -45
  184. sphinx/writers/text.py +48 -30
  185. sphinx/writers/xml.py +11 -8
  186. {sphinx-8.1.2.dist-info → sphinx-8.2.0rc1.dist-info}/LICENSE.rst +1 -1
  187. {sphinx-8.1.2.dist-info → sphinx-8.2.0rc1.dist-info}/METADATA +23 -15
  188. {sphinx-8.1.2.dist-info → sphinx-8.2.0rc1.dist-info}/RECORD +190 -186
  189. {sphinx-8.1.2.dist-info → sphinx-8.2.0rc1.dist-info}/WHEEL +1 -1
  190. sphinx/builders/html/transforms.py +0 -90
  191. sphinx/ext/apidoc.py +0 -721
  192. sphinx/util/exceptions.py +0 -74
  193. {sphinx-8.1.2.dist-info → sphinx-8.2.0rc1.dist-info}/entry_points.txt +0 -0
sphinx/registry.py CHANGED
@@ -6,9 +6,9 @@ import traceback
6
6
  from importlib import import_module
7
7
  from importlib.metadata import entry_points
8
8
  from types import MethodType
9
- from typing import TYPE_CHECKING, Any
9
+ from typing import TYPE_CHECKING
10
10
 
11
- from sphinx.domains import Domain, Index, ObjType
11
+ from sphinx.domains import ObjType
12
12
  from sphinx.domains.std import GenericObject, Target
13
13
  from sphinx.errors import ExtensionError, SphinxError, VersionRequirementError
14
14
  from sphinx.extension import Extension
@@ -17,10 +17,13 @@ from sphinx.locale import __
17
17
  from sphinx.parsers import Parser as SphinxParser
18
18
  from sphinx.roles import XRefRole
19
19
  from sphinx.util import logging
20
+ from sphinx.util._pathlib import _StrPath
20
21
  from sphinx.util.logging import prefixed_warnings
21
22
 
22
23
  if TYPE_CHECKING:
23
- from collections.abc import Callable, Iterator, Sequence
24
+ import os
25
+ from collections.abc import Callable, Iterator, Mapping, Sequence
26
+ from typing import Any, TypeAlias
24
27
 
25
28
  from docutils import nodes
26
29
  from docutils.core import Publisher
@@ -29,26 +32,43 @@ if TYPE_CHECKING:
29
32
  from docutils.parsers.rst import Directive
30
33
  from docutils.transforms import Transform
31
34
 
35
+ from sphinx import addnodes
32
36
  from sphinx.application import Sphinx
33
37
  from sphinx.builders import Builder
34
38
  from sphinx.config import Config
39
+ from sphinx.domains import Domain, Index
35
40
  from sphinx.environment import BuildEnvironment
36
41
  from sphinx.ext.autodoc import Documenter
42
+ from sphinx.util.docfields import Field
37
43
  from sphinx.util.typing import (
38
44
  ExtensionMetadata,
39
45
  RoleFunction,
40
46
  TitleGetter,
41
47
  _ExtensionSetupFunc,
42
48
  )
49
+ from sphinx.writers.html5 import HTML5Translator
50
+
51
+ # visit/depart function
52
+ # the parameters should be (SphinxTranslator, Element)
53
+ # or any subtype of either, but mypy rejects this.
54
+ _NodeHandler: TypeAlias = Callable[[Any, Any], None]
55
+ _NodeHandlerPair: TypeAlias = tuple[_NodeHandler, _NodeHandler | None]
56
+
57
+ _MathsRenderer: TypeAlias = Callable[[HTML5Translator, nodes.math], None]
58
+ _MathsBlockRenderer: TypeAlias = Callable[[HTML5Translator, nodes.math_block], None]
59
+ _MathsInlineRenderers: TypeAlias = tuple[_MathsRenderer, _MathsRenderer | None]
60
+ _MathsBlockRenderers: TypeAlias = tuple[
61
+ _MathsBlockRenderer, _MathsBlockRenderer | None
62
+ ]
43
63
 
44
64
  logger = logging.getLogger(__name__)
45
65
 
46
66
  # list of deprecated extensions. Keys are extension name.
47
67
  # Values are Sphinx version that merge the extension.
48
68
  EXTENSION_BLACKLIST = {
49
- "sphinxjp.themecore": "1.2",
69
+ 'sphinxjp.themecore': '1.2',
50
70
  'sphinxcontrib-napoleon': '1.3',
51
- "sphinxprettysearchresults": "2.0.0",
71
+ 'sphinxprettysearchresults': '2.0.0',
52
72
  }
53
73
 
54
74
 
@@ -91,16 +111,20 @@ class SphinxComponentRegistry:
91
111
 
92
112
  #: HTML inline and block math renderers
93
113
  #: a dict of name -> tuple of visit function and depart function
94
- self.html_inline_math_renderers: dict[str,
95
- tuple[Callable, Callable | None]] = {}
96
- self.html_block_math_renderers: dict[str,
97
- tuple[Callable, Callable | None]] = {}
114
+ self.html_inline_math_renderers: dict[
115
+ str,
116
+ _MathsInlineRenderers,
117
+ ] = {}
118
+ self.html_block_math_renderers: dict[
119
+ str,
120
+ _MathsBlockRenderers,
121
+ ] = {}
98
122
 
99
123
  #: HTML assets
100
124
  self.html_assets_policy: str = 'per_page'
101
125
 
102
126
  #: HTML themes
103
- self.html_themes: dict[str, str] = {}
127
+ self.html_themes: dict[str, _StrPath] = {}
104
128
 
105
129
  #: js_files; list of JS paths or URLs
106
130
  self.js_files: list[tuple[str | None, dict[str, Any]]] = []
@@ -124,7 +148,7 @@ class SphinxComponentRegistry:
124
148
 
125
149
  #: custom handlers for translators
126
150
  #: a dict of builder name -> dict of node name -> visitor and departure functions
127
- self.translation_handlers: dict[str, dict[str, tuple[Callable, Callable | None]]] = {}
151
+ self.translation_handlers: dict[str, dict[str, _NodeHandlerPair]] = {}
128
152
 
129
153
  #: additional transforms; list of transforms
130
154
  self.transforms: list[type[Transform]] = []
@@ -139,10 +163,14 @@ class SphinxComponentRegistry:
139
163
  def add_builder(self, builder: type[Builder], override: bool = False) -> None:
140
164
  logger.debug('[app] adding builder: %r', builder)
141
165
  if not hasattr(builder, 'name'):
142
- raise ExtensionError(__('Builder class %s has no "name" attribute') % builder)
166
+ raise ExtensionError(
167
+ __('Builder class %s has no "name" attribute') % builder
168
+ )
143
169
  if builder.name in self.builders and not override:
144
- raise ExtensionError(__('Builder %r already exists (in module %s)') %
145
- (builder.name, self.builders[builder.name].__module__))
170
+ raise ExtensionError(
171
+ __('Builder %r already exists (in module %s)')
172
+ % (builder.name, self.builders[builder.name].__module__)
173
+ )
146
174
  self.builders[builder.name] = builder
147
175
 
148
176
  def preload_builder(self, app: Sphinx, name: str) -> None:
@@ -154,8 +182,13 @@ class SphinxComponentRegistry:
154
182
  try:
155
183
  entry_point = builder_entry_points[name]
156
184
  except KeyError as exc:
157
- raise SphinxError(__('Builder name %s not registered or available'
158
- ' through entry point') % name) from exc
185
+ raise SphinxError(
186
+ __(
187
+ 'Builder name %s not registered or available'
188
+ ' through entry point'
189
+ )
190
+ % name
191
+ ) from exc
159
192
 
160
193
  self.load_extension(app, entry_point.module)
161
194
 
@@ -187,39 +220,52 @@ class SphinxComponentRegistry:
187
220
 
188
221
  yield domain
189
222
 
190
- def add_directive_to_domain(self, domain: str, name: str,
191
- cls: type[Directive], override: bool = False) -> None:
223
+ def add_directive_to_domain(
224
+ self, domain: str, name: str, cls: type[Directive], override: bool = False
225
+ ) -> None:
192
226
  logger.debug('[app] adding directive to domain: %r', (domain, name, cls))
193
227
  if domain not in self.domains:
194
228
  raise ExtensionError(__('domain %s not yet registered') % domain)
195
229
 
196
- directives: dict[str, type[Directive]] = self.domain_directives.setdefault(domain, {})
230
+ directives: dict[str, type[Directive]] = self.domain_directives.setdefault(
231
+ domain, {}
232
+ )
197
233
  if name in directives and not override:
198
- raise ExtensionError(__('The %r directive is already registered to domain %s') %
199
- (name, domain))
234
+ raise ExtensionError(
235
+ __('The %r directive is already registered to domain %s')
236
+ % (name, domain)
237
+ )
200
238
  directives[name] = cls
201
239
 
202
- def add_role_to_domain(self, domain: str, name: str,
203
- role: RoleFunction | XRefRole, override: bool = False,
204
- ) -> None:
240
+ def add_role_to_domain(
241
+ self,
242
+ domain: str,
243
+ name: str,
244
+ role: RoleFunction | XRefRole,
245
+ override: bool = False,
246
+ ) -> None:
205
247
  logger.debug('[app] adding role to domain: %r', (domain, name, role))
206
248
  if domain not in self.domains:
207
249
  raise ExtensionError(__('domain %s not yet registered') % domain)
208
250
  roles = self.domain_roles.setdefault(domain, {})
209
251
  if name in roles and not override:
210
- raise ExtensionError(__('The %r role is already registered to domain %s') %
211
- (name, domain))
252
+ raise ExtensionError(
253
+ __('The %r role is already registered to domain %s') % (name, domain)
254
+ )
212
255
  roles[name] = role
213
256
 
214
- def add_index_to_domain(self, domain: str, index: type[Index],
215
- override: bool = False) -> None:
257
+ def add_index_to_domain(
258
+ self, domain: str, index: type[Index], override: bool = False
259
+ ) -> None:
216
260
  logger.debug('[app] adding index to domain: %r', (domain, index))
217
261
  if domain not in self.domains:
218
262
  raise ExtensionError(__('domain %s not yet registered') % domain)
219
263
  indices = self.domain_indices.setdefault(domain, [])
220
264
  if index in indices and not override:
221
- raise ExtensionError(__('The %r index is already registered to domain %s') %
222
- (index.name, domain))
265
+ raise ExtensionError(
266
+ __('The %r index is already registered to domain %s')
267
+ % (index.name, domain)
268
+ )
223
269
  indices.append(index)
224
270
 
225
271
  def add_object_type(
@@ -227,30 +273,45 @@ class SphinxComponentRegistry:
227
273
  directivename: str,
228
274
  rolename: str,
229
275
  indextemplate: str = '',
230
- parse_node: Callable | None = None,
276
+ parse_node: Callable[[BuildEnvironment, str, addnodes.desc_signature], str]
277
+ | None = None,
231
278
  ref_nodeclass: type[TextElement] | None = None,
232
279
  objname: str = '',
233
- doc_field_types: Sequence = (),
280
+ doc_field_types: Sequence[Field] = (),
234
281
  override: bool = False,
235
282
  ) -> None:
236
- logger.debug('[app] adding object type: %r',
237
- (directivename, rolename, indextemplate, parse_node,
238
- ref_nodeclass, objname, doc_field_types))
283
+ logger.debug(
284
+ '[app] adding object type: %r',
285
+ (
286
+ directivename,
287
+ rolename,
288
+ indextemplate,
289
+ parse_node,
290
+ ref_nodeclass,
291
+ objname,
292
+ doc_field_types,
293
+ ),
294
+ )
239
295
 
240
296
  # create a subclass of GenericObject as the new directive
241
- directive = type(directivename,
242
- (GenericObject, object),
243
- {'indextemplate': indextemplate,
244
- 'parse_node': parse_node and staticmethod(parse_node),
245
- 'doc_field_types': doc_field_types})
297
+ directive = type(
298
+ directivename,
299
+ (GenericObject, object),
300
+ {
301
+ 'indextemplate': indextemplate,
302
+ 'parse_node': parse_node and staticmethod(parse_node),
303
+ 'doc_field_types': doc_field_types,
304
+ },
305
+ )
246
306
 
247
307
  self.add_directive_to_domain('std', directivename, directive)
248
308
  self.add_role_to_domain('std', rolename, XRefRole(innernodeclass=ref_nodeclass))
249
309
 
250
310
  object_types = self.domain_object_types.setdefault('std', {})
251
311
  if directivename in object_types and not override:
252
- raise ExtensionError(__('The %r object_type is already registered') %
253
- directivename)
312
+ raise ExtensionError(
313
+ __('The %r object_type is already registered') % directivename
314
+ )
254
315
  object_types[directivename] = ObjType(objname or directivename, rolename)
255
316
 
256
317
  def add_crossref_type(
@@ -262,24 +323,31 @@ class SphinxComponentRegistry:
262
323
  objname: str = '',
263
324
  override: bool = False,
264
325
  ) -> None:
265
- logger.debug('[app] adding crossref type: %r',
266
- (directivename, rolename, indextemplate, ref_nodeclass, objname))
326
+ logger.debug(
327
+ '[app] adding crossref type: %r',
328
+ (directivename, rolename, indextemplate, ref_nodeclass, objname),
329
+ )
267
330
 
268
331
  # create a subclass of Target as the new directive
269
- directive = type(directivename,
270
- (Target, object),
271
- {'indextemplate': indextemplate})
332
+ directive = type(
333
+ directivename,
334
+ (Target, object),
335
+ {'indextemplate': indextemplate},
336
+ )
272
337
 
273
338
  self.add_directive_to_domain('std', directivename, directive)
274
339
  self.add_role_to_domain('std', rolename, XRefRole(innernodeclass=ref_nodeclass))
275
340
 
276
341
  object_types = self.domain_object_types.setdefault('std', {})
277
342
  if directivename in object_types and not override:
278
- raise ExtensionError(__('The %r crossref_type is already registered') %
279
- directivename)
343
+ raise ExtensionError(
344
+ __('The %r crossref_type is already registered') % directivename
345
+ )
280
346
  object_types[directivename] = ObjType(objname or directivename, rolename)
281
347
 
282
- def add_source_suffix(self, suffix: str, filetype: str, override: bool = False) -> None:
348
+ def add_source_suffix(
349
+ self, suffix: str, filetype: str, override: bool = False
350
+ ) -> None:
283
351
  logger.debug('[app] adding source_suffix: %r, %r', suffix, filetype)
284
352
  if suffix in self.source_suffix and not override:
285
353
  raise ExtensionError(__('source_suffix %r is already registered') % suffix)
@@ -291,15 +359,18 @@ class SphinxComponentRegistry:
291
359
  # create a map from filetype to parser
292
360
  for filetype in parser.supported:
293
361
  if filetype in self.source_parsers and not override:
294
- raise ExtensionError(__('source_parser for %r is already registered') %
295
- filetype)
362
+ raise ExtensionError(
363
+ __('source_parser for %r is already registered') % filetype
364
+ )
296
365
  self.source_parsers[filetype] = parser
297
366
 
298
367
  def get_source_parser(self, filetype: str) -> type[Parser]:
299
368
  try:
300
369
  return self.source_parsers[filetype]
301
370
  except KeyError as exc:
302
- raise SphinxError(__('Source parser for %s not registered') % filetype) from exc
371
+ raise SphinxError(
372
+ __('Source parser for %s not registered') % filetype
373
+ ) from exc
303
374
 
304
375
  def get_source_parsers(self) -> dict[str, type[Parser]]:
305
376
  return self.source_parsers
@@ -311,28 +382,32 @@ class SphinxComponentRegistry:
311
382
  parser.set_application(app)
312
383
  return parser
313
384
 
314
- def add_translator(self, name: str, translator: type[nodes.NodeVisitor],
315
- override: bool = False) -> None:
385
+ def add_translator(
386
+ self, name: str, translator: type[nodes.NodeVisitor], override: bool = False
387
+ ) -> None:
316
388
  logger.debug('[app] Change of translator for the %s builder.', name)
317
389
  if name in self.translators and not override:
318
390
  raise ExtensionError(__('Translator for %r already exists') % name)
319
391
  self.translators[name] = translator
320
392
 
321
393
  def add_translation_handlers(
322
- self,
323
- node: type[Element],
324
- **kwargs: tuple[Callable, Callable | None],
394
+ self, node: type[Element], **kwargs: _NodeHandlerPair
325
395
  ) -> None:
326
396
  logger.debug('[app] adding translation_handlers: %r, %r', node, kwargs)
327
397
  for builder_name, handlers in kwargs.items():
328
- translation_handlers = self.translation_handlers.setdefault(builder_name, {})
398
+ translation_handlers = self.translation_handlers.setdefault(
399
+ builder_name, {}
400
+ )
329
401
  try:
330
402
  visit, depart = handlers # unpack once for assertion
331
403
  translation_handlers[node.__name__] = (visit, depart)
332
404
  except ValueError as exc:
333
405
  raise ExtensionError(
334
- __('kwargs for add_node() must be a (visit, depart) '
335
- 'function tuple: %r=%r') % (builder_name, handlers),
406
+ __(
407
+ 'kwargs for add_node() must be a (visit, depart) '
408
+ 'function tuple: %r=%r'
409
+ )
410
+ % (builder_name, handlers),
336
411
  ) from exc
337
412
 
338
413
  def get_translator_class(self, builder: Builder) -> type[nodes.NodeVisitor]:
@@ -379,8 +454,9 @@ class SphinxComponentRegistry:
379
454
  def add_documenter(self, objtype: str, documenter: type[Documenter]) -> None:
380
455
  self.documenters[objtype] = documenter
381
456
 
382
- def add_autodoc_attrgetter(self, typ: type,
383
- attrgetter: Callable[[Any, str, Any], Any]) -> None:
457
+ def add_autodoc_attrgetter(
458
+ self, typ: type, attrgetter: Callable[[Any, str, Any], Any]
459
+ ) -> None:
384
460
  self.autodoc_attrgetters[typ] = attrgetter
385
461
 
386
462
  def add_css_files(self, filename: str, **attributes: Any) -> None:
@@ -395,7 +471,7 @@ class SphinxComponentRegistry:
395
471
  return bool([x for x in packages if x[0] == name])
396
472
 
397
473
  def add_latex_package(
398
- self, name: str, options: str | None, after_hyperref: bool = False,
474
+ self, name: str, options: str | None, after_hyperref: bool = False
399
475
  ) -> None:
400
476
  if self.has_latex_package(name):
401
477
  logger.warning("latex package '%s' already included", name)
@@ -410,9 +486,12 @@ class SphinxComponentRegistry:
410
486
  self,
411
487
  node: type[Node],
412
488
  figtype: str,
413
- title_getter: TitleGetter | None = None, override: bool = False,
489
+ title_getter: TitleGetter | None = None,
490
+ override: bool = False,
414
491
  ) -> None:
415
- logger.debug('[app] adding enumerable node: (%r, %r, %r)', node, figtype, title_getter)
492
+ logger.debug(
493
+ '[app] adding enumerable node: (%r, %r, %r)', node, figtype, title_getter
494
+ )
416
495
  if node in self.enumerable_nodes and not override:
417
496
  raise ExtensionError(__('enumerable_node %r already registered') % node)
418
497
  self.enumerable_nodes[node] = (figtype, title_getter)
@@ -420,11 +499,15 @@ class SphinxComponentRegistry:
420
499
  def add_html_math_renderer(
421
500
  self,
422
501
  name: str,
423
- inline_renderers: tuple[Callable, Callable | None] | None,
424
- block_renderers: tuple[Callable, Callable | None] | None,
502
+ inline_renderers: _MathsInlineRenderers | None,
503
+ block_renderers: _MathsBlockRenderers | None,
425
504
  ) -> None:
426
- logger.debug('[app] adding html_math_renderer: %s, %r, %r',
427
- name, inline_renderers, block_renderers)
505
+ logger.debug(
506
+ '[app] adding html_math_renderer: %s, %r, %r',
507
+ name,
508
+ inline_renderers,
509
+ block_renderers,
510
+ )
428
511
  if name in self.html_inline_math_renderers:
429
512
  raise ExtensionError(__('math renderer %s is already registered') % name)
430
513
 
@@ -433,17 +516,22 @@ class SphinxComponentRegistry:
433
516
  if block_renderers is not None:
434
517
  self.html_block_math_renderers[name] = block_renderers
435
518
 
436
- def add_html_theme(self, name: str, theme_path: str) -> None:
437
- self.html_themes[name] = theme_path
519
+ def add_html_theme(self, name: str, theme_path: str | os.PathLike[str]) -> None:
520
+ self.html_themes[name] = _StrPath(theme_path)
438
521
 
439
522
  def load_extension(self, app: Sphinx, extname: str) -> None:
440
523
  """Load a Sphinx extension."""
441
524
  if extname in app.extensions: # already loaded
442
525
  return
443
526
  if extname in EXTENSION_BLACKLIST:
444
- logger.warning(__('the extension %r was already merged with Sphinx since '
445
- 'version %s; this extension is ignored.'),
446
- extname, EXTENSION_BLACKLIST[extname])
527
+ logger.warning(
528
+ __(
529
+ 'the extension %r was already merged with Sphinx since '
530
+ 'version %s; this extension is ignored.'
531
+ ),
532
+ extname,
533
+ EXTENSION_BLACKLIST[extname],
534
+ )
447
535
  return
448
536
 
449
537
  # update loading context
@@ -453,13 +541,19 @@ class SphinxComponentRegistry:
453
541
  mod = import_module(extname)
454
542
  except ImportError as err:
455
543
  logger.verbose(__('Original exception:\n') + traceback.format_exc())
456
- raise ExtensionError(__('Could not import extension %s') % extname,
457
- err) from err
544
+ raise ExtensionError(
545
+ __('Could not import extension %s') % extname, err
546
+ ) from err
458
547
 
459
548
  setup: _ExtensionSetupFunc | None = getattr(mod, 'setup', None)
460
549
  if setup is None:
461
- logger.warning(__('extension %r has no setup() function; is it really '
462
- 'a Sphinx extension module?'), extname)
550
+ logger.warning(
551
+ __(
552
+ 'extension %r has no setup() function; is it really '
553
+ 'a Sphinx extension module?'
554
+ ),
555
+ extname,
556
+ )
463
557
  metadata: ExtensionMetadata = {}
464
558
  else:
465
559
  try:
@@ -467,27 +561,33 @@ class SphinxComponentRegistry:
467
561
  except VersionRequirementError as err:
468
562
  # add the extension name to the version required
469
563
  raise VersionRequirementError(
470
- __('The %s extension used by this project needs at least '
471
- 'Sphinx v%s; it therefore cannot be built with this '
472
- 'version.') % (extname, err),
564
+ __(
565
+ 'The %s extension used by this project needs at least '
566
+ 'Sphinx v%s; it therefore cannot be built with this '
567
+ 'version.'
568
+ )
569
+ % (extname, err),
473
570
  ) from err
474
571
 
475
572
  if metadata is None:
476
573
  metadata = {}
477
574
  elif not isinstance(metadata, dict):
478
- logger.warning(__('extension %r returned an unsupported object from '
479
- 'its setup() function; it should return None or a '
480
- 'metadata dictionary'), extname)
575
+ logger.warning(
576
+ __(
577
+ 'extension %r returned an unsupported object from '
578
+ 'its setup() function; it should return None or a '
579
+ 'metadata dictionary'
580
+ ),
581
+ extname,
582
+ )
481
583
  metadata = {}
482
584
 
483
585
  app.extensions[extname] = Extension(extname, mod, **metadata)
484
586
 
485
- def get_envversion(self, app: Sphinx) -> dict[str, int]:
486
- from sphinx.environment import ENV_VERSION
487
- envversion = {ext.name: ext.metadata['env_version'] for ext in app.extensions.values()
488
- if ext.metadata.get('env_version')}
489
- envversion['sphinx'] = ENV_VERSION
490
- return envversion
587
+ def get_envversion(self, app: Sphinx) -> Mapping[str, int]:
588
+ from sphinx.environment import _get_env_version
589
+
590
+ return _get_env_version(app.extensions)
491
591
 
492
592
  def get_publisher(self, app: Sphinx, filetype: str) -> Publisher:
493
593
  try:
@@ -502,7 +602,7 @@ class SphinxComponentRegistry:
502
602
  def merge_source_suffix(app: Sphinx, config: Config) -> None:
503
603
  """Merge any user-specified source_suffix with any added by extensions."""
504
604
  for suffix, filetype in app.registry.source_suffix.items():
505
- if suffix not in app.config.source_suffix: # NoQA: SIM114
605
+ if suffix not in app.config.source_suffix:
506
606
  app.config.source_suffix[suffix] = filetype
507
607
  elif app.config.source_suffix[suffix] == 'restructuredtext':
508
608
  # The filetype is not specified (default filetype).
sphinx/roles.py CHANGED
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import re
6
- from typing import TYPE_CHECKING, Any
6
+ from typing import TYPE_CHECKING
7
7
 
8
8
  import docutils.parsers.rst.directives
9
9
  import docutils.parsers.rst.roles
@@ -17,7 +17,7 @@ from sphinx.util.docutils import ReferenceRole, SphinxRole
17
17
 
18
18
  if TYPE_CHECKING:
19
19
  from collections.abc import Sequence
20
- from typing import Final
20
+ from typing import Any, Final
21
21
 
22
22
  from docutils.nodes import Element, Node, TextElement, system_message
23
23
 
@@ -29,7 +29,6 @@ if TYPE_CHECKING:
29
29
  generic_docroles = {
30
30
  'command': addnodes.literal_strong,
31
31
  'dfn': nodes.emphasis,
32
- 'kbd': nodes.literal,
33
32
  'mailheader': addnodes.literal_emphasis,
34
33
  'makevar': addnodes.literal_strong,
35
34
  'mimetype': addnodes.literal_emphasis,
@@ -43,8 +42,7 @@ generic_docroles = {
43
42
 
44
43
 
45
44
  class XRefRole(ReferenceRole):
46
- """
47
- A generic cross-referencing role. To create a callable that can be used as
45
+ """A generic cross-referencing role. To create a callable that can be used as
48
46
  a role function, create an instance of this class.
49
47
 
50
48
  The general features of this role are:
@@ -371,8 +369,7 @@ class RFC(ReferenceRole):
371
369
 
372
370
 
373
371
  def _format_rfc_target(target: str, /) -> str:
374
- """
375
- Takes an RFC number with an optional anchor (like ``123#section-2.5.3``)
372
+ """Takes an RFC number with an optional anchor (like ``123#section-2.5.3``)
376
373
  and attempts to produce a human-friendly title for it.
377
374
 
378
375
  We have a set of known anchors that we format nicely,
@@ -479,6 +476,59 @@ class Abbreviation(SphinxRole):
479
476
  return [nodes.abbreviation(self.rawtext, text, **options)], []
480
477
 
481
478
 
479
+ class Keyboard(SphinxRole):
480
+ """Implement the :kbd: role.
481
+
482
+ Split words in the text by separator or whitespace,
483
+ but keep multi-word keys together.
484
+ """
485
+
486
+ # capture ('-', '+', '^', or whitespace) in between any two characters
487
+ _pattern: Final = re.compile(r'(?<=.)([\-+^]| +)(?=.)')
488
+
489
+ def run(self) -> tuple[list[Node], list[system_message]]:
490
+ classes = ['kbd']
491
+ if 'classes' in self.options:
492
+ classes.extend(self.options['classes'])
493
+
494
+ parts = self._pattern.split(self.text)
495
+ if len(parts) == 1 or self._is_multi_word_key(parts):
496
+ return [nodes.literal(self.rawtext, self.text, classes=classes)], []
497
+
498
+ compound: list[Node] = []
499
+ while parts:
500
+ if self._is_multi_word_key(parts):
501
+ key = ''.join(parts[:3])
502
+ parts[:3] = []
503
+ else:
504
+ key = parts.pop(0)
505
+ compound.append(nodes.literal(key, key, classes=classes))
506
+
507
+ try:
508
+ sep = parts.pop(0) # key separator ('-', '+', '^', etc)
509
+ except IndexError:
510
+ break
511
+ else:
512
+ compound.append(nodes.Text(sep))
513
+
514
+ return compound, []
515
+
516
+ @staticmethod
517
+ def _is_multi_word_key(parts: list[str]) -> bool:
518
+ if len(parts) <= 2 or not parts[1].isspace():
519
+ return False
520
+ name = parts[0].lower(), parts[2].lower()
521
+ return name in frozenset({
522
+ ('back', 'space'),
523
+ ('caps', 'lock'),
524
+ ('num', 'lock'),
525
+ ('page', 'down'),
526
+ ('page', 'up'),
527
+ ('scroll', 'lock'),
528
+ ('sys', 'rq'),
529
+ })
530
+
531
+
482
532
  class Manpage(ReferenceRole):
483
533
  _manpage_re = re.compile(r'^(?P<path>(?P<page>.+)[(.](?P<section>[1-9]\w*)?\)?)$')
484
534
 
@@ -576,6 +626,7 @@ specific_docroles: dict[str, RoleFunction] = {
576
626
  'samp': EmphasizedLiteral(),
577
627
  # other
578
628
  'abbr': Abbreviation(),
629
+ 'kbd': Keyboard(),
579
630
  'manpage': Manpage(),
580
631
  }
581
632