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/writers/html5.py CHANGED
@@ -2,12 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import os
6
5
  import posixpath
7
6
  import re
8
7
  import urllib.parse
9
- from collections.abc import Iterable
10
- from typing import TYPE_CHECKING, cast
8
+ from typing import TYPE_CHECKING
11
9
 
12
10
  from docutils import nodes
13
11
  from docutils.writers.html5_polyglot import HTMLTranslator as BaseTranslator
@@ -44,9 +42,7 @@ def multiply_length(length: str, scale: int) -> str:
44
42
 
45
43
 
46
44
  class HTML5Translator(SphinxTranslator, BaseTranslator): # type: ignore[misc]
47
- """
48
- Our custom HTML translator.
49
- """
45
+ """Our custom HTML translator."""
50
46
 
51
47
  builder: StandaloneHTMLBuilder
52
48
  # Override docutils.writers.html5_polyglot:HTMLTranslator
@@ -66,6 +62,7 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): # type: ignore[misc]
66
62
  self._table_row_indices = [0]
67
63
  self._fieldlist_row_indices = [0]
68
64
  self.required_params_left = 0
65
+ self._has_maths_elements: bool = False
69
66
 
70
67
  def visit_start_of_file(self, node: Element) -> None:
71
68
  # only occurs in the single-file builder
@@ -175,6 +172,7 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): # type: ignore[misc]
175
172
  self.required_params_left = sum(self.list_is_required_param)
176
173
  self.param_separator = node.child_text_separator
177
174
  self.multi_line_parameter_list = node.get('multi_line_parameter_list', False)
175
+ self.trailing_comma = node.get('multi_line_trailing_comma', False)
178
176
  if self.multi_line_parameter_list:
179
177
  self.body.append('\n\n')
180
178
  self.body.append(self.starttag(node, 'dl'))
@@ -201,10 +199,11 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): # type: ignore[misc]
201
199
 
202
200
  # If required parameters are still to come, then put the comma after
203
201
  # the parameter. Otherwise, put the comma before. This ensures that
204
- # signatures like the following render correctly (see issue #1001):
202
+ # signatures like the following render correctly:
205
203
  #
206
204
  # foo([a, ]b, c[, d])
207
205
  #
206
+ # See: https://github.com/sphinx-doc/sphinx/issues/1001
208
207
  def visit_desc_parameter(self, node: Element) -> None:
209
208
  on_separate_line = self.multi_line_parameter_list
210
209
  if on_separate_line and not (
@@ -232,14 +231,15 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): # type: ignore[misc]
232
231
  next_is_required = (
233
232
  not is_last_group
234
233
  and self.list_is_required_param[self.param_group_index + 1]
235
- ) # fmt: skip
234
+ )
236
235
  opt_param_left_at_level = self.params_left_at_level > 0
237
236
  if (
238
237
  opt_param_left_at_level
239
238
  or is_required
240
239
  and (is_last_group or next_is_required)
241
240
  ):
242
- self.body.append(self.param_separator)
241
+ if not is_last_group or opt_param_left_at_level or self.trailing_comma:
242
+ self.body.append(self.param_separator)
243
243
  self.body.append('</dd>\n')
244
244
 
245
245
  elif self.required_params_left:
@@ -282,19 +282,26 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): # type: ignore[misc]
282
282
 
283
283
  def depart_desc_optional(self, node: Element) -> None:
284
284
  self.optional_param_level -= 1
285
+ level = self.optional_param_level
285
286
  if self.multi_line_parameter_list:
286
- # If it's the first time we go down one level, add the separator
287
- # before the bracket.
288
- if self.optional_param_level == self.max_optional_param_level - 1:
287
+ max_level = self.max_optional_param_level
288
+ len_lirp = len(self.list_is_required_param)
289
+ is_last_group = self.param_group_index + 1 == len_lirp
290
+ # If it's the first time we go down one level, add the separator before the
291
+ # bracket, except if this is the last parameter and the parameter list
292
+ # should not feature a trailing comma.
293
+ if level == max_level - 1 and (
294
+ not is_last_group or level > 0 or self.trailing_comma
295
+ ):
289
296
  self.body.append(self.param_separator)
290
297
  self.body.append('<span class="optional">]</span>')
291
298
  # End the line if we have just closed the last bracket of this
292
299
  # optional parameter group.
