Sphinx 8.1.2__py3-none-any.whl → 8.2.0__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 (328) 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 +50 -40
  55. sphinx/domains/python/__init__.py +402 -211
  56. sphinx/domains/python/_annotations.py +134 -61
  57. sphinx/domains/python/_object.py +155 -68
  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 +66 -0
  73. sphinx/ext/apidoc/__main__.py +9 -0
  74. sphinx/ext/apidoc/_cli.py +356 -0
  75. sphinx/ext/apidoc/_extension.py +262 -0
  76. sphinx/ext/apidoc/_generate.py +356 -0
  77. sphinx/ext/apidoc/_shared.py +99 -0
  78. sphinx/ext/autodoc/__init__.py +837 -483
  79. sphinx/ext/autodoc/directive.py +57 -21
  80. sphinx/ext/autodoc/importer.py +184 -67
  81. sphinx/ext/autodoc/mock.py +25 -10
  82. sphinx/ext/autodoc/preserve_defaults.py +17 -9
  83. sphinx/ext/autodoc/type_comment.py +56 -29
  84. sphinx/ext/autodoc/typehints.py +49 -26
  85. sphinx/ext/autosectionlabel.py +28 -11
  86. sphinx/ext/autosummary/__init__.py +281 -142
  87. sphinx/ext/autosummary/generate.py +121 -51
  88. sphinx/ext/coverage.py +152 -91
  89. sphinx/ext/doctest.py +169 -101
  90. sphinx/ext/duration.py +12 -6
  91. sphinx/ext/extlinks.py +33 -21
  92. sphinx/ext/githubpages.py +8 -8
  93. sphinx/ext/graphviz.py +175 -109
  94. sphinx/ext/ifconfig.py +11 -6
  95. sphinx/ext/imgconverter.py +48 -25
  96. sphinx/ext/imgmath.py +127 -97
  97. sphinx/ext/inheritance_diagram.py +177 -103
  98. sphinx/ext/intersphinx/__init__.py +22 -13
  99. sphinx/ext/intersphinx/__main__.py +3 -1
  100. sphinx/ext/intersphinx/_cli.py +18 -14
  101. sphinx/ext/intersphinx/_load.py +91 -82
  102. sphinx/ext/intersphinx/_resolve.py +108 -74
  103. sphinx/ext/intersphinx/_shared.py +2 -2
  104. sphinx/ext/linkcode.py +28 -12
  105. sphinx/ext/mathjax.py +60 -29
  106. sphinx/ext/napoleon/__init__.py +19 -7
  107. sphinx/ext/napoleon/docstring.py +229 -231
  108. sphinx/ext/todo.py +44 -49
  109. sphinx/ext/viewcode.py +105 -57
  110. sphinx/extension.py +3 -1
  111. sphinx/highlighting.py +13 -7
  112. sphinx/io.py +9 -13
  113. sphinx/jinja2glue.py +29 -26
  114. sphinx/locale/__init__.py +8 -9
  115. sphinx/locale/ar/LC_MESSAGES/sphinx.mo +0 -0
  116. sphinx/locale/ar/LC_MESSAGES/sphinx.po +2155 -2050
  117. sphinx/locale/bg/LC_MESSAGES/sphinx.mo +0 -0
  118. sphinx/locale/bg/LC_MESSAGES/sphinx.po +2045 -1940
  119. sphinx/locale/bn/LC_MESSAGES/sphinx.mo +0 -0
  120. sphinx/locale/bn/LC_MESSAGES/sphinx.po +2175 -2070
  121. sphinx/locale/ca/LC_MESSAGES/sphinx.js +3 -3
  122. sphinx/locale/ca/LC_MESSAGES/sphinx.mo +0 -0
  123. sphinx/locale/ca/LC_MESSAGES/sphinx.po +2690 -2585
  124. sphinx/locale/ca@valencia/LC_MESSAGES/sphinx.js +63 -0
  125. sphinx/locale/ca@valencia/LC_MESSAGES/sphinx.mo +0 -0
  126. sphinx/locale/ca@valencia/LC_MESSAGES/sphinx.po +4216 -0
  127. sphinx/locale/cak/LC_MESSAGES/sphinx.mo +0 -0
  128. sphinx/locale/cak/LC_MESSAGES/sphinx.po +2096 -1991
  129. sphinx/locale/cs/LC_MESSAGES/sphinx.mo +0 -0
  130. sphinx/locale/cs/LC_MESSAGES/sphinx.po +2248 -2143
  131. sphinx/locale/cy/LC_MESSAGES/sphinx.mo +0 -0
  132. sphinx/locale/cy/LC_MESSAGES/sphinx.po +2201 -2096
  133. sphinx/locale/da/LC_MESSAGES/sphinx.mo +0 -0
  134. sphinx/locale/da/LC_MESSAGES/sphinx.po +2282 -2177
  135. sphinx/locale/de/LC_MESSAGES/sphinx.mo +0 -0
  136. sphinx/locale/de/LC_MESSAGES/sphinx.po +2261 -2156
  137. sphinx/locale/de_DE/LC_MESSAGES/sphinx.mo +0 -0
  138. sphinx/locale/de_DE/LC_MESSAGES/sphinx.po +2045 -1940
  139. sphinx/locale/el/LC_MESSAGES/sphinx.mo +0 -0
  140. sphinx/locale/el/LC_MESSAGES/sphinx.po +2604 -2499
  141. sphinx/locale/en_DE/LC_MESSAGES/sphinx.mo +0 -0
  142. sphinx/locale/en_DE/LC_MESSAGES/sphinx.po +2045 -1940
  143. sphinx/locale/en_FR/LC_MESSAGES/sphinx.mo +0 -0
  144. sphinx/locale/en_FR/LC_MESSAGES/sphinx.po +2045 -1940
  145. sphinx/locale/en_GB/LC_MESSAGES/sphinx.mo +0 -0
  146. sphinx/locale/en_GB/LC_MESSAGES/sphinx.po +2631 -2526
  147. sphinx/locale/en_HK/LC_MESSAGES/sphinx.mo +0 -0
  148. sphinx/locale/en_HK/LC_MESSAGES/sphinx.po +2045 -1940
  149. sphinx/locale/eo/LC_MESSAGES/sphinx.mo +0 -0
  150. sphinx/locale/eo/LC_MESSAGES/sphinx.po +2078 -1973
  151. sphinx/locale/es/LC_MESSAGES/sphinx.mo +0 -0
  152. sphinx/locale/es/LC_MESSAGES/sphinx.po +2633 -2528
  153. sphinx/locale/es_CO/LC_MESSAGES/sphinx.mo +0 -0
  154. sphinx/locale/es_CO/LC_MESSAGES/sphinx.po +2045 -1940
  155. sphinx/locale/et/LC_MESSAGES/sphinx.mo +0 -0
  156. sphinx/locale/et/LC_MESSAGES/sphinx.po +2449 -2344
  157. sphinx/locale/eu/LC_MESSAGES/sphinx.mo +0 -0
  158. sphinx/locale/eu/LC_MESSAGES/sphinx.po +2241 -2136
  159. sphinx/locale/fa/LC_MESSAGES/sphinx.mo +0 -0
  160. sphinx/locale/fa/LC_MESSAGES/sphinx.po +504 -500
  161. sphinx/locale/fi/LC_MESSAGES/sphinx.mo +0 -0
  162. sphinx/locale/fi/LC_MESSAGES/sphinx.po +499 -495
  163. sphinx/locale/fr/LC_MESSAGES/sphinx.mo +0 -0
  164. sphinx/locale/fr/LC_MESSAGES/sphinx.po +513 -509
  165. sphinx/locale/fr_FR/LC_MESSAGES/sphinx.mo +0 -0
  166. sphinx/locale/fr_FR/LC_MESSAGES/sphinx.po +499 -495
  167. sphinx/locale/gl/LC_MESSAGES/sphinx.mo +0 -0
  168. sphinx/locale/gl/LC_MESSAGES/sphinx.po +2644 -2539
  169. sphinx/locale/he/LC_MESSAGES/sphinx.mo +0 -0
  170. sphinx/locale/he/LC_MESSAGES/sphinx.po +499 -495
  171. sphinx/locale/hi/LC_MESSAGES/sphinx.mo +0 -0
  172. sphinx/locale/hi/LC_MESSAGES/sphinx.po +504 -500
  173. sphinx/locale/hi_IN/LC_MESSAGES/sphinx.mo +0 -0
  174. sphinx/locale/hi_IN/LC_MESSAGES/sphinx.po +499 -495
  175. sphinx/locale/hr/LC_MESSAGES/sphinx.mo +0 -0
  176. sphinx/locale/hr/LC_MESSAGES/sphinx.po +501 -497
  177. sphinx/locale/hu/LC_MESSAGES/sphinx.mo +0 -0
  178. sphinx/locale/hu/LC_MESSAGES/sphinx.po +499 -495
  179. sphinx/locale/id/LC_MESSAGES/sphinx.mo +0 -0
  180. sphinx/locale/id/LC_MESSAGES/sphinx.po +2609 -2504
  181. sphinx/locale/is/LC_MESSAGES/sphinx.mo +0 -0
  182. sphinx/locale/is/LC_MESSAGES/sphinx.po +499 -495
  183. sphinx/locale/it/LC_MESSAGES/sphinx.mo +0 -0
  184. sphinx/locale/it/LC_MESSAGES/sphinx.po +2265 -2160
  185. sphinx/locale/ja/LC_MESSAGES/sphinx.mo +0 -0
  186. sphinx/locale/ja/LC_MESSAGES/sphinx.po +2621 -2516
  187. sphinx/locale/ka/LC_MESSAGES/sphinx.mo +0 -0
  188. sphinx/locale/ka/LC_MESSAGES/sphinx.po +2567 -2462
  189. sphinx/locale/ko/LC_MESSAGES/sphinx.mo +0 -0
  190. sphinx/locale/ko/LC_MESSAGES/sphinx.po +2631 -2526
  191. sphinx/locale/lt/LC_MESSAGES/sphinx.mo +0 -0
  192. sphinx/locale/lt/LC_MESSAGES/sphinx.po +2214 -2109
  193. sphinx/locale/lv/LC_MESSAGES/sphinx.mo +0 -0
  194. sphinx/locale/lv/LC_MESSAGES/sphinx.po +2218 -2113
  195. sphinx/locale/mk/LC_MESSAGES/sphinx.mo +0 -0
  196. sphinx/locale/mk/LC_MESSAGES/sphinx.po +2088 -1983
  197. sphinx/locale/nb_NO/LC_MESSAGES/sphinx.mo +0 -0
  198. sphinx/locale/nb_NO/LC_MESSAGES/sphinx.po +2247 -2142
  199. sphinx/locale/ne/LC_MESSAGES/sphinx.mo +0 -0
  200. sphinx/locale/ne/LC_MESSAGES/sphinx.po +2227 -2122
  201. sphinx/locale/nl/LC_MESSAGES/sphinx.mo +0 -0
  202. sphinx/locale/nl/LC_MESSAGES/sphinx.po +2316 -2211
  203. sphinx/locale/pl/LC_MESSAGES/sphinx.js +2 -2
  204. sphinx/locale/pl/LC_MESSAGES/sphinx.mo +0 -0
  205. sphinx/locale/pl/LC_MESSAGES/sphinx.po +2442 -2336
  206. sphinx/locale/pt/LC_MESSAGES/sphinx.mo +0 -0
  207. sphinx/locale/pt/LC_MESSAGES/sphinx.po +2045 -1940
  208. sphinx/locale/pt_BR/LC_MESSAGES/sphinx.mo +0 -0
  209. sphinx/locale/pt_BR/LC_MESSAGES/sphinx.po +2657 -2552
  210. sphinx/locale/pt_PT/LC_MESSAGES/sphinx.mo +0 -0
  211. sphinx/locale/pt_PT/LC_MESSAGES/sphinx.po +2243 -2138
  212. sphinx/locale/ro/LC_MESSAGES/sphinx.mo +0 -0
  213. sphinx/locale/ro/LC_MESSAGES/sphinx.po +2244 -2139
  214. sphinx/locale/ru/LC_MESSAGES/sphinx.js +1 -1
  215. sphinx/locale/ru/LC_MESSAGES/sphinx.mo +0 -0
  216. sphinx/locale/ru/LC_MESSAGES/sphinx.po +2660 -2555
  217. sphinx/locale/si/LC_MESSAGES/sphinx.mo +0 -0
  218. sphinx/locale/si/LC_MESSAGES/sphinx.po +2134 -2029
  219. sphinx/locale/sk/LC_MESSAGES/sphinx.mo +0 -0
  220. sphinx/locale/sk/LC_MESSAGES/sphinx.po +2614 -2509
  221. sphinx/locale/sl/LC_MESSAGES/sphinx.mo +0 -0
  222. sphinx/locale/sl/LC_MESSAGES/sphinx.po +2167 -2062
  223. sphinx/locale/sphinx.pot +2069 -1964
  224. sphinx/locale/sq/LC_MESSAGES/sphinx.mo +0 -0
  225. sphinx/locale/sq/LC_MESSAGES/sphinx.po +2661 -2556
  226. sphinx/locale/sr/LC_MESSAGES/sphinx.mo +0 -0
  227. sphinx/locale/sr/LC_MESSAGES/sphinx.po +2213 -2108
  228. sphinx/locale/sv/LC_MESSAGES/sphinx.mo +0 -0
  229. sphinx/locale/sv/LC_MESSAGES/sphinx.po +2229 -2124
  230. sphinx/locale/te/LC_MESSAGES/sphinx.mo +0 -0
  231. sphinx/locale/te/LC_MESSAGES/sphinx.po +2045 -1940
  232. sphinx/locale/tr/LC_MESSAGES/sphinx.mo +0 -0
  233. sphinx/locale/tr/LC_MESSAGES/sphinx.po +2608 -2503
  234. sphinx/locale/uk_UA/LC_MESSAGES/sphinx.mo +0 -0
  235. sphinx/locale/uk_UA/LC_MESSAGES/sphinx.po +2167 -2062
  236. sphinx/locale/ur/LC_MESSAGES/sphinx.mo +0 -0
  237. sphinx/locale/ur/LC_MESSAGES/sphinx.po +2045 -1940
  238. sphinx/locale/vi/LC_MESSAGES/sphinx.mo +0 -0
  239. sphinx/locale/vi/LC_MESSAGES/sphinx.po +2204 -2099
  240. sphinx/locale/yue/LC_MESSAGES/sphinx.mo +0 -0
  241. sphinx/locale/yue/LC_MESSAGES/sphinx.po +2045 -1940
  242. sphinx/locale/zh_HK/LC_MESSAGES/sphinx.mo +0 -0
  243. sphinx/locale/zh_HK/LC_MESSAGES/sphinx.po +2045 -1940
  244. sphinx/locale/zh_TW/LC_MESSAGES/sphinx.mo +0 -0
  245. sphinx/locale/zh_TW/LC_MESSAGES/sphinx.po +2659 -2554
  246. sphinx/locale/zh_TW.Big5/LC_MESSAGES/sphinx.mo +0 -0
  247. sphinx/locale/zh_TW.Big5/LC_MESSAGES/sphinx.po +2045 -1940
  248. sphinx/parsers.py +8 -7
  249. sphinx/project.py +2 -2
  250. sphinx/pycode/__init__.py +31 -21
  251. sphinx/pycode/ast.py +6 -3
  252. sphinx/pycode/parser.py +14 -8
  253. sphinx/pygments_styles.py +4 -5
  254. sphinx/registry.py +192 -92
  255. sphinx/roles.py +58 -7
  256. sphinx/search/__init__.py +75 -54
  257. sphinx/search/en.py +11 -13
  258. sphinx/search/fi.py +1 -1
  259. sphinx/search/ja.py +8 -6
  260. sphinx/search/nl.py +1 -1
  261. sphinx/search/zh.py +19 -21
  262. sphinx/testing/fixtures.py +26 -29
  263. sphinx/testing/path.py +26 -62
  264. sphinx/testing/restructuredtext.py +14 -8
  265. sphinx/testing/util.py +21 -19
  266. sphinx/texinputs/make.bat.jinja +50 -50
  267. sphinx/texinputs/sphinx.sty +4 -3
  268. sphinx/texinputs/sphinxlatexadmonitions.sty +1 -1
  269. sphinx/texinputs/sphinxlatexobjects.sty +29 -10
  270. sphinx/themes/basic/static/searchtools.js +8 -5
  271. sphinx/theming.py +49 -61
  272. sphinx/transforms/__init__.py +17 -38
  273. sphinx/transforms/compact_bullet_list.py +5 -3
  274. sphinx/transforms/i18n.py +8 -21
  275. sphinx/transforms/post_transforms/__init__.py +142 -93
  276. sphinx/transforms/post_transforms/code.py +5 -5
  277. sphinx/transforms/post_transforms/images.py +28 -24
  278. sphinx/transforms/references.py +3 -1
  279. sphinx/util/__init__.py +109 -60
  280. sphinx/util/_files.py +39 -23
  281. sphinx/util/_importer.py +4 -1
  282. sphinx/util/_inventory_file_reader.py +76 -0
  283. sphinx/util/_io.py +2 -2
  284. sphinx/util/_lines.py +6 -3
  285. sphinx/util/_pathlib.py +40 -2
  286. sphinx/util/build_phase.py +2 -0
  287. sphinx/util/cfamily.py +19 -14
  288. sphinx/util/console.py +44 -179
  289. sphinx/util/display.py +9 -10
  290. sphinx/util/docfields.py +140 -122
  291. sphinx/util/docstrings.py +1 -1
  292. sphinx/util/docutils.py +118 -77
  293. sphinx/util/fileutil.py +25 -26
  294. sphinx/util/http_date.py +2 -0
  295. sphinx/util/i18n.py +77 -64
  296. sphinx/util/images.py +8 -6
  297. sphinx/util/inspect.py +147 -38
  298. sphinx/util/inventory.py +215 -116
  299. sphinx/util/logging.py +33 -33
  300. sphinx/util/matching.py +12 -4
  301. sphinx/util/nodes.py +18 -13
  302. sphinx/util/osutil.py +38 -39
  303. sphinx/util/parallel.py +22 -13
  304. sphinx/util/parsing.py +2 -1
  305. sphinx/util/png.py +6 -2
  306. sphinx/util/requests.py +33 -2
  307. sphinx/util/rst.py +3 -2
  308. sphinx/util/tags.py +1 -1
  309. sphinx/util/template.py +18 -10
  310. sphinx/util/texescape.py +8 -6
  311. sphinx/util/typing.py +148 -122
  312. sphinx/versioning.py +3 -3
  313. sphinx/writers/html.py +3 -1
  314. sphinx/writers/html5.py +63 -52
  315. sphinx/writers/latex.py +83 -67
  316. sphinx/writers/manpage.py +19 -38
  317. sphinx/writers/texinfo.py +47 -47
  318. sphinx/writers/text.py +50 -32
  319. sphinx/writers/xml.py +11 -8
  320. {sphinx-8.1.2.dist-info → sphinx-8.2.0.dist-info}/LICENSE.rst +1 -1
  321. {sphinx-8.1.2.dist-info → sphinx-8.2.0.dist-info}/METADATA +25 -15
  322. sphinx-8.2.0.dist-info/RECORD +606 -0
  323. {sphinx-8.1.2.dist-info → sphinx-8.2.0.dist-info}/WHEEL +1 -1
  324. sphinx/builders/html/transforms.py +0 -90
  325. sphinx/ext/apidoc.py +0 -721
  326. sphinx/util/exceptions.py +0 -74
  327. sphinx-8.1.2.dist-info/RECORD +0 -598
  328. {sphinx-8.1.2.dist-info → sphinx-8.2.0.dist-info}/entry_points.txt +0 -0
@@ -5,14 +5,15 @@ from __future__ import annotations
5
5
  import contextlib
6
6
  import html
7
7
  import os
8
+ import os.path
8
9
  import posixpath
9
10
  import re
10
11
  import shutil
11
12
  import sys
12
13
  import warnings
13
- from os import path
14
14
  from pathlib import Path
15
- from typing import TYPE_CHECKING, Any
15
+ from types import NoneType
16
+ from typing import TYPE_CHECKING
16
17
  from urllib.parse import quote
17
18
 
18
19
  import docutils.readers.doctree
@@ -20,10 +21,10 @@ from docutils import nodes
20
21
  from docutils.core import Publisher
21
22
  from docutils.frontend import OptionParser
22
23
  from docutils.io import DocTreeInput, StringOutput
23
- from docutils.utils import relative_path
24
24
 
25
25
  from sphinx import __display_version__, package_dir
26
26
  from sphinx import version_info as sphinx_version
27
+ from sphinx._cli.util.colour import bold
27
28
  from sphinx.builders import Builder
28
29
  from sphinx.builders.html._assets import (
29
30
  _CascadingStyleSheet,
@@ -31,7 +32,7 @@ from sphinx.builders.html._assets import (
31
32
  _JavaScript,
32
33
  )
33
34
  from sphinx.builders.html._build_info import BuildInfo
34
- from sphinx.config import ENUM, Config
35
+ from sphinx.config import ENUM
35
36
  from sphinx.deprecation import _deprecation_warning
36
37
  from sphinx.domains import Index, IndexEntry
37
38
  from sphinx.environment.adapters.asset import ImageAdapter
@@ -43,9 +44,9 @@ from sphinx.locale import _, __
43
44
  from sphinx.search import js_index
44
45
  from sphinx.theming import HTMLThemeFactory
45
46
  from sphinx.util import logging
47
+ from sphinx.util._pathlib import _StrPath
46
48
  from sphinx.util._timestamps import _format_rfc3339_microseconds
47
49
  from sphinx.util._uri import is_url
48
- from sphinx.util.console import bold
49
50
  from sphinx.util.display import progress_message, status_iterator
50
51
  from sphinx.util.docutils import new_document
51
52
  from sphinx.util.fileutil import copy_asset
@@ -55,9 +56,9 @@ from sphinx.util.matching import DOTFILES, Matcher, patmatch
55
56
  from sphinx.util.osutil import (
56
57
  SEP,
57
58
  _last_modified_time,
59
+ _relative_path,
58
60
  copyfile,
59
61
  ensuredir,
60
- os_path,
61
62
  relative_uri,
62
63
  )
63
64
  from sphinx.writers.html import HTMLWriter
@@ -65,12 +66,13 @@ from sphinx.writers.html5 import HTML5Translator
65
66
 
66
67
  if TYPE_CHECKING:
67
68
  from collections.abc import Iterator, Set
68
- from typing import TypeAlias
69
+ from typing import Any, TypeAlias
69
70
 
70
71
  from docutils.nodes import Node
71
72
  from docutils.readers import Reader
72
73
 
73
74
  from sphinx.application import Sphinx
75
+ from sphinx.config import Config
74
76
  from sphinx.environment import BuildEnvironment
75
77
  from sphinx.util.typing import ExtensionMetadata
76
78
 
@@ -104,9 +106,7 @@ def convert_locale_to_language_tag(locale: str | None) -> str | None:
104
106
 
105
107
 
106
108
  class StandaloneHTMLBuilder(Builder):
107
- """
108
- Builds standalone HTML docs.
109
- """
109
+ """Builds standalone HTML docs."""
110
110
 
111
111
  name = 'html'
112
112
  format = 'html'
@@ -138,6 +138,12 @@ class StandaloneHTMLBuilder(Builder):
138
138
  def __init__(self, app: Sphinx, env: BuildEnvironment) -> None:
139
139
  super().__init__(app, env)
140
140
 
141
+ # Static and asset directories
142
+ self._static_dir = Path(self.outdir / '_static')
143
+ self._sources_dir = Path(self.outdir / '_sources')
144
+ self._downloads_dir = Path(self.outdir / '_downloads')
145
+ self._images_dir = Path(self.outdir / '_images')
146
+
141
147
  # CSS files
142
148
  self._css_files: list[_CascadingStyleSheet] = []
143
149
 
@@ -187,23 +193,25 @@ class StandaloneHTMLBuilder(Builder):
187
193
  def create_build_info(self) -> BuildInfo:
188
194
  return BuildInfo(self.config, self.tags, frozenset({'html'}))
189
195
 
190
- def _get_translations_js(self) -> str:
191
- candidates = [
192
- path.join(dir, self.config.language, 'LC_MESSAGES', 'sphinx.js')
193
- for dir in self.config.locale_dirs
194
- ] + [
195
- path.join(
196
- package_dir, 'locale', self.config.language, 'LC_MESSAGES', 'sphinx.js'
197
- ),
198
- path.join(
199
- sys.prefix, 'share/sphinx/locale', self.config.language, 'sphinx.js'
200
- ),
201
- ]
196
+ def _get_translations_js(self) -> Path | None:
197
+ for dir_ in self.config.locale_dirs:
198
+ js_file = Path(dir_, self.config.language, 'LC_MESSAGES', 'sphinx.js')
199
+ if js_file.is_file():
200
+ return js_file
202
201
 
203
- for jsfile in candidates:
204
- if path.isfile(jsfile):
205
- return jsfile
206
- return ''
202
+ js_file = package_dir.joinpath(
203
+ 'locale', self.config.language, 'LC_MESSAGES', 'sphinx.js'
204
+ )
205
+ if js_file.is_file():
206
+ return js_file
207
+
208
+ js_file = Path(
209
+ sys.prefix, 'share', 'sphinx', 'locale', self.config.language, 'sphinx.js'
210
+ )
211
+ if js_file.is_file():
212
+ return js_file
213
+
214
+ return None
207
215
 
208
216
  def _get_style_filenames(self) -> Iterator[str]:
209
217
  if isinstance(self.config.html_style, str):
@@ -268,7 +276,7 @@ class StandaloneHTMLBuilder(Builder):
268
276
  for filename in self._get_style_filenames():
269
277
  self.add_css_file(filename, priority=200)
270
278
 
271
- for filename, attrs in self.app.registry.css_files:
279
+ for filename, attrs in self.env._registry.css_files:
272
280
  self.add_css_file(filename, **attrs)
273
281
 
274
282
  for filename, attrs in self.get_builder_config('css_files', 'html'):
@@ -295,7 +303,7 @@ class StandaloneHTMLBuilder(Builder):
295
303
  self.add_js_file('doctools.js', priority=200)
296
304
  self.add_js_file('sphinx_highlight.js', priority=200)
297
305
 
298
- for filename, attrs in self.app.registry.js_files:
306
+ for filename, attrs in self.env._registry.js_files:
299
307
  self.add_js_file(filename or '', **attrs)
300
308
 
301
309
  for filename, attrs in self.get_builder_config('js_files', 'html'):
@@ -320,7 +328,7 @@ class StandaloneHTMLBuilder(Builder):
320
328
  return name
321
329
  else:
322
330
  # not given: choose a math_renderer from registered ones as possible
323
- renderers = list(self.app.registry.html_inline_math_renderers)
331
+ renderers = list(self.env._registry.html_inline_math_renderers)
324
332
  if len(renderers) == 1:
325
333
  # only default math_renderer (mathjax) is registered
326
334
  return renderers[0]
@@ -333,9 +341,9 @@ class StandaloneHTMLBuilder(Builder):
333
341
  return None
334
342
 
335
343
  def get_outdated_docs(self) -> Iterator[str]:
336
- build_info_fname = self.outdir / '.buildinfo'
344
+ build_info_path = self.outdir / '.buildinfo'
337
345
  try:
338
- build_info = BuildInfo.load(build_info_fname)
346
+ build_info = BuildInfo.load(build_info_path)
339
347
  except ValueError as exc:
340
348
  logger.warning(__('Failed to read build info file: %r'), exc)
341
349
  except OSError:
@@ -344,10 +352,10 @@ class StandaloneHTMLBuilder(Builder):
344
352
  else:
345
353
  if self.build_info != build_info:
346
354
  # log the mismatch and backup the old build info
347
- build_info_backup = build_info_fname.with_name('.buildinfo.bak')
355
+ build_info_backup = build_info_path.with_name('.buildinfo.bak')
348
356
  try:
349
- shutil.move(build_info_fname, build_info_backup)
350
- self.build_info.dump(build_info_fname)
357
+ shutil.move(build_info_path, build_info_backup)
358
+ self.build_info.dump(build_info_path)
351
359
  except OSError:
352
360
  pass # ignore errors
353
361
  else:
@@ -355,7 +363,7 @@ class StandaloneHTMLBuilder(Builder):
355
363
  msg = __(
356
364
  'build_info mismatch, copying .buildinfo to .buildinfo.bak'
357
365
  )
358
- logger.info(bold(__('building [html]: ')) + msg)
366
+ logger.info(bold(__('building [html]: ')) + msg) # NoQA: G003
359
367
 
360
368
  yield from self.env.found_docs
361
369
  return
@@ -363,14 +371,14 @@ class StandaloneHTMLBuilder(Builder):
363
371
  if self.templates:
364
372
  template_mtime = int(self.templates.newest_template_mtime() * 10**6)
365
373
  try:
366
- old_mtime = _last_modified_time(build_info_fname)
374
+ old_mtime = _last_modified_time(build_info_path)
367
375
  except Exception:
368
376
  pass
369
377
  else:
370
378
  # Let users know they have a newer template
371
379
  if template_mtime > old_mtime:
372
380
  logger.info(
373
- bold('building [html]: ')
381
+ bold('building [html]: ') # NoQA: G003
374
382
  + __(
375
383
  'template %s has been changed since the previous build, '
376
384
  'all docs will be rebuilt'
@@ -384,19 +392,19 @@ class StandaloneHTMLBuilder(Builder):
384
392
  logger.debug('[build target] did not in env: %r', docname)
385
393
  yield docname
386
394
  continue
387
- targetname = self.get_outfilename(docname)
395
+ target_name = self.get_output_path(docname)
388
396
  try:
389
- targetmtime = _last_modified_time(targetname)
390
- except Exception:
391
- targetmtime = 0
397
+ target_mtime = _last_modified_time(target_name)
398
+ except OSError:
399
+ target_mtime = 0
392
400
  try:
393
401
  doc_mtime = _last_modified_time(self.env.doc2path(docname))
394
402
  srcmtime = max(doc_mtime, template_mtime)
395
- if srcmtime > targetmtime:
403
+ if srcmtime > target_mtime:
396
404
  logger.debug(
397
- '[build target] targetname %r(%s), template(%s), docname %r(%s)',
398
- targetname,
399
- _format_rfc3339_microseconds(targetmtime),
405
+ '[build target] target_name %r(%s), template(%s), docname %r(%s)',
406
+ target_name,
407
+ _format_rfc3339_microseconds(target_mtime),
400
408
  _format_rfc3339_microseconds(template_mtime),
401
409
  docname,
402
410
  _format_rfc3339_microseconds(doc_mtime),
@@ -488,24 +496,29 @@ class StandaloneHTMLBuilder(Builder):
488
496
  favicon = self.config.html_favicon or ''
489
497
 
490
498
  if not is_url(logo):
491
- logo = path.basename(logo)
499
+ logo = os.path.basename(logo)
492
500
  if not is_url(favicon):
493
- favicon = path.basename(favicon)
501
+ favicon = os.path.basename(favicon)
494
502
 
495
503
  self.relations = self.env.collect_relations()
496
504
 
497
505
  rellinks: list[tuple[str, str, str, str]] = []
498
506
  if self.use_index:
499
507
  rellinks.append(('genindex', _('General Index'), 'I', _('index')))
500
- for indexname, indexcls, _content, _collapse in self.domain_indices:
508
+ for index_name, index_cls, _content, _collapse in self.domain_indices:
501
509
  # if it has a short name
502
- if indexcls.shortname:
503
- rellinks.append((indexname, indexcls.localname, '', indexcls.shortname))
510
+ if index_cls.shortname:
511
+ rellinks.append((
512
+ index_name,
513
+ index_cls.localname,
514
+ '',
515
+ index_cls.shortname,
516
+ ))
504
517
 
505
518
  # add assets registered after ``Builder.init()``.
506
- for css_filename, attrs in self.app.registry.css_files:
519
+ for css_filename, attrs in self.env._registry.css_files:
507
520
  self.add_css_file(css_filename, **attrs)
508
- for js_filename, attrs in self.app.registry.js_files:
521
+ for js_filename, attrs in self.env._registry.js_files:
509
522
  self.add_js_file(js_filename or '', **attrs)
510
523
 
511
524
  # back up _css_files and _js_files to allow adding CSS/JS files to a specific page.
@@ -555,6 +568,10 @@ class StandaloneHTMLBuilder(Builder):
555
568
  }
556
569
  self.globalcontext |= self.config.html_context
557
570
 
571
+ if self.copysource:
572
+ # Create _sources
573
+ ensuredir(self._sources_dir)
574
+
558
575
  def get_doc_context(self, docname: str, body: str, metatags: str) -> dict[str, Any]:
559
576
  """Collect items for the template context of a page."""
560
577
  # find out relations
@@ -656,6 +673,7 @@ class StandaloneHTMLBuilder(Builder):
656
673
  metatags = self.docwriter.clean_meta
657
674
 
658
675
  ctx = self.get_doc_context(docname, body, metatags)
676
+ ctx['has_maths_elements'] = self.docwriter._has_maths_elements
659
677
  self.handle_page(docname, ctx, event_arg=doctree)
660
678
 
661
679
  def write_doc_serialized(self, docname: str, doctree: nodes.document) -> None:
@@ -694,7 +712,7 @@ class StandaloneHTMLBuilder(Builder):
694
712
  def gen_additional_pages(self) -> None:
695
713
  # additional pages from conf.py
696
714
  for pagename, template in self.config.html_additional_pages.items():
697
- logger.info(pagename + ' ', nonl=True)
715
+ logger.info('%s ', pagename, nonl=True)
698
716
  self.handle_page(pagename, {}, template)
699
717
 
700
718
  # the search page
@@ -705,8 +723,12 @@ class StandaloneHTMLBuilder(Builder):
705
723
  # the opensearch xml file
706
724
  if self.config.html_use_opensearch and self.search:
707
725
  logger.info('opensearch ', nonl=True)
708
- fn = path.join(self.outdir, '_static', 'opensearch.xml')
709
- self.handle_page('opensearch', {}, 'opensearch.xml', outfilename=fn)
726
+ self.handle_page(
727
+ 'opensearch',
728
+ {},
729
+ 'opensearch.xml',
730
+ outfilename=self._static_dir / 'opensearch.xml',
731
+ )
710
732
 
711
733
  def write_genindex(self) -> None:
712
734
  # the total count of lines for each index letter, used to distribute
@@ -739,19 +761,19 @@ class StandaloneHTMLBuilder(Builder):
739
761
  self.handle_page('genindex', genindexcontext, 'genindex.html')
740
762
 
741
763
  def write_domain_indices(self) -> None:
742
- for indexname, indexcls, content, collapse in self.domain_indices:
743
- indexcontext = {
744
- 'indextitle': indexcls.localname,
764
+ for index_name, index_cls, content, collapse in self.domain_indices:
765
+ index_context = {
766
+ 'indextitle': index_cls.localname,
745
767
  'content': content,
746
768
  'collapse_index': collapse,
747
769
  }
748
- logger.info(indexname + ' ', nonl=True)
749
- self.handle_page(indexname, indexcontext, 'domainindex.html')
770
+ logger.info('%s ', index_name, nonl=True)
771
+ self.handle_page(index_name, index_context, 'domainindex.html')
750
772
 
751
773
  def copy_image_files(self) -> None:
752
774
  if self.images:
753
- stringify_func = ImageAdapter(self.app.env).get_original_image_uri
754
- ensuredir(self.outdir / self.imagedir)
775
+ stringify_func = ImageAdapter(self.env).get_original_image_uri
776
+ ensuredir(self._images_dir)
755
777
  for src in status_iterator(
756
778
  self.images,
757
779
  __('copying images... '),
@@ -764,7 +786,7 @@ class StandaloneHTMLBuilder(Builder):
764
786
  try:
765
787
  copyfile(
766
788
  self.srcdir / src,
767
- self.outdir / self.imagedir / dest,
789
+ self._images_dir / dest,
768
790
  force=True,
769
791
  )
770
792
  except Exception as err:
@@ -774,11 +796,11 @@ class StandaloneHTMLBuilder(Builder):
774
796
 
775
797
  def copy_download_files(self) -> None:
776
798
  def to_relpath(f: str) -> str:
777
- return relative_path(self.srcdir, f)
799
+ return _relative_path(Path(f), self.srcdir).as_posix()
778
800
 
779
801
  # copy downloadable files
780
802
  if self.env.dlfiles:
781
- ensuredir(self.outdir / '_downloads')
803
+ ensuredir(self._downloads_dir)
782
804
  for src in status_iterator(
783
805
  self.env.dlfiles,
784
806
  __('copying downloadable files... '),
@@ -788,7 +810,7 @@ class StandaloneHTMLBuilder(Builder):
788
810
  stringify_func=to_relpath,
789
811
  ):
790
812
  try:
791
- dest = self.outdir / '_downloads' / self.env.dlfiles[src][1]
813
+ dest = self._downloads_dir / self.env.dlfiles[src][1]
792
814
  ensuredir(dest.parent)
793
815
  copyfile(self.srcdir / src, dest, force=True)
794
816
  except OSError as err:
@@ -800,22 +822,21 @@ class StandaloneHTMLBuilder(Builder):
800
822
 
801
823
  def create_pygments_style_file(self) -> None:
802
824
  """Create a style file for pygments."""
803
- pyg_path = path.join(self.outdir, '_static', 'pygments.css')
825
+ pyg_path = self._static_dir / 'pygments.css'
804
826
  with open(pyg_path, 'w', encoding='utf-8') as f:
805
827
  f.write(self.highlighter.get_stylesheet())
806
828
 
807
829
  if self.dark_highlighter:
808
- dark_path = path.join(self.outdir, '_static', 'pygments_dark.css')
830
+ dark_path = self._static_dir / 'pygments_dark.css'
809
831
  with open(dark_path, 'w', encoding='utf-8') as f:
810
832
  f.write(self.dark_highlighter.get_stylesheet())
811
833
 
812
834
  def copy_translation_js(self) -> None:
813
835
  """Copy a JavaScript file for translations."""
814
- jsfile = self._get_translations_js()
815
- if jsfile:
836
+ if js_file := self._get_translations_js():
816
837
  copyfile(
817
- jsfile,
818
- self.outdir / '_static' / 'translations.js',
838
+ js_file,
839
+ self._static_dir / 'translations.js',
819
840
  force=True,
820
841
  )
821
842
 
@@ -827,14 +848,14 @@ class StandaloneHTMLBuilder(Builder):
827
848
  js_path = Path(jsfile)
828
849
  copyfile(
829
850
  js_path,
830
- self.outdir / '_static' / js_path.name,
851
+ self._static_dir / js_path.name,
831
852
  force=True,
832
853
  )
833
854
  else:
834
855
  if js_stemmer_rawcode := self.indexer.get_js_stemmer_rawcode():
835
856
  copyfile(
836
857
  js_stemmer_rawcode,
837
- self.outdir / '_static' / '_stemmer.js',
858
+ self._static_dir / '_stemmer.js',
838
859
  force=True,
839
860
  )
840
861
 
@@ -846,8 +867,8 @@ class StandaloneHTMLBuilder(Builder):
846
867
  if self.theme:
847
868
  for entry in reversed(self.theme.get_theme_dirs()):
848
869
  copy_asset(
849
- Path(entry) / 'static',
850
- self.outdir / '_static',
870
+ Path(entry, 'static'),
871
+ self._static_dir,
851
872
  excluded=DOTFILES,
852
873
  context=context,
853
874
  renderer=self.templates,
@@ -865,7 +886,7 @@ class StandaloneHTMLBuilder(Builder):
865
886
  for entry in self.config.html_static_path:
866
887
  copy_asset(
867
888
  self.confdir / entry,
868
- self.outdir / '_static',
889
+ self._static_dir,
869
890
  excluded=excluded,
870
891
  context=context,
871
892
  renderer=self.templates,
@@ -878,7 +899,7 @@ class StandaloneHTMLBuilder(Builder):
878
899
  source_path = self.confdir / self.config.html_logo
879
900
  copyfile(
880
901
  source_path,
881
- self.outdir / '_static' / source_path.name,
902
+ self._static_dir / source_path.name,
882
903
  force=True,
883
904
  )
884
905
 
@@ -887,14 +908,15 @@ class StandaloneHTMLBuilder(Builder):
887
908
  source_path = self.confdir / self.config.html_favicon
888
909
  copyfile(
889
910
  source_path,
890
- self.outdir / '_static' / source_path.name,
911
+ self._static_dir / source_path.name,
891
912
  force=True,
892
913
  )
893
914
 
894
915
  def copy_static_files(self) -> None:
895
916
  try:
896
917
  with progress_message(__('copying static files'), nonl=False):
897
- ensuredir(self.outdir / '_static')
918
+ # Ensure that the static directory exists
919
+ self._static_dir.mkdir(parents=True, exist_ok=True)
898
920
 
899
921
  # prepare context for templates
900
922
  context = self.globalcontext.copy()
@@ -969,12 +991,12 @@ class StandaloneHTMLBuilder(Builder):
969
991
  assert self.indexer is not None
970
992
  keep = set(self.env.all_docs).difference(docnames)
971
993
  try:
972
- searchindexfn = path.join(self.outdir, self.searchindex_filename)
994
+ search_index_path = self.outdir / self.searchindex_filename
973
995
  if self.indexer_dumps_unicode:
974
- with open(searchindexfn, encoding='utf-8') as ft:
996
+ with open(search_index_path, encoding='utf-8') as ft:
975
997
  self.indexer.load(ft, self.indexer_format)
976
998
  else:
977
- with open(searchindexfn, 'rb') as fb:
999
+ with open(search_index_path, 'rb') as fb:
978
1000
  self.indexer.load(fb, self.indexer_format)
979
1001
  except (OSError, ValueError):
980
1002
  if keep:
@@ -991,7 +1013,7 @@ class StandaloneHTMLBuilder(Builder):
991
1013
  def index_page(self, pagename: str, doctree: nodes.document, title: str) -> None:
992
1014
  # only index pages with title
993
1015
  if self.indexer is not None and title:
994
- filename = str(self.env.doc2path(pagename, base=False))
1016
+ filename = self.env.doc2path(pagename, base=False)
995
1017
  metadata = self.env.metadata.get(pagename, {})
996
1018
  if 'no-search' in metadata or 'nosearch' in metadata:
997
1019
  self.indexer.feed(pagename, filename, '', new_document(''))
@@ -1003,15 +1025,18 @@ class StandaloneHTMLBuilder(Builder):
1003
1025
  ) -> str:
1004
1026
  if 'includehidden' not in kwargs:
1005
1027
  kwargs['includehidden'] = False
1006
- if kwargs.get('maxdepth') == '':
1028
+ if kwargs.get('maxdepth') == '': # NoQA: PLC1901
1007
1029
  kwargs.pop('maxdepth')
1008
1030
  toctree = global_toctree_for_doc(
1009
1031
  self.env, docname, self, collapse=collapse, **kwargs
1010
1032
  )
1011
1033
  return self.render_partial(toctree)['fragment']
1012
1034
 
1013
- def get_outfilename(self, pagename: str) -> str:
1014
- return path.join(self.outdir, os_path(pagename) + self.out_suffix)
1035
+ def get_output_path(self, page_name: str, /) -> Path:
1036
+ return Path(self.outdir, page_name + self.out_suffix)
1037
+
1038
+ def get_outfilename(self, pagename: str) -> _StrPath:
1039
+ return _StrPath(self.get_output_path(pagename))
1015
1040
 
1016
1041
  def add_sidebars(self, pagename: str, ctx: dict[str, Any]) -> None:
1017
1042
  def has_wildcard(pattern: str) -> bool:
@@ -1030,7 +1055,7 @@ class StandaloneHTMLBuilder(Builder):
1030
1055
  if matched and has_wildcard(pattern):
1031
1056
  # warn if both patterns contain wildcards
1032
1057
  if has_wildcard(matched):
1033
- logger.warning(msg, pagename, matched)
1058
+ logger.warning(msg, pagename, matched, pattern)
1034
1059
  # else the already matched pattern is more specific
1035
1060
  # than the present one, because it contains no wildcard
1036
1061
  continue
@@ -1049,7 +1074,8 @@ class StandaloneHTMLBuilder(Builder):
1049
1074
  pagename: str,
1050
1075
  addctx: dict[str, Any],
1051
1076
  templatename: str = 'page.html',
1052
- outfilename: str | None = None,
1077
+ *,
1078
+ outfilename: Path | None = None,
1053
1079
  event_arg: Any = None,
1054
1080
  ) -> None:
1055
1081
  ctx = self.globalcontext.copy()
@@ -1137,8 +1163,9 @@ class StandaloneHTMLBuilder(Builder):
1137
1163
  return f'<script {" ".join(sorted(attrs))}>{body}</script>'
1138
1164
  return f'<script>{body}</script>'
1139
1165
 
1140
- uri = pathto(os.fspath(js.filename), resource=True)
1141
- if 'MathJax.js?' in os.fspath(js.filename):
1166
+ js_filename_str = os.fspath(js.filename)
1167
+ uri = pathto(js_filename_str, resource=True)
1168
+ if 'MathJax.js?' in js_filename_str:
1142
1169
  # MathJax v2 reads a ``?config=...`` query parameter,
1143
1170
  # special case this and just skip adding the checksum.
1144
1171
  # https://docs.mathjax.org/en/v2.7-latest/configuration.html#considerations-for-using-combined-configuration-files
@@ -1159,21 +1186,21 @@ class StandaloneHTMLBuilder(Builder):
1159
1186
  self._js_files[:] = self._orig_js_files
1160
1187
 
1161
1188
  self.update_page_context(pagename, templatename, ctx, event_arg)
1162
- newtmpl = self.app.emit_firstresult(
1189
+ if new_template := self.events.emit_firstresult(
1163
1190
  'html-page-context', pagename, templatename, ctx, event_arg
1164
- )
1165
- if newtmpl:
1166
- templatename = newtmpl
1191
+ ):
1192
+ templatename = new_template
1167
1193
 
1168
1194
  # sort JS/CSS before rendering HTML
1169
1195
  try: # NoQA: SIM105
1170
- # Convert script_files to list to support non-list script_files (refs: #8889)
1196
+ # Convert script_files to list to support non-list script_files
1197
+ # See: https://github.com/sphinx-doc/sphinx/issues/8889
1171
1198
  ctx['script_files'] = sorted(
1172
1199
  ctx['script_files'], key=lambda js: js.priority
1173
1200
  )
1174
1201
  except AttributeError:
1175
1202
  # Skip sorting if users modifies script_files directly (maybe via `html_context`).
1176
- # refs: #8885
1203
+ # See: https://github.com/sphinx-doc/sphinx/issues/8885
1177
1204
  #
1178
1205
  # Note: priority sorting feature will not work in this case.
1179
1206
  pass
@@ -1200,22 +1227,23 @@ class StandaloneHTMLBuilder(Builder):
1200
1227
  )
1201
1228
  raise ThemeError(msg) from exc
1202
1229
 
1203
- if not outfilename:
1204
- outfilename = self.get_outfilename(pagename)
1205
- # outfilename's path is in general different from self.outdir
1206
- ensuredir(path.dirname(outfilename))
1230
+ if outfilename:
1231
+ output_path = Path(outfilename)
1232
+ else:
1233
+ output_path = self.get_output_path(pagename)
1234
+ # The output path is in general different from self.outdir
1235
+ ensuredir(output_path.parent)
1207
1236
  try:
1208
- with open(
1209
- outfilename, 'w', encoding=ctx['encoding'], errors='xmlcharrefreplace'
1210
- ) as f:
1211
- f.write(output)
1237
+ output_path.write_text(
1238
+ output, encoding=ctx['encoding'], errors='xmlcharrefreplace'
1239
+ )
1212
1240
  except OSError as err:
1213
- logger.warning(__('error writing file %s: %s'), outfilename, err)
1241
+ logger.warning(__('error writing file %s: %s'), output_path, err)
1214
1242
  if self.copysource and ctx.get('sourcename'):
1215
1243
  # copy the source file for the "show source" link
1216
- source_name = path.join(self.outdir, '_sources', os_path(ctx['sourcename']))
1217
- ensuredir(path.dirname(source_name))
1218
- copyfile(self.env.doc2path(pagename), source_name, force=True)
1244
+ source_file_path = self._sources_dir / ctx['sourcename']
1245
+ source_file_path.parent.mkdir(parents=True, exist_ok=True)
1246
+ copyfile(self.env.doc2path(pagename), source_file_path, force=True)
1219
1247
 
1220
1248
  def update_page_context(
1221
1249
  self, pagename: str, templatename: str, ctx: dict[str, Any], event_arg: Any
@@ -1228,7 +1256,7 @@ class StandaloneHTMLBuilder(Builder):
1228
1256
 
1229
1257
  @progress_message(__('dumping object inventory'))
1230
1258
  def dump_inventory(self) -> None:
1231
- InventoryFile.dump(path.join(self.outdir, INVENTORY_FILENAME), self.env, self)
1259
+ InventoryFile.dump(self.outdir / INVENTORY_FILENAME, self.env, self)
1232
1260
 
1233
1261
  def dump_search_index(self) -> None:
1234
1262
  if self.indexer is None:
@@ -1236,16 +1264,17 @@ class StandaloneHTMLBuilder(Builder):
1236
1264
 
1237
1265
  with progress_message(__('dumping search index in %s') % self.indexer.label()):
1238
1266
  self.indexer.prune(self.env.all_docs)
1239
- searchindexfn = path.join(self.outdir, self.searchindex_filename)
1267
+ search_index_path = self.outdir / self.searchindex_filename
1268
+ search_index_tmp = self.outdir / f'{self.searchindex_filename}.tmp'
1240
1269
  # first write to a temporary file, so that if dumping fails,
1241
1270
  # the existing index won't be overwritten
1242
1271
  if self.indexer_dumps_unicode:
1243
- with open(searchindexfn + '.tmp', 'w', encoding='utf-8') as ft:
1272
+ with open(search_index_tmp, 'w', encoding='utf-8') as ft:
1244
1273
  self.indexer.dump(ft, self.indexer_format)
1245
1274
  else:
1246
- with open(searchindexfn + '.tmp', 'wb') as fb:
1275
+ with open(search_index_tmp, 'wb') as fb:
1247
1276
  self.indexer.dump(fb, self.indexer_format)
1248
- os.replace(searchindexfn + '.tmp', searchindexfn)
1277
+ Path(search_index_tmp).replace(search_index_path)
1249
1278
 
1250
1279
 
1251
1280
  def convert_html_css_files(app: Sphinx, config: Config) -> None:
@@ -1319,43 +1348,49 @@ def validate_math_renderer(app: Sphinx) -> None:
1319
1348
 
1320
1349
  def validate_html_extra_path(app: Sphinx, config: Config) -> None:
1321
1350
  """Check html_extra_paths setting."""
1322
- for entry in config.html_extra_path[:]:
1323
- extra_path = path.normpath(path.join(app.confdir, entry))
1324
- if not path.exists(extra_path):
1351
+ html_extra_path = []
1352
+ for entry in config.html_extra_path:
1353
+ extra_path = (app.confdir / entry).resolve()
1354
+ if extra_path.exists():
1355
+ if (
1356
+ app.outdir.drive == extra_path.drive
1357
+ and extra_path.is_relative_to(app.outdir)
1358
+ ): # fmt: skip
1359
+ logger.warning(
1360
+ __('html_extra_path entry %r is placed inside outdir'), entry
1361
+ )
1362
+ else:
1363
+ html_extra_path.append(entry)
1364
+ else:
1325
1365
  logger.warning(__('html_extra_path entry %r does not exist'), entry)
1326
- config.html_extra_path.remove(entry)
1327
- elif (
1328
- path.splitdrive(app.outdir)[0] == path.splitdrive(extra_path)[0]
1329
- and path.commonpath((app.outdir, extra_path)) == path.normpath(app.outdir)
1330
- ): # fmt: skip
1331
- logger.warning(
1332
- __('html_extra_path entry %r is placed inside outdir'), entry
1333
- )
1334
- config.html_extra_path.remove(entry)
1366
+ config.html_extra_path = html_extra_path
1335
1367
 
1336
1368
 
1337
1369
  def validate_html_static_path(app: Sphinx, config: Config) -> None:
1338
1370
  """Check html_static_paths setting."""
1339
- for entry in config.html_static_path[:]:
1340
- static_path = path.normpath(path.join(app.confdir, entry))
1341
- if not path.exists(static_path):
1371
+ html_static_path = []
1372
+ for entry in config.html_static_path:
1373
+ static_path = (app.confdir / entry).resolve()
1374
+ if static_path.exists():
1375
+ if (
1376
+ app.outdir.drive == static_path.drive
1377
+ and static_path.is_relative_to(app.outdir)
1378
+ ): # fmt: skip
1379
+ logger.warning(
1380
+ __('html_static_path entry %r is placed inside outdir'), entry
1381
+ )
1382
+ else:
1383
+ html_static_path.append(entry)
1384
+ else:
1342
1385
  logger.warning(__('html_static_path entry %r does not exist'), entry)
1343
- config.html_static_path.remove(entry)
1344
- elif (
1345
- path.splitdrive(app.outdir)[0] == path.splitdrive(static_path)[0]
1346
- and path.commonpath((app.outdir, static_path)) == path.normpath(app.outdir)
1347
- ): # fmt: skip
1348
- logger.warning(
1349
- __('html_static_path entry %r is placed inside outdir'), entry
1350
- )
1351
- config.html_static_path.remove(entry)
1386
+ config.html_static_path = html_static_path
1352
1387
 
1353
1388
 
1354
1389
  def validate_html_logo(app: Sphinx, config: Config) -> None:
1355
1390
  """Check html_logo setting."""
1356
1391
  if (
1357
1392
  config.html_logo
1358
- and not path.isfile(path.join(app.confdir, config.html_logo))
1393
+ and not (app.confdir / config.html_logo).is_file()
1359
1394
  and not is_url(config.html_logo)
1360
1395
  ):
1361
1396
  logger.warning(__('logo file %r does not exist'), config.html_logo)
@@ -1366,7 +1401,7 @@ def validate_html_favicon(app: Sphinx, config: Config) -> None:
1366
1401
  """Check html_favicon setting."""
1367
1402
  if (
1368
1403
  config.html_favicon
1369
- and not path.isfile(path.join(app.confdir, config.html_favicon))
1404
+ and not (app.confdir / config.html_favicon).is_file()
1370
1405
  and not is_url(config.html_favicon)
1371
1406
  ):
1372
1407
  logger.warning(__('favicon file %r does not exist'), config.html_favicon)
@@ -1406,56 +1441,78 @@ def setup(app: Sphinx) -> ExtensionMetadata:
1406
1441
  app.add_builder(StandaloneHTMLBuilder)
1407
1442
 
1408
1443
  # config values
1409
- app.add_config_value('html_theme', 'alabaster', 'html')
1410
- app.add_config_value('html_theme_path', [], 'html')
1411
- app.add_config_value('html_theme_options', {}, 'html')
1444
+ app.add_config_value('html_theme', 'alabaster', 'html', types=frozenset({str}))
1445
+ app.add_config_value('html_theme_path', [], 'html', types=frozenset({list, tuple}))
1446
+ app.add_config_value('html_theme_options', {}, 'html', types=frozenset({dict}))
1412
1447
  app.add_config_value(
1413
1448
  'html_title',
1414
1449
  lambda c: _('%s %s documentation') % (c.project, c.release),
1415
1450
  'html',
1416
- str,
1451
+ types=frozenset({str}),
1452
+ )
1453
+ app.add_config_value(
1454
+ 'html_short_title', lambda self: self.html_title, 'html', types=frozenset({str})
1455
+ )
1456
+ app.add_config_value(
1457
+ 'html_style', None, 'html', types=frozenset({list, str, tuple})
1458
+ )
1459
+ app.add_config_value('html_logo', None, 'html', types=frozenset({str}))
1460
+ app.add_config_value('html_favicon', None, 'html', types=frozenset({str}))
1461
+ app.add_config_value('html_css_files', [], 'html', types=frozenset({list, tuple}))
1462
+ app.add_config_value('html_js_files', [], 'html', types=frozenset({list, tuple}))
1463
+ app.add_config_value('html_static_path', [], 'html', types=frozenset({list, tuple}))
1464
+ app.add_config_value('html_extra_path', [], 'html', types=frozenset({list, tuple}))
1465
+ app.add_config_value('html_last_updated_fmt', None, 'html', types=frozenset({str}))
1466
+ app.add_config_value(
1467
+ 'html_last_updated_use_utc', False, 'html', types=frozenset({bool})
1468
+ )
1469
+ app.add_config_value('html_sidebars', {}, 'html', types=frozenset({dict}))
1470
+ app.add_config_value('html_additional_pages', {}, 'html', types=frozenset({dict}))
1471
+ app.add_config_value(
1472
+ 'html_domain_indices',
1473
+ True,
1474
+ 'html',
1475
+ types=frozenset({frozenset, list, set, tuple}),
1476
+ )
1477
+ app.add_config_value('html_permalinks', True, 'html', types=frozenset({bool}))
1478
+ app.add_config_value('html_permalinks_icon', '¶', 'html', types=frozenset({str}))
1479
+ app.add_config_value('html_use_index', True, 'html', types=frozenset({bool}))
1480
+ app.add_config_value('html_split_index', False, 'html', types=frozenset({bool}))
1481
+ app.add_config_value('html_copy_source', True, 'html', types=frozenset({bool}))
1482
+ app.add_config_value('html_show_sourcelink', True, 'html', types=frozenset({bool}))
1483
+ app.add_config_value(
1484
+ 'html_sourcelink_suffix', '.txt', 'html', types=frozenset({str})
1485
+ )
1486
+ app.add_config_value('html_use_opensearch', '', 'html', types=frozenset({str}))
1487
+ app.add_config_value('html_file_suffix', None, 'html', types=frozenset({str}))
1488
+ app.add_config_value('html_link_suffix', None, 'html', types=frozenset({str}))
1489
+ app.add_config_value('html_show_copyright', True, 'html', types=frozenset({bool}))
1490
+ app.add_config_value(
1491
+ 'html_show_search_summary', True, 'html', types=frozenset({bool})
1492
+ )
1493
+ app.add_config_value('html_show_sphinx', True, 'html', types=frozenset({bool}))
1494
+ app.add_config_value('html_context', {}, 'html', types=frozenset({dict}))
1495
+ app.add_config_value(
1496
+ 'html_output_encoding', 'utf-8', 'html', types=frozenset({str})
1497
+ )
1498
+ app.add_config_value('html_compact_lists', True, 'html', types=frozenset({bool}))
1499
+ app.add_config_value('html_secnumber_suffix', '. ', 'html', types=frozenset({str}))
1500
+ app.add_config_value('html_search_language', None, 'html', types=frozenset({str}))
1501
+ app.add_config_value('html_search_options', {}, 'html', types=frozenset({dict}))
1502
+ app.add_config_value('html_search_scorer', '', '', types=frozenset({str}))
1503
+ app.add_config_value(
1504
+ 'html_scaled_image_link', True, 'html', types=frozenset({bool})
1417
1505
  )
1418
- app.add_config_value('html_short_title', lambda self: self.html_title, 'html')
1419
- app.add_config_value('html_style', None, 'html', {list, str})
1420
- app.add_config_value('html_logo', None, 'html', str)
1421
- app.add_config_value('html_favicon', None, 'html', str)
1422
- app.add_config_value('html_css_files', [], 'html')
1423
- app.add_config_value('html_js_files', [], 'html')
1424
- app.add_config_value('html_static_path', [], 'html')
1425
- app.add_config_value('html_extra_path', [], 'html')
1426
- app.add_config_value('html_last_updated_fmt', None, 'html', str)
1427
- app.add_config_value('html_last_updated_use_utc', False, 'html', types={bool})
1428
- app.add_config_value('html_sidebars', {}, 'html')
1429
- app.add_config_value('html_additional_pages', {}, 'html')
1430
- app.add_config_value('html_domain_indices', True, 'html', types={set, list})
1431
- app.add_config_value('html_permalinks', True, 'html')
1432
- app.add_config_value('html_permalinks_icon', '¶', 'html')
1433
- app.add_config_value('html_use_index', True, 'html')
1434
- app.add_config_value('html_split_index', False, 'html')
1435
- app.add_config_value('html_copy_source', True, 'html')
1436
- app.add_config_value('html_show_sourcelink', True, 'html')
1437
- app.add_config_value('html_sourcelink_suffix', '.txt', 'html')
1438
- app.add_config_value('html_use_opensearch', '', 'html')
1439
- app.add_config_value('html_file_suffix', None, 'html', str)
1440
- app.add_config_value('html_link_suffix', None, 'html', str)
1441
- app.add_config_value('html_show_copyright', True, 'html')
1442
- app.add_config_value('html_show_search_summary', True, 'html')
1443
- app.add_config_value('html_show_sphinx', True, 'html')
1444
- app.add_config_value('html_context', {}, 'html')
1445
- app.add_config_value('html_output_encoding', 'utf-8', 'html')
1446
- app.add_config_value('html_compact_lists', True, 'html')
1447
- app.add_config_value('html_secnumber_suffix', '. ', 'html')
1448
- app.add_config_value('html_search_language', None, 'html', str)
1449
- app.add_config_value('html_search_options', {}, 'html')
1450
- app.add_config_value('html_search_scorer', '', '')
1451
- app.add_config_value('html_scaled_image_link', True, 'html')
1452
- app.add_config_value('html_baseurl', '', 'html')
1453
- # removal is indefinitely on hold (ref: https://github.com/sphinx-doc/sphinx/issues/10265)
1506
+ app.add_config_value('html_baseurl', '', 'html', types=frozenset({str}))
1507
+ # removal is indefinitely on hold
1508
+ # See: https://github.com/sphinx-doc/sphinx/issues/10265
1454
1509
  app.add_config_value(
1455
- 'html_codeblock_linenos_style', 'inline', 'html', ENUM('table', 'inline')
1510
+ 'html_codeblock_linenos_style', 'inline', 'html', types=ENUM('table', 'inline')
1456
1511
  )
1457
- app.add_config_value('html_math_renderer', None, 'env')
1458
- app.add_config_value('html4_writer', False, 'html')
1512
+ app.add_config_value(
1513
+ 'html_math_renderer', None, 'env', types=frozenset({str, NoneType})
1514
+ )
1515
+ app.add_config_value('html4_writer', False, 'html', types=frozenset({bool}))
1459
1516
 
1460
1517
  # events
1461
1518
  app.add_event('html-collect-pages')
@@ -1476,9 +1533,6 @@ def setup(app: Sphinx) -> ExtensionMetadata:
1476
1533
  # load default math renderer
1477
1534
  app.setup_extension('sphinx.ext.mathjax')
1478
1535
 
1479
- # load transforms for HTML builder
1480
- app.setup_extension('sphinx.builders.html.transforms')
1481
-
1482
1536
  return {
1483
1537
  'version': 'builtin',
1484
1538
  'parallel_read_safe': True,