Enable automatic formatting for `sphinx/ext/intersphinx/` (#12970)

This commit is contained in:
Adam Turner 2024-10-04 17:08:30 +01:00 committed by GitHub
parent a404f3e66c
commit 3a066f2bbc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 223 additions and 101 deletions

View File

@ -468,7 +468,6 @@ exclude = [
"sphinx/ext/imgconverter.py", "sphinx/ext/imgconverter.py",
"sphinx/ext/imgmath.py", "sphinx/ext/imgmath.py",
"sphinx/ext/inheritance_diagram.py", "sphinx/ext/inheritance_diagram.py",
"sphinx/ext/intersphinx/*",
"sphinx/ext/linkcode.py", "sphinx/ext/linkcode.py",
"sphinx/ext/mathjax.py", "sphinx/ext/mathjax.py",
"sphinx/ext/napoleon/__init__.py", "sphinx/ext/napoleon/__init__.py",

View File

@ -10,9 +10,11 @@ from sphinx.ext.intersphinx._load import _fetch_inventory
def inspect_main(argv: list[str], /) -> int: def inspect_main(argv: list[str], /) -> int:
"""Debug functionality to print out an inventory""" """Debug functionality to print out an inventory"""
if len(argv) < 1: if len(argv) < 1:
print('Print out an inventory file.\n' print(
'Print out an inventory file.\n'
'Error: must specify local path or URL to an inventory file.', 'Error: must specify local path or URL to an inventory file.',
file=sys.stderr) file=sys.stderr,
)
return 1 return 1
class MockConfig: class MockConfig:
@ -27,7 +29,7 @@ def inspect_main(argv: list[str], /) -> int:
target_uri='', target_uri='',
inv_location=filename, inv_location=filename,
config=MockConfig(), # type: ignore[arg-type] config=MockConfig(), # type: ignore[arg-type]
srcdir='' # type: ignore[arg-type] srcdir='', # type: ignore[arg-type]
) )
for key in sorted(inv_data or {}): for key in sorted(inv_data or {}):
print(key) print(key)

View File

@ -89,8 +89,10 @@ def validate_intersphinx_mapping(app: Sphinx, config: Config) -> None:
# ensure target URIs are non-empty and unique # ensure target URIs are non-empty and unique
if not uri or not isinstance(uri, str): if not uri or not isinstance(uri, str):
errors += 1 errors += 1
msg = __('Invalid target URI value `%r` in intersphinx_mapping[%r][0]. ' msg = __(
'Target URIs must be unique non-empty strings.') 'Invalid target URI value `%r` in intersphinx_mapping[%r][0]. '
'Target URIs must be unique non-empty strings.'
)
LOGGER.error(msg, uri, name) LOGGER.error(msg, uri, name)
del config.intersphinx_mapping[name] del config.intersphinx_mapping[name]
continue continue
@ -105,9 +107,12 @@ def validate_intersphinx_mapping(app: Sphinx, config: Config) -> None:
continue continue
seen[uri] = name seen[uri] = name
if not isinstance(inv, tuple | list):
inv = (inv,)
# ensure inventory locations are None or non-empty # ensure inventory locations are None or non-empty
targets: list[InventoryLocation] = [] targets: list[InventoryLocation] = []
for target in (inv if isinstance(inv, (tuple | list)) else (inv,)): for target in inv:
if target is None or target and isinstance(target, str): if target is None or target and isinstance(target, str):
targets.append(target) targets.append(target)
else: else:
@ -143,9 +148,13 @@ def load_mappings(app: Sphinx) -> None:
projects = [] projects = []
for name, (uri, locations) in intersphinx_mapping.values(): for name, (uri, locations) in intersphinx_mapping.values():
try: try:
project = _IntersphinxProject(name=name, target_uri=uri, locations=locations) project = _IntersphinxProject(
name=name, target_uri=uri, locations=locations
)
except ValueError as err: except ValueError as err:
msg = __('An invalid intersphinx_mapping entry was added after normalisation.') msg = __(
'An invalid intersphinx_mapping entry was added after normalisation.'
)
raise ConfigError(msg) from err raise ConfigError(msg) from err
else: else:
projects.append(project) projects.append(project)
@ -223,8 +232,11 @@ def _fetch_inventory_group(
or project.target_uri not in cache or project.target_uri not in cache
or cache[project.target_uri][1] < cache_time or cache[project.target_uri][1] < cache_time
): ):
LOGGER.info(__("loading intersphinx inventory '%s' from %s ..."), LOGGER.info(
project.name, _get_safe_url(inv)) __("loading intersphinx inventory '%s' from %s ..."),
project.name,
_get_safe_url(inv),
)
try: try:
invdata = _fetch_inventory( invdata = _fetch_inventory(
@ -245,14 +257,21 @@ def _fetch_inventory_group(
if not failures: if not failures:
pass pass
elif len(failures) < len(project.locations): elif len(failures) < len(project.locations):
LOGGER.info(__('encountered some issues with some of the inventories,' LOGGER.info(
' but they had working alternatives:')) __(
'encountered some issues with some of the inventories,'
' but they had working alternatives:'
)
)
for fail in failures: for fail in failures:
LOGGER.info(*fail) LOGGER.info(*fail)
else: 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 ' LOGGER.warning(
'with the following issues:') + '\n' + issues) __('failed to reach any of the inventories ' 'with the following issues:')
+ '\n'
+ issues
)
return updated return updated
@ -267,7 +286,7 @@ def fetch_inventory(app: Sphinx, uri: InventoryURI, inv: str) -> Inventory:
def _fetch_inventory( def _fetch_inventory(
*, target_uri: InventoryURI, inv_location: str, config: Config, srcdir: Path, *, target_uri: InventoryURI, inv_location: str, config: Config, srcdir: Path
) -> Inventory: ) -> Inventory:
"""Fetch, parse and return an intersphinx inventory file.""" """Fetch, parse and return an intersphinx inventory file."""
# both *target_uri* (base URI of the links to generate) # both *target_uri* (base URI of the links to generate)
@ -282,8 +301,12 @@ def _fetch_inventory(
else: else:
f = open(path.join(srcdir, inv_location), 'rb') # NoQA: SIM115 f = open(path.join(srcdir, inv_location), 'rb') # NoQA: SIM115
except Exception as err: except Exception as err:
err.args = ('intersphinx inventory %r not fetchable due to %s: %s', err.args = (
inv_location, err.__class__, str(err)) 'intersphinx inventory %r not fetchable due to %s: %s',
inv_location,
err.__class__,
str(err),
)
raise raise
try: try:
if hasattr(f, 'url'): if hasattr(f, 'url'):
@ -295,17 +318,22 @@ def _fetch_inventory(
if target_uri in { if target_uri in {
inv_location, inv_location,
path.dirname(inv_location), path.dirname(inv_location),
path.dirname(inv_location) + '/' path.dirname(inv_location) + '/',
}: }:
target_uri = path.dirname(new_inv_location) target_uri = path.dirname(new_inv_location)
with f: with f:
try: try:
invdata = InventoryFile.load(f, target_uri, posixpath.join) invdata = InventoryFile.load(f, target_uri, posixpath.join)
except ValueError as exc: except ValueError as exc:
raise ValueError('unknown or unsupported inventory version: %r' % exc) from exc msg = f'unknown or unsupported inventory version: {exc!r}'
raise ValueError(msg) from exc
except Exception as err: except Exception as err:
err.args = ('intersphinx inventory %r not readable due to %s: %s', err.args = (
inv_location, err.__class__.__name__, str(err)) 'intersphinx inventory %r not readable due to %s: %s',
inv_location,
err.__class__.__name__,
str(err),
)
raise raise
else: else:
return invdata return invdata
@ -373,9 +401,13 @@ def _read_from_url(url: str, *, config: Config) -> HTTPResponse:
:return: data read from resource described by *url* :return: data read from resource described by *url*
:rtype: ``file``-like object :rtype: ``file``-like object
""" """
r = requests.get(url, stream=True, timeout=config.intersphinx_timeout, r = requests.get(
url,
stream=True,
timeout=config.intersphinx_timeout,
_user_agent=config.user_agent, _user_agent=config.user_agent,
_tls_info=(config.tls_verify, config.tls_cacerts)) _tls_info=(config.tls_verify, config.tls_cacerts),
)
r.raise_for_status() r.raise_for_status()
# For inv_location / new_inv_location # For inv_location / new_inv_location

View File

@ -32,9 +32,13 @@ if TYPE_CHECKING:
from sphinx.util.typing import Inventory, InventoryItem, RoleFunction from sphinx.util.typing import Inventory, InventoryItem, RoleFunction
def _create_element_from_result(domain: Domain, inv_name: InventoryName | None, def _create_element_from_result(
domain: Domain,
inv_name: InventoryName | None,
data: InventoryItem, data: InventoryItem,
node: pending_xref, contnode: TextElement) -> nodes.reference: node: pending_xref,
contnode: TextElement,
) -> nodes.reference:
proj, version, uri, dispname = data proj, version, uri, dispname = data
if '://' not in uri and node.get('refdoc'): if '://' not in uri and node.get('refdoc'):
# get correct path in case of subdirectories # get correct path in case of subdirectories
@ -51,8 +55,11 @@ def _create_element_from_result(domain: Domain, inv_name: InventoryName | None,
# use whatever title was given, but strip prefix # use whatever title was given, but strip prefix
title = contnode.astext() title = contnode.astext()
if inv_name is not None and title.startswith(inv_name + ':'): if inv_name is not None and title.startswith(inv_name + ':'):
newnode.append(contnode.__class__(title[len(inv_name) + 1:], newnode.append(
title[len(inv_name) + 1:])) contnode.__class__(
title[len(inv_name) + 1 :], title[len(inv_name) + 1 :]
)
)
else: else:
newnode.append(contnode) newnode.append(contnode)
else: else:
@ -62,10 +69,14 @@ def _create_element_from_result(domain: Domain, inv_name: InventoryName | None,
def _resolve_reference_in_domain_by_target( def _resolve_reference_in_domain_by_target(
inv_name: InventoryName | None, inventory: Inventory, inv_name: InventoryName | None,
domain: Domain, objtypes: Iterable[str], inventory: Inventory,
domain: Domain,
objtypes: Iterable[str],
target: str, target: str,
node: pending_xref, contnode: TextElement) -> nodes.reference | None: node: pending_xref,
contnode: TextElement,
) -> nodes.reference | None:
for objtype in objtypes: for objtype in objtypes:
if objtype not in inventory: if objtype not in inventory:
# Continue if there's nothing of this kind in the inventory # Continue if there's nothing of this kind in the inventory
@ -79,19 +90,34 @@ def _resolve_reference_in_domain_by_target(
# * 'term': https://github.com/sphinx-doc/sphinx/issues/9291 # * 'term': https://github.com/sphinx-doc/sphinx/issues/9291
# * 'label': https://github.com/sphinx-doc/sphinx/issues/12008 # * 'label': https://github.com/sphinx-doc/sphinx/issues/12008
target_lower = target.lower() target_lower = target.lower()
insensitive_matches = list(filter(lambda k: k.lower() == target_lower, insensitive_matches = list(
inventory[objtype].keys())) filter(lambda k: k.lower() == target_lower, inventory[objtype].keys())
)
if len(insensitive_matches) > 1: if len(insensitive_matches) > 1:
data_items = {inventory[objtype][match] for match in insensitive_matches} data_items = {
inventory[objtype][match] for match in insensitive_matches
}
inv_descriptor = inv_name or 'main_inventory' inv_descriptor = inv_name or 'main_inventory'
if len(data_items) == 1: # these are duplicates; relatively innocuous if len(data_items) == 1: # these are duplicates; relatively innocuous
LOGGER.debug(__("inventory '%s': duplicate matches found for %s:%s"), LOGGER.debug(
inv_descriptor, objtype, target, __("inventory '%s': duplicate matches found for %s:%s"),
type='intersphinx', subtype='external', location=node) inv_descriptor,
objtype,
target,
type='intersphinx',
subtype='external',
location=node,
)
else: else:
LOGGER.warning(__("inventory '%s': multiple matches found for %s:%s"), LOGGER.warning(
inv_descriptor, objtype, target, __("inventory '%s': multiple matches found for %s:%s"),
type='intersphinx', subtype='external', location=node) inv_descriptor,
objtype,
target,
type='intersphinx',
subtype='external',
location=node,
)
if insensitive_matches: if insensitive_matches:
data = inventory[objtype][insensitive_matches[0]] data = inventory[objtype][insensitive_matches[0]]
else: else:
@ -106,11 +132,15 @@ def _resolve_reference_in_domain_by_target(
return None return None
def _resolve_reference_in_domain(env: BuildEnvironment, def _resolve_reference_in_domain(
inv_name: InventoryName | None, inventory: Inventory, env: BuildEnvironment,
inv_name: InventoryName | None,
inventory: Inventory,
honor_disabled_refs: bool, honor_disabled_refs: bool,
domain: Domain, objtypes: Iterable[str], domain: Domain,
node: pending_xref, contnode: TextElement, objtypes: Iterable[str],
node: pending_xref,
contnode: TextElement,
) -> nodes.reference | None: ) -> nodes.reference | None:
obj_types: dict[str, None] = {}.fromkeys(objtypes) obj_types: dict[str, None] = {}.fromkeys(objtypes)
@ -129,15 +159,16 @@ def _resolve_reference_in_domain(env: BuildEnvironment,
# now that the objtypes list is complete we can remove the disabled ones # now that the objtypes list is complete we can remove the disabled ones
if honor_disabled_refs: if honor_disabled_refs:
disabled = set(env.config.intersphinx_disabled_reftypes) disabled = set(env.config.intersphinx_disabled_reftypes)
obj_types = {obj_type: None obj_types = {
for obj_type in obj_types obj_type: None for obj_type in obj_types if obj_type not in disabled
if obj_type not in disabled} }
objtypes = [*obj_types.keys()] objtypes = [*obj_types.keys()]
# without qualification # without qualification
res = _resolve_reference_in_domain_by_target(inv_name, inventory, domain, objtypes, res = _resolve_reference_in_domain_by_target(
node['reftarget'], node, contnode) inv_name, inventory, domain, objtypes, node['reftarget'], node, contnode
)
if res is not None: if res is not None:
return res return res
@ -145,14 +176,19 @@ def _resolve_reference_in_domain(env: BuildEnvironment,
full_qualified_name = domain.get_full_qualified_name(node) full_qualified_name = domain.get_full_qualified_name(node)
if full_qualified_name is None: if full_qualified_name is None:
return None return None
return _resolve_reference_in_domain_by_target(inv_name, inventory, domain, objtypes, return _resolve_reference_in_domain_by_target(
full_qualified_name, node, contnode) inv_name, inventory, domain, objtypes, full_qualified_name, node, contnode
)
def _resolve_reference(env: BuildEnvironment, def _resolve_reference(
inv_name: InventoryName | None, inventory: Inventory, env: BuildEnvironment,
inv_name: InventoryName | None,
inventory: Inventory,
honor_disabled_refs: bool, honor_disabled_refs: bool,
node: pending_xref, contnode: TextElement) -> nodes.reference | None: node: pending_xref,
contnode: TextElement,
) -> nodes.reference | None:
# disabling should only be done if no inventory is given # disabling should only be done if no inventory is given
honor_disabled_refs = honor_disabled_refs and inv_name is None honor_disabled_refs = honor_disabled_refs and inv_name is None
intersphinx_disabled_reftypes = env.config.intersphinx_disabled_reftypes intersphinx_disabled_reftypes = env.config.intersphinx_disabled_reftypes
@ -163,13 +199,22 @@ def _resolve_reference(env: BuildEnvironment,
typ = node['reftype'] typ = node['reftype']
if typ == 'any': if typ == 'any':
for domain in env.domains.sorted(): for domain in env.domains.sorted():
if honor_disabled_refs and f'{domain.name}:*' in intersphinx_disabled_reftypes: if (
honor_disabled_refs
and f'{domain.name}:*' in intersphinx_disabled_reftypes
):
continue continue
objtypes: Iterable[str] = domain.object_types.keys() objtypes: Iterable[str] = domain.object_types.keys()
res = _resolve_reference_in_domain(env, inv_name, inventory, res = _resolve_reference_in_domain(
env,
inv_name,
inventory,
honor_disabled_refs, honor_disabled_refs,
domain, objtypes, domain,
node, contnode) objtypes,
node,
contnode,
)
if res is not None: if res is not None:
return res return res
return None return None
@ -184,19 +229,27 @@ def _resolve_reference(env: BuildEnvironment,
objtypes = domain.objtypes_for_role(typ) or () objtypes = domain.objtypes_for_role(typ) or ()
if not objtypes: if not objtypes:
return None return None
return _resolve_reference_in_domain(env, inv_name, inventory, return _resolve_reference_in_domain(
env,
inv_name,
inventory,
honor_disabled_refs, honor_disabled_refs,
domain, objtypes, domain,
node, contnode) objtypes,
node,
contnode,
)
def inventory_exists(env: BuildEnvironment, inv_name: InventoryName) -> bool: def inventory_exists(env: BuildEnvironment, inv_name: InventoryName) -> bool:
return inv_name in InventoryAdapter(env).named_inventory return inv_name in InventoryAdapter(env).named_inventory
def resolve_reference_in_inventory(env: BuildEnvironment, def resolve_reference_in_inventory(
env: BuildEnvironment,
inv_name: InventoryName, inv_name: InventoryName,
node: pending_xref, contnode: TextElement, node: pending_xref,
contnode: TextElement,
) -> nodes.reference | None: ) -> nodes.reference | None:
"""Attempt to resolve a missing reference via intersphinx references. """Attempt to resolve a missing reference via intersphinx references.
@ -205,25 +258,38 @@ def resolve_reference_in_inventory(env: BuildEnvironment,
Requires ``inventory_exists(env, inv_name)``. Requires ``inventory_exists(env, inv_name)``.
""" """
assert inventory_exists(env, inv_name) assert inventory_exists(env, inv_name)
return _resolve_reference(env, inv_name, InventoryAdapter(env).named_inventory[inv_name], return _resolve_reference(
False, node, contnode) env,
inv_name,
InventoryAdapter(env).named_inventory[inv_name],
False,
node,
contnode,
)
def resolve_reference_any_inventory(env: BuildEnvironment, def resolve_reference_any_inventory(
env: BuildEnvironment,
honor_disabled_refs: bool, honor_disabled_refs: bool,
node: pending_xref, contnode: TextElement, node: pending_xref,
contnode: TextElement,
) -> nodes.reference | None: ) -> nodes.reference | None:
"""Attempt to resolve a missing reference via intersphinx references. """Attempt to resolve a missing reference via intersphinx references.
Resolution is tried with the target as is in any inventory. Resolution is tried with the target as is in any inventory.
""" """
return _resolve_reference(env, None, InventoryAdapter(env).main_inventory, return _resolve_reference(
env,
None,
InventoryAdapter(env).main_inventory,
honor_disabled_refs, honor_disabled_refs,
node, contnode) node,
contnode,
)
def resolve_reference_detect_inventory(env: BuildEnvironment, def resolve_reference_detect_inventory(
node: pending_xref, contnode: TextElement, env: BuildEnvironment, node: pending_xref, contnode: TextElement
) -> nodes.reference | None: ) -> nodes.reference | None:
"""Attempt to resolve a missing reference via intersphinx references. """Attempt to resolve a missing reference via intersphinx references.
@ -250,8 +316,9 @@ def resolve_reference_detect_inventory(env: BuildEnvironment,
return res_inv return res_inv
def missing_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref, def missing_reference(
contnode: TextElement) -> nodes.reference | None: app: Sphinx, env: BuildEnvironment, node: pending_xref, contnode: TextElement
) -> nodes.reference | None:
"""Attempt to resolve a missing reference via intersphinx references.""" """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)
@ -263,7 +330,11 @@ class IntersphinxDispatcher(CustomReSTDispatcher):
""" """
def role( def role(
self, role_name: str, language_module: ModuleType, lineno: int, reporter: Reporter, self,
role_name: str,
language_module: ModuleType,
lineno: int,
reporter: Reporter,
) -> tuple[RoleFunction, list[system_message]]: ) -> tuple[RoleFunction, list[system_message]]:
if len(role_name) > 9 and role_name.startswith(('external:', 'external+')): if len(role_name) > 9 and role_name.startswith(('external:', 'external+')):
return IntersphinxRole(role_name), [] return IntersphinxRole(role_name), []
@ -461,7 +532,9 @@ class IntersphinxRole(SphinxRole):
except ExtensionError: except ExtensionError:
return False return False
def invoke_role(self, role: tuple[str, str]) -> tuple[list[Node], list[system_message]]: def invoke_role(
self, role: tuple[str, str]
) -> tuple[list[Node], list[system_message]]:
"""Invoke the role described by a ``(domain, role name)`` pair.""" """Invoke the role described by a ``(domain, role name)`` pair."""
_deprecation_warning( _deprecation_warning(
__name__, f'{self.__class__.__name__}.invoke_role', '', remove=(9, 0) __name__, f'{self.__class__.__name__}.invoke_role', '', remove=(9, 0)
@ -471,8 +544,15 @@ class IntersphinxRole(SphinxRole):
role_func = domain.role(role[1]) role_func = domain.role(role[1])
assert role_func is not None assert role_func is not None
return role_func(':'.join(role), self.rawtext, self.text, self.lineno, return role_func(
self.inliner, self.options, self.content) ':'.join(role),
self.rawtext,
self.text,
self.lineno,
self.inliner,
self.options,
self.content,
)
else: else:
return [], [] return [], []
@ -493,13 +573,20 @@ class IntersphinxRoleResolver(ReferencesResolver):
inv_name = node['inventory'] inv_name = node['inventory']
if inv_name is not None: if inv_name is not None:
assert inventory_exists(self.env, inv_name) 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
)
else: else:
newnode = resolve_reference_any_inventory(self.env, False, node, contnode) newnode = resolve_reference_any_inventory(
self.env, False, node, contnode
)
if newnode is None: if newnode is None:
typ = node['reftype'] typ = node['reftype']
msg = (__('external %s:%s reference target not found: %s') % msg = __('external %s:%s reference target not found: %s') % (
(node['refdomain'], typ, node['reftarget'])) node['refdomain'],
typ,
node['reftarget'],
)
LOGGER.warning(msg, location=node, type='ref', subtype=typ) LOGGER.warning(msg, location=node, type='ref', subtype=typ)
node.replace_self(contnode) node.replace_self(contnode)
else: else:

View File

@ -54,7 +54,7 @@ class _IntersphinxProject:
'locations': 'A tuple of local or remote targets containing ' 'locations': 'A tuple of local or remote targets containing '
'the inventory data to fetch. ' 'the inventory data to fetch. '
'None indicates the default inventory file name.', 'None indicates the default inventory file name.',
} } # fmt: skip
def __init__( def __init__(
self, self,
@ -83,10 +83,12 @@ class _IntersphinxProject:
object.__setattr__(self, 'locations', tuple(locations)) object.__setattr__(self, 'locations', tuple(locations))
def __repr__(self) -> str: def __repr__(self) -> str:
return (f'{self.__class__.__name__}(' return (
f'{self.__class__.__name__}('
f'name={self.name!r}, ' f'name={self.name!r}, '
f'target_uri={self.target_uri!r}, ' f'target_uri={self.target_uri!r}, '
f'locations={self.locations!r})') f'locations={self.locations!r})'
)
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool:
if not isinstance(other, _IntersphinxProject): if not isinstance(other, _IntersphinxProject):