diff options
Diffstat (limited to '')
-rw-r--r-- | sphinx/ext/intersphinx.py | 163 |
1 files changed, 140 insertions, 23 deletions
diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index 453bb6e..a8a2cf1 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -34,6 +34,7 @@ from docutils.utils import relative_path import sphinx from sphinx.addnodes import pending_xref from sphinx.builders.html import INVENTORY_FILENAME +from sphinx.deprecation import _deprecation_warning from sphinx.errors import ExtensionError from sphinx.locale import _, __ from sphinx.transforms.post_transforms import ReferencesResolver @@ -53,7 +54,7 @@ if TYPE_CHECKING: from sphinx.config import Config from sphinx.domains import Domain from sphinx.environment import BuildEnvironment - from sphinx.util.typing import Inventory, InventoryItem, RoleFunction + from sphinx.util.typing import ExtensionMetadata, Inventory, InventoryItem, RoleFunction InventoryCacheEntry = tuple[Union[str, None], int, Inventory] @@ -245,7 +246,7 @@ def fetch_inventory_group( for fail in failures: logger.info(*fail) else: - issues = '\n'.join([f[0] % f[1:] for f in failures]) + issues = '\n'.join(f[0] % f[1:] for f in failures) logger.warning(__("failed to reach any of the inventories " "with the following issues:") + "\n" + issues) @@ -334,8 +335,10 @@ def _resolve_reference_in_domain_by_target( if target in inventory[objtype]: # Case sensitive match, use it data = inventory[objtype][target] - elif objtype == 'std:term': - # Check for potential case insensitive matches for terms only + elif objtype in {'std:label', 'std:term'}: + # Some types require case insensitive matches: + # * 'term': https://github.com/sphinx-doc/sphinx/issues/9291 + # * 'label': https://github.com/sphinx-doc/sphinx/issues/12008 target_lower = target.lower() insensitive_matches = list(filter(lambda k: k.lower() == target_lower, inventory[objtype].keys())) @@ -479,7 +482,6 @@ def resolve_reference_detect_inventory(env: BuildEnvironment, to form ``inv_name:newtarget``. If ``inv_name`` is a named inventory, then resolution 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) if res is not None: @@ -501,7 +503,6 @@ def resolve_reference_detect_inventory(env: BuildEnvironment, 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) @@ -533,17 +534,90 @@ class IntersphinxRole(SphinxRole): assert self.name == self.orig_name.lower() inventory, name_suffix = self.get_inventory_and_name_suffix(self.orig_name) if inventory and not inventory_exists(self.env, inventory): - logger.warning(__('inventory for external cross-reference not found: %s'), - inventory, location=(self.env.docname, self.lineno)) + self._emit_warning( + __('inventory for external cross-reference not found: %r'), inventory + ) return [], [] - role_name = self.get_role_name(name_suffix) + domain_name, role_name = self._get_domain_role(name_suffix) + if role_name is None: - logger.warning(__('role for external cross-reference not found: %s'), name_suffix, - location=(self.env.docname, self.lineno)) + self._emit_warning( + __('invalid external cross-reference suffix: %r'), name_suffix + ) return [], [] - result, messages = self.invoke_role(role_name) + # attempt to find a matching role function + role_func: RoleFunction | None + + if domain_name is not None: + # the user specified a domain, so we only check that + if (domain := self.env.domains.get(domain_name)) is None: + self._emit_warning( + __('domain for external cross-reference not found: %r'), domain_name + ) + return [], [] + if (role_func := domain.roles.get(role_name)) is None: + msg = 'role for external cross-reference not found in domain %r: %r' + if ( + object_types := domain.object_types.get(role_name) + ) is not None and object_types.roles: + self._emit_warning( + __(f'{msg} (perhaps you meant one of: %s)'), + domain_name, + role_name, + self._concat_strings(object_types.roles), + ) + else: + self._emit_warning(__(msg), domain_name, role_name) + return [], [] + + else: + # the user did not specify a domain, + # so we check first the default (if available) then standard domains + domains: list[Domain] = [] + if default_domain := self.env.temp_data.get('default_domain'): + domains.append(default_domain) + if ( + std_domain := self.env.domains.get('std') + ) is not None and std_domain not in domains: + domains.append(std_domain) + + role_func = None + for domain in domains: + if (role_func := domain.roles.get(role_name)) is not None: + domain_name = domain.name + break + + if role_func is None or domain_name is None: + domains_str = self._concat_strings(d.name for d in domains) + msg = 'role for external cross-reference not found in domains %s: %r' + possible_roles: set[str] = set() + for d in domains: + if o := d.object_types.get(role_name): + possible_roles.update(f'{d.name}:{r}' for r in o.roles) + if possible_roles: + msg = f'{msg} (perhaps you meant one of: %s)' + self._emit_warning( + __(msg), + domains_str, + role_name, + self._concat_strings(possible_roles), + ) + else: + self._emit_warning(__(msg), domains_str, role_name) + return [], [] + + result, messages = role_func( + f'{domain_name}:{role_name}', + self.rawtext, + self.text, + self.lineno, + self.inliner, + self.options, + self.content, + ) + for node in result: if isinstance(node, pending_xref): node['intersphinx'] = True @@ -552,13 +626,17 @@ class IntersphinxRole(SphinxRole): return result, messages def get_inventory_and_name_suffix(self, name: str) -> tuple[str | None, str]: + """Extract an inventory name (if any) and ``domain+name`` suffix from a role *name*. + and the domain+name suffix. + + The role name is expected to be of one of the following forms: + + - ``external+inv:name`` -- explicit inventory and name, any domain. + - ``external+inv:domain:name`` -- explicit inventory, domain and name. + - ``external:name`` -- any inventory and domain, explicit name. + - ``external:domain:name`` -- any inventory, explicit domain and name. + """ assert name.startswith('external'), name - # either we have an explicit inventory name, i.e, - # :external+inv:role: or - # :external+inv:domain:role: - # or we look in all inventories, i.e., - # :external:role: or - # :external:domain:role: suffix = name[9:] if name[8] == '+': inv_name, suffix = suffix.split(':', 1) @@ -569,7 +647,39 @@ class IntersphinxRole(SphinxRole): msg = f'Malformed :external: role name: {name}' raise ValueError(msg) + def _get_domain_role(self, name: str) -> tuple[str | None, str | None]: + """Convert the *name* string into a domain and a role name. + + - If *name* contains no ``:``, return ``(None, name)``. + - If *name* contains a single ``:``, the domain/role is split on this. + - If *name* contains multiple ``:``, return ``(None, None)``. + """ + names = name.split(':') + if len(names) == 1: + return None, names[0] + elif len(names) == 2: + return names[0], names[1] + else: + return None, None + + def _emit_warning(self, msg: str, /, *args: Any) -> None: + logger.warning( + msg, + *args, + type='intersphinx', + subtype='external', + location=(self.env.docname, self.lineno), + ) + + def _concat_strings(self, strings: Iterable[str]) -> str: + return ', '.join(f'{s!r}' for s in sorted(strings)) + + # deprecated methods + def get_role_name(self, name: str) -> tuple[str, str] | None: + _deprecation_warning( + __name__, f'{self.__class__.__name__}.get_role_name', '', remove=(9, 0) + ) names = name.split(':') if len(names) == 1: # role @@ -591,6 +701,9 @@ class IntersphinxRole(SphinxRole): return None def is_existent_role(self, domain_name: str, role_name: str) -> bool: + _deprecation_warning( + __name__, f'{self.__class__.__name__}.is_existent_role', '', remove=(9, 0) + ) try: domain = self.env.get_domain(domain_name) return role_name in domain.roles @@ -598,6 +711,10 @@ class IntersphinxRole(SphinxRole): return False def invoke_role(self, role: tuple[str, str]) -> tuple[list[Node], list[system_message]]: + """Invoke the role described by a ``(domain, role name)`` pair.""" + _deprecation_warning( + __name__, f'{self.__class__.__name__}.invoke_role', '', remove=(9, 0) + ) domain = self.env.get_domain(role[0]) if domain: role_func = domain.role(role[1]) @@ -681,11 +798,11 @@ def normalize_intersphinx_mapping(app: Sphinx, config: Config) -> None: config.intersphinx_mapping.pop(key) -def setup(app: Sphinx) -> dict[str, Any]: - app.add_config_value('intersphinx_mapping', {}, True) - app.add_config_value('intersphinx_cache_limit', 5, False) - app.add_config_value('intersphinx_timeout', None, False) - app.add_config_value('intersphinx_disabled_reftypes', ['std:doc'], True) +def setup(app: Sphinx) -> ExtensionMetadata: + app.add_config_value('intersphinx_mapping', {}, 'env') + app.add_config_value('intersphinx_cache_limit', 5, '') + app.add_config_value('intersphinx_timeout', None, '') + app.add_config_value('intersphinx_disabled_reftypes', ['std:doc'], 'env') app.connect('config-inited', normalize_intersphinx_mapping, priority=800) app.connect('builder-inited', load_mappings) app.connect('source-read', install_dispatcher) |