Sphinx 8.1.3__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 +829 -480
  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.3.dist-info → sphinx-8.2.0rc1.dist-info}/LICENSE.rst +1 -1
  187. {sphinx-8.1.3.dist-info → sphinx-8.2.0rc1.dist-info}/METADATA +23 -15
  188. {sphinx-8.1.3.dist-info → sphinx-8.2.0rc1.dist-info}/RECORD +190 -186
  189. {sphinx-8.1.3.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.3.dist-info → sphinx-8.2.0rc1.dist-info}/entry_points.txt +0 -0
sphinx/writers/manpage.py CHANGED
@@ -2,8 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections.abc import Iterable
6
- from typing import TYPE_CHECKING, Any, cast
5
+ from typing import TYPE_CHECKING, cast
7
6
 
8
7
  from docutils import nodes
9
8
  from docutils.writers.manpage import Translator as BaseTranslator
@@ -17,6 +16,9 @@ from sphinx.util.i18n import format_date
17
16
  from sphinx.util.nodes import NodeMatcher
18
17
 
19
18
  if TYPE_CHECKING:
19
+ from collections.abc import Iterable
20
+ from typing import Any
21
+
20
22
  from docutils.nodes import Element
21
23
 
22
24
  from sphinx.builders import Builder
@@ -33,14 +35,13 @@ class ManualPageWriter(Writer): # type: ignore[misc]
33
35
  transform = NestedInlineTransform(self.document)
34
36
  transform.apply()
35
37
  visitor = self.builder.create_translator(self.document, self.builder)
36
- self.visitor = cast(ManualPageTranslator, visitor)
38
+ self.visitor = cast('ManualPageTranslator', visitor)
37
39
  self.document.walkabout(visitor)
38
40
  self.output = self.visitor.astext()
39
41
 
40
42
 
41
43
  class NestedInlineTransform:
42
- """
43
- Flatten nested inline nodes:
44
+ """Flatten nested inline nodes:
44
45
 
45
46
  Before:
46
47
  <strong>foo=<emphasis>1</emphasis>
@@ -71,17 +72,13 @@ class NestedInlineTransform:
71
72
 
72
73
 
73
74
  class ManualPageTranslator(SphinxTranslator, BaseTranslator): # type: ignore[misc]
74
- """
75
- Custom man page translator.
76
- """
75
+ """Custom man page translator."""
77
76
 
78
77
  _docinfo: dict[str, Any] = {}
79
78
 
80
79
  def __init__(self, document: nodes.document, builder: Builder) -> None:
81
80
  super().__init__(document, builder)
82
81
 
83
- self.in_productionlist = 0
84
-
85
82
  # first title is the manpage title
86
83
  self.section_level = -1
87
84
 
@@ -117,7 +114,7 @@ class ManualPageTranslator(SphinxTranslator, BaseTranslator): # type: ignore[mi
117
114
  ' "%(date)s" "%(version)s" "%(manual_group)s"\n'
118
115
  )
119
116
  if self._docinfo['subtitle']:
120
- tmpl += '.SH NAME\n' '%(title)s \\- %(subtitle)s\n'
117
+ tmpl += '.SH NAME\n%(title)s \\- %(subtitle)s\n'
121
118
  return tmpl % self._docinfo
122
119
 
123
120
  def visit_start_of_file(self, node: Element) -> None:
@@ -259,7 +256,7 @@ class ManualPageTranslator(SphinxTranslator, BaseTranslator): # type: ignore[mi
259
256
  # overwritten -- handle footnotes rubric
260
257
  def visit_rubric(self, node: Element) -> None:
261
258
  self.ensure_eol()
262
- if len(node) == 1 and node.astext() in ('Footnotes', _('Footnotes')):
259
+ if len(node) == 1 and node.astext() in {'Footnotes', _('Footnotes')}:
263
260
  self.body.append('.SH ' + self.deunicode(node.astext()).upper() + '\n')
264
261
  raise nodes.SkipNode
265
262
  self.body.append('.sp\n')
@@ -275,26 +272,10 @@ class ManualPageTranslator(SphinxTranslator, BaseTranslator): # type: ignore[mi
275
272
 
276
273
  def visit_productionlist(self, node: Element) -> None:
277
274
  self.ensure_eol()
