Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] deprecate intersphinx format #12083

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 0 additions & 22 deletions doc/usage/extensions/intersphinx.rst
Expand Up @@ -126,28 +126,6 @@ linking:
('../../otherbook/build/html/objects.inv', None)),
}

**Old format for this config value**

.. deprecated:: 6.2

.. RemovedInSphinx80Warning

.. caution:: This is the format used before Sphinx 1.0.
It is deprecated and will be removed in Sphinx 8.0.

A dictionary mapping URIs to either ``None`` or an URI. The keys are the
base URI of the foreign Sphinx documentation sets and can be local paths or
HTTP URIs. The values indicate where the inventory file can be found: they
can be ``None`` (at the same location as the base URI) or another local or
HTTP URI.

Example:

.. code:: python

intersphinx_mapping = {'https://docs.python.org/': None}


.. confval:: intersphinx_cache_limit

The maximum number of days to cache remote inventories. The default is
Expand Down
123 changes: 84 additions & 39 deletions sphinx/ext/intersphinx/_load.py
Expand Up @@ -21,55 +21,100 @@

from sphinx.application import Sphinx
from sphinx.config import Config
from sphinx.ext.intersphinx._shared import InventoryCacheEntry
from sphinx.ext.intersphinx._shared import (
IntersphinxMapping,
InventoryCacheEntry,
InventoryLocation,
InventoryName,
InventoryURI,
)
from sphinx.util.typing import Inventory


def normalize_intersphinx_mapping(app: Sphinx, config: Config) -> None:
for key, value in config.intersphinx_mapping.copy().items():
# URIs should NOT be duplicated, otherwise different builds may use
# different project names (and thus, the build are no more reproducible)
# depending on which one is inserted last in the cache.
seen: dict[InventoryURI, InventoryName] = {}

for name, value in config.intersphinx_mapping.copy().items():
if not isinstance(name, str):
LOGGER.warning(__('intersphinx identifier %r is not string. Ignored'),
name, type='intersphinx', subtype='config')
del config.intersphinx_mapping[name]
continue

# ensure that intersphinx projects are always named
if not name:
LOGGER.warning(
__('ignoring empty intersphinx identifier'),
type='intersphinx', subtype='config',
)
del config.intersphinx_mapping[name]
continue

if not isinstance(value, (tuple, list)):
LOGGER.warning(
__('intersphinx_mapping[%r]: expecting a tuple or a list, got: %r; ignoring.'),
name, value, type='intersphinx', subtype='config',
)
del config.intersphinx_mapping[name]
continue

try:
if isinstance(value, (list, tuple)):
# new format
name, (uri, inv) = key, value
if not isinstance(name, str):
LOGGER.warning(__('intersphinx identifier %r is not string. Ignored'),
name)
config.intersphinx_mapping.pop(key)
continue
uri, inv = value
except Exception as exc:
LOGGER.warning(
__('Failed to read intersphinx_mapping[%s], ignored: %r'),
name, exc, type='intersphinx', subtype='config',
)
del config.intersphinx_mapping[name]
continue

if not uri or not isinstance(uri, str):
LOGGER.warning(
__('intersphinx_mapping[%r]: URI must be a non-empty string, '
'got: %r; ignoring.'),
name, uri, type='intersphinx', subtype='config',
)
del config.intersphinx_mapping[name]
continue

if (name_for_uri := seen.setdefault(uri, name)) != name:
LOGGER.warning(
__('intersphinx_mapping[%r]: URI %r shadows URI from intersphinx_mapping[%r]; '
'ignoring.'), name, uri, name_for_uri, type='intersphinx', subtype='config',
)
del config.intersphinx_mapping[name]
continue

