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

Remove support for the Sphinx 0.5 intersphinx_mapping format #12083

Merged
merged 19 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 30 additions & 33 deletions sphinx/ext/intersphinx/_load.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from urllib.parse import urlsplit, urlunsplit

from sphinx.builders.html import INVENTORY_FILENAME
from sphinx.errors import ConfigError
from sphinx.ext.intersphinx._shared import LOGGER, InventoryAdapter
from sphinx.locale import __
from sphinx.util import requests
Expand All @@ -37,70 +38,66 @@ def normalize_intersphinx_mapping(app: Sphinx, config: Config) -> None:
# depending on which one is inserted last in the cache.
seen: dict[InventoryURI, InventoryName] = {}

errors = []
for name, value in config.intersphinx_mapping.copy().items():
# ensure that intersphinx projects are always named
if not isinstance(name, str):
LOGGER.warning(__('intersphinx identifier %r is not string. Ignored'),
name, type='intersphinx', subtype='config')
msg = __('project identifier must be a string')
errors.append((msg, name, name))
picnixz marked this conversation as resolved.
Show resolved Hide resolved
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',
)
msg = __('expected an intersphinx project identifier')
errors.append((msg, name, name))
del config.intersphinx_mapping[name]
continue

# ensure values are properly formatted
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',
)
msg = __('expected a tuple or a list')
errors.append((msg, name, value))
del config.intersphinx_mapping[name]
continue

try:
uri, inv = value
except Exception as exc:
LOGGER.warning(
__('Failed to read intersphinx_mapping[%s], ignored: %r'),
name, exc, type='intersphinx', subtype='config',
)
except Exception:
msg = __('values must be a (target URI, inventory locations) pair')
errors.append((msg, name, value))
del config.intersphinx_mapping[name]
continue

# ensure target URIs are non-empty and unique
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',
)
msg = __('target URI must be a non-empty string')
errors.append((msg, name, uri))
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',
)
msg = __('target URI must be unique (other instance in `intersphinx_mapping[%r]`)')
errors.append((msg % name_for_uri, name, uri))
del config.intersphinx_mapping[name]
continue

# ensure inventory locations are None or non-empty
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:
LOGGER.warning(
__('intersphinx_mapping[%r]: inventory location must '
'be a non-empty string or None, got: %r; ignoring.'),
name, target, type='intersphinx', subtype='config',
)
msg = __('inventory location must be a non-empty string or None')
errors.append((msg, name, target))
del config.intersphinx_mapping[name]
continue

config.intersphinx_mapping[name] = (name, (uri, tuple(targets)))

if len(errors) > 0:
picnixz marked this conversation as resolved.
Show resolved Hide resolved
for (msg, name, value) in errors:
picnixz marked this conversation as resolved.
Show resolved Hide resolved
error_msg = __('Invalid value %r in intersphinx_mapping[%r]: %s')
LOGGER.error(error_msg % (value, name, msg))
msg = __('Invalid `intersphinx_mapping` configuration (%s errors).')
picnixz marked this conversation as resolved.
Show resolved Hide resolved
raise ConfigError(msg % len(errors))


def load_mappings(app: Sphinx) -> None:
"""Load all intersphinx mappings into the environment.
Expand Down
2 changes: 1 addition & 1 deletion sphinx/util/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def __call__(
str, # display name
]

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


Expand Down
99 changes: 54 additions & 45 deletions tests/test_extensions/test_ext_intersphinx.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Test the intersphinx extension."""

from __future__ import annotations

import http.server
Expand All @@ -10,6 +11,7 @@

