Sphinx 8.0.1__py3-none-any.whl → 8.1.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 (424) hide show
  1. sphinx/__init__.py +6 -3
  2. sphinx/_cli/__init__.py +40 -20
  3. sphinx/_cli/util/colour.py +5 -4
  4. sphinx/_cli/util/errors.py +28 -11
  5. sphinx/application.py +361 -38
  6. sphinx/builders/__init__.py +229 -83
  7. sphinx/builders/_epub_base.py +118 -71
  8. sphinx/builders/changes.py +39 -21
  9. sphinx/builders/dirhtml.py +4 -4
  10. sphinx/builders/dummy.py +2 -5
  11. sphinx/builders/epub3.py +43 -22
  12. sphinx/builders/gettext.py +43 -25
  13. sphinx/builders/html/__init__.py +284 -218
  14. sphinx/builders/html/_assets.py +62 -26
  15. sphinx/builders/html/_build_info.py +76 -0
  16. sphinx/builders/html/transforms.py +11 -9
  17. sphinx/builders/latex/__init__.py +139 -81
  18. sphinx/builders/latex/constants.py +7 -7
  19. sphinx/builders/latex/nodes.py +3 -2
  20. sphinx/builders/latex/theming.py +7 -5
  21. sphinx/builders/latex/transforms.py +27 -19
  22. sphinx/builders/linkcheck.py +146 -72
  23. sphinx/builders/manpage.py +30 -13
  24. sphinx/builders/singlehtml.py +22 -14
  25. sphinx/builders/texinfo.py +67 -37
  26. sphinx/builders/text.py +5 -5
  27. sphinx/builders/xml.py +6 -9
  28. sphinx/cmd/build.py +282 -103
  29. sphinx/cmd/make_mode.py +106 -63
  30. sphinx/cmd/quickstart.py +341 -145
  31. sphinx/config.py +45 -12
  32. sphinx/deprecation.py +8 -2
  33. sphinx/directives/__init__.py +28 -19
  34. sphinx/directives/code.py +86 -56
  35. sphinx/directives/other.py +50 -36
  36. sphinx/directives/patches.py +29 -19
  37. sphinx/domains/__init__.py +20 -120
  38. sphinx/domains/_domains_container.py +281 -0
  39. sphinx/domains/_index.py +110 -0
  40. sphinx/domains/c/__init__.py +3 -3
  41. sphinx/domains/c/_parser.py +10 -6
  42. sphinx/domains/changeset.py +5 -3
  43. sphinx/domains/citation.py +5 -3
  44. sphinx/domains/cpp/__init__.py +9 -11
  45. sphinx/domains/cpp/_parser.py +8 -7
  46. sphinx/domains/index.py +3 -3
  47. sphinx/domains/javascript.py +12 -7
  48. sphinx/domains/math.py +2 -2
  49. sphinx/domains/python/__init__.py +10 -5
  50. sphinx/domains/python/_object.py +1 -1
  51. sphinx/domains/rst.py +5 -5
  52. sphinx/domains/std/__init__.py +16 -11
  53. sphinx/environment/__init__.py +206 -146
  54. sphinx/environment/adapters/asset.py +3 -2
  55. sphinx/environment/adapters/indexentries.py +74 -33
  56. sphinx/environment/adapters/toctree.py +100 -43
  57. sphinx/environment/collectors/__init__.py +19 -8
  58. sphinx/environment/collectors/asset.py +47 -15
  59. sphinx/environment/collectors/dependencies.py +8 -4
  60. sphinx/environment/collectors/metadata.py +7 -2
  61. sphinx/environment/collectors/title.py +7 -2
  62. sphinx/environment/collectors/toctree.py +54 -22
  63. sphinx/errors.py +4 -1
  64. sphinx/events.py +314 -7
  65. sphinx/ext/apidoc.py +42 -18
  66. sphinx/ext/autodoc/__init__.py +52 -24
  67. sphinx/ext/autodoc/importer.py +6 -9
  68. sphinx/ext/autosectionlabel.py +1 -2
  69. sphinx/ext/autosummary/__init__.py +3 -1
  70. sphinx/ext/autosummary/generate.py +28 -14
  71. sphinx/ext/coverage.py +7 -7
  72. sphinx/ext/doctest.py +4 -8
  73. sphinx/ext/duration.py +6 -5
  74. sphinx/ext/inheritance_diagram.py +1 -1
  75. sphinx/ext/intersphinx/_cli.py +6 -4
  76. sphinx/ext/intersphinx/_load.py +77 -32
  77. sphinx/ext/intersphinx/_resolve.py +173 -79
  78. sphinx/ext/intersphinx/_shared.py +7 -5
  79. sphinx/ext/linkcode.py +7 -1
  80. sphinx/ext/mathjax.py +1 -2
  81. sphinx/ext/napoleon/__init__.py +37 -24
  82. sphinx/ext/napoleon/docstring.py +202 -134
  83. sphinx/ext/todo.py +5 -3
  84. sphinx/highlighting.py +10 -3
  85. sphinx/io.py +1 -1
  86. sphinx/jinja2glue.py +27 -6
  87. sphinx/locale/__init__.py +6 -2
  88. sphinx/locale/ar/LC_MESSAGES/sphinx.js +8 -1
  89. sphinx/locale/ar/LC_MESSAGES/sphinx.mo +0 -0
  90. sphinx/locale/ar/LC_MESSAGES/sphinx.po +2246 -2288
  91. sphinx/locale/bg/LC_MESSAGES/sphinx.js +4 -1
  92. sphinx/locale/bg/LC_MESSAGES/sphinx.mo +0 -0
  93. sphinx/locale/bg/LC_MESSAGES/sphinx.po +2113 -2159
  94. sphinx/locale/bn/LC_MESSAGES/sphinx.js +4 -1
  95. sphinx/locale/bn/LC_MESSAGES/sphinx.mo +0 -0
  96. sphinx/locale/bn/LC_MESSAGES/sphinx.po +2349 -2395
  97. sphinx/locale/ca/LC_MESSAGES/sphinx.js +4 -1
  98. sphinx/locale/ca/LC_MESSAGES/sphinx.mo +0 -0
  99. sphinx/locale/ca/LC_MESSAGES/sphinx.po +2846 -2892
  100. sphinx/locale/cak/LC_MESSAGES/sphinx.js +4 -1
  101. sphinx/locale/cak/LC_MESSAGES/sphinx.mo +0 -0
  102. sphinx/locale/cak/LC_MESSAGES/sphinx.po +2213 -2259
  103. sphinx/locale/cs/LC_MESSAGES/sphinx.js +6 -1
  104. sphinx/locale/cs/LC_MESSAGES/sphinx.mo +0 -0
  105. sphinx/locale/cs/LC_MESSAGES/sphinx.po +2225 -2269
  106. sphinx/locale/cy/LC_MESSAGES/sphinx.js +6 -1
  107. sphinx/locale/cy/LC_MESSAGES/sphinx.mo +0 -0
  108. sphinx/locale/cy/LC_MESSAGES/sphinx.po +2403 -2447
  109. sphinx/locale/da/LC_MESSAGES/sphinx.js +4 -1
  110. sphinx/locale/da/LC_MESSAGES/sphinx.mo +0 -0
  111. sphinx/locale/da/LC_MESSAGES/sphinx.po +2214 -2260
  112. sphinx/locale/de/LC_MESSAGES/sphinx.js +4 -1
  113. sphinx/locale/de/LC_MESSAGES/sphinx.mo +0 -0
  114. sphinx/locale/de/LC_MESSAGES/sphinx.po +2230 -2276
  115. sphinx/locale/de_DE/LC_MESSAGES/sphinx.js +4 -1
  116. sphinx/locale/de_DE/LC_MESSAGES/sphinx.mo +0 -0
  117. sphinx/locale/de_DE/LC_MESSAGES/sphinx.po +2113 -2159
  118. sphinx/locale/el/LC_MESSAGES/sphinx.js +4 -1
  119. sphinx/locale/el/LC_MESSAGES/sphinx.mo +0 -0
  120. sphinx/locale/el/LC_MESSAGES/sphinx.po +2619 -2665
  121. sphinx/locale/en_DE/LC_MESSAGES/sphinx.js +4 -1
  122. sphinx/locale/en_DE/LC_MESSAGES/sphinx.mo +0 -0
  123. sphinx/locale/en_DE/LC_MESSAGES/sphinx.po +2113 -2159
  124. sphinx/locale/en_FR/LC_MESSAGES/sphinx.js +4 -1
  125. sphinx/locale/en_FR/LC_MESSAGES/sphinx.mo +0 -0
  126. sphinx/locale/en_FR/LC_MESSAGES/sphinx.po +2113 -2159
  127. sphinx/locale/en_GB/LC_MESSAGES/sphinx.js +4 -1
  128. sphinx/locale/en_GB/LC_MESSAGES/sphinx.mo +0 -0
  129. sphinx/locale/en_GB/LC_MESSAGES/sphinx.po +2519 -2565
  130. sphinx/locale/en_HK/LC_MESSAGES/sphinx.js +4 -1
  131. sphinx/locale/en_HK/LC_MESSAGES/sphinx.mo +0 -0
  132. sphinx/locale/en_HK/LC_MESSAGES/sphinx.po +2113 -2159
  133. sphinx/locale/eo/LC_MESSAGES/sphinx.js +4 -1
  134. sphinx/locale/eo/LC_MESSAGES/sphinx.mo +0 -0
  135. sphinx/locale/eo/LC_MESSAGES/sphinx.po +2232 -2278
  136. sphinx/locale/es/LC_MESSAGES/sphinx.js +5 -1
  137. sphinx/locale/es/LC_MESSAGES/sphinx.mo +0 -0
  138. sphinx/locale/es/LC_MESSAGES/sphinx.po +2516 -2561
  139. sphinx/locale/es_CO/LC_MESSAGES/sphinx.js +5 -1
  140. sphinx/locale/es_CO/LC_MESSAGES/sphinx.mo +0 -0
  141. sphinx/locale/es_CO/LC_MESSAGES/sphinx.po +2114 -2159
  142. sphinx/locale/et/LC_MESSAGES/sphinx.js +4 -1
  143. sphinx/locale/et/LC_MESSAGES/sphinx.mo +0 -0
  144. sphinx/locale/et/LC_MESSAGES/sphinx.po +2317 -2363
  145. sphinx/locale/eu/LC_MESSAGES/sphinx.js +4 -1
  146. sphinx/locale/eu/LC_MESSAGES/sphinx.mo +0 -0
  147. sphinx/locale/eu/LC_MESSAGES/sphinx.po +2218 -2264
  148. sphinx/locale/fa/LC_MESSAGES/sphinx.js +4 -1
  149. sphinx/locale/fa/LC_MESSAGES/sphinx.mo +0 -0
  150. sphinx/locale/fa/LC_MESSAGES/sphinx.po +2505 -2551
  151. sphinx/locale/fi/LC_MESSAGES/sphinx.js +4 -1
  152. sphinx/locale/fi/LC_MESSAGES/sphinx.mo +0 -0
  153. sphinx/locale/fi/LC_MESSAGES/sphinx.po +2303 -2349
  154. sphinx/locale/fr/LC_MESSAGES/sphinx.js +6 -2
  155. sphinx/locale/fr/LC_MESSAGES/sphinx.mo +0 -0
  156. sphinx/locale/fr/LC_MESSAGES/sphinx.po +2863 -2908
  157. sphinx/locale/fr_FR/LC_MESSAGES/sphinx.js +5 -1
  158. sphinx/locale/fr_FR/LC_MESSAGES/sphinx.mo +0 -0
  159. sphinx/locale/fr_FR/LC_MESSAGES/sphinx.po +2114 -2159
  160. sphinx/locale/gl/LC_MESSAGES/sphinx.js +4 -1
  161. sphinx/locale/gl/LC_MESSAGES/sphinx.mo +0 -0
  162. sphinx/locale/gl/LC_MESSAGES/sphinx.po +2571 -2617
  163. sphinx/locale/he/LC_MESSAGES/sphinx.js +5 -1
  164. sphinx/locale/he/LC_MESSAGES/sphinx.mo +0 -0
  165. sphinx/locale/he/LC_MESSAGES/sphinx.po +2307 -2352
  166. sphinx/locale/hi/LC_MESSAGES/sphinx.js +4 -1
  167. sphinx/locale/hi/LC_MESSAGES/sphinx.mo +0 -0
  168. sphinx/locale/hi/LC_MESSAGES/sphinx.po +2580 -2626
  169. sphinx/locale/hi_IN/LC_MESSAGES/sphinx.js +4 -1
  170. sphinx/locale/hi_IN/LC_MESSAGES/sphinx.mo +0 -0
  171. sphinx/locale/hi_IN/LC_MESSAGES/sphinx.po +2113 -2159
  172. sphinx/locale/hr/LC_MESSAGES/sphinx.js +5 -1
  173. sphinx/locale/hr/LC_MESSAGES/sphinx.mo +0 -0
  174. sphinx/locale/hr/LC_MESSAGES/sphinx.po +2238 -2283
  175. sphinx/locale/hu/LC_MESSAGES/sphinx.js +4 -1
  176. sphinx/locale/hu/LC_MESSAGES/sphinx.mo +0 -0
  177. sphinx/locale/hu/LC_MESSAGES/sphinx.po +2228 -2274
  178. sphinx/locale/id/LC_MESSAGES/sphinx.js +3 -1
  179. sphinx/locale/id/LC_MESSAGES/sphinx.mo +0 -0
  180. sphinx/locale/id/LC_MESSAGES/sphinx.po +2787 -2834
  181. sphinx/locale/is/LC_MESSAGES/sphinx.js +4 -1
  182. sphinx/locale/is/LC_MESSAGES/sphinx.mo +0 -0
  183. sphinx/locale/is/LC_MESSAGES/sphinx.po +2224 -2270
  184. sphinx/locale/it/LC_MESSAGES/sphinx.js +5 -1
  185. sphinx/locale/it/LC_MESSAGES/sphinx.mo +0 -0
  186. sphinx/locale/it/LC_MESSAGES/sphinx.po +2231 -2276
  187. sphinx/locale/ja/LC_MESSAGES/sphinx.js +3 -1
  188. sphinx/locale/ja/LC_MESSAGES/sphinx.mo +0 -0
  189. sphinx/locale/ja/LC_MESSAGES/sphinx.po +2507 -2554
  190. sphinx/locale/ka/LC_MESSAGES/sphinx.js +4 -1
  191. sphinx/locale/ka/LC_MESSAGES/sphinx.mo +0 -0
  192. sphinx/locale/ka/LC_MESSAGES/sphinx.po +2428 -2474
  193. sphinx/locale/ko/LC_MESSAGES/sphinx.js +3 -1
  194. sphinx/locale/ko/LC_MESSAGES/sphinx.mo +0 -0
  195. sphinx/locale/ko/LC_MESSAGES/sphinx.po +2516 -2563
  196. sphinx/locale/lt/LC_MESSAGES/sphinx.js +6 -1
  197. sphinx/locale/lt/LC_MESSAGES/sphinx.mo +0 -0
  198. sphinx/locale/lt/LC_MESSAGES/sphinx.po +2425 -2469
  199. sphinx/locale/lv/LC_MESSAGES/sphinx.js +5 -1
  200. sphinx/locale/lv/LC_MESSAGES/sphinx.mo +0 -0
  201. sphinx/locale/lv/LC_MESSAGES/sphinx.po +2362 -2407
  202. sphinx/locale/mk/LC_MESSAGES/sphinx.js +4 -1
  203. sphinx/locale/mk/LC_MESSAGES/sphinx.mo +0 -0
  204. sphinx/locale/mk/LC_MESSAGES/sphinx.po +2121 -2167
  205. sphinx/locale/nb_NO/LC_MESSAGES/sphinx.js +4 -1
  206. sphinx/locale/nb_NO/LC_MESSAGES/sphinx.mo +0 -0
  207. sphinx/locale/nb_NO/LC_MESSAGES/sphinx.po +2220 -2266
  208. sphinx/locale/ne/LC_MESSAGES/sphinx.js +4 -1
  209. sphinx/locale/ne/LC_MESSAGES/sphinx.mo +0 -0
  210. sphinx/locale/ne/LC_MESSAGES/sphinx.po +2221 -2267
  211. sphinx/locale/nl/LC_MESSAGES/sphinx.js +4 -1
  212. sphinx/locale/nl/LC_MESSAGES/sphinx.mo +0 -0
  213. sphinx/locale/nl/LC_MESSAGES/sphinx.po +2240 -2286
  214. sphinx/locale/pl/LC_MESSAGES/sphinx.js +6 -1
  215. sphinx/locale/pl/LC_MESSAGES/sphinx.mo +0 -0
  216. sphinx/locale/pl/LC_MESSAGES/sphinx.po +2319 -2363
  217. sphinx/locale/pt/LC_MESSAGES/sphinx.js +5 -1
  218. sphinx/locale/pt/LC_MESSAGES/sphinx.mo +0 -0
  219. sphinx/locale/pt/LC_MESSAGES/sphinx.po +2114 -2159
  220. sphinx/locale/pt_BR/LC_MESSAGES/sphinx.js +5 -1
  221. sphinx/locale/pt_BR/LC_MESSAGES/sphinx.mo +0 -0
  222. sphinx/locale/pt_BR/LC_MESSAGES/sphinx.po +2854 -2899
  223. sphinx/locale/pt_PT/LC_MESSAGES/sphinx.js +5 -1
  224. sphinx/locale/pt_PT/LC_MESSAGES/sphinx.mo +0 -0
  225. sphinx/locale/pt_PT/LC_MESSAGES/sphinx.po +2224 -2269
  226. sphinx/locale/ro/LC_MESSAGES/sphinx.js +5 -1
  227. sphinx/locale/ro/LC_MESSAGES/sphinx.mo +0 -0
  228. sphinx/locale/ro/LC_MESSAGES/sphinx.po +2226 -2271
  229. sphinx/locale/ru/LC_MESSAGES/sphinx.js +8 -3
  230. sphinx/locale/ru/LC_MESSAGES/sphinx.mo +0 -0
  231. sphinx/locale/ru/LC_MESSAGES/sphinx.po +2841 -2885
  232. sphinx/locale/si/LC_MESSAGES/sphinx.js +4 -1
  233. sphinx/locale/si/LC_MESSAGES/sphinx.mo +0 -0
  234. sphinx/locale/si/LC_MESSAGES/sphinx.po +2294 -2340
  235. sphinx/locale/sk/LC_MESSAGES/sphinx.js +6 -1
  236. sphinx/locale/sk/LC_MESSAGES/sphinx.mo +0 -0
  237. sphinx/locale/sk/LC_MESSAGES/sphinx.po +2497 -2541
  238. sphinx/locale/sl/LC_MESSAGES/sphinx.js +6 -1
  239. sphinx/locale/sl/LC_MESSAGES/sphinx.mo +0 -0
  240. sphinx/locale/sl/LC_MESSAGES/sphinx.po +2331 -2375
  241. sphinx/locale/sphinx.pot +2121 -2167
  242. sphinx/locale/sq/LC_MESSAGES/sphinx.js +4 -1
  243. sphinx/locale/sq/LC_MESSAGES/sphinx.mo +0 -0
  244. sphinx/locale/sq/LC_MESSAGES/sphinx.po +2855 -2901
  245. sphinx/locale/sr/LC_MESSAGES/sphinx.js +5 -1
  246. sphinx/locale/sr/LC_MESSAGES/sphinx.mo +0 -0
  247. sphinx/locale/sr/LC_MESSAGES/sphinx.po +2203 -2248
  248. sphinx/locale/sr@latin/LC_MESSAGES/sphinx.mo +0 -0
  249. sphinx/locale/sr_RS/LC_MESSAGES/sphinx.mo +0 -0
  250. sphinx/locale/sv/LC_MESSAGES/sphinx.js +4 -1
  251. sphinx/locale/sv/LC_MESSAGES/sphinx.mo +0 -0
  252. sphinx/locale/sv/LC_MESSAGES/sphinx.po +2423 -2469
  253. sphinx/locale/te/LC_MESSAGES/sphinx.js +4 -1
  254. sphinx/locale/te/LC_MESSAGES/sphinx.mo +0 -0
  255. sphinx/locale/te/LC_MESSAGES/sphinx.po +2113 -2159
  256. sphinx/locale/tr/LC_MESSAGES/sphinx.js +4 -1
  257. sphinx/locale/tr/LC_MESSAGES/sphinx.mo +0 -0
  258. sphinx/locale/tr/LC_MESSAGES/sphinx.po +2443 -2489
  259. sphinx/locale/uk_UA/LC_MESSAGES/sphinx.js +6 -1
  260. sphinx/locale/uk_UA/LC_MESSAGES/sphinx.mo +0 -0
  261. sphinx/locale/uk_UA/LC_MESSAGES/sphinx.po +2329 -2373
  262. sphinx/locale/ur/LC_MESSAGES/sphinx.js +4 -1
  263. sphinx/locale/ur/LC_MESSAGES/sphinx.mo +0 -0
  264. sphinx/locale/ur/LC_MESSAGES/sphinx.po +2113 -2159
  265. sphinx/locale/vi/LC_MESSAGES/sphinx.js +3 -1
  266. sphinx/locale/vi/LC_MESSAGES/sphinx.mo +0 -0
  267. sphinx/locale/vi/LC_MESSAGES/sphinx.po +2199 -2246
  268. sphinx/locale/yue/LC_MESSAGES/sphinx.js +3 -1
  269. sphinx/locale/yue/LC_MESSAGES/sphinx.mo +0 -0
  270. sphinx/locale/yue/LC_MESSAGES/sphinx.po +2112 -2159
  271. sphinx/locale/zh_HK/LC_MESSAGES/sphinx.js +3 -1
  272. sphinx/locale/zh_HK/LC_MESSAGES/sphinx.mo +0 -0
  273. sphinx/locale/zh_HK/LC_MESSAGES/sphinx.po +2112 -2159
  274. sphinx/locale/zh_TW/LC_MESSAGES/sphinx.js +3 -1
  275. sphinx/locale/zh_TW/LC_MESSAGES/sphinx.mo +0 -0
  276. sphinx/locale/zh_TW/LC_MESSAGES/sphinx.po +2845 -2892
  277. sphinx/locale/zh_TW.Big5/LC_MESSAGES/sphinx.js +3 -1
  278. sphinx/locale/zh_TW.Big5/LC_MESSAGES/sphinx.mo +0 -0
  279. sphinx/locale/zh_TW.Big5/LC_MESSAGES/sphinx.po +2112 -2159
  280. sphinx/parsers.py +3 -1
  281. sphinx/project.py +6 -2
  282. sphinx/pycode/__init__.py +11 -4
  283. sphinx/pycode/ast.py +58 -58
  284. sphinx/pycode/parser.py +49 -28
  285. sphinx/pygments_styles.py +49 -49
  286. sphinx/registry.py +8 -3
  287. sphinx/roles.py +133 -13
  288. sphinx/search/__init__.py +146 -87
  289. sphinx/search/da.py +2 -4
  290. sphinx/search/de.py +2 -4
  291. sphinx/search/en.py +4 -4
  292. sphinx/search/es.py +2 -4
  293. sphinx/search/fi.py +2 -4
  294. sphinx/search/fr.py +2 -4
  295. sphinx/search/hu.py +2 -4
  296. sphinx/search/it.py +2 -4
  297. sphinx/search/ja.py +55 -32
  298. sphinx/search/nl.py +2 -4
  299. sphinx/search/no.py +2 -4
  300. sphinx/search/pt.py +2 -4
  301. sphinx/search/ro.py +0 -2
  302. sphinx/search/ru.py +2 -4
  303. sphinx/search/sv.py +2 -4
  304. sphinx/search/tr.py +0 -2
  305. sphinx/search/zh.py +18 -13
  306. sphinx/templates/graphviz/graphviz.css +0 -7
  307. sphinx/testing/fixtures.py +6 -5
  308. sphinx/testing/path.py +7 -5
  309. sphinx/testing/util.py +63 -29
  310. sphinx/texinputs/sphinx.sty +107 -39
  311. sphinx/texinputs/sphinxlatexadmonitions.sty +51 -35
  312. sphinx/texinputs/sphinxlatexcontainers.sty +1 -1
  313. sphinx/texinputs/sphinxlatexgraphics.sty +3 -2
  314. sphinx/texinputs/sphinxlatexindbibtoc.sty +1 -1
  315. sphinx/texinputs/sphinxlatexlists.sty +1 -1
  316. sphinx/texinputs/sphinxlatexliterals.sty +4 -1
  317. sphinx/texinputs/sphinxlatexnumfig.sty +22 -9
  318. sphinx/texinputs/sphinxlatexobjects.sty +1 -1
  319. sphinx/texinputs/sphinxlatexshadowbox.sty +72 -10
  320. sphinx/texinputs/sphinxlatexstyleheadings.sty +7 -2
  321. sphinx/texinputs/sphinxlatexstylepage.sty +2 -8
  322. sphinx/texinputs/sphinxlatexstyletext.sty +2 -4
  323. sphinx/texinputs/sphinxlatextables.sty +1 -1
  324. sphinx/texinputs/sphinxoptionsgeometry.sty +1 -1
  325. sphinx/texinputs/sphinxoptionshyperref.sty +1 -1
  326. sphinx/themes/agogo/layout.html +1 -10
  327. sphinx/themes/agogo/static/agogo.css.jinja +0 -7
  328. sphinx/themes/basic/defindex.html +1 -8
  329. sphinx/themes/basic/domainindex.html +1 -9
  330. sphinx/themes/basic/genindex-single.html +1 -9
  331. sphinx/themes/basic/genindex-split.html +1 -9
  332. sphinx/themes/basic/genindex.html +1 -9
  333. sphinx/themes/basic/globaltoc.html +1 -9
  334. sphinx/themes/basic/layout.html +1 -9
  335. sphinx/themes/basic/localtoc.html +1 -9
  336. sphinx/themes/basic/page.html +1 -9
  337. sphinx/themes/basic/relations.html +1 -9
  338. sphinx/themes/basic/search.html +1 -9
  339. sphinx/themes/basic/searchbox.html +1 -9
  340. sphinx/themes/basic/searchfield.html +4 -10
  341. sphinx/themes/basic/sourcelink.html +1 -9
  342. sphinx/themes/basic/static/basic.css.jinja +2 -13
  343. sphinx/themes/basic/static/doctools.js +0 -7
  344. sphinx/themes/basic/static/language_data.js.jinja +0 -7
  345. sphinx/themes/basic/static/searchtools.js +25 -13
  346. sphinx/themes/bizstyle/layout.html +1 -9
  347. sphinx/themes/bizstyle/static/bizstyle.css.jinja +0 -7
  348. sphinx/themes/bizstyle/static/bizstyle.js.jinja +5 -11
  349. sphinx/themes/classic/layout.html +1 -9
  350. sphinx/themes/classic/static/classic.css.jinja +0 -7
  351. sphinx/themes/classic/static/sidebar.js.jinja +0 -6
  352. sphinx/themes/epub/epub-cover.html +1 -9
  353. sphinx/themes/epub/layout.html +1 -9
  354. sphinx/themes/epub/static/epub.css.jinja +0 -7
  355. sphinx/themes/haiku/layout.html +1 -9
  356. sphinx/themes/haiku/static/haiku.css.jinja +0 -6
  357. sphinx/themes/nature/static/nature.css.jinja +0 -7
  358. sphinx/themes/nonav/layout.html +1 -9
  359. sphinx/themes/nonav/static/nonav.css.jinja +0 -7
  360. sphinx/themes/pyramid/static/epub.css.jinja +0 -7
  361. sphinx/themes/pyramid/static/pyramid.css.jinja +0 -7
  362. sphinx/themes/scrolls/layout.html +1 -10
  363. sphinx/themes/scrolls/static/scrolls.css.jinja +0 -7
  364. sphinx/themes/sphinxdoc/static/sphinxdoc.css.jinja +2 -7
  365. sphinx/themes/traditional/static/traditional.css.jinja +0 -7
  366. sphinx/theming.py +18 -6
  367. sphinx/transforms/__init__.py +56 -35
  368. sphinx/transforms/compact_bullet_list.py +3 -2
  369. sphinx/transforms/i18n.py +132 -50
  370. sphinx/transforms/post_transforms/__init__.py +94 -43
  371. sphinx/transforms/post_transforms/code.py +7 -6
  372. sphinx/transforms/post_transforms/images.py +71 -54
  373. sphinx/transforms/references.py +1 -2
  374. sphinx/util/__init__.py +23 -194
  375. sphinx/util/_files.py +80 -0
  376. sphinx/util/_importer.py +27 -0
  377. sphinx/util/_io.py +1 -2
  378. sphinx/util/_lines.py +26 -0
  379. sphinx/util/_pathlib.py +5 -2
  380. sphinx/util/_serialise.py +53 -0
  381. sphinx/util/_timestamps.py +2 -1
  382. sphinx/util/_uri.py +16 -0
  383. sphinx/util/cfamily.py +48 -25
  384. sphinx/util/console.py +1 -0
  385. sphinx/util/display.py +1 -1
  386. sphinx/util/docfields.py +125 -45
  387. sphinx/util/docstrings.py +1 -1
  388. sphinx/util/docutils.py +118 -44
  389. sphinx/util/exceptions.py +11 -5
  390. sphinx/util/fileutil.py +53 -32
  391. sphinx/util/http_date.py +9 -7
  392. sphinx/util/i18n.py +49 -16
  393. sphinx/util/images.py +7 -6
  394. sphinx/util/inspect.py +29 -12
  395. sphinx/util/inventory.py +47 -29
  396. sphinx/util/logging.py +58 -85
  397. sphinx/util/matching.py +3 -3
  398. sphinx/util/math.py +1 -1
  399. sphinx/util/nodes.py +176 -108
  400. sphinx/util/osutil.py +13 -10
  401. sphinx/util/parallel.py +5 -4
  402. sphinx/util/parsing.py +5 -3
  403. sphinx/util/png.py +3 -3
  404. sphinx/util/requests.py +8 -4
  405. sphinx/util/rst.py +5 -3
  406. sphinx/util/tags.py +5 -2
  407. sphinx/util/template.py +26 -11
  408. sphinx/util/texescape.py +2 -2
  409. sphinx/util/typing.py +89 -38
  410. sphinx/versioning.py +3 -1
  411. sphinx/writers/html.py +22 -7
  412. sphinx/writers/html5.py +113 -64
  413. sphinx/writers/latex.py +408 -221
  414. sphinx/writers/manpage.py +25 -15
  415. sphinx/writers/texinfo.py +94 -82
  416. sphinx/writers/text.py +87 -53
  417. sphinx/writers/xml.py +5 -4
  418. sphinx-8.1.0.dist-info/LICENSE.rst +31 -0
  419. {sphinx-8.0.1.dist-info → sphinx-8.1.0.dist-info}/METADATA +13 -11
  420. sphinx-8.1.0.dist-info/RECORD +598 -0
  421. sphinx-8.0.1.dist-info/LICENSE.rst +0 -67
  422. sphinx-8.0.1.dist-info/RECORD +0 -590
  423. {sphinx-8.0.1.dist-info → sphinx-8.1.0.dist-info}/WHEEL +0 -0
  424. {sphinx-8.0.1.dist-info → sphinx-8.1.0.dist-info}/entry_points.txt +0 -0
