Sphinx 7.4.6__py3-none-any.whl → 8.0.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 (242) hide show
  1. sphinx/__init__.py +2 -2
  2. sphinx/_cli/__init__.py +4 -4
  3. sphinx/application.py +2 -2
  4. sphinx/builders/__init__.py +2 -3
  5. sphinx/builders/_epub_base.py +33 -12
  6. sphinx/builders/changes.py +13 -5
  7. sphinx/builders/epub3.py +6 -2
  8. sphinx/builders/html/__init__.py +90 -59
  9. sphinx/builders/latex/__init__.py +38 -12
  10. sphinx/builders/latex/transforms.py +1 -1
  11. sphinx/builders/linkcheck.py +8 -49
  12. sphinx/builders/texinfo.py +12 -6
  13. sphinx/builders/text.py +7 -3
  14. sphinx/builders/xml.py +7 -3
  15. sphinx/cmd/quickstart.py +10 -20
  16. sphinx/config.py +13 -13
  17. sphinx/deprecation.py +8 -8
  18. sphinx/directives/__init__.py +14 -9
  19. sphinx/directives/other.py +2 -3
  20. sphinx/directives/patches.py +2 -2
  21. sphinx/domains/__init__.py +4 -2
  22. sphinx/domains/c/__init__.py +2 -2
  23. sphinx/domains/c/_ast.py +3 -2
  24. sphinx/domains/c/_parser.py +4 -3
  25. sphinx/domains/cpp/__init__.py +2 -2
  26. sphinx/domains/cpp/_ast.py +1 -2
  27. sphinx/domains/cpp/_parser.py +2 -2
  28. sphinx/domains/cpp/_symbol.py +2 -2
  29. sphinx/domains/javascript.py +1 -1
  30. sphinx/domains/math.py +1 -1
  31. sphinx/domains/python/__init__.py +1 -1
  32. sphinx/domains/python/_annotations.py +23 -1
  33. sphinx/domains/python/_object.py +0 -1
  34. sphinx/domains/std/__init__.py +7 -8
  35. sphinx/environment/__init__.py +15 -32
  36. sphinx/environment/adapters/indexentries.py +4 -6
  37. sphinx/environment/adapters/toctree.py +4 -4
  38. sphinx/environment/collectors/title.py +1 -1
  39. sphinx/environment/collectors/toctree.py +1 -1
  40. sphinx/events.py +3 -1
  41. sphinx/ext/autodoc/__init__.py +25 -67
  42. sphinx/ext/autodoc/directive.py +7 -5
  43. sphinx/ext/autodoc/importer.py +2 -1
  44. sphinx/ext/autodoc/preserve_defaults.py +2 -2
  45. sphinx/ext/autosummary/__init__.py +15 -7
  46. sphinx/ext/autosummary/generate.py +5 -4
  47. sphinx/ext/doctest.py +5 -5
  48. sphinx/ext/graphviz.py +1 -1
  49. sphinx/ext/imgmath.py +1 -1
  50. sphinx/ext/inheritance_diagram.py +1 -1
  51. sphinx/ext/intersphinx/__init__.py +25 -5
  52. sphinx/ext/intersphinx/_cli.py +7 -6
  53. sphinx/ext/intersphinx/_load.py +240 -115
  54. sphinx/ext/intersphinx/_resolve.py +12 -11
  55. sphinx/ext/intersphinx/_shared.py +102 -9
  56. sphinx/ext/mathjax.py +1 -1
  57. sphinx/ext/napoleon/docstring.py +2 -2
  58. sphinx/ext/todo.py +2 -2
  59. sphinx/ext/viewcode.py +2 -1
  60. sphinx/highlighting.py +3 -3
  61. sphinx/io.py +2 -2
  62. sphinx/jinja2glue.py +13 -6
  63. sphinx/locale/__init__.py +4 -3
  64. sphinx/locale/ar/LC_MESSAGES/sphinx.mo +0 -0
  65. sphinx/locale/ar/LC_MESSAGES/sphinx.po +2383 -2186
  66. sphinx/locale/bg/LC_MESSAGES/sphinx.mo +0 -0
  67. sphinx/locale/bg/LC_MESSAGES/sphinx.po +2249 -2052
  68. sphinx/locale/bn/LC_MESSAGES/sphinx.mo +0 -0
  69. sphinx/locale/bn/LC_MESSAGES/sphinx.po +2412 -2215
  70. sphinx/locale/ca/LC_MESSAGES/sphinx.mo +0 -0
  71. sphinx/locale/ca/LC_MESSAGES/sphinx.po +3029 -2832
  72. sphinx/locale/cak/LC_MESSAGES/sphinx.mo +0 -0
  73. sphinx/locale/cak/LC_MESSAGES/sphinx.po +2308 -2111
  74. sphinx/locale/cs/LC_MESSAGES/sphinx.mo +0 -0
  75. sphinx/locale/cs/LC_MESSAGES/sphinx.po +2469 -2272
  76. sphinx/locale/cy/LC_MESSAGES/sphinx.mo +0 -0
  77. sphinx/locale/cy/LC_MESSAGES/sphinx.po +2393 -2196
  78. sphinx/locale/da/LC_MESSAGES/sphinx.mo +0 -0
  79. sphinx/locale/da/LC_MESSAGES/sphinx.po +2532 -2335
  80. sphinx/locale/de/LC_MESSAGES/sphinx.mo +0 -0
  81. sphinx/locale/de/LC_MESSAGES/sphinx.po +2492 -2295
  82. sphinx/locale/de_DE/LC_MESSAGES/sphinx.mo +0 -0
  83. sphinx/locale/de_DE/LC_MESSAGES/sphinx.po +2250 -2053
  84. sphinx/locale/el/LC_MESSAGES/sphinx.mo +0 -0
  85. sphinx/locale/el/LC_MESSAGES/sphinx.po +2879 -2682
  86. sphinx/locale/en_DE/LC_MESSAGES/sphinx.mo +0 -0
  87. sphinx/locale/en_DE/LC_MESSAGES/sphinx.po +2250 -2053
  88. sphinx/locale/en_FR/LC_MESSAGES/sphinx.mo +0 -0
  89. sphinx/locale/en_FR/LC_MESSAGES/sphinx.po +2250 -2053
  90. sphinx/locale/en_GB/LC_MESSAGES/sphinx.mo +0 -0
  91. sphinx/locale/en_GB/LC_MESSAGES/sphinx.po +2989 -2792
  92. sphinx/locale/en_HK/LC_MESSAGES/sphinx.mo +0 -0
  93. sphinx/locale/en_HK/LC_MESSAGES/sphinx.po +2250 -2053
  94. sphinx/locale/eo/LC_MESSAGES/sphinx.mo +0 -0
  95. sphinx/locale/eo/LC_MESSAGES/sphinx.po +2297 -2100
  96. sphinx/locale/es/LC_MESSAGES/sphinx.mo +0 -0
  97. sphinx/locale/es/LC_MESSAGES/sphinx.po +3017 -2820
  98. sphinx/locale/es_CO/LC_MESSAGES/sphinx.mo +0 -0
  99. sphinx/locale/es_CO/LC_MESSAGES/sphinx.po +2250 -2053
  100. sphinx/locale/et/LC_MESSAGES/sphinx.mo +0 -0
  101. sphinx/locale/et/LC_MESSAGES/sphinx.po +2748 -2551
  102. sphinx/locale/eu/LC_MESSAGES/sphinx.mo +0 -0
  103. sphinx/locale/eu/LC_MESSAGES/sphinx.po +2459 -2262
  104. sphinx/locale/fa/LC_MESSAGES/sphinx.mo +0 -0
  105. sphinx/locale/fa/LC_MESSAGES/sphinx.po +2957 -2760
  106. sphinx/locale/fi/LC_MESSAGES/sphinx.mo +0 -0
  107. sphinx/locale/fi/LC_MESSAGES/sphinx.po +2321 -2124
  108. sphinx/locale/fr/LC_MESSAGES/sphinx.mo +0 -0
  109. sphinx/locale/fr/LC_MESSAGES/sphinx.po +2977 -2780
  110. sphinx/locale/fr_FR/LC_MESSAGES/sphinx.mo +0 -0
  111. sphinx/locale/fr_FR/LC_MESSAGES/sphinx.po +2250 -2053
  112. sphinx/locale/gl/LC_MESSAGES/sphinx.mo +0 -0
  113. sphinx/locale/gl/LC_MESSAGES/sphinx.po +2992 -2795
  114. sphinx/locale/he/LC_MESSAGES/sphinx.mo +0 -0
  115. sphinx/locale/he/LC_MESSAGES/sphinx.po +2375 -2178
  116. sphinx/locale/hi/LC_MESSAGES/sphinx.mo +0 -0
  117. sphinx/locale/hi/LC_MESSAGES/sphinx.po +2937 -2740
  118. sphinx/locale/hi_IN/LC_MESSAGES/sphinx.mo +0 -0
  119. sphinx/locale/hi_IN/LC_MESSAGES/sphinx.po +2250 -2053
  120. sphinx/locale/hr/LC_MESSAGES/sphinx.mo +0 -0
  121. sphinx/locale/hr/LC_MESSAGES/sphinx.po +2532 -2335
  122. sphinx/locale/hu/LC_MESSAGES/sphinx.mo +0 -0
  123. sphinx/locale/hu/LC_MESSAGES/sphinx.po +2505 -2308
  124. sphinx/locale/id/LC_MESSAGES/sphinx.mo +0 -0
  125. sphinx/locale/id/LC_MESSAGES/sphinx.po +2925 -2728
  126. sphinx/locale/is/LC_MESSAGES/sphinx.mo +0 -0
  127. sphinx/locale/is/LC_MESSAGES/sphinx.po +2307 -2110
  128. sphinx/locale/it/LC_MESSAGES/sphinx.mo +0 -0
  129. sphinx/locale/it/LC_MESSAGES/sphinx.po +2514 -2317
  130. sphinx/locale/ja/LC_MESSAGES/sphinx.mo +0 -0
  131. sphinx/locale/ja/LC_MESSAGES/sphinx.po +2970 -2773
  132. sphinx/locale/ka/LC_MESSAGES/sphinx.mo +0 -0
  133. sphinx/locale/ka/LC_MESSAGES/sphinx.po +2868 -2671
  134. sphinx/locale/ko/LC_MESSAGES/sphinx.mo +0 -0
  135. sphinx/locale/ko/LC_MESSAGES/sphinx.po +3016 -2819
  136. sphinx/locale/lt/LC_MESSAGES/sphinx.mo +0 -0
  137. sphinx/locale/lt/LC_MESSAGES/sphinx.po +2476 -2279
  138. sphinx/locale/lv/LC_MESSAGES/sphinx.mo +0 -0
  139. sphinx/locale/lv/LC_MESSAGES/sphinx.po +2477 -2280
  140. sphinx/locale/mk/LC_MESSAGES/sphinx.mo +0 -0
  141. sphinx/locale/mk/LC_MESSAGES/sphinx.po +2292 -2095
  142. sphinx/locale/nb_NO/LC_MESSAGES/sphinx.mo +0 -0
  143. sphinx/locale/nb_NO/LC_MESSAGES/sphinx.po +2479 -2282
  144. sphinx/locale/ne/LC_MESSAGES/sphinx.mo +0 -0
  145. sphinx/locale/ne/LC_MESSAGES/sphinx.po +2481 -2284
  146. sphinx/locale/nl/LC_MESSAGES/sphinx.mo +0 -0
  147. sphinx/locale/nl/LC_MESSAGES/sphinx.po +2557 -2360
  148. sphinx/locale/pl/LC_MESSAGES/sphinx.mo +0 -0
  149. sphinx/locale/pl/LC_MESSAGES/sphinx.po +2696 -2499
  150. sphinx/locale/pt/LC_MESSAGES/sphinx.mo +0 -0
  151. sphinx/locale/pt/LC_MESSAGES/sphinx.po +2250 -2053
  152. sphinx/locale/pt_BR/LC_MESSAGES/sphinx.mo +0 -0
  153. sphinx/locale/pt_BR/LC_MESSAGES/sphinx.po +2979 -2782
  154. sphinx/locale/pt_PT/LC_MESSAGES/sphinx.mo +0 -0
  155. sphinx/locale/pt_PT/LC_MESSAGES/sphinx.po +2469 -2272
  156. sphinx/locale/ro/LC_MESSAGES/sphinx.mo +0 -0
  157. sphinx/locale/ro/LC_MESSAGES/sphinx.po +2473 -2276
  158. sphinx/locale/ru/LC_MESSAGES/sphinx.mo +0 -0
  159. sphinx/locale/ru/LC_MESSAGES/sphinx.po +2746 -2549
  160. sphinx/locale/si/LC_MESSAGES/sphinx.mo +0 -0
  161. sphinx/locale/si/LC_MESSAGES/sphinx.po +2331 -2134
  162. sphinx/locale/sk/LC_MESSAGES/sphinx.mo +0 -0
  163. sphinx/locale/sk/LC_MESSAGES/sphinx.po +2966 -2769
  164. sphinx/locale/sl/LC_MESSAGES/sphinx.mo +0 -0
  165. sphinx/locale/sl/LC_MESSAGES/sphinx.po +2404 -2207
  166. sphinx/locale/sphinx.pot +2262 -2065
  167. sphinx/locale/sq/LC_MESSAGES/sphinx.mo +0 -0
  168. sphinx/locale/sq/LC_MESSAGES/sphinx.po +2972 -2775
  169. sphinx/locale/sr/LC_MESSAGES/sphinx.mo +0 -0
  170. sphinx/locale/sr/LC_MESSAGES/sphinx.po +2440 -2243
  171. sphinx/locale/sv/LC_MESSAGES/sphinx.mo +0 -0
  172. sphinx/locale/sv/LC_MESSAGES/sphinx.po +2483 -2286
  173. sphinx/locale/ta/LC_MESSAGES/sphinx.js +54 -54
  174. sphinx/locale/ta/LC_MESSAGES/sphinx.mo +0 -0
  175. sphinx/locale/ta/LC_MESSAGES/sphinx.po +1578 -1843
  176. sphinx/locale/te/LC_MESSAGES/sphinx.mo +0 -0
  177. sphinx/locale/te/LC_MESSAGES/sphinx.po +2250 -2053
  178. sphinx/locale/tr/LC_MESSAGES/sphinx.mo +0 -0
  179. sphinx/locale/tr/LC_MESSAGES/sphinx.po +2892 -2695
  180. sphinx/locale/uk_UA/LC_MESSAGES/sphinx.mo +0 -0
  181. sphinx/locale/uk_UA/LC_MESSAGES/sphinx.po +2400 -2203
  182. sphinx/locale/ur/LC_MESSAGES/sphinx.mo +0 -0
  183. sphinx/locale/ur/LC_MESSAGES/sphinx.po +2250 -2053
  184. sphinx/locale/vi/LC_MESSAGES/sphinx.mo +0 -0
  185. sphinx/locale/vi/LC_MESSAGES/sphinx.po +2422 -2225
  186. sphinx/locale/yue/LC_MESSAGES/sphinx.mo +0 -0
  187. sphinx/locale/yue/LC_MESSAGES/sphinx.po +2250 -2053
  188. sphinx/locale/zh_CN/LC_MESSAGES/sphinx.po +496 -704
  189. sphinx/locale/zh_HK/LC_MESSAGES/sphinx.mo +0 -0
  190. sphinx/locale/zh_HK/LC_MESSAGES/sphinx.po +2250 -2053
  191. sphinx/locale/zh_TW/LC_MESSAGES/sphinx.mo +0 -0
  192. sphinx/locale/zh_TW/LC_MESSAGES/sphinx.po +3028 -2831
  193. sphinx/locale/zh_TW.Big5/LC_MESSAGES/sphinx.mo +0 -0
  194. sphinx/locale/zh_TW.Big5/LC_MESSAGES/sphinx.po +2250 -2053
  195. sphinx/project.py +25 -20
  196. sphinx/pycode/ast.py +2 -2
  197. sphinx/pycode/parser.py +2 -2
  198. sphinx/pygments_styles.py +3 -3
  199. sphinx/registry.py +3 -8
  200. sphinx/search/__init__.py +1 -1
  201. sphinx/testing/path.py +2 -1
  202. sphinx/testing/util.py +1 -1
  203. sphinx/texinputs/Makefile.jinja +2 -1
  204. sphinx/texinputs_win/Makefile.jinja +2 -1
  205. sphinx/theming.py +3 -12
  206. sphinx/transforms/__init__.py +5 -5
  207. sphinx/transforms/references.py +1 -1
  208. sphinx/util/__init__.py +11 -35
  209. sphinx/util/_pathlib.py +31 -19
  210. sphinx/util/_timestamps.py +12 -0
  211. sphinx/util/cfamily.py +5 -5
  212. sphinx/util/console.py +4 -3
  213. sphinx/util/display.py +3 -3
  214. sphinx/util/docfields.py +1 -1
  215. sphinx/util/docutils.py +44 -10
  216. sphinx/util/fileutil.py +41 -9
  217. sphinx/util/i18n.py +9 -4
  218. sphinx/util/images.py +3 -2
  219. sphinx/util/inspect.py +29 -44
  220. sphinx/util/inventory.py +2 -2
  221. sphinx/util/matching.py +2 -2
  222. sphinx/util/math.py +1 -1
  223. sphinx/util/nodes.py +8 -8
  224. sphinx/util/osutil.py +52 -26
  225. sphinx/util/parallel.py +2 -2
  226. sphinx/util/requests.py +1 -1
  227. sphinx/util/template.py +3 -3
  228. sphinx/util/typing.py +67 -70
  229. sphinx/writers/html.py +1 -1
  230. sphinx/writers/html5.py +1 -1
  231. sphinx/writers/latex.py +4 -4
  232. sphinx/writers/manpage.py +2 -2
  233. sphinx/writers/texinfo.py +5 -5
  234. sphinx/writers/text.py +4 -4
  235. sphinx/writers/xml.py +2 -2
  236. {sphinx-7.4.6.dist-info → sphinx-8.0.0.dist-info}/METADATA +11 -10
  237. {sphinx-7.4.6.dist-info → sphinx-8.0.0.dist-info}/RECORD +240 -241
  238. sphinx/templates/quickstart/Makefile.jinja +0 -98
  239. sphinx/templates/quickstart/make.bat.jinja +0 -110
  240. {sphinx-7.4.6.dist-info → sphinx-8.0.0.dist-info}/LICENSE.rst +0 -0
  241. {sphinx-7.4.6.dist-info → sphinx-8.0.0.dist-info}/WHEEL +0 -0
  242. {sphinx-7.4.6.dist-info → sphinx-8.0.0.dist-info}/entry_points.txt +0 -0
@@ -6,175 +6,300 @@ import concurrent.futures
6
6
  import functools
7
7
  import posixpath
8
8
  import time
9
+ from operator import itemgetter
9
10
  from os import path
10
11
  from typing import TYPE_CHECKING
11
12
  from urllib.parse import urlsplit, urlunsplit
12
13
 
13
14
  from sphinx.builders.html import INVENTORY_FILENAME
14
- from sphinx.ext.intersphinx._shared import LOGGER, InventoryAdapter
15
+ from sphinx.errors import ConfigError
16
+ from sphinx.ext.intersphinx._shared import LOGGER, InventoryAdapter, _IntersphinxProject
15
17
  from sphinx.locale import __
16
18
  from sphinx.util import requests
17
19
  from sphinx.util.inventory import InventoryFile
18
20
 
19
21
  if TYPE_CHECKING:
22
+ from pathlib import Path
20
23
  from typing import IO
21
24
 
22
25
  from sphinx.application import Sphinx
23
26
  from sphinx.config import Config
24
- from sphinx.ext.intersphinx._shared import InventoryCacheEntry
27
+ from sphinx.ext.intersphinx._shared import (
28
+ IntersphinxMapping,
29
+ InventoryCacheEntry,
30
+ InventoryLocation,
31
+ InventoryName,
32
+ InventoryURI,
33
+ )
25
34
  from sphinx.util.typing import Inventory
26
35
 
27
36
 
28
- def normalize_intersphinx_mapping(app: Sphinx, config: Config) -> None:
29
- for key, value in config.intersphinx_mapping.copy().items():
37
+ def validate_intersphinx_mapping(app: Sphinx, config: Config) -> None:
38
+ """Validate and normalise :confval:`intersphinx_mapping`.
39
+
40
+ Ensure that:
41
+
42
+ * Keys are non-empty strings.
43
+ * Values are two-element tuples or lists.
44
+ * The first element of each value pair (the target URI)
45
+ is a non-empty string.
46
+ * The second element of each value pair (inventory locations)
47
+ is a tuple of non-empty strings or None.
48
+ """
49
+ # URIs should NOT be duplicated, otherwise different builds may use
50
+ # different project names (and thus, the build are no more reproducible)
51
+ # depending on which one is inserted last in the cache.
52
+ seen: dict[InventoryURI, InventoryName] = {}
53
+
54
+ errors = 0
55
+ for name, value in config.intersphinx_mapping.copy().items():
56
+ # ensure that intersphinx projects are always named
57
+ if not isinstance(name, str) or not name:
58
+ errors += 1
59
+ msg = __(
60
+ 'Invalid intersphinx project identifier `%r` in intersphinx_mapping. '
61
+ 'Project identifiers must be non-empty strings.'
62
+ )
63
+ LOGGER.error(msg, name)
64
+ del config.intersphinx_mapping[name]
65
+ continue
66
+
67
+ # ensure values are properly formatted
68
+ if not isinstance(value, (tuple | list)):
69
+ errors += 1
70
+ msg = __(
71
+ 'Invalid value `%r` in intersphinx_mapping[%r]. '
72
+ 'Expected a two-element tuple or list.'
73
+ )
74
+ LOGGER.error(msg, value, name)
75
+ del config.intersphinx_mapping[name]
76
+ continue
30
77
  try:
31
- if isinstance(value, (list, tuple)):
32
- # new format
33
- name, (uri, inv) = key, value
34
- if not isinstance(name, str):
35
- LOGGER.warning(__('intersphinx identifier %r is not string. Ignored'),
36
- name)
37
- config.intersphinx_mapping.pop(key)
38
- continue
78
+ uri, inv = value
79
+ except (TypeError, ValueError, Exception):
80
+ errors += 1
81
+ msg = __(
82
+ 'Invalid value `%r` in intersphinx_mapping[%r]. '
83
+ 'Values must be a (target URI, inventory locations) pair.'
84
+ )
85
+ LOGGER.error(msg, value, name)
86
+ del config.intersphinx_mapping[name]
87
+ continue
88
+
89
+ # ensure target URIs are non-empty and unique
90
+ if not uri or not isinstance(uri, str):
91
+ errors += 1
92
+ msg = __('Invalid target URI value `%r` in intersphinx_mapping[%r][0]. '
93
+ 'Target URIs must be unique non-empty strings.')
94
+ LOGGER.error(msg, uri, name)
95
+ del config.intersphinx_mapping[name]
96
+ continue
97
+ if uri in seen:
98
+ errors += 1
99
+ msg = __(
100
+ 'Invalid target URI value `%r` in intersphinx_mapping[%r][0]. '
101
+ 'Target URIs must be unique (other instance in intersphinx_mapping[%r]).'
102
+ )
103
+ LOGGER.error(msg, uri, name, seen[uri])
104
+ del config.intersphinx_mapping[name]
105
+ continue
106
+ seen[uri] = name
107
+
108
+ # ensure inventory locations are None or non-empty
109
+ targets: list[InventoryLocation] = []
110
+ for target in (inv if isinstance(inv, (tuple | list)) else (inv,)):
111
+ if target is None or target and isinstance(target, str):
112
+ targets.append(target)
39
113
  else:
40
- # old format, no name
41
- # xref RemovedInSphinx80Warning
42
- name, uri, inv = None, key, value
43
- msg = (
44
- "The pre-Sphinx 1.0 'intersphinx_mapping' format is "
45
- 'deprecated and will be removed in Sphinx 8. Update to the '
46
- 'current format as described in the documentation. '
47
- f"Hint: `intersphinx_mapping = {{'<name>': {(uri, inv)!r}}}`."
48
- 'https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#confval-intersphinx_mapping' # NoQA: E501
114
+ errors += 1
115
+ msg = __(
116
+ 'Invalid inventory location value `%r` in intersphinx_mapping[%r][1]. '
117
+ 'Inventory locations must be non-empty strings or None.'
49
118
  )
50
- LOGGER.warning(msg)
119
+ LOGGER.error(msg, target, name)
120
+ del config.intersphinx_mapping[name]
121
+ continue
51
122
 
52
- if not isinstance(inv, tuple):
53
- config.intersphinx_mapping[key] = (name, (uri, (inv,)))
54
- else:
55
- config.intersphinx_mapping[key] = (name, (uri, inv))
56
- except Exception as exc:
57
- LOGGER.warning(__('Failed to read intersphinx_mapping[%s], ignored: %r'), key, exc)
58
- config.intersphinx_mapping.pop(key)
123
+ config.intersphinx_mapping[name] = (name, (uri, tuple(targets)))
124
+
125
+ if errors == 1:
126
+ msg = __('Invalid `intersphinx_mapping` configuration (1 error).')
127
+ raise ConfigError(msg)
128
+ if errors > 1:
129
+ msg = __('Invalid `intersphinx_mapping` configuration (%s errors).')
130
+ raise ConfigError(msg % errors)
59
131
 
60
132
 
61
133
  def load_mappings(app: Sphinx) -> None:
62
- """Load all intersphinx mappings into the environment."""
134
+ """Load all intersphinx mappings into the environment.
135
+
136
+ The intersphinx mappings are expected to be normalized.
137
+ """
63
138
  now = int(time.time())
64
139
  inventories = InventoryAdapter(app.builder.env)
65
- intersphinx_cache: dict[str, InventoryCacheEntry] = inventories.cache
140
+ intersphinx_cache: dict[InventoryURI, InventoryCacheEntry] = inventories.cache
141
+ intersphinx_mapping: IntersphinxMapping = app.config.intersphinx_mapping
142
+
143
+ projects = []
144
+ for name, (uri, locations) in intersphinx_mapping.values():
145
+ try:
146
+ project = _IntersphinxProject(name=name, target_uri=uri, locations=locations)
147
+ except ValueError as err:
148
+ msg = __('An invalid intersphinx_mapping entry was added after normalisation.')
149
+ raise ConfigError(msg) from err
150
+ else:
151
+ projects.append(project)
152
+
153
+ expected_uris = {project.target_uri for project in projects}
154
+ for uri in frozenset(intersphinx_cache):
155
+ if intersphinx_cache[uri][0] not in intersphinx_mapping:
156
+ # Remove all cached entries that are no longer in `intersphinx_mapping`.
157
+ del intersphinx_cache[uri]
158
+ elif uri not in expected_uris:
159
+ # Remove cached entries with a different target URI
160
+ # than the one in `intersphinx_mapping`.
161
+ # This happens when the URI in `intersphinx_mapping` is changed.
162
+ del intersphinx_cache[uri]
66
163
 
67
164
  with concurrent.futures.ThreadPoolExecutor() as pool:
68
- futures = []
69
- name: str | None
70
- uri: str
71
- invs: tuple[str | None, ...]
72
- for name, (uri, invs) in app.config.intersphinx_mapping.values():
73
- futures.append(pool.submit(
74
- fetch_inventory_group, name, uri, invs, intersphinx_cache, app, now,
75
- ))
165
+ futures = [
166
+ pool.submit(
167
+ _fetch_inventory_group,
168
+ project=project,
169
+ cache=intersphinx_cache,
170
+ now=now,
171
+ config=app.config,
172
+ srcdir=app.srcdir,
173
+ )
174
+ for project in projects
175
+ ]
76
176
  updated = [f.result() for f in concurrent.futures.as_completed(futures)]
77
177
 
78
178
  if any(updated):
179
+ # clear the local inventories
79
180
  inventories.clear()
80
181
 
81
182
  # Duplicate values in different inventories will shadow each
82
- # other; which one will override which can vary between builds
83
- # since they are specified using an unordered dict. To make
84
- # it more consistent, we sort the named inventories and then
85
- # add the unnamed inventories last. This means that the
86
- # unnamed inventories will shadow the named ones but the named
87
- # ones can still be accessed when the name is specified.
88
- named_vals = []
89
- unnamed_vals = []
90
- for name, _expiry, invdata in intersphinx_cache.values():
91
- if name:
92
- named_vals.append((name, invdata))
93
- else:
94
- unnamed_vals.append((name, invdata))
95
- for name, invdata in sorted(named_vals) + unnamed_vals:
96
- if name:
97
- inventories.named_inventory[name] = invdata
98
- for type, objects in invdata.items():
99
- inventories.main_inventory.setdefault(type, {}).update(objects)
100
-
101
-
102
- def fetch_inventory_group(
103
- name: str | None,
104
- uri: str,
105
- invs: tuple[str | None, ...],
106
- cache: dict[str, InventoryCacheEntry],
107
- app: Sphinx,
183
+ # other; which one will override which can vary between builds.
184
+ #
185
+ # In an attempt to make this more consistent,
186
+ # we sort the named inventories in the cache
187
+ # by their name and expiry time ``(NAME, EXPIRY)``.
188
+ by_name_and_time = itemgetter(0, 1) # 0: name, 1: expiry
189
+ cache_values = sorted(intersphinx_cache.values(), key=by_name_and_time)
190
+ for name, _expiry, invdata in cache_values:
191
+ inventories.named_inventory[name] = invdata
192
+ for objtype, objects in invdata.items():
193
+ inventories.main_inventory.setdefault(objtype, {}).update(objects)
194
+
195
+
196
+ def _fetch_inventory_group(
197
+ *,
198
+ project: _IntersphinxProject,
199
+ cache: dict[InventoryURI, InventoryCacheEntry],
108
200
  now: int,
201
+ config: Config,
202
+ srcdir: Path,
109
203
  ) -> bool:
110
- cache_time = now - app.config.intersphinx_cache_limit * 86400
204
+ cache_time = now - config.intersphinx_cache_limit * 86400
205
+
206
+ updated = False
111
207
  failures = []
112
- try:
113
- for inv in invs:
114
- if not inv:
115
- inv = posixpath.join(uri, INVENTORY_FILENAME)
116
- # decide whether the inventory must be read: always read local
117
- # files; remote ones only if the cache time is expired
118
- if '://' not in inv or uri not in cache or cache[uri][1] < cache_time:
119
- safe_inv_url = _get_safe_url(inv)
120
- inv_descriptor = name or 'main_inventory'
121
- LOGGER.info(__("loading intersphinx inventory '%s' from %s..."),
122
- inv_descriptor, safe_inv_url)
123
- try:
124
- invdata = fetch_inventory(app, uri, inv)
125
- except Exception as err:
126
- failures.append(err.args)
127
- continue
128
- if invdata:
129
- cache[uri] = name, now, invdata
130
- return True
131
- return False
132
- finally:
133
- if failures == []:
134
- pass
135
- elif len(failures) < len(invs):
136
- LOGGER.info(__('encountered some issues with some of the inventories,'
137
- ' but they had working alternatives:'))
138
- for fail in failures:
139
- LOGGER.info(*fail)
140
- else:
141
- issues = '\n'.join(f[0] % f[1:] for f in failures)
142
- LOGGER.warning(__('failed to reach any of the inventories '
143
- 'with the following issues:') + '\n' + issues)
208
+
209
+ for location in project.locations:
210
+ # location is either None or a non-empty string
211
+ inv = f'{project.target_uri}/{INVENTORY_FILENAME}' if location is None else location
212
+
213
+ # decide whether the inventory must be read: always read local
214
+ # files; remote ones only if the cache time is expired
215
+ if (
216
+ '://' not in inv
217
+ or project.target_uri not in cache
218
+ or cache[project.target_uri][1] < cache_time
219
+ ):
220
+ LOGGER.info(__("loading intersphinx inventory '%s' from %s ..."),
221
+ project.name, _get_safe_url(inv))
222
+
223
+ try:
224
+ invdata = _fetch_inventory(
225
+ target_uri=project.target_uri,
226
+ inv_location=inv,
227
+ config=config,
228
+ srcdir=srcdir,
229
+ )
230
+ except Exception as err:
231
+ failures.append(err.args)
232
+ continue
233
+
234
+ if invdata:
235
+ cache[project.target_uri] = project.name, now, invdata
236
+ updated = True
237
+ break
238
+
239
+ if not failures:
240
+ pass
241
+ elif len(failures) < len(project.locations):
242
+ LOGGER.info(__('encountered some issues with some of the inventories,'
243
+ ' but they had working alternatives:'))
244
+ for fail in failures:
245
+ LOGGER.info(*fail)
246
+ else:
247
+ issues = '\n'.join(f[0] % f[1:] for f in failures)
248
+ LOGGER.warning(__('failed to reach any of the inventories '
249
+ 'with the following issues:') + '\n' + issues)
250
+ return updated
251
+
252
+
253
+ def fetch_inventory(app: Sphinx, uri: InventoryURI, inv: str) -> Inventory:
254
+ """Fetch, parse and return an intersphinx inventory file."""
255
+ return _fetch_inventory(
256
+ target_uri=uri,
257
+ inv_location=inv,
258
+ config=app.config,
259
+ srcdir=app.srcdir,
260
+ )
144
261
 
145
262
 
146
- def fetch_inventory(app: Sphinx, uri: str, inv: str) -> Inventory:
263
+ def _fetch_inventory(
264
+ *, target_uri: InventoryURI, inv_location: str, config: Config, srcdir: Path,
265
+ ) -> Inventory:
147
266
  """Fetch, parse and return an intersphinx inventory file."""
148
- # both *uri* (base URI of the links to generate) and *inv* (actual
149
- # location of the inventory file) can be local or remote URIs
150
- if '://' in uri:
267
+ # both *target_uri* (base URI of the links to generate)
268
+ # and *inv_location* (actual location of the inventory file)
269
+ # can be local or remote URIs
270
+ if '://' in target_uri:
151
271
  # case: inv URI points to remote resource; strip any existing auth
152
- uri = _strip_basic_auth(uri)
272
+ target_uri = _strip_basic_auth(target_uri)
153
273
  try:
154
- if '://' in inv:
155
- f = _read_from_url(inv, config=app.config)
274
+ if '://' in inv_location:
275
+ f = _read_from_url(inv_location, config=config)
156
276
  else:
157
- f = open(path.join(app.srcdir, inv), 'rb') # NoQA: SIM115
277
+ f = open(path.join(srcdir, inv_location), 'rb') # NoQA: SIM115
158
278
  except Exception as err:
159
279
  err.args = ('intersphinx inventory %r not fetchable due to %s: %s',
160
- inv, err.__class__, str(err))
280
+ inv_location, err.__class__, str(err))
161
281
  raise
162
282
  try:
163
283
  if hasattr(f, 'url'):
164
- newinv = f.url
165
- if inv != newinv:
166
- LOGGER.info(__('intersphinx inventory has moved: %s -> %s'), inv, newinv)
167
-
168
- if uri in (inv, path.dirname(inv), path.dirname(inv) + '/'):
169
- uri = path.dirname(newinv)
284
+ new_inv_location = f.url
285
+ if inv_location != new_inv_location:
286
+ msg = __('intersphinx inventory has moved: %s -> %s')
287
+ LOGGER.info(msg, inv_location, new_inv_location)
288
+
289
+ if target_uri in {
290
+ inv_location,
291
+ path.dirname(inv_location),
292
+ path.dirname(inv_location) + '/'
293
+ }:
294
+ target_uri = path.dirname(new_inv_location)
170
295
  with f:
171
296
  try:
172
- invdata = InventoryFile.load(f, uri, posixpath.join)
297
+ invdata = InventoryFile.load(f, target_uri, posixpath.join)
173
298
  except ValueError as exc:
174
299
  raise ValueError('unknown or unsupported inventory version: %r' % exc) from exc
175
300
  except Exception as err:
176
301
  err.args = ('intersphinx inventory %r not readable due to %s: %s',
177
- inv, err.__class__.__name__, str(err))
302
+ inv_location, err.__class__.__name__, str(err))
178
303
  raise
179
304
  else:
180
305
  return invdata
@@ -28,10 +28,11 @@ if TYPE_CHECKING:
28
28
  from sphinx.application import Sphinx
29
29
  from sphinx.domains import Domain
30
30
  from sphinx.environment import BuildEnvironment
31
+ from sphinx.ext.intersphinx._shared import InventoryName
31
32
  from sphinx.util.typing import Inventory, InventoryItem, RoleFunction
32
33
 
33
34
 
34
- def _create_element_from_result(domain: Domain, inv_name: str | None,
35
+ def _create_element_from_result(domain: Domain, inv_name: InventoryName | None,
35
36
  data: InventoryItem,
36
37
  node: pending_xref, contnode: TextElement) -> nodes.reference:
37
38
  proj, version, uri, dispname = data
@@ -61,7 +62,7 @@ def _create_element_from_result(domain: Domain, inv_name: str | None,
61
62
 
62
63
 
63
64
  def _resolve_reference_in_domain_by_target(
64
- inv_name: str | None, inventory: Inventory,
65
+ inv_name: InventoryName | None, inventory: Inventory,
65
66
  domain: Domain, objtypes: Iterable[str],
66
67
  target: str,
67
68
  node: pending_xref, contnode: TextElement) -> nodes.reference | None:
@@ -100,7 +101,7 @@ def _resolve_reference_in_domain_by_target(
100
101
 
101
102
 
102
103
  def _resolve_reference_in_domain(env: BuildEnvironment,
103
- inv_name: str | None, inventory: Inventory,
104
+ inv_name: InventoryName | None, inventory: Inventory,
104
105
  honor_disabled_refs: bool,
105
106
  domain: Domain, objtypes: Iterable[str],
106
107
  node: pending_xref, contnode: TextElement,
@@ -142,20 +143,21 @@ def _resolve_reference_in_domain(env: BuildEnvironment,
142
143
  full_qualified_name, node, contnode)
143
144
 
144
145
 
145
- def _resolve_reference(env: BuildEnvironment, inv_name: str | None, inventory: Inventory,
146
+ def _resolve_reference(env: BuildEnvironment,
147
+ inv_name: InventoryName | None, inventory: Inventory,
146
148
  honor_disabled_refs: bool,
147
149
  node: pending_xref, contnode: TextElement) -> nodes.reference | None:
148
150
  # disabling should only be done if no inventory is given
149
151
  honor_disabled_refs = honor_disabled_refs and inv_name is None
152
+ intersphinx_disabled_reftypes = env.config.intersphinx_disabled_reftypes
150
153
 
151
- if honor_disabled_refs and '*' in env.config.intersphinx_disabled_reftypes:
154
+ if honor_disabled_refs and '*' in intersphinx_disabled_reftypes:
152
155
  return None
153
156
 
154
157
  typ = node['reftype']
155
158
  if typ == 'any':
156
159
  for domain_name, domain in env.domains.items():
157
- if (honor_disabled_refs
158
- and (domain_name + ':*') in env.config.intersphinx_disabled_reftypes):
160
+ if honor_disabled_refs and f'{domain_name}:*' in intersphinx_disabled_reftypes:
159
161
  continue
160
162
  objtypes: Iterable[str] = domain.object_types.keys()
161
163
  res = _resolve_reference_in_domain(env, inv_name, inventory,
@@ -170,8 +172,7 @@ def _resolve_reference(env: BuildEnvironment, inv_name: str | None, inventory: I
170
172
  if not domain_name:
171
173
  # only objects in domains are in the inventory
172
174
  return None
173
- if (honor_disabled_refs
174
- and (domain_name + ':*') in env.config.intersphinx_disabled_reftypes):
175
+ if honor_disabled_refs and f'{domain_name}:*' in intersphinx_disabled_reftypes:
175
176
  return None
176
177
  domain = env.get_domain(domain_name)
177
178
  objtypes = domain.objtypes_for_role(typ) or ()
@@ -183,12 +184,12 @@ def _resolve_reference(env: BuildEnvironment, inv_name: str | None, inventory: I
183
184
  node, contnode)
184
185
 
185
186
 
186
- def inventory_exists(env: BuildEnvironment, inv_name: str) -> bool:
187
+ def inventory_exists(env: BuildEnvironment, inv_name: InventoryName) -> bool:
187
188
  return inv_name in InventoryAdapter(env).named_inventory
188
189
 
189
190
 
190
191
  def resolve_reference_in_inventory(env: BuildEnvironment,
191
- inv_name: str,
192
+ inv_name: InventoryName,
192
193
  node: pending_xref, contnode: TextElement,
193
194
  ) -> nodes.reference | None:
194
195
  """Attempt to resolve a missing reference via intersphinx references.
@@ -2,19 +2,113 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import TYPE_CHECKING, Final, Union
5
+ from typing import TYPE_CHECKING, Any, Final, NoReturn
6
6
 
7
7
  from sphinx.util import logging
8
8
 
9
9
  if TYPE_CHECKING:
10
+ from collections.abc import Sequence
11
+ from typing import TypeAlias
12
+
10
13
  from sphinx.environment import BuildEnvironment
11
14
  from sphinx.util.typing import Inventory
12
15
 
13
- InventoryCacheEntry = tuple[Union[str, None], int, Inventory]
16
+ #: The inventory project URL to which links are resolved.
17
+ #:
18
+ #: This value is unique in :confval:`intersphinx_mapping`.
19
+ InventoryURI = str
20
+
21
+ #: The inventory (non-empty) name.
22
+ #:
23
+ #: It is unique and in bijection with an inventory remote URL.
24
+ InventoryName = str
25
+
26
+ #: A target (local or remote) containing the inventory data to fetch.
27
+ #:
28
+ #: Empty strings are not expected and ``None`` indicates the default
29
+ #: inventory file name :data:`~sphinx.builder.html.INVENTORY_FILENAME`.
30
+ InventoryLocation = str | None
31
+
32
+ #: Inventory cache entry. The integer field is the cache expiration time.
33
+ InventoryCacheEntry: TypeAlias = tuple[InventoryName, int, Inventory]
34
+
35
+ #: The type of :confval:`intersphinx_mapping` *after* normalisation.
36
+ IntersphinxMapping = dict[
37
+ InventoryName,
38
+ tuple[InventoryName, tuple[InventoryURI, tuple[InventoryLocation, ...]]],
39
+ ]
14
40
 
15
41
  LOGGER: Final[logging.SphinxLoggerAdapter] = logging.getLogger('sphinx.ext.intersphinx')
16
42
 
17
43
 
44
+ class _IntersphinxProject:
45
+ name: InventoryName
46
+ target_uri: InventoryURI
47
+ locations: tuple[InventoryLocation, ...]
48
+
49
+ __slots__ = {
50
+ 'name': 'The inventory name. '
51
+ 'It is unique and in bijection with an remote inventory URL.',
52
+ 'target_uri': 'The inventory project URL to which links are resolved. '
53
+ 'It is unique and in bijection with an inventory name.',
54
+ 'locations': 'A tuple of local or remote targets containing '
55
+ 'the inventory data to fetch. '
56
+ 'None indicates the default inventory file name.',
57
+ }
58
+
59
+ def __init__(
60
+ self,
61
+ *,
62
+ name: InventoryName,
63
+ target_uri: InventoryURI,
64
+ locations: Sequence[InventoryLocation],
65
+ ) -> None:
66
+ if not name or not isinstance(name, str):
67
+ msg = 'name must be a non-empty string'
68
+ raise ValueError(msg)
69
+ if not target_uri or not isinstance(target_uri, str):
70
+ msg = 'target_uri must be a non-empty string'
71
+ raise ValueError(msg)
72
+ if not locations or not isinstance(locations, tuple):
73
+ msg = 'locations must be a non-empty tuple'
74
+ raise ValueError(msg)
75
+ if any(
76
+ location is not None and (not location or not isinstance(location, str))
77
+ for location in locations
78
+ ):
79
+ msg = 'locations must be a tuple of strings or None'
80
+ raise ValueError(msg)
81
+ object.__setattr__(self, 'name', name)
82
+ object.__setattr__(self, 'target_uri', target_uri)
83
+ object.__setattr__(self, 'locations', tuple(locations))
84
+
85
+ def __repr__(self) -> str:
86
+ return (f'{self.__class__.__name__}('
87
+ f'name={self.name!r}, '
88
+ f'target_uri={self.target_uri!r}, '
89
+ f'locations={self.locations!r})')
90
+
91
+ def __eq__(self, other: object) -> bool:
92
+ if not isinstance(other, _IntersphinxProject):
93
+ return NotImplemented
94
+ return (
95
+ self.name == other.name
96
+ and self.target_uri == other.target_uri
97
+ and self.locations == other.locations
98
+ )
99
+
100
+ def __hash__(self) -> int:
101
+ return hash((self.name, self.target_uri, self.locations))
102
+
103
+ def __setattr__(self, key: str, value: Any) -> NoReturn:
104
+ msg = f'{self.__class__.__name__} is immutable'
105
+ raise AttributeError(msg)
106
+
107
+ def __delattr__(self, key: str) -> NoReturn:
108
+ msg = f'{self.__class__.__name__} is immutable'
109
+ raise AttributeError(msg)
110
+
111
+
18
112
  class InventoryAdapter:
19
113
  """Inventory adapter for environment"""
20
114
 
@@ -29,14 +123,13 @@ class InventoryAdapter:
29
123
  self.env.intersphinx_named_inventory = {} # type: ignore[attr-defined]
30
124
 
31
125
  @property
32
- def cache(self) -> dict[str, InventoryCacheEntry]:
126
+ def cache(self) -> dict[InventoryURI, InventoryCacheEntry]:
33
127
  """Intersphinx cache.
34
128
 
35
- - Key is the URI of the remote inventory
36
- - Element one is the key given in the Sphinx intersphinx_mapping
37
- configuration value
38
- - Element two is a time value for cache invalidation, a float
39
- - Element three is the loaded remote inventory, type Inventory
129
+ - Key is the URI of the remote inventory.
130
+ - Element one is the key given in the Sphinx :confval:`intersphinx_mapping`.
131
+ - Element two is a time value for cache invalidation, an integer.
132
+ - Element three is the loaded remote inventory of type :class:`!Inventory`.
40
133
  """
41
134
  return self.env.intersphinx_cache # type: ignore[attr-defined]
42
135
 
@@ -45,7 +138,7 @@ class InventoryAdapter:
45
138
  return self.env.intersphinx_inventory # type: ignore[attr-defined]
46
139
 
47
140
  @property
48
- def named_inventory(self) -> dict[str, Inventory]:
141
+ def named_inventory(self) -> dict[InventoryName, Inventory]:
49
142
  return self.env.intersphinx_named_inventory # type: ignore[attr-defined]
50
143
 
51
144
  def clear(self) -> None:
sphinx/ext/mathjax.py CHANGED
@@ -22,7 +22,7 @@ from sphinx.util.math import get_node_equation_number
22
22
  if TYPE_CHECKING:
23
23
  from sphinx.application import Sphinx
24
24
  from sphinx.util.typing import ExtensionMetadata
25
- from sphinx.writers.html import HTML5Translator
25
+ from sphinx.writers.html5 import HTML5Translator
26
26
 
27
27
  # more information for mathjax secure url is here:
28
28
  # https://docs.mathjax.org/en/latest/web/start.html#using-mathjax-from-a-content-delivery-network-cdn