293
- if self.optional_param_level == 0:
300
+ if level == 0:
294
301
  self.body.append('</dd>\n')
295
302
  else:
296
303
  self.body.append('<span class="optional">]</span>')
297
- if self.optional_param_level == 0:
304
+ if level == 0:
298
305
  self.param_group_index += 1
299
306
 
300
307
  def visit_desc_annotation(self, node: Element) -> None:
@@ -327,9 +334,9 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): # type: ignore[misc]
327
334
  atts['href'] = self.cloak_mailto(atts['href'])
328
335
  self.in_mailto = True
329
336
  else:
330
- assert (
331
- 'refid' in node
332
- ), 'References must have "refuri" or "refid" attribute.'
337
+ assert 'refid' in node, (
338
+ 'References must have "refuri" or "refid" attribute.'
339
+ )
333
340
  atts['href'] = '#' + node['refid']
334
341
  if not isinstance(node.parent, nodes.TextElement):
335
342
  assert len(node) == 1 and isinstance(node[0], nodes.image) # NoQA: PT018
@@ -359,12 +366,21 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): # type: ignore[misc]
359
366
 
360
367
  # overwritten
361
368
  def visit_admonition(self, node: Element, name: str = '') -> None:
362
- self.body.append(self.starttag(node, 'div', CLASS=('admonition ' + name)))
369
+ attributes = {}
370
+ tag_name = 'div'
371
+ if collapsible := node.get('collapsible'):
372
+ tag_name = 'details'
373
+ if collapsible == 'open':
374
+ attributes['open'] = 'open'
375
+ self.body.append(
376
+ self.starttag(node, tag_name, CLASS=f'admonition {name}', **attributes)
377
+ )
378
+ self.context.append(f'</{tag_name}>\n')
363
379
  if name:
364
380
  node.insert(0, nodes.title(name, admonitionlabels[name]))
365
381
 
366
382
  def depart_admonition(self, node: Element | None = None) -> None:
367
- self.body.append('</div>\n')
383
+ self.body.append(self.context.pop())
368
384
 
369
385
  def visit_seealso(self, node: Element) -> None:
370
386
  self.visit_admonition(node, 'seealso')
@@ -379,7 +395,7 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): # type: ignore[misc]
379
395
  if isinstance(node.parent, nodes.section):
380
396
  if self.builder.name == 'singlehtml':
381
397
  docname = self.docnames[-1]
382
- anchorname = f"{docname}/#{node.parent['ids'][0]}"
398
+ anchorname = f'{docname}/#{node.parent["ids"][0]}'
383
399
  if anchorname not in self.builder.secnumbers:
384
400
  # try first heading which has no anchor
385
401
  anchorname = f'{docname}/'
@@ -419,9 +435,7 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): # type: ignore[misc]
419
435
  self.body.append(prefix % '.'.join(map(str, numbers)) + ' ')
420
436
  self.body.append('</span>')
421
437
 
422
- figtype = self.builder.env.domains.standard_domain.get_enumerable_node_type(
423
- node
424
- )
438
+ figtype = self._domains.standard_domain.get_enumerable_node_type(node)
425
439
  if figtype:
426
440
  if len(node['ids']) == 0:
427
441
  msg = __('Any IDs not assigned for %s node') % node.tagname
@@ -494,6 +508,15 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): # type: ignore[misc]
494
508
  )
495
509
  self.body.append('<span class="caption-text">')
496
510
  self.context.append('</span></p>\n')
511
+ elif (
512
+ isinstance(node.parent, nodes.Admonition)
513
+ and isinstance(node.parent, nodes.Element)
514
+ and 'collapsible' in node.parent
515
+ ):
516
+ self.body.append(
517
+ self.starttag(node, 'summary', '', CLASS='admonition-title')
518
+ )
519
+ self.context.append('</summary>\n')
497
520
  else:
498
521
  super().visit_title(node)
499
522
  self.add_secnumber(node)
@@ -671,24 +694,9 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): # type: ignore[misc]
671
694
 
672
695
  def visit_productionlist(self, node: Element) -> None:
673
696
  self.body.append(self.starttag(node, 'pre'))
