Skip to content

Commit 4c19d9f

Browse files
committed
Merge branch '4.x'
2 parents b32d4b5 + a8eb1aa commit 4c19d9f

File tree

8 files changed

+342
-125
lines changed

8 files changed

+342
-125
lines changed

CHANGES

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ Features added
6868
* #9695: More CSS classes on Javascript domain descriptions
6969
* #9683: Revert the removal of ``add_stylesheet()`` API. It will be kept until
7070
the Sphinx-6.0 release
71+
* #2068, add :confval:`intersphinx_disabled_reftypes` for disabling
72+
interphinx resolution of cross-references that do not have an explicit
73+
inventory specification. Specific types of cross-references can be disabled,
74+
e.g., ``std:doc`` or all cross-references in a specific domain,
75+
e.g., ``std:*``.
7176

7277
Bugs fixed
7378
----------
@@ -105,6 +110,9 @@ Bugs fixed
105110
* #9688: Fix :rst:dir:`code`` does not recognize ``:class:`` option
106111
* #9733: Fix for logging handler flushing warnings in the middle of the docs
107112
build
113+
* #9656: Fix warnings without subtype being incorrectly suppressed
114+
* Intersphinx, for unresolved references with an explicit inventory,
115+
e.g., ``proj:myFunc``, leave the inventory prefix in the unresolved text.
108116

109117
Testing
110118
--------

doc/usage/extensions/intersphinx.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,35 @@ linking:
148148
exception is raised if the server has not issued a response for timeout
149149
seconds.
150150

151+
.. confval:: intersphinx_disabled_reftypes
152+
153+
.. versionadded:: 4.3
154+
155+
A list of strings being either:
156+
157+
- the name of a specific reference type in a domain,
158+
e.g., ``std:doc``, ``py:func``, or ``cpp:class``,
159+
- the name of a domain, and a wildcard, e.g.,
160+
``std:*``, ``py:*``, or ``cpp:*``, or
161+
- simply a wildcard ``*``.
162+
163+
The default value is an empty list.
164+
165+
When a cross-reference without an explicit inventory specification is being
166+
resolved by intersphinx, skip resolution if it matches one of the
167+
specifications in this list.
168+
169+
For example, with ``intersphinx_disabled_reftypes = ['std:doc']``
170+
a cross-reference ``:doc:`installation``` will not be attempted to be
171+
resolved by intersphinx, but ``:doc:`otherbook:installation``` will be
172+
attempted to be resolved in the inventory named ``otherbook`` in
173+
:confval:`intersphinx_mapping`.
174+
At the same time, all cross-references generated in, e.g., Python,
175+
declarations will still be attempted to be resolved by intersphinx.
176+
177+
If ``*`` is in the list of domains, then no references without an explicit
178+
inventory will be resolved by intersphinx.
179+
151180

152181
Showing all links of an Intersphinx mapping file
153182
------------------------------------------------

sphinx/ext/intersphinx.py

Lines changed: 203 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,24 @@
2929
import sys
3030
import time
3131
from os import path
32-
from typing import IO, Any, Dict, List, Tuple
32+
from typing import IO, Any, Dict, List, Optional, Tuple
3333
from urllib.parse import urlsplit, urlunsplit
3434

3535
from docutils import nodes
36-
from docutils.nodes import TextElement
36+
from docutils.nodes import Element, TextElement
3737
from docutils.utils import relative_path
3838

3939
import sphinx
4040
from sphinx.addnodes import pending_xref
4141
from sphinx.application import Sphinx
4242
from sphinx.builders.html import INVENTORY_FILENAME
4343
from sphinx.config import Config
44+
from sphinx.domains import Domain
4445
from sphinx.environment import BuildEnvironment
4546
from sphinx.locale import _, __
4647
from sphinx.util import logging, requests
4748
from sphinx.util.inventory import InventoryFile
48-
from sphinx.util.typing import Inventory
49+
from sphinx.util.typing import Inventory, InventoryItem
4950

5051
logger = logging.getLogger(__name__)
5152

@@ -258,105 +259,211 @@ def load_mappings(app: Sphinx) -> None:
258259
inventories.main_inventory.setdefault(type, {}).update(objects)
259260

260261

