diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py
index 5c6af2dfb75..f3e1f1db6aa 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, 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 TextElement
-from docutils.utils import relative_path
+from docutils.nodes import Node, TextElement, system_message
+from docutils.utils import Reporter, relative_path
import sphinx
from sphinx.addnodes import pending_xref
@@ -42,11 +43,14 @@
from sphinx.builders.html import INVENTORY_FILENAME
from sphinx.config import Config
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.nodes import find_pending_xref_condition
-from sphinx.util.typing import Inventory
+from sphinx.util.typing import Inventory, RoleFunction
logger = logging.getLogger(__name__)
@@ -351,6 +355,115 @@ def missing_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref,
return None
+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:
@@ -381,7 +494,9 @@ def setup(app: Sphinx) -> Dict[str, Any]:
app.add_config_value('intersphinx_timeout', None, False)
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/sphinx/util/docutils.py b/sphinx/util/docutils.py
index c2e12e15254..725d7ba3223 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:
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 00000000000..9485eb2075b
--- /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 00000000000..b8a7d871606
--- /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 62456a3f47d..36aa6a4c71e 100644
--- a/tests/test_ext_intersphinx.py
+++ b/tests/test_ext_intersphinx.py
@@ -461,3 +461,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)