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/util/nodes.py CHANGED
@@ -95,12 +95,11 @@ class NodeMatcher(Generic[N]):
95
95
  confounds type checkers' ability to determine the return type of the iterator.
96
96
  """
97
97
  for found in node.findall(self):
98
- yield cast(N, found)
98
+ yield cast('N', found)
99
99
 
100
100
 
101
101
  def get_full_module_name(node: Node) -> str:
102
- """
103
- Return full module dotted path like: 'docutils.nodes.paragraph'
102
+ """Return full module dotted path like: 'docutils.nodes.paragraph'
104
103
 
105
104
  :param nodes.Node node: target node
106
105
  :return: full module dotted path
@@ -109,8 +108,7 @@ def get_full_module_name(node: Node) -> str:
109
108
 
110
109
 
111
110
  def repr_domxml(node: Node, length: int = 80) -> str:
112
- """
113
- return DOM XML representation of the specified node like:
111
+ """Return DOM XML representation of the specified node like:
114
112
  '<paragraph translatable="False"><inline classes="versionadded">Added in version...'
115
113
 
116
114
  :param nodes.Node node: target node
@@ -181,7 +179,8 @@ def apply_source_workaround(node: Element) -> None:
181
179
  )
182
180
  node.source, node.line = node.parent.source, node.parent.line
183
181
 
184
- # workaround: literal_block under bullet list (#4913)
182
+ # workaround: literal_block under bullet list
183
+ # See: https://github.com/sphinx-doc/sphinx/issues/4913
185
184
  if isinstance(node, nodes.literal_block) and node.source is None:
186
185
  with contextlib.suppress(ValueError):
187
186
  node.source = get_node_source(node)
@@ -197,10 +196,14 @@ def apply_source_workaround(node: Element) -> None:
197
196
  if isinstance(
198
197
  node,
199
198
  (
200
- nodes.rubric # #1305 rubric directive
201
- | nodes.line # #1477 line node
202
- | nodes.image # #3093 image directive in substitution
203
- | nodes.field_name # #3335 field list syntax
199
+ # https://github.com/sphinx-doc/sphinx/issues/1305 rubric directive
200
+ nodes.rubric
201
+ # https://github.com/sphinx-doc/sphinx/issues/1477 line node
202
+ | nodes.line
203
+ # https://github.com/sphinx-doc/sphinx/issues/3093 image directive in substitution
204
+ | nodes.image
205
+ # https://github.com/sphinx-doc/sphinx/issues/3335 field list syntax
206
+ | nodes.field_name
204
207
  ),
205
208
  ):
206
209
  logger.debug(
@@ -471,7 +474,7 @@ def inline_all_toctrees(
471
474
  if includefile not in traversed:
472
475
  try:
473
476
  traversed.append(includefile)
474
- logger.info(indent + colorfunc(includefile))
477
+ logger.info(indent + colorfunc(includefile)) # NoQA: G003
475
478
  subtree = inline_all_toctrees(
476
479
  builder,
477
480
  docnameset,
@@ -487,6 +490,8 @@ def inline_all_toctrees(
487
490
  __('toctree contains ref to nonexisting file %r'),
488
491
  includefile,
489
492
  location=docname,
493
+ type='toc',
494
+ subtype='not_readable',
490
495
  )
491
496
  else:
492
497
  sof = addnodes.start_of_file(docname=includefile)
@@ -593,7 +598,7 @@ def make_id(
593
598
  node_id = None
594
599
  elif term:
595
600
  node_id = _make_id(term)
596
- if node_id == '':
601
+ if not node_id:
597
602
  node_id = None # fallback to None
598
603
 
599
604
  while node_id is None or node_id in document.ids:
@@ -709,7 +714,7 @@ def _copy_except__document(el: Element) -> Element:
709
714
  """Monkey-patch ```nodes.Element.copy``` to not copy the ``_document``
710
715
  attribute.
711
716
 