261-
def missing_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref,
262-
contnode: TextElement) -> nodes.reference:
263-
"""Attempt to resolve a missing reference via intersphinx references."""
264-
target = node['reftarget']
265-
inventories = InventoryAdapter(env)
266-
objtypes: List[str] = None
267-
if node['reftype'] == 'any':
268-
# we search anything!
269-
objtypes = ['%s:%s' % (domain.name, objtype)
270-
for domain in env.domains.values()
271-
for objtype in domain.object_types]
272-
domain = None
262+
def _create_element_from_result(domain: Domain, inv_name: Optional[str],
263+
data: InventoryItem,
264+
node: pending_xref, contnode: TextElement) -> Element:
265+
proj, version, uri, dispname = data
266+
if '://' not in uri and node.get('refdoc'):
267+
# get correct path in case of subdirectories
268+
uri = path.join(relative_path(node['refdoc'], '.'), uri)
269+
if version:
270+
reftitle = _('(in %s v%s)') % (proj, version)
273271
else:
274-
domain = node.get('refdomain')
275-
if not domain:
272+
reftitle = _('(in %s)') % (proj,)
273+
newnode = nodes.reference('', '', internal=False, refuri=uri, reftitle=reftitle)
274+
if node.get('refexplicit'):
275+
# use whatever title was given
276+
newnode.append(contnode)
277+
elif dispname == '-' or \
278+
(domain.name == 'std' and node['reftype'] == 'keyword'):
279+
# use whatever title was given, but strip prefix
280+
title = contnode.astext()
281+
if inv_name is not None and title.startswith(inv_name + ':'):
282+
newnode.append(contnode.__class__(title[len(inv_name) + 1:],
283+
title[len(inv_name) + 1:]))
284+
else:
285+
newnode.append(contnode)
286+
else:
287+
# else use the given display name (used for :ref:)
288+
newnode.append(contnode.__class__(dispname, dispname))
289+
return newnode
290+
291+
292+
def _resolve_reference_in_domain_by_target(
293+
inv_name: Optional[str], inventory: Inventory,
294+
domain: Domain, objtypes: List[str],
295+
target: str,
296+
node: pending_xref, contnode: TextElement) -> Optional[Element]:
297+
for objtype in objtypes:
298+
if objtype not in inventory:
299+
# Continue if there's nothing of this kind in the inventory
300+
continue
301+
302+
if target in inventory[objtype]:
303+
# Case sensitive match, use it
304+
data = inventory[objtype][target]
305+
elif objtype == 'std:term':
306+
# Check for potential case insensitive matches for terms only
307+
target_lower = target.lower()
308+
insensitive_matches = list(filter(lambda k: k.lower() == target_lower,
309+
inventory[objtype].keys()))
310+
if insensitive_matches:
311+
data = inventory[objtype][insensitive_matches[0]]
312+
else:
313+
# No case insensitive match either, continue to the next candidate
314+
continue
315+
else:
316+
# Could reach here if we're not a term but have a case insensitive match.
317+
# This is a fix for terms specifically, but potentially should apply to
318+
# other types.
319+
continue
320+
return _create_element_from_result(domain, inv_name, data, node, contnode)
321+
return None
322+
323+
324+
def _resolve_reference_in_domain(env: BuildEnvironment,
325+
inv_name: Optional[str], inventory: Inventory,
326+
honor_disabled_refs: bool,
327+
domain: Domain, objtypes: List[str],
328+
node: pending_xref, contnode: TextElement
329+
) -> Optional[Element]:
330+
# we adjust the object types for backwards compatibility
331+
if domain.name == 'std' and 'cmdoption' in objtypes:
332+
# until Sphinx-1.6, cmdoptions are stored as std:option
333+
objtypes.append('option')
334+
if domain.name == 'py' and 'attribute' in objtypes:
335+
# Since Sphinx-2.1, properties are stored as py:method
336+
objtypes.append('method')
337+
338+
# the inventory contains domain:type as objtype
339+
objtypes = ["{}:{}".format(domain.name, t) for t in objtypes]
340+
341+
# now that the objtypes list is complete we can remove the disabled ones
342+
if honor_disabled_refs:
343+
disabled = env.config.intersphinx_disabled_reftypes
344+
objtypes = [o for o in objtypes if o not in disabled]
345+
346+
# without qualification
347+
res = _resolve_reference_in_domain_by_target(inv_name, inventory, domain, objtypes,
348+
node['reftarget'], node, contnode)
349+
if res is not None:
350+
return res
351+
352+
# try with qualification of the current scope instead
353+
full_qualified_name = domain.get_full_qualified_name(node)
354+
if full_qualified_name is None:
355+
return None
356+
return _resolve_reference_in_domain_by_target(inv_name, inventory, domain, objtypes,
357+
full_qualified_name, node, contnode)
358+
359+
360+
def _resolve_reference(env: BuildEnvironment, inv_name: Optional[str], inventory: Inventory,
361+
honor_disabled_refs: bool,
362+
node: pending_xref, contnode: TextElement) -> Optional[Element]:
363+
# disabling should only be done if no inventory is given
364+
honor_disabled_refs = honor_disabled_refs and inv_name is None
365+
366+
if honor_disabled_refs and '*' in env.config.intersphinx_disabled_reftypes:
367+
return None
368+
369+
typ = node['reftype']
370+
if typ == 'any':
371+
for domain_name, domain in env.domains.items():
372+
if honor_disabled_refs \
373+
and (domain_name + ":*") in env.config.intersphinx_disabled_reftypes:
374+
continue
375+
objtypes = list(domain.object_types)
376+
res = _resolve_reference_in_domain(env, inv_name, inventory,
377+
honor_disabled_refs,
378+
domain, objtypes,
379+
node, contnode)
380+
if res is not None:
381+
return res
382+
return None
383+
else:
384+
domain_name = node.get('refdomain')
385+
if not domain_name:
276386
# only objects in domains are in the inventory
277387
return None
278-
objtypes = env.get_domain(domain).objtypes_for_role(node['reftype'])
388+
if honor_disabled_refs \
389+
and (domain_name + ":*") in env.config.intersphinx_disabled_reftypes:
390+
return None
391+
domain = env.get_domain(domain_name)
392+
objtypes = domain.objtypes_for_role(typ)
279393
if not objtypes:
280394
return None
281-
objtypes = ['%s:%s' % (domain, objtype) for objtype in objtypes]
282-
if 'std:cmdoption' in objtypes:
283-
# until Sphinx-1.6, cmdoptions are stored as std:option
284-
objtypes.append('std:option')
285-
if 'py:attribute' in objtypes:
286-
# Since Sphinx-2.1, properties are stored as py:method
287-
objtypes.append('py:method')
288-
289-
to_try = [(inventories.main_inventory, target)]
290-
if domain:
291-
full_qualified_name = env.get_domain(domain).get_full_qualified_name(node)
292-
if full_qualified_name:
293-
to_try.append((inventories.main_inventory, full_qualified_name))
294-
in_set = None
295-
if ':' in target:
296-
# first part may be the foreign doc set name
297-
setname, newtarget = target.split(':', 1)
298-
if setname in inventories.named_inventory:
299-
in_set = setname
300-
to_try.append((inventories.named_inventory[setname], newtarget))
301-
if domain:
302-
node['reftarget'] = newtarget
303-
full_qualified_name = env.get_domain(domain).get_full_qualified_name(node)
304-
if full_qualified_name:
305-
to_try.append((inventories.named_inventory[setname], full_qualified_name))
306-
for inventory, target in to_try:
307-
for objtype in objtypes:
308-
if objtype not in inventory:
309-
# Continue if there's nothing of this kind in the inventory
310-
continue
311-
if target in inventory[objtype]:
312-
# Case sensitive match, use it
313-
proj, version, uri, dispname = inventory[objtype][target]
314-
elif objtype == 'std:term':
315-
# Check for potential case insensitive matches for terms only
316-
target_lower = target.lower()
317-
insensitive_matches = list(filter(lambda k: k.lower() == target_lower,
318-
inventory[objtype].keys()))
319-
if insensitive_matches:
320-
proj, version, uri, dispname = inventory[objtype][insensitive_matches[0]]
321-
else:
322-
# No case insensitive match either, continue to the next candidate
323-
continue
324-
else:
325-
# Could reach here if we're not a term but have a case insensitive match.
326-
# This is a fix for terms specifically, but potentially should apply to
327-
# other types.
328-
continue
395+
return _resolve_reference_in_domain(env, inv_name, inventory,
396+
honor_disabled_refs,
397+
domain, objtypes,
398+
node, contnode)
329399

