From b4227dbe1b73f9c5745f240657ecb13c5e41d233 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Mon, 5 Apr 2021 00:33:34 +0900 Subject: [PATCH 01/18] refactor: Add CustomReSTDispatcher as a base class of custom dispatchers To create custom reST dispatcher easily, this adds CustomReSTDispatcher class as a base class of custom dispatchers. --- sphinx/util/docutils.py | 51 ++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index c3a6ff9e2e..5ab7666496 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -166,16 +166,14 @@ def patch_docutils(confdir: Optional[str] = None) -> Generator[None, None, None] yield -class ElementLookupError(Exception): - pass - +class CustomReSTDispatcher: + """Custom reST's mark-up dispatcher. -class sphinx_domains: - """Monkey-patch directive and role dispatch, so that domain-specific - markup takes precedence. + This replaces docutils's directives and roles dispatch mechanism for reST parser + by original one temporarily. """ - def __init__(self, env: "BuildEnvironment") -> None: - self.env = env + + def __init__(self) -> None: self.directive_func: Callable = lambda *args: (None, []) self.roles_func: Callable = lambda *args: (None, []) @@ -189,13 +187,35 @@ def enable(self) -> None: self.directive_func = directives.directive self.role_func = roles.role - directives.directive = self.lookup_directive - roles.role = self.lookup_role + directives.directive = self.directive + roles.role = self.role def disable(self) -> None: directives.directive = self.directive_func roles.role = self.role_func + def directive(self, + directive_name: str, language_module: ModuleType, document: nodes.document + ) -> Tuple[Optional[Type[Directive]], List[system_message]]: + return self.directive_func(directive_name, language_module, document) + + def role(self, role_name: str, language_module: ModuleType, lineno: int, reporter: Reporter + ) -> Tuple[RoleFunction, List[system_message]]: + return self.role_func(role_name, language_module, lineno, reporter) + + +class ElementLookupError(Exception): + pass + + +class sphinx_domains(CustomReSTDispatcher): + """Monkey-patch directive and role dispatch, so that domain-specific + markup takes precedence. + """ + def __init__(self, env: "BuildEnvironment") -> None: + self.env = env + super().__init__() + def lookup_domain_element(self, type: str, name: str) -> Any: """Lookup a markup element (directive or role), given its name which can be a full name (with domain). @@ -226,17 +246,20 @@ def lookup_domain_element(self, type: str, name: str) -> Any: raise ElementLookupError - def lookup_directive(self, directive_name: str, language_module: ModuleType, document: nodes.document) -> Tuple[Optional[Type[Directive]], List[system_message]]: # NOQA + def directive(self, + directive_name: str, language_module: ModuleType, document: nodes.document + ) -> Tuple[Optional[Type[Directive]], List[system_message]]: try: return self.lookup_domain_element('directive', directive_name) except ElementLookupError: - return self.directive_func(directive_name, language_module, document) + return super().directive(directive_name, language_module, document) - def lookup_role(self, role_name: str, language_module: ModuleType, lineno: int, reporter: Reporter) -> Tuple[RoleFunction, List[system_message]]: # NOQA + def role(self, role_name: str, language_module: ModuleType, lineno: int, reporter: Reporter + ) -> Tuple[RoleFunction, List[system_message]]: try: return self.lookup_domain_element('role', role_name) except ElementLookupError: - return self.role_func(role_name, language_module, lineno, reporter) + return super().role(role_name, language_module, lineno, reporter) class WarningStream: From 35dcc60a7c0ac9084512e56d1c9b6bcc15428952 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Mon, 5 Apr 2021 01:34:45 +0900 Subject: [PATCH 02/18] intersphinx: Add :intersphinx:***: role --- sphinx/ext/intersphinx.py | 123 +++++++++++++++++- tests/roots/test-ext-intersphinx-role/conf.py | 1 + .../roots/test-ext-intersphinx-role/index.rst | 11 ++ tests/test_ext_intersphinx.py | 46 +++++++ 4 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 tests/roots/test-ext-intersphinx-role/conf.py create mode 100644 tests/roots/test-ext-intersphinx-role/index.rst diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index 7f3588ade7..146e0c246e 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -29,12 +29,13 @@ import sys import time from os import path -from typing import IO, Any, Dict, List, Optional, Tuple +from types import ModuleType +from typing import IO, Any, Dict, List, Optional, Tuple, cast from urllib.parse import urlsplit, urlunsplit from docutils import nodes -from docutils.nodes import Element, TextElement -from docutils.utils import relative_path +from docutils.nodes import Element, Node, TextElement, system_message +from docutils.utils import Reporter, relative_path import sphinx from sphinx.addnodes import pending_xref @@ -43,10 +44,13 @@ from sphinx.config import Config from sphinx.domains import Domain from sphinx.environment import BuildEnvironment +from sphinx.errors import ExtensionError from sphinx.locale import _, __ +from sphinx.transforms.post_transforms import ReferencesResolver from sphinx.util import logging, requests +from sphinx.util.docutils import CustomReSTDispatcher, SphinxRole from sphinx.util.inventory import InventoryFile -from sphinx.util.typing import Inventory, InventoryItem +from sphinx.util.typing import Inventory, InventoryItem, RoleFunction logger = logging.getLogger(__name__) @@ -466,6 +470,115 @@ def missing_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref, return resolve_reference_detect_inventory(env, node, contnode) +class IntersphinxDispatcher(CustomReSTDispatcher): + """Custom dispatcher for intersphinx role. + + This enables :intersphinx:***: roles on parsing reST document. + """ + + def __init__(self) -> None: + super().__init__() + + def role(self, role_name: str, language_module: ModuleType, lineno: int, reporter: Reporter + ) -> Tuple[RoleFunction, List[system_message]]: + if role_name.split(':')[0] == 'intersphinx': + return IntersphinxRole(), [] + else: + return super().role(role_name, language_module, lineno, reporter) + + +class IntersphinxRole(SphinxRole): + def run(self) -> Tuple[List[Node], List[system_message]]: + role_name = self.get_role_name(self.name) + if role_name is None: + logger.warning(__('role not found: %s'), self.name, + location=(self.env.docname, self.lineno)) + return [], [] + + result, messages = self.invoke_role(role_name) + for node in result: + if isinstance(node, pending_xref): + node['intersphinx'] = True + + return result, messages + + def get_role_name(self, name: str) -> Optional[Tuple[str, str]]: + names = name.split(':') + if len(names) == 2: + # :intersphinx:role: + domain = self.env.temp_data.get('default_domain') + role = names[1] + elif len(names) == 3: + # :intersphinx:domain:role: + domain = names[1] + role = names[2] + else: + return None + + if domain and self.is_existent_role(domain, role): + return (domain, role) + elif self.is_existent_role('std', role): + return ('std', role) + else: + return None + + def is_existent_role(self, domain_name: str, role_name: str) -> bool: + try: + domain = self.env.get_domain(domain_name) + if role_name in domain.roles: + return True + else: + return False + except ExtensionError: + return False + + def invoke_role(self, role: Tuple[str, str]) -> Tuple[List[Node], List[system_message]]: + domain = self.env.get_domain(role[0]) + if domain: + role_func = domain.role(role[1]) + + 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: + for node in self.document.traverse(pending_xref): + if 'intersphinx' in node: + contnode = cast(nodes.TextElement, node[0].deepcopy()) + refdoc = node.get('refdoc', self.env.docname) + try: + domain = self.env.get_domain(node['refdomain']) + except Exception: + domain = None + + newnode = missing_reference(self.app, self.env, node, contnode) + if newnode is None: + self.warn_missing_reference(refdoc, node['reftype'], node['reftarget'], + node, domain) + else: + node.replace_self(newnode) + + +def install_dispatcher(app: Sphinx, docname: str, source: List[str]) -> None: + """Enable IntersphinxDispatcher. + + .. note:: The installed dispatcher will uninstalled on disabling sphinx_domain + automatically. + """ + dispatcher = IntersphinxDispatcher() + dispatcher.enable() + + def normalize_intersphinx_mapping(app: Sphinx, config: Config) -> None: for key, value in config.intersphinx_mapping.copy().items(): try: @@ -497,7 +610,9 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('intersphinx_disabled_reftypes', [], True) app.connect('config-inited', normalize_intersphinx_mapping, priority=800) app.connect('builder-inited', load_mappings) + app.connect('source-read', install_dispatcher) app.connect('missing-reference', missing_reference) + app.add_post_transform(IntersphinxRoleResolver) return { 'version': sphinx.__display_version__, 'env_version': 1, diff --git a/tests/roots/test-ext-intersphinx-role/conf.py b/tests/roots/test-ext-intersphinx-role/conf.py new file mode 100644 index 0000000000..9485eb2075 --- /dev/null +++ b/tests/roots/test-ext-intersphinx-role/conf.py @@ -0,0 +1 @@ +extensions = ['sphinx.ext.intersphinx'] diff --git a/tests/roots/test-ext-intersphinx-role/index.rst b/tests/roots/test-ext-intersphinx-role/index.rst new file mode 100644 index 0000000000..b8a7d87160 --- /dev/null +++ b/tests/roots/test-ext-intersphinx-role/index.rst @@ -0,0 +1,11 @@ +:intersphinx:py:mod:`module1` +:intersphinx:py:mod:`inv:module2` + +.. py:module:: module1 + +:intersphinx:py:func:`func` +:intersphinx:py:meth:`Foo.bar` + +:intersphinx:c:func:`CFunc` +:intersphinx:doc:`docname` +:intersphinx:option:`ls -l` diff --git a/tests/test_ext_intersphinx.py b/tests/test_ext_intersphinx.py index 7f369e9a3c..9801bb81f1 100644 --- a/tests/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx.py @@ -524,3 +524,49 @@ def log_message(*args, **kwargs): stdout, stderr = capsys.readouterr() assert stdout.startswith("c:function\n") assert stderr == "" + + +@pytest.mark.sphinx('html', testroot='ext-intersphinx-role') +def test_intersphinx_role(app): + inv_file = app.srcdir / 'inventory' + inv_file.write_bytes(inventory_v2) + app.config.intersphinx_mapping = { + 'inv': ('http://example.org/', inv_file), + } + app.config.intersphinx_cache_limit = 0 + app.config.nitpicky = True + + # load the inventory and check if it's done correctly + normalize_intersphinx_mapping(app, app.config) + load_mappings(app) + + app.build() + content = (app.outdir / 'index.html').read_text() + + # :intersphinx:py:module:`module1` + assert ('' in content) + + # :intersphinx:py:module:`inv:module2` + assert ('' in content) + + # py:module + :intersphinx:py:function:`func` + assert ('' in content) + + # py:module + :intersphinx:py:method:`Foo.bar` + assert ('' in content) + + # :intersphinx:c:function:`CFunc` + assert ('' in content) + + # :intersphinx:doc:`docname` + assert ('' in content) + + # :intersphinx:option:`ls -l` + assert ('' in content) From 8605644e833be2c397517475d15a26588bcc6cb7 Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Tue, 13 Jul 2021 12:39:26 +0200 Subject: [PATCH 03/18] intersphinx: more role testing --- .../roots/test-ext-intersphinx-role/index.rst | 14 +++++++ tests/test_ext_intersphinx.py | 42 +++++++------------ 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/tests/roots/test-ext-intersphinx-role/index.rst b/tests/roots/test-ext-intersphinx-role/index.rst index b8a7d87160..25352838d7 100644 --- a/tests/roots/test-ext-intersphinx-role/index.rst +++ b/tests/roots/test-ext-intersphinx-role/index.rst @@ -4,8 +4,22 @@ .. py:module:: module1 :intersphinx:py:func:`func` +:intersphinx:py:func:`inv:func` :intersphinx:py:meth:`Foo.bar` +:intersphinx:py:meth:`inv:Foo.bar` :intersphinx:c:func:`CFunc` +:intersphinx:c:func:`inv:CFunc` :intersphinx:doc:`docname` +:intersphinx:doc:`inv:docname` :intersphinx:option:`ls -l` +:intersphinx:option:`inv:ls -l` + +.. cpp:type:: std::uint8_t +.. cpp:class:: foo::Bar + +:intersphinx:cpp:type:`std::uint8_t` +:intersphinx:cpp:class:`inv:foo::Bar` + +:intersphinx:cpp:type:`FoonsTitle ` +:intersphinx:cpp:type:`inv:BarType ` \ No newline at end of file diff --git a/tests/test_ext_intersphinx.py b/tests/test_ext_intersphinx.py index 9801bb81f1..a2ff8d7acd 100644 --- a/tests/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx.py @@ -543,30 +543,18 @@ def test_intersphinx_role(app): app.build() content = (app.outdir / 'index.html').read_text() - # :intersphinx:py:module:`module1` - assert ('' in content) - - # :intersphinx:py:module:`inv:module2` - assert ('' in content) - - # py:module + :intersphinx:py:function:`func` - assert ('' in content) - - # py:module + :intersphinx:py:method:`Foo.bar` - assert ('' in content) - - # :intersphinx:c:function:`CFunc` - assert ('' in content) - - # :intersphinx:doc:`docname` - assert ('' in content) - - # :intersphinx:option:`ls -l` - assert ('' in content) + targets = ( + 'foo.html#module-module1', + 'foo.html#module-module2', + 'sub/foo.html#module1.func', + 'index.html#foo.Bar.baz', + 'cfunc.html#CFunc', + 'docname.html', + 'index.html#cmdoption-ls-l', + 'index.html#std_uint8_t', + 'index.html#foons', + 'index.html#foons_bartype', + ) + html = '' + for t in targets: + assert html.format(t) in content From 69fa5260fcb243510b513a1cbf0fd9a32cf46d5a Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Tue, 13 Jul 2021 12:48:17 +0200 Subject: [PATCH 04/18] intersphinx role: rename to 'external' --- sphinx/ext/intersphinx.py | 10 +++--- .../roots/test-ext-intersphinx-role/index.rst | 32 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index 146e0c246e..0ad640800c 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -471,9 +471,9 @@ def missing_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref, class IntersphinxDispatcher(CustomReSTDispatcher): - """Custom dispatcher for intersphinx role. + """Custom dispatcher for external role. - This enables :intersphinx:***: roles on parsing reST document. + This enables :external:***: roles on parsing reST document. """ def __init__(self) -> None: @@ -481,7 +481,7 @@ def __init__(self) -> None: def role(self, role_name: str, language_module: ModuleType, lineno: int, reporter: Reporter ) -> Tuple[RoleFunction, List[system_message]]: - if role_name.split(':')[0] == 'intersphinx': + if role_name.split(':')[0] == 'external': return IntersphinxRole(), [] else: return super().role(role_name, language_module, lineno, reporter) @@ -505,11 +505,11 @@ def run(self) -> Tuple[List[Node], List[system_message]]: def get_role_name(self, name: str) -> Optional[Tuple[str, str]]: names = name.split(':') if len(names) == 2: - # :intersphinx:role: + # :external:role: domain = self.env.temp_data.get('default_domain') role = names[1] elif len(names) == 3: - # :intersphinx:domain:role: + # :external:domain:role: domain = names[1] role = names[2] else: diff --git a/tests/roots/test-ext-intersphinx-role/index.rst b/tests/roots/test-ext-intersphinx-role/index.rst index 25352838d7..4eeaf807a3 100644 --- a/tests/roots/test-ext-intersphinx-role/index.rst +++ b/tests/roots/test-ext-intersphinx-role/index.rst @@ -1,25 +1,25 @@ -:intersphinx:py:mod:`module1` -:intersphinx:py:mod:`inv:module2` +:external:py:mod:`module1` +:external:py:mod:`inv:module2` .. py:module:: module1 -:intersphinx:py:func:`func` -:intersphinx:py:func:`inv:func` -:intersphinx:py:meth:`Foo.bar` -:intersphinx:py:meth:`inv:Foo.bar` +:external:py:func:`func` +:external:py:func:`inv:func` +:external:py:meth:`Foo.bar` +:external:py:meth:`inv:Foo.bar` -:intersphinx:c:func:`CFunc` -:intersphinx:c:func:`inv:CFunc` -:intersphinx:doc:`docname` -:intersphinx:doc:`inv:docname` -:intersphinx:option:`ls -l` -:intersphinx:option:`inv:ls -l` +:external:c:func:`CFunc` +:external:c:func:`inv:CFunc` +:external:doc:`docname` +:external:doc:`inv:docname` +:external:option:`ls -l` +:external:option:`inv:ls -l` .. cpp:type:: std::uint8_t .. cpp:class:: foo::Bar -:intersphinx:cpp:type:`std::uint8_t` -:intersphinx:cpp:class:`inv:foo::Bar` +:external:cpp:type:`std::uint8_t` +:external:cpp:class:`inv:foo::Bar` -:intersphinx:cpp:type:`FoonsTitle ` -:intersphinx:cpp:type:`inv:BarType ` \ No newline at end of file +:external:cpp:type:`FoonsTitle ` +:external:cpp:type:`inv:BarType ` \ No newline at end of file From 454a1c599e9faf58bcb25e28d6943b4645b48d29 Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Tue, 13 Jul 2021 13:18:16 +0200 Subject: [PATCH 05/18] intersphinx: remove dual warning from role resolution --- sphinx/ext/intersphinx.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index 0ad640800c..3cfbdf1dd5 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -555,16 +555,10 @@ def run(self, **kwargs: Any) -> None: for node in self.document.traverse(pending_xref): if 'intersphinx' in node: contnode = cast(nodes.TextElement, node[0].deepcopy()) - refdoc = node.get('refdoc', self.env.docname) - try: - domain = self.env.get_domain(node['refdomain']) - except Exception: - domain = None - newnode = missing_reference(self.app, self.env, node, contnode) if newnode is None: - self.warn_missing_reference(refdoc, node['reftype'], node['reftarget'], - node, domain) + # no warning, the normal missing_reference handler will do that + pass else: node.replace_self(newnode) From 36c6eafdf17edeaec3c902d15cc757edb3937d5d Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Tue, 13 Jul 2021 13:41:41 +0200 Subject: [PATCH 06/18] intersphinx: explicitly parse out inventory specification --- sphinx/ext/intersphinx.py | 13 +++++++++++++ tests/roots/test-ext-intersphinx-role/index.rst | 1 + tests/test_ext_intersphinx.py | 2 ++ 3 files changed, 16 insertions(+) diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index 3cfbdf1dd5..d004f867fb 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -495,10 +495,20 @@ def run(self) -> Tuple[List[Node], List[system_message]]: location=(self.env.docname, self.lineno)) return [], [] + # extract inventory specification + inventory = None + if self.text.startswith('\\:'): + # escaped :, so not a real inventory specification + self.text = self.text[1:] + elif self.text[0] == ':': # format: :inv:normalRoleArg + inventory = self.text.split(':')[1] + self.text = self.text[(len(inventory) + 2):] + result, messages = self.invoke_role(role_name) for node in result: if isinstance(node, pending_xref): node['intersphinx'] = True + node['inventory'] = inventory return result, messages @@ -555,6 +565,9 @@ def run(self, **kwargs: Any) -> None: for node in self.document.traverse(pending_xref): if 'intersphinx' in node: contnode = cast(nodes.TextElement, node[0].deepcopy()) + # temporary hax to glue on inventory info again + if node['inventory'] is not None: + node['reftarget'] = node['inventory'] + ":" + node['reftarget'] newnode = missing_reference(self.app, self.env, node, contnode) if newnode is None: # no warning, the normal missing_reference handler will do that diff --git a/tests/roots/test-ext-intersphinx-role/index.rst b/tests/roots/test-ext-intersphinx-role/index.rst index 4eeaf807a3..164f2cfd4c 100644 --- a/tests/roots/test-ext-intersphinx-role/index.rst +++ b/tests/roots/test-ext-intersphinx-role/index.rst @@ -20,6 +20,7 @@ :external:cpp:type:`std::uint8_t` :external:cpp:class:`inv:foo::Bar` +:external:cpp:func:`:inv2:foo::Bar::baz` :external:cpp:type:`FoonsTitle ` :external:cpp:type:`inv:BarType ` \ No newline at end of file diff --git a/tests/test_ext_intersphinx.py b/tests/test_ext_intersphinx.py index a2ff8d7acd..a1118e48da 100644 --- a/tests/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx.py @@ -558,3 +558,5 @@ def test_intersphinx_role(app): html = '' for t in targets: assert html.format(t) in content + + assert html.format('index.html#cpp_foo_bar_baz') not in content From 184fd6a4aa7389d784e20f399c0eea69a3c7dc0b Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Sun, 31 Oct 2021 15:30:13 +0100 Subject: [PATCH 07/18] intersphinx role, update based on merged refactoring --- sphinx/ext/intersphinx.py | 26 +++++++++++-------- tests/roots/test-ext-intersphinx-role/conf.py | 2 ++ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index d004f867fb..c3cf0eec7f 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -563,17 +563,21 @@ class IntersphinxRoleResolver(ReferencesResolver): def run(self, **kwargs: Any) -> None: for node in self.document.traverse(pending_xref): - if 'intersphinx' in node: - contnode = cast(nodes.TextElement, node[0].deepcopy()) - # temporary hax to glue on inventory info again - if node['inventory'] is not None: - node['reftarget'] = node['inventory'] + ":" + node['reftarget'] - newnode = missing_reference(self.app, self.env, node, contnode) - if newnode is None: - # no warning, the normal missing_reference handler will do that - pass - else: - node.replace_self(newnode) + if 'intersphinx' not in node: + continue + contnode = cast(nodes.TextElement, node[0].deepcopy()) + inv_name = node['inventory'] + if inv_name is not None: + if not inventory_exists(self.env, inv_name): + continue + 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: + # no warning, the normal missing_reference handler will do that + pass + else: + node.replace_self(newnode) def install_dispatcher(app: Sphinx, docname: str, source: List[str]) -> None: diff --git a/tests/roots/test-ext-intersphinx-role/conf.py b/tests/roots/test-ext-intersphinx-role/conf.py index 9485eb2075..a54f5c2ad6 100644 --- a/tests/roots/test-ext-intersphinx-role/conf.py +++ b/tests/roots/test-ext-intersphinx-role/conf.py @@ -1 +1,3 @@ extensions = ['sphinx.ext.intersphinx'] +# the role should not honor this conf var +intersphinx_disabled_reftypes = ['*'] From ce5512e3afc5d4a4af084aef8f622b01ffee5c2b Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Sat, 6 Nov 2021 13:25:49 +0100 Subject: [PATCH 08/18] intersphinx role, implement warning scheme and temporarily remove explicit inventories --- sphinx/ext/intersphinx.py | 18 ++++------ .../roots/test-ext-intersphinx-role/index.rst | 35 ++++++++----------- tests/test_ext_intersphinx.py | 24 +++++-------- 3 files changed, 29 insertions(+), 48 deletions(-) diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index c3cf0eec7f..ce10f12288 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -495,20 +495,11 @@ def run(self) -> Tuple[List[Node], List[system_message]]: location=(self.env.docname, self.lineno)) return [], [] - # extract inventory specification - inventory = None - if self.text.startswith('\\:'): - # escaped :, so not a real inventory specification - self.text = self.text[1:] - elif self.text[0] == ':': # format: :inv:normalRoleArg - inventory = self.text.split(':')[1] - self.text = self.text[(len(inventory) + 2):] - result, messages = self.invoke_role(role_name) for node in result: if isinstance(node, pending_xref): node['intersphinx'] = True - node['inventory'] = inventory + node['inventory'] = None return result, messages @@ -574,8 +565,11 @@ def run(self, **kwargs: Any) -> None: else: newnode = resolve_reference_any_inventory(self.env, False, node, contnode) if newnode is None: - # no warning, the normal missing_reference handler will do that - pass + 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) else: node.replace_self(newnode) diff --git a/tests/roots/test-ext-intersphinx-role/index.rst b/tests/roots/test-ext-intersphinx-role/index.rst index 164f2cfd4c..bb7a6c4f19 100644 --- a/tests/roots/test-ext-intersphinx-role/index.rst +++ b/tests/roots/test-ext-intersphinx-role/index.rst @@ -1,26 +1,21 @@ -:external:py:mod:`module1` -:external:py:mod:`inv:module2` +- ``module1`` is only defined in ``inv``: + :external:py:mod:`module1` -.. py:module:: module1 +.. py:module:: module2 -:external:py:func:`func` -:external:py:func:`inv:func` -:external:py:meth:`Foo.bar` -:external:py:meth:`inv:Foo.bar` +- ``module2`` is defined here and also in ``inv``, but should resolve to inv: + :external:py:mod:`module2` -:external:c:func:`CFunc` -:external:c:func:`inv:CFunc` -:external:doc:`docname` -:external:doc:`inv:docname` -:external:option:`ls -l` -:external:option:`inv:ls -l` +- ``module3`` is not defined anywhere, so should warn: + :external:py:mod:`module3` -.. cpp:type:: std::uint8_t -.. cpp:class:: foo::Bar +.. py:module:: module10 -:external:cpp:type:`std::uint8_t` -:external:cpp:class:`inv:foo::Bar` -:external:cpp:func:`:inv2:foo::Bar::baz` +- ``module10`` is only defined here, but should still not be resolved to: + :external:py:mod:`module10` -:external:cpp:type:`FoonsTitle ` -:external:cpp:type:`inv:BarType ` \ No newline at end of file + +- a function in inv: + :external:py:func:`module1.func` +- a method, but with old style inventory prefix, which shouldn't work: + :external:py:meth:`inv:Foo.bar` diff --git a/tests/test_ext_intersphinx.py b/tests/test_ext_intersphinx.py index a1118e48da..ec2aad4409 100644 --- a/tests/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx.py @@ -527,7 +527,7 @@ def log_message(*args, **kwargs): @pytest.mark.sphinx('html', testroot='ext-intersphinx-role') -def test_intersphinx_role(app): +def test_intersphinx_role(app, warning): inv_file = app.srcdir / 'inventory' inv_file.write_bytes(inventory_v2) app.config.intersphinx_mapping = { @@ -542,21 +542,13 @@ def test_intersphinx_role(app): app.build() content = (app.outdir / 'index.html').read_text() + wStr = warning.getvalue() - targets = ( - 'foo.html#module-module1', - 'foo.html#module-module2', - 'sub/foo.html#module1.func', - 'index.html#foo.Bar.baz', - 'cfunc.html#CFunc', - 'docname.html', - 'index.html#cmdoption-ls-l', - 'index.html#std_uint8_t', - 'index.html#foons', - 'index.html#foons_bartype', - ) html = '' - for t in targets: - assert html.format(t) in content + assert html.format('foo.html#module-module1') in content + assert html.format('foo.html#module-module2') in content + assert "WARNING: external py:mod reference target not found: module3" in wStr + assert "WARNING: external py:mod reference target not found: module10" in wStr - assert html.format('index.html#cpp_foo_bar_baz') not in content + assert html.format('sub/foo.html#module1.func') in content + assert "WARNING: external py:meth reference target not found: inv:Foo.bar" in wStr From 029d0e5b1837de4e33ee92cbb88d650e9afb8a3d Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Sat, 6 Nov 2021 14:02:37 +0100 Subject: [PATCH 09/18] intersphinx role: new inventory parsing from role name --- sphinx/ext/intersphinx.py | 42 +++++++++++++------ .../roots/test-ext-intersphinx-role/index.rst | 25 ++++++++++- tests/test_ext_intersphinx.py | 17 ++++++++ 3 files changed, 71 insertions(+), 13 deletions(-) diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index ce10f12288..2c640925b9 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -489,9 +489,15 @@ def role(self, role_name: str, language_module: ModuleType, lineno: int, reporte class IntersphinxRole(SphinxRole): def run(self) -> Tuple[List[Node], List[system_message]]: - role_name = self.get_role_name(self.name) + inventory, name_suffix = self.get_inventory_and_name_suffix(self.name) + if inventory and not inventory_exists(self.env, inventory): + logger.warning(__('inventory for external cross-reference not found: %s'), inventory, + location=(self.env.docname, self.lineno)) + return [], [] + + role_name = self.get_role_name(name_suffix) if role_name is None: - logger.warning(__('role not found: %s'), self.name, + logger.warning(__('role for external cross-reference not found: %s'), name_suffix, location=(self.env.docname, self.lineno)) return [], [] @@ -499,20 +505,33 @@ def run(self) -> Tuple[List[Node], List[system_message]]: for node in result: if isinstance(node, pending_xref): node['intersphinx'] = True - node['inventory'] = None + node['inventory'] = inventory return result, messages + def get_inventory_and_name_suffix(self, name: str) -> Tuple[Optional[str], Optional[str]]: + assert name.startswith('external:'), name + name = name[9:] + inv_names = name.split('+') + inventory = None + if len(inv_names) > 1: + # inv+role + # inv+domain:role + inventory = inv_names[0] + name = name[len(inventory)+1:] + return inventory, name + def get_role_name(self, name: str) -> Optional[Tuple[str, str]]: names = name.split(':') - if len(names) == 2: - # :external:role: - domain = self.env.temp_data.get('default_domain') + if len(names) == 1: + # role + default_domain = self.env.temp_data.get('default_domain') + domain = default_domain.name if default_domain else None + role = names[0] + elif len(names) == 2: + # domain:role: + domain = names[0] role = names[1] - elif len(names) == 3: - # :external:domain:role: - domain = names[1] - role = names[2] else: return None @@ -559,8 +578,7 @@ def run(self, **kwargs: Any) -> None: contnode = cast(nodes.TextElement, node[0].deepcopy()) inv_name = node['inventory'] if inv_name is not None: - if not inventory_exists(self.env, inv_name): - continue + assert inventory_exists(self.env, inv_name) newnode = resolve_reference_in_inventory(self.env, inv_name, node, contnode) else: newnode = resolve_reference_any_inventory(self.env, False, node, contnode) diff --git a/tests/roots/test-ext-intersphinx-role/index.rst b/tests/roots/test-ext-intersphinx-role/index.rst index bb7a6c4f19..5c2cdbd008 100644 --- a/tests/roots/test-ext-intersphinx-role/index.rst +++ b/tests/roots/test-ext-intersphinx-role/index.rst @@ -14,8 +14,31 @@ - ``module10`` is only defined here, but should still not be resolved to: :external:py:mod:`module10` - - a function in inv: :external:py:func:`module1.func` - a method, but with old style inventory prefix, which shouldn't work: :external:py:meth:`inv:Foo.bar` +- a non-existing role: + :external:py:nope:`something` + +.. default-domain:: cpp + +- a type where the default domain is used to find the role: + :external:type:`std::uint8_t` +- a non-existing role in default domain: + :external:nope:`somethingElse` + +- two roles in ``std`` which can be found without a default domain: + + - :external:doc:`docname` + - :external:option:`ls -l` + + +- a function with explicit inventory: + :external:inv+c:func:`CFunc` +- a class with explicit non-existing inventory, which also has upper-case in name: + :external:invNope+cpp:class:`foo::Bar` + + +- explicit title: + :external:cpp:type:`FoonsTitle ` diff --git a/tests/test_ext_intersphinx.py b/tests/test_ext_intersphinx.py index ec2aad4409..11450e2c34 100644 --- a/tests/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx.py @@ -552,3 +552,20 @@ def test_intersphinx_role(app, warning): assert html.format('sub/foo.html#module1.func') in content assert "WARNING: external py:meth reference target not found: inv:Foo.bar" in wStr + + assert "WARNING: role for external cross-reference not found: py:nope" in wStr + + # default domain + assert html.format('index.html#std_uint8_t') in content + assert "WARNING: role for external cross-reference not found: nope" in wStr + + # std roles without domain prefix + assert html.format('docname.html') in content + assert html.format('index.html#cmdoption-ls-l') in content + + # explicit inventory + assert html.format('cfunc.html#CFunc') in content + #assert "WARNING: inventory for external cross-reference not found: invNope" in wStr + + # explicit title + assert html.format('index.html#foons') in content From ccc4f95b42c4caba74cf01f9aa898a96a3372b9e Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Sat, 6 Nov 2021 14:33:47 +0100 Subject: [PATCH 10/18] intersphinx role, hax role name to preserve case --- sphinx/ext/intersphinx.py | 8 ++++++-- tests/test_ext_intersphinx.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index 2c640925b9..90ad8e7583 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -482,14 +482,18 @@ def __init__(self) -> None: def role(self, role_name: str, language_module: ModuleType, lineno: int, reporter: Reporter ) -> Tuple[RoleFunction, List[system_message]]: if role_name.split(':')[0] == 'external': - return IntersphinxRole(), [] + return IntersphinxRole(role_name), [] else: return super().role(role_name, language_module, lineno, reporter) class IntersphinxRole(SphinxRole): + def __init__(self, orig_name: str) -> None: + self.orig_name = orig_name + def run(self) -> Tuple[List[Node], List[system_message]]: - inventory, name_suffix = self.get_inventory_and_name_suffix(self.name) + assert self.name == self.orig_name.lower() + inventory, name_suffix = self.get_inventory_and_name_suffix(self.orig_name) if inventory and not inventory_exists(self.env, inventory): logger.warning(__('inventory for external cross-reference not found: %s'), inventory, location=(self.env.docname, self.lineno)) diff --git a/tests/test_ext_intersphinx.py b/tests/test_ext_intersphinx.py index 11450e2c34..b2ad8afe52 100644 --- a/tests/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx.py @@ -565,7 +565,7 @@ def test_intersphinx_role(app, warning): # explicit inventory assert html.format('cfunc.html#CFunc') in content - #assert "WARNING: inventory for external cross-reference not found: invNope" in wStr + assert "WARNING: inventory for external cross-reference not found: invNope" in wStr # explicit title assert html.format('index.html#foons') in content From 540d76035cc6bbf7ee18d0eb9bf63e4c3651d1f9 Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Sat, 6 Nov 2021 15:27:31 +0100 Subject: [PATCH 11/18] intersphinx role, documentation --- doc/usage/extensions/intersphinx.rst | 80 ++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/doc/usage/extensions/intersphinx.rst b/doc/usage/extensions/intersphinx.rst index a3e65bed64..4b754fe460 100644 --- a/doc/usage/extensions/intersphinx.rst +++ b/doc/usage/extensions/intersphinx.rst @@ -8,20 +8,25 @@ .. versionadded:: 0.5 -This extension can generate automatic links to the documentation of objects in -other projects. - -Usage is simple: whenever Sphinx encounters a cross-reference that has no -matching target in the current documentation set, it looks for targets in the -documentation sets configured in :confval:`intersphinx_mapping`. A reference -like ``:py:class:`zipfile.ZipFile``` can then link to the Python documentation +This extension can generate links to the documentation of objects in external +projects, either explicitly through the :rst:role:`external` role, or as a +fallback resolution for any other cross-reference. + +Usage for fallback resolution is simple: whenever Sphinx encounters a +cross-reference that has no matching target in the current documentation set, +it looks for targets in the external documentation sets configured in +:confval:`intersphinx_mapping`. A reference like +``:py:class:`zipfile.ZipFile``` can then link to the Python documentation for the ZipFile class, without you having to specify where it is located exactly. -When using the "new" format (see below), you can even force lookup in a foreign -set by prefixing the link target appropriately. A link like ``:ref:`comparison -manual ``` will then link to the label "comparisons" in the -doc set "python", if it exists. +When using the :rst:role:`external` role, you can force lookup to any external +projects, and optionally to a specific external project. +A link like ``:external:ref:`comparison manual ``` will then link +to the label "comparisons" in whichever configured external project, if it +exists, +and a link like ``:external:python+ref:`comparison manual ``` will +link to the label "comparisons" only in the doc set "python", if it exists. Behind the scenes, this works as follows: @@ -30,8 +35,8 @@ Behind the scenes, this works as follows: * Projects using the Intersphinx extension can specify the location of such mapping files in the :confval:`intersphinx_mapping` config value. The mapping - will then be used to resolve otherwise missing references to objects into - links to the other documentation. + will then be used to resolve both :rst:role:`external` references, and also + otherwise missing references to objects into links to the other documentation. * 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 @@ -79,10 +84,10 @@ linking: at the same location as the base URI) or another local file path or a full HTTP URI to an inventory file. - The unique identifier can be used to prefix cross-reference targets, so that + The unique identifier can be used in the :rst:role:`external` role, so that it is clear which intersphinx set the target belongs to. A link like - ``:ref:`comparison manual ``` will link to the label - "comparisons" in the doc set "python", if it exists. + ``external:python+ref:`comparison manual ``` will link to the + label "comparisons" in the doc set "python", if it exists. **Example** @@ -162,21 +167,50 @@ linking: The default value is an empty list. - When a cross-reference without an explicit inventory specification is being - resolved by intersphinx, skip resolution if it matches one of the - specifications in this list. + When a non-:rst:role:`external` cross-reference is being resolved by + intersphinx, skip resolution if it matches one of the specifications in this + list. For example, with ``intersphinx_disabled_reftypes = ['std:doc']`` 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 + resolved by intersphinx, but ``:external:otherbook+doc:`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 ``*`` is in the list of domains, then no references without an explicit - inventory will be resolved by intersphinx. + If ``*`` is in the list of domains, then no non-:rst:role:`external` + references will be resolved by intersphinx. + +Explicitly Reference External Objects +------------------------------------- + +The Intersphinx extension provides the following role. + +.. rst:role:: external + + .. versionadded:: 4.4 + + Use Intersphinx to perform lookup only in external projects, and not the + current project. Intersphinx still needs to know the type of object you + would like to find, so the general form of this role is to write the + cross-refererence as if the object is in the current project, but then prefix + it with ``:external``. + The two forms are then + + - ``:external:domain:reftype:`target```, + e.g., ``:external:py:class:`zipfile.ZipFile```, or + - ``:external:reftype:`target```, + e.g., ``:external:doc:`installation```. + + If you would like to constrain the lookup to a specific external project, + then the key of the project, as specified in :confval:`intersphinx_mapping`, + is added as well to get the two forms + - ``:external:invname+domain:reftype:`target```, + e.g., ``:external:python+py:class:`zipfile.ZipFile```, or + - ``:external:invname+reftype:`target```, + e.g., ``:external:python+doc:`installation```. Showing all links of an Intersphinx mapping file ------------------------------------------------ From 9a3f2b85421948c98647b10106c1bbb5ff1b0628 Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Sat, 6 Nov 2021 15:51:53 +0100 Subject: [PATCH 12/18] intersphinx role, CHANGES --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index d2ddae71c8..54b09e5563 100644 --- a/CHANGES +++ b/CHANGES @@ -47,6 +47,8 @@ Features added * #9391: texinfo: improve variable in ``samp`` role * #9578: texinfo: Add :confval:`texinfo_cross_references` to disable cross references for readability with standalone readers +* #9822 (and #9062), add new Intersphinx role :rst:role:`external` for explict + lookup in the external projects, without resolving to the local project. Bugs fixed ---------- From 941db550f02d76ee2b93300584ac85dc599d21e6 Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Sat, 6 Nov 2021 16:25:59 +0100 Subject: [PATCH 13/18] intersphinx role, fix flake8 warnings --- sphinx/ext/intersphinx.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index 90ad8e7583..50cc13251e 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -495,8 +495,8 @@ def run(self) -> Tuple[List[Node], List[system_message]]: assert self.name == self.orig_name.lower() inventory, name_suffix = self.get_inventory_and_name_suffix(self.orig_name) if inventory and not inventory_exists(self.env, inventory): - logger.warning(__('inventory for external cross-reference not found: %s'), inventory, - location=(self.env.docname, self.lineno)) + logger.warning(__('inventory for external cross-reference not found: %s'), + inventory, location=(self.env.docname, self.lineno)) return [], [] role_name = self.get_role_name(name_suffix) @@ -522,7 +522,7 @@ def get_inventory_and_name_suffix(self, name: str) -> Tuple[Optional[str], Optio # inv+role # inv+domain:role inventory = inv_names[0] - name = name[len(inventory)+1:] + name = name[len(inventory) + 1:] return inventory, name def get_role_name(self, name: str) -> Optional[Tuple[str, str]]: From 9589a2bc0531598cdd69f260f2f2c2dbc5852d6e Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Sat, 15 Jan 2022 13:31:47 +0100 Subject: [PATCH 14/18] intersphinx role, remove redundant method --- sphinx/ext/intersphinx.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index 50cc13251e..5cbc59a961 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -476,9 +476,6 @@ class IntersphinxDispatcher(CustomReSTDispatcher): This enables :external:***: roles on parsing reST document. """ - def __init__(self) -> None: - super().__init__() - def role(self, role_name: str, language_module: ModuleType, lineno: int, reporter: Reporter ) -> Tuple[RoleFunction, List[system_message]]: if role_name.split(':')[0] == 'external': From c11b109d591a74f87de071ec4782ac3ab782ea38 Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Sat, 15 Jan 2022 14:02:21 +0100 Subject: [PATCH 15/18] intersphinx role: :external+inv:**: instead of :external:inv+**: --- sphinx/ext/intersphinx.py | 30 +++++++++++-------- .../roots/test-ext-intersphinx-role/index.rst | 4 +-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index 5cbc59a961..d35a9d9b94 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -473,12 +473,12 @@ def missing_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref, class IntersphinxDispatcher(CustomReSTDispatcher): """Custom dispatcher for external role. - This enables :external:***: roles on parsing reST document. + This enables :external:***:/:external+***: roles on parsing reST document. """ def role(self, role_name: str, language_module: ModuleType, lineno: int, reporter: Reporter ) -> Tuple[RoleFunction, List[system_message]]: - if role_name.split(':')[0] == 'external': + if len(role_name) > 9 and role_name.startswith('external') and role_name[8] in ':+': return IntersphinxRole(role_name), [] else: return super().role(role_name, language_module, lineno, reporter) @@ -510,17 +510,23 @@ def run(self) -> Tuple[List[Node], List[system_message]]: return result, messages - def get_inventory_and_name_suffix(self, name: str) -> Tuple[Optional[str], Optional[str]]: - assert name.startswith('external:'), name + def get_inventory_and_name_suffix(self, name: str) -> Tuple[Optional[str], str]: + assert name.startswith('external'), name + assert name[8] in ':+', name + typ = name[8] name = name[9:] - inv_names = name.split('+') - inventory = None - if len(inv_names) > 1: - # inv+role - # inv+domain:role - inventory = inv_names[0] - name = name[len(inventory) + 1:] - return inventory, name + if typ == '+': + # we have an explicit inventory name, i.e, + # :external+inv:role: or + # :external+inv:domain:role: + inv, name = name.split(':', 1) + return inv, name + else: + assert typ == ':' + # we look in all inventories, i.e., + # :external:role: or + # :external:domain:role: + return None, name def get_role_name(self, name: str) -> Optional[Tuple[str, str]]: names = name.split(':') diff --git a/tests/roots/test-ext-intersphinx-role/index.rst b/tests/roots/test-ext-intersphinx-role/index.rst index 5c2cdbd008..58edb7a1a0 100644 --- a/tests/roots/test-ext-intersphinx-role/index.rst +++ b/tests/roots/test-ext-intersphinx-role/index.rst @@ -35,9 +35,9 @@ - a function with explicit inventory: - :external:inv+c:func:`CFunc` + :external+inv:c:func:`CFunc` - a class with explicit non-existing inventory, which also has upper-case in name: - :external:invNope+cpp:class:`foo::Bar` + :external+invNope:cpp:class:`foo::Bar` - explicit title: From 3bf8bcd6e151a78b0dd003a3e76ff4c65566b6e6 Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Sat, 15 Jan 2022 14:05:42 +0100 Subject: [PATCH 16/18] intersphinx role, update docs --- doc/usage/extensions/intersphinx.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/usage/extensions/intersphinx.rst b/doc/usage/extensions/intersphinx.rst index 4b754fe460..2bcce68d0e 100644 --- a/doc/usage/extensions/intersphinx.rst +++ b/doc/usage/extensions/intersphinx.rst @@ -25,7 +25,7 @@ projects, and optionally to a specific external project. A link like ``:external:ref:`comparison manual ``` will then link to the label "comparisons" in whichever configured external project, if it exists, -and a link like ``:external:python+ref:`comparison manual ``` will +and a link like ``:external+python:ref:`comparison manual ``` will link to the label "comparisons" only in the doc set "python", if it exists. Behind the scenes, this works as follows: @@ -173,7 +173,7 @@ linking: For example, with ``intersphinx_disabled_reftypes = ['std:doc']`` a cross-reference ``:doc:`installation``` will not be attempted to be - resolved by intersphinx, but ``:external:otherbook+doc:`installation``` will + resolved by intersphinx, but ``:external+otherbook:doc:`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, @@ -207,10 +207,10 @@ The Intersphinx extension provides the following role. then the key of the project, as specified in :confval:`intersphinx_mapping`, is added as well to get the two forms - - ``:external:invname+domain:reftype:`target```, - e.g., ``:external:python+py:class:`zipfile.ZipFile```, or - - ``:external:invname+reftype:`target```, - e.g., ``:external:python+doc:`installation```. + - ``:external+invname:domain:reftype:`target```, + e.g., ``:external+python:py:class:`zipfile.ZipFile```, or + - ``:external+invname:reftype:`target```, + e.g., ``:external+python:doc:`installation```. Showing all links of an Intersphinx mapping file ------------------------------------------------ From 6ee0ecbe40ab8a3251538409cf35ffcc04765bfa Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Sun, 16 Jan 2022 09:40:23 +0100 Subject: [PATCH 17/18] intersphinx role, simplify role name matching --- sphinx/ext/intersphinx.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index d35a9d9b94..f19f5fa156 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -26,6 +26,7 @@ import concurrent.futures import functools import posixpath +import re import sys import time from os import path @@ -485,6 +486,11 @@ def role(self, role_name: str, language_module: ModuleType, lineno: int, reporte class IntersphinxRole(SphinxRole): + # 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"(\+([^:]+))?:(.*)") + def __init__(self, orig_name: str) -> None: self.orig_name = orig_name @@ -513,20 +519,13 @@ def run(self) -> Tuple[List[Node], List[system_message]]: def get_inventory_and_name_suffix(self, name: str) -> Tuple[Optional[str], str]: assert name.startswith('external'), name assert name[8] in ':+', name - typ = name[8] - name = name[9:] - if typ == '+': - # we have an explicit inventory name, i.e, - # :external+inv:role: or - # :external+inv:domain:role: - inv, name = name.split(':', 1) - return inv, name - else: - assert typ == ':' - # we look in all inventories, i.e., - # :external:role: or - # :external:domain:role: - return None, name + # either we have an explicit inventory name, i.e, + # :external+inv:role: or + # :external+inv:domain:role: + # or we look in all inventories, i.e., + # :external:role: or + # :external:domain:role: + return IntersphinxRole._re_inv_ref.fullmatch(name, 8).group(2, 3) def get_role_name(self, name: str) -> Optional[Tuple[str, str]]: names = name.split(':') From 5d595ec0c4294f45f3138c4c581b84c39cae5e29 Mon Sep 17 00:00:00 2001 From: Jakob Lykke Andersen Date: Sat, 15 Jan 2022 21:52:25 +0100 Subject: [PATCH 18/18] intersphinx role, simplify role_name check Co-authored-by: Takeshi KOMIYA --- sphinx/ext/intersphinx.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index f19f5fa156..2f8ab2588e 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -479,7 +479,7 @@ class IntersphinxDispatcher(CustomReSTDispatcher): def role(self, role_name: str, language_module: ModuleType, lineno: int, reporter: Reporter ) -> Tuple[RoleFunction, List[system_message]]: - if len(role_name) > 9 and role_name.startswith('external') and role_name[8] in ':+': + if len(role_name) > 9 and role_name.startswith(('external:', 'external+')): return IntersphinxRole(role_name), [] else: return super().role(role_name, language_module, lineno, reporter) @@ -525,7 +525,8 @@ def get_inventory_and_name_suffix(self, name: str) -> Tuple[Optional[str], str]: # or we look in all inventories, i.e., # :external:role: or # :external:domain:role: - return IntersphinxRole._re_inv_ref.fullmatch(name, 8).group(2, 3) + inv, suffix = IntersphinxRole._re_inv_ref.fullmatch(name, 8).group(2, 3) + return inv, suffix def get_role_name(self, name: str) -> Optional[Tuple[str, str]]: names = name.split(':')