Sphinx 8.0.2__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 +9 -2
  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.2.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.2.dist-info/LICENSE.rst +0 -67
  422. sphinx-8.0.2.dist-info/RECORD +0 -590
  423. {sphinx-8.0.2.dist-info → sphinx-8.1.0.dist-info}/WHEEL +0 -0
  424. {sphinx-8.0.2.dist-info → sphinx-8.1.0.dist-info}/entry_points.txt +0 -0
sphinx/util/__init__.py CHANGED
@@ -6,15 +6,14 @@ import hashlib
6
6
  import os
7
7
  import posixpath
8
8
  import re
9
- from importlib import import_module
10
- from os import path
11
- from typing import IO, Any
12
- from urllib.parse import parse_qsl, quote_plus, urlencode, urlsplit, urlunsplit
9
+ from typing import Any
13
10
 
14
- from sphinx.errors import ExtensionError, FiletypeNotFoundError
15
- from sphinx.locale import __
11
+ from sphinx.errors import FiletypeNotFoundError
12
+ from sphinx.util import _files, _importer, logging
16
13
  from sphinx.util import index_entries as _index_entries
17
- from sphinx.util import logging
14
+ from sphinx.util._lines import parse_line_num_spec as parselinenos # NoQA: F401
15
+ from sphinx.util._uri import encode_uri # NoQA: F401
16
+ from sphinx.util._uri import is_url as isurl # NoQA: F401
18
17
  from sphinx.util.console import strip_colors # NoQA: F401
19
18
  from sphinx.util.matching import patfilter # NoQA: F401