674
- productionlist = cast(Iterable[addnodes.production], node)
675
- names = (production['tokenname'] for production in productionlist)
676
- maxlen = max(len(name) for name in names)
677
- lastname = None
678
- for production in productionlist:
679
- if production['tokenname']:
680
- lastname = production['tokenname'].ljust(maxlen)
681
- self.body.append(self.starttag(production, 'strong', ''))
682
- self.body.append(lastname + '</strong> ::= ')
683
- elif lastname is not None:
684
- self.body.append('%s ' % (' ' * len(lastname)))
685
- production.walkabout(self)
686
- self.body.append('\n')
687
- self.body.append('</pre>\n')
688
- raise nodes.SkipNode
689
697
 
690
698
  def depart_productionlist(self, node: Element) -> None:
691
- pass
699
+ self.body.append('</pre>\n')
692
700
 
693
701
  def visit_production(self, node: Element) -> None:
694
702
  pass
@@ -752,8 +760,7 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): # type: ignore[misc]
752
760
  # but it tries the final file name, which does not necessarily exist
753
761
  # yet at the time the HTML file is written.
754
762
  if not ('width' in node and 'height' in node):
755
- path = os.path.join(self.builder.srcdir, olduri) # type: ignore[has-type]
756
- size = get_image_size(path)
763
+ size = get_image_size(self.builder.srcdir / olduri)
757
764
  if size is None:
758
765
  logger.warning(
759
766
  __('Could not obtain image size. :scale: option is ignored.'),
@@ -820,7 +827,7 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): # type: ignore[misc]
820
827
  if token.strip():
821
828
  # protect literal text from line wrapping
822
829
  self.body.append('<span class="pre">%s</span>' % token)
823
- elif token in ' \n':
830
+ elif token in {' ', '\n'}:
824
831
  # allow breaks at whitespace
825
832
  self.body.append(token)
826
833
  else:
@@ -957,29 +964,33 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): # type: ignore[misc]
957
964
  else:
958
965
  node['classes'].append('field-odd')
959
966
 
960
- def visit_math(self, node: Element, math_env: str = '') -> None:
967
+ def visit_math(self, node: nodes.math, math_env: str = '') -> None:
968
+ self._has_maths_elements = True
969
+
961
970
  # see validate_math_renderer
962
971
  name: str = self.builder.math_renderer_name # type: ignore[assignment]
963
- visit, _ = self.builder.app.registry.html_inline_math_renderers[name]
972
+ visit, _ = self.builder.env._registry.html_inline_math_renderers[name]
964
973
  visit(self, node)
965
974
 
966
- def depart_math(self, node: Element, math_env: str = '') -> None:
975
+ def depart_math(self, node: nodes.math, math_env: str = '') -> None:
967
976
  # see validate_math_renderer
968
977
  name: str = self.builder.math_renderer_name # type: ignore[assignment]
969
- _, depart = self.builder.app.registry.html_inline_math_renderers[name]
978
+ _, depart = self.builder.env._registry.html_inline_math_renderers[name]
970
979
  if depart:
971
980
  depart(self, node)
972
981
 
973
- def visit_math_block(self, node: Element, math_env: str = '') -> None:
982
+ def visit_math_block(self, node: nodes.math_block, math_env: str = '') -> None:
983
+ self._has_maths_elements = True
984
+
974
985
  # see validate_math_renderer
975
986
  name: str = self.builder.math_renderer_name # type: ignore[assignment]
976
- visit, _ = self.builder.app.registry.html_block_math_renderers[name]
987
+ visit, _ = self.builder.env._registry.html_block_math_renderers[name]
977
988
  visit(self, node)
978
989
 
979
- def depart_math_block(self, node: Element, math_env: str = '') -> None:
990
+ def depart_math_block(self, node: nodes.math_block, math_env: str = '') -> None:
980
991
  # see validate_math_renderer
981
992
  name: str = self.builder.math_renderer_name # type: ignore[assignment]
982
- _, depart = self.builder.app.registry.html_block_math_renderers[name]
993
+ _, depart = self.builder.env._registry.html_block_math_renderers[name]
983
994
  if depart:
984
995
  depart(self, node)
985
996
 
sphinx/writers/latex.py CHANGED
@@ -8,11 +8,11 @@ from __future__ import annotations
8
8
 
9
9
  import re
10
10
  from collections import defaultdict
11
- from collections.abc import Iterable
12
- from os import path
13
- from typing import TYPE_CHECKING, Any, ClassVar, cast
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING, cast
14
13
 