330-
if '://' not in uri and node.get('refdoc'):
331-
# get correct path in case of subdirectories
332-
uri = path.join(relative_path(node['refdoc'], '.'), uri)
333-
if version:
334-
reftitle = _('(in %s v%s)') % (proj, version)
335-
else:
336-
reftitle = _('(in %s)') % (proj,)
337-
newnode = nodes.reference('', '', internal=False, refuri=uri, reftitle=reftitle)
338-
if node.get('refexplicit'):
339-
# use whatever title was given
340-
newnode.append(contnode)
341-
elif dispname == '-' or \
342-
(domain == 'std' and node['reftype'] == 'keyword'):
343-
# use whatever title was given, but strip prefix
344-
title = contnode.astext()
345-
if in_set and title.startswith(in_set + ':'):
346-
newnode.append(contnode.__class__(title[len(in_set) + 1:],
347-
title[len(in_set) + 1:]))
348-
else:
349-
newnode.append(contnode)
350-
else:
351-
# else use the given display name (used for :ref:)
352-
newnode.append(contnode.__class__(dispname, dispname))
353-
return newnode
354-
# at least get rid of the ':' in the target if no explicit title given
355-
if in_set is not None and not node.get('refexplicit', True):
356-
if len(contnode) and isinstance(contnode[0], nodes.Text):
357-
contnode[0] = nodes.Text(newtarget, contnode[0].rawsource)
358400