20
19
  from sphinx.util.nodes import ( # NoQA: F401
@@ -44,11 +43,14 @@ url_re: re.Pattern[str] = re.compile(r'(?P<schema>.+)://.*')
44
43
 
45
44
  # High-level utility functions.
46
45
 
46
+
47
47
  def docname_join(basedocname: str, docname: str) -> str:
48
48
  return posixpath.normpath(posixpath.join('/' + basedocname, '..', docname))[1:]
49
49
 
50
50
 
51
- def get_filetype(source_suffix: dict[str, str], filename: str | os.PathLike) -> str:
51
+ def get_filetype(
52
+ source_suffix: dict[str, str], filename: str | os.PathLike[str]
53
+ ) -> str:
52
54
  for suffix, filetype in source_suffix.items():
53
55
  if os.fspath(filename).endswith(suffix):
54
56
  # If default filetype (None), considered as restructuredtext.
@@ -56,49 +58,6 @@ def get_filetype(source_suffix: dict[str, str], filename: str | os.PathLike) ->
56
58
  raise FiletypeNotFoundError
57
59
 
58
60
 
59
- class FilenameUniqDict(dict):
60
- """
61
- A dictionary that automatically generates unique names for its keys,
62
- interpreted as filenames, and keeps track of a set of docnames they
63
- appear in. Used for images and downloadable files in the environment.
64
- """
65
-
66
- def __init__(self) -> None:
67
- self._existing: set[str] = set()
68
-
69
- def add_file(self, docname: str, newfile: str) -> str:
70
- if newfile in self:
71
- self[newfile][0].add(docname)
72
- return self[newfile][1]
73
- uniquename = path.basename(newfile)
74
- base, ext = path.splitext(uniquename)
75
- i = 0
76
- while uniquename in self._existing:
77
- i += 1
78
- uniquename = f'{base}{i}{ext}'
79
- self[newfile] = ({docname}, uniquename)
80
- self._existing.add(uniquename)
81
- return uniquename
82
-
83
- def purge_doc(self, docname: str) -> None:
84
- for filename, (docs, unique) in list(self.items()):
85
- docs.discard(docname)
86
- if not docs:
87
- del self[filename]
88
- self._existing.discard(unique)
89
-
90
- def merge_other(self, docnames: set[str], other: dict[str, tuple[set[str], Any]]) -> None:
91
- for filename, (docs, _unique) in other.items():
92
- for doc in docs & set(docnames):
93
- self.add_file(doc, filename)
94
-
95
- def __getstate__(self) -> set[str]:
96
- return self._existing
97
-
98
- def __setstate__(self, state: set[str]) -> None:
99
- self._existing = state
100
-
101
-
102
61
  def _md5(data: bytes = b'', **_kw: Any) -> hashlib._Hash:
103
62
  """Deprecated wrapper around hashlib.md5
104
63
 
@@ -115,153 +74,23 @@ def _sha1(data: bytes = b'', **_kw: Any) -> hashlib._Hash:
115
74
  return hashlib.sha1(data, usedforsecurity=False)
116
75
 
117
76
 
118
- class DownloadFiles(dict):
119
- """A special dictionary for download files.
120
-
121
- .. important:: This class would be refactored in nearly future.
122
- Hence don't hack this directly.
123
- """
124
-
125
- def add_file(self, docname: str, filename: str) -> str:
126
- if filename not in self:
127
- digest = hashlib.md5(filename.encode(), usedforsecurity=False).hexdigest()
128
- dest = f'{digest}/{os.path.basename(filename)}'
129
- self[filename] = (set(), dest)
130
-
131
- self[filename][0].add(docname)
132
- return self[filename][1]
133
-
134
- def purge_doc(self, docname: str) -> None:
135
- for filename, (docs, _dest) in list(self.items()):
136
- docs.discard(docname)
137
- if not docs:
138
- del self[filename]
139
-
140
- def merge_other(self, docnames: set[str], other: dict[str, tuple[set[str], Any]]) -> None:
141
- for filename, (docs, _dest) in other.items():
142
- for docname in docs & set(docnames):
143
- self.add_file(docname, filename)
144
-
145
-
146
- # a regex to recognize coding cookies
147
- _coding_re = re.compile(r'coding[:=]\s*([-\w.]+)')
148
-
149
-
150
- class UnicodeDecodeErrorHandler:
151
- """Custom error handler for open() that warns and replaces."""
152
-
153
- def __init__(self, docname: str) -> None:
154
- self.docname = docname
155
-
156
- def __call__(self, error: UnicodeDecodeError) -> tuple[str, int]:
157
- linestart = error.object.rfind(b'\n', 0, error.start)
158
- lineend = error.object.find(b'\n', error.start)
159
- if lineend == -1:
160
- lineend = len(error.object)
161
- lineno = error.object.count(b'\n', 0, error.start) + 1
162
- logger.warning(__('undecodable source characters, replacing with "?": %r'),
163
- (error.object[linestart + 1:error.start] + b'>>>' +
164
- error.object[error.start:error.end] + b'<<<' +
165
- error.object[error.end:lineend]),
166
- location=(self.docname, lineno))
167
- return ('?', error.end)
168
-
169
-
170
- # Low-level utility functions and classes.
171
-
172
- class Tee:
173
- """
174
- File-like object writing to two streams.
175
- """
176
-
177
- def __init__(self, stream1: IO, stream2: IO) -> None:
178
- self.stream1 = stream1
179
- self.stream2 = stream2
180
-
181
- def write(self, text: str) -> None:
182
- self.stream1.write(text)
183
- self.stream2.write(text)
184
-
185
- def flush(self) -> None:
186
- if hasattr(self.stream1, 'flush'):
187
- self.stream1.flush()
188
- if hasattr(self.stream2, 'flush'):
189
- self.stream2.flush()
190
-
191
-
192
- def parselinenos(spec: str, total: int) -> list[int]:
193
- """Parse a line number spec (such as "1,2,4-6") and return a list of
194
- wanted line numbers.
195
- """
196
- items = []
197
- parts = spec.split(',')
198
- for part in parts:
199
- try:
200
- begend = part.strip().split('-')
201
- if begend == ['', '']:
202
- raise ValueError
203
- if len(begend) == 1:
204
- items.append(int(begend[0]) - 1)
205
- elif len(begend) == 2:
206
- start = int(begend[0] or 1) # left half open (cf. -10)
207
- end = int(begend[1] or max(start, total)) # right half open (cf. 10-)
208
- if start > end: # invalid range (cf. 10-1)
209
- raise ValueError
210
- items.extend(range(start - 1, end))
211
- else:
212
- raise ValueError
213
- except ValueError as exc:
214
- msg = f'invalid line number spec: {spec!r}'
215
- raise ValueError(msg) from exc
216
-
217
- return items
218
-
219
-
220
- def import_object(objname: str, source: str | None = None) -> Any:
221
- """Import python object by qualname."""
222
- try:
223
- objpath = objname.split('.')
224
- modname = objpath.pop(0)
225
- obj = import_module(modname)
226
- for name in objpath:
227
- modname += '.' + name
228
- try:
229
- obj = getattr(obj, name)
230
- except AttributeError:
231
- obj = import_module(modname)
232
-
233
- return obj
234
- except (AttributeError, ImportError) as exc:
235
- if source:
236
- raise ExtensionError('Could not import %s (needed for %s)' %
237
- (objname, source), exc) from exc
238
- raise ExtensionError('Could not import %s' % objname, exc) from exc
239
-
240
-
241
- def encode_uri(uri: str) -> str:
242
- split = list(urlsplit(uri))
243
- split[1] = split[1].encode('idna').decode('ascii')
244
- split[2] = quote_plus(split[2].encode(), '/')
245
- query = [(q, v.encode()) for (q, v) in parse_qsl(split[3])]
246
- split[3] = urlencode(query)
247
- return urlunsplit(split)
248
-
249
-
250
- def isurl(url: str) -> bool:
251
- """Check *url* is URL or not."""
252
- return bool(url) and '://' in url
253
-
254
-
255
77
  # deprecated name -> (object to return, canonical path or empty string)
256
78
  _DEPRECATED_OBJECTS: dict[str, tuple[Any, str, tuple[int, int]]] = {
257
- 'split_index_msg': (_index_entries.split_index_msg,
258
- 'sphinx.util.index_entries.split_index_msg',
259
- (9, 0)),
260
- 'split_into': (_index_entries.split_index_msg,
261
- 'sphinx.util.index_entries.split_into',
262
- (9, 0)),
79
+ 'split_index_msg': (
80
+ _index_entries.split_index_msg,
81
+ 'sphinx.util.index_entries.split_index_msg',
82
+ (9, 0),
83
+ ),
84
+ 'split_into': (
85
+ _index_entries.split_index_msg,
86
+ 'sphinx.util.index_entries.split_into',
87
+ (9, 0),
88
+ ),
263
89
  'md5': (_md5, '', (9, 0)),
264
90
  'sha1': (_sha1, '', (9, 0)),
91
+ 'import_object': (_importer.import_object, '', (10, 0)),
92
+ 'FilenameUniqDict': (_files.FilenameUniqDict, '', (10, 0)),
93
+ 'DownloadFiles': (_files.DownloadFiles, '', (10, 0)),
265
94
  }
266
95
 
267
96
 
sphinx/util/_files.py ADDED
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import os.path
5
+ from typing import Any
6
+
7
+
8
+ class FilenameUniqDict(dict[str, tuple[set[str], str]]):
9
+ """
10
+ A dictionary that automatically generates unique names for its keys,
11
+ interpreted as filenames, and keeps track of a set of docnames they
12
+ appear in. Used for images and downloadable files in the environment.
13
+ """
14
+
15
+ def __init__(self) -> None:
16
+ self._existing: set[str] = set()
17
+
18
+ def add_file(self, docname: str, newfile: str) -> str:
19
+ if newfile in self:
20
+ self[newfile][0].add(docname)
21
+ return self[newfile][1]
22
+ uniquename = os.path.basename(newfile)
23
+ base, ext = os.path.splitext(uniquename)
24
+ i = 0
25
+ while uniquename in self._existing:
26
+ i += 1
27
+ uniquename = f'{base}{i}{ext}'
28
+ self[newfile] = ({docname}, uniquename)
29
+ self._existing.add(uniquename)
30
+ return uniquename
31
+
32
+ def purge_doc(self, docname: str) -> None:
33
+ for filename, (docs, unique) in list(self.items()):
34
+ docs.discard(docname)
35
+ if not docs:
36
+ del self[filename]
37
+ self._existing.discard(unique)
38
+
39
+ def merge_other(
40
+ self, docnames: set[str], other: dict[str, tuple[set[str], Any]]
41
+ ) -> None:
42
+ for filename, (docs, _unique) in other.items():
43
+ for doc in docs & set(docnames):
44
+ self.add_file(doc, filename)
45
+
46
+ def __getstate__(self) -> set[str]:
47
+ return self._existing
48
+
49
+ def __setstate__(self, state: set[str]) -> None:
50
+ self._existing = state
51
+
52
+
53
+ class DownloadFiles(dict[str, tuple[set[str], str]]):
54
+ """A special dictionary for download files.
55
+
56
+ .. important:: This class would be refactored in nearly future.
57
+ Hence don't hack this directly.
58
+ """
59
+
60
+ def add_file(self, docname: str, filename: str) -> str:
61
+ if filename not in self:
62
+ digest = hashlib.md5(filename.encode(), usedforsecurity=False).hexdigest()
63
+ dest = f'{digest}/{os.path.basename(filename)}'
64
+ self[filename] = (set(), dest)
65
+
66
+ self[filename][0].add(docname)
67
+ return self[filename][1]
68
+
69
+ def purge_doc(self, docname: str) -> None:
70
+ for filename, (docs, _dest) in list(self.items()):
71
+ docs.discard(docname)
72
+ if not docs:
73
+ del self[filename]
74
+
75
+ def merge_other(
76
+ self, docnames: set[str], other: dict[str, tuple[set[str], Any]]
77
+ ) -> None:
78
+ for filename, (docs, _dest) in other.items():
79
+ for docname in docs & set(docnames):
80
+ self.add_file(docname, filename)
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib import import_module
4
+ from typing import Any
5
+
6
+ from sphinx.errors import ExtensionError
7
+
8
+
9
+ def import_object(object_name: str, /, source: str = '') -> Any:
10
+ """Import python object by qualname."""
11
+ obj_path = object_name.split('.')
12
+ module_name = obj_path.pop(0)
13
+ try:
14
+ obj = import_module(module_name)
15
+ for name in obj_path:
16
+ module_name += '.' + name
17
+ try:
18
+ obj = getattr(obj, name)
19
+ except AttributeError:
20
+ obj = import_module(module_name)
21
+ except (AttributeError, ImportError) as exc:
22
+ if source:
23
+ msg = f'Could not import {object_name} (needed for {source})'
24
+ raise ExtensionError(msg, exc) from exc
25
+ msg = f'Could not import {object_name}'
26
+ raise ExtensionError(msg, exc) from exc
27
+ return obj
sphinx/util/_io.py CHANGED
@@ -8,8 +8,7 @@ if TYPE_CHECKING:
8
8
  from typing import Protocol
9
9
 
10
10
  class SupportsWrite(Protocol):
11
- def write(self, text: str, /) -> int | None:
12
- ...
11
+ def write(self, text: str, /) -> int | None: ... # NoQA: E704
13
12
 
14
13
 
15
14
  class TeeStripANSI:
sphinx/util/_lines.py ADDED
@@ -0,0 +1,26 @@
1
+ def parse_line_num_spec(spec: str, total: int) -> list[int]:
2
+ """Parse a line number spec (such as "1,2,4-6") and return a list of
3
+ wanted line numbers.
4
+ """
5
+ items = []
6
+ parts = spec.split(',')
7
+ for part in parts:
8
+ try:
9
+ begend = part.strip().split('-')
10
+ if begend == ['', '']:
11
+ raise ValueError
12
+ if len(begend) == 1:
13
+ items.append(int(begend[0]) - 1)
14
+ elif len(begend) == 2:
15
+ start = int(begend[0] or 1) # left half open (cf. -10)
16
+ end = int(begend[1] or max(start, total)) # right half open (cf. 10-)
17
+ if start > end: # invalid range (cf. 10-1)
18
+ raise ValueError
19
+ items.extend(range(start - 1, end))
20
+ else:
21
+ raise ValueError
22
+ except ValueError as exc:
23
+ msg = f'invalid line number spec: {spec!r}'
24
+ raise ValueError(msg) from exc
25
+
26
+ return items
sphinx/util/_pathlib.py CHANGED
@@ -33,9 +33,10 @@ _MSG = (
33
33
  # https://docs.python.org/3/library/stdtypes.html#string-methods
34
34
 
35
35
  if sys.platform == 'win32':
36
+
36
37
  class _StrPath(WindowsPath):
37
38
  def replace( # type: ignore[override]
38
- self, old: str, new: str, count: int = -1, /,
39
+ self, old: str, new: str, count: int = -1, /
39
40
  ) -> str:
40
41
  # replace exists in both Path and str;
41
42
  # in Path it makes filesystem changes, so we use the safer str version
@@ -81,10 +82,12 @@ if sys.platform == 'win32':
81
82
  def __len__(self) -> int:
82
83
  warnings.warn(_MSG, RemovedInSphinx90Warning, stacklevel=2)
83
84
  return len(self.__str__())
85
+
84
86
  else:
87
+
85
88
  class _StrPath(PosixPath):
86
89
  def replace( # type: ignore[override]
87
- self, old: str, new: str, count: int = -1, /,
90
+ self, old: str, new: str, count: int = -1, /
88
91
  ) -> str:
89
92
  # replace exists in both Path and str;
90
93
  # in Path it makes filesystem changes, so we use the safer str version
@@ -0,0 +1,53 @@
1
+ """Serialise objects to a stable representation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import types
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from typing import Any
12
+
13
+
14
+ def stable_hash(obj: Any) -> str:
15
+ """Return a stable hash for a Python data structure.
16
+
17
+ We can't just use the md5 of str(obj) as the order of collections
18
+ may be random.
19
+ """
20
+ if isinstance(obj, dict):
21
+ obj = sorted(map(stable_hash, obj.items()))
22
+ if isinstance(obj, list | tuple | set | frozenset):
23
+ obj = sorted(map(stable_hash, obj))
24
+ elif isinstance(obj, type | types.FunctionType):
25
+ # The default repr() of functions includes the ID, which is not ideal.
26
+ # We use the fully qualified name instead.
27
+ obj = f'{obj.__module__}.{obj.__qualname__}'
28
+ return hashlib.md5(str(obj).encode(), usedforsecurity=False).hexdigest()
29
+
30
+
31
+ def stable_str(obj: Any, *, indent: int | None = None) -> str:
32
+ """Return a stable string representation of a Python data structure.
33
+
34
+ We can't just use ``str(obj)`` as the order of collections may be random.
35
+ """
36
+ return json.dumps(_stable_str_prep(obj), indent=indent)
37
+
38
+
39
+ def _stable_str_prep(obj: Any) -> dict[str, Any] | list[Any] | str:
40
+ if isinstance(obj, dict):
41
+ # Convert to a sorted dict
42
+ obj = [(_stable_str_prep(k), _stable_str_prep(v)) for k, v in obj.items()]
43
+ obj.sort()
44
+ return dict(obj)
45
+ if isinstance(obj, list | tuple | set | frozenset):
46
+ # Convert to a sorted list
47
+ return sorted(map(_stable_str_prep, obj), key=str)
48
+ if isinstance(obj, type | types.FunctionType):
49
+ # The default repr() of functions includes the ID, which is not ideal.
50
+ # We use the fully qualified name instead.
51
+ return f'{obj.__module__}.{obj.__qualname__}'
52
+ # We can't do any better, just use the string representation
53
+ return str(obj)
@@ -9,4 +9,5 @@ def _format_rfc3339_microseconds(timestamp: int, /) -> str:
9
9
  :param timestamp: The timestamp to format, in microseconds.
10
10
  """
11
11
  seconds, fraction = divmod(timestamp, 10**6)
12
- return time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(seconds)) + f'.{fraction // 1_000}'
12
+ time_tuple = time.gmtime(seconds)
13
+ return time.strftime('%Y-%m-%d %H:%M:%S', time_tuple) + f'.{fraction // 1_000}'
sphinx/util/_uri.py ADDED
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from urllib.parse import parse_qsl, quote_plus, urlencode, urlsplit, urlunsplit
4
+
5
+
6
+ def encode_uri(uri: str) -> str:
7
+ scheme, netloc, url, query, fragment = urlsplit(uri)
8
+ netloc = netloc.encode('idna').decode('ascii')
9
+ url = quote_plus(url.encode(), '/')
10
+ query = urlencode([(q, v.encode()) for (q, v) in parse_qsl(query)])
11
+ return urlunsplit((scheme, netloc, url, query, fragment))
12
+
13
+
14
+ def is_url(url: str) -> bool:
15
+ """Check *url* is URL or not."""
16
+ return bool(url) and '://' in url