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

Intersphinx lookups: log warnings for ambiguous target definitions and resolutions. #12329

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c65b877
Intersphinx lookups: log warning for ambiguous target resolution.
jayaddison Apr 25, 2024
fe3a647
Refactor: use existing shared Intersphinx logger.
jayaddison Apr 26, 2024
57099f1
Code review feedback: include type, subtype and location in warning.
jayaddison Apr 26, 2024
c992329
Cleanup: remove unused import from test module.
jayaddison Apr 26, 2024
d9417e0
Tests: refactor: extract INVENTORY_V2_AMBIGUOUS_TERMS fixture data.
jayaddison Apr 26, 2024
c07c799
Merge branch 'refs/heads/master' into pr-12033-follow/intersphinx-amb…
AA-Turner Apr 29, 2024
436297c
Allow translation
AA-Turner Apr 29, 2024
ea7349b
Cleanup: remove redundant domain.name from log message line.
jayaddison Apr 29, 2024
72b266d
Intersphinx lookups: add origin-hyperlink to log warnings about ambig…
jayaddison Apr 29, 2024
4935023
Revert "Intersphinx lookups: add origin-hyperlink to log warnings abo…
jayaddison Apr 29, 2024
ab78182
Intersphinx lookups: log inventory name at both load-time and ambiguo…
jayaddison Apr 29, 2024
88a338a
Intersphinx inventories: add loading-time warning when ambiguous name…
jayaddison Apr 29, 2024
8b51aaf
linting: resolve type-inconsistency warnings by returning an empty se…
jayaddison Apr 29, 2024
897e268
linting: cleanup: remove unused variable.
jayaddison Apr 29, 2024
7f0b375
Fixup: include type namespace when checking for ambiguities.
jayaddison Apr 29, 2024
4d61b14
Revert "linting: cleanup: remove unused variable."
jayaddison Apr 29, 2024
330c2eb
Merge branch 'master' into pr-12033-follow/intersphinx-ambiguous-look…
jayaddison Apr 29, 2024
15d2416
Add CHANGES.rst entry.
jayaddison May 4, 2024
f10c9ca
Merge branch 'master' into pr-12033-follow/intersphinx-ambiguous-look…
jayaddison May 9, 2024
a8ee7aa
Intersphinx inventories: code review feedback: retain the existing pu…
jayaddison Jun 11, 2024
cfce8d9
Merge branch 'master' into pr-12033-follow/intersphinx-ambiguous-look…
jayaddison Jun 11, 2024
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
43 changes: 30 additions & 13 deletions sphinx/ext/intersphinx/_resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@
inv_name: str | None, inventory: Inventory,
domain: Domain, objtypes: Iterable[str],
target: str,
node: pending_xref, contnode: TextElement) -> nodes.reference | None:
node: pending_xref, contnode: TextElement,
app: Sphinx | None = None
jayaddison marked this conversation as resolved.
Show resolved Hide resolved
) -> nodes.reference | None:
for objtype in objtypes:
if objtype not in inventory:
# Continue if there's nothing of this kind in the inventory
Expand All @@ -80,6 +82,15 @@
target_lower = target.lower()
insensitive_matches = list(filter(lambda k: k.lower() == target_lower,
inventory[objtype].keys()))
if len(insensitive_matches) > 1:
inventory_location = (
app.config.intersphinx_mapping[inv_name][1][0]
if (inv_name and app and inv_name in app.config.intersphinx_mapping) else
"./objects.inv"
)
jayaddison marked this conversation as resolved.
Show resolved Hide resolved
LOGGER.warning(__("multiple matches found for %s:%s in <%s>"),
objtype, target, inventory_location,
type='intersphinx', subtype='external', location=node)
if insensitive_matches:
data = inventory[objtype][insensitive_matches[0]]
else:
Expand All @@ -99,6 +110,7 @@
honor_disabled_refs: bool,
domain: Domain, objtypes: Iterable[str],
node: pending_xref, contnode: TextElement,
app: Sphinx | None = None,
) -> nodes.reference | None:
obj_types: dict[str, None] = {}.fromkeys(objtypes)

Expand All @@ -125,7 +137,7 @@

# without qualification
res = _resolve_reference_in_domain_by_target(inv_name, inventory, domain, objtypes,
node['reftarget'], node, contnode)
node['reftarget'], node, contnode, app)
if res is not None:
return res

Expand All @@ -134,12 +146,14 @@
if full_qualified_name is None:
return None
return _resolve_reference_in_domain_by_target(inv_name, inventory, domain, objtypes,
full_qualified_name, node, contnode)
full_qualified_name, node, contnode, app)


def _resolve_reference(env: BuildEnvironment, inv_name: str | None, inventory: Inventory,
honor_disabled_refs: bool,
node: pending_xref, contnode: TextElement) -> nodes.reference | None:
node: pending_xref, contnode: TextElement,
app: Sphinx | None = None
) -> nodes.reference | None:
# disabling should only be done if no inventory is given
honor_disabled_refs = honor_disabled_refs and inv_name is None

Expand All @@ -156,7 +170,7 @@
res = _resolve_reference_in_domain(env, inv_name, inventory,
honor_disabled_refs,
domain, objtypes,
node, contnode)
node, contnode, app)
if res is not None:
return res
return None
Expand All @@ -175,7 +189,7 @@
return _resolve_reference_in_domain(env, inv_name, inventory,
honor_disabled_refs,
domain, objtypes,
node, contnode)
node, contnode, app)


