Sphinx 7.4.7__py3-none-any.whl → 8.0.0rc1__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.
- sphinx/__init__.py +2 -2
- sphinx/_cli/__init__.py +4 -4
- sphinx/application.py +7 -7
- sphinx/builders/__init__.py +2 -3
- sphinx/builders/_epub_base.py +33 -12
- sphinx/builders/changes.py +13 -5
- sphinx/builders/epub3.py +6 -2
- sphinx/builders/html/__init__.py +88 -58
- sphinx/builders/latex/__init__.py +38 -12
- sphinx/builders/latex/transforms.py +1 -1
- sphinx/builders/linkcheck.py +8 -49
- sphinx/builders/texinfo.py +12 -6
- sphinx/builders/text.py +7 -3
- sphinx/builders/xml.py +7 -3
- sphinx/cmd/quickstart.py +10 -20
- sphinx/config.py +12 -12
- sphinx/deprecation.py +8 -8
- sphinx/directives/other.py +2 -3
- sphinx/directives/patches.py +2 -2
- sphinx/domains/__init__.py +4 -2
- sphinx/domains/c/__init__.py +2 -2
- sphinx/domains/c/_ast.py +3 -2
- sphinx/domains/c/_parser.py +4 -3
- sphinx/domains/cpp/__init__.py +2 -2
- sphinx/domains/cpp/_ast.py +1 -2
- sphinx/domains/cpp/_parser.py +2 -2
- sphinx/domains/cpp/_symbol.py +2 -2
- sphinx/domains/math.py +1 -1
- sphinx/domains/python/_object.py +0 -1
- sphinx/domains/std/__init__.py +7 -8
- sphinx/environment/__init__.py +14 -32
- sphinx/environment/adapters/indexentries.py +4 -6
- sphinx/environment/adapters/toctree.py +4 -4
- sphinx/environment/collectors/title.py +1 -1
- sphinx/environment/collectors/toctree.py +1 -1
- sphinx/events.py +3 -1
- sphinx/ext/autodoc/__init__.py +17 -63
- sphinx/ext/autodoc/directive.py +7 -5
- sphinx/ext/autodoc/importer.py +2 -1
- sphinx/ext/autodoc/preserve_defaults.py +2 -2
- sphinx/ext/autosummary/__init__.py +7 -6
- sphinx/ext/autosummary/generate.py +5 -4
- sphinx/ext/doctest.py +5 -5
- sphinx/ext/graphviz.py +1 -1
- sphinx/ext/imgmath.py +1 -1
- sphinx/ext/inheritance_diagram.py +1 -1
- sphinx/ext/intersphinx/__init__.py +25 -5
- sphinx/ext/intersphinx/_cli.py +7 -6
- sphinx/ext/intersphinx/_load.py +240 -115
- sphinx/ext/intersphinx/_resolve.py +12 -11
- sphinx/ext/intersphinx/_shared.py +102 -9
- sphinx/ext/mathjax.py +1 -1
- sphinx/ext/napoleon/docstring.py +2 -2
- sphinx/ext/todo.py +2 -2
- sphinx/ext/viewcode.py +2 -1
- sphinx/highlighting.py +3 -3
- sphinx/io.py +2 -2
- sphinx/jinja2glue.py +13 -6
- sphinx/locale/__init__.py +4 -3
- sphinx/project.py +23 -19
- sphinx/pycode/ast.py +2 -2
- sphinx/pycode/parser.py +2 -2
- sphinx/pygments_styles.py +3 -3
- sphinx/registry.py +3 -8
- sphinx/search/__init__.py +1 -1
- sphinx/testing/path.py +2 -1
- sphinx/testing/util.py +1 -1
- sphinx/texinputs/Makefile.jinja +2 -1
- sphinx/texinputs_win/Makefile.jinja +2 -1
- sphinx/theming.py +3 -12
- sphinx/transforms/__init__.py +5 -5
- sphinx/transforms/references.py +1 -1
- sphinx/util/__init__.py +11 -35
- sphinx/util/_timestamps.py +12 -0
- sphinx/util/cfamily.py +5 -5
- sphinx/util/console.py +4 -3
- sphinx/util/display.py +3 -3
- sphinx/util/docfields.py +1 -1
- sphinx/util/docutils.py +44 -10
- sphinx/util/fileutil.py +25 -20
- sphinx/util/i18n.py +9 -4
- sphinx/util/images.py +3 -2
- sphinx/util/inspect.py +28 -43
- sphinx/util/inventory.py +2 -2
- sphinx/util/matching.py +2 -2
- sphinx/util/math.py +1 -1
- sphinx/util/nodes.py +8 -8
- sphinx/util/osutil.py +29 -28
- sphinx/util/parallel.py +2 -2
- sphinx/util/requests.py +1 -1
- sphinx/util/template.py +3 -3
- sphinx/util/typing.py +36 -72
- sphinx/writers/html.py +1 -1
- sphinx/writers/html5.py +1 -1
- sphinx/writers/latex.py +4 -4
- sphinx/writers/manpage.py +2 -2
- sphinx/writers/texinfo.py +5 -5
- sphinx/writers/text.py +4 -4
- sphinx/writers/xml.py +2 -2
- {sphinx-7.4.7.dist-info → sphinx-8.0.0rc1.dist-info}/METADATA +10 -9
- {sphinx-7.4.7.dist-info → sphinx-8.0.0rc1.dist-info}/RECORD +104 -106
- sphinx/templates/quickstart/Makefile.jinja +0 -98
- sphinx/templates/quickstart/make.bat.jinja +0 -110
- sphinx/util/_pathlib.py +0 -120
- {sphinx-7.4.7.dist-info → sphinx-8.0.0rc1.dist-info}/LICENSE.rst +0 -0
- {sphinx-7.4.7.dist-info → sphinx-8.0.0rc1.dist-info}/WHEEL +0 -0
- {sphinx-7.4.7.dist-info → sphinx-8.0.0rc1.dist-info}/entry_points.txt +0 -0
|
@@ -69,13 +69,14 @@ import sphinx
|
|
|
69
69
|
from sphinx import addnodes
|
|
70
70
|
from sphinx.config import Config
|
|
71
71
|
from sphinx.environment import BuildEnvironment
|
|
72
|
-
from sphinx.
|
|
73
|
-
from sphinx.ext.autodoc
|
|
72
|
+
from sphinx.errors import PycodeError
|
|
73
|
+
from sphinx.ext.autodoc import INSTANCEATTR, Documenter, Options
|
|
74
|
+
from sphinx.ext.autodoc.directive import DocumenterBridge
|
|
74
75
|
from sphinx.ext.autodoc.importer import import_module
|
|
75
76
|
from sphinx.ext.autodoc.mock import mock
|
|
76
77
|
from sphinx.locale import __
|
|
77
78
|
from sphinx.project import Project
|
|
78
|
-
from sphinx.pycode import ModuleAnalyzer
|
|
79
|
+
from sphinx.pycode import ModuleAnalyzer
|
|
79
80
|
from sphinx.registry import SphinxComponentRegistry
|
|
80
81
|
from sphinx.util import logging, rst
|
|
81
82
|
from sphinx.util.docutils import (
|
|
@@ -97,7 +98,7 @@ if TYPE_CHECKING:
|
|
|
97
98
|
from sphinx.application import Sphinx
|
|
98
99
|
from sphinx.extension import Extension
|
|
99
100
|
from sphinx.util.typing import ExtensionMetadata, OptionSpec
|
|
100
|
-
from sphinx.writers.
|
|
101
|
+
from sphinx.writers.html5 import HTML5Translator
|
|
101
102
|
|
|
102
103
|
logger = logging.getLogger(__name__)
|
|
103
104
|
|
|
@@ -248,7 +249,7 @@ class Autosummary(SphinxDirective):
|
|
|
248
249
|
docname = posixpath.join(tree_prefix, real_name)
|
|
249
250
|
docname = posixpath.normpath(posixpath.join(dirname, docname))
|
|
250
251
|
if docname not in self.env.found_docs:
|
|
251
|
-
if excluded(self.env.doc2path(docname, False)):
|
|
252
|
+
if excluded(str(self.env.doc2path(docname, False))):
|
|
252
253
|
msg = __('autosummary references excluded document %r. Ignored.')
|
|
253
254
|
else:
|
|
254
255
|
msg = __('autosummary: stub file not found %r. '
|
|
@@ -801,7 +802,7 @@ def process_generate_options(app: Sphinx) -> None:
|
|
|
801
802
|
|
|
802
803
|
if genfiles is True:
|
|
803
804
|
env = app.builder.env
|
|
804
|
-
genfiles = [env.doc2path(x, base=False) for x in env.found_docs
|
|
805
|
+
genfiles = [str(env.doc2path(x, base=False)) for x in env.found_docs
|
|
805
806
|
if os.path.isfile(env.doc2path(x))]
|
|
806
807
|
elif genfiles is False:
|
|
807
808
|
pass
|
|
@@ -34,6 +34,7 @@ import sphinx.locale
|
|
|
34
34
|
from sphinx import __display_version__, package_dir
|
|
35
35
|
from sphinx.builders import Builder
|
|
36
36
|
from sphinx.config import Config
|
|
37
|
+
from sphinx.errors import PycodeError
|
|
37
38
|
from sphinx.ext.autodoc.importer import import_module
|
|
38
39
|
from sphinx.ext.autosummary import (
|
|
39
40
|
ImportExceptionGroup,
|
|
@@ -42,7 +43,7 @@ from sphinx.ext.autosummary import (
|
|
|
42
43
|
import_ivar_by_name,
|
|
43
44
|
)
|
|
44
45
|
from sphinx.locale import __
|
|
45
|
-
from sphinx.pycode import ModuleAnalyzer
|
|
46
|
+
from sphinx.pycode import ModuleAnalyzer
|
|
46
47
|
from sphinx.registry import SphinxComponentRegistry
|
|
47
48
|
from sphinx.util import logging, rst
|
|
48
49
|
from sphinx.util.inspect import getall, safe_getattr
|
|
@@ -145,7 +146,7 @@ class AutosummaryRenderer:
|
|
|
145
146
|
# ``install_gettext_translations`` is injected by the ``jinja2.ext.i18n`` extension
|
|
146
147
|
self.env.install_gettext_translations(app.translator) # type: ignore[attr-defined]
|
|
147
148
|
|
|
148
|
-
def render(self, template_name: str, context: dict) -> str:
|
|
149
|
+
def render(self, template_name: str, context: dict[str, Any]) -> str:
|
|
149
150
|
"""Render a template file."""
|
|
150
151
|
try:
|
|
151
152
|
template = self.env.get_template(template_name)
|
|
@@ -282,7 +283,7 @@ def generate_autosummary_content(
|
|
|
282
283
|
imported_members: bool,
|
|
283
284
|
app: Any,
|
|
284
285
|
recursive: bool,
|
|
285
|
-
context: dict,
|
|
286
|
+
context: dict[str, Any],
|
|
286
287
|
modname: str | None = None,
|
|
287
288
|
qualname: str | None = None,
|
|
288
289
|
) -> str:
|
|
@@ -392,7 +393,7 @@ def _skip_member(app: Sphinx, obj: Any, name: str, objtype: str) -> bool:
|
|
|
392
393
|
|
|
393
394
|
|
|
394
395
|
def _get_class_members(obj: Any) -> dict[str, Any]:
|
|
395
|
-
members = sphinx.ext.autodoc.get_class_members(obj, None, safe_getattr)
|
|
396
|
+
members = sphinx.ext.autodoc.importer.get_class_members(obj, None, safe_getattr)
|
|
396
397
|
return {name: member.object for name, member in members.items()}
|
|
397
398
|
|
|
398
399
|
|
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,
|
|
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,
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
'
|
|
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
|
-
|
|
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',
|
|
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
|
sphinx/ext/intersphinx/_cli.py
CHANGED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import sys
|
|
6
6
|
|
|
7
|
-
from sphinx.ext.intersphinx._load import
|
|
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 =
|
|
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())
|
sphinx/ext/intersphinx/_load.py
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
29
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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.
|
|
119
|
+
LOGGER.error(msg, target, name)
|
|
120
|
+
del config.intersphinx_mapping[name]
|
|
121
|
+
continue
|
|
51
122
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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[
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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 -
|
|
204
|
+
cache_time = now - config.intersphinx_cache_limit * 86400
|
|
205
|
+
|
|
206
|
+
updated = False
|
|
111
207
|
failures = []
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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 *
|
|
149
|
-
# location of the inventory file)
|
|
150
|
-
|
|
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
|
-
|
|
272
|
+
target_uri = _strip_basic_auth(target_uri)
|
|
153
273
|
try:
|
|
154
|
-
if '://' in
|
|
155
|
-
f = _read_from_url(
|
|
274
|
+
if '://' in inv_location:
|
|
275
|
+
f = _read_from_url(inv_location, config=config)
|
|
156
276
|
else:
|
|
157
|
-
f = open(path.join(
|
|
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
|
-
|
|
280
|
+
inv_location, err.__class__, str(err))
|
|
161
281
|
raise
|
|
162
282
|
try:
|
|
163
283
|
if hasattr(f, 'url'):
|
|
164
|
-
|
|
165
|
-
if
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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,
|
|
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
|
-
|
|
302
|
+
inv_location, err.__class__.__name__, str(err))
|
|
178
303
|
raise
|
|
179
304
|
else:
|
|
180
305
|
return invdata
|