from sphinx import addnodes
from sphinx.builders.html import INVENTORY_FILENAME
from sphinx.errors import ConfigError
from sphinx.ext.intersphinx import (
fetch_inventory,
inspect_main,
Expand All @@ -32,6 +34,11 @@
from typing import NoReturn


class FakeList(list):
def __iter__(self) -> NoReturn:
raise NotImplementedError


def fake_node(domain, type, target, content, **attrs):
contnode = nodes.emphasis(content, content)
node = addnodes.pending_xref('')
Expand Down Expand Up @@ -406,55 +413,57 @@ def test_inventory_not_having_version(tmp_path, app, status, warning):
assert rn[0].astext() == 'Long Module desc'


def test_normalize_intersphinx_mapping_warnings(tmp_path, app, warning):
def test_normalize_intersphinx_mapping_warnings(app, warning):
"""Check warnings in :func:`sphinx.ext.intersphinx.normalize_intersphinx_mapping`."""
inv_file = tmp_path / 'inventory'
inv_file.write_bytes(INVENTORY_V2)
targets = (str(inv_file),)

class FakeList(list):
def __iter__(self) -> NoReturn:
raise NotImplementedError

set_config(app, bad_intersphinx_mapping := {
bad_intersphinx_mapping = {
# fmt: off
'': ('67890.net', targets), # invalid project name (value)
12345: ('12345.net', targets), # invalid project name (type)
'bad-dict-item': 0, # invalid dict item type
'unpack-except-1': [0], # invalid dict item size (native ValueError)
'unpack-except-2': FakeList(), # invalid dict item size (custom exception)
'bad-uri-type-1': (123456789, targets), # invalid URI type
'bad-uri-type-2': (None, targets), # invalid URI type
'bad-uri-value': ('', targets), # invalid URI value
'good': ('foo.com', targets), # duplicated URI (good entry)
'dedup-good': ('foo.com', targets), # duplicated URI
'bad-target-1': ('a.com', 1), # invalid URI type (single input, bad type)
'bad-target-2': ('b.com', ''), # invalid URI type (single input, bad string)
'bad-target-3': ('c.com', [2, 'x']), # invalid URI type (sequence input, bad type)
'bad-target-4': ('d.com', ['y', '']), # invalid URI type (sequence input, bad string)
'': ('789.example', None), # invalid project name (value)
12345: ('456.example', None), # invalid project name (type)
None: ('123.example', None), # invalid project name (type)
'https://example/': 'inventory', # Sphinx 0.x style value
'bad-dict-item': 0, # invalid dict item type
'unpack-except-1': [0], # invalid dict item size (native ValueError)
'unpack-except-2': FakeList(), # invalid dict item size (custom exception)
'bad-uri-type-1': (123456789, None), # invalid target URI type
'bad-uri-type-2': (None, None), # invalid target URI type
'bad-uri-value': ('', None), # invalid target URI value
'good': ('example.org', None), # duplicated target URI (good entry)
'dedup-good': ('example.org', None), # duplicated target URI
'bad-location-1': ('a.example', 1), # invalid inventory location (single input, bad type)
'bad-location-2': ('b.example', ''), # invalid inventory location (single input, bad string)
'bad-location-3': ('c.example', [2, 'x']), # invalid inventory location (sequence input, bad type)
'bad-location-4': ('d.example', ['y', '']), # invalid inventory location (sequence input, bad string)
'good-target-1': ('e.example', None), # valid inventory location (None)
'good-target-2': ('f.example', ('x',)), # valid inventory location (sequence input)
# fmt: on
})

# normalize the inventory and check if it's done correctly
normalize_intersphinx_mapping(app, app.config)
}
set_config(app, bad_intersphinx_mapping)

# normalise the inventory and check if it's done correctly
with pytest.raises(
ConfigError,
match=r'Invalid `intersphinx_mapping` configuration \(15 errors\).',
):
normalize_intersphinx_mapping(app, app.config)
warnings = strip_colors(warning.getvalue()).splitlines()
assert len(warnings) == len(bad_intersphinx_mapping) - 1
for index, messages in enumerate((
"ignoring empty intersphinx identifier",
'intersphinx identifier 12345 is not string. Ignored',
"intersphinx_mapping['bad-dict-item']: expecting a tuple or a list, got: 0; ignoring.",
"Failed to read intersphinx_mapping[unpack-except-1], ignored: ValueError('not enough values to unpack (expected 2, got 1)')",
"Failed to read intersphinx_mapping[unpack-except-2], ignored: NotImplementedError()",
"intersphinx_mapping['bad-uri-type-1']: URI must be a non-empty string, got: 123456789; ignoring.",
"intersphinx_mapping['bad-uri-type-2']: URI must be a non-empty string, got: None; ignoring.",
"intersphinx_mapping['bad-uri-value']: URI must be a non-empty string, got: ''; ignoring.",
"intersphinx_mapping['dedup-good']: URI 'foo.com' shadows URI from intersphinx_mapping['good']; ignoring.",
"intersphinx_mapping['bad-target-1']: inventory location must be a non-empty string or None, got: 1; ignoring.",
"intersphinx_mapping['bad-target-2']: inventory location must be a non-empty string or None, got: ''; ignoring.",
"intersphinx_mapping['bad-target-3']: inventory location must be a non-empty string or None, got: 2; ignoring.",
"intersphinx_mapping['bad-target-4']: inventory location must be a non-empty string or None, got: ''; ignoring.",
)):
assert messages in warnings[index]
assert len(warnings) == len(bad_intersphinx_mapping) - 3
assert list(enumerate(warnings)) == list(enumerate((
"ERROR: Invalid value '' in intersphinx_mapping['']: expected an intersphinx project identifier",
"ERROR: Invalid value 12345 in intersphinx_mapping[12345]: project identifier must be a string",
"ERROR: Invalid value None in intersphinx_mapping[None]: project identifier must be a string",
"ERROR: Invalid value 'inventory' in intersphinx_mapping['https://example/']: expected a tuple or a list",
"ERROR: Invalid value 0 in intersphinx_mapping['bad-dict-item']: expected a tuple or a list",
"ERROR: Invalid value [0] in intersphinx_mapping['unpack-except-1']: values must be a (target URI, inventory locations) pair",
"ERROR: Invalid value [] in intersphinx_mapping['unpack-except-2']: values must be a (target URI, inventory locations) pair",
"ERROR: Invalid value 123456789 in intersphinx_mapping['bad-uri-type-1']: target URI must be a non-empty string",
"ERROR: Invalid value None in intersphinx_mapping['bad-uri-type-2']: target URI must be a non-empty string",
"ERROR: Invalid value '' in intersphinx_mapping['bad-uri-value']: target URI must be a non-empty string",
"ERROR: Invalid value 'example.org' in intersphinx_mapping['dedup-good']: target URI must be unique (other instance in `intersphinx_mapping['good']`)",
"ERROR: Invalid value 1 in intersphinx_mapping['bad-location-1']: inventory location must be a non-empty string or None",
"ERROR: Invalid value '' in intersphinx_mapping['bad-location-2']: inventory location must be a non-empty string or None",
"ERROR: Invalid value 2 in intersphinx_mapping['bad-location-3']: inventory location must be a non-empty string or None",
"ERROR: Invalid value '' in intersphinx_mapping['bad-location-4']: inventory location must be a non-empty string or None",
)))


def test_load_mappings_fallback(tmp_path, app, status, warning):
Expand Down
Loading