@@ -6,6 +6,7 @@ import codecs
6
6
  import pickle
7
7
  import re
8
8
  import time
9
+ from contextlib import nullcontext
9
10
  from os import path
10
11
  from typing import TYPE_CHECKING, Any, Literal, final
11
12
 
@@ -16,21 +17,31 @@ from sphinx.environment import CONFIG_CHANGED_REASON, CONFIG_OK, BuildEnvironmen
16
17
  from sphinx.environment.adapters.asset import ImageAdapter
17
18
  from sphinx.errors import SphinxError
18
19
  from sphinx.locale import __
19
- from sphinx.util import UnicodeDecodeErrorHandler, get_filetype, import_object, logging, rst
20
+ from sphinx.util import (
21
+ get_filetype,
22
+ logging,
23
+ rst,
24
+ )
25
+ from sphinx.util._importer import import_object
20
26
  from sphinx.util.build_phase import BuildPhase
21
27
  from sphinx.util.console import bold
22
28
  from sphinx.util.display import progress_message, status_iterator
23
29
  from sphinx.util.docutils import sphinx_domains
24
30
  from sphinx.util.i18n import CatalogInfo, CatalogRepository, docname_to_domain
25
31
  from sphinx.util.osutil import SEP, canon_path, ensuredir, relative_uri, relpath
