Sphinx 7.4.7__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 (234) 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 +88 -58
  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/other.py +2 -3
  19. sphinx/directives/patches.py +2 -2
  20. sphinx/domains/__init__.py +4 -2
  21. sphinx/domains/c/__init__.py +2 -2
  22. sphinx/domains/c/_ast.py +3 -2
  23. sphinx/domains/c/_parser.py +4 -3
  24. sphinx/domains/cpp/__init__.py +2 -2
  25. sphinx/domains/cpp/_ast.py +1 -2
  26. sphinx/domains/cpp/_parser.py +2 -2
  27. sphinx/domains/cpp/_symbol.py +2 -2
  28. sphinx/domains/math.py +1 -1
  29. sphinx/domains/python/_object.py +0 -1
  30. sphinx/domains/std/__init__.py +7 -8
  31. sphinx/environment/__init__.py +15 -32
  32. sphinx/environment/adapters/indexentries.py +4 -6
  33. sphinx/environment/adapters/toctree.py +4 -4
  34. sphinx/environment/collectors/title.py +1 -1
  35. sphinx/environment/collectors/toctree.py +1 -1
  36. sphinx/events.py +3 -1
  37. sphinx/ext/autodoc/__init__.py +17 -63
  38. sphinx/ext/autodoc/directive.py +7 -5
  39. sphinx/ext/autodoc/importer.py +2 -1
  40. sphinx/ext/autodoc/preserve_defaults.py +2 -2
  41. sphinx/ext/autosummary/__init__.py +7 -6
  42. sphinx/ext/autosummary/generate.py +5 -4
  43. sphinx/ext/doctest.py +5 -5
  44. sphinx/ext/graphviz.py +1 -1
  45. sphinx/ext/imgmath.py +1 -1
  46. sphinx/ext/inheritance_diagram.py +1 -1
  47. sphinx/ext/intersphinx/__init__.py +25 -5
  48. sphinx/ext/intersphinx/_cli.py +7 -6
  49. sphinx/ext/intersphinx/_load.py +240 -115
  50. sphinx/ext/intersphinx/_resolve.py +12 -11
  51. sphinx/ext/intersphinx/_shared.py +102 -9
  52. sphinx/ext/mathjax.py +1 -1
  53. sphinx/ext/napoleon/docstring.py +2 -2
  54. sphinx/ext/todo.py +2 -2
  55. sphinx/ext/viewcode.py +2 -1
  56. sphinx/highlighting.py +3 -3
  57. sphinx/io.py +2 -2
  58. sphinx/jinja2glue.py +13 -6
  59. sphinx/locale/__init__.py +4 -3
  60. sphinx/locale/ar/LC_MESSAGES/sphinx.mo +0 -0
  61. sphinx/locale/ar/LC_MESSAGES/sphinx.po +2383 -2186
  62. sphinx/locale/bg/LC_MESSAGES/sphinx.mo +0 -0
  63. sphinx/locale/bg/LC_MESSAGES/sphinx.po +2249 -2052
  64. sphinx/locale/bn/LC_MESSAGES/sphinx.mo +0 -0
  65. sphinx/locale/bn/LC_MESSAGES/sphinx.po +2412 -2215
  66. sphinx/locale/ca/LC_MESSAGES/sphinx.mo +0 -0
  67. sphinx/locale/ca/LC_MESSAGES/sphinx.po +3029 -2832
  68. sphinx/locale/cak/LC_MESSAGES/sphinx.mo +0 -0
  69. sphinx/locale/cak/LC_MESSAGES/sphinx.po +2308 -2111
  70. sphinx/locale/cs/LC_MESSAGES/sphinx.mo +0 -0
  71. sphinx/locale/cs/LC_MESSAGES/sphinx.po +2469 -2272
  72. sphinx/locale/cy/LC_MESSAGES/sphinx.mo +0 -0
  73. sphinx/locale/cy/LC_MESSAGES/sphinx.po +2393 -2196
  74. sphinx/locale/da/LC_MESSAGES/sphinx.mo +0 -0
  75. sphinx/locale/da/LC_MESSAGES/sphinx.po +2532 -2335
  76. sphinx/locale/de/LC_MESSAGES/sphinx.mo +0 -0
  77. sphinx/locale/de/LC_MESSAGES/sphinx.po +2492 -2295
  78. sphinx/locale/de_DE/LC_MESSAGES/sphinx.mo +0 -0
  79. sphinx/locale/de_DE/LC_MESSAGES/sphinx.po +2250 -2053
  80. sphinx/locale/el/LC_MESSAGES/sphinx.mo +0 -0
  81. sphinx/locale/el/LC_MESSAGES/sphinx.po +2879 -2682
  82. sphinx/locale/en_DE/LC_MESSAGES/sphinx.mo +0 -0
  83. sphinx/locale/en_DE/LC_MESSAGES/sphinx.po +2250 -2053
  84. sphinx/locale/en_FR/LC_MESSAGES/sphinx.mo +0 -0
  85. sphinx/locale/en_FR/LC_MESSAGES/sphinx.po +2250 -2053
  86. sphinx/locale/en_GB/LC_MESSAGES/sphinx.mo +0 -0
  87. sphinx/locale/en_GB/LC_MESSAGES/sphinx.po +2989 -2792
  88. sphinx/locale/en_HK/LC_MESSAGES/sphinx.mo +0 -0
  89. sphinx/locale/en_HK/LC_MESSAGES/sphinx.po +2250 -2053
  90. sphinx/locale/eo/LC_MESSAGES/sphinx.mo +0 -0
  91. sphinx/locale/eo/LC_MESSAGES/sphinx.po +2297 -2100
  92. sphinx/locale/es/LC_MESSAGES/sphinx.mo +0 -0
  93. sphinx/locale/es/LC_MESSAGES/sphinx.po +3017 -2820
  94. sphinx/locale/es_CO/LC_MESSAGES/sphinx.mo +0 -0
  95. sphinx/locale/es_CO/LC_MESSAGES/sphinx.po +2250 -2053
  96. sphinx/locale/et/LC_MESSAGES/sphinx.mo +0 -0
  97. sphinx/locale/et/LC_MESSAGES/sphinx.po +2748 -2551
  98. sphinx/locale/eu/LC_MESSAGES/sphinx.mo +0 -0
  99. sphinx/locale/eu/LC_MESSAGES/sphinx.po +2459 -2262
  100. sphinx/locale/fa/LC_MESSAGES/sphinx.mo +0 -0
  101. sphinx/locale/fa/LC_MESSAGES/sphinx.po +2957 -2760
  102. sphinx/locale/fi/LC_MESSAGES/sphinx.mo +0 -0
  103. sphinx/locale/fi/LC_MESSAGES/sphinx.po +2321 -2124
  104. sphinx/locale/fr/LC_MESSAGES/sphinx.mo +0 -0
  105. sphinx/locale/fr/LC_MESSAGES/sphinx.po +2977 -2780
  106. sphinx/locale/fr_FR/LC_MESSAGES/sphinx.mo +0 -0
  107. sphinx/locale/fr_FR/LC_MESSAGES/sphinx.po +2250 -2053
  108. sphinx/locale/gl/LC_MESSAGES/sphinx.mo +0 -0
  109. sphinx/locale/gl/LC_MESSAGES/sphinx.po +2992 -2795
  110. sphinx/locale/he/LC_MESSAGES/sphinx.mo +0 -0
  111. sphinx/locale/he/LC_MESSAGES/sphinx.po +2375 -2178
  112. sphinx/locale/hi/LC_MESSAGES/sphinx.mo +0 -0
  113. sphinx/locale/hi/LC_MESSAGES/sphinx.po +2937 -2740
  114. sphinx/locale/hi_IN/LC_MESSAGES/sphinx.mo +0 -0
  115. sphinx/locale/hi_IN/LC_MESSAGES/sphinx.po +2250 -2053
  116. sphinx/locale/hr/LC_MESSAGES/sphinx.mo +0 -0
  117. sphinx/locale/hr/LC_MESSAGES/sphinx.po +2532 -2335
  118. sphinx/locale/hu/LC_MESSAGES/sphinx.mo +0 -0
  119. sphinx/locale/hu/LC_MESSAGES/sphinx.po +2505 -2308
  120. sphinx/locale/id/LC_MESSAGES/sphinx.mo +0 -0
  121. sphinx/locale/id/LC_MESSAGES/sphinx.po +2925 -2728
  122. sphinx/locale/is/LC_MESSAGES/sphinx.mo +0 -0
  123. sphinx/locale/is/LC_MESSAGES/sphinx.po +2307 -2110
  124. sphinx/locale/it/LC_MESSAGES/sphinx.mo +0 -0
  125. sphinx/locale/it/LC_MESSAGES/sphinx.po +2514 -2317
  126. sphinx/locale/ja/LC_MESSAGES/sphinx.mo +0 -0
  127. sphinx/locale/ja/LC_MESSAGES/sphinx.po +2970 -2773
  128. sphinx/locale/ka/LC_MESSAGES/sphinx.mo +0 -0
  129. sphinx/locale/ka/LC_MESSAGES/sphinx.po +2868 -2671
  130. sphinx/locale/ko/LC_MESSAGES/sphinx.mo +0 -0
  131. sphinx/locale/ko/LC_MESSAGES/sphinx.po +3016 -2819
  132. sphinx/locale/lt/LC_MESSAGES/sphinx.mo +0 -0
  133. sphinx/locale/lt/LC_MESSAGES/sphinx.po +2476 -2279
  134. sphinx/locale/lv/LC_MESSAGES/sphinx.mo +0 -0
  135. sphinx/locale/lv/LC_MESSAGES/sphinx.po +2477 -2280
  136. sphinx/locale/mk/LC_MESSAGES/sphinx.mo +0 -0
  137. sphinx/locale/mk/LC_MESSAGES/sphinx.po +2292 -2095
  138. sphinx/locale/nb_NO/LC_MESSAGES/sphinx.mo +0 -0
  139. sphinx/locale/nb_NO/LC_MESSAGES/sphinx.po +2479 -2282
  140. sphinx/locale/ne/LC_MESSAGES/sphinx.mo +0 -0
  141. sphinx/locale/ne/LC_MESSAGES/sphinx.po +2481 -2284
  142. sphinx/locale/nl/LC_MESSAGES/sphinx.mo +0 -0
  143. sphinx/locale/nl/LC_MESSAGES/sphinx.po +2557 -2360
  144. sphinx/locale/pl/LC_MESSAGES/sphinx.mo +0 -0
  145. sphinx/locale/pl/LC_MESSAGES/sphinx.po +2696 -2499
  146. sphinx/locale/pt/LC_MESSAGES/sphinx.mo +0 -0
  147. sphinx/locale/pt/LC_MESSAGES/sphinx.po +2250 -2053
  148. sphinx/locale/pt_BR/LC_MESSAGES/sphinx.mo +0 -0
  149. sphinx/locale/pt_BR/LC_MESSAGES/sphinx.po +2979 -2782
  150. sphinx/locale/pt_PT/LC_MESSAGES/sphinx.mo +0 -0
  151. sphinx/locale/pt_PT/LC_MESSAGES/sphinx.po +2469 -2272
  152. sphinx/locale/ro/LC_MESSAGES/sphinx.mo +0 -0
  153. sphinx/locale/ro/LC_MESSAGES/sphinx.po +2473 -2276
  154. sphinx/locale/ru/LC_MESSAGES/sphinx.mo +0 -0
  155. sphinx/locale/ru/LC_MESSAGES/sphinx.po +2746 -2549
  156. sphinx/locale/si/LC_MESSAGES/sphinx.mo +0 -0
  157. sphinx/locale/si/LC_MESSAGES/sphinx.po +2331 -2134
  158. sphinx/locale/sk/LC_MESSAGES/sphinx.mo +0 -0
  159. sphinx/locale/sk/LC_MESSAGES/sphinx.po +2966 -2769
  160. sphinx/locale/sl/LC_MESSAGES/sphinx.mo +0 -0
  161. sphinx/locale/sl/LC_MESSAGES/sphinx.po +2404 -2207
  162. sphinx/locale/sphinx.pot +2262 -2065
  163. sphinx/locale/sq/LC_MESSAGES/sphinx.mo +0 -0
  164. sphinx/locale/sq/LC_MESSAGES/sphinx.po +2972 -2775
  165. sphinx/locale/sr/LC_MESSAGES/sphinx.mo +0 -0
  166. sphinx/locale/sr/LC_MESSAGES/sphinx.po +2440 -2243
  167. sphinx/locale/sv/LC_MESSAGES/sphinx.mo +0 -0
  168. sphinx/locale/sv/LC_MESSAGES/sphinx.po +2483 -2286
  169. sphinx/locale/te/LC_MESSAGES/sphinx.mo +0 -0
  170. sphinx/locale/te/LC_MESSAGES/sphinx.po +2250 -2053
  171. sphinx/locale/tr/LC_MESSAGES/sphinx.mo +0 -0
  172. sphinx/locale/tr/LC_MESSAGES/sphinx.po +2892 -2695
  173. sphinx/locale/uk_UA/LC_MESSAGES/sphinx.mo +0 -0
  174. sphinx/locale/uk_UA/LC_MESSAGES/sphinx.po +2400 -2203
  175. sphinx/locale/ur/LC_MESSAGES/sphinx.mo +0 -0
  176. sphinx/locale/ur/LC_MESSAGES/sphinx.po +2250 -2053
  177. sphinx/locale/vi/LC_MESSAGES/sphinx.mo +0 -0
  178. sphinx/locale/vi/LC_MESSAGES/sphinx.po +2422 -2225
  179. sphinx/locale/yue/LC_MESSAGES/sphinx.mo +0 -0
  180. sphinx/locale/yue/LC_MESSAGES/sphinx.po +2250 -2053
  181. sphinx/locale/zh_HK/LC_MESSAGES/sphinx.mo +0 -0
  182. sphinx/locale/zh_HK/LC_MESSAGES/sphinx.po +2250 -2053
  183. sphinx/locale/zh_TW/LC_MESSAGES/sphinx.mo +0 -0
  184. sphinx/locale/zh_TW/LC_MESSAGES/sphinx.po +3028 -2831
  185. sphinx/locale/zh_TW.Big5/LC_MESSAGES/sphinx.mo +0 -0
  186. sphinx/locale/zh_TW.Big5/LC_MESSAGES/sphinx.po +2250 -2053
  187. sphinx/project.py +25 -20
  188. sphinx/pycode/ast.py +2 -2
  189. sphinx/pycode/parser.py +2 -2
  190. sphinx/pygments_styles.py +3 -3
  191. sphinx/registry.py +3 -8
  192. sphinx/search/__init__.py +1 -1
  193. sphinx/testing/path.py +2 -1
  194. sphinx/testing/util.py +1 -1
  195. sphinx/texinputs/Makefile.jinja +2 -1
  196. sphinx/texinputs_win/Makefile.jinja +2 -1
  197. sphinx/theming.py +3 -12
  198. sphinx/transforms/__init__.py +5 -5
  199. sphinx/transforms/references.py +1 -1
  200. sphinx/util/__init__.py +11 -35
  201. sphinx/util/_pathlib.py +31 -19
  202. sphinx/util/_timestamps.py +12 -0
  203. sphinx/util/cfamily.py +5 -5
  204. sphinx/util/console.py +4 -3
  205. sphinx/util/display.py +3 -3
  206. sphinx/util/docfields.py +1 -1
  207. sphinx/util/docutils.py +44 -10
  208. sphinx/util/fileutil.py +25 -20
  209. sphinx/util/i18n.py +9 -4
  210. sphinx/util/images.py +3 -2
  211. sphinx/util/inspect.py +28 -43
  212. sphinx/util/inventory.py +2 -2
  213. sphinx/util/matching.py +2 -2
  214. sphinx/util/math.py +1 -1
  215. sphinx/util/nodes.py +8 -8
  216. sphinx/util/osutil.py +36 -32
  217. sphinx/util/parallel.py +2 -2
  218. sphinx/util/requests.py +1 -1
  219. sphinx/util/template.py +3 -3
  220. sphinx/util/typing.py +36 -72
  221. sphinx/writers/html.py +1 -1
  222. sphinx/writers/html5.py +1 -1
  223. sphinx/writers/latex.py +4 -4
  224. sphinx/writers/manpage.py +2 -2
  225. sphinx/writers/texinfo.py +5 -5
  226. sphinx/writers/text.py +4 -4
  227. sphinx/writers/xml.py +2 -2
  228. {sphinx-7.4.7.dist-info → sphinx-8.0.0.dist-info}/METADATA +11 -10
  229. {sphinx-7.4.7.dist-info → sphinx-8.0.0.dist-info}/RECORD +232 -233
  230. sphinx/templates/quickstart/Makefile.jinja +0 -98
  231. sphinx/templates/quickstart/make.bat.jinja +0 -110
  232. {sphinx-7.4.7.dist-info → sphinx-8.0.0.dist-info}/LICENSE.rst +0 -0
  233. {sphinx-7.4.7.dist-info → sphinx-8.0.0.dist-info}/WHEEL +0 -0
  234. {sphinx-7.4.7.dist-info → sphinx-8.0.0.dist-info}/entry_points.txt +0 -0
sphinx/ext/doctest.py CHANGED
@@ -11,7 +11,7 @@ import sys
11
11
  import time
12
12
  from io import StringIO
13
13
  from os import path
14
- from typing import TYPE_CHECKING, Any, Callable, ClassVar
14
+ from typing import TYPE_CHECKING, Any, ClassVar
15
15
 
16
16
  from docutils import nodes
17
17
  from docutils.parsers.rst import directives
@@ -27,7 +27,7 @@ from sphinx.util.docutils import SphinxDirective
27
27
  from sphinx.util.osutil import relpath
28
28
 
29
29
  if TYPE_CHECKING:
30
- from collections.abc import Iterable, Sequence
30
+ from collections.abc import Callable, Iterable, Sequence
31
31
 
32
32
  from docutils.nodes import Element, Node, TextElement
33
33
 
@@ -373,7 +373,7 @@ Doctest summary
373
373
  try:
374
374
  filename = relpath(node.source, self.env.srcdir).rsplit(':docstring of ', maxsplit=1)[0] # type: ignore[arg-type] # noqa: E501
375
375
  except Exception:
376
- filename = self.env.doc2path(docname, False)
376
+ filename = str(self.env.doc2path(docname, False))
377
377
  return filename
378
378
 
379
379
  @staticmethod
@@ -420,12 +420,12 @@ Doctest summary
420
420
 
421
421
  if self.config.doctest_test_doctest_blocks:
422
422
  def condition(node: Node) -> bool:
423
- return (isinstance(node, (nodes.literal_block, nodes.comment)) and
423
+ return (isinstance(node, nodes.literal_block | nodes.comment) and
424
424
  'testnodetype' in node) or \
425
425
  isinstance(node, nodes.doctest_block)
426
426
  else:
427
427
  def condition(node: Node) -> bool:
428
- return isinstance(node, (nodes.literal_block, nodes.comment)) \
428
+ return isinstance(node, nodes.literal_block | nodes.comment) \
429
429
  and 'testnodetype' in node
430
430
  for node in doctree.findall(condition):
431
431
  if self.skipped(node): # type: ignore[arg-type]
sphinx/ext/graphviz.py CHANGED
@@ -32,7 +32,7 @@ if TYPE_CHECKING:
32
32
  from sphinx.application import Sphinx
33
33
  from sphinx.config import Config
34
34
  from sphinx.util.typing import ExtensionMetadata, OptionSpec
35
- from sphinx.writers.html import HTML5Translator
35
+ from sphinx.writers.html5 import HTML5Translator
36
36
  from sphinx.writers.latex import LaTeXTranslator
37
37
  from sphinx.writers.manpage import ManualPageTranslator
38
38
  from sphinx.writers.texinfo import TexinfoTranslator
sphinx/ext/imgmath.py CHANGED
@@ -36,7 +36,7 @@ if TYPE_CHECKING:
36
36
  from sphinx.builders import Builder
37
37
  from sphinx.config import Config
38
38
  from sphinx.util.typing import ExtensionMetadata
39
- from sphinx.writers.html import HTML5Translator
39
+ from sphinx.writers.html5 import HTML5Translator
40
40
 
41
41
  logger = logging.getLogger(__name__)
42
42
 
@@ -59,7 +59,7 @@ if TYPE_CHECKING:
59
59
  from sphinx.application import Sphinx
60
60
  from sphinx.environment import BuildEnvironment
61
61
  from sphinx.util.typing import ExtensionMetadata, OptionSpec
62
- from sphinx.writers.html import HTML5Translator
62
+ from sphinx.writers.html5 import HTML5Translator
63
63
  from sphinx.writers.latex import LaTeXTranslator
64
64
  from sphinx.writers.texinfo import TexinfoTranslator
65
65
 
@@ -21,9 +21,8 @@ from __future__ import annotations
21
21
  __all__ = (
22
22
  'InventoryAdapter',
23
23
  'fetch_inventory',
24
- 'fetch_inventory_group',
25
24
  'load_mappings',
26
- 'normalize_intersphinx_mapping',
25
+ 'validate_intersphinx_mapping',
27
26
  'IntersphinxRoleResolver',
28
27
  'inventory_exists',
29
28
  'install_dispatcher',
@@ -42,9 +41,8 @@ import sphinx
42
41
  from sphinx.ext.intersphinx._cli import inspect_main
43
42
  from sphinx.ext.intersphinx._load import (
44
43
  fetch_inventory,
45
- fetch_inventory_group,
46
44
  load_mappings,
47
- normalize_intersphinx_mapping,
45
+ validate_intersphinx_mapping,
48
46
  )
49
47
  from sphinx.ext.intersphinx._resolve import (
50
48
  IntersphinxDispatcher,
@@ -69,7 +67,7 @@ def setup(app: Sphinx) -> ExtensionMetadata:
69
67
  app.add_config_value('intersphinx_cache_limit', 5, '')
70
68
  app.add_config_value('intersphinx_timeout', None, '')
71
69
  app.add_config_value('intersphinx_disabled_reftypes', ['std:doc'], 'env')
72
- app.connect('config-inited', normalize_intersphinx_mapping, priority=800)
70
+ app.connect('config-inited', validate_intersphinx_mapping, priority=800)
73
71
  app.connect('builder-inited', load_mappings)
74
72
  app.connect('source-read', install_dispatcher)
75
73
  app.connect('missing-reference', missing_reference)
@@ -79,3 +77,25 @@ def setup(app: Sphinx) -> ExtensionMetadata:
79
77
  'env_version': 1,
80
78
  'parallel_read_safe': True,
81
79
  }
80
+
81
+
82
+ # deprecated name -> (object to return, canonical path or empty string, removal version)
83
+ _DEPRECATED_OBJECTS: dict[str, tuple[object, str, tuple[int, int]]] = {
84
+ 'normalize_intersphinx_mapping': (
85
+ validate_intersphinx_mapping,
86
+ 'sphinx.ext.intersphinx.validate_intersphinx_mapping',
87
+ (10, 0),
88
+ ),
89
+ }
90
+
91
+
92
+ def __getattr__(name: str) -> object:
93
+ if name not in _DEPRECATED_OBJECTS:
94
+ msg = f'module {__name__!r} has no attribute {name!r}'
95
+ raise AttributeError(msg)
96
+
97
+ from sphinx.deprecation import _deprecation_warning
98
+
99
+ deprecated_object, canonical_name, remove = _DEPRECATED_OBJECTS[name]
100
+ _deprecation_warning(__name__, name, canonical_name, remove=remove)
101
+ return deprecated_object
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import sys
6
6
 
7
- from sphinx.ext.intersphinx._load import fetch_inventory
7
+ from sphinx.ext.intersphinx._load import _fetch_inventory
8
8
 
9
9
 
10
10
  def inspect_main(argv: list[str], /) -> int:
@@ -21,13 +21,14 @@ def inspect_main(argv: list[str], /) -> int:
21
21
  tls_cacerts: str | dict[str, str] | None = None
22
22
  user_agent: str = ''
23
23
 
24
- class MockApp:
25
- srcdir = ''
26
- config = MockConfig()
27
-
28
24
  try:
29
25
  filename = argv[0]
30
- inv_data = fetch_inventory(MockApp(), '', filename) # type: ignore[arg-type]
26
+ inv_data = _fetch_inventory(
27
+ target_uri='',
28
+ inv_location=filename,
29
+ config=MockConfig(), # type: ignore[arg-type]
30
+ srcdir='' # type: ignore[arg-type]
31
+ )
31
32
  for key in sorted(inv_data or {}):
32
33
  print(key)
33
34
  inv_entries = sorted(inv_data[key].items())
@@ -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.