2009-09-07 15:52:26 -05:00
|
|
|
"""Insert links to objects documented in remote Sphinx documentation.
|
2008-08-04 12:31:25 -05:00
|
|
|
|
|
|
|
This works as follows:
|
|
|
|
|
2009-09-07 15:52:26 -05:00
|
|
|
* Each Sphinx HTML build creates a file named "objects.inv" that contains a
|
|
|
|
mapping from object names to URIs relative to the HTML set's root.
|
2008-08-04 12:31:25 -05:00
|
|
|
|
|
|
|
* Projects using the Intersphinx extension can specify links to such mapping
|
|
|
|
files in the `intersphinx_mapping` config value. The mapping will then be
|
2009-09-07 15:52:26 -05:00
|
|
|
used to resolve otherwise missing references to objects into links to the
|
|
|
|
other documentation.
|
2008-08-04 12:31:25 -05:00
|
|
|
|
|
|
|
* By default, the mapping file is assumed to be at the same location as the
|
|
|
|
rest of the documentation; however, the location of the mapping file can
|
|
|
|
also be specified individually, e.g. if the docs should be buildable
|
|
|
|
without Internet access.
|
|
|
|
"""
|
|
|
|
|
2021-04-04 11:34:45 -05:00
|
|
|
from __future__ import annotations
|
2022-12-30 14:14:18 -06:00
|
|
|
|
2024-04-19 18:47:10 -05:00
|
|
|
__all__ = (
|
|
|
|
'InventoryAdapter',
|
|
|
|
'fetch_inventory',
|
|
|
|
'fetch_inventory_group',
|
|
|
|
'load_mappings',
|
|
|
|
'normalize_intersphinx_mapping',
|
|
|
|
'IntersphinxRoleResolver',
|
|
|
|
'inventory_exists',
|
|
|
|
'install_dispatcher',
|
|
|
|
'resolve_reference_in_inventory',
|
|
|
|
'resolve_reference_any_inventory',
|
|
|
|
'resolve_reference_detect_inventory',
|
|
|
|
'missing_reference',
|
|
|
|
'IntersphinxDispatcher',
|
|
|
|
'IntersphinxRole',
|
|
|
|
'inspect_main',
|
|
|
|
)
|
|
|
|
|
2019-11-14 21:27:30 -06:00
|
|
|
import concurrent.futures
|
2017-01-15 09:51:40 -06:00
|
|
|
import functools
|
2008-08-04 12:31:25 -05:00
|
|
|
import posixpath
|
2022-01-16 02:40:23 -06:00
|
|
|
import re
|
2018-01-27 10:52:16 -06:00
|
|
|
import time
|
2008-08-04 12:31:25 -05:00
|
|
|
from os import path
|
2023-07-24 20:07:23 -05:00
|
|
|
from typing import TYPE_CHECKING, cast
|
2018-11-11 10:02:14 -06:00
|
|
|
from urllib.parse import urlsplit, urlunsplit
|
2008-08-04 12:31:25 -05:00
|
|
|
|
|
|
|
from docutils import nodes
|
2023-03-17 12:39:44 -05:00
|
|
|
from docutils.utils import relative_path
|
2008-08-04 12:31:25 -05:00
|
|
|
|
2014-09-03 09:39:30 -05:00
|
|
|
import sphinx
|
2021-03-05 23:43:13 -06:00
|
|
|
from sphinx.addnodes import pending_xref
|
2008-12-05 05:27:08 -06:00
|
|
|
from sphinx.builders.html import INVENTORY_FILENAME
|
👌 Improve `external` role warnings (and revert object fallback) (#12193)
The key issue this commit seeks to address, is that existing tools / documentation often lead users to mistakenly use object types and not role names, a classic example being `function` not `func`
Previously, the warning message for using e.g. `` external:function`target` `` (with `py` as the default domain), would be:
```
WARNING: role for external cross-reference not found: function
```
This gives no information to the user on how to fix the issue, even though there is actually quite an easy fix
This commit adds logic to create more specific / helpful warning messages, e.g.
```
WARNING: role for external cross-reference not found in domains 'py', 'std': 'function' (perhaps you meant one of: 'py:func', 'py:obj')
```
This goes through the same original logic but, if the role is not found, it will look if the role name is actually an available object type on the domain(s), and then suggest its related roles.
This commit also reverts #12133, which provided a (silent) fallback to auto convert an object type to a role name.
2024-03-25 08:39:51 -05:00
|
|
|
from sphinx.deprecation import _deprecation_warning
|
2021-04-04 11:34:45 -05:00
|
|
|
from sphinx.errors import ExtensionError
|
2024-04-19 18:47:30 -05:00
|
|
|
from sphinx.ext.intersphinx._cli import inspect_main
|
2024-04-19 18:47:20 -05:00
|
|
|
from sphinx.ext.intersphinx._shared import LOGGER as logger
|
|
|
|
from sphinx.ext.intersphinx._shared import InventoryAdapter
|
2018-02-25 07:16:09 -06:00
|
|
|
from sphinx.locale import _, __
|
2021-04-04 11:34:45 -05:00
|
|
|
from sphinx.transforms.post_transforms import ReferencesResolver
|
2024-04-19 18:47:20 -05:00
|
|
|
from sphinx.util import requests
|
2021-04-04 11:34:45 -05:00
|
|
|
from sphinx.util.docutils import CustomReSTDispatcher, SphinxRole
|
2017-03-02 07:51:42 -06:00
|
|
|
from sphinx.util.inventory import InventoryFile
|
2008-08-04 12:31:25 -05:00
|
|
|
|
2023-01-31 12:30:43 -06:00
|
|
|
if TYPE_CHECKING:
|
2023-07-24 20:07:23 -05:00
|
|
|
from collections.abc import Iterable
|
2023-01-31 12:30:43 -06:00
|
|
|
from types import ModuleType
|
2024-04-19 18:47:20 -05:00
|
|
|
from typing import IO, Any
|
2023-03-17 12:39:44 -05:00
|
|
|
|
|
|
|
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
|
2024-04-19 18:47:20 -05:00
|
|
|
from sphinx.ext.intersphinx._shared import InventoryCacheEntry
|
2024-03-21 10:19:26 -05:00
|
|
|
from sphinx.util.typing import ExtensionMetadata, Inventory, InventoryItem, RoleFunction
|
2023-03-17 12:39:44 -05:00
|
|
|
|
2017-01-29 10:55:39 -06:00
|
|
|
|
2015-10-15 16:37:55 -05:00
|
|
|
def _strip_basic_auth(url: str) -> str:
|
|
|
|
"""Returns *url* with basic auth credentials removed. Also returns the
|
|
|
|
basic auth username and password if they're present in *url*.
|
|
|
|
|
|
|
|
E.g.: https://user:pass@example.com => https://example.com
|
|
|
|
|
|
|
|
*url* need not include basic auth credentials.
|
|
|
|
|
|
|
|
:param url: url which may or may not contain basic auth credentials
|
|
|
|
:type url: ``str``
|
|
|
|
|
2016-08-17 22:41:38 -05:00
|
|
|
:return: *url* with any basic auth creds removed
|
|
|
|
:rtype: ``str``
|
2015-10-15 16:37:55 -05:00
|
|
|
"""
|
2016-08-17 22:41:38 -05:00
|
|
|
frags = list(urlsplit(url))
|
2015-10-15 16:37:55 -05:00
|
|
|
# swap out "user[:pass]@hostname" for "hostname"
|
2016-08-17 22:41:38 -05:00
|
|
|
if '@' in frags[1]:
|
|
|
|
frags[1] = frags[1].split('@')[1]
|
|
|
|
return urlunsplit(frags)
|
2015-10-15 16:37:55 -05:00
|
|
|
|
|
|
|
|
2023-07-27 14:17:35 -05:00
|
|
|
def _read_from_url(url: str, *, config: Config) -> IO:
|
2015-10-15 16:37:55 -05:00
|
|
|
"""Reads data from *url* with an HTTP *GET*.
|
|
|
|
|
|
|
|
This function supports fetching from resources which use basic HTTP auth as
|
|
|
|
laid out by RFC1738 § 3.1. See § 5 for grammar definitions for URLs.
|
|
|
|
|
|
|
|
.. seealso:
|
|
|
|
|
|
|
|
https://www.ietf.org/rfc/rfc1738.txt
|
|
|
|
|
|
|
|
:param url: URL of an HTTP resource
|
|
|
|
:type url: ``str``
|
|
|
|
|
|
|
|
:return: data read from resource described by *url*
|
|
|
|
:rtype: ``file``-like object
|
|
|
|
"""
|
2023-07-22 18:01:41 -05:00
|
|
|
r = requests.get(url, stream=True, timeout=config.intersphinx_timeout,
|
|
|
|
_user_agent=config.user_agent,
|
|
|
|
_tls_info=(config.tls_verify, config.tls_cacerts))
|
2016-08-17 22:33:28 -05:00
|
|
|
r.raise_for_status()
|
|
|
|
r.raw.url = r.url
|
2017-01-15 09:51:40 -06:00
|
|
|
# decode content-body based on the header.
|
2023-07-22 18:01:41 -05:00
|
|
|
# ref: https://github.com/psf/requests/issues/2155
|
2017-01-15 09:51:40 -06:00
|
|
|
r.raw.read = functools.partial(r.raw.read, decode_content=True)
|
2016-08-17 22:33:28 -05:00
|
|
|
return r.raw
|
2015-10-15 16:37:55 -05:00
|
|
|
|
|
|
|
|
|
|
|
def _get_safe_url(url: str) -> str:
|
|
|
|
"""Gets version of *url* with basic auth passwords obscured. This function
|
|
|
|
returns results suitable for printing and logging.
|
|
|
|
|
2016-08-17 10:58:17 -05:00
|
|
|
E.g.: https://user:12345@example.com => https://user@example.com
|
2015-10-15 16:37:55 -05:00
|
|
|
|
|
|
|
:param url: a url
|
|
|
|
:type url: ``str``
|
|
|
|
|
2016-08-17 10:58:17 -05:00
|
|
|
:return: *url* with password removed
|
2015-10-15 16:37:55 -05:00
|
|
|
:rtype: ``str``
|
|
|
|
"""
|
2016-08-17 11:25:29 -05:00
|
|
|
parts = urlsplit(url)
|
|
|
|
if parts.username is None:
|
|
|
|
return url
|
|
|
|
else:
|
|
|
|
frags = list(parts)
|
|
|
|
if parts.port:
|
2022-10-17 09:54:59 -05:00
|
|
|
frags[1] = f'{parts.username}@{parts.hostname}:{parts.port}'
|
2016-08-17 11:25:29 -05:00
|
|
|
else:
|
2022-10-17 09:54:59 -05:00
|
|
|
frags[1] = f'{parts.username}@{parts.hostname}'
|
2016-08-17 11:25:29 -05:00
|
|
|
|
|
|
|
return urlunsplit(frags)
|
2015-10-15 16:37:55 -05:00
|
|
|
|
|
|
|
|
2023-03-17 12:39:44 -05:00
|
|
|
def fetch_inventory(app: Sphinx, uri: str, inv: str) -> Inventory:
|
2008-08-04 12:31:25 -05:00
|
|
|
"""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
|
2023-08-11 22:23:48 -05:00
|
|
|
if '://' in uri:
|
2015-10-15 16:37:55 -05:00
|
|
|
# case: inv URI points to remote resource; strip any existing auth
|
2016-08-17 22:41:38 -05:00
|
|
|
uri = _strip_basic_auth(uri)
|
2008-08-04 12:31:25 -05:00
|
|
|
try:
|
2015-10-22 01:09:16 -05:00
|
|
|
if '://' in inv:
|
2016-11-16 08:23:18 -06:00
|
|
|
f = _read_from_url(inv, config=app.config)
|
2008-08-04 12:31:25 -05:00
|
|
|
else:
|
2023-08-13 17:17:59 -05:00
|
|
|
f = open(path.join(app.srcdir, inv), 'rb') # NoQA: SIM115
|
2014-01-19 04:17:10 -06:00
|
|
|
except Exception as err:
|
2017-07-21 21:15:52 -05:00
|
|
|
err.args = ('intersphinx inventory %r not fetchable due to %s: %s',
|
2019-03-06 01:49:52 -06:00
|
|
|
inv, err.__class__, str(err))
|
2017-07-21 21:15:52 -05:00
|
|
|
raise
|
2008-08-04 12:31:25 -05:00
|
|
|
try:
|
2016-08-17 22:33:28 -05:00
|
|
|
if hasattr(f, 'url'):
|
2022-11-13 14:36:24 -06:00
|
|
|
newinv = f.url
|
2016-08-10 21:19:42 -05:00
|
|
|
if inv != newinv:
|
2019-01-08 09:54:42 -06:00
|
|
|
logger.info(__('intersphinx inventory has moved: %s -> %s'), inv, newinv)
|
2016-08-10 21:19:42 -05:00
|
|
|
|
|
|
|
if uri in (inv, path.dirname(inv), path.dirname(inv) + '/'):
|
|
|
|
uri = path.dirname(newinv)
|
2016-08-17 10:56:40 -05:00
|
|
|
with f:
|
|
|
|
try:
|
2023-08-11 22:23:48 -05:00
|
|
|
invdata = InventoryFile.load(f, uri, posixpath.join)
|
2017-01-07 10:41:12 -06:00
|
|
|
except ValueError as exc:
|
2020-06-13 16:46:19 -05:00
|
|
|
raise ValueError('unknown or unsupported inventory version: %r' % exc) from exc
|
2014-01-19 04:17:10 -06:00
|
|
|
except Exception as err:
|
2017-07-21 21:15:52 -05:00
|
|
|
err.args = ('intersphinx inventory %r not readable due to %s: %s',
|
2019-03-06 01:49:52 -06:00
|
|
|
inv, err.__class__.__name__, str(err))
|
2017-07-21 21:15:52 -05:00
|
|
|
raise
|
2008-08-04 12:31:25 -05:00
|
|
|
else:
|
|
|
|
return invdata
|
|
|
|
|
|
|
|
|
2019-11-14 21:27:30 -06:00
|
|
|
def fetch_inventory_group(
|
2023-03-17 12:39:44 -05:00
|
|
|
name: str | None,
|
|
|
|
uri: str,
|
|
|
|
invs: tuple[str | None, ...],
|
|
|
|
cache: dict[str, InventoryCacheEntry],
|
|
|
|
app: Sphinx,
|
|
|
|
now: int,
|
2019-11-14 21:27:30 -06:00
|
|
|
) -> bool:
|
2008-08-04 12:31:25 -05:00
|
|
|
cache_time = now - app.config.intersphinx_cache_limit * 86400
|
2019-11-14 21:27:30 -06:00
|
|
|
failures = []
|
|
|
|
try:
|
2014-08-19 12:56:47 -05:00
|
|
|
for inv in invs:
|
|
|
|
if not inv:
|
|
|
|
inv = posixpath.join(uri, INVENTORY_FILENAME)
|
|
|
|
# decide whether the inventory must be read: always read local
|
|
|
|
# files; remote ones only if the cache time is expired
|
2019-11-14 21:27:30 -06:00
|
|
|
if '://' not in inv or uri not in cache or cache[uri][1] < cache_time:
|
2018-12-12 10:33:14 -06:00
|
|
|
safe_inv_url = _get_safe_url(inv)
|
2019-01-08 09:54:42 -06:00
|
|
|
logger.info(__('loading intersphinx inventory from %s...'), safe_inv_url)
|
2017-07-21 21:15:52 -05:00
|
|
|
try:
|
|
|
|
invdata = fetch_inventory(app, uri, inv)
|
|
|
|
except Exception as err:
|
|
|
|
failures.append(err.args)
|
|
|
|
continue
|
2014-08-19 12:56:47 -05:00
|
|
|
if invdata:
|
2023-03-17 12:39:44 -05:00
|
|
|
cache[uri] = name, now, invdata
|
2019-11-14 21:27:30 -06:00
|
|
|
return True
|
|
|
|
return False
|
|
|
|
finally:
|
2018-01-30 10:21:46 -06:00
|
|
|
if failures == []:
|
|
|
|
pass
|
|
|
|
elif len(failures) < len(invs):
|
2019-01-08 09:54:42 -06:00
|
|
|
logger.info(__("encountered some issues with some of the inventories,"
|
|
|
|
" but they had working alternatives:"))
|
2017-07-21 21:15:52 -05:00
|
|
|
for fail in failures:
|
|
|
|
logger.info(*fail)
|
|
|
|
else:
|
2024-01-16 16:06:34 -06:00
|
|
|
issues = '\n'.join(f[0] % f[1:] for f in failures)
|
2018-02-25 07:16:09 -06:00
|
|
|
logger.warning(__("failed to reach any of the inventories "
|
2019-03-06 01:49:52 -06:00
|
|
|
"with the following issues:") + "\n" + issues)
|
2017-07-21 21:15:52 -05:00
|
|
|
|
2019-11-14 21:27:30 -06:00
|
|
|
|
|
|
|
def load_mappings(app: Sphinx) -> None:
|
|
|
|
"""Load all intersphinx mappings into the environment."""
|
|
|
|
now = int(time.time())
|
|
|
|
inventories = InventoryAdapter(app.builder.env)
|
2023-03-17 12:43:44 -05:00
|
|
|
intersphinx_cache: dict[str, InventoryCacheEntry] = inventories.cache
|
2019-11-14 21:27:30 -06:00
|
|
|
|
|
|
|
with concurrent.futures.ThreadPoolExecutor() as pool:
|
|
|
|
futures = []
|
2023-03-17 12:39:44 -05:00
|
|
|
name: str | None
|
|
|
|
uri: str
|
|
|
|
invs: tuple[str | None, ...]
|
2019-11-14 21:27:30 -06:00
|
|
|
for name, (uri, invs) in app.config.intersphinx_mapping.values():
|
|
|
|
futures.append(pool.submit(
|
2023-03-17 12:43:44 -05:00
|
|
|
fetch_inventory_group, name, uri, invs, intersphinx_cache, app, now,
|
2019-11-14 21:27:30 -06:00
|
|
|
))
|
|
|
|
updated = [f.result() for f in concurrent.futures.as_completed(futures)]
|
|
|
|
|
|
|
|
if any(updated):
|
2017-01-29 10:55:39 -06:00
|
|
|
inventories.clear()
|
|
|
|
|
2012-11-01 11:58:47 -05:00
|
|
|
# Duplicate values in different inventories will shadow each
|
|
|
|
# other; which one will override which can vary between builds
|
|
|
|
# since they are specified using an unordered dict. To make
|
|
|
|
# it more consistent, we sort the named inventories and then
|
|
|
|
# add the unnamed inventories last. This means that the
|
|
|
|
# unnamed inventories will shadow the named ones but the named
|
|
|
|
# ones can still be accessed when the name is specified.
|
2023-03-17 12:43:44 -05:00
|
|
|
named_vals = []
|
|
|
|
unnamed_vals = []
|
|
|
|
for name, _expiry, invdata in intersphinx_cache.values():
|
|
|
|
if name:
|
|
|
|
named_vals.append((name, invdata))
|
|
|
|
else:
|
|
|
|
unnamed_vals.append((name, invdata))
|
|
|
|
for name, invdata in sorted(named_vals) + unnamed_vals:
|
2010-05-23 06:06:01 -05:00
|
|
|
if name:
|
2017-01-29 10:55:39 -06:00
|
|
|
inventories.named_inventory[name] = invdata
|
2018-09-11 07:50:55 -05:00
|
|
|
for type, objects in invdata.items():
|
2017-01-29 10:55:39 -06:00
|
|
|
inventories.main_inventory.setdefault(type, {}).update(objects)
|
2008-08-04 12:31:25 -05:00
|
|
|
|
|
|
|
|
2021-07-16 06:12:52 -05:00
|
|
|
def _create_element_from_result(domain: Domain, inv_name: str | None,
|
2021-10-31 07:26:38 -05:00
|
|
|
data: InventoryItem,
|
2023-03-17 12:39:44 -05:00
|
|
|
node: pending_xref, contnode: TextElement) -> nodes.reference:
|
2021-07-16 06:12:52 -05:00
|
|
|
proj, version, uri, dispname = data
|
|
|
|
if '://' not in uri and node.get('refdoc'):
|
|
|
|
# get correct path in case of subdirectories
|
2023-08-11 22:23:48 -05:00
|
|
|
uri = posixpath.join(relative_path(node['refdoc'], '.'), uri)
|
2021-07-16 06:12:52 -05:00
|
|
|
if version:
|
|
|
|
reftitle = _('(in %s v%s)') % (proj, version)
|
|
|
|
else:
|
|
|
|
reftitle = _('(in %s)') % (proj,)
|
|
|
|
newnode = nodes.reference('', '', internal=False, refuri=uri, reftitle=reftitle)
|
|
|
|
if node.get('refexplicit'):
|
|
|
|
# use whatever title was given
|
|
|
|
newnode.append(contnode)
|
|
|
|
elif dispname == '-' or \
|
|
|
|
(domain.name == 'std' and node['reftype'] == 'keyword'):
|
|
|
|
# use whatever title was given, but strip prefix
|
|
|
|
title = contnode.astext()
|
|
|
|
if inv_name is not None and title.startswith(inv_name + ':'):
|
|
|
|
newnode.append(contnode.__class__(title[len(inv_name) + 1:],
|
|
|
|
title[len(inv_name) + 1:]))
|
|
|
|
else:
|
|
|
|
newnode.append(contnode)
|
|
|
|
else:
|
|
|
|
# else use the given display name (used for :ref:)
|
|
|
|
newnode.append(contnode.__class__(dispname, dispname))
|
|
|
|
return newnode
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_reference_in_domain_by_target(
|
|
|
|
inv_name: str | None, inventory: Inventory,
|
2023-07-24 05:35:12 -05:00
|
|
|
domain: Domain, objtypes: Iterable[str],
|
2021-07-16 06:12:52 -05:00
|
|
|
target: str,
|
2023-03-17 12:39:44 -05:00
|
|
|
node: pending_xref, contnode: TextElement) -> nodes.reference | None:
|
2021-07-16 06:12:52 -05:00
|
|
|
for objtype in objtypes:
|
|
|
|
if objtype not in inventory:
|
|
|
|
# Continue if there's nothing of this kind in the inventory
|
|
|
|
continue
|
|
|
|
|
|
|
|
if target in inventory[objtype]:
|
|
|
|
# Case sensitive match, use it
|
|
|
|
data = inventory[objtype][target]
|
2024-03-02 05:39:51 -06:00
|
|
|
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
|
2021-07-16 06:12:52 -05:00
|
|
|
target_lower = target.lower()
|
|
|
|
insensitive_matches = list(filter(lambda k: k.lower() == target_lower,
|
|
|
|
inventory[objtype].keys()))
|
|
|
|
if insensitive_matches:
|
|
|
|
data = inventory[objtype][insensitive_matches[0]]
|
|
|
|
else:
|
|
|
|
# No case insensitive match either, continue to the next candidate
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
# Could reach here if we're not a term but have a case insensitive match.
|
|
|
|
# This is a fix for terms specifically, but potentially should apply to
|
|
|
|
# other types.
|
|
|
|
continue
|
|
|
|
return _create_element_from_result(domain, inv_name, data, node, contnode)
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
2021-10-02 04:12:49 -05:00
|
|
|
def _resolve_reference_in_domain(env: BuildEnvironment,
|
|
|
|
inv_name: str | None, inventory: Inventory,
|
|
|
|
honor_disabled_refs: bool,
|
2023-07-24 05:35:12 -05:00
|
|
|
domain: Domain, objtypes: Iterable[str],
|
2023-02-17 16:11:14 -06:00
|
|
|
node: pending_xref, contnode: TextElement,
|
2023-03-17 12:39:44 -05:00
|
|
|
) -> nodes.reference | None:
|
2023-07-24 05:35:12 -05:00
|
|
|
obj_types: dict[str, None] = {}.fromkeys(objtypes)
|
|
|
|
|
2021-07-16 06:12:52 -05:00
|
|
|
# we adjust the object types for backwards compatibility
|
2023-07-24 05:35:12 -05:00
|
|
|
if domain.name == 'std' and 'cmdoption' in obj_types:
|
2023-04-26 19:02:09 -05:00
|
|
|
# cmdoptions were stored as std:option until Sphinx 1.6
|
2023-07-24 05:35:12 -05:00
|
|
|
obj_types['option'] = None
|
|
|
|
if domain.name == 'py' and 'attribute' in obj_types:
|
2023-04-26 19:02:09 -05:00
|
|
|
# properties are stored as py:method since Sphinx 2.1
|
2023-07-24 05:35:12 -05:00
|
|
|
obj_types['method'] = None
|
2021-07-16 06:12:52 -05:00
|
|
|
|
|
|
|
# the inventory contains domain:type as objtype
|
2023-07-24 05:35:12 -05:00
|
|
|
domain_name = domain.name
|
|
|
|
obj_types = {f"{domain_name}:{obj_type}": None for obj_type in obj_types}
|
2021-07-16 06:12:52 -05:00
|
|
|
|
2021-10-02 04:12:49 -05:00
|
|
|
# now that the objtypes list is complete we can remove the disabled ones
|
|
|
|
if honor_disabled_refs:
|
2023-07-24 05:35:12 -05:00
|
|
|
disabled = set(env.config.intersphinx_disabled_reftypes)
|
|
|
|
obj_types = {obj_type: None
|
|
|
|
for obj_type in obj_types
|
|
|
|
if obj_type not in disabled}
|
|
|
|
|
|
|
|
objtypes = [*obj_types.keys()]
|
2021-10-02 04:12:49 -05:00
|
|
|
|
2021-07-16 06:12:52 -05:00
|
|
|
# without qualification
|
|
|
|
res = _resolve_reference_in_domain_by_target(inv_name, inventory, domain, objtypes,
|
|
|
|
node['reftarget'], node, contnode)
|
|
|
|
if res is not None:
|
|
|
|
return res
|
|
|
|
|
|
|
|
# try with qualification of the current scope instead
|
|
|
|
full_qualified_name = domain.get_full_qualified_name(node)
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_reference(env: BuildEnvironment, inv_name: str | None, inventory: Inventory,
|
2021-10-02 04:12:49 -05:00
|
|
|
honor_disabled_refs: bool,
|
2023-03-17 12:39:44 -05:00
|
|
|
node: pending_xref, contnode: TextElement) -> nodes.reference | None:
|
2021-07-16 07:34:35 -05:00
|
|
|
# disabling should only be done if no inventory is given
|
2021-10-02 04:12:49 -05:00
|
|
|
honor_disabled_refs = honor_disabled_refs and inv_name is None
|
2021-07-16 07:34:35 -05:00
|
|
|
|
2021-10-31 07:56:26 -05:00
|
|
|
if honor_disabled_refs and '*' in env.config.intersphinx_disabled_reftypes:
|
2021-07-16 07:34:35 -05:00
|
|
|
return None
|
|
|
|
|
2021-07-16 06:12:52 -05:00
|
|
|
typ = node['reftype']
|
|
|
|
if typ == 'any':
|
|
|
|
for domain_name, domain in env.domains.items():
|
2023-03-17 12:43:44 -05:00
|
|
|
if (honor_disabled_refs
|
|
|
|
and (domain_name + ":*") in env.config.intersphinx_disabled_reftypes):
|
2021-07-16 07:34:35 -05:00
|
|
|
continue
|
2023-07-24 05:35:12 -05:00
|
|
|
objtypes: Iterable[str] = domain.object_types.keys()
|
2021-10-02 04:12:49 -05:00
|
|
|
res = _resolve_reference_in_domain(env, inv_name, inventory,
|
|
|
|
honor_disabled_refs,
|
2021-07-16 06:12:52 -05:00
|
|
|
domain, objtypes,
|
|
|
|
node, contnode)
|
|
|
|
if res is not None:
|
|
|
|
return res
|
|
|
|
return None
|
2014-09-19 06:31:55 -05:00
|
|
|
else:
|
2021-07-16 06:12:52 -05:00
|
|
|
domain_name = node.get('refdomain')
|
|
|
|
if not domain_name:
|
2014-09-19 06:31:55 -05:00
|
|
|
# only objects in domains are in the inventory
|
2018-11-30 08:51:16 -06:00
|
|
|
return None
|
2021-10-02 04:12:49 -05:00
|
|
|
if honor_disabled_refs \
|
2021-10-31 07:56:26 -05:00
|
|
|
and (domain_name + ":*") in env.config.intersphinx_disabled_reftypes:
|
2021-07-16 07:34:35 -05:00
|
|
|
return None
|
2021-07-16 06:12:52 -05:00
|
|
|
domain = env.get_domain(domain_name)
|
2023-07-27 14:17:35 -05:00
|
|
|
objtypes = domain.objtypes_for_role(typ) or ()
|
2014-09-19 06:31:55 -05:00
|
|
|
if not objtypes:
|
2018-11-30 08:51:16 -06:00
|
|
|
return None
|
2021-10-02 04:12:49 -05:00
|
|
|
return _resolve_reference_in_domain(env, inv_name, inventory,
|
|
|
|
honor_disabled_refs,
|
2021-07-16 06:12:52 -05:00
|
|
|
domain, objtypes,
|
|
|
|
node, contnode)
|
2021-06-04 14:51:02 -05:00
|
|
|
|
2008-08-04 12:31:25 -05:00
|
|
|
|
2021-07-16 06:12:52 -05:00
|
|
|
def inventory_exists(env: BuildEnvironment, inv_name: str) -> bool:
|
|
|
|
return inv_name in InventoryAdapter(env).named_inventory
|
|
|
|
|
|
|
|
|
|
|
|
def resolve_reference_in_inventory(env: BuildEnvironment,
|
|
|
|
inv_name: str,
|
2023-02-17 16:11:14 -06:00
|
|
|
node: pending_xref, contnode: TextElement,
|
2023-03-17 12:39:44 -05:00
|
|
|
) -> nodes.reference | None:
|
2021-07-16 06:12:52 -05:00
|
|
|
"""Attempt to resolve a missing reference via intersphinx references.
|
|
|
|
|
|
|
|
Resolution is tried in the given inventory with the target as is.
|
|
|
|
|
|
|
|
Requires ``inventory_exists(env, inv_name)``.
|
|
|
|
"""
|
|
|
|
assert inventory_exists(env, inv_name)
|
|
|
|
return _resolve_reference(env, inv_name, InventoryAdapter(env).named_inventory[inv_name],
|
2021-07-16 07:34:35 -05:00
|
|
|
False, node, contnode)
|
2021-07-16 06:12:52 -05:00
|
|
|
|
|
|
|
|
|
|
|
def resolve_reference_any_inventory(env: BuildEnvironment,
|
2021-10-02 04:12:49 -05:00
|
|
|
honor_disabled_refs: bool,
|
2023-02-17 16:11:14 -06:00
|
|
|
node: pending_xref, contnode: TextElement,
|
2023-03-17 12:39:44 -05:00
|
|
|
) -> nodes.reference | None:
|
2021-07-16 06:12:52 -05:00
|
|
|
"""Attempt to resolve a missing reference via intersphinx references.
|
|
|
|
|
|
|
|
Resolution is tried with the target as is in any inventory.
|
|
|
|
"""
|
2021-07-16 07:34:35 -05:00
|
|
|
return _resolve_reference(env, None, InventoryAdapter(env).main_inventory,
|
2021-10-02 04:12:49 -05:00
|
|
|
honor_disabled_refs,
|
2021-07-16 07:34:35 -05:00
|
|
|
node, contnode)
|
2021-07-16 06:12:52 -05:00
|
|
|
|
|
|
|
|
|
|
|
def resolve_reference_detect_inventory(env: BuildEnvironment,
|
2023-02-17 16:11:14 -06:00
|
|
|
node: pending_xref, contnode: TextElement,
|
2023-03-17 12:39:44 -05:00
|
|
|
) -> nodes.reference | None:
|
2021-07-16 06:12:52 -05:00
|
|
|
"""Attempt to resolve a missing reference via intersphinx references.
|
|
|
|
|
|
|
|
Resolution is tried first with the target as is in any inventory.
|
|
|
|
If this does not succeed, then the target is split by the first ``:``,
|
|
|
|
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
|
2021-10-31 07:40:39 -05:00
|
|
|
res = resolve_reference_any_inventory(env, True, node, contnode)
|
2021-07-16 06:12:52 -05:00
|
|
|
if res is not None:
|
|
|
|
return res
|
|
|
|
|
|
|
|
# try splitting the target into 'inv_name:target'
|
|
|
|
target = node['reftarget']
|
|
|
|
if ':' not in target:
|
|
|
|
return None
|
|
|
|
inv_name, newtarget = target.split(':', 1)
|
|
|
|
if not inventory_exists(env, inv_name):
|
|
|
|
return None
|
|
|
|
node['reftarget'] = newtarget
|
|
|
|
res_inv = resolve_reference_in_inventory(env, inv_name, node, contnode)
|
|
|
|
node['reftarget'] = target
|
|
|
|
return res_inv
|
|
|
|
|
|
|
|
|
|
|
|
def missing_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref,
|
2023-03-17 12:39:44 -05:00
|
|
|
contnode: TextElement) -> nodes.reference | None:
|
2021-07-16 06:12:52 -05:00
|
|
|
"""Attempt to resolve a missing reference via intersphinx references."""
|
2021-10-31 07:40:39 -05:00
|
|
|
return resolve_reference_detect_inventory(env, node, contnode)
|
2018-11-30 08:51:16 -06:00
|
|
|
|
2008-08-04 12:31:25 -05:00
|
|
|
|
2021-04-04 11:34:45 -05:00
|
|
|
class IntersphinxDispatcher(CustomReSTDispatcher):
|
2021-07-13 05:48:17 -05:00
|
|
|
"""Custom dispatcher for external role.
|
2021-04-04 11:34:45 -05:00
|
|
|
|
2022-01-15 07:02:21 -06:00
|
|
|
This enables :external:***:/:external+***: roles on parsing reST document.
|
2021-04-04 11:34:45 -05:00
|
|
|
"""
|
|
|
|
|
2023-02-17 16:11:14 -06:00
|
|
|
def role(
|
|
|
|
self, role_name: str, language_module: ModuleType, lineno: int, reporter: Reporter,
|
|
|
|
) -> tuple[RoleFunction, list[system_message]]:
|
2022-01-15 14:52:25 -06:00
|
|
|
if len(role_name) > 9 and role_name.startswith(('external:', 'external+')):
|
2021-11-06 08:33:47 -05:00
|
|
|
return IntersphinxRole(role_name), []
|
2021-04-04 11:34:45 -05:00
|
|
|
else:
|
|
|
|
return super().role(role_name, language_module, lineno, reporter)
|
|
|
|
|
|
|
|
|
|
|
|
class IntersphinxRole(SphinxRole):
|
2022-01-16 02:40:23 -06:00
|
|
|
# group 1: just for the optionality of the inventory name
|
|
|
|
# group 2: the inventory name (optional)
|
|
|
|
# group 3: the domain:role or role part
|
|
|
|
_re_inv_ref = re.compile(r"(\+([^:]+))?:(.*)")
|
|
|
|
|
2021-11-06 08:33:47 -05:00
|
|
|
def __init__(self, orig_name: str) -> None:
|
|
|
|
self.orig_name = orig_name
|
|
|
|
|
2021-04-04 11:34:45 -05:00
|
|
|
def run(self) -> tuple[list[Node], list[system_message]]:
|
2021-11-06 08:33:47 -05:00
|
|
|
assert self.name == self.orig_name.lower()
|
|
|
|
inventory, name_suffix = self.get_inventory_and_name_suffix(self.orig_name)
|
2021-11-06 08:02:37 -05:00
|
|
|
if inventory and not inventory_exists(self.env, inventory):
|
👌 Improve `external` role warnings (and revert object fallback) (#12193)
The key issue this commit seeks to address, is that existing tools / documentation often lead users to mistakenly use object types and not role names, a classic example being `function` not `func`
Previously, the warning message for using e.g. `` external:function`target` `` (with `py` as the default domain), would be:
```
WARNING: role for external cross-reference not found: function
```
This gives no information to the user on how to fix the issue, even though there is actually quite an easy fix
This commit adds logic to create more specific / helpful warning messages, e.g.
```
WARNING: role for external cross-reference not found in domains 'py', 'std': 'function' (perhaps you meant one of: 'py:func', 'py:obj')
```
This goes through the same original logic but, if the role is not found, it will look if the role name is actually an available object type on the domain(s), and then suggest its related roles.
This commit also reverts #12133, which provided a (silent) fallback to auto convert an object type to a role name.
2024-03-25 08:39:51 -05:00
|
|
|
self._emit_warning(
|
|
|
|
__('inventory for external cross-reference not found: %r'), inventory
|
|
|
|
)
|
2021-11-06 08:02:37 -05:00
|
|
|
return [], []
|
|
|
|
|
👌 Improve `external` role warnings (and revert object fallback) (#12193)
The key issue this commit seeks to address, is that existing tools / documentation often lead users to mistakenly use object types and not role names, a classic example being `function` not `func`
Previously, the warning message for using e.g. `` external:function`target` `` (with `py` as the default domain), would be:
```
WARNING: role for external cross-reference not found: function
```
This gives no information to the user on how to fix the issue, even though there is actually quite an easy fix
This commit adds logic to create more specific / helpful warning messages, e.g.
```
WARNING: role for external cross-reference not found in domains 'py', 'std': 'function' (perhaps you meant one of: 'py:func', 'py:obj')
```
This goes through the same original logic but, if the role is not found, it will look if the role name is actually an available object type on the domain(s), and then suggest its related roles.
This commit also reverts #12133, which provided a (silent) fallback to auto convert an object type to a role name.
2024-03-25 08:39:51 -05:00
|
|
|
domain_name, role_name = self._get_domain_role(name_suffix)
|
|
|
|
|
2021-04-04 11:34:45 -05:00
|
|
|
if role_name is None:
|
👌 Improve `external` role warnings (and revert object fallback) (#12193)
The key issue this commit seeks to address, is that existing tools / documentation often lead users to mistakenly use object types and not role names, a classic example being `function` not `func`
Previously, the warning message for using e.g. `` external:function`target` `` (with `py` as the default domain), would be:
```
WARNING: role for external cross-reference not found: function
```
This gives no information to the user on how to fix the issue, even though there is actually quite an easy fix
This commit adds logic to create more specific / helpful warning messages, e.g.
```
WARNING: role for external cross-reference not found in domains 'py', 'std': 'function' (perhaps you meant one of: 'py:func', 'py:obj')
```
This goes through the same original logic but, if the role is not found, it will look if the role name is actually an available object type on the domain(s), and then suggest its related roles.
This commit also reverts #12133, which provided a (silent) fallback to auto convert an object type to a role name.
2024-03-25 08:39:51 -05:00
|
|
|
self._emit_warning(
|
|
|
|
__('invalid external cross-reference suffix: %r'), name_suffix
|
|
|
|
)
|
2021-04-04 11:34:45 -05:00
|
|
|
return [], []
|
|
|
|
|
👌 Improve `external` role warnings (and revert object fallback) (#12193)
The key issue this commit seeks to address, is that existing tools / documentation often lead users to mistakenly use object types and not role names, a classic example being `function` not `func`
Previously, the warning message for using e.g. `` external:function`target` `` (with `py` as the default domain), would be:
```
WARNING: role for external cross-reference not found: function
```
This gives no information to the user on how to fix the issue, even though there is actually quite an easy fix
This commit adds logic to create more specific / helpful warning messages, e.g.
```
WARNING: role for external cross-reference not found in domains 'py', 'std': 'function' (perhaps you meant one of: 'py:func', 'py:obj')
```
This goes through the same original logic but, if the role is not found, it will look if the role name is actually an available object type on the domain(s), and then suggest its related roles.
This commit also reverts #12133, which provided a (silent) fallback to auto convert an object type to a role name.
2024-03-25 08:39:51 -05:00
|
|
|
# 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,
|
|
|
|
)
|
|
|
|
|
2021-04-04 11:34:45 -05:00
|
|
|
for node in result:
|
|
|
|
if isinstance(node, pending_xref):
|
|
|
|
node['intersphinx'] = True
|
2021-11-06 08:02:37 -05:00
|
|
|
node['inventory'] = inventory
|
2021-04-04 11:34:45 -05:00
|
|
|
|
|
|
|
return result, messages
|
|
|
|
|
2022-01-15 07:02:21 -06:00
|
|
|
def get_inventory_and_name_suffix(self, name: str) -> tuple[str | None, str]:
|
👌 Handle external references pointing to object types (#12133)
This commit fixes the issue of `objects.inv` denoting object names, whilst the `external` role only allows for role names.
As an example, take the `objects.inv` for the sphinx documentation, which contains:
```
py:function
compile : usage/domains/python.html#compile
```
A user might understandably expect that they could reference this using `` :external:py:function:`compile` ``, but actually this would previously error with:
```
WARNING: role for external cross-reference not found: py:function
```
this is because, `function` is the object type, yet `external` expects the related role name `func`.
It should not be necessary for the user to know about this distinction,
so in this commit, we add logic, to first look if the name relates to a role name (as previous, to not be back-breaking) but, if not, then also look if the name relates to an object that has a known role and, if so, use that.
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
2024-03-19 07:42:50 -05:00
|
|
|
"""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.
|
|
|
|
"""
|
2022-01-15 07:02:21 -06:00
|
|
|
assert name.startswith('external'), name
|
2023-07-27 14:17:35 -05:00
|
|
|
suffix = name[9:]
|
|
|
|
if name[8] == '+':
|
|
|
|
inv_name, suffix = suffix.split(':', 1)
|
|
|
|
return inv_name, suffix
|
|
|
|
elif name[8] == ':':
|
|
|
|
return None, suffix
|
|
|
|
else:
|
2023-08-13 14:07:28 -05:00
|
|
|
msg = f'Malformed :external: role name: {name}'
|
|
|
|
raise ValueError(msg)
|
2021-11-06 08:02:37 -05:00
|
|
|
|
👌 Improve `external` role warnings (and revert object fallback) (#12193)
The key issue this commit seeks to address, is that existing tools / documentation often lead users to mistakenly use object types and not role names, a classic example being `function` not `func`
Previously, the warning message for using e.g. `` external:function`target` `` (with `py` as the default domain), would be:
```
WARNING: role for external cross-reference not found: function
```
This gives no information to the user on how to fix the issue, even though there is actually quite an easy fix
This commit adds logic to create more specific / helpful warning messages, e.g.
```
WARNING: role for external cross-reference not found in domains 'py', 'std': 'function' (perhaps you meant one of: 'py:func', 'py:obj')
```
This goes through the same original logic but, if the role is not found, it will look if the role name is actually an available object type on the domain(s), and then suggest its related roles.
This commit also reverts #12133, which provided a (silent) fallback to auto convert an object type to a role name.
2024-03-25 08:39:51 -05:00
|
|
|
def _get_domain_role(self, name: str) -> tuple[str | None, str | None]:
|
|
|
|
"""Convert the *name* string into a domain and a role name.
|
👌 Handle external references pointing to object types (#12133)
This commit fixes the issue of `objects.inv` denoting object names, whilst the `external` role only allows for role names.
As an example, take the `objects.inv` for the sphinx documentation, which contains:
```
py:function
compile : usage/domains/python.html#compile
```
A user might understandably expect that they could reference this using `` :external:py:function:`compile` ``, but actually this would previously error with:
```
WARNING: role for external cross-reference not found: py:function
```
this is because, `function` is the object type, yet `external` expects the related role name `func`.
It should not be necessary for the user to know about this distinction,
so in this commit, we add logic, to first look if the name relates to a role name (as previous, to not be back-breaking) but, if not, then also look if the name relates to an object that has a known role and, if so, use that.
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
2024-03-19 07:42:50 -05:00
|
|
|
|
👌 Improve `external` role warnings (and revert object fallback) (#12193)
The key issue this commit seeks to address, is that existing tools / documentation often lead users to mistakenly use object types and not role names, a classic example being `function` not `func`
Previously, the warning message for using e.g. `` external:function`target` `` (with `py` as the default domain), would be:
```
WARNING: role for external cross-reference not found: function
```
This gives no information to the user on how to fix the issue, even though there is actually quite an easy fix
This commit adds logic to create more specific / helpful warning messages, e.g.
```
WARNING: role for external cross-reference not found in domains 'py', 'std': 'function' (perhaps you meant one of: 'py:func', 'py:obj')
```
This goes through the same original logic but, if the role is not found, it will look if the role name is actually an available object type on the domain(s), and then suggest its related roles.
This commit also reverts #12133, which provided a (silent) fallback to auto convert an object type to a role name.
2024-03-25 08:39:51 -05:00
|
|
|
- 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)``.
|
👌 Handle external references pointing to object types (#12133)
This commit fixes the issue of `objects.inv` denoting object names, whilst the `external` role only allows for role names.
As an example, take the `objects.inv` for the sphinx documentation, which contains:
```
py:function
compile : usage/domains/python.html#compile
```
A user might understandably expect that they could reference this using `` :external:py:function:`compile` ``, but actually this would previously error with:
```
WARNING: role for external cross-reference not found: py:function
```
this is because, `function` is the object type, yet `external` expects the related role name `func`.
It should not be necessary for the user to know about this distinction,
so in this commit, we add logic, to first look if the name relates to a role name (as previous, to not be back-breaking) but, if not, then also look if the name relates to an object that has a known role and, if so, use that.
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
2024-03-19 07:42:50 -05:00
|
|
|
"""
|
2021-04-04 11:34:45 -05:00
|
|
|
names = name.split(':')
|
2021-11-06 08:02:37 -05:00
|
|
|
if len(names) == 1:
|
👌 Improve `external` role warnings (and revert object fallback) (#12193)
The key issue this commit seeks to address, is that existing tools / documentation often lead users to mistakenly use object types and not role names, a classic example being `function` not `func`
Previously, the warning message for using e.g. `` external:function`target` `` (with `py` as the default domain), would be:
```
WARNING: role for external cross-reference not found: function
```
This gives no information to the user on how to fix the issue, even though there is actually quite an easy fix
This commit adds logic to create more specific / helpful warning messages, e.g.
```
WARNING: role for external cross-reference not found in domains 'py', 'std': 'function' (perhaps you meant one of: 'py:func', 'py:obj')
```
This goes through the same original logic but, if the role is not found, it will look if the role name is actually an available object type on the domain(s), and then suggest its related roles.
This commit also reverts #12133, which provided a (silent) fallback to auto convert an object type to a role name.
2024-03-25 08:39:51 -05:00
|
|
|
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
|
2021-11-06 08:02:37 -05:00
|
|
|
default_domain = self.env.temp_data.get('default_domain')
|
|
|
|
domain = default_domain.name if default_domain else None
|
👌 Improve `external` role warnings (and revert object fallback) (#12193)
The key issue this commit seeks to address, is that existing tools / documentation often lead users to mistakenly use object types and not role names, a classic example being `function` not `func`
Previously, the warning message for using e.g. `` external:function`target` `` (with `py` as the default domain), would be:
```
WARNING: role for external cross-reference not found: function
```
This gives no information to the user on how to fix the issue, even though there is actually quite an easy fix
This commit adds logic to create more specific / helpful warning messages, e.g.
```
WARNING: role for external cross-reference not found in domains 'py', 'std': 'function' (perhaps you meant one of: 'py:func', 'py:obj')
```
This goes through the same original logic but, if the role is not found, it will look if the role name is actually an available object type on the domain(s), and then suggest its related roles.
This commit also reverts #12133, which provided a (silent) fallback to auto convert an object type to a role name.
2024-03-25 08:39:51 -05:00
|
|
|
role = names[0]
|
2021-11-06 08:02:37 -05:00
|
|
|
elif len(names) == 2:
|
👌 Improve `external` role warnings (and revert object fallback) (#12193)
The key issue this commit seeks to address, is that existing tools / documentation often lead users to mistakenly use object types and not role names, a classic example being `function` not `func`
Previously, the warning message for using e.g. `` external:function`target` `` (with `py` as the default domain), would be:
```
WARNING: role for external cross-reference not found: function
```
This gives no information to the user on how to fix the issue, even though there is actually quite an easy fix
This commit adds logic to create more specific / helpful warning messages, e.g.
```
WARNING: role for external cross-reference not found in domains 'py', 'std': 'function' (perhaps you meant one of: 'py:func', 'py:obj')
```
This goes through the same original logic but, if the role is not found, it will look if the role name is actually an available object type on the domain(s), and then suggest its related roles.
This commit also reverts #12133, which provided a (silent) fallback to auto convert an object type to a role name.
2024-03-25 08:39:51 -05:00
|
|
|
# domain:role:
|
2021-11-06 08:02:37 -05:00
|
|
|
domain = names[0]
|
👌 Improve `external` role warnings (and revert object fallback) (#12193)
The key issue this commit seeks to address, is that existing tools / documentation often lead users to mistakenly use object types and not role names, a classic example being `function` not `func`
Previously, the warning message for using e.g. `` external:function`target` `` (with `py` as the default domain), would be:
```
WARNING: role for external cross-reference not found: function
```
This gives no information to the user on how to fix the issue, even though there is actually quite an easy fix
This commit adds logic to create more specific / helpful warning messages, e.g.
```
WARNING: role for external cross-reference not found in domains 'py', 'std': 'function' (perhaps you meant one of: 'py:func', 'py:obj')
```
This goes through the same original logic but, if the role is not found, it will look if the role name is actually an available object type on the domain(s), and then suggest its related roles.
This commit also reverts #12133, which provided a (silent) fallback to auto convert an object type to a role name.
2024-03-25 08:39:51 -05:00
|
|
|
role = names[1]
|
2021-04-04 11:34:45 -05:00
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
👌 Improve `external` role warnings (and revert object fallback) (#12193)
The key issue this commit seeks to address, is that existing tools / documentation often lead users to mistakenly use object types and not role names, a classic example being `function` not `func`
Previously, the warning message for using e.g. `` external:function`target` `` (with `py` as the default domain), would be:
```
WARNING: role for external cross-reference not found: function
```
This gives no information to the user on how to fix the issue, even though there is actually quite an easy fix
This commit adds logic to create more specific / helpful warning messages, e.g.
```
WARNING: role for external cross-reference not found in domains 'py', 'std': 'function' (perhaps you meant one of: 'py:func', 'py:obj')
```
This goes through the same original logic but, if the role is not found, it will look if the role name is actually an available object type on the domain(s), and then suggest its related roles.
This commit also reverts #12133, which provided a (silent) fallback to auto convert an object type to a role name.
2024-03-25 08:39:51 -05:00
|
|
|
if domain and self.is_existent_role(domain, role):
|
2021-04-04 11:34:45 -05:00
|
|
|
return (domain, role)
|
👌 Improve `external` role warnings (and revert object fallback) (#12193)
The key issue this commit seeks to address, is that existing tools / documentation often lead users to mistakenly use object types and not role names, a classic example being `function` not `func`
Previously, the warning message for using e.g. `` external:function`target` `` (with `py` as the default domain), would be:
```
WARNING: role for external cross-reference not found: function
```
This gives no information to the user on how to fix the issue, even though there is actually quite an easy fix
This commit adds logic to create more specific / helpful warning messages, e.g.
```
WARNING: role for external cross-reference not found in domains 'py', 'std': 'function' (perhaps you meant one of: 'py:func', 'py:obj')
```
This goes through the same original logic but, if the role is not found, it will look if the role name is actually an available object type on the domain(s), and then suggest its related roles.
This commit also reverts #12133, which provided a (silent) fallback to auto convert an object type to a role name.
2024-03-25 08:39:51 -05:00
|
|
|
elif self.is_existent_role('std', role):
|
2021-04-04 11:34:45 -05:00
|
|
|
return ('std', role)
|
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
👌 Improve `external` role warnings (and revert object fallback) (#12193)
The key issue this commit seeks to address, is that existing tools / documentation often lead users to mistakenly use object types and not role names, a classic example being `function` not `func`
Previously, the warning message for using e.g. `` external:function`target` `` (with `py` as the default domain), would be:
```
WARNING: role for external cross-reference not found: function
```
This gives no information to the user on how to fix the issue, even though there is actually quite an easy fix
This commit adds logic to create more specific / helpful warning messages, e.g.
```
WARNING: role for external cross-reference not found in domains 'py', 'std': 'function' (perhaps you meant one of: 'py:func', 'py:obj')
```
This goes through the same original logic but, if the role is not found, it will look if the role name is actually an available object type on the domain(s), and then suggest its related roles.
This commit also reverts #12133, which provided a (silent) fallback to auto convert an object type to a role name.
2024-03-25 08:39:51 -05:00
|
|
|
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)
|
|
|
|
)
|
2021-04-04 11:34:45 -05:00
|
|
|
try:
|
|
|
|
domain = self.env.get_domain(domain_name)
|
👌 Improve `external` role warnings (and revert object fallback) (#12193)
The key issue this commit seeks to address, is that existing tools / documentation often lead users to mistakenly use object types and not role names, a classic example being `function` not `func`
Previously, the warning message for using e.g. `` external:function`target` `` (with `py` as the default domain), would be:
```
WARNING: role for external cross-reference not found: function
```
This gives no information to the user on how to fix the issue, even though there is actually quite an easy fix
This commit adds logic to create more specific / helpful warning messages, e.g.
```
WARNING: role for external cross-reference not found in domains 'py', 'std': 'function' (perhaps you meant one of: 'py:func', 'py:obj')
```
This goes through the same original logic but, if the role is not found, it will look if the role name is actually an available object type on the domain(s), and then suggest its related roles.
This commit also reverts #12133, which provided a (silent) fallback to auto convert an object type to a role name.
2024-03-25 08:39:51 -05:00
|
|
|
return role_name in domain.roles
|
2021-04-04 11:34:45 -05:00
|
|
|
except ExtensionError:
|
👌 Improve `external` role warnings (and revert object fallback) (#12193)
The key issue this commit seeks to address, is that existing tools / documentation often lead users to mistakenly use object types and not role names, a classic example being `function` not `func`
Previously, the warning message for using e.g. `` external:function`target` `` (with `py` as the default domain), would be:
```
WARNING: role for external cross-reference not found: function
```
This gives no information to the user on how to fix the issue, even though there is actually quite an easy fix
This commit adds logic to create more specific / helpful warning messages, e.g.
```
WARNING: role for external cross-reference not found in domains 'py', 'std': 'function' (perhaps you meant one of: 'py:func', 'py:obj')
```
This goes through the same original logic but, if the role is not found, it will look if the role name is actually an available object type on the domain(s), and then suggest its related roles.
This commit also reverts #12133, which provided a (silent) fallback to auto convert an object type to a role name.
2024-03-25 08:39:51 -05:00
|
|
|
return False
|
2021-04-04 11:34:45 -05:00
|
|
|
|
|
|
|
def invoke_role(self, role: tuple[str, str]) -> tuple[list[Node], list[system_message]]:
|
👌 Handle external references pointing to object types (#12133)
This commit fixes the issue of `objects.inv` denoting object names, whilst the `external` role only allows for role names.
As an example, take the `objects.inv` for the sphinx documentation, which contains:
```
py:function
compile : usage/domains/python.html#compile
```
A user might understandably expect that they could reference this using `` :external:py:function:`compile` ``, but actually this would previously error with:
```
WARNING: role for external cross-reference not found: py:function
```
this is because, `function` is the object type, yet `external` expects the related role name `func`.
It should not be necessary for the user to know about this distinction,
so in this commit, we add logic, to first look if the name relates to a role name (as previous, to not be back-breaking) but, if not, then also look if the name relates to an object that has a known role and, if so, use that.
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
2024-03-19 07:42:50 -05:00
|
|
|
"""Invoke the role described by a ``(domain, role name)`` pair."""
|
👌 Improve `external` role warnings (and revert object fallback) (#12193)
The key issue this commit seeks to address, is that existing tools / documentation often lead users to mistakenly use object types and not role names, a classic example being `function` not `func`
Previously, the warning message for using e.g. `` external:function`target` `` (with `py` as the default domain), would be:
```
WARNING: role for external cross-reference not found: function
```
This gives no information to the user on how to fix the issue, even though there is actually quite an easy fix
This commit adds logic to create more specific / helpful warning messages, e.g.
```
WARNING: role for external cross-reference not found in domains 'py', 'std': 'function' (perhaps you meant one of: 'py:func', 'py:obj')
```
This goes through the same original logic but, if the role is not found, it will look if the role name is actually an available object type on the domain(s), and then suggest its related roles.
This commit also reverts #12133, which provided a (silent) fallback to auto convert an object type to a role name.
2024-03-25 08:39:51 -05:00
|
|
|
_deprecation_warning(
|
|
|
|
__name__, f'{self.__class__.__name__}.invoke_role', '', remove=(9, 0)
|
|
|
|
)
|
2021-04-04 11:34:45 -05:00
|
|
|
domain = self.env.get_domain(role[0])
|
|
|
|
if domain:
|
|
|
|
role_func = domain.role(role[1])
|
2023-07-27 14:17:35 -05:00
|
|
|
assert role_func is not None
|
2021-04-04 11:34:45 -05:00
|
|
|
|
|
|
|
return role_func(':'.join(role), self.rawtext, self.text, self.lineno,
|
|
|
|
self.inliner, self.options, self.content)
|
|
|
|
else:
|
|
|
|
return [], []
|
|
|
|
|
|
|
|
|
|
|
|
class IntersphinxRoleResolver(ReferencesResolver):
|
|
|
|
"""pending_xref node resolver for intersphinx role.
|
|
|
|
|
|
|
|
This resolves pending_xref nodes generated by :intersphinx:***: role.
|
|
|
|
"""
|
|
|
|
|
|
|
|
default_priority = ReferencesResolver.default_priority - 1
|
|
|
|
|
|
|
|
def run(self, **kwargs: Any) -> None:
|
2022-04-21 21:34:31 -05:00
|
|
|
for node in self.document.findall(pending_xref):
|
2021-10-31 09:30:13 -05:00
|
|
|
if 'intersphinx' not in node:
|
|
|
|
continue
|
|
|
|
contnode = cast(nodes.TextElement, node[0].deepcopy())
|
|
|
|
inv_name = node['inventory']
|
|
|
|
if inv_name is not None:
|
2021-11-06 08:02:37 -05:00
|
|
|
assert inventory_exists(self.env, inv_name)
|
2021-10-31 09:30:13 -05:00
|
|
|
newnode = resolve_reference_in_inventory(self.env, inv_name, node, contnode)
|
|
|
|
else:
|
|
|
|
newnode = resolve_reference_any_inventory(self.env, False, node, contnode)
|
|
|
|
if newnode is None:
|
2021-11-06 07:25:49 -05:00
|
|
|
typ = node['reftype']
|
|
|
|
msg = (__('external %s:%s reference target not found: %s') %
|
|
|
|
(node['refdomain'], typ, node['reftarget']))
|
|
|
|
logger.warning(msg, location=node, type='ref', subtype=typ)
|
|
|
|
node.replace_self(contnode)
|
2021-10-31 09:30:13 -05:00
|
|
|
else:
|
|
|
|
node.replace_self(newnode)
|
2021-04-04 11:34:45 -05:00
|
|
|
|
|
|
|
|
|
|
|
def install_dispatcher(app: Sphinx, docname: str, source: list[str]) -> None:
|
|
|
|
"""Enable IntersphinxDispatcher.
|
|
|
|
|
2022-05-07 02:13:26 -05:00
|
|
|
.. note:: The installed dispatcher will be uninstalled on disabling sphinx_domain
|
2021-04-04 11:34:45 -05:00
|
|
|
automatically.
|
|
|
|
"""
|
|
|
|
dispatcher = IntersphinxDispatcher()
|
|
|
|
dispatcher.enable()
|
|
|
|
|
|
|
|
|
2018-11-04 05:22:50 -06:00
|
|
|
def normalize_intersphinx_mapping(app: Sphinx, config: Config) -> None:
|
|
|
|
for key, value in config.intersphinx_mapping.copy().items():
|
|
|
|
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
|
|
|
|
else:
|
|
|
|
# old format, no name
|
2023-03-17 11:41:22 -05:00
|
|
|
# xref RemovedInSphinx80Warning
|
2018-11-04 05:22:50 -06:00
|
|
|
name, uri, inv = None, key, value
|
2023-03-17 12:43:44 -05:00
|
|
|
msg = (
|
2023-03-17 11:41:22 -05:00
|
|
|
"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}}}\"."
|
2023-03-17 12:43:44 -05:00
|
|
|
"https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#confval-intersphinx_mapping" # NoQA: E501
|
2023-03-17 11:41:22 -05:00
|
|
|
)
|
2023-03-17 12:43:44 -05:00
|
|
|
logger.warning(msg)
|
2018-11-04 05:22:50 -06:00
|
|
|
|
|
|
|
if not isinstance(inv, tuple):
|
|
|
|
config.intersphinx_mapping[key] = (name, (uri, (inv,)))
|
|
|
|
else:
|
|
|
|
config.intersphinx_mapping[key] = (name, (uri, inv))
|
|
|
|
except Exception as exc:
|
2020-03-08 19:47:11 -05:00
|
|
|
logger.warning(__('Failed to read intersphinx_mapping[%s], ignored: %r'), key, exc)
|
2018-11-04 05:22:50 -06:00
|
|
|
config.intersphinx_mapping.pop(key)
|
|
|
|
|
|
|
|
|
2024-03-21 10:19:26 -05:00
|
|
|
def setup(app: Sphinx) -> ExtensionMetadata:
|
2024-01-02 22:17:46 -06:00
|
|
|
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')
|
2020-04-04 23:59:42 -05:00
|
|
|
app.connect('config-inited', normalize_intersphinx_mapping, priority=800)
|
2008-08-04 12:31:25 -05:00
|
|
|
app.connect('builder-inited', load_mappings)
|
2021-04-04 11:34:45 -05:00
|
|
|
app.connect('source-read', install_dispatcher)
|
2018-11-04 05:22:50 -06:00
|
|
|
app.connect('missing-reference', missing_reference)
|
2021-04-04 11:34:45 -05:00
|
|
|
app.add_post_transform(IntersphinxRoleResolver)
|
2018-01-24 06:37:54 -06:00
|
|
|
return {
|
|
|
|
'version': sphinx.__display_version__,
|
|
|
|
'env_version': 1,
|
2023-02-17 16:11:14 -06:00
|
|
|
'parallel_read_safe': True,
|
2018-01-24 06:37:54 -06:00
|
|
|
}
|