mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
intersphinx: Add :intersphinx:***: role
This commit is contained in:
committed by
Jakob Lykke Andersen
parent
b4227dbe1b
commit
35dcc60a7c
@@ -29,12 +29,13 @@ import posixpath
|
||||
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.builders.html import INVENTORY_FILENAME
|
||||
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,
|
||||
|
||||
1
tests/roots/test-ext-intersphinx-role/conf.py
Normal file
1
tests/roots/test-ext-intersphinx-role/conf.py
Normal file
@@ -0,0 +1 @@
|
||||
extensions = ['sphinx.ext.intersphinx']
|
||||
11
tests/roots/test-ext-intersphinx-role/index.rst
Normal file
11
tests/roots/test-ext-intersphinx-role/index.rst
Normal file
@@ -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`
|
||||
@@ -524,3 +524,49 @@ def test_inspect_main_url(capsys):
|
||||
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 ('<a class="reference external" href="http://example.org/foo.html#module-module1"'
|
||||
' title="(in foo v2.0)">' in content)
|
||||
|
||||
# :intersphinx:py:module:`inv:module2`
|
||||
assert ('<a class="reference external" href="http://example.org/foo.html#module-module2"'
|
||||
' title="(in foo v2.0)">' in content)
|
||||
|
||||
# py:module + :intersphinx:py:function:`func`
|
||||
assert ('<a class="reference external" href="http://example.org/sub/foo.html#module1.func"'
|
||||
' title="(in foo v2.0)">' in content)
|
||||
|
||||
# py:module + :intersphinx:py:method:`Foo.bar`
|
||||
assert ('<a class="reference external" href="http://example.org/index.html#foo.Bar.baz"'
|
||||
' title="(in foo v2.0)">' in content)
|
||||
|
||||
# :intersphinx:c:function:`CFunc`
|
||||
assert ('<a class="reference external" href="http://example.org/cfunc.html#CFunc"'
|
||||
' title="(in foo v2.0)">' in content)
|
||||
|
||||
# :intersphinx:doc:`docname`
|
||||
assert ('<a class="reference external" href="http://example.org/docname.html"'
|
||||
' title="(in foo v2.0)">' in content)
|
||||
|
||||
# :intersphinx:option:`ls -l`
|
||||
assert ('<a class="reference external" href="http://example.org/index.html#cmdoption-ls-l"'
|
||||
' title="(in foo v2.0)">' in content)
|
||||
|
||||
Reference in New Issue
Block a user