359-
return None
401+
def inventory_exists(env: BuildEnvironment, inv_name: str) -> bool:
402+
return inv_name in InventoryAdapter(env).named_inventory
403+
404+
405+
def resolve_reference_in_inventory(env: BuildEnvironment,
406+
inv_name: str,
407+
node: pending_xref, contnode: TextElement
408+
) -> Optional[Element]:
409+
"""Attempt to resolve a missing reference via intersphinx references.
410+
411+
Resolution is tried in the given inventory with the target as is.
412+
413+
Requires ``inventory_exists(env, inv_name)``.
414+
"""
415+
assert inventory_exists(env, inv_name)
416+
return _resolve_reference(env, inv_name, InventoryAdapter(env).named_inventory[inv_name],
417+
False, node, contnode)
418+
419+
420+
def resolve_reference_any_inventory(env: BuildEnvironment,
421+
honor_disabled_refs: bool,
422+
node: pending_xref, contnode: TextElement
423+
) -> Optional[Element]:
424+
"""Attempt to resolve a missing reference via intersphinx references.
425+
426+
Resolution is tried with the target as is in any inventory.
427+
"""
428+
return _resolve_reference(env, None, InventoryAdapter(env).main_inventory,
429+
honor_disabled_refs,
430+
node, contnode)
431+
432+
433+
def resolve_reference_detect_inventory(env: BuildEnvironment,
434+
node: pending_xref, contnode: TextElement
435+
) -> Optional[Element]:
436+
"""Attempt to resolve a missing reference via intersphinx references.
437+
438+
Resolution is tried first with the target as is in any inventory.
439+
If this does not succeed, then the target is split by the first ``:``,
440+
to form ``inv_name:newtarget``. If ``inv_name`` is a named inventory, then resolution
441+
is tried in that inventory with the new target.
442+
"""
443+
444+
# ordinary direct lookup, use data as is
445+
res = resolve_reference_any_inventory(env, True, node, contnode)
446+
if res is not None:
447+
return res
448+
449+
# try splitting the target into 'inv_name:target'
450+
target = node['reftarget']
451+
if ':' not in target:
452+
return None
453+
inv_name, newtarget = target.split(':', 1)
454+
if not inventory_exists(env, inv_name):
455+
return None
456+
node['reftarget'] = newtarget
457+
res_inv = resolve_reference_in_inventory(env, inv_name, node, contnode)
458+
node['reftarget'] = target
459+
return res_inv
460+
461+
462+
def missing_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref,
463+
contnode: TextElement) -> Optional[Element]:
464+
"""Attempt to resolve a missing reference via intersphinx references."""
465+
466+
return resolve_reference_detect_inventory(env, node, contnode)
360467

361468

362469
def normalize_intersphinx_mapping(app: Sphinx, config: Config) -> None:
@@ -387,6 +494,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
387494
app.add_config_value('intersphinx_mapping', {}, True)
388495
app.add_config_value('intersphinx_cache_limit', 5, False)
389496
app.add_config_value('intersphinx_timeout', None, False)
497+
app.add_config_value('intersphinx_disabled_reftypes', [], True)
390498
app.connect('config-inited', normalize_intersphinx_mapping, priority=800)
391499
app.connect('builder-inited', load_mappings)
392500
app.connect('missing-reference', missing_reference)

0 commit comments

Comments
 (0)