1313
1414from sphinx .builders .html import INVENTORY_FILENAME
1515from sphinx .errors import ConfigError
16- from sphinx .ext .intersphinx ._shared import LOGGER , InventoryAdapter
16+ from sphinx .ext .intersphinx ._shared import LOGGER , InventoryAdapter , _IntersphinxProject
1717from sphinx .locale import __
1818from sphinx .util import requests
1919from sphinx .util .inventory import InventoryFile
2020
2121if 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
227253def 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
0 commit comments