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
sphinx/ext/doctest.py CHANGED
@@ -6,12 +6,12 @@ The extension automatically execute code snippets and checks their results.
6
6
  from __future__ import annotations
7
7
 
8
8
  import doctest
9
+ import os.path
9
10
  import re
10
11
  import sys
11
12
  import time
12
13
  from io import StringIO
13
- from os import path
14
- from typing import TYPE_CHECKING, Any, ClassVar
14
+ from typing import TYPE_CHECKING
15
15
 
16
16
  from docutils import nodes
17
17
  from docutils.parsers.rst import directives
@@ -19,15 +19,16 @@ from packaging.specifiers import InvalidSpecifier, SpecifierSet
19
19
  from packaging.version import Version
20
20
 
21
21
  import sphinx
22
+ from sphinx._cli.util.colour import bold
22
23
  from sphinx.builders import Builder
23
24
  from sphinx.locale import __
24
25
  from sphinx.util import logging
25
- from sphinx.util.console import bold
26
26
  from sphinx.util.docutils import SphinxDirective
27
27
  from sphinx.util.osutil import relpath
28
28
 
29
29
  if TYPE_CHECKING:
30
30
  from collections.abc import Callable, Set
31
+ from typing import Any, ClassVar
31
32
 
32
33
  from docutils.nodes import Element, Node, TextElement
33
34
 
@@ -38,7 +39,7 @@ if TYPE_CHECKING:
38
39
  logger = logging.getLogger(__name__)
39
40
 
40
41
  blankline_re = re.compile(r'^\s*<BLANKLINE>', re.MULTILINE)
41
- doctestopt_re = re.compile(r'#\s*doctest:.+$', re.MULTILINE)
42
+ doctestopt_re = re.compile(r'[ \t]*#\s*doctest:.+$', re.MULTILINE)
42
43
 
43
44
 
44
45
  def is_allowed_version(spec: str, version: str) -> bool:
@@ -61,10 +62,9 @@ def is_allowed_version(spec: str, version: str) -> bool:
61
62
 
62
63
  # set up the necessary directives
63
64
 
65
+
64
66
  class TestDirective(SphinxDirective):
65
- """
66
- Base class for doctest-related directives.
67
- """
67
+ """Base class for doctest-related directives."""
68
68
 
69
69
  has_content = True
70
70
  required_arguments = 0
@@ -81,12 +81,15 @@ class TestDirective(SphinxDirective):
81
81
  # convert <BLANKLINE>s to ordinary blank lines for presentation
82
82
  test = code
83
83
  code = blankline_re.sub('', code)
84
- if doctestopt_re.search(code) and 'no-trim-doctest-flags' not in self.options:
84
+ if (
85
+ doctestopt_re.search(code)
86
+ and 'no-trim-doctest-flags' not in self.options
87
+ ):
85
88
  if not test:
86
89
  test = code
87
90
  code = doctestopt_re.sub('', code)
88
91
  nodetype: type[TextElement] = nodes.literal_block
89
- if self.name in ('testsetup', 'testcleanup') or 'hide' in self.options:
92
+ if self.name in {'testsetup', 'testcleanup'} or 'hide' in self.options:
90
93
  nodetype = nodes.comment
91
94
  if self.arguments:
92
95
  groups = [x.strip() for x in self.arguments[0].split(',')]
@@ -105,7 +108,7 @@ class TestDirective(SphinxDirective):
105
108
  # don't try to highlight output
106
109
  node['language'] = 'none'
107
110
  node['options'] = {}
108
- if self.name in ('doctest', 'testoutput') and 'options' in self.options:
111
+ if self.name in {'doctest', 'testoutput'} and 'options' in self.options:
109
112
  # parse doctest-like output comparison flags
110
113
  option_strings = self.options['options'].replace(',', ' ').split()
111
114
  for option in option_strings:
@@ -113,15 +116,17 @@ class TestDirective(SphinxDirective):
113
116
  if prefix not in '+-':