15
14
  from docutils import nodes, writers
15
+ from roman_numerals import RomanNumeral
16
16
 
17
17
  from sphinx import addnodes, highlighting
18
18
  from sphinx.errors import SphinxError
@@ -24,13 +24,10 @@ from sphinx.util.nodes import clean_astext, get_prev_node
24
24
  from sphinx.util.template import LaTeXRenderer
25
25
  from sphinx.util.texescape import tex_replace_map
26
26
 
27
- try:
28
- from docutils.utils.roman import toRoman
29
- except ImportError:
30
- # In Debian/Ubuntu, roman package is provided as roman, not as docutils.utils.roman
31
- from roman import toRoman # type: ignore[no-redef, import-not-found]
32
-
33
27
  if TYPE_CHECKING:
28
+ from collections.abc import Iterable
29
+ from typing import Any, ClassVar
30
+
34
31
  from docutils.nodes import Element, Node, Text
35
32
 
36
33
  from sphinx.builders.latex import LaTeXBuilder
@@ -100,7 +97,7 @@ class LaTeXWriter(writers.Writer): # type: ignore[type-arg]
100
97
  self.document, self.builder, self.theme
101
98
  )
102
99
  self.document.walkabout(visitor)
103
- self.output = cast(LaTeXTranslator, visitor).astext()
100
+ self.output = cast('LaTeXTranslator', visitor).astext()
104
101
 
105
102
 
106
103
  # Helper classes
@@ -219,8 +216,8 @@ class Table:
219
216
  self.cell_id += 1
220
217
  for col in range(width):
221
218
  for row in range(height):
222
- assert self.cells[(self.row + row, self.col + col)] == 0
223
- self.cells[(self.row + row, self.col + col)] = self.cell_id
219
+ assert self.cells[self.row + row, self.col + col] == 0
220
+ self.cells[self.row + row, self.col + col] = self.cell_id
224
221
 
225
222
  def cell(
226
223
  self,
@@ -246,25 +243,25 @@ class TableCell:
246
243
  """Data of a cell in a table."""
247
244
 
248
245
  def __init__(self, table: Table, row: int, col: int) -> None:
249
- if table.cells[(row, col)] == 0:
246
+ if table.cells[row, col] == 0:
250
247
  raise IndexError
251
248
 
252
249
  self.table = table
253
- self.cell_id = table.cells[(row, col)]
250
+ self.cell_id = table.cells[row, col]
254
251
  self.row = row
255
252
  self.col = col
256
253
 
257
254
  # adjust position for multirow/multicol cell
258
- while table.cells[(self.row - 1, self.col)] == self.cell_id:
255
+ while table.cells[self.row - 1, self.col] == self.cell_id:
259
256
  self.row -= 1
260
- while table.cells[(self.row, self.col - 1)] == self.cell_id:
257
+ while table.cells[self.row, self.col - 1] == self.cell_id:
261
258
  self.col -= 1
262
259
 
263
260
  @property
264
261
  def width(self) -> int:
265
262
  """Returns the cell width."""
266
263
  width = 0
267
- while self.table.cells[(self.row, self.col + width)] == self.cell_id:
264
+ while self.table.cells[self.row, self.col + width] == self.cell_id:
268
265
  width += 1
269
266
  return width
270
267
 
@@ -272,7 +269,7 @@ class TableCell:
272
269
  def height(self) -> int:
273
270
  """Returns the cell height."""
274
271
  height = 0
275
- while self.table.cells[(self.row + height, self.col)] == self.cell_id:
272
+ while self.table.cells[self.row + height, self.col] == self.cell_id:
276
273
  height += 1
277
274
  return height
278
275
 
@@ -291,7 +288,7 @@ def rstdim_to_latexdim(width_str: str, scale: int = 100) -> str:
291
288
  amount, unit = match.groups()[:2]
292
289
  if scale == 100:
293
290
  float(amount) # validate amount is float
294
- if unit in ('', 'px'):
291
+ if unit in {'', 'px'}:
295
292
  res = r'%s\sphinxpxdimen' % amount
296
293
  elif unit == 'pt':
297
294
  res = '%sbp' % amount # convert to 'bp'
@@ -299,7 +296,7 @@ def rstdim_to_latexdim(width_str: str, scale: int = 100) -> str:
299
296
  res = r'%.3f\linewidth' % (float(amount) / 100.0)
300
297
  else:
301
298
  amount_float = float(amount) * scale / 100.0
302
- if unit in ('', 'px'):
299
+ if unit in {'', 'px'}:
303
300
  res = r'%.5f\sphinxpxdimen' % amount_float
304
301
  elif unit == 'pt':
305
302
  res = '%.5fbp' % amount_float
@@ -326,7 +323,7 @@ class LaTeXTranslator(SphinxTranslator):
326
323
 
327
324
  # flags
328
325
  self.in_title = 0
329
- self.in_production_list = 0
326
+ self.in_production_list = False
330
327
  self.in_footnote = 0
331
328
  self.in_caption = 0
332
329
  self.in_term = 0
@@ -564,7 +561,7 @@ class LaTeXTranslator(SphinxTranslator):
564
561
  indices_config = frozenset(indices_config)
565
562
  else:
566
563
  check_names = False
567
- for domain in self.builder.env.domains.sorted():
564
+ for domain in self._domains.sorted():
568
565
  for index_cls in domain.indices:
569
566
  index_name = f'{domain.name}-{index_cls.name}'
570
567
  if check_names and index_name not in indices_config:
@@ -583,18 +580,19 @@ class LaTeXTranslator(SphinxTranslator):
583
580
  def render(self, template_name: str, variables: dict[str, Any]) -> str:
584
581
  renderer = LaTeXRenderer(latex_engine=self.config.latex_engine)
585
582
  for template_dir in self.config.templates_path:
586
- template = path.join(self.builder.confdir, template_dir, template_name)
587
- if path.exists(template):
588
- return renderer.render(template, variables)
589
- elif template.endswith('.jinja'):
590
- legacy_template = template.removesuffix('.jinja') + '_t'
591
- if path.exists(legacy_template):
583
+ template = self.builder.confdir / template_dir / template_name
584
+ if template.exists():
585
+ return renderer.render(str(template), variables)
586
+ elif template.suffix == '.jinja':
587
+ legacy_template_name = template.name.removesuffix('.jinja') + '_t'
588
+ legacy_template = template.with_name(legacy_template_name)
589
+ if legacy_template.exists():
592
590
  logger.warning(
593
591
  __('template %s not found; loading from legacy %s instead'),
594
592
  template_name,
595
593
  legacy_template,
596
594
  )
597
- return renderer.render(legacy_template, variables)
595
+ return renderer.render(str(legacy_template), variables)
598
596
 
599
597
  return renderer.render(template_name, variables)
600
598
 
@@ -673,22 +671,20 @@ class LaTeXTranslator(SphinxTranslator):
673
671
  def visit_productionlist(self, node: Element) -> None:
674
672
  self.body.append(BLANKLINE)
675
673
  self.body.append(r'\begin{productionlist}' + CR)
676
- self.in_production_list = 1
674
+ self.in_production_list = True
677
675
 
678
676
  def depart_productionlist(self, node: Element) -> None:
677
+ self.in_production_list = False
679
678
  self.body.append(r'\end{productionlist}' + BLANKLINE)
680
- self.in_production_list = 0
681
679
 
682
680
  def visit_production(self, node: Element) -> None:
683
- if node['tokenname']:
684
- tn = node['tokenname']
685
- self.body.append(self.hypertarget('grammar-token-' + tn))
686
- self.body.append(r'\production{%s}{' % self.encode(tn))
687
- else:
688
- self.body.append(r'\productioncont{')
681
+ # Nothing to do, the productionlist LaTeX environment
682
+ # is configured to render the nodes line-by-line
683
+ # But see also visit_literal_strong special clause.
684
+ pass
689
685
 
690
686
  def depart_production(self, node: Element) -> None:
691
- self.body.append('}' + CR)
687
+ pass
692
688
 
693
689
  def visit_transition(self, node: Element) -> None:
694
690
  self.body.append(self.elements['transition'])
@@ -956,6 +952,7 @@ class LaTeXTranslator(SphinxTranslator):
956
952
  self.required_params_left = sum(self.list_is_required_param)
957
953
  self.param_separator = r'\sphinxparamcomma '
958
954
  self.multi_line_parameter_list = node.get('multi_line_parameter_list', False)
955
+ self.trailing_comma = node.get('multi_line_trailing_comma', False)
959
956
 
960
957
  def visit_desc_parameterlist(self, node: Element) -> None:
961
958
  if self.has_tp_list:
@@ -1015,7 +1012,7 @@ class LaTeXTranslator(SphinxTranslator):
1015
1012
  if (
1016
1013
  opt_param_left_at_level
1017
1014
  or is_required
1018
- and (is_last_group or next_is_required)
1015
+ and (next_is_required or self.trailing_comma)
1019
1016
  ):
1020
1017
  self.body.append(self.param_separator)
1021
1018
 
@@ -1057,13 +1054,20 @@ class LaTeXTranslator(SphinxTranslator):
1057
1054
 
1058
1055
  def depart_desc_optional(self, node: Element) -> None:
1059
1056
  self.optional_param_level -= 1
1057
+ level = self.optional_param_level
1060
1058
  if self.multi_line_parameter_list:
1059
+ max_level = self.max_optional_param_level
1060
+ len_lirp = len(self.list_is_required_param)
1061
+ is_last_group = self.param_group_index + 1 == len_lirp
1061
1062
  # If it's the first time we go down one level, add the separator before the
1062
- # bracket.
1063
- if self.optional_param_level == self.max_optional_param_level - 1:
1063
+ # bracket, except if this is the last parameter and the parameter list
1064
+ # should not feature a trailing comma.
1065
+ if level == max_level - 1 and (
1066
+ not is_last_group or level > 0 or self.trailing_comma
1067
+ ):
1064
1068
  self.body.append(self.param_separator)
1065
1069
  self.body.append('}')
1066
- if self.optional_param_level == 0:
1070
+ if level == 0:
1067
1071
  self.param_group_index += 1
1068
1072
 
1069
1073
  def visit_desc_annotation(self, node: Element) -> None:
@@ -1090,7 +1094,7 @@ class LaTeXTranslator(SphinxTranslator):
1090
1094
  self.no_latex_floats -= 1
1091
1095
 
1092
1096
  def visit_rubric(self, node: nodes.rubric) -> None:
1093
- if len(node) == 1 and node.astext() in ('Footnotes', _('Footnotes')):
1097
+ if len(node) == 1 and node.astext() in {'Footnotes', _('Footnotes')}:
1094
1098
  raise nodes.SkipNode
1095
1099
  tag = 'subsubsection'
1096
1100
  if 'heading-level' in node:
@@ -1115,7 +1119,7 @@ class LaTeXTranslator(SphinxTranslator):
1115
1119
 
1116
1120
  def visit_footnote(self, node: Element) -> None:
1117
1121
  self.in_footnote += 1
1118
- label = cast(nodes.label, node[0])
1122
+ label = cast('nodes.label', node[0])
1119
1123
  if self.in_parsed_literal:
1120
1124
  self.body.append(r'\begin{footnote}[%s]' % label.astext())
1121
1125
  else:
@@ -1337,7 +1341,7 @@ class LaTeXTranslator(SphinxTranslator):
1337
1341
  if (
1338
1342
  len(node) == 1
1339
1343
  and isinstance(node[0], nodes.paragraph)
1340
- and node.astext() == ''
1344
+ and not node.astext()
1341
1345
  ):
1342
1346
  pass
1343
1347
  else:
@@ -1386,8 +1390,8 @@ class LaTeXTranslator(SphinxTranslator):
1386
1390
  def visit_acks(self, node: Element) -> None:
1387
1391
  # this is a list in the source, but should be rendered as a
1388
1392
  # comma-separated list here
1389
- bullet_list = cast(nodes.bullet_list, node[0])
1390
- list_items = cast(Iterable[nodes.list_item], bullet_list)
1393
+ bullet_list = cast('nodes.bullet_list', node[0])
1394
+ list_items = cast('Iterable[nodes.list_item]', bullet_list)
1391
1395
  self.body.append(BLANKLINE)
1392
1396
  self.body.append(', '.join(n.astext() for n in list_items) + '.')
1393
1397
  self.body.append(BLANKLINE)
@@ -1420,8 +1424,9 @@ class LaTeXTranslator(SphinxTranslator):
1420
1424
  else:
1421
1425
  return get_nested_level(node.parent)
1422
1426
 
1423
- enum = 'enum%s' % toRoman(get_nested_level(node)).lower()
1424
- enumnext = 'enum%s' % toRoman(get_nested_level(node) + 1).lower()
1427
+ nested_level = get_nested_level(node)
1428
+ enum = f'enum{RomanNumeral(nested_level).to_lowercase()}'
1429
+ enumnext = f'enum{RomanNumeral(nested_level + 1).to_lowercase()}'
1425
1430
  style = ENUMERATE_LIST_STYLE.get(get_enumtype(node))
1426
1431
  prefix = node.get('prefix', '')
1427
1432
  suffix = node.get('suffix', '.')
@@ -1648,7 +1653,9 @@ class LaTeXTranslator(SphinxTranslator):
1648
1653
  options = ''
1649
1654
  if include_graphics_options:
1650
1655
  options = '[%s]' % ','.join(include_graphics_options)
1651
- base, ext = path.splitext(uri)
1656
+ img_path = Path(uri)
1657
+ base = img_path.with_suffix('')
1658
+ ext = img_path.suffix
1652
1659
 
1653
1660
  if self.in_title and base:
1654
1661
  # Lowercase tokens forcely because some fncychap themes capitalize
@@ -1657,8 +1664,8 @@ class LaTeXTranslator(SphinxTranslator):
1657
1664
  else:
1658
1665
  cmd = rf'\sphinxincludegraphics{options}{{{{{base}}}{ext}}}'
1659
1666
  # escape filepath for includegraphics, https://tex.stackexchange.com/a/202714/41112
1660
- if '#' in base:
1661
- cmd = r'{\catcode`\#=12' + cmd + '}'
1667
+ if '#' in str(base):
1668
+ cmd = rf'{{\catcode`\#=12{cmd}}}'
1662
1669
  self.body.append(cmd)
1663
1670
  self.body.extend(post)
1664
1671
 
@@ -1684,7 +1691,7 @@ class LaTeXTranslator(SphinxTranslator):
1684
1691
  if any(isinstance(child, nodes.caption) for child in node):
1685
1692
  self.body.append(r'\capstart')
1686
1693
  self.context.append(r'\end{sphinxfigure-in-table}\relax' + CR)
1687
- elif node.get('align', '') in ('left', 'right'):
1694
+ elif node.get('align', '') in {'left', 'right'}:
1688
1695
  length = None
1689
1696
  if 'width' in node:
1690
1697
  length = self.latex_image_length(node['width'])
@@ -1816,7 +1823,7 @@ class LaTeXTranslator(SphinxTranslator):
1816
1823
  while isinstance(next_node, nodes.target):
1817
1824
  next_node = next_node.next_node(ascend=True)
1818
1825
 
1819
- domain = self.builder.env.domains.standard_domain
1826
+ domain = self._domains.standard_domain
1820
1827
  if isinstance(next_node, HYPERLINK_SUPPORT_NODES):
1821
1828
  return
1822
1829
  if (
@@ -1840,7 +1847,8 @@ class LaTeXTranslator(SphinxTranslator):
1840
1847
  else:
1841
1848
  add_target(node['refid'])
1842
1849
  # Temporary fix for https://github.com/sphinx-doc/sphinx/issues/11093
1843
- # TODO: investigate if a more elegant solution exists (see comments of #11093)
1850
+ # TODO: investigate if a more elegant solution exists
1851
+ # (see comments of https://github.com/sphinx-doc/sphinx/issues/11093)
1844
1852
  if node.get('ismod', False):
1845
1853
  # Detect if the previous nodes are label targets. If so, remove
1846
1854
  # the refid thereof from node['ids'] to avoid duplicated ids.
@@ -2061,9 +2069,16 @@ class LaTeXTranslator(SphinxTranslator):
2061
2069
  self.body.append('}')
2062
2070
 
2063
2071
  def visit_literal_strong(self, node: Element) -> None:
2072
+ if self.in_production_list:
2073
+ ctx = [r'\phantomsection']
2074
+ ctx += [self.hypertarget(id_, anchor=False) for id_ in node['ids']]
2075
+ self.body.append(''.join(ctx))
2076
+ return
2064
2077
  self.body.append(r'\sphinxstyleliteralstrong{\sphinxupquote{')
2065
2078
 
2066
2079
  def depart_literal_strong(self, node: Element) -> None:
2080
+ if self.in_production_list:
2081
+ return
2067
2082
  self.body.append('}}')
2068
2083
 
2069
2084
  def visit_abbreviation(self, node: Element) -> None:
@@ -2092,8 +2107,8 @@ class LaTeXTranslator(SphinxTranslator):
2092
2107
  self.body.append('}')
2093
2108
 
2094
2109
  def visit_thebibliography(self, node: Element) -> None:
2095
- citations = cast(Iterable[nodes.citation], node)
2096
- labels = (cast(nodes.label, citation[0]) for citation in citations)
2110
+ citations = cast('Iterable[nodes.citation]', node)
2111
+ labels = (cast('nodes.label', citation[0]) for citation in citations)
2097
2112
  longest_label = max((label.astext() for label in labels), key=len)
2098
2113
  if len(longest_label) > MAX_CITATION_LABEL_LENGTH:
2099
2114
  # adjust max width of citation labels not to break the layout
@@ -2107,7 +2122,7 @@ class LaTeXTranslator(SphinxTranslator):
2107
2122
  self.body.append(r'\end{sphinxthebibliography}' + CR)
2108
2123
 
2109
2124
  def visit_citation(self, node: Element) -> None:
2110
- label = cast(nodes.label, node[0])
2125
+ label = cast('nodes.label', node[0])
2111
2126
  self.body.append(
2112
2127
  rf'\bibitem[{self.encode(label.astext())}]'
2113
2128
  rf'{{{node["docname"]}:{node["ids"][0]}}}'
@@ -2160,7 +2175,7 @@ class LaTeXTranslator(SphinxTranslator):
2160
2175
  self.body.append(']')
2161
2176
 
2162
2177
  def visit_footnotetext(self, node: Element) -> None:
2163
- label = cast(nodes.label, node[0])
2178
+ label = cast('nodes.label', node[0])
2164
2179
  self.body.append('%' + CR)
2165
2180
  self.body.append(r'\begin{footnotetext}[%s]' % label.astext())
2166
2181
  self.body.append(r'\sphinxAtStartFootnote' + CR)
@@ -2443,20 +2458,20 @@ class LaTeXTranslator(SphinxTranslator):
2443
2458
  def depart_system_message(self, node: Element) -> None:
2444
2459
  self.body.append(CR)
2445
2460
 
2446
- def visit_math(self, node: Element) -> None:
2461
+ def visit_math(self, node: nodes.math) -> None:
2447
2462
  if self.in_title:
2448
2463
  self.body.append(r'\protect\(%s\protect\)' % node.astext())
2449
2464
  else:
2450
2465
  self.body.append(r'\(%s\)' % node.astext())
2451
2466
  raise nodes.SkipNode
2452
2467
 
2453
- def visit_math_block(self, node: Element) -> None:
2468
+ def visit_math_block(self, node: nodes.math_block) -> None:
2454
2469
  if node.get('label'):
2455
- label = f"equation:{node['docname']}:{node['label']}"
2470
+ label = f'equation:{node["docname"]}:{node["label"]}'
2456
2471
  else:
2457
2472
  label = None
2458
2473
 
2459
- if node.get('nowrap'):
2474
+ if node.get('no-wrap', node.get('nowrap', False)):
2460
2475
  if label:
2461
2476
  self.body.append(r'\label{%s}' % label)
2462
2477
  self.body.append(node.astext())
@@ -2469,7 +2484,7 @@ class LaTeXTranslator(SphinxTranslator):
2469
2484
  raise nodes.SkipNode
2470
2485
 
2471
2486
  def visit_math_reference(self, node: Element) -> None:
2472
- label = f"equation:{node['docname']}:{node['target']}"
2487
+ label = f'equation:{node["docname"]}:{node["target"]}'
2473
2488
  eqref_format = self.config.math_eqref_format
2474
2489
  if eqref_format:
2475
2490
  try:
@@ -2486,7 +2501,7 @@ class LaTeXTranslator(SphinxTranslator):
2486
2501
 
2487
2502
 
2488
2503
  # FIXME: Workaround to avoid circular import
2489
- # refs: https://github.com/sphinx-doc/sphinx/issues/5433
2504
+ # See: https://github.com/sphinx-doc/sphinx/issues/5433
2490
2505
  from sphinx.builders.latex.nodes import ( # NoQA: E402 # isort:skip
2491
2506
  HYPERLINK_SUPPORT_NODES,
2492
2507
  captioned_literal_block,