712
- xref: https://github.com/sphinx-doc/sphinx/issues/11116#issuecomment-1376767086
717
+ See: https://github.com/sphinx-doc/sphinx/issues/11116#issuecomment-1376767086
713
718
  """
714
719
  newnode = object.__new__(el.__class__)
715
720
  # set in Element.__init__()
sphinx/util/osutil.py CHANGED
@@ -5,12 +5,12 @@ from __future__ import annotations
5
5
  import contextlib
6
6
  import filecmp
7
7
  import os
8
+ import os.path
8
9
  import re
9
10
  import shutil
10
11
  import sys
11
12
  import unicodedata
12
13
  from io import StringIO
13
- from os import path
14
14
  from pathlib import Path
15
15
  from typing import TYPE_CHECKING
16
16
 
@@ -18,7 +18,7 @@ from sphinx.locale import __
18
18
 
19
19
  if TYPE_CHECKING:
20
20
  from types import TracebackType
21
- from typing import Any
21
+ from typing import Any, Self
22
22
 
23
23
  # SEP separates path elements in the canonical file names
24
24
  #
@@ -29,12 +29,12 @@ SEP = '/'
29
29
 
30
30
 
31
31
  def os_path(canonical_path: str, /) -> str:
32
- return canonical_path.replace(SEP, path.sep)
32
+ return canonical_path.replace(SEP, os.path.sep)
33
33
 
34
34
 
35
35
  def canon_path(native_path: str | os.PathLike[str], /) -> str:
36
36
  """Return path in OS-independent form"""
37
- return os.fspath(native_path).replace(path.sep, SEP)
37
+ return os.fspath(native_path).replace(os.path.sep, SEP)
38
38
 
39
39
 
40
40
  def path_stabilize(filepath: str | os.PathLike[str], /) -> str:
@@ -134,10 +134,14 @@ def copyfile(
134
134
  logger.warning(msg, source, dest, type='misc', subtype='copy_overwrite')
135
135
  return
136
136
 
137
- shutil.copyfile(source, dest)
138
- with contextlib.suppress(OSError):
139
- # don't do full copystat because the source may be read-only
140
- _copy_times(source, dest)
137
+ if sys.platform == 'win32':
138
+ # copy2() uses Windows API calls
139
+ shutil.copy2(source, dest)
140
+ else:
141
+ shutil.copyfile(source, dest)
142
+ with contextlib.suppress(OSError):
143
+ # don't do full copystat because the source may be read-only
144
+ _copy_times(source, dest)
141
145
 
142
146
 
143
147
  _no_fn_re = re.compile(r'[^a-zA-Z0-9_-]')
@@ -166,36 +170,27 @@ def relpath(
166
170
  return str(path)
167
171
 
168
172
 
169
- safe_relpath = relpath # for compatibility
170
- fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
171
-
172
-
173
- abspath = path.abspath
173
+ def _relative_path(path: Path, root: Path, /) -> Path:
174
+ """Return a relative filepath to *path* from the given *root* directory.
174
175
 