114
117
  self.state.document.reporter.warning(
115
118
  __("missing '+' or '-' in '%s' option.") % option,
116
- line=self.lineno)
119
+ line=self.lineno,
120
+ )
117
121
  continue
118
122
  if option_name not in doctest.OPTIONFLAGS_BY_NAME:
119
123
  self.state.document.reporter.warning(
120
124
  __("'%s' is not a valid option.") % option_name,
121
- line=self.lineno)
125
+ line=self.lineno,
126
+ )
122
127
  continue
123
128
  flag = doctest.OPTIONFLAGS_BY_NAME[option[1:]]
124
- node['options'][flag] = (option[0] == '+')
129
+ node['options'][flag] = option[0] == '+'
125
130
  if self.name == 'doctest' and 'pyversion' in self.options:
126
131
  try:
127
132
  spec = self.options['pyversion']
@@ -131,8 +136,8 @@ class TestDirective(SphinxDirective):
131
136
  node['options'][flag] = True # Skip the test
132
137
  except InvalidSpecifier:
133
138
  self.state.document.reporter.warning(
134
- __("'%s' is not a valid pyversion option") % spec,
135
- line=self.lineno)
139
+ __("'%s' is not a valid pyversion option") % spec, line=self.lineno
140
+ )
136
141
  if 'skipif' in self.options:
137
142
  node['skipif'] = self.options['skipif']
138
143
  if 'trim-doctest-flags' in self.options:
@@ -191,6 +196,7 @@ parser = doctest.DocTestParser()
191
196
 
192
197
  # helper classes
193
198
 
199
+
194
200
  class TestGroup:
195
201
  def __init__(self, name: str) -> None:
196
202
  self.name = name
@@ -220,27 +226,38 @@ class TestGroup:
220
226
  raise RuntimeError(__('invalid TestCode type'))
221
227
 
222
228
  def __repr__(self) -> str:
223
- return (f'TestGroup(name={self.name!r}, setup={self.setup!r}, '
224
- f'cleanup={self.cleanup!r}, tests={self.tests!r})')
229
+ return (
230
+ f'TestGroup(name={self.name!r}, setup={self.setup!r}, '
231
+ f'cleanup={self.cleanup!r}, tests={self.tests!r})'
232
+ )
225
233
 
226
234
 
227
235
  class TestCode:
228
- def __init__(self, code: str, type: str, filename: str,
229
- lineno: int, options: dict | None = None) -> None:
236
+ def __init__(
237
+ self,
238
+ code: str,
239
+ type: str,
240
+ filename: str,
241
+ lineno: int,
242
+ options: dict[int, bool] | None = None,
243
+ ) -> None:
230
244
  self.code = code
231
245
  self.type = type
232
246
  self.filename = filename
233
247
  self.lineno = lineno
234
- self.options = options or {}
248
+ self.options: dict[int, bool] = options or {}
235
249
 
236
250
  def __repr__(self) -> str:
237
- return (f'TestCode({self.code!r}, {self.type!r}, filename={self.filename!r}, '
238
- f'lineno={self.lineno!r}, options={self.options!r})')
251
+ return (
252
+ f'TestCode({self.code!r}, {self.type!r}, filename={self.filename!r}, '
253
+ f'lineno={self.lineno!r}, options={self.options!r})'
254
+ )
239
255
 
240
256
 
241
257
  class SphinxDocTestRunner(doctest.DocTestRunner):
242
- def summarize(self, out: Callable, verbose: bool | None = None, # type: ignore[override]
243
- ) -> tuple[int, int]:
258
+ def summarize( # type: ignore[override]
259
+ self, out: Callable[[str], None], verbose: bool | None = None
260
+ ) -> tuple[int, int]:
244
261
  string_io = StringIO()
245
262
  old_stdout = sys.stdout
246
263
  sys.stdout = string_io
@@ -251,11 +268,11 @@ class SphinxDocTestRunner(doctest.DocTestRunner):
251
268
  out(string_io.getvalue())
252
269
  return res
253
270
 
254
- def _DocTestRunner__patched_linecache_getlines(self, filename: str,
255
- module_globals: Any = None) -> Any:
271
+ def _DocTestRunner__patched_linecache_getlines(
272
+ self, filename: str, module_globals: Any = None
273
+ ) -> Any:
256
274
  # this is overridden from DocTestRunner adding the try-except below
257
- m = self._DocTestRunner__LINECACHE_FILENAME_RE.match( # type: ignore[attr-defined]
258
- filename)
275
+ m = self._DocTestRunner__LINECACHE_FILENAME_RE.match(filename) # type: ignore[attr-defined]
259
276
  if m and m.group('name') == self.test.name:
260
277
  try:
261
278
  example = self.test.examples[int(m.group('examplenum'))]
@@ -266,20 +283,20 @@ class SphinxDocTestRunner(doctest.DocTestRunner):
266
283
  pass
267
284
  else:
268
285
  return example.source.splitlines(True)
269
- return self.save_linecache_getlines( # type: ignore[attr-defined]
270
- filename, module_globals)
286
+ return self.save_linecache_getlines(filename, module_globals) # type: ignore[attr-defined]
271
287
 
272
288
 
273
289
  # the new builder -- use sphinx-build.py -b doctest to run
274
290
 
291
+
275
292
  class DocTestBuilder(Builder):
276
- """
277
- Runs test snippets in the documentation.
278
- """
293
+ """Runs test snippets in the documentation."""
279
294
 
280
295
  name = 'doctest'
281
- epilog = __('Testing of doctests in the sources finished, look at the '
282
- 'results in %(outdir)s/output.txt.')
296
+ epilog = __(
297
+ 'Testing of doctests in the sources finished, look at the '
298
+ 'results in %(outdir)s/output.txt.'
299
+ )
283
300
 
284
301
  def init(self) -> None:
285
302
  # default options
@@ -306,10 +323,12 @@ class DocTestBuilder(Builder):
306
323
  date = time.strftime('%Y-%m-%d %H:%M:%S')
307
324
 
308
325
  outpath = self.outdir.joinpath('output.txt')
309
- self.outfile = outpath.open('w', encoding='utf-8') # NoQA: SIM115
310
- self.outfile.write(('Results of doctest builder run on %s\n'
311
- '==================================%s\n') %
312
- (date, '=' * len(date)))
326
+ self.outfile = outpath.open('w', encoding='utf-8')
327
+ line = '=' * len(date)
328
+ self.outfile.write(
329
+ f'Results of doctest builder run on {date}\n'
330
+ f'=================================={line}\n'
331
+ )
313
332
 
314
333
  def __del__(self) -> None:
315
334
  # free resources upon destruction (the file handler might not be
@@ -338,18 +357,17 @@ class DocTestBuilder(Builder):
338
357
  # write executive summary
339
358
  def s(v: int) -> str:
340
359
  return 's' if v != 1 else ''
341
- repl = (self.total_tries, s(self.total_tries),
342
- self.total_failures, s(self.total_failures),
343
- self.setup_failures, s(self.setup_failures),
344
- self.cleanup_failures, s(self.cleanup_failures))
345
- self._out('''
360
+
361
+ self._out(
362
+ f"""
346
363
  Doctest summary
347
364
  ===============
348
- %5d test%s
349
- %5d failure%s in tests
350
- %5d failure%s in setup code
351
- %5d failure%s in cleanup code
352
- ''' % repl)
365
+ {self.total_tries:5} test{s(self.total_tries)}
366
+ {self.total_failures:5} failure{s(self.total_failures)} in tests
367
+ {self.setup_failures:5} failure{s(self.setup_failures)} in setup code
368
+ {self.cleanup_failures:5} failure{s(self.cleanup_failures)} in cleanup code
369
+ """
370
+ )
353
371
  self.outfile.close()
354
372
 
355
373
  if self.total_failures or self.setup_failures or self.cleanup_failures:
@@ -367,17 +385,17 @@ Doctest summary
367
385
  filename of the document it's included in.
368
386
  """
369
387
  try:
370
- filename = relpath(node.source, self.env.srcdir).rsplit(':docstring of ', maxsplit=1)[0] # type: ignore[arg-type] # noqa: E501
388
+ filename = relpath(node.source, self.env.srcdir) # type: ignore[arg-type]
389
+ return filename.rsplit(':docstring of ', maxsplit=1)[0]
371
390
  except Exception:
372
- filename = str(self.env.doc2path(docname, False))
373
- return filename
391
+ return str(self.env.doc2path(docname, False))
374
392
 
375
393
  @staticmethod
376
394
  def get_line_number(node: Node) -> int | None:
377
395
  """Get the real line number or admit we don't know."""
378
396
  # TODO: Work out how to store or calculate real (file-relative)
379
397
  # line numbers for doctest blocks in docstrings.
380
- if ':docstring of ' in path.basename(node.source or ''):
398
+ if ':docstring of ' in os.path.basename(node.source or ''):
381
399
  # The line number is given relative to the stripped docstring,
382
400
  # not the file. This is correct where it is set, in
383
401
  # `docutils.nodes.Node.setup_child`, but Sphinx should report
@@ -404,25 +422,29 @@ Doctest summary
404
422
  def test_doc(self, docname: str, doctree: Node) -> None:
405
423
  groups: dict[str, TestGroup] = {}
406
424
  add_to_all_groups = []
407
- self.setup_runner = SphinxDocTestRunner(verbose=False,
408
- optionflags=self.opt)
409
- self.test_runner = SphinxDocTestRunner(verbose=False,
410
- optionflags=self.opt)
411
- self.cleanup_runner = SphinxDocTestRunner(verbose=False,
412
- optionflags=self.opt)
425
+ self.setup_runner = SphinxDocTestRunner(verbose=False, optionflags=self.opt)
426
+ self.test_runner = SphinxDocTestRunner(verbose=False, optionflags=self.opt)
427
+ self.cleanup_runner = SphinxDocTestRunner(verbose=False, optionflags=self.opt)
413
428
 
414
429
  self.test_runner._fakeout = self.setup_runner._fakeout # type: ignore[attr-defined]
415
430
  self.cleanup_runner._fakeout = self.setup_runner._fakeout # type: ignore[attr-defined]
416
431
 
417
432
  if self.config.doctest_test_doctest_blocks:
433
+
418
434
  def condition(node: Node) -> bool:
419
- return (isinstance(node, nodes.literal_block | nodes.comment) and
420
- 'testnodetype' in node) or \
421
- isinstance(node, nodes.doctest_block)
435
+ return (
436
+ isinstance(node, nodes.literal_block | nodes.comment)
437
+ and 'testnodetype' in node
438
+ ) or isinstance(node, nodes.doctest_block)
439
+
422
440
  else:
441
+
423
442
  def condition(node: Node) -> bool:
424
- return isinstance(node, nodes.literal_block | nodes.comment) \
443
+ return (
444
+ isinstance(node, nodes.literal_block | nodes.comment)
425
445
  and 'testnodetype' in node
446
+ )
447
+
426
448
  for node in doctree.findall(condition):
427
449
  if self.skipped(node): # type: ignore[arg-type]
428
450
  continue
@@ -431,12 +453,19 @@ Doctest summary
431
453
  filename = self.get_filename_for_node(node, docname)
432
454
  line_number = self.get_line_number(node)
433
455
  if not source:
434
- logger.warning(__('no code/output in %s block at %s:%s'),
435
- node.get('testnodetype', 'doctest'), # type: ignore[attr-defined]
436
- filename, line_number)
437
- code = TestCode(source, type=node.get('testnodetype', 'doctest'), # type: ignore[attr-defined]
438
- filename=filename, lineno=line_number, # type: ignore[arg-type]
439
- options=node.get('options')) # type: ignore[attr-defined]
456
+ logger.warning(
457
+ __('no code/output in %s block at %s:%s'),
458
+ node.get('testnodetype', 'doctest'), # type: ignore[attr-defined]
459
+ filename,
460
+ line_number,
461
+ )
462
+ code = TestCode(
463
+ source,
464
+ type=node.get('testnodetype', 'doctest'), # type: ignore[attr-defined]
465
+ filename=filename,
466
+ lineno=line_number, # type: ignore[arg-type]
467
+ options=node.get('options'), # type: ignore[attr-defined]
468
+ )
440
469
  node_groups = node.get('groups', ['default']) # type: ignore[attr-defined]
441
470
  if '*' in node_groups:
442
471
  add_to_all_groups.append(code)
@@ -449,13 +478,21 @@ Doctest summary
449
478
  for group in groups.values():
450
479
  group.add_code(code)
451
480
  if self.config.doctest_global_setup:
452
- code = TestCode(self.config.doctest_global_setup,
453
- 'testsetup', filename='<global_setup>', lineno=0)
481
+ code = TestCode(
482
+ self.config.doctest_global_setup,
483
+ 'testsetup',
484
+ filename='<global_setup>',
485
+ lineno=0,
486
+ )
454
487
  for group in groups.values():
455
488
  group.add_code(code, prepend=True)
456
489
  if self.config.doctest_global_cleanup:
457
- code = TestCode(self.config.doctest_global_cleanup,
458
- 'testcleanup', filename='<global_cleanup>', lineno=0)
490
+ code = TestCode(
491
+ self.config.doctest_global_cleanup,
492
+ 'testcleanup',
493
+ filename='<global_cleanup>',
494
+ lineno=0,
495
+ )
459
496
  for group in groups.values():
460
497
  group.add_code(code)
461
498
  if not groups:
@@ -463,9 +500,7 @@ Doctest summary
463
500
 
464
501
  show_successes = self.config.doctest_show_successes
465
502
  if show_successes:
466
- self._out('\n'
467
- f'Document: {docname}\n'
468
- f'----------{"-" * len(docname)}\n')
503
+ self._out(f'\nDocument: {docname}\n----------{"-" * len(docname)}\n')
469
504
  for group in groups.values():
470
505
  self.test_group(group)
471
506
  # Separately count results from setup code
@@ -473,23 +508,27 @@ Doctest summary
473
508
  self.setup_failures += res_f
474
509
  self.setup_tries += res_t
475
510
  if self.test_runner.tries:
476
- res_f, res_t = self.test_runner.summarize(
477
- self._out, verbose=show_successes)
511
+ res_f, res_t = self.test_runner.summarize(self._out, verbose=show_successes)
478
512
  self.total_failures += res_f
479
513
  self.total_tries += res_t
480
514
  if self.cleanup_runner.tries:
481
515
  res_f, res_t = self.cleanup_runner.summarize(
482
- self._out, verbose=show_successes)
516
+ self._out, verbose=show_successes
517
+ )
483
518
  self.cleanup_failures += res_f
484
519
  self.cleanup_tries += res_t
485
520
 
486
- def compile(self, code: str, name: str, type: str, flags: Any, dont_inherit: bool) -> Any:
521
+ def compile(
522
+ self, code: str, name: str, type: str, flags: Any, dont_inherit: bool
523
+ ) -> Any:
487
524
  return compile(code, name, self.type, flags, dont_inherit)
488
525
 
489
526
  def test_group(self, group: TestGroup) -> None:
490
- ns: dict = {}
527
+ ns: dict[str, Any] = {}
491
528
 
492
- def run_setup_cleanup(runner: Any, testcodes: list[TestCode], what: Any) -> bool:
529
+ def run_setup_cleanup(
530
+ runner: Any, testcodes: list[TestCode], what: Any
531
+ ) -> bool:
493
532
  examples = []
494
533
  for testcode in testcodes:
495
534
  example = doctest.Example(testcode.code, '', lineno=testcode.lineno)
@@ -497,9 +536,14 @@ Doctest summary
497
536
  if not examples:
498
537
  return True
499
538
  # simulate a doctest with the code
500
- sim_doctest = doctest.DocTest(examples, {},
501
- f'{group.name} ({what} code)',
502
- testcodes[0].filename, 0, None)
539
+ sim_doctest = doctest.DocTest(
540
+ examples,
541
+ {},
542
+ f'{group.name} ({what} code)',
543
+ testcodes[0].filename,
544
+ 0,
545
+ None,
546
+ )
503
547
  sim_doctest.globs = ns
504
548
  old_f = runner.failures
505
549
  self.type = 'exec' # the snippet may contain multiple statements
@@ -516,11 +560,15 @@ Doctest summary
516
560
  if len(code) == 1:
517
561
  # ordinary doctests (code/output interleaved)
518
562
  try:
519
- test = parser.get_doctest(code[0].code, {}, group.name,
520
- code[0].filename, code[0].lineno)
563
+ test = parser.get_doctest(
564
+ code[0].code, {}, group.name, code[0].filename, code[0].lineno
565
+ )
521
566
  except Exception:
522
- logger.warning(__('ignoring invalid doctest code: %r'), code[0].code,
523
- location=(code[0].filename, code[0].lineno))
567
+ logger.warning(
568
+ __('ignoring invalid doctest code: %r'),
569
+ code[0].code,
570
+ location=(code[0].filename, code[0].lineno),
571
+ )
524
572
  continue
525
573
  if not test.examples:
526
574
  continue
@@ -542,10 +590,21 @@ Doctest summary
542
590
  exc_msg = m.group('msg')
543
591
  else:
544
592
  exc_msg = None
545
- example = doctest.Example(code[0].code, output, exc_msg=exc_msg,
546
- lineno=code[0].lineno, options=options)
547
- test = doctest.DocTest([example], {}, group.name,
548
- code[0].filename, code[0].lineno, None)
593
+ example = doctest.Example(
594
+ code[0].code,
595
+ output,
596
+ exc_msg=exc_msg,
597
+ lineno=code[0].lineno,
598
+ options=options,
599
+ )
600
+ test = doctest.DocTest(
601
+ [example],
602
+ {},
603
+ group.name,
604
+ code[0].filename,
605
+ code[0].lineno,
606
+ None,
607
+ )
549
608
  self.type = 'exec' # multiple statements again
550
609
  # DocTest.__init__ copies the globs namespace, which we don't want
551
610
  test.globs = ns
@@ -564,13 +623,22 @@ def setup(app: Sphinx) -> ExtensionMetadata:
564
623
  app.add_directive('testoutput', TestoutputDirective)
565
624
  app.add_builder(DocTestBuilder)
566
625
  # this config value adds to sys.path
567
- app.add_config_value('doctest_show_successes', True, '', bool)
568
- app.add_config_value('doctest_path', [], '')
569
- app.add_config_value('doctest_test_doctest_blocks', 'default', '')
570
- app.add_config_value('doctest_global_setup', '', '')
571
- app.add_config_value('doctest_global_cleanup', '', '')
626
+ app.add_config_value('doctest_show_successes', True, '', types=frozenset({bool}))
627
+ app.add_config_value('doctest_path', (), '', types=frozenset({list, tuple}))
628
+ app.add_config_value(
629
+ 'doctest_test_doctest_blocks', 'default', '', types=frozenset({str})
630
+ )
631
+ app.add_config_value('doctest_global_setup', '', '', types=frozenset({str}))
632
+ app.add_config_value('doctest_global_cleanup', '', '', types=frozenset({str}))
572
633
  app.add_config_value(
573
634
  'doctest_default_flags',
574
- doctest.DONT_ACCEPT_TRUE_FOR_1 | doctest.ELLIPSIS | doctest.IGNORE_EXCEPTION_DETAIL,
575
- '')
576
- return {'version': sphinx.__display_version__, 'parallel_read_safe': True}
635
+ doctest.DONT_ACCEPT_TRUE_FOR_1
636
+ | doctest.ELLIPSIS
637
+ | doctest.IGNORE_EXCEPTION_DETAIL,
638
+ '',
639
+ types=frozenset({int}),
640
+ )
641
+ return {
642
+ 'version': sphinx.__display_version__,
643
+ 'parallel_read_safe': True,
644
+ }
sphinx/ext/duration.py CHANGED
@@ -23,6 +23,7 @@ if TYPE_CHECKING:
23
23
  class _DurationDomainData(TypedDict):
24
24
  reading_durations: dict[str, float]
25
25
 
26
+
26
27
  logger = logging.getLogger(__name__)
27
28
 
28
29
 
@@ -44,7 +45,9 @@ class DurationDomain(Domain):
44
45
  def clear_doc(self, docname: str) -> None:
45
46
  self.reading_durations.pop(docname, None)
46
47
 
47
- def merge_domaindata(self, docnames: Set[str], otherdata: _DurationDomainData) -> None: # type: ignore[override]
48
+ def merge_domaindata( # type: ignore[override]
49
+ self, docnames: Set[str], otherdata: _DurationDomainData
50
+ ) -> None:
48
51
  other_reading_durations = otherdata.get('reading_durations', {})
49
52
  docnames_set = frozenset(docnames)
50
53
  for docname, duration in other_reading_durations.items():
@@ -63,13 +66,12 @@ def on_builder_inited(app: Sphinx) -> None:
63
66
 
64
67
  def on_source_read(app: Sphinx, docname: str, content: list[str]) -> None:
65
68
  """Start to measure reading duration."""
66
- app.env.temp_data['started_at'] = time.monotonic()
69
+ app.env.current_document.reading_started_at = time.monotonic()
67
70
 
68
71
 
69
72
  def on_doctree_read(app: Sphinx, doctree: nodes.document) -> None:
70
73
  """Record a reading duration."""
71
- started_at = app.env.temp_data['started_at']
72
- duration = time.monotonic() - started_at
74
+ duration = time.monotonic() - app.env.current_document.reading_started_at
73
75
  domain = app.env.domains['duration']
74
76
  domain.note_reading_duration(duration)
75
77
 
@@ -79,10 +81,14 @@ def on_build_finished(app: Sphinx, error: Exception) -> None:
79
81
  domain = app.env.domains['duration']
80
82
  if not domain.reading_durations:
81
83
  return
82
- durations = sorted(domain.reading_durations.items(), key=itemgetter(1), reverse=True)
84
+ durations = sorted(
85
+ domain.reading_durations.items(), key=itemgetter(1), reverse=True
86
+ )
83
87
 
84
88
  logger.info('')
85
- logger.info(__('====================== slowest reading durations ======================='))
89
+ logger.info(
90
+ __('====================== slowest reading durations =======================')
91
+ )
86
92
  for docname, d in islice(durations, 5):
87
93
  logger.info(f'{d:.3f} {docname}') # NoQA: G004
88
94
 
sphinx/ext/extlinks.py CHANGED
@@ -20,7 +20,7 @@ Both, the url string and the caption string must escape ``%`` as ``%%``.
20
20
  from __future__ import annotations
21
21
 
22
22
  import re
23
- from typing import TYPE_CHECKING, Any
23
+ from typing import TYPE_CHECKING
24
24
 
25
25
  from docutils import nodes, utils
26
26
 
@@ -32,6 +32,7 @@ from sphinx.util.nodes import split_explicit_title
32
32
 
33
33
  if TYPE_CHECKING:
34
34
  from collections.abc import Sequence
35
+ from typing import Any
35
36
 
36
37
  from docutils.nodes import Node, system_message
37
38
  from docutils.parsers.rst.states import Inliner
@@ -43,8 +44,7 @@ logger = logging.getLogger(__name__)
43
44
 
44
45
 
45
46
  class ExternalLinksChecker(SphinxPostTransform):
46
- """
47
- For each external link, check if it can be replaced by an extlink.
47
+ """For each external link, check if it can be replaced by an extlink.
48
48
 
49
49
  We treat each ``reference`` node without ``internal`` attribute as an external link.
50
50
  """
@@ -59,8 +59,7 @@ class ExternalLinksChecker(SphinxPostTransform):
59
59
  self.check_uri(refnode)
60
60
 
61
61
  def check_uri(self, refnode: nodes.reference) -> None:
62
- """
63
- If the URI in ``refnode`` has a replacement in ``extlinks``,
62
+ """If the URI in ``refnode`` has a replacement in ``extlinks``,
64
63
  emit a warning with a replacement suggestion.
65
64
  """
66
65
  if 'internal' in refnode or 'refuri' not in refnode:
@@ -74,31 +73,38 @@ class ExternalLinksChecker(SphinxPostTransform):
74
73
 
75
74
  match = uri_pattern.match(uri)
76
75
  if (
77
- match and
78
- match.groupdict().get('value') and
79
- '/' not in match.groupdict()['value']
76
+ match
77
+ and match.groupdict().get('value')
78
+ and '/' not in match.groupdict()['value']
80
79
  ):
81
80
  # build a replacement suggestion
82
- msg = __('hardcoded link %r could be replaced by an extlink '
83
- '(try using %r instead)')
81
+ msg = __(
82
+ 'hardcoded link %r could be replaced by an extlink '
83
+ '(try using %r instead)'
84
+ )
84
85
  value = match.groupdict().get('value')
85
86
  if uri != title:
86
- replacement = f":{alias}:`{rst.escape(title)} <{value}>`"
87
+ replacement = f':{alias}:`{rst.escape(title)} <{value}>`'
87
88
  else:
88
- replacement = f":{alias}:`{value}`"
89
+ replacement = f':{alias}:`{value}`'
89
90
  logger.warning(msg, uri, replacement, location=refnode)
90
91
 
91
92
 
92
- def make_link_role(name: str, base_url: str, caption: str) -> RoleFunction:
93
+ def make_link_role(name: str, base_url: str, caption: str | None) -> RoleFunction:
93
94
  # Check whether we have base_url and caption strings have an '%s' for
94
95
  # expansion. If not, fall back to the old behaviour and use the string as
95
96
  # a prefix.
96
97
  # Remark: It is an implementation detail that we use Python's %-formatting.
97
98
  # So far we only expose ``%s`` and require quoting of ``%`` using ``%%``.
98
- def role(typ: str, rawtext: str, text: str, lineno: int,
99
- inliner: Inliner, options: dict[str, Any] | None = None,
100
- content: Sequence[str] = (),
101
- ) -> tuple[list[Node], list[system_message]]:
99
+ def role(
100
+ typ: str,
101
+ rawtext: str,
102
+ text: str,
103
+ lineno: int,
104
+ inliner: Inliner,
105
+ options: dict[str, Any] | None = None,
106
+ content: Sequence[str] = (),
107
+ ) -> tuple[list[Node], list[system_message]]:
102
108
  text = utils.unescape(text)
103
109
  has_explicit_title, title, part = split_explicit_title(text)
104
110
  full_url = base_url % part
@@ -108,8 +114,9 @@ def make_link_role(name: str, base_url: str, caption: str) -> RoleFunction:
108
114
  else:
109
115
  title = caption % part
110
116
  pnode = nodes.reference(title, title, internal=False, refuri=full_url)
111
- pnode["classes"].append(f"extlink-{name}")
117
+ pnode['classes'].append(f'extlink-{name}')
112
118
  return [pnode], []
119
+
113
120
  return role
114
121
 
115
122
 
@@ -119,9 +126,14 @@ def setup_link_roles(app: Sphinx) -> None:
119
126
 
120
127
 
121
128
  def setup(app: Sphinx) -> ExtensionMetadata:
122
- app.add_config_value('extlinks', {}, 'env')
123
- app.add_config_value('extlinks_detect_hardcoded_links', False, 'env')
129
+ app.add_config_value('extlinks', {}, 'env', types=frozenset({dict}))
130
+ app.add_config_value(
131
+ 'extlinks_detect_hardcoded_links', False, 'env', types=frozenset({bool})
132
+ )
124
133
 
125
134
  app.connect('builder-inited', setup_link_roles)
126
135
  app.add_post_transform(ExternalLinksChecker)
127
- return {'version': sphinx.__display_version__, 'parallel_read_safe': True}
136
+ return {
137
+ 'version': sphinx.__display_version__,
138
+ 'parallel_read_safe': True,
139
+ }