278
- self.in_productionlist += 1
279
275
  self.body.append('.sp\n.nf\n')
280
- productionlist = cast(Iterable[addnodes.production], node)
281
- names = (production['tokenname'] for production in productionlist)
282
- maxlen = max(len(name) for name in names)
283
- lastname = None
284
- for production in productionlist:
285
- if production['tokenname']:
286
- lastname = production['tokenname'].ljust(maxlen)
287
- self.body.append(self.defs['strong'][0])
288
- self.body.append(self.deunicode(lastname))
289
- self.body.append(self.defs['strong'][1])
290
- self.body.append(' ::= ')
291
- elif lastname is not None:
292
- self.body.append('%s ' % (' ' * len(lastname)))
293
- production.walkabout(self)
294
- self.body.append('\n')
276
+
277
+ def depart_productionlist(self, node: Element) -> None:
295
278
  self.body.append('\n.fi\n')
296
- self.in_productionlist -= 1
297
- raise nodes.SkipNode
298
279
 
299
280
  def visit_production(self, node: Element) -> None:
300
281
  pass
@@ -379,11 +360,11 @@ class ManualPageTranslator(SphinxTranslator, BaseTranslator): # type: ignore[mi
379
360
  pass
380
361
 
381
362
  def visit_acks(self, node: Element) -> None:
382
- bullet_list = cast(nodes.bullet_list, node[0])
383
- list_items = cast(Iterable[nodes.list_item], bullet_list)
363
+ bullet_list = cast('nodes.bullet_list', node[0])
364
+ list_items = cast('Iterable[nodes.list_item]', bullet_list)
384
365
  self.ensure_eol()
385
- bullet_list = cast(nodes.bullet_list, node[0])
386
- list_items = cast(Iterable[nodes.list_item], bullet_list)
366
+ bullet_list = cast('nodes.bullet_list', node[0])
367
+ list_items = cast('Iterable[nodes.list_item]', bullet_list)
387
368
  self.body.append(', '.join(n.astext() for n in list_items) + '.')
388
369
  self.body.append('\n')
389
370
  raise nodes.SkipNode
@@ -477,14 +458,14 @@ class ManualPageTranslator(SphinxTranslator, BaseTranslator): # type: ignore[mi
477
458
  def depart_inline(self, node: Element) -> None:
478
459
  pass
479
460
 
480
- def visit_math(self, node: Element) -> None:
461
+ def visit_math(self, node: nodes.math) -> None:
481
462
  pass
482
463
 
483
- def depart_math(self, node: Element) -> None:
464
+ def depart_math(self, node: nodes.math) -> None:
484
465
  pass
485
466
 
486
- def visit_math_block(self, node: Element) -> None:
467
+ def visit_math_block(self, node: nodes.math_block) -> None:
487
468
  self.visit_centered(node)
488
469
 
489
- def depart_math_block(self, node: Element) -> None:
470
+ def depart_math_block(self, node: nodes.math_block) -> None:
490
471
  self.depart_centered(node)
sphinx/writers/texinfo.py CHANGED
@@ -2,16 +2,14 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os.path
5
6
  import re
6
7
  import textwrap
7
- from collections.abc import Iterable, Iterator
8
- from os import path
9
- from typing import TYPE_CHECKING, Any, ClassVar, cast
8
+ from typing import TYPE_CHECKING, cast
10
9
 
11
10
  from docutils import nodes, writers
12
11
 
13
12
  from sphinx import __display_version__, addnodes
14
- from sphinx.errors import ExtensionError
15
13
  from sphinx.locale import _, __, admonitionlabels
16
14
  from sphinx.util import logging
17
15
  from sphinx.util.docutils import SphinxTranslator
@@ -19,6 +17,9 @@ from sphinx.util.i18n import format_date
19
17
  from sphinx.writers.latex import collected_footnote
20
18
 
21
19
  if TYPE_CHECKING:
20
+ from collections.abc import Iterable, Iterator
21
+ from typing import Any, ClassVar
22
+
22
23
  from docutils.nodes import Element, Node, Text
23
24
 
24
25
  from sphinx.builders.texinfo import TexinfoBuilder
@@ -133,7 +134,7 @@ class TexinfoWriter(writers.Writer): # type: ignore[type-arg]
133
134
  def translate(self) -> None:
134
135
  assert isinstance(self.document, nodes.document)
135
136
  visitor = self.builder.create_translator(self.document, self.builder)
136
- self.visitor = cast(TexinfoTranslator, visitor)
137
+ self.visitor = cast('TexinfoTranslator', visitor)
137
138
  self.document.walkabout(visitor)
138
139
  self.visitor.finish()
139
140
  for attr in self.visitor_attributes:
@@ -188,6 +189,7 @@ class TexinfoTranslator(SphinxTranslator):
188
189
  self.escape_hyphens = 0
189
190
  self.curfilestack: list[str] = []
190
191
  self.footnotestack: list[dict[str, list[collected_footnote | bool]]] = []
192
+ self.in_production_list = False
191
193
  self.in_footnote = 0
192
194
  self.in_samp = 0
193
195
  self.handled_abbrs: set[str] = set()
@@ -240,7 +242,7 @@ class TexinfoTranslator(SphinxTranslator):
240
242
  # filename
241
243
  if not elements['filename']:
242
244
  elements['filename'] = self.document.get('source') or 'untitled'
243
- if elements['filename'][-4:] in ('.txt', '.rst'): # type: ignore[index]
245
+ if elements['filename'][-4:] in {'.txt', '.rst'}: # type: ignore[index]
244
246
  elements['filename'] = elements['filename'][:-4] # type: ignore[index]
245
247
  elements['filename'] += '.info' # type: ignore[operator]
246
248
  # direntry
@@ -250,9 +252,10 @@ class TexinfoTranslator(SphinxTranslator):
250
252
  '(%s)' % elements['filename'],
251
253
  self.escape_arg(self.settings.texinfo_dir_description),
252
254
  )
253
- elements['direntry'] = (
254
- '@dircategory %s\n' '@direntry\n' '%s' '@end direntry\n'
255
- ) % (self.escape_id(self.settings.texinfo_dir_category), entry)
255
+ elements['direntry'] = '@dircategory %s\n@direntry\n%s@end direntry\n' % (
256
+ self.escape_id(self.settings.texinfo_dir_category),
257
+ entry,
258
+ )
256
259
  elements['copying'] = COPYING % elements
257
260
  # allow the user to override them all
258
261
  elements.update(self.settings.texinfo_elements)
@@ -287,7 +290,7 @@ class TexinfoTranslator(SphinxTranslator):
287
290
  ]
288
291
  # each section is also a node
289
292
  for section in self.document.findall(nodes.section):
290
- title = cast(nodes.TextElement, section.next_node(nodes.Titular)) # type: ignore[type-var]
293
+ title = cast('nodes.TextElement', section.next_node(nodes.Titular)) # type: ignore[type-var]
291
294
  name = title.astext() if title else '<untitled>'
292
295
  section['node_name'] = add_node_name(name)
293
296
 
@@ -448,10 +451,10 @@ class TexinfoTranslator(SphinxTranslator):
448
451
  for subentry in entries:
449
452
  _add_detailed_menu(subentry)
450
453
 
451
- self.body.append('\n@detailmenu\n' ' --- The Detailed Node Listing ---\n')
454
+ self.body.append('\n@detailmenu\n --- The Detailed Node Listing ---\n')
452
455
  for entry in entries:
453
456
  _add_detailed_menu(entry)
454
- self.body.append('\n@end detailmenu\n' '@end menu\n')
457
+ self.body.append('\n@end detailmenu\n@end menu\n')
455
458
 
456
459
  def tex_image_length(self, width_str: str) -> str:
457
460
  match = re.match(r'(\d*\.?\d*)\s*(\S*)', width_str)
@@ -491,7 +494,7 @@ class TexinfoTranslator(SphinxTranslator):
491
494
  indices_config = frozenset(indices_config)
492
495
  else:
493
496
  check_names = False
494
- for domain in self.builder.env.domains.sorted():
497
+ for domain in self._domains.sorted():
495
498
  for index_cls in domain.indices:
496
499
  index_name = f'{domain.name}-{index_cls.name}'
497
500
  if check_names and index_name not in indices_config:
@@ -505,7 +508,7 @@ class TexinfoTranslator(SphinxTranslator):
505
508
  generate(content, collapsed),
506
509
  ))
507
510
  # only add the main Index if it's not empty
