From 2287e4a1b591c6740903470e31d3f2656121e959 Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Fri, 16 Jul 2021 14:34:35 +0200 Subject: [PATCH] Add intersphinx_disabled_domains Fixes sphinx-doc/sphinx#2068 Replaces sphinx-doc/sphinx#8981 --- CHANGES | 5 ++ doc/usage/extensions/intersphinx.rst | 19 ++++++ sphinx/ext/intersphinx.py | 39 +++++++---- sphinx/templates/quickstart/conf.py_t | 4 ++ tests/test_ext_intersphinx.py | 93 ++++++++++++++++++++------- 5 files changed, 127 insertions(+), 33 deletions(-) diff --git a/CHANGES b/CHANGES index e218c1f70b6..4604463f991 100644 --- a/CHANGES +++ b/CHANGES @@ -22,6 +22,11 @@ Features added * #9691: C, added new info-field ``retval`` for :rst:dir:`c:function` and :rst:dir:`c:macro`. * C++, added new info-field ``retval`` for :rst:dir:`cpp:function`. +* #2068, add :confval:`intersphinx_disabled_domains` for disabling + interphinx resolution of cross-references in specific domains when they + do not have an explicit inventory specification. + ``sphinx-quickstart`` will insert + ``intersphinx_disabled_domains = ['std']`` in its generated ``conf.py``. Bugs fixed ---------- diff --git a/doc/usage/extensions/intersphinx.rst b/doc/usage/extensions/intersphinx.rst index 178655caeba..696a149e953 100644 --- a/doc/usage/extensions/intersphinx.rst +++ b/doc/usage/extensions/intersphinx.rst @@ -148,6 +148,25 @@ linking: exception is raised if the server has not issued a response for timeout seconds. +.. confval:: intersphinx_disabled_domains + + .. versionadded:: 4.2 + + A list of strings being the name of a domain, or the special name ``all``. + When a cross-reference without an explicit inventory specification is being + resolve by intersphinx, skip resolution if either the domain of the + cross-reference is in this list or the special name ``all`` is in the list. + + For example, with ``intersphinx_disabled_domains = ['std']`` a cross-reference + ``:doc:`installation``` will not be attempted to be resolved by intersphinx, but + ``:doc:`otherbook:installation``` will be attempted to be resolved in the + inventory named ``otherbook`` in :confval:`intersphinx_mapping`. + At the same time, all cross-references generated in, e.g., Python, declarations + will still be attempted to be resolved by intersphinx. + + If ``all`` is in the list of domains, then no references without an explicit + inventory will be resolved by intersphinx. + Showing all links of an Intersphinx mapping file ------------------------------------------------ diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index 848a12eb45c..7764390eebd 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -351,11 +351,20 @@ def _resolve_reference_in_domain(inv_name: Optional[str], inventory: Inventory, def _resolve_reference(env: BuildEnvironment, inv_name: Optional[str], inventory: Inventory, + honor_disabled_domains: bool, node: pending_xref, contnode: TextElement) -> Optional[Element]: - # figure out which object types we should look for + # disabling should only be done if no inventory is given + honor_disabled_domains = honor_disabled_domains and inv_name is None + + if honor_disabled_domains and 'all' in env.config.intersphinx_disabled_domains: + return None + typ = node['reftype'] if typ == 'any': for domain_name, domain in env.domains.items(): + if honor_disabled_domains \ + and domain_name in env.config.intersphinx_disabled_domains: + continue objtypes = list(domain.object_types) res = _resolve_reference_in_domain(inv_name, inventory, domain, objtypes, @@ -368,6 +377,9 @@ def _resolve_reference(env: BuildEnvironment, inv_name: Optional[str], inventory if not domain_name: # only objects in domains are in the inventory return None + if honor_disabled_domains \ + and domain_name in env.config.intersphinx_disabled_domains: + return None domain = env.get_domain(domain_name) objtypes = domain.objtypes_for_role(typ) if not objtypes: @@ -383,8 +395,8 @@ def inventory_exists(env: BuildEnvironment, inv_name: str) -> bool: def resolve_reference_in_inventory(env: BuildEnvironment, inv_name: str, - node: pending_xref, - contnode: TextElement) -> Optional[Element]: + node: pending_xref, contnode: TextElement + ) -> Optional[Element]: """Attempt to resolve a missing reference via intersphinx references. Resolution is tried in the given inventory with the target as is. @@ -393,22 +405,26 @@ def resolve_reference_in_inventory(env: BuildEnvironment, """ assert inventory_exists(env, inv_name) return _resolve_reference(env, inv_name, InventoryAdapter(env).named_inventory[inv_name], - node, contnode) + False, node, contnode) def resolve_reference_any_inventory(env: BuildEnvironment, - node: pending_xref, - contnode: TextElement) -> Optional[Element]: + honor_disabled_domains: bool, + node: pending_xref, contnode: TextElement + ) -> Optional[Element]: """Attempt to resolve a missing reference via intersphinx references. Resolution is tried with the target as is in any inventory. """ - return _resolve_reference(env, None, InventoryAdapter(env).main_inventory, node, contnode) + return _resolve_reference(env, None, InventoryAdapter(env).main_inventory, + honor_disabled_domains, + node, contnode) def resolve_reference_detect_inventory(env: BuildEnvironment, - node: pending_xref, - contnode: TextElement) -> Optional[Element]: + honor_disabled_domains: bool, + node: pending_xref, contnode: TextElement + ) -> Optional[Element]: """Attempt to resolve a missing reference via intersphinx references. Resolution is tried first with the target as is in any inventory. @@ -418,7 +434,7 @@ def resolve_reference_detect_inventory(env: BuildEnvironment, """ # ordinary direct lookup, use data as is - res = resolve_reference_any_inventory(env, node, contnode) + res = resolve_reference_any_inventory(env, honor_disabled_domains, node, contnode) if res is not None: return res @@ -439,7 +455,7 @@ def missing_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref, contnode: TextElement) -> Optional[Element]: """Attempt to resolve a missing reference via intersphinx references.""" - return resolve_reference_detect_inventory(env, node, contnode) + return resolve_reference_detect_inventory(env, True, node, contnode) def normalize_intersphinx_mapping(app: Sphinx, config: Config) -> None: @@ -470,6 +486,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('intersphinx_mapping', {}, True) app.add_config_value('intersphinx_cache_limit', 5, False) app.add_config_value('intersphinx_timeout', None, False) + app.add_config_value('intersphinx_disabled_domains', [], True) app.connect('config-inited', normalize_intersphinx_mapping, priority=800) app.connect('builder-inited', load_mappings) app.connect('missing-reference', missing_reference) diff --git a/sphinx/templates/quickstart/conf.py_t b/sphinx/templates/quickstart/conf.py_t index f1da41c4a64..4e37f31308c 100644 --- a/sphinx/templates/quickstart/conf.py_t +++ b/sphinx/templates/quickstart/conf.py_t @@ -108,6 +108,10 @@ html_static_path = ['{{ dot }}static'] intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), } + +# Prevent accidental intersphinx resolution for labels, documents, and other +# basic cross-references. +intersphinx_disabled_domains = ['std'] {%- endif %} {%- if 'sphinx.ext.todo' in extensions %} diff --git a/tests/test_ext_intersphinx.py b/tests/test_ext_intersphinx.py index 2f18748ddc7..6856c6e6253 100644 --- a/tests/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx.py @@ -42,6 +42,12 @@ def reference_check(app, *args, **kwds): return missing_reference(app, app.env, node, contnode) +def set_config(app, mapping): + app.config.intersphinx_mapping = mapping + app.config.intersphinx_cache_limit = 0 + app.config.intersphinx_disabled_domains = [] + + @mock.patch('sphinx.ext.intersphinx.InventoryFile') @mock.patch('sphinx.ext.intersphinx._read_from_url') def test_fetch_inventory_redirection(_read_from_url, InventoryFile, app, status, warning): @@ -90,13 +96,12 @@ def test_fetch_inventory_redirection(_read_from_url, InventoryFile, app, status, def test_missing_reference(tempdir, app, status, warning): inv_file = tempdir / 'inventory' inv_file.write_bytes(inventory_v2) - app.config.intersphinx_mapping = { + set_config(app, { 'https://docs.python.org/': inv_file, 'py3k': ('https://docs.python.org/py3k/', inv_file), 'py3krel': ('py3k', inv_file), # relative path 'py3krelparent': ('../../py3k', inv_file), # relative path, parent dir - } - app.config.intersphinx_cache_limit = 0 + }) # load the inventory and check if it's done correctly normalize_intersphinx_mapping(app, app.config) @@ -169,10 +174,9 @@ def test_missing_reference(tempdir, app, status, warning): def test_missing_reference_pydomain(tempdir, app, status, warning): inv_file = tempdir / 'inventory' inv_file.write_bytes(inventory_v2) - app.config.intersphinx_mapping = { + set_config(app, { 'https://docs.python.org/': inv_file, - } - app.config.intersphinx_cache_limit = 0 + }) # load the inventory and check if it's done correctly normalize_intersphinx_mapping(app, app.config) @@ -210,10 +214,9 @@ def test_missing_reference_pydomain(tempdir, app, status, warning): def test_missing_reference_stddomain(tempdir, app, status, warning): inv_file = tempdir / 'inventory' inv_file.write_bytes(inventory_v2) - app.config.intersphinx_mapping = { + set_config(app, { 'cmd': ('https://docs.python.org/', inv_file), - } - app.config.intersphinx_cache_limit = 0 + }) # load the inventory and check if it's done correctly normalize_intersphinx_mapping(app, app.config) @@ -242,10 +245,9 @@ def test_missing_reference_stddomain(tempdir, app, status, warning): def test_missing_reference_cppdomain(tempdir, app, status, warning): inv_file = tempdir / 'inventory' inv_file.write_bytes(inventory_v2) - app.config.intersphinx_mapping = { + set_config(app, { 'https://docs.python.org/': inv_file, - } - app.config.intersphinx_cache_limit = 0 + }) # load the inventory and check if it's done correctly normalize_intersphinx_mapping(app, app.config) @@ -269,10 +271,9 @@ def test_missing_reference_cppdomain(tempdir, app, status, warning): def test_missing_reference_jsdomain(tempdir, app, status, warning): inv_file = tempdir / 'inventory' inv_file.write_bytes(inventory_v2) - app.config.intersphinx_mapping = { + set_config(app, { 'https://docs.python.org/': inv_file, - } - app.config.intersphinx_cache_limit = 0 + }) # load the inventory and check if it's done correctly normalize_intersphinx_mapping(app, app.config) @@ -291,14 +292,63 @@ def test_missing_reference_jsdomain(tempdir, app, status, warning): assert rn.astext() == 'baz()' +def test_missing_reference_disabled_domain(tempdir, app, status, warning): + inv_file = tempdir / 'inventory' + inv_file.write_bytes(inventory_v2) + set_config(app, { + 'inv': ('https://docs.python.org/', inv_file), + }) + + # load the inventory and check if it's done correctly + normalize_intersphinx_mapping(app, app.config) + load_mappings(app) + + def case(std_without, std_with, py_without, py_with): + def assert_(rn, expected): + if expected is None: + assert rn is None + else: + assert rn.astext() == expected + + kwargs = {} + + node, contnode = fake_node('std', 'doc', 'docname', 'docname', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert_(rn, std_without) + + node, contnode = fake_node('std', 'doc', 'inv:docname', 'docname', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert_(rn, std_with) + + # an arbitrary ref in another domain + node, contnode = fake_node('py', 'func', 'module1.func', 'func()', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert_(rn, py_without) + + node, contnode = fake_node('py', 'func', 'inv:module1.func', 'func()', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert_(rn, py_with) + + # the base case, everything should resolve + assert app.config.intersphinx_disabled_domains == [] + case('docname', 'docname', 'func()', 'func()') + + # disabled one domain + app.config.intersphinx_disabled_domains = ['std'] + case(None, 'docname', 'func()', 'func()') + + # disabled all domains + app.config.intersphinx_disabled_domains = ['all'] + case(None, 'docname', None, 'func()') + + @pytest.mark.xfail(os.name != 'posix', reason="Path separator mismatch issue") def test_inventory_not_having_version(tempdir, app, status, warning): inv_file = tempdir / 'inventory' inv_file.write_bytes(inventory_v2_not_having_version) - app.config.intersphinx_mapping = { + set_config(app, { 'https://docs.python.org/': inv_file, - } - app.config.intersphinx_cache_limit = 0 + }) # load the inventory and check if it's done correctly normalize_intersphinx_mapping(app, app.config) @@ -318,16 +368,15 @@ def test_load_mappings_warnings(tempdir, app, status, warning): """ inv_file = tempdir / 'inventory' inv_file.write_bytes(inventory_v2) - app.config.intersphinx_mapping = { + set_config(app, { 'https://docs.python.org/': inv_file, 'py3k': ('https://docs.python.org/py3k/', inv_file), 'repoze.workflow': ('http://docs.repoze.org/workflow/', inv_file), 'django-taggit': ('http://django-taggit.readthedocs.org/en/latest/', inv_file), 12345: ('http://www.sphinx-doc.org/en/stable/', inv_file), - } + }) - app.config.intersphinx_cache_limit = 0 # load the inventory and check if it's done correctly normalize_intersphinx_mapping(app, app.config) load_mappings(app) @@ -337,7 +386,7 @@ def test_load_mappings_warnings(tempdir, app, status, warning): def test_load_mappings_fallback(tempdir, app, status, warning): inv_file = tempdir / 'inventory' inv_file.write_bytes(inventory_v2) - app.config.intersphinx_cache_limit = 0 + set_config(app, {}) # connect to invalid path app.config.intersphinx_mapping = {