26
- from sphinx.util.parallel import ParallelTasks, SerialTasks, make_chunks, parallel_available
32
+ from sphinx.util.parallel import (
33
+ ParallelTasks,
34
+ SerialTasks,
35
+ make_chunks,
36
+ parallel_available,
37
+ )
27
38
 
28
39
  # side effect: registers roles and directives
29
40
  from sphinx import directives # NoQA: F401 isort:skip
30
41
  from sphinx import roles # NoQA: F401 isort:skip
31
42
 
32
43
  if TYPE_CHECKING:
33
- from collections.abc import Iterable, Sequence
44
+ from collections.abc import Iterable, Sequence, Set
34
45
 
35
46
  from docutils.nodes import Node
36
47
 
@@ -85,8 +96,7 @@ class Builder:
85
96
 
86
97
  self.app: Sphinx = app
87
98
  self.env: BuildEnvironment = env
88
- self.env.set_versioning_method(self.versioning_method,
89
- self.versioning_compare)
99
+ self.env.set_versioning_method(self.versioning_method, self.versioning_compare)
90
100
  self.events: EventManager = app.events
91
101
  self.config: Config = app.config
92
102
  self.tags: Tags = app.tags
@@ -98,9 +108,9 @@ class Builder:
98
108
  # images that need to be copied over (source -> dest)