508
- domain = self.builder.env.domains.index_domain
511
+ domain = self._domains.index_domain
509
512
  for docname in self.builder.docnames:
510
513
  if domain.entries[docname]:
511
514
  self.indices.append((_('Index'), '\n@printindex ge\n'))
@@ -529,7 +532,7 @@ class TexinfoTranslator(SphinxTranslator):
529
532
 
530
533
  fnotes: dict[str, list[collected_footnote | bool]] = {}
531
534
  for fn in footnotes_under(node):
532
- label = cast(nodes.label, fn[0])
535
+ label = cast('nodes.label', fn[0])
533
536
  num = label.astext().strip()
534
537
  fnotes[num] = [collected_footnote('', *fn.children), False]
535
538
  return fnotes
@@ -608,7 +611,7 @@ class TexinfoTranslator(SphinxTranslator):
608
611
  self.add_anchor(id, node)
609
612
 
610
613
  self.next_section_ids.clear()
611
- self.previous_section = cast(nodes.section, node)
614
+ self.previous_section = cast('nodes.section', node)
612
615
  self.section_level += 1
613
616
 
614
617
  def depart_section(self, node: Element) -> None:
@@ -657,7 +660,7 @@ class TexinfoTranslator(SphinxTranslator):
657
660
  self.body.append('\n\n')
658
661
 
659
662
  def visit_rubric(self, node: Element) -> None:
660
- if len(node) == 1 and node.astext() in ('Footnotes', _('Footnotes')):
663
+ if len(node) == 1 and node.astext() in {'Footnotes', _('Footnotes')}:
661
664
  raise nodes.SkipNode
662
665
  try:
663
666
  rubric = self.rubrics[self.section_level]
@@ -1109,7 +1112,7 @@ class TexinfoTranslator(SphinxTranslator):
1109
1112
 
1110
1113
  def visit_admonition(self, node: Element, name: str = '') -> None:
1111
1114
  if not name:
1112
- title = cast(nodes.title, node[0])
1115
+ title = cast('nodes.title', node[0])
1113
1116
  name = self.escape(title.astext())
1114
1117
  self.body.append('\n@cartouche\n@quotation %s ' % name)
1115
1118
 
@@ -1119,7 +1122,7 @@ class TexinfoTranslator(SphinxTranslator):
1119
1122
 
1120
1123
  def depart_admonition(self, node: Element) -> None:
1121
1124
  self.ensure_eol()
1122
- self.body.append('@end quotation\n' '@end cartouche\n')
1125
+ self.body.append('@end quotation\n@end cartouche\n')
1123
1126
 
1124
1127
  visit_attention = _visit_named_admonition
1125
1128
  depart_attention = depart_admonition
@@ -1170,9 +1173,9 @@ class TexinfoTranslator(SphinxTranslator):
1170
1173
 
1171
1174
  def visit_topic(self, node: Element) -> None:
1172
1175
  # ignore TOC's since we have to have a "menu" anyway
1173
- if 'contents' in node.get('classes', []):
1176
+ if 'contents' in node.get('classes', ()):
1174
1177
  raise nodes.SkipNode
1175
- title = cast(nodes.title, node[0])
1178
+ title = cast('nodes.title', node[0])
1176
1179
  self.visit_rubric(title)
1177
1180
  self.body.append('%s\n' % self.escape(title.astext()))
1178
1181
  self.depart_rubric(title)
@@ -1231,12 +1234,12 @@ class TexinfoTranslator(SphinxTranslator):
1231
1234
  if uri.find('://') != -1:
1232
1235
  # ignore remote images
1233
1236
  return
1234
- name, ext = path.splitext(uri)
1237
+ name, ext = os.path.splitext(uri)
1235
1238
  # width and height ignored in non-tex output
1236
1239
  width = self.tex_image_length(node.get('width', ''))
1237
1240
  height = self.tex_image_length(node.get('height', ''))
1238
1241
  alt = self.escape_arg(node.get('alt', ''))
1239
- filename = f"{self.elements['filename'][:-5]}-figures/{name}" # type: ignore[index]
1242
+ filename = f'{self.elements["filename"][:-5]}-figures/{name}' # type: ignore[index]
1240
1243
  self.body.append(f'\n@image{{{filename},{width},{height},{alt},{ext[1:]}}}\n')
1241
1244
 
1242
1245
  def depart_image(self, node: Element) -> None:
@@ -1280,7 +1283,7 @@ class TexinfoTranslator(SphinxTranslator):
1280
1283
 
1281
1284
  def visit_system_message(self, node: Element) -> None:
1282
1285
  self.body.append(
1283
- '\n@verbatim\n' '<SYSTEM MESSAGE: %s>\n' '@end verbatim\n' % node.astext()
1286
+ '\n@verbatim\n<SYSTEM MESSAGE: %s>\n@end verbatim\n' % node.astext()
1284
1287
  )
1285
1288
  raise nodes.SkipNode
1286
1289
 
@@ -1306,21 +1309,11 @@ class TexinfoTranslator(SphinxTranslator):
1306
1309
 
1307
1310
  def visit_productionlist(self, node: Element) -> None:
1308
1311
  self.visit_literal_block(None)
1309
- productionlist = cast(Iterable[addnodes.production], node)
1310
- names = (production['tokenname'] for production in productionlist)
1311
- maxlen = max(len(name) for name in names)
1312
-
1313
- for production in productionlist:
1314
- if production['tokenname']:
1315
- for id in production.get('ids'):
1316
- self.add_anchor(id, production)
1317
- s = production['tokenname'].ljust(maxlen) + ' ::='
1318
- else:
1319
- s = '%s ' % (' ' * maxlen)
1320
- self.body.append(self.escape(s))
1321
- self.body.append(self.escape(production.astext() + '\n'))
1312
+ self.in_production_list = True
1313
+
1314
+ def depart_productionlist(self, node: Element) -> None:
1315
+ self.in_production_list = False
1322
1316
  self.depart_literal_block(None)
1323
- raise nodes.SkipNode
1324
1317
 
1325
1318
  def visit_production(self, node: Element) -> None:
1326
1319
  pass
@@ -1335,9 +1328,15 @@ class TexinfoTranslator(SphinxTranslator):
1335
1328
  self.body.append('}')
1336
1329
 
1337
1330
  def visit_literal_strong(self, node: Element) -> None:
1331
+ if self.in_production_list:
1332
+ for id_ in node['ids']:
1333
+ self.add_anchor(id_, node)
1334
+ return
1338
1335
  self.body.append('@code{')
1339
1336
 
1340
1337
  def depart_literal_strong(self, node: Element) -> None:
1338
+ if self.in_production_list:
1339
+ return
1341
1340
  self.body.append('}')
1342
1341
 
1343
1342
  def visit_index(self, node: Element) -> None:
@@ -1387,8 +1386,8 @@ class TexinfoTranslator(SphinxTranslator):
1387
1386
  pass
1388
1387
 
1389
1388
  def visit_acks(self, node: Element) -> None:
1390
- bullet_list = cast(nodes.bullet_list, node[0])
1391
- list_items = cast(Iterable[nodes.list_item], bullet_list)
1389
+ bullet_list = cast('nodes.bullet_list', node[0])
1390
+ list_items = cast('Iterable[nodes.list_item]', bullet_list)
1392
1391
  self.body.append('\n\n')
1393
1392
  self.body.append(', '.join(n.astext() for n in list_items) + '.')
1394
1393
  self.body.append('\n\n')
@@ -1418,11 +1417,11 @@ class TexinfoTranslator(SphinxTranslator):
1418
1417
  self.add_anchor(id, node)
1419
1418
  # use the full name of the objtype for the category
1420
1419
  try:
1421
- domain = self.builder.env.get_domain(node.parent['domain'])
1420
+ domain = self._domains[node.parent['domain']]
1422
1421
  name = domain.get_type_name(
1423
1422
  domain.object_types[objtype], self.config.primary_domain == domain.name
1424
1423
  )
1425
- except (KeyError, ExtensionError):
1424
+ except KeyError:
1426
1425
  name = objtype
1427
1426
  # by convention, the deffn category should be capitalized like a title
1428
1427
  category = self.escape_arg(smart_capwords(name))
@@ -1501,7 +1500,7 @@ class TexinfoTranslator(SphinxTranslator):
1501
1500
  self.first_param = 0
1502
1501
  text = self.escape(node.astext())
1503
1502
  # replace no-break spaces with normal ones