def inventory_exists(env: BuildEnvironment, inv_name: str) -> bool:
Expand All @@ -185,6 +199,7 @@
def resolve_reference_in_inventory(env: BuildEnvironment,
inv_name: str,
node: pending_xref, contnode: TextElement,
app: Sphinx | None = None,
) -> nodes.reference | None:
"""Attempt to resolve a missing reference via intersphinx references.

Expand All @@ -194,24 +209,26 @@
"""
assert inventory_exists(env, inv_name)
return _resolve_reference(env, inv_name, InventoryAdapter(env).named_inventory[inv_name],
False, node, contnode)
False, node, contnode, app)


def resolve_reference_any_inventory(env: BuildEnvironment,
honor_disabled_refs: bool,
node: pending_xref, contnode: TextElement,
app: Sphinx | None = None,
) -> nodes.reference | None:
"""Attempt to resolve a missing reference via intersphinx references.

Resolution is tried with the target as is in any inventory.
"""
return _resolve_reference(env, None, InventoryAdapter(env).main_inventory,
honor_disabled_refs,
node, contnode)
node, contnode, app)


def resolve_reference_detect_inventory(env: BuildEnvironment,
node: pending_xref, contnode: TextElement,
app: Sphinx | None = None
) -> nodes.reference | None:
"""Attempt to resolve a missing reference via intersphinx references.

Expand All @@ -221,7 +238,7 @@
is tried in that inventory with the new target.
"""
# ordinary direct lookup, use data as is
res = resolve_reference_any_inventory(env, True, node, contnode)
res = resolve_reference_any_inventory(env, True, node, contnode, app)
if res is not None:
return res

Expand All @@ -233,15 +250,15 @@
if not inventory_exists(env, inv_name):
return None
node['reftarget'] = newtarget
res_inv = resolve_reference_in_inventory(env, inv_name, node, contnode)
res_inv = resolve_reference_in_inventory(env, inv_name, node, contnode, app)
node['reftarget'] = target
return res_inv


def missing_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref,
contnode: TextElement) -> nodes.reference | None:
"""Attempt to resolve a missing reference via intersphinx references."""
return resolve_reference_detect_inventory(env, node, contnode)
return resolve_reference_detect_inventory(env, node, contnode, app)


class IntersphinxDispatcher(CustomReSTDispatcher):
Expand Down Expand Up @@ -480,9 +497,9 @@
inv_name = node['inventory']
if inv_name is not None:
assert inventory_exists(self.env, inv_name)
newnode = resolve_reference_in_inventory(self.env, inv_name, node, contnode)
newnode = resolve_reference_in_inventory(self.env, inv_name, node, contnode, self.app)

Check failure on line 500 in sphinx/ext/intersphinx/_resolve.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

sphinx/ext/intersphinx/_resolve.py:500:96: E501 Line too long (102 > 95)
else:
newnode = resolve_reference_any_inventory(self.env, False, node, contnode)
newnode = resolve_reference_any_inventory(self.env, False, node, contnode, self.app)

Check failure on line 502 in sphinx/ext/intersphinx/_resolve.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (E501)

sphinx/ext/intersphinx/_resolve.py:502:96: E501 Line too long (100 > 95)
if newnode is None:
typ = node['reftype']
msg = (__('external %s:%s reference target not found: %s') %
Expand Down
24 changes: 23 additions & 1 deletion tests/test_extensions/test_ext_intersphinx.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
from sphinx.ext.intersphinx._load import _get_safe_url, _strip_basic_auth
from sphinx.util.console import strip_colors

from tests.test_util.intersphinx_data import INVENTORY_V2, INVENTORY_V2_NO_VERSION
from tests.test_util.intersphinx_data import (
INVENTORY_V2,
INVENTORY_V2_AMBIGUOUS_TERMS,
INVENTORY_V2_NO_VERSION,
)
from tests.utils import http_server


Expand Down Expand Up @@ -247,6 +251,24 @@ def test_missing_reference_stddomain(tmp_path, app, status, warning):
assert rn.astext() == 'The Julia Domain'


def test_ambiguous_reference_warning(tmp_path, app, warning):
inv_file = tmp_path / 'inventory'
inv_file.write_bytes(INVENTORY_V2_AMBIGUOUS_TERMS)
set_config(app, {
'cmd': ('https://docs.python.org/', str(inv_file)),
})

# load the inventory
normalize_intersphinx_mapping(app, app.config)
load_mappings(app)

# term reference (case insensitive)
node, contnode = fake_node('std', 'term', 'A TERM', 'A TERM')
missing_reference(app, app.env, node, contnode)

assert 'multiple matches found for std:term:A TERM' in warning.getvalue()


@pytest.mark.sphinx('html', testroot='ext-intersphinx-cppdomain')
def test_missing_reference_cppdomain(tmp_path, app, status, warning):
inv_file = tmp_path / 'inventory'
Expand Down
10 changes: 10 additions & 0 deletions tests/test_util/intersphinx_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,13 @@
''' + zlib.compress(b'''\
module1 py:module 0 foo.html#module-module1 Long Module desc
''')

INVENTORY_V2_AMBIGUOUS_TERMS: Final[bytes] = b'''\
# Sphinx inventory version 2
# Project: foo
# Version: 2.0
# The remainder of this file is compressed with zlib.
''' + zlib.compress(b'''\
a term std:term -1 glossary.html#term-a-term -
A term std:term -1 glossary.html#term-a-term -
''')