Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 46671f2

Browse files
authored
intersphinx: Add a type for project data (sphinx-doc#12657)
1 parent 34bc4e6 commit 46671f2

5 files changed

Lines changed: 180 additions & 49 deletions

File tree

sphinx/ext/intersphinx/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
__all__ = (
2222
'InventoryAdapter',
2323
'fetch_inventory',
24-
'fetch_inventory_group',
2524
'load_mappings',
2625
'validate_intersphinx_mapping',
2726
'IntersphinxRoleResolver',
@@ -42,7 +41,6 @@
4241
from sphinx.ext.intersphinx._cli import inspect_main
4342
from sphinx.ext.intersphinx._load import (
4443
fetch_inventory,
45-
fetch_inventory_group,
4644
load_mappings,
4745
validate_intersphinx_mapping,
4846
)

sphinx/ext/intersphinx/_cli.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import sys
66

7-
from sphinx.ext.intersphinx._load import fetch_inventory
7+
from sphinx.ext.intersphinx._load import _fetch_inventory
88

99

1010
def inspect_main(argv: list[str], /) -> int:
@@ -21,13 +21,14 @@ class MockConfig:
2121
tls_cacerts: str | dict[str, str] | None = None
2222
user_agent: str = ''
2323

24-
class MockApp:
25-
srcdir = ''
26-
config = MockConfig()
27-
2824
try:
2925
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+
)
3132
for key in sorted(inv_data or {}):
3233
print(key)
3334
inv_entries = sorted(inv_data[key].items())

sphinx/ext/intersphinx/_load.py

Lines changed: 77 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313

1414
from sphinx.builders.html import INVENTORY_FILENAME
1515
from sphinx.errors import ConfigError
16-
from sphinx.ext.intersphinx._shared import LOGGER, InventoryAdapter
16+
from sphinx.ext.intersphinx._shared import LOGGER, InventoryAdapter, _IntersphinxProject
1717
from sphinx.locale import __
1818
from sphinx.util import requests
1919
from sphinx.util.inventory import InventoryFile
2020

2121
if TYPE_CHECKING:
22+
from pathlib import Path
2223
from typing import IO
2324

2425
from sphinx.application import Sphinx
@@ -139,8 +140,17 @@ def load_mappings(app: Sphinx) -> None:
139140
intersphinx_cache: dict[InventoryURI, InventoryCacheEntry] = inventories.cache
140141
intersphinx_mapping: IntersphinxMapping = app.config.intersphinx_mapping
141142

142-
expected_uris = {uri for _name, (uri, _invs) in intersphinx_mapping.values()}
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)
143152

153+
expected_uris = {project.target_uri for project in projects}
144154
for uri in frozenset(intersphinx_cache):
145155
if intersphinx_cache[uri][0] not in intersphinx_mapping:
146156
# Remove all cached entries that are no longer in `intersphinx_mapping`.
@@ -153,8 +163,15 @@ def load_mappings(app: Sphinx) -> None:
153163

154164
with concurrent.futures.ThreadPoolExecutor() as pool:
155165
futures = [
156-
pool.submit(fetch_inventory_group, name, uri, invs, intersphinx_cache, app, now)
157-
for name, (uri, invs) in app.config.intersphinx_mapping.values()
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
158175
]
159176
updated = [f.result() for f in concurrent.futures.as_completed(futures)]
160177

@@ -176,43 +193,52 @@ def load_mappings(app: Sphinx) -> None:
176193
inventories.main_inventory.setdefault(objtype, {}).update(objects)
177194

178195

179-
def fetch_inventory_group(
180-
name: InventoryName,
181-
uri: InventoryURI,
182-
invs: tuple[InventoryLocation, ...],
196+
def _fetch_inventory_group(
197+
*,
198+
project: _IntersphinxProject,
183199
cache: dict[InventoryURI, InventoryCacheEntry],
184-
app: Sphinx,
185200
now: int,
201+
config: Config,
202+
srcdir: Path,
186203
) -> bool:
187-
cache_time = now - app.config.intersphinx_cache_limit * 86400
204+
cache_time = now - config.intersphinx_cache_limit * 86400
188205

189206
updated = False
190207
failures = []
191208

192-
for location in invs:
209+
for location in project.locations:
193210
# location is either None or a non-empty string
194-
inv = f'{uri}/{INVENTORY_FILENAME}' if location is None else location
211+
inv = f'{project.target_uri}/{INVENTORY_FILENAME}' if location is None else location
195212

196213
# decide whether the inventory must be read: always read local
197214
# files; remote ones only if the cache time is expired
198-
if '://' not in inv or uri not in cache or cache[uri][1] < cache_time:
215+
if (
216+
'://' not in inv
217+
or project.target_uri not in cache
218+
or cache[project.target_uri][1] < cache_time
219+
):
199220
LOGGER.info(__("loading intersphinx inventory '%s' from %s ..."),
200-
name, _get_safe_url(inv))
221+
project.name, _get_safe_url(inv))
201222

202223
try:
203-
invdata = fetch_inventory(app, uri, inv)
224+
invdata = _fetch_inventory(
225+
target_uri=project.target_uri,
226+
inv_location=inv,
227+
config=config,
228+
srcdir=srcdir,
229+
)
204230
except Exception as err:
205231
failures.append(err.args)
206232
continue
207233

208234
if invdata:
209-
cache[uri] = name, now, invdata
235+
cache[project.target_uri] = project.name, now, invdata
210236
updated = True
211237
break
212238

213239
if not failures:
214240
pass
215-
elif len(failures) < len(invs):
241+
elif len(failures) < len(project.locations):
216242
LOGGER.info(__('encountered some issues with some of the inventories,'
217243
' but they had working alternatives:'))
218244
for fail in failures:
@@ -226,36 +252,54 @@ def fetch_inventory_group(
226252

227253
def fetch_inventory(app: Sphinx, uri: InventoryURI, inv: str) -> Inventory:
228254
"""Fetch, parse and return an intersphinx inventory file."""
229-
# both *uri* (base URI of the links to generate) and *inv* (actual
230-
# location of the inventory file) can be local or remote URIs
231-
if '://' in uri:
255+
return _fetch_inventory(
256+
target_uri=uri,
257+
inv_location=inv,
258+
config=app.config,
259+
srcdir=app.srcdir,
260+
)
261+
262+
263+
def _fetch_inventory(
264+
*, target_uri: InventoryURI, inv_location: str, config: Config, srcdir: Path,
265+
) -> Inventory:
266+
"""Fetch, parse and return an intersphinx inventory file."""
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:
232271
# case: inv URI points to remote resource; strip any existing auth
233-
uri = _strip_basic_auth(uri)
272+
target_uri = _strip_basic_auth(target_uri)
234273
try:
235-
if '://' in inv:
236-
f = _read_from_url(inv, config=app.config)
274+
if '://' in inv_location:
275+
f = _read_from_url(inv_location, config=config)
237276
else:
238-
f = open(path.join(app.srcdir, inv), 'rb') # NoQA: SIM115
277+
f = open(path.join(srcdir, inv_location), 'rb') # NoQA: SIM115
239278
except Exception as err:
240279
err.args = ('intersphinx inventory %r not fetchable due to %s: %s',
241-
inv, err.__class__, str(err))
280+
inv_location, err.__class__, str(err))
242281
raise
243282
try:
244283
if hasattr(f, 'url'):
245-
newinv = f.url
246-
if inv != newinv:
247-
LOGGER.info(__('intersphinx inventory has moved: %s -> %s'), inv, newinv)
248-
249-
if uri in (inv, path.dirname(inv), path.dirname(inv) + '/'):
250-
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)
251295
with f:
252296
try:
253-
invdata = InventoryFile.load(f, uri, posixpath.join)
297+
invdata = InventoryFile.load(f, target_uri, posixpath.join)
254298
except ValueError as exc:
255299
raise ValueError('unknown or unsupported inventory version: %r' % exc) from exc
256300
except Exception as err:
257301
err.args = ('intersphinx inventory %r not readable due to %s: %s',
258-
inv, err.__class__.__name__, str(err))
302+
inv_location, err.__class__.__name__, str(err))
259303
raise
260304
else:
261305
return invdata

sphinx/ext/intersphinx/_shared.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
from __future__ import annotations
44

5-
from typing import TYPE_CHECKING, Final
5+
from typing import TYPE_CHECKING, Any, Final, NoReturn
66

77
from sphinx.util import logging
88

99
if TYPE_CHECKING:
10+
from collections.abc import Sequence
1011
from typing import TypeAlias
1112

1213
from sphinx.environment import BuildEnvironment
@@ -31,7 +32,7 @@
3132
#: Inventory cache entry. The integer field is the cache expiration time.
3233
InventoryCacheEntry: TypeAlias = tuple[InventoryName, int, Inventory]
3334

34-
#: The type of :confval:`intersphinx_mapping` *after* normalization.
35+
#: The type of :confval:`intersphinx_mapping` *after* normalisation.
3536
IntersphinxMapping = dict[
3637
InventoryName,
3738
tuple[InventoryName, tuple[InventoryURI, tuple[InventoryLocation, ...]]],
@@ -40,6 +41,74 @@
4041
LOGGER: Final[logging.SphinxLoggerAdapter] = logging.getLogger('sphinx.ext.intersphinx')
4142

4243

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+
43112
class InventoryAdapter:
44113
"""Inventory adapter for environment"""
45114

0 commit comments

Comments
 (0)