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
@@ -7,29 +7,34 @@ import json
7
7
  import re
8
8
  import socket
9
9
  import time
10
+ from enum import StrEnum
10
11
  from html.parser import HTMLParser
11
- from os import path
12
12
  from queue import PriorityQueue, Queue
13
13
  from threading import Thread
14
14
  from typing import TYPE_CHECKING, NamedTuple, cast
15
15
  from urllib.parse import quote, unquote, urlparse, urlsplit, urlunparse
16
16
 
17
17
  from docutils import nodes
18
- from requests.exceptions import ConnectionError, HTTPError, SSLError, TooManyRedirects
18
+ from requests.exceptions import (
19
+ ConnectionError, # NoQA: A004
20
+ HTTPError,
21
+ SSLError,
22
+ TooManyRedirects,
23
+ )
19
24
  from requests.exceptions import Timeout as RequestTimeout
20
25
 
26
+ from sphinx._cli.util.colour import darkgray, darkgreen, purple, red, turquoise
21
27
  from sphinx.builders.dummy import DummyBuilder
22
28
  from sphinx.locale import __
23
29
  from sphinx.transforms.post_transforms import SphinxPostTransform
24
30
  from sphinx.util import logging, requests
25
31
  from sphinx.util._uri import encode_uri
26
- from sphinx.util.console import darkgray, darkgreen, purple, red, turquoise
27
32
  from sphinx.util.http_date import rfc1123_to_epoch
28
33
  from sphinx.util.nodes import get_node_line
29
34
 
30
35
  if TYPE_CHECKING:
31
36
  from collections.abc import Callable, Iterator
32
- from typing import Any
37
+ from typing import Any, Literal, TypeAlias
33
38
 
34
39
  from requests import Response
35
40
 
@@ -38,6 +43,20 @@ if TYPE_CHECKING:
38
43
  from sphinx.util._pathlib import _StrPath
39
44
  from sphinx.util.typing import ExtensionMetadata
40
45
 
46
+ _URIProperties: TypeAlias = tuple['_Status', str, int]
47
+
48
+
49
+ class _Status(StrEnum):
50
+ BROKEN = 'broken'
51
+ IGNORED = 'ignored'
52
+ RATE_LIMITED = 'rate-limited'
53
+ REDIRECTED = 'redirected'
54
+ TIMEOUT = 'timeout'
55
+ UNCHECKED = 'unchecked'
56
+ UNKNOWN = 'unknown'
57
+ WORKING = 'working'
58
+
59
+
41
60
  logger = logging.getLogger(__name__)
42
61
 
43
62
  # matches to foo:// and // (a protocol relative URL)
@@ -52,9 +71,7 @@ DEFAULT_DELAY = 60.0
52
71
 
53
72
 
54
73
  class CheckExternalLinksBuilder(DummyBuilder):
55
- """
56
- Checks for broken external links.
57
- """
74
+ """Checks for broken external links."""
58
75
 
59
76
  name = 'linkcheck'
60
77
  epilog = __('Look for any errors in the above output or in %(outdir)s/output.txt')
@@ -70,8 +87,8 @@ class CheckExternalLinksBuilder(DummyBuilder):
70
87
  checker = HyperlinkAvailabilityChecker(self.config)
71
88
  logger.info('')
72
89
 