176
+ This is an alternative of ``Path.relative_to``.
177
+ It returns the original path if *path* and *root* are on different drives,
178
+ which may happen on Windows.
179
+ """
180
+ if path.anchor != root.anchor or '..' in root.parts:
181
+ # If the drives are different, no relative path exists.
182
+ # Path.relative_to() requires fully-resolved paths (no '..').
183
+ return path
184
+ if sys.version_info[:2] < (3, 12):
185
+ return Path(os.path.relpath(path, root))
186
+ return path.relative_to(root, walk_up=True)
175
187
 
176
- class _chdir:
177
- """Remove this fall-back once support for Python 3.10 is removed."""
178
-
179
- def __init__(self, target_dir: str, /) -> None:
180
- self.path = target_dir
181
- self._dirs: list[str] = []
182
-
183
- def __enter__(self) -> None:
184
- self._dirs.append(os.getcwd())
185
- os.chdir(self.path)
186
188
 
187
- def __exit__(
188
- self,
189
- type: type[BaseException] | None,
190
- value: BaseException | None,
191
- traceback: TracebackType | None,
192
- /,
193
- ) -> None:
194
- os.chdir(self._dirs.pop())
189
+ safe_relpath = relpath # for compatibility
190
+ fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
195
191
 
196
192
 
197
- if sys.version_info[:2] < (3, 11):
198
- cd = _chdir
193
+ abspath = os.path.abspath
199
194
 
200
195
 
201
196
  class FileAvoidWrite:
@@ -232,19 +227,22 @@ class FileAvoidWrite:
232
227
  try:
233
228
  with open(self._path, encoding='utf-8') as old_f:
234
229
  old_content = old_f.read()
235
- if old_content == buf:
236
- return
230
+ if old_content == buf:
231
+ return
237
232
  except OSError:
238
233
  pass
239
234
 
240
235
  with open(self._path, 'w', encoding='utf-8') as f:
241
236
  f.write(buf)
242
237
 
243
- def __enter__(self) -> FileAvoidWrite:
238
+ def __enter__(self) -> Self:
244
239
  return self
245
240
 
246
241
  def __exit__(
247
- self, exc_type: type[Exception], exc_value: Exception, traceback: Any
242
+ self,
243
+ exc_type: type[BaseException] | None,
244
+ exc_value: BaseException | None,
245
+ traceback: TracebackType | None,
248
246
  ) -> bool:
249
247
  self.close()
250
248
  return True
@@ -258,8 +256,9 @@ class FileAvoidWrite:
258
256
  return getattr(self._io, name)
259
257
 
260
258
 
261
- def rmtree(path: str) -> None:
262
- if os.path.isdir(path):
259
+ def rmtree(path: str | os.PathLike[str], /) -> None:
260
+ path = Path(path)
261
+ if path.is_dir():
263
262
  shutil.rmtree(path)
264
263
  else:
265
264
  os.remove(path)
sphinx/util/parallel.py CHANGED
@@ -6,7 +6,7 @@ import os
6
6
  import time
7
7
  import traceback
8
8
  from math import sqrt
9
- from typing import TYPE_CHECKING, Any
9
+ from typing import TYPE_CHECKING
10
10
 
11
11
  try:
12
12
  import multiprocessing
@@ -20,6 +20,7 @@ from sphinx.util import logging
20
20
 
21
21
  if TYPE_CHECKING:
22
22
  from collections.abc import Callable, Sequence
23
+ from typing import Any
23
24
 
24
25
  logger = logging.getLogger(__name__)
25
26
 
@@ -34,12 +35,15 @@ class SerialTasks:
34
35
  pass
35
36
 
36
37
  def add_task(
37
- self, task_func: Callable, arg: Any = None, result_func: Callable | None = None
38
+ self,
39
+ task_func: Callable[[Any], Any] | Callable[[], Any],
40
+ arg: Any = None,
41
+ result_func: Callable[[Any], Any] | None = None,
38
42
  ) -> None:
39
43
  if arg is not None:
40
- res = task_func(arg)
44
+ res = task_func(arg) # type: ignore[call-arg]
41
45
  else:
42
- res = task_func()
46
+ res = task_func() # type: ignore[call-arg]
43
47
  if result_func:
44
48
  result_func(res)
45
49
 
@@ -53,7 +57,7 @@ class ParallelTasks:
53
57
  def __init__(self, nproc: int) -> None:
54
58
  self.nproc = nproc
55
59
  # (optional) function performed by each task on the result of main task
56
- self._result_funcs: dict[int, Callable] = {}
60
+ self._result_funcs: dict[int, Callable[[Any, Any], Any]] = {}
57
61
  # task arguments
58
62
  self._args: dict[int, list[Any] | None] = {}
59
63
  # list of subprocesses (both started and waiting)
@@ -61,20 +65,22 @@ class ParallelTasks:
61
65
  # list of receiving pipe connections of running subprocesses
62
66
  self._precvs: dict[int, Any] = {}
63
67
  # list of receiving pipe connections of waiting subprocesses
64
- self._precvsWaiting: dict[int, Any] = {}
68
+ self._precvs_waiting: dict[int, Any] = {}
65
69
  # number of working subprocesses
66
70
  self._pworking = 0
67
71
  # task number of each subprocess
68
72
  self._taskid = 0
69
73
 
70
- def _process(self, pipe: Any, func: Callable, arg: Any) -> None:
74
+ def _process(
75
+ self, pipe: Any, func: Callable[[Any], Any] | Callable[[], Any], arg: Any
76
+ ) -> None:
71
77
  try:
72
78
  collector = logging.LogCollector()
73
79
  with collector.collect():
74
80
  if arg is None:
75
- ret = func()
81
+ ret = func() # type: ignore[call-arg]
76
82
  else:
77
- ret = func(arg)
83
+ ret = func(arg) # type: ignore[call-arg]
78
84
  failed = False
79
85
  except BaseException as err:
80
86
  failed = True
@@ -84,7 +90,10 @@ class ParallelTasks:
84
90
  pipe.send((failed, collector.logs, ret))
85
91
 
86
92
  def add_task(
87
- self, task_func: Callable, arg: Any = None, result_func: Callable | None = None
93
+ self,
94
+ task_func: Callable[[Any], Any] | Callable[[], Any],
95
+ arg: Any = None,
96
+ result_func: Callable[[Any, Any], Any] | None = None,
88
97
  ) -> None:
89
98
  tid = self._taskid
90
99
  self._taskid += 1
@@ -94,7 +103,7 @@ class ParallelTasks:
94
103
  context: Any = multiprocessing.get_context('fork')
95
104
  proc = context.Process(target=self._process, args=(psend, task_func, arg))
96
105
  self._procs[tid] = proc
97
- self._precvsWaiting[tid] = precv
106
+ self._precvs_waiting[tid] = precv
98
107
  try:
99
108
  self._join_one()
100
109
  except Exception:
@@ -135,8 +144,8 @@ class ParallelTasks:
135
144
  joined_any = True
136
145
  break
137
146
 
138
- while self._precvsWaiting and self._pworking < self.nproc:
139
- newtid, newprecv = self._precvsWaiting.popitem()
147
+ while self._precvs_waiting and self._pworking < self.nproc:
148
+ newtid, newprecv = self._precvs_waiting.popitem()
140
149
  self._precvs[newtid] = newprecv
141
150
  self._procs[newtid].start()
142
151
  self._pworking += 1
sphinx/util/parsing.py CHANGED
@@ -5,12 +5,13 @@ from __future__ import annotations
5
5
  import contextlib
6
6
  from typing import TYPE_CHECKING
7
7
 
8
- from docutils.nodes import Element, Node
8
+ from docutils.nodes import Element
9
9
  from docutils.statemachine import StringList, string2lines
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from collections.abc import Iterator
13
13
 
14
+ from docutils.nodes import Node
14
15
  from docutils.parsers.rst.states import RSTState
15
16
 
16
17
 
sphinx/util/png.py CHANGED
@@ -4,6 +4,10 @@ from __future__ import annotations
4
4
 
5
5
  import binascii
6
6
  import struct
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ import os
7
11
 
8
12
  LEN_IEND = 12
9
13
  LEN_DEPTH = 22
@@ -13,7 +17,7 @@ DEPTH_CHUNK_START = b'tEXtDepth\x00'
13
17
  IEND_CHUNK = b'\x00\x00\x00\x00IEND\xae\x42\x60\x82'
14
18
 
15
19
 
16
- def read_png_depth(filename: str) -> int | None:
20
+ def read_png_depth(filename: str | os.PathLike[str]) -> int | None:
17
21
  """Read the special tEXt chunk indicating the depth from a PNG file."""
18
22
  with open(filename, 'rb') as f:
19
23
  f.seek(-(LEN_IEND + LEN_DEPTH), 2)
@@ -25,7 +29,7 @@ def read_png_depth(filename: str) -> int | None:
25
29
  return struct.unpack('!i', depthchunk[14:18])[0]
26
30
 
27
31
 
28
- def write_png_depth(filename: str, depth: int) -> None:
32
+ def write_png_depth(filename: str | os.PathLike[str], depth: int) -> None:
29
33
  """Write the special tEXt chunk indicating the depth to a PNG file.