1504
- text = text.replace(' ', '@w{ }')
1503
+ text = text.replace('\N{NO-BREAK SPACE}', '@w{ }')
1505
1504
  self.body.append(text)
1506
1505
  raise nodes.SkipNode
1507
1506
 
@@ -1579,11 +1578,11 @@ class TexinfoTranslator(SphinxTranslator):
1579
1578
  def depart_pending_xref(self, node: Element) -> None:
1580
1579
  pass
1581
1580
 
1582
- def visit_math(self, node: Element) -> None:
1581
+ def visit_math(self, node: nodes.math) -> None:
1583
1582
  self.body.append('@math{' + self.escape_arg(node.astext()) + '}')
1584
1583
  raise nodes.SkipNode
1585
1584
 
1586
- def visit_math_block(self, node: Element) -> None:
1585
+ def visit_math_block(self, node: nodes.math_block) -> None:
1587
1586
  if node.get('label'):
1588
1587
  self.add_anchor(node['label'], node)
1589
1588
  self.body.append(
sphinx/writers/text.py CHANGED
@@ -6,9 +6,8 @@ import math
6
6
  import os
7
7
  import re
8
8
  import textwrap
9
- from collections.abc import Iterable, Iterator, Sequence
10
9
  from itertools import chain, groupby, pairwise
11
- from typing import TYPE_CHECKING, Any, ClassVar, cast
10
+ from typing import TYPE_CHECKING, cast
12
11
 
13
12
  from docutils import nodes, writers
14
13
  from docutils.utils import column_width
@@ -18,6 +17,9 @@ from sphinx.locale import _, admonitionlabels
18
17
  from sphinx.util.docutils import SphinxTranslator
19
18
 
20
19
  if TYPE_CHECKING:
20
+ from collections.abc import Iterable, Iterator, Sequence
21
+ from typing import Any, ClassVar
22
+
21
23
  from docutils.nodes import Element, Text
22
24
 
23
25
  from sphinx.builders.text import TextBuilder
@@ -45,7 +47,7 @@ class Cell:
45
47
  return hash((self.col, self.row))
46
48
 
47
49
  def __bool__(self) -> bool:
48
- return self.text != '' and self.col is not None and self.row is not None
50
+ return bool(self.text) and self.col is not None and self.row is not None
49
51
 
50
52
  def wrap(self, width: int) -> None:
51
53
  self.wrapped = my_wrap(self.text, width)
@@ -292,7 +294,7 @@ class TextWrapper(textwrap.TextWrapper):
292
294
 
293
295
  width = self.width - column_width(indent)
294
296
 
295
- if self.drop_whitespace and chunks[-1].strip() == '' and lines:
297
+ if self.drop_whitespace and not chunks[-1].strip() and lines:
296
298
  del chunks[-1]
297
299
 
298
300
  while chunks:
@@ -308,7 +310,7 @@ class TextWrapper(textwrap.TextWrapper):
308
310
  if chunks and column_width(chunks[-1]) > width:
309
311
  self._handle_long_word(chunks, cur_line, cur_len, width)
310
312
 
311
- if self.drop_whitespace and cur_line and cur_line[-1].strip() == '':
313
+ if self.drop_whitespace and cur_line and not cur_line[-1].strip():
312
314
  del cur_line[-1]
313
315
 
314
316
  if cur_line:
@@ -381,7 +383,7 @@ class TextWriter(writers.Writer): # type: ignore[type-arg]
381
383
  assert isinstance(self.document, nodes.document)
382
384
  visitor = self.builder.create_translator(self.document, self.builder)
383
385
  self.document.walkabout(visitor)
384
- self.output = cast(TextTranslator, visitor).body
386
+ self.output = cast('TextTranslator', visitor).body
385
387
 
386
388
 
387
389
  class TextTranslator(SphinxTranslator):
@@ -406,6 +408,7 @@ class TextTranslator(SphinxTranslator):
406
408
  self.sectionlevel = 0
407
409
  self.lineblocklevel = 0
408
410
  self.table: Table
411
+ self.in_production_list = False
409
412
 
410
413
  self.context: list[str] = []
411
414
  """Heterogeneous stack.
@@ -646,6 +649,7 @@ class TextTranslator(SphinxTranslator):
646
649
  self.required_params_left = sum(self.list_is_required_param)
647
650
  self.param_separator = ', '
648
651
  self.multi_line_parameter_list = node.get('multi_line_parameter_list', False)
652
+ self.trailing_comma = node.get('multi_line_trailing_comma', False)
649
653
  if self.multi_line_parameter_list:
650
654
  self.param_separator = self.param_separator.rstrip()
651
655
  self.context.append(sig_close_paren)
@@ -697,7 +701,8 @@ class TextTranslator(SphinxTranslator):
697
701
  or is_required
698
702
  and (is_last_group or next_is_required)
699
703
  ):
700
- self.add_text(self.param_separator)
704
+ if not is_last_group or opt_param_left_at_level or self.trailing_comma:
705
+ self.add_text(self.param_separator)
701
706
  self.end_state(wrap=False, end=None)
702
707
 
703
708
  elif self.required_params_left:
@@ -738,20 +743,27 @@ class TextTranslator(SphinxTranslator):
738
743
 
739
744
  def depart_desc_optional(self, node: Element) -> None:
740
745
  self.optional_param_level -= 1
746
+ level = self.optional_param_level
741
747
  if self.multi_line_parameter_list:
748
+ max_level = self.max_optional_param_level
749
+ len_lirp = len(self.list_is_required_param)
750
+ is_last_group = self.param_group_index + 1 == len_lirp
742
751
  # If it's the first time we go down one level, add the separator before the
743
- # bracket.
744
- if self.optional_param_level == self.max_optional_param_level - 1:
752
+ # bracket, except if this is the last parameter and the parameter list
753
+ # should not feature a trailing comma.
754
+ if level == max_level - 1 and (
755
+ not is_last_group or level > 0 or self.trailing_comma
756
+ ):
745
757
  self.add_text(self.param_separator)
746
758
  self.add_text(']')
747
759
  # End the line if we have just closed the last bracket of this group of
748
760
  # optional parameters.
749
- if self.optional_param_level == 0:
761
+ if level == 0:
750
762
  self.end_state(wrap=False, end=None)
751
763
 
752
764
  else:
753
765
  self.add_text(']')
754
- if self.optional_param_level == 0:
766
+ if level == 0:
755
767
  self.param_group_index += 1
756
768
 
757
769
  def visit_desc_annotation(self, node: Element) -> None:
@@ -776,22 +788,20 @@ class TextTranslator(SphinxTranslator):
776
788
 
777
789
  def visit_productionlist(self, node: Element) -> None:
778
790
  self.new_state()
779
- productionlist = cast(Iterable[addnodes.production], node)
780
- names = (production['tokenname'] for production in productionlist)
781
- maxlen = max(len(name) for name in names)
782
- lastname = None
783
- for production in productionlist:
784
- if production['tokenname']:
785
- self.add_text(production['tokenname'].ljust(maxlen) + ' ::=')
786
- lastname = production['tokenname']
787
- elif lastname is not None:
788
- self.add_text('%s ' % (' ' * len(lastname)))
789
- self.add_text(production.astext() + self.nl)
791
+ self.in_production_list = True
792
+
793
+ def depart_productionlist(self, node: Element) -> None:
794
+ self.in_production_list = False
790
795
  self.end_state(wrap=False)
791
- raise nodes.SkipNode
796
+
797
+ def visit_production(self, node: Element) -> None:
798
+ pass
799
+
800
+ def depart_production(self, node: Element) -> None:
801
+ pass
792
802
 
793
803
  def visit_footnote(self, node: Element) -> None:
794
- label = cast(nodes.label, node[0])
804
+ label = cast('nodes.label', node[0])
795
805
  self._footnote = label.astext().strip()
796
806
  self.new_state(len(self._footnote) + 3)
797
807
 
@@ -923,8 +933,8 @@ class TextTranslator(SphinxTranslator):
923
933
  self.end_state(wrap=False)
924
934
 
925
935
  def visit_acks(self, node: Element) -> None:
926
- bullet_list = cast(nodes.bullet_list, node[0])
927
- list_items = cast(Iterable[nodes.list_item], bullet_list)
936
+ bullet_list = cast('nodes.bullet_list', node[0])
937
+ list_items = cast('Iterable[nodes.list_item]', bullet_list)
928
938
  self.new_state(0)
929
939
  self.add_text(', '.join(n.astext() for n in list_items) + '.')
930
940
  self.end_state()
@@ -1214,9 +1224,13 @@ class TextTranslator(SphinxTranslator):
1214
1224
  self.add_text('**')
1215
1225
 
1216
1226
  def visit_literal_strong(self, node: Element) -> None:
1227
+ if self.in_production_list:
1228
+ return
1217
1229
  self.add_text('**')
1218
1230
 
1219
1231
  def depart_literal_strong(self, node: Element) -> None:
1232
+ if self.in_production_list:
1233
+ return
1220
1234
  self.add_text('**')
1221
1235
 
1222
1236
  def visit_abbreviation(self, node: Element) -> None:
@@ -1239,9 +1253,13 @@ class TextTranslator(SphinxTranslator):
1239
1253
  self.add_text('*')
1240
1254
 
1241
1255
  def visit_literal(self, node: Element) -> None:
1256
+ if self.in_production_list:
1257
+ return
1242
1258
  self.add_text('"')
1243
1259
 
1244
1260
  def depart_literal(self, node: Element) -> None:
1261
+ if self.in_production_list:
1262
+ return
1245
1263
  self.add_text('"')
1246
1264
 
1247
1265
  def visit_subscript(self, node: Element) -> None:
@@ -1316,14 +1334,14 @@ class TextTranslator(SphinxTranslator):
1316
1334
  self.end_state(wrap=False)
1317
1335
  raise nodes.SkipNode
1318
1336
 
1319
- def visit_math(self, node: Element) -> None:
1337
+ def visit_math(self, node: nodes.math) -> None:
1320
1338
  pass
1321
1339
 
1322
- def depart_math(self, node: Element) -> None:
1340
+ def depart_math(self, node: nodes.math) -> None:
1323
1341
  pass
1324
1342
 
1325
- def visit_math_block(self, node: Element) -> None:
1343
+ def visit_math_block(self, node: nodes.math_block) -> None:
1326
1344
  self.new_state()
1327
1345
 
1328
- def depart_math_block(self, node: Element) -> None:
1346
+ def depart_math_block(self, node: nodes.math_block) -> None:
1329
1347
  self.end_state()
sphinx/writers/xml.py CHANGED
@@ -2,11 +2,13 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Any
5
+ from typing import TYPE_CHECKING
6
6
 
7
7
  from docutils.writers.docutils_xml import Writer as BaseXMLWriter
8
8
 
9
9
  if TYPE_CHECKING:
10
+ from typing import Any
11
+
10
12
  from sphinx.builders import Builder
11
13
 
12
14
 
@@ -16,19 +18,20 @@ class XMLWriter(BaseXMLWriter): # type: ignore[misc]
16
18
  def __init__(self, builder: Builder) -> None:
17
19
  super().__init__()
18
20
  self.builder = builder
19
-
20
- # A lambda function to generate translator lazily
21
- self.translator_class = lambda document: self.builder.create_translator(
22
- document
23
- )
21
+ self._config = builder.config
24
22
 
25
23
  def translate(self, *args: Any, **kwargs: Any) -> None:
26
24
  self.document.settings.newlines = self.document.settings.indents = (
27
- self.builder.env.config.xml_pretty
25
+ self._config.xml_pretty
28
26
  )
29
27
  self.document.settings.xml_declaration = True
30
28
  self.document.settings.doctype_declaration = True
31
- return super().translate()
29
+
30
+ # copied from docutils.writers.docutils_xml.Writer.translate()
31
+ # so that we can override the translator class
32
+ self.visitor = visitor = self.builder.create_translator(self.document)
33
+ self.document.walkabout(visitor)
34
+ self.output = ''.join(visitor.output) # type: ignore[attr-defined]
32
35
 
33
36
 
34
37
  class PseudoXMLWriter(BaseXMLWriter): # type: ignore[misc]
@@ -4,7 +4,7 @@ License for Sphinx
4
4
  Unless otherwise indicated, all code in the Sphinx project is licenced under the
5
5
  two clause BSD licence below.
6
6
 
7
- Copyright (c) 2007-2024 by the Sphinx team (see AUTHORS file).
7
+ Copyright (c) 2007-2025 by the Sphinx team (see AUTHORS file).
8
8
  All rights reserved.
9
9
 
10
10
  Redistribution and use in source and binary forms, with or without