73
- output_text = path.join(self.outdir, 'output.txt')
74
- output_json = path.join(self.outdir, 'output.json')
90
+ output_text = self.outdir / 'output.txt'
91
+ output_json = self.outdir / 'output.json'
75
92
  with (
76
93
  open(output_text, 'w', encoding='utf-8') as self.txt_outfile,
77
94
  open(output_json, 'w', encoding='utf-8') as self.json_outfile,
@@ -84,112 +101,107 @@ class CheckExternalLinksBuilder(DummyBuilder):
84
101
 
85
102
  def process_result(self, result: CheckResult) -> None:
86
103
  filename = self.env.doc2path(result.docname, False)
104
+ res_uri = result.uri
87
105
 
88
- linkstat: dict[str, str | int] = {
106
+ linkstat: dict[str, str | int | _Status] = {
89
107
  'filename': str(filename),
90
108
  'lineno': result.lineno,
91
109
  'status': result.status,
92
110
  'code': result.code,
93
- 'uri': result.uri,
111
+ 'uri': res_uri,
94
112
  'info': result.message,
95
113
  }
96
114
  self.write_linkstat(linkstat)
97
115
 
98
- if result.status == 'unchecked':
99
- return
100
- if result.status == 'working' and result.message == 'old':
101
- return
102
- if result.lineno:
116
+ if result.lineno and result.status != _Status.UNCHECKED:
117
+ # unchecked links are not logged
103
118
  logger.info('(%16s: line %4d) ', result.docname, result.lineno, nonl=True)
104
- if result.status == 'ignored':
105
- if result.message:
106
- logger.info(darkgray('-ignored- ') + result.uri + ': ' + result.message)
107
- else:
108
- logger.info(darkgray('-ignored- ') + result.uri)
109
- elif result.status == 'local':
110
- logger.info(darkgray('-local- ') + result.uri)
111
- self.write_entry(
112
- 'local', result.docname, filename, result.lineno, result.uri
113
- )
114
- elif result.status == 'working':
115
- logger.info(darkgreen('ok ') + result.uri + result.message)
116
- elif result.status == 'timeout':
117
- if self.app.quiet:
118
- logger.warning(
119
- 'timeout ' + result.uri + result.message,
120
- location=(result.docname, result.lineno),
121
- )
122
- else:
123
- logger.info(
124
- red('timeout ') + result.uri + red(' - ' + result.message)
125
- )
126
- self.write_entry(
127
- 'timeout',
128
- result.docname,
129
- filename,
130
- result.lineno,
131
- result.uri + ': ' + result.message,
132
- )
133
- self.timed_out_hyperlinks += 1
134
- elif result.status == 'broken':
135
- if self.app.quiet:
136
- logger.warning(
137
- __('broken link: %s (%s)'),
138
- result.uri,
139
- result.message,
140
- location=(result.docname, result.lineno),
141
- )
142
- else:
143
- logger.info(
144
- red('broken ') + result.uri + red(' - ' + result.message)
119
+
120
+ match result.status:
121
+ case _Status.RATE_LIMITED | _Status.UNCHECKED:
122
+ pass
123
+ case _Status.IGNORED:
124
+ if result.message:
125
+ msg = f'{res_uri}: {result.message}'
126
+ else:
127
+ msg = res_uri
128
+ logger.info(darkgray('-ignored- ') + msg) # NoQA: G003
129
+ case _Status.WORKING:
130
+ logger.info(darkgreen('ok ') + f'{res_uri}{result.message}') # NoQA: G003
131
+ case _Status.TIMEOUT:
132
+ if self.app.quiet:
133
+ msg = 'timeout ' + f'{res_uri}{result.message}'
134
+ logger.warning(msg, location=(result.docname, result.lineno))
135
+ else:
136
+ msg = red('timeout ') + res_uri + red(f' - {result.message}')
137
+ logger.info(msg)
138
+ self.write_entry(
139
+ _Status.TIMEOUT,
140
+ result.docname,
141
+ filename,
142
+ result.lineno,
143
+ f'{res_uri}: {result.message}',
145
144
  )
146
- self.write_entry(
147
- 'broken',
148
- result.docname,
149
- filename,
150
- result.lineno,
151
- result.uri + ': ' + result.message,
152
- )
153
- self.broken_hyperlinks += 1
154
- elif result.status == 'redirected':
155
- try:
156
- text, color = {
157
- 301: ('permanently', purple),
158
- 302: ('with Found', purple),
159
- 303: ('with See Other', purple),
160
- 307: ('temporarily', turquoise),
161
- 308: ('permanently', purple),
162
- }[result.code]
163
- except KeyError:
164
- text, color = ('with unknown code', purple)
165
- linkstat['text'] = text
166
- if self.config.linkcheck_allowed_redirects:
167
- logger.warning(
168
- 'redirect ' + result.uri + ' - ' + text + ' to ' + result.message,
169
- location=(result.docname, result.lineno),
145
+ self.timed_out_hyperlinks += 1
146
+ case _Status.BROKEN:
147
+ if self.app.quiet:
148
+ logger.warning(
149
+ __('broken link: %s (%s)'),
150
+ res_uri,
151
+ result.message,
152
+ location=(result.docname, result.lineno),
153
+ )
154
+ else:
155
+ msg = red('broken ') + res_uri + red(f' - {result.message}')
156
+ logger.info(msg)
157
+ self.write_entry(
158
+ _Status.BROKEN,
159
+ result.docname,
160
+ filename,
161
+ result.lineno,
162
+ f'{res_uri}: {result.message}',
170
163
  )
171
- else:
172
- logger.info(
173
- color('redirect ')
174
- + result.uri
175
- + color(' - ' + text + ' to ' + result.message)
164
+ self.broken_hyperlinks += 1
165
+ case _Status.REDIRECTED:
166
+ match result.code:
167
+ case 301:
168
+ text = 'permanently'
169
+ case 302:
170
+ text = 'with Found'
171
+ case 303:
172
+ text = 'with See Other'
173
+ case 307:
174
+ text = 'temporarily'
175
+ case 308:
176
+ text = 'permanently'
177
+ case _:
178
+ text = 'with unknown code'
179
+ linkstat['text'] = text
180
+ redirection = f'{text} to {result.message}'
181
+ if self.config.linkcheck_allowed_redirects:
182
+ msg = f'redirect {res_uri} - {redirection}'
183
+ logger.warning(msg, location=(result.docname, result.lineno))
184
+ else:
185
+ colour = turquoise if result.code == 307 else purple
186
+ msg = colour('redirect ') + res_uri + colour(f' - {redirection}')
187
+ logger.info(msg)
188
+ self.write_entry(
189
+ f'redirected {text}',
190
+ result.docname,
191
+ filename,
192
+ result.lineno,
193
+ f'{res_uri} to {result.message}',
176
194
  )
177
- self.write_entry(
178
- 'redirected ' + text,
179
- result.docname,
180
- filename,
181
- result.lineno,
182
- result.uri + ' to ' + result.message,
183
- )
184
- else:
185
- raise ValueError('Unknown status %s.' % result.status)
195
+ case _Status.UNKNOWN:
196
+ msg = 'Unknown status.'
197
+ raise ValueError(msg)
186
198
 
187
- def write_linkstat(self, data: dict[str, str | int]) -> None:
199
+ def write_linkstat(self, data: dict[str, str | int | _Status]) -> None:
188
200
  self.json_outfile.write(json.dumps(data))
189
201
  self.json_outfile.write('\n')
190
202
 
191
203
  def write_entry(
192
- self, what: str, docname: str, filename: _StrPath, line: int, uri: str
204
+ self, what: _Status | str, docname: str, filename: _StrPath, line: int, uri: str
193
205
  ) -> None:
194
206
  self.txt_outfile.write(f'{filename}:{line}: [{what}] {uri}\n')
195
207
 
@@ -246,11 +258,11 @@ class HyperlinkCollector(SphinxPostTransform):
246
258
  :param uri: URI to add
247
259
  :param node: A node class where the URI was found
248
260
  """
249
- builder = cast(CheckExternalLinksBuilder, self.app.builder)
261
+ builder = cast('CheckExternalLinksBuilder', self.app.builder)
250
262
  hyperlinks = builder.hyperlinks
251
263
  docname = self.env.docname
252
264
 
253
- if newuri := self.app.emit_firstresult('linkcheck-process-uri', uri):
265
+ if newuri := self.app.events.emit_firstresult('linkcheck-process-uri', uri):
254
266
  uri = newuri
255
267
 
256
268
  try:
@@ -291,7 +303,12 @@ class HyperlinkAvailabilityChecker:
291
303
  for hyperlink in hyperlinks.values():
292
304
  if self.is_ignored_uri(hyperlink.uri):
293
305
  yield CheckResult(
294
- hyperlink.uri, hyperlink.docname, hyperlink.lineno, 'ignored', '', 0
306
+ uri=hyperlink.uri,
307
+ docname=hyperlink.docname,
308
+ lineno=hyperlink.lineno,
309
+ status=_Status.IGNORED,
310
+ message='',
311
+ code=0,
295
312
  )
296
313
  else:
297
314
  self.wqueue.put(CheckRequest(CHECK_IMMEDIATELY, hyperlink), False)
@@ -330,7 +347,7 @@ class CheckResult(NamedTuple):
330
347
  uri: str
331
348
  docname: str
332
349
  lineno: int
333
- status: str
350
+ status: _Status
334
351
  message: str
335
352
  code: int
336
353
 
@@ -373,16 +390,19 @@ class HyperlinkAvailabilityCheckWorker(Thread):
373
390
  self.retries: int = config.linkcheck_retries
374
391
  self.rate_limit_timeout = config.linkcheck_rate_limit_timeout
375
392
  self._allow_unauthorized = config.linkcheck_allow_unauthorized
393
+ self._timeout_status: Literal[_Status.BROKEN, _Status.TIMEOUT]
376
394
  if config.linkcheck_report_timeouts_as_broken:
377
- self._timeout_status = 'broken'
395
+ self._timeout_status = _Status.BROKEN
378
396
  else:
379
- self._timeout_status = 'timeout'
397
+ self._timeout_status = _Status.TIMEOUT
380
398
 
381
399
  self.user_agent = config.user_agent
382
400
  self.tls_verify = config.tls_verify
383
401
  self.tls_cacerts = config.tls_cacerts
384
402
 
385
- self._session = requests._Session()
403
+ self._session = requests._Session(
404
+ _ignored_redirects=tuple(map(re.compile, config.linkcheck_ignore))
405
+ )
386
406
 
387
407
  super().__init__(daemon=True)
388
408
 
@@ -413,17 +433,15 @@ class HyperlinkAvailabilityCheckWorker(Thread):
413
433
  self.wqueue.task_done()
414
434
  continue
415
435
  status, info, code = self._check(docname, uri, hyperlink)
416
- if status == 'rate-limited':
436
+ if status == _Status.RATE_LIMITED:
417
437
  logger.info(
418
- darkgray('-rate limited- ') + uri + darkgray(' | sleeping...')
438
+ darkgray('-rate limited- ') + uri + darkgray(' | sleeping...') # NoQA: G003
419
439
  )
420
440
  else:
421
441
  self.rqueue.put(CheckResult(uri, docname, lineno, status, info, code))
422
442
  self.wqueue.task_done()
423
443
 
424
- def _check(
425
- self, docname: str, uri: str, hyperlink: Hyperlink
426
- ) -> tuple[str, str, int]:
444
+ def _check(self, docname: str, uri: str, hyperlink: Hyperlink) -> _URIProperties:
427
445
  # check for various conditions without bothering the network
428
446
 
429
447
  for doc_matcher in self.documents_exclude:
@@ -432,25 +450,25 @@ class HyperlinkAvailabilityCheckWorker(Thread):
432
450
  f'{docname} matched {doc_matcher.pattern} from '
433
451
  'linkcheck_exclude_documents'
434
452
  )
435
- return 'ignored', info, 0
453
+ return _Status.IGNORED, info, 0
436
454
 
437
455
  if len(uri) == 0 or uri.startswith(('#', 'mailto:', 'tel:')):
438
- return 'unchecked', '', 0
456
+ return _Status.UNCHECKED, '', 0
439
457
  if not uri.startswith(('http:', 'https:')):
440
458
  if uri_re.match(uri):
441
459
  # Non-supported URI schemes (ex. ftp)
442
- return 'unchecked', '', 0
460
+ return _Status.UNCHECKED, '', 0
443
461
 
444
- src_dir = path.dirname(hyperlink.docpath)
445
- if path.exists(path.join(src_dir, uri)):
446
- return 'working', '', 0
447
- return 'broken', '', 0
462
+ if (hyperlink.docpath.parent / uri).exists():
463
+ return _Status.WORKING, '', 0
464
+ return _Status.BROKEN, '', 0
448
465
 
449
466
  # need to actually check the URI
450
- status, info, code = '', '', 0
467
+ status: _Status
468
+ status, info, code = _Status.UNKNOWN, '', 0
451
469
  for _ in range(self.retries):
452
470
  status, info, code = self._check_uri(uri, hyperlink)
453
- if status != 'broken':
471
+ if status != _Status.BROKEN:
454
472
  break
455
473
 
456
474
  return status, info, code
@@ -464,7 +482,7 @@ class HyperlinkAvailabilityCheckWorker(Thread):
464
482
  yield self._session.head, {'allow_redirects': True}
465
483
  yield self._session.get, {'stream': True}
466
484
 
467
- def _check_uri(self, uri: str, hyperlink: Hyperlink) -> tuple[str, str, int]:
485
+ def _check_uri(self, uri: str, hyperlink: Hyperlink) -> _URIProperties:
468
486
  req_url, delimiter, anchor = uri.partition('#')
469
487
  if delimiter and anchor:
470
488
  for rex in self.anchors_ignore:
@@ -519,10 +537,14 @@ class HyperlinkAvailabilityCheckWorker(Thread):
519
537
  try:
520
538
  found = contains_anchor(response, anchor)
521
539
  except UnicodeDecodeError:
522
- return 'ignored', 'unable to decode response content', 0
540
+ return (
541
+ _Status.IGNORED,
542
+ 'unable to decode response content',
543
+ 0,
544
+ )
523
545
  if not found:
524
546
  return (
525
- 'broken',
547
+ _Status.BROKEN,
526
548
  __("Anchor '%s' not found") % quote(anchor),
527
549
  0,
528
550
  )
@@ -531,7 +553,7 @@ class HyperlinkAvailabilityCheckWorker(Thread):
531
553
  status_code = response.status_code
532
554
  redirect_status_code = (
533
555
  response.history[-1].status_code if response.history else None
534
- ) # NoQA: E501
556
+ )
535
557
  retry_after = response.headers.get('Retry-After', '')
536
558
  response_url = f'{response.url}'
537
559
  response.raise_for_status()
@@ -543,7 +565,7 @@ class HyperlinkAvailabilityCheckWorker(Thread):
543
565
 
544
566
  except SSLError as err:
545
567
  # SSL failure; report that the link is broken.
546
- return 'broken', str(err), 0
568
+ return _Status.BROKEN, str(err), 0
547
569
 
548
570
  except (ConnectionError, TooManyRedirects) as err:
549
571
  # Servers drop the connection on HEAD requests, causing
@@ -551,24 +573,34 @@ class HyperlinkAvailabilityCheckWorker(Thread):
551
573
  error_message = str(err)
552
574
  continue
553
575
 
576
+ except requests._IgnoredRedirection as err:
577
+ # A redirection to an ignored URI was attempted; report it appropriately
578
+ return (
579
+ _Status.IGNORED,
580
+ f'ignored redirect: {err.destination}',
581
+ err.status_code,
582
+ )
583
+
554
584
  except HTTPError as err:
555
585
  error_message = str(err)
556
586
 
557
587
  # Unauthorized: the client did not provide required credentials
558
588
  if status_code == 401:
559
- status = 'working' if self._allow_unauthorized else 'broken'
560
- return status, 'unauthorized', 0
589
+ if self._allow_unauthorized:
590
+ return _Status.WORKING, 'unauthorized', 0
591
+ else:
592
+ return _Status.BROKEN, 'unauthorized', 0
561
593
 
562
594
  # Rate limiting; back-off if allowed, or report failure otherwise
563
595
  if status_code == 429:
564
596
  if next_check := self.limit_rate(response_url, retry_after):
565
597
  self.wqueue.put(CheckRequest(next_check, hyperlink), False)
566
- return 'rate-limited', '', 0
567
- return 'broken', error_message, 0
598
+ return _Status.RATE_LIMITED, '', 0
599
+ return _Status.BROKEN, error_message, 0
568
600
 
569
601
  # Don't claim success/failure during server-side outages
570
602
  if status_code == 503:
571
- return 'ignored', 'service unavailable', 0
603
+ return _Status.IGNORED, 'service unavailable', 0
572
604
 
573
605
  # For most HTTP failures, continue attempting alternate retrieval methods
574
606
  continue
@@ -576,12 +608,12 @@ class HyperlinkAvailabilityCheckWorker(Thread):
576
608
  except Exception as err:
577
609
  # Unhandled exception (intermittent or permanent); report that
578
610
  # the link is broken.
579
- return 'broken', str(err), 0
611
+ return _Status.BROKEN, str(err), 0
580
612
 
581
613
  else:
582
614
  # All available retrieval methods have been exhausted; report
583
615
  # that the link is broken.
584
- return 'broken', error_message, 0
616
+ return _Status.BROKEN, error_message, 0
585
617
 
586
618
  # Success; clear rate limits for the origin
587
619
  netloc = urlsplit(req_url).netloc
@@ -591,11 +623,11 @@ class HyperlinkAvailabilityCheckWorker(Thread):
591
623
  (response_url.rstrip('/') == req_url.rstrip('/'))
592
624
  or _allowed_redirect(req_url, response_url, self.allowed_redirects)
593
625
  ): # fmt: skip
594
- return 'working', '', 0
626
+ return _Status.WORKING, '', 0
595
627
  elif redirect_status_code is not None:
596
- return 'redirected', response_url, redirect_status_code
628
+ return _Status.REDIRECTED, response_url, redirect_status_code
597
629
  else:
598
- return 'redirected', response_url, 0
630
+ return _Status.REDIRECTED, response_url, 0
599
631
 
600
632
  def limit_rate(self, response_url: str, retry_after: str | None) -> float | None:
601
633
  delay = DEFAULT_DELAY
@@ -681,7 +713,7 @@ class AnchorCheckParser(HTMLParser):
681
713
 
682
714
  def handle_starttag(self, tag: Any, attrs: Any) -> None:
683
715
  for key, value in attrs:
684
- if key in ('id', 'name') and value == self.search_anchor:
716
+ if key in {'id', 'name'} and value == self.search_anchor:
685
717
  self.found = True
686
718
  break
687
719
 
@@ -736,29 +768,41 @@ def setup(app: Sphinx) -> ExtensionMetadata:
736
768
  app.add_builder(CheckExternalLinksBuilder)
737
769
  app.add_post_transform(HyperlinkCollector)
738
770
 
739
- app.add_config_value('linkcheck_ignore', [], '')
740
- app.add_config_value('linkcheck_exclude_documents', [], '')
741
- app.add_config_value('linkcheck_allowed_redirects', {}, '')
742
- app.add_config_value('linkcheck_auth', [], '')
743
- app.add_config_value('linkcheck_request_headers', {}, '')
744
- app.add_config_value('linkcheck_retries', 1, '')
745
- app.add_config_value('linkcheck_timeout', 30, '', (int, float))
746
- app.add_config_value('linkcheck_workers', 5, '')
747
- app.add_config_value('linkcheck_anchors', True, '')
771
+ app.add_config_value('linkcheck_ignore', [], '', types=frozenset({list, tuple}))
772
+ app.add_config_value(
773
+ 'linkcheck_exclude_documents', [], '', types=frozenset({list, tuple})
774
+ )
775
+ app.add_config_value('linkcheck_allowed_redirects', {}, '', types=frozenset({dict}))
776
+ app.add_config_value('linkcheck_auth', [], '', types=frozenset({list, tuple}))
777
+ app.add_config_value('linkcheck_request_headers', {}, '', types=frozenset({dict}))
778
+ app.add_config_value('linkcheck_retries', 1, '', types=frozenset({int}))
779
+ app.add_config_value('linkcheck_timeout', 30, '', types=frozenset({float, int}))
780
+ app.add_config_value('linkcheck_workers', 5, '', types=frozenset({int}))
781
+ app.add_config_value('linkcheck_anchors', True, '', types=frozenset({bool}))
748
782
  # Anchors starting with ! are ignored since they are
749
783
  # commonly used for dynamic pages
750
- app.add_config_value('linkcheck_anchors_ignore', ['^!'], '')
751
- app.add_config_value('linkcheck_anchors_ignore_for_url', (), '', (tuple, list))
752
- app.add_config_value('linkcheck_rate_limit_timeout', 300.0, '', (int, float))
753
- app.add_config_value('linkcheck_allow_unauthorized', False, '')
754
- app.add_config_value('linkcheck_report_timeouts_as_broken', False, '', bool)
784
+ app.add_config_value(
785
+ 'linkcheck_anchors_ignore', ['^!'], '', types=frozenset({list, tuple})
786
+ )
787
+ app.add_config_value(
788
+ 'linkcheck_anchors_ignore_for_url', (), '', types=frozenset({list, tuple})
789
+ )
790
+ app.add_config_value(
791
+ 'linkcheck_rate_limit_timeout', 300.0, '', types=frozenset({float, int})
792
+ )
793
+ app.add_config_value(
794
+ 'linkcheck_allow_unauthorized', False, '', types=frozenset({bool})
795
+ )
796
+ app.add_config_value(
797
+ 'linkcheck_report_timeouts_as_broken', False, '', types=frozenset({bool})
798
+ )
755
799
 
756
800
  app.add_event('linkcheck-process-uri')
757
801
 
758
802
  app.connect('config-inited', compile_linkcheck_allowed_redirects, priority=800)
759
803
 
760
804
  # FIXME: Disable URL rewrite handler for github.com temporarily.
761
- # ref: https://github.com/sphinx-doc/sphinx/issues/9435
805
+ # See: https://github.com/sphinx-doc/sphinx/issues/9435
762
806
  # app.connect('linkcheck-process-uri', rewrite_github_anchor)
763
807
 
764
808
  return {
@@ -3,17 +3,16 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import warnings
6
- from os import path
7
- from typing import TYPE_CHECKING, Any
6
+ from typing import TYPE_CHECKING
8
7
 
9
8
  from docutils.frontend import OptionParser
10
9
  from docutils.io import FileOutput
11
10
 
12
11
  from sphinx import addnodes
12
+ from sphinx._cli.util.colour import darkgreen
13
13
  from sphinx.builders import Builder
14
14
  from sphinx.locale import __
15
15
  from sphinx.util import logging
16
- from sphinx.util.console import darkgreen
17
16
  from sphinx.util.display import progress_message
18
17
  from sphinx.util.nodes import inline_all_toctrees
19
18
  from sphinx.util.osutil import ensuredir, make_filename_from_project
@@ -21,6 +20,7 @@ from sphinx.writers.manpage import ManualPageTranslator, ManualPageWriter
21
20
 
22
21
  if TYPE_CHECKING:
23
22
  from collections.abc import Set
23
+ from typing import Any
24
24
 
25
25
  from sphinx.application import Sphinx
26
26
  from sphinx.config import Config
@@ -30,9 +30,7 @@ logger = logging.getLogger(__name__)
30
30
 
31
31
 
32
32
  class ManualPageBuilder(Builder):
33
- """
34
- Builds groff output in manual page format.
35
- """
33
+ """Builds groff output in manual page format."""
36
34
 
37
35
  name = 'man'
38
36
  format = 'man'
@@ -44,10 +42,7 @@ class ManualPageBuilder(Builder):
44
42
  def init(self) -> None:
45
43
  if not self.config.man_pages:
46
44
  logger.warning(
47
- __(
48
- 'no "man_pages" config value found; no manual pages '
49
- 'will be written'
50
- )
45
+ __('no "man_pages" config value found; no manual pages will be written')
51
46
  )
52
47
 
53
48
  def get_outdated_docs(self) -> str | list[str]:
@@ -73,7 +68,7 @@ class ManualPageBuilder(Builder):
73
68
  docname, name, description, authors, section = info
74
69
  if docname not in self.env.all_docs:
75
70
  logger.warning(
76
- __('"man_pages" config value references unknown ' 'document %s'),
71
+ __('"man_pages" config value references unknown document %s'),
77
72
  docname,
78
73
  )
79
74
  continue
@@ -90,14 +85,15 @@ class ManualPageBuilder(Builder):
90
85
 
91
86
  if self.config.man_make_section_directory:
92
87
  dirname = 'man%s' % section
93
- ensuredir(path.join(self.outdir, dirname))
88
+ ensuredir(self.outdir / dirname)
94
89
  targetname = f'{dirname}/{name}.{section}'
95
90
  else:
96
91
  targetname = f'{name}.{section}'
97
92
 
98
- logger.info(darkgreen(targetname) + ' { ')
93
+ logger.info('%s { ', darkgreen(targetname))
99
94
  destination = FileOutput(
100
- destination_path=path.join(self.outdir, targetname), encoding='utf-8'
95
+ destination_path=self.outdir / targetname,
96
+ encoding='utf-8',
101
97
  )
102
98
 
103
99
  tree = self.env.get_doctree(docname)
@@ -135,9 +131,13 @@ def default_man_pages(config: Config) -> list[tuple[str, str, str, list[str], in
135
131
  def setup(app: Sphinx) -> ExtensionMetadata:
136
132
  app.add_builder(ManualPageBuilder)
137
133
 
138
- app.add_config_value('man_pages', default_man_pages, '')
139
- app.add_config_value('man_show_urls', False, '')
140
- app.add_config_value('man_make_section_directory', False, '')
134
+ app.add_config_value(
135
+ 'man_pages', default_man_pages, '', types=frozenset({list, tuple})
136
+ )
137
+ app.add_config_value('man_show_urls', False, '', types=frozenset({bool}))
138
+ app.add_config_value(
139
+ 'man_make_section_directory', False, '', types=frozenset({bool})
140
+ )
141
141
 
142
142
  return {
143
143
  'version': 'builtin',