30
34
 
31
35
  The chunk is placed immediately before the special IEND chunk.
sphinx/util/requests.py CHANGED
@@ -3,20 +3,34 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import warnings
6
- from typing import Any
7
- from urllib.parse import urlsplit
6
+ from typing import TYPE_CHECKING
7
+ from urllib.parse import urljoin, urlsplit
8
8
 
9
9
  import requests
10
10
  from urllib3.exceptions import InsecureRequestWarning
11
11
 
12
12
  import sphinx
13
13
 
14
+ if TYPE_CHECKING:
15
+ import re
16
+ from collections.abc import Sequence
17
+ from typing import Any
18
+
19
+
14
20
  _USER_AGENT = (
15
21
  f'Mozilla/5.0 (X11; Linux x86_64; rv:100.0) Gecko/20100101 Firefox/100.0 '
16
22
  f'Sphinx/{sphinx.__version__}'
17
23
  )
18
24
 
19
25
 
26
+ class _IgnoredRedirection(Exception):
27
+ """Sphinx-internal exception raised when an HTTP redirect is ignored"""
28
+
29
+ def __init__(self, destination: str, status_code: int) -> None:
30
+ self.destination = destination
31
+ self.status_code = status_code
32
+
33
+
20
34
  def _get_tls_cacert(url: str, certs: str | dict[str, str] | None) -> str | bool:
21
35
  """Get additional CA cert for a specific URL."""
22
36
  if not certs:
@@ -50,6 +64,23 @@ def head(url: str, **kwargs: Any) -> requests.Response:
50
64
 
51
65
 
52
66
  class _Session(requests.Session):
67
+ _ignored_redirects: Sequence[re.Pattern[str]]
68
+
69
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
70
+ self._ignored_redirects = kwargs.pop('_ignored_redirects', ())
71
+ super().__init__(*args, **kwargs)
72
+
73
+ def get_redirect_target(self, resp: requests.Response) -> str | None:
74
+ """Overrides the default requests.Session.get_redirect_target"""
75
+ # do not follow redirections that match ignored URI patterns
76
+ if resp.is_redirect:
77
+ destination = urljoin(resp.url, resp.headers['location'])
78
+ if any(pat.match(destination) for pat in self._ignored_redirects):
79
+ raise _IgnoredRedirection(
80
+ destination=destination, status_code=resp.status_code
81
+ )
82
+ return super().get_redirect_target(resp)
83
+
53
84
  def request( # type: ignore[override]
54
85
  self,
55
86
  method: str,
sphinx/util/rst.py CHANGED
@@ -12,7 +12,7 @@ from docutils.parsers.rst import roles
12
12
  from docutils.parsers.rst.languages import en as english # type: ignore[attr-defined]
13
13
  from docutils.parsers.rst.states import Body
14
14
  from docutils.utils import Reporter
15
- from jinja2 import Environment, pass_environment
15
+ from jinja2 import pass_environment
16
16
 
17
17
  from sphinx.locale import __
18
18
  from sphinx.util import docutils, logging
@@ -21,6 +21,7 @@ if TYPE_CHECKING:
21
21
  from collections.abc import Iterator
22
22
 
23
23
  from docutils.statemachine import StringList
24
+ from jinja2 import Environment
24
25
 
25
26
  logger = logging.getLogger(__name__)
26
27
 
@@ -105,7 +106,7 @@ def append_epilog(content: StringList, epilog: str) -> None:
105
106
  if len(content) > 0:
106
107
  source, lineno = content.info(-1)
107
108
  # lineno will never be None, since len(content) > 0
108
- lineno = cast(int, lineno)
109
+ lineno = cast('int', lineno)
109
110
  else:
110
111
  source = '<generated>'
111
112
  lineno = 0
sphinx/util/tags.py CHANGED
@@ -112,4 +112,4 @@ class Tags:
112
112
  return node.name in self._tags
113
113
  else:
114
114
  msg = 'invalid node, check parsing'
115
- raise ValueError(msg)
115
+ raise TypeError(msg)
sphinx/util/template.py CHANGED
@@ -4,8 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  import os
6
6
  from functools import partial
7
- from os import path
8
- from typing import TYPE_CHECKING, Any
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
9
 
10
10
  from jinja2 import TemplateNotFound
11
11
  from jinja2.loaders import BaseLoader
@@ -18,9 +18,13 @@ from sphinx.util import rst, texescape
18
18
 
19
19
  if TYPE_CHECKING:
20
20
  from collections.abc import Callable, Sequence
21
+ from typing import Any
21
22
 
22
23
  from jinja2.environment import Environment
23
24
 
25
+ _TEMPLATES_PATH = package_dir / 'templates'
26
+ _LATEX_TEMPLATES_PATH = _TEMPLATES_PATH / 'latex'
27
+
24
28
 
25
29
  class BaseRenderer:
26
30
  def __init__(self, loader: BaseLoader | None = None) -> None:
@@ -49,11 +53,12 @@ class FileRenderer(BaseRenderer):
49
53
 
50
54
  @classmethod
51
55
  def render_from_file(
52
- cls: type[FileRenderer], filename: str, context: dict[str, Any]
56
+ cls: type[FileRenderer],
57
+ filename: str | os.PathLike[str],
58
+ context: dict[str, Any],
53
59
  ) -> str:
54
- dirname = os.path.dirname(filename)
55
- basename = os.path.basename(filename)
56
- return cls(dirname).render(basename, context)
60
+ filename = Path(filename)
61
+ return cls((filename.parent,)).render(filename.name, context)
57
62
 
58
63
 
59
64
  class SphinxRenderer(FileRenderer):
@@ -61,12 +66,14 @@ class SphinxRenderer(FileRenderer):
61
66
  self, template_path: Sequence[str | os.PathLike[str]] | None = None
62
67
  ) -> None:
63
68
  if template_path is None:
64
- template_path = os.path.join(package_dir, 'templates')
69
+ template_path = (_TEMPLATES_PATH,)
65
70
  super().__init__(template_path)
66
71
 
67
72
  @classmethod
68
73
  def render_from_file(
69
- cls: type[FileRenderer], filename: str, context: dict[str, Any]
74
+ cls: type[FileRenderer],
75
+ filename: str | os.PathLike[str],
76
+ context: dict[str, Any],
70
77
  ) -> str:
71
78
  return FileRenderer.render_from_file(filename, context)
72
79
 
@@ -78,7 +85,7 @@ class LaTeXRenderer(SphinxRenderer):
78
85
  latex_engine: str | None = None,
79
86
  ) -> None:
80
87
  if template_path is None:
81
- template_path = [os.path.join(package_dir, 'templates', 'latex')]
88
+ template_path = (_LATEX_TEMPLATES_PATH,)
82
89
  super().__init__(template_path)
83
90
 
84
91
  # use texescape as escape filter
@@ -126,8 +133,9 @@ class SphinxTemplateLoader(BaseLoader):
126
133
  self.loaders = []
127
134
  self.sysloaders = []
128
135
 
136
+ conf_dir = Path(confdir)
129
137
  for templates_path in templates_paths:
130
- loader = SphinxFileSystemLoader(path.join(confdir, templates_path))
138
+ loader = SphinxFileSystemLoader(conf_dir / templates_path)
131
139
  self.loaders.append(loader)
132
140
 
133
141
  for templates_path in system_templates_paths:
sphinx/util/texescape.py CHANGED
@@ -29,14 +29,16 @@ tex_replacements = [
29
29
  # map some special Unicode characters to similar ASCII ones
30
30
  # (even for Unicode LaTeX as may not be supported by OpenType font)
31
31
  ('⎽', r'\_'),
32
- ('ℯ', r'e'),
33
- ('ⅈ', r'i'),
32
+ ('ℯ', r'e'), # U+212F # NoQA: RUF001
33
+ ('ⅈ', r'i'), # U+2148 # NoQA: RUF001
34
34
  # Greek alphabet not escaped: pdflatex handles it via textalpha and inputenc
35
35
  # OHM SIGN U+2126 is handled by LaTeX textcomp package
36
36
  ]
37
37
 
38
38
  # A map to avoid TeX ligatures or character replacements in PDF output
39
- # xelatex/lualatex/uplatex are handled differently (#5790, #6888)
39
+ # xelatex/lualatex/uplatex are handled differently
40
+ # https://github.com/sphinx-doc/sphinx/pull/5790
41
+ # https://github.com/sphinx-doc/sphinx/pull/6888
40
42
  ascii_tex_replacements = [
41
43
  # Note: the " renders curly in OT1 encoding but straight in T1, T2A, LY1...
42
44
  # escaping it to \textquotedbl would break documents using OT1
@@ -63,7 +65,7 @@ unicode_tex_replacements = [
63
65
  ('±', r'\(\pm\)'),
64
66
  ('→', r'\(\rightarrow\)'),
65
67
  ('‣', r'\(\rightarrow\)'),
66
- ('', r'\textendash{}'),
68
+ ('\N{EN DASH}', r'\textendash{}'),
67
69
  # superscript
68
70
  ('⁰', r'\(\sp{\text{0}}\)'),
69
71
  ('¹', r'\(\sp{\text{1}}\)'),
@@ -103,7 +105,7 @@ _tex_hlescape_map_without_unicode: dict[int, str] = {}
103
105
 
104
106
  def escape(s: str, latex_engine: str | None = None) -> str:
105
107
  """Escape text for LaTeX output."""
106
- if latex_engine in ('lualatex', 'xelatex'):
108
+ if latex_engine in {'lualatex', 'xelatex'}:
107
109
  # unicode based LaTeX engine
108
110
  return s.translate(_tex_escape_map_without_unicode)
109
111
  else:
@@ -112,7 +114,7 @@ def escape(s: str, latex_engine: str | None = None) -> str:
112
114
 
113
115
  def hlescape(s: str, latex_engine: str | None = None) -> str:
114
116
  """Escape text for LaTeX highlighter."""
115
- if latex_engine in ('lualatex', 'xelatex'):
117
+ if latex_engine in {'lualatex', 'xelatex'}:
116
118
  # unicode based LaTeX engine
117
119
  return s.translate(_tex_hlescape_map_without_unicode)
118
120
  else: