diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index f6e6d7964..95410c0c4 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -29,26 +29,32 @@ from typing import IO, TYPE_CHECKING, Any, cast from urllib.parse import urlsplit, urlunsplit from docutils import nodes -from docutils.nodes import Element, Node, TextElement, system_message -from docutils.utils import Reporter, relative_path +from docutils.utils import relative_path import sphinx from sphinx.addnodes import pending_xref -from sphinx.application import Sphinx from sphinx.builders.html import INVENTORY_FILENAME -from sphinx.config import Config -from sphinx.domains import Domain -from sphinx.environment import BuildEnvironment from sphinx.errors import ExtensionError from sphinx.locale import _, __ from sphinx.transforms.post_transforms import ReferencesResolver from sphinx.util import logging, requests from sphinx.util.docutils import CustomReSTDispatcher, SphinxRole from sphinx.util.inventory import InventoryFile -from sphinx.util.typing import Inventory, InventoryItem, RoleFunction if TYPE_CHECKING: from types import ModuleType + from typing import Tuple, Union + + from docutils.nodes import Node, TextElement, system_message + from docutils.utils import Reporter + + from sphinx.application import Sphinx + from sphinx.config import Config + from sphinx.domains import Domain + from sphinx.environment import BuildEnvironment + from sphinx.util.typing import Inventory, InventoryItem, RoleFunction + + InventoryCacheEntry = Tuple[Union[str, None], int, Inventory] logger = logging.getLogger(__name__) @@ -60,12 +66,22 @@ class InventoryAdapter: self.env = env if not hasattr(env, 'intersphinx_cache'): + # initial storage when fetching inventories before processing self.env.intersphinx_cache = {} # type: ignore + self.env.intersphinx_inventory = {} # type: ignore self.env.intersphinx_named_inventory = {} # type: ignore @property - def cache(self) -> dict[str, tuple[str, int, Inventory]]: + def cache(self) -> dict[str, InventoryCacheEntry]: + """Intersphinx cache. + + - Key is the URI of the remote inventory + - Element one is the key given in the Sphinx intersphinx_mapping + configuration value + - Element two is a time value for cache invalidation, a float + - Element three is the loaded remote inventory, type Inventory + """ return self.env.intersphinx_cache # type: ignore @property @@ -152,7 +168,7 @@ def _get_safe_url(url: str) -> str: return urlunsplit(frags) -def fetch_inventory(app: Sphinx, uri: str, inv: Any) -> Any: +def fetch_inventory(app: Sphinx, uri: str, inv: str) -> Inventory: """Fetch, parse and return an intersphinx inventory file.""" # both *uri* (base URI of the links to generate) and *inv* (actual # location of the inventory file) can be local or remote URIs @@ -192,7 +208,12 @@ def fetch_inventory(app: Sphinx, uri: str, inv: Any) -> Any: def fetch_inventory_group( - name: str, uri: str, invs: Any, cache: Any, app: Any, now: float, + name: str | None, + uri: str, + invs: tuple[str | None, ...], + cache: dict[str, InventoryCacheEntry], + app: Sphinx, + now: int, ) -> bool: cache_time = now - app.config.intersphinx_cache_limit * 86400 failures = [] @@ -211,7 +232,7 @@ def fetch_inventory_group( failures.append(err.args) continue if invdata: - cache[uri] = (name, now, invdata) + cache[uri] = name, now, invdata return True return False finally: @@ -235,6 +256,9 @@ def load_mappings(app: Sphinx) -> None: with concurrent.futures.ThreadPoolExecutor() as pool: futures = [] + name: str | None + uri: str + invs: tuple[str | None, ...] for name, (uri, invs) in app.config.intersphinx_mapping.values(): futures.append(pool.submit( fetch_inventory_group, name, uri, invs, inventories.cache, app, now, @@ -263,7 +287,7 @@ def load_mappings(app: Sphinx) -> None: def _create_element_from_result(domain: Domain, inv_name: str | None, data: InventoryItem, - node: pending_xref, contnode: TextElement) -> Element: + node: pending_xref, contnode: TextElement) -> nodes.reference: proj, version, uri, dispname = data if '://' not in uri and node.get('refdoc'): # get correct path in case of subdirectories @@ -295,7 +319,7 @@ def _resolve_reference_in_domain_by_target( inv_name: str | None, inventory: Inventory, domain: Domain, objtypes: list[str], target: str, - node: pending_xref, contnode: TextElement) -> Element | None: + node: pending_xref, contnode: TextElement) -> nodes.reference | None: for objtype in objtypes: if objtype not in inventory: # Continue if there's nothing of this kind in the inventory @@ -328,7 +352,7 @@ def _resolve_reference_in_domain(env: BuildEnvironment, honor_disabled_refs: bool, domain: Domain, objtypes: list[str], node: pending_xref, contnode: TextElement, - ) -> Element | None: + ) -> nodes.reference | None: # we adjust the object types for backwards compatibility if domain.name == 'std' and 'cmdoption' in objtypes: # until Sphinx-1.6, cmdoptions are stored as std:option @@ -361,7 +385,7 @@ def _resolve_reference_in_domain(env: BuildEnvironment, def _resolve_reference(env: BuildEnvironment, inv_name: str | None, inventory: Inventory, honor_disabled_refs: bool, - node: pending_xref, contnode: TextElement) -> Element | None: + node: pending_xref, contnode: TextElement) -> nodes.reference | None: # disabling should only be done if no inventory is given honor_disabled_refs = honor_disabled_refs and inv_name is None @@ -407,7 +431,7 @@ def inventory_exists(env: BuildEnvironment, inv_name: str) -> bool: def resolve_reference_in_inventory(env: BuildEnvironment, inv_name: str, node: pending_xref, contnode: TextElement, - ) -> Element | None: + ) -> nodes.reference | None: """Attempt to resolve a missing reference via intersphinx references. Resolution is tried in the given inventory with the target as is. @@ -422,7 +446,7 @@ def resolve_reference_in_inventory(env: BuildEnvironment, def resolve_reference_any_inventory(env: BuildEnvironment, honor_disabled_refs: bool, node: pending_xref, contnode: TextElement, - ) -> Element | None: + ) -> nodes.reference | None: """Attempt to resolve a missing reference via intersphinx references. Resolution is tried with the target as is in any inventory. @@ -434,7 +458,7 @@ def resolve_reference_any_inventory(env: BuildEnvironment, def resolve_reference_detect_inventory(env: BuildEnvironment, node: pending_xref, contnode: TextElement, - ) -> Element | None: + ) -> nodes.reference | None: """Attempt to resolve a missing reference via intersphinx references. Resolution is tried first with the target as is in any inventory. @@ -462,7 +486,7 @@ def resolve_reference_detect_inventory(env: BuildEnvironment, def missing_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref, - contnode: TextElement) -> Element | None: + contnode: TextElement) -> nodes.reference | None: """Attempt to resolve a missing reference via intersphinx references.""" return resolve_reference_detect_inventory(env, node, contnode) diff --git a/sphinx/util/inventory.py b/sphinx/util/inventory.py index a879a17dd..123a86d25 100644 --- a/sphinx/util/inventory.py +++ b/sphinx/util/inventory.py @@ -7,7 +7,7 @@ import zlib from typing import IO, TYPE_CHECKING, Callable, Iterator from sphinx.util import logging -from sphinx.util.typing import Inventory +from sphinx.util.typing import Inventory, InventoryItem BUFSIZE = 16 * 1024 logger = logging.getLogger(__name__) @@ -133,8 +133,8 @@ class InventoryFile: if location.endswith('$'): location = location[:-1] + name location = join(uri, location) - invdata.setdefault(type, {})[name] = (projname, version, - location, dispname) + inv_item: InventoryItem = projname, version, location, dispname + invdata.setdefault(type, {})[name] = inv_item return invdata @classmethod diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 3f2acb822..d4e87ef1d 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -51,7 +51,12 @@ OptionSpec = Dict[str, Callable[[str], Any]] TitleGetter = Callable[[nodes.Node], str] # inventory data on memory -InventoryItem = Tuple[str, str, str, str] +InventoryItem = Tuple[ + str, # project name + str, # project version + str, # URL + str, # display name +] Inventory = Dict[str, Dict[str, InventoryItem]] diff --git a/tests/test_util_inventory.py b/tests/test_util_inventory.py index e79602330..583edf669 100644 --- a/tests/test_util_inventory.py +++ b/tests/test_util_inventory.py @@ -4,7 +4,7 @@ import posixpath import zlib from io import BytesIO -from sphinx.ext.intersphinx import InventoryFile +from sphinx.util.inventory import InventoryFile inventory_v1 = b'''\ # Sphinx inventory version 1