targets: list[InventoryLocation] = []
for target in (inv if isinstance(inv, (tuple, list)) else (inv,)):
if target is None or target and isinstance(target, str):
targets.append(target)
else:
# old format, no name
# xref RemovedInSphinx80Warning
name, uri, inv = None, key, value
msg = (
"The pre-Sphinx 1.0 'intersphinx_mapping' format is "
'deprecated and will be removed in Sphinx 8. Update to the '
'current format as described in the documentation. '
f"Hint: `intersphinx_mapping = {{'<name>': {(uri, inv)!r}}}`."
'https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#confval-intersphinx_mapping' # NoQA: E501
LOGGER.warning(
__('intersphinx_mapping[%r]: inventory location must '
'be a non-empty string or None, got: %r; ignoring.'),
name, target, type='intersphinx', subtype='config',
)
LOGGER.warning(msg)

if not isinstance(inv, tuple):
config.intersphinx_mapping[key] = (name, (uri, (inv,)))
else:
config.intersphinx_mapping[key] = (name, (uri, inv))
except Exception as exc:
LOGGER.warning(__('Failed to read intersphinx_mapping[%s], ignored: %r'), key, exc)
config.intersphinx_mapping.pop(key)
config.intersphinx_mapping[name] = (name, (uri, tuple(targets)))


def load_mappings(app: Sphinx) -> None:
"""Load all intersphinx mappings into the environment."""
"""Load all intersphinx mappings into the environment.

The intersphinx mappings are expected to be normalized.
"""
now = int(time.time())
inventories = InventoryAdapter(app.builder.env)
intersphinx_cache: dict[str, InventoryCacheEntry] = inventories.cache
intersphinx_cache: dict[InventoryURI, InventoryCacheEntry] = inventories.cache
intersphinx_mapping: IntersphinxMapping = app.config.intersphinx_mapping

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():
for name, (uri, invs) in intersphinx_mapping.values():
futures.append(pool.submit(
fetch_inventory_group, name, uri, invs, intersphinx_cache, app, now,
))
Expand Down Expand Up @@ -100,10 +145,10 @@ def load_mappings(app: Sphinx) -> None:


def fetch_inventory_group(
name: str | None,
uri: str,
invs: tuple[str | None, ...],
cache: dict[str, InventoryCacheEntry],
name: InventoryName,
uri: InventoryURI,
invs: tuple[InventoryLocation, ...],
cache: dict[InventoryURI, InventoryCacheEntry],
app: Sphinx,
now: int,
) -> bool:
Expand All @@ -128,7 +173,7 @@ def fetch_inventory_group(
return True
return False
finally:
if failures == []:
if not failures:
pass
elif len(failures) < len(invs):
LOGGER.info(__('encountered some issues with some of the inventories,'
Expand All @@ -141,7 +186,7 @@ def fetch_inventory_group(
'with the following issues:') + '\n' + issues)


def fetch_inventory(app: Sphinx, uri: str, inv: str) -> Inventory:
def fetch_inventory(app: Sphinx, uri: InventoryURI, 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
Expand Down
23 changes: 12 additions & 11 deletions sphinx/ext/intersphinx/_resolve.py
Expand Up @@ -28,10 +28,11 @@
from sphinx.application import Sphinx
from sphinx.domains import Domain
from sphinx.environment import BuildEnvironment
from sphinx.ext.intersphinx._shared import InventoryName
from sphinx.util.typing import Inventory, InventoryItem, RoleFunction


def _create_element_from_result(domain: Domain, inv_name: str | None,
def _create_element_from_result(domain: Domain, inv_name: InventoryName | None,
data: InventoryItem,
node: pending_xref, contnode: TextElement) -> nodes.reference:
proj, version, uri, dispname = data
Expand Down Expand Up @@ -61,7 +62,7 @@ def _create_element_from_result(domain: Domain, inv_name: str | None,


def _resolve_reference_in_domain_by_target(
inv_name: str | None, inventory: Inventory,
inv_name: InventoryName | None, inventory: Inventory,
domain: Domain, objtypes: Iterable[str],
target: str,
node: pending_xref, contnode: TextElement) -> nodes.reference | None:
Expand Down Expand Up @@ -95,7 +96,7 @@ def _resolve_reference_in_domain_by_target(


def _resolve_reference_in_domain(env: BuildEnvironment,
inv_name: str | None, inventory: Inventory,
inv_name: InventoryName | None, inventory: Inventory,
honor_disabled_refs: bool,
domain: Domain, objtypes: Iterable[str],
node: pending_xref, contnode: TextElement,
Expand Down Expand Up @@ -137,20 +138,21 @@ def _resolve_reference_in_domain(env: BuildEnvironment,
full_qualified_name, node, contnode)


def _resolve_reference(env: BuildEnvironment, inv_name: str | None, inventory: Inventory,
def _resolve_reference(env: BuildEnvironment,
inv_name: InventoryName | None, inventory: Inventory,
honor_disabled_refs: bool,
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
intersphinx_disabled_reftypes = env.config.intersphinx_disabled_reftypes

if honor_disabled_refs and '*' in env.config.intersphinx_disabled_reftypes:
if honor_disabled_refs and '*' in intersphinx_disabled_reftypes:
return None

typ = node['reftype']
if typ == 'any':
for domain_name, domain in env.domains.items():
if (honor_disabled_refs
and (domain_name + ':*') in env.config.intersphinx_disabled_reftypes):
if honor_disabled_refs and f'{domain_name}:*' in intersphinx_disabled_reftypes:
continue
objtypes: Iterable[str] = domain.object_types.keys()
res = _resolve_reference_in_domain(env, inv_name, inventory,
Expand All @@ -165,8 +167,7 @@ def _resolve_reference(env: BuildEnvironment, inv_name: str | None, inventory: I
if not domain_name:
# only objects in domains are in the inventory
return None
if (honor_disabled_refs
and (domain_name + ':*') in env.config.intersphinx_disabled_reftypes):
if honor_disabled_refs and f'{domain_name}:*' in intersphinx_disabled_reftypes:
return None
domain = env.get_domain(domain_name)
objtypes = domain.objtypes_for_role(typ) or ()
Expand All @@ -178,12 +179,12 @@ def _resolve_reference(env: BuildEnvironment, inv_name: str | None, inventory: I
node, contnode)


def inventory_exists(env: BuildEnvironment, inv_name: str) -> bool:
def inventory_exists(env: BuildEnvironment, inv_name: InventoryName) -> bool:
return inv_name in InventoryAdapter(env).named_inventory


def resolve_reference_in_inventory(env: BuildEnvironment,
inv_name: str,
inv_name: InventoryName,
node: pending_xref, contnode: TextElement,
) -> nodes.reference | None:
"""Attempt to resolve a missing reference via intersphinx references.
Expand Down
42 changes: 33 additions & 9 deletions sphinx/ext/intersphinx/_shared.py
Expand Up @@ -2,15 +2,40 @@

from __future__ import annotations

from typing import TYPE_CHECKING, Final, Union
from typing import TYPE_CHECKING, Final

from sphinx.util import logging

if TYPE_CHECKING:
from typing import Optional

from sphinx.environment import BuildEnvironment
from sphinx.util.typing import Inventory

InventoryCacheEntry = tuple[Union[str, None], int, Inventory]
#: The inventory project URL to which links are resolved.
#:
#: This value is unique in :confval:`intersphinx_mapping`.
InventoryURI = str

#: The inventory (non-empty) name.
#:
#: It is unique and in bijection with an inventory remote URL.
InventoryName = str

#: A target (local or remote) containing the inventory data to fetch.
#:
#: Empty strings are not expected and ``None`` indicates the default
#: inventory file name :data:`~sphinx.builder.html.INVENTORY_FILENAME`.
InventoryLocation = Optional[str]

#: Inventory cache entry. The integer field is the cache expiration time.
InventoryCacheEntry = tuple[InventoryName, int, Inventory]

#: The type of :confval:`intersphinx_mapping` *after* normalization.
IntersphinxMapping = dict[
InventoryName,
tuple[InventoryName, tuple[InventoryURI, tuple[InventoryLocation, ...]]],
]

LOGGER: Final[logging.SphinxLoggerAdapter] = logging.getLogger('sphinx.ext.intersphinx')

Expand All @@ -29,14 +54,13 @@ def __init__(self, env: BuildEnvironment) -> None:
self.env.intersphinx_named_inventory = {} # type: ignore[attr-defined]

@property
def cache(self) -> dict[str, InventoryCacheEntry]:
def cache(self) -> dict[InventoryURI, 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
- Key is the URI of the remote inventory.
- Element one is the key given in the Sphinx :confval:`intersphinx_mapping`.
- Element two is a time value for cache invalidation, an integer.
- Element three is the loaded remote inventory of type :class:`!Inventory`.
"""
return self.env.intersphinx_cache # type: ignore[attr-defined]

Expand All @@ -45,7 +69,7 @@ def main_inventory(self) -> Inventory:
return self.env.intersphinx_inventory # type: ignore[attr-defined]

@property
def named_inventory(self) -> dict[str, Inventory]:
def named_inventory(self) -> dict[InventoryName, Inventory]:
return self.env.intersphinx_named_inventory # type: ignore[attr-defined]

def clear(self) -> None:
Expand Down
2 changes: 2 additions & 0 deletions sphinx/util/typing.py
Expand Up @@ -111,6 +111,8 @@ def is_invalid_builtin_class(obj: Any) -> bool:
str, # URL
str, # display name
]

# referencable role => (reference name => inventory item)
Inventory = dict[str, dict[str, InventoryItem]]


Expand Down
2 changes: 1 addition & 1 deletion tests/test_domains/test_domain_c.py
Expand Up @@ -771,7 +771,7 @@ def test_domain_c_build_intersphinx(tmp_path, app, status, warning):
_var c:member 1 index.html#c.$ -
''')) # NoQA: W291
app.config.intersphinx_mapping = {
'https://localhost/intersphinx/c/': str(inv_file),
'local': ('https://localhost/intersphinx/c/', str(inv_file)),
}
app.config.intersphinx_cache_limit = 0
# load the inventory and check if it's done correctly
Expand Down
2 changes: 1 addition & 1 deletion tests/test_domains/test_domain_cpp.py
Expand Up @@ -1424,7 +1424,7 @@ def test_domain_cpp_build_intersphinx(tmp_path, app, status, warning):
_var cpp:member 1 index.html#_CPPv44$ -
''')) # NoQA: W291
app.config.intersphinx_mapping = {
'https://localhost/intersphinx/cpp/': str(inv_file),
'test': ('https://localhost/intersphinx/cpp/', str(inv_file)),
}
app.config.intersphinx_cache_limit = 0
# load the inventory and check if it's done correctly
Expand Down