99
109
  self.images: dict[str, str] = {}
100
110
  # basename of images directory
101
- self.imagedir = ""
111
+ self.imagedir = ''
102
112
  # relative path to image directory from current docname (used at writing docs)
103
- self.imgpath = ""
113
+ self.imgpath = ''
104
114
 
105
115
  # these get set later
106
116
  self.parallel_ok = False
@@ -128,10 +138,14 @@ class Builder:
128
138
  def create_template_bridge(self) -> None:
129
139
  """Return the template bridge configured."""
130
140
  if self.config.template_bridge:
131
- self.templates = import_object(self.config.template_bridge,
132
- 'template_bridge setting')()
141
+ template_bridge_cls = import_object(
142
+ self.config.template_bridge,
143
+ source='template_bridge setting',
144
+ )
145
+ self.templates = template_bridge_cls()
133
146
  else:
134
147
  from sphinx.jinja2glue import BuiltinTemplateLoader
148
+
135
149
  self.templates = BuiltinTemplateLoader()
136
150
 
137
151
  def get_target_uri(self, docname: str, typ: str | None = None) -> str:
@@ -147,8 +161,10 @@ class Builder:
147
161
 
148
162
  May raise environment.NoUri if there's no way to return a sensible URI.
149
163
  """
150
- return relative_uri(self.get_target_uri(from_),
151
- self.get_target_uri(to, typ))
164
+ return relative_uri(
165
+ self.get_target_uri(from_),
166
+ self.get_target_uri(to, typ),
167
+ )
152
168
 
153
169
  def get_outdated_docs(self) -> str | Iterable[str]:
154
170
  """Return an iterable of output files that are outdated, or a string
@@ -180,12 +196,20 @@ class Builder:
180
196
  mimetypes = sorted(node['candidates'])
181
197
  image_uri = images.get_original_image_uri(node['uri'])
182
198
  if mimetypes:
183
- logger.warning(__('a suitable image for %s builder not found: '
184
- '%s (%s)'),
185
- self.name, mimetypes, image_uri, location=node)
199
+ logger.warning(
200
+ __('a suitable image for %s builder not found: ' '%s (%s)'),
201
+ self.name,
202
+ mimetypes,
203
+ image_uri,
204
+ location=node,
205
+ )
186
206
  else:
187
- logger.warning(__('a suitable image for %s builder not found: %s'),
188
- self.name, image_uri, location=node)
207
+ logger.warning(
208
+ __('a suitable image for %s builder not found: %s'),
209
+ self.name,
210
+ image_uri,
211
+ location=node,
212
+ )
189
213
  continue
190
214
  node['uri'] = candidate
191
215
  else:
@@ -205,15 +229,25 @@ class Builder:
205
229
  return relpath(cat.mo_path, self.env.srcdir).replace(path.sep, SEP)
206
230
 
207
231
  logger.info(bold(__('building [mo]: ')) + message)
208
- for catalog in status_iterator(catalogs, __('writing output... '), "darkgreen",
209
- len(catalogs), self.app.verbosity,
210
- stringify_func=cat2relpath):
211
- catalog.write_mo(self.config.language,
212
- self.config.gettext_allow_fuzzy_translations)
232
+ for catalog in status_iterator(
233
+ catalogs,
234
+ __('writing output... '),
235
+ 'darkgreen',
236
+ len(catalogs),
237
+ self.app.verbosity,
238
+ stringify_func=cat2relpath,
239
+ ):
240
+ catalog.write_mo(
241
+ self.config.language, self.config.gettext_allow_fuzzy_translations
242
+ )
213
243
 
214
244
  def compile_all_catalogs(self) -> None:
215
- repo = CatalogRepository(self.srcdir, self.config.locale_dirs,
216
- self.config.language, self.config.source_encoding)
245
+ repo = CatalogRepository(
246
+ self.srcdir,
247
+ self.config.locale_dirs,
248
+ self.config.language,
249
+ self.config.source_encoding,
250
+ )
217
251
  message = __('all of %d po files') % len(list(repo.catalogs))
218
252
  self.compile_catalogs(set(repo.catalogs), message)
219
253
 
@@ -227,8 +261,12 @@ class Builder:
227
261
 
228
262
  catalogs = set()
229
263
  domains = set(map(to_domain, specified_files))
230
- repo = CatalogRepository(self.srcdir, self.config.locale_dirs,
231
- self.config.language, self.config.source_encoding)
264
+ repo = CatalogRepository(
265
+ self.srcdir,
266
+ self.config.locale_dirs,
267
+ self.config.language,
268
+ self.config.source_encoding,
269
+ )
232
270
  for catalog in repo.catalogs:
233
271
  if catalog.domain in domains and catalog.is_outdated():
234
272
  catalogs.add(catalog)
@@ -237,8 +275,12 @@ class Builder:
237
275
 
238
276
  # TODO(stephenfin): This would make more sense as 'compile_outdated_catalogs'
239
277
  def compile_update_catalogs(self) -> None:
240
- repo = CatalogRepository(self.srcdir, self.config.locale_dirs,
241
- self.config.language, self.config.source_encoding)
278
+ repo = CatalogRepository(
279
+ self.srcdir,
280
+ self.config.locale_dirs,
281
+ self.config.language,
282
+ self.config.source_encoding,
283
+ )
242
284
  catalogs = {c for c in repo.catalogs if c.is_outdated()}
243
285
  message = __('targets for %d po files that are out of date') % len(catalogs)
244
286
  self.compile_catalogs(catalogs, message)
@@ -261,27 +303,41 @@ class Builder:
261
303
  filename = path.normpath(path.abspath(filename))
262
304
 
263
305
  if not path.isfile(filename):
264
- logger.warning(__('file %r given on command line does not exist, '),
265
- filename)
306
+ logger.warning(
307
+ __('file %r given on command line does not exist, '), filename
308
+ )
266
309
  continue
267
310
 
268
311
  if not filename.startswith(str(self.srcdir)):
269
- logger.warning(__('file %r given on command line is not under the '
270
- 'source directory, ignoring'), filename)
312
+ logger.warning(
313
+ __(
314
+ 'file %r given on command line is not under the '
315
+ 'source directory, ignoring'
316
+ ),
317
+ filename,
318
+ )
271
319
  continue
272
320
 
273
321
  docname = self.env.path2doc(filename)
274
322
  if not docname:
275
- logger.warning(__('file %r given on command line is not a valid '
276
- 'document, ignoring'), filename)
323
+ logger.warning(
324
+ __(
325
+ 'file %r given on command line is not a valid '
326
+ 'document, ignoring'
327
+ ),
328
+ filename,
329
+ )
277
330
  continue
278
331
 
279
332
  docnames.append(docname)
280
333
 
281
334
  self.compile_specific_catalogs(filenames)
282
335
 
283
- self.build(docnames, method='specific',
284
- summary=__('%d source files given on command line') % len(docnames))
336
+ self.build(
337
+ docnames,
338
+ method='specific',
339
+ summary=__('%d source files given on command line') % len(docnames),
340
+ )
285
341
 
286
342
  @final
287
343
  def build_update(self) -> None:
@@ -293,9 +349,11 @@ class Builder:
293
349
  self.build(['__all__'], to_build)
294
350
  else:
295
351
  to_build = list(to_build)
296
- self.build(to_build,
297
- summary=__('targets for %d source files that are out of date') %
298
- len(to_build))
352
+ self.build(
353
+ to_build,
354
+ summary=__('targets for %d source files that are out of date')
355
+ % len(to_build),
356
+ )
299
357
 
300
358
  @final
301
359
  def build(
@@ -313,7 +371,11 @@ class Builder:
313
371
  logger.info(bold(__('building [%s]: ')) + summary, self.name)
314
372
 
315
373
  # while reading, collect all warnings from docutils
316
- with logging.pending_warnings():
374
+ with (
375
+ nullcontext()
376
+ if self.app._exception_on_warning
377
+ else logging.pending_warnings()
378
+ ):
317
379
  updated_docnames = set(self.read())
318
380
 
319
381
  doccount = len(updated_docnames)
@@ -328,8 +390,11 @@ class Builder:
328
390
  if updated_docnames:
329
391
  # save the environment
330
392
  from sphinx.application import ENV_PICKLE_FILENAME
331
- with progress_message(__('pickling environment')), \
332
- open(path.join(self.doctreedir, ENV_PICKLE_FILENAME), 'wb') as f:
393
+
394
+ with (
395
+ progress_message(__('pickling environment')),
396
+ open(path.join(self.doctreedir, ENV_PICKLE_FILENAME), 'wb') as f,
397
+ ):
333
398
  pickle.dump(self.env, f, pickle.HIGHEST_PROTOCOL)
334
399
 
335
400
  # global actions
@@ -381,11 +446,13 @@ class Builder:
381
446
  logger.info(bold(__('updating environment: ')), nonl=True)
382
447
 
383
448
  self.env.find_files(self.config, self)
384
- updated = (self.env.config_status != CONFIG_OK)
449
+ updated = self.env.config_status != CONFIG_OK
385
450
  added, changed, removed = self.env.get_outdated_files(updated)
386
451
 
387
452
  # allow user intervention as well
388
- for docs in self.events.emit('env-get-outdated', self.env, added, changed, removed):
453
+ for docs in self.events.emit(
454
+ 'env-get-outdated', self.env, added, changed, removed
455
+ ):
389
456
  changed.update(set(docs) & self.env.found_docs)
390
457
 
391
458
  # if files were added or removed, all documents with globbed toctrees
@@ -395,12 +462,17 @@ class Builder:
395
462
  changed.update(self.env.glob_toctrees & self.env.found_docs)
396
463
 
397
464
  if updated: # explain the change iff build config status was not ok
398
- reason = (CONFIG_CHANGED_REASON.get(self.env.config_status, '') +
399
- (self.env.config_status_extra or ''))
465
+ reason = CONFIG_CHANGED_REASON.get(self.env.config_status, '') + (
466
+ self.env.config_status_extra or ''
467
+ )
400
468
  logger.info('[%s] ', reason, nonl=True)
401
469
 
402
- logger.info(__('%s added, %s changed, %s removed'),
403
- len(added), len(changed), len(removed))
470
+ logger.info(
471
+ __('%s added, %s changed, %s removed'),
472
+ len(added),
473
+ len(changed),
474
+ len(removed),
475
+ )
404
476
 
405
477
  # clear all files no longer present
406
478
  for docname in removed:
@@ -413,7 +485,7 @@ class Builder:
413
485
  self.events.emit('env-before-read-docs', self.env, docnames)
414
486
 
415
487
  # check if we should do parallel or serial read
416
- if parallel_available and len(docnames) > 5 and self.app.parallel > 1:
488
+ if parallel_available and self.app.parallel > 1:
417
489
  par_ok = self.app.is_parallel_allowed('read')
418
490
  else:
419
491
  par_ok = False
@@ -432,30 +504,38 @@ class Builder:
432
504
  for pat in EXCLUDE_PATHS:
433
505
  if not re.match(_translate_pattern(pat), master_doc_canon):
434
506
  continue
435
- msg = __('Sphinx is unable to load the master document (%s) '
436
- 'because it matches a built-in exclude pattern %r. '
437
- 'Please move your master document to a different location.')
507
+ msg = __(
508
+ 'Sphinx is unable to load the master document (%s) '
509
+ 'because it matches a built-in exclude pattern %r. '
510
+ 'Please move your master document to a different location.'
511
+ )
438
512
  raise SphinxError(msg % (master_doc_path, pat))
439
513
  for pat in self.config.exclude_patterns:
440
514
  if not re.match(_translate_pattern(pat), master_doc_canon):
441
515
  continue
442
- msg = __('Sphinx is unable to load the master document (%s) '
443
- 'because it matches an exclude pattern specified '
444
- 'in conf.py, %r. '
445
- 'Please remove this pattern from conf.py.')
516
+ msg = __(
517
+ 'Sphinx is unable to load the master document (%s) '
518
+ 'because it matches an exclude pattern specified '
519
+ 'in conf.py, %r. '
520
+ 'Please remove this pattern from conf.py.'
521
+ )
446
522
  raise SphinxError(msg % (master_doc_path, pat))
447
523
  if set(self.config.include_patterns) != {'**'} and not any(
448
524
  re.match(_translate_pattern(pat), master_doc_canon)
449
525
  for pat in self.config.include_patterns
450
526
  ):
451
- msg = __('Sphinx is unable to load the master document (%s) '
452
- 'because it is not included in the custom include_patterns = %r. '
453
- 'Ensure that a pattern in include_patterns matches the '
454
- 'master document.')
527
+ msg = __(
528
+ 'Sphinx is unable to load the master document (%s) '
529
+ 'because it is not included in the custom include_patterns = %r. '
530
+ 'Ensure that a pattern in include_patterns matches the '
531
+ 'master document.'
532
+ )
455
533
  raise SphinxError(msg % (master_doc_path, self.config.include_patterns))
456
- msg = __('Sphinx is unable to load the master document (%s). '
457
- 'The master document must be within the source directory '
458
- 'or a subdirectory of it.')
534
+ msg = __(
535
+ 'Sphinx is unable to load the master document (%s). '
536
+ 'The master document must be within the source directory '
537
+ 'or a subdirectory of it.'
538
+ )
459
539
  raise SphinxError(msg % master_doc_path)
460
540
 
461
541
  for retval in self.events.emit('env-updated', self.env):
@@ -468,8 +548,13 @@ class Builder:
468
548
  return sorted(docnames)
469
549
 
470
550
  def _read_serial(self, docnames: list[str]) -> None:
471
- for docname in status_iterator(docnames, __('reading sources... '), "purple",
472
- len(docnames), self.app.verbosity):
551
+ for docname in status_iterator(
552
+ docnames,
553
+ __('reading sources... '),
554
+ 'purple',
555
+ len(docnames),
556
+ self.app.verbosity,
557
+ ):
473
558
  # remove all inventory entries for that file
474
559
  self.events.emit('env-purge-doc', self.env, docname)
475
560
  self.env.clear_doc(docname)
@@ -480,8 +565,9 @@ class Builder:
480
565
 
481
566
  # create a status_iterator to step progressbar after reading a document
482
567
  # (see: ``merge()`` function)
483
- progress = status_iterator(chunks, __('reading sources... '), "purple",
484
- len(chunks), self.app.verbosity)
568
+ progress = status_iterator(
569
+ chunks, __('reading sources... '), 'purple', len(chunks), self.app.verbosity
570
+ )
485
571
 
486
572
  # clear all outdated docs at once
487
573
  for docname in docnames:
@@ -526,10 +612,13 @@ class Builder:
526
612
  # record_dependencies is mutable even though it is in settings,
527
613
  # explicitly re-initialise for each document
528
614
  publisher.settings.record_dependencies = DependencyList()
529
- with sphinx_domains(self.env), rst.default_role(docname, self.config.default_role):
615
+ with (
616
+ sphinx_domains(self.env),
617
+ rst.default_role(docname, self.config.default_role),
618
+ ):
530
619
  # set up error_handler for the target document
531
- codecs.register_error('sphinx',
532
- UnicodeDecodeErrorHandler(docname)) # type: ignore[arg-type]
620
+ error_handler = _UnicodeDecodeErrorHandler(docname)
621
+ codecs.register_error('sphinx', error_handler) # type: ignore[arg-type]
533
622
 
534
623
  publisher.set_source(source_path=filename)
535
624
  publisher.publish()
@@ -546,7 +635,11 @@ class Builder:
546
635
 
547
636
  @final
548
637
  def write_doctree(
549
- self, docname: str, doctree: nodes.document, *, _cache: bool = True,
638
+ self,
639
+ docname: str,
640
+ doctree: nodes.document,
641
+ *,
642
+ _cache: bool = True,
550
643
  ) -> None:
551
644
  """Write the doctree to a file, to be used as a cache by re-builds."""
552
645
  # make it picklable
@@ -571,6 +664,7 @@ class Builder:
571
664
  if _cache:
572
665
  self.env._write_doc_doctree_cache[docname] = doctree
573
666
 
667
+ @final
574
668
  def write(
575
669
  self,
576
670
  build_docnames: Iterable[str] | None,
@@ -592,11 +686,15 @@ class Builder:
592
686
  logger.debug(__('docnames to write: %s'), ', '.join(sorted(docnames)))
593
687
 
594
688
  # add all toctree-containing files that may have changed
595
- for docname in list(docnames):
689
+ extra = {self.config.root_doc}
690
+ for docname in docnames:
596
691
  for tocdocname in self.env.files_to_rebuild.get(docname, set()):
597
692
  if tocdocname in self.env.found_docs:
598
- docnames.add(tocdocname)
599
- docnames.add(self.config.root_doc)
693
+ extra.add(tocdocname)
694
+ docnames |= extra
695
+
696
+ # sort to ensure deterministic toctree generation
697
+ self.env.toctree_includes = dict(sorted(self.env.toctree_includes.items()))
600
698
 
601
699
  with progress_message(__('preparing documents')):
602
700
  self.prepare_writing(docnames)
@@ -604,18 +702,35 @@ class Builder:
604
702
  with progress_message(__('copying assets'), nonl=False):
605
703
  self.copy_assets()
606
704
 
705
+ self.write_documents(docnames)
706
+
707
+ def write_documents(self, docnames: Set[str]) -> None:
708
+ """Write all documents in *docnames*.
709
+
710
+ This method can be overridden if a builder does not create
711
+ output files for each document.
712
+ """
713
+ sorted_docnames = sorted(docnames)
607
714
  if self.parallel_ok:
608
715
  # number of subprocesses is parallel-1 because the main process
609
716
  # is busy loading doctrees and doing write_doc_serialized()
610
- self._write_parallel(sorted(docnames),
611
- nproc=self.app.parallel - 1)
717
+ self._write_parallel(sorted_docnames, nproc=self.app.parallel - 1)
612
718
  else:
613
- self._write_serial(sorted(docnames))
719
+ self._write_serial(sorted_docnames)
614
720
 
615
721
  def _write_serial(self, docnames: Sequence[str]) -> None:
616
- with logging.pending_warnings():
617
- for docname in status_iterator(docnames, __('writing output... '), "darkgreen",
618
- len(docnames), self.app.verbosity):
722
+ with (
723
+ nullcontext()
724
+ if self.app._exception_on_warning
725
+ else logging.pending_warnings()
726
+ ):
727
+ for docname in status_iterator(
728
+ docnames,
729
+ __('writing output... '),
730
+ 'darkgreen',
731
+ len(docnames),
732
+ self.app.verbosity,
733
+ ):
619
734
  self.app.phase = BuildPhase.RESOLVING
620
735
  doctree = self.env.get_and_resolve_doctree(docname, self)
621
736
  self.app.phase = BuildPhase.WRITING
@@ -641,8 +756,13 @@ class Builder:
641
756
 
642
757
  # create a status_iterator to step progressbar after writing a document
643
758
  # (see: ``on_chunk_done()`` function)
644
- progress = status_iterator(chunks, __('writing output... '), "darkgreen",
645
- len(chunks), self.app.verbosity)
759
+ progress = status_iterator(
760
+ chunks,
761
+ __('writing output... '),
762
+ 'darkgreen',
763
+ len(chunks),
764
+ self.app.verbosity,
765
+ )
646
766
 
647
767
  def on_chunk_done(args: list[tuple[str, nodes.document]], result: None) -> None:
648
768
  next(progress)
@@ -660,9 +780,9 @@ class Builder:
660
780
  tasks.join()
661
781
  logger.info('')
662
782
 
663
- def prepare_writing(self, docnames: set[str]) -> None:
783
+ def prepare_writing(self, docnames: Set[str]) -> None:
664
784
  """A place where you can add logic before :meth:`write_doc` is run"""
665
- raise NotImplementedError
785
+ pass
666
786
 
667
787
  def copy_assets(self) -> None:
668
788
  """Where assets (images, static files, etc) are copied before writing"""
@@ -707,3 +827,29 @@ class Builder:
707
827
  except AttributeError:
708
828
  optname = f'{default}_{option}'
709
829
  return getattr(self.config, optname)
830
+
831
+
832
+ class _UnicodeDecodeErrorHandler:
833
+ """Custom error handler for open() that warns and replaces."""
834
+
835
+ def __init__(self, docname: str, /) -> None:
836
+ self.docname = docname
837
+
838
+ def __call__(self, error: UnicodeDecodeError) -> tuple[str, int]:
839
+ line_start = error.object.rfind(b'\n', 0, error.start)
840
+ line_end = error.object.find(b'\n', error.start)
841
+ if line_end == -1:
842
+ line_end = len(error.object)
843
+ line_num = error.object.count(b'\n', 0, error.start) + 1
844
+ logger.warning(
845
+ __('undecodable source characters, replacing with "?": %r'),
846
+ (
847
+ error.object[line_start + 1 : error.start]
848
+ + b'>>>'
849
+ + error.object[error.start : error.end]
850
+ + b'<<<'
851
+ + error.object[error.end : line_end]
852
+ ),
853
+ location=(self.docname, line_num),
854
+ )
855
+ return '?', error.end