Skip to content

Commit eab049e

Browse files
committed
intersphinx: Add :intersphinx:***: role
1 parent f8a6315 commit eab049e

File tree

1 file changed

+118
-4
lines changed

1 file changed

+118
-4
lines changed

sphinx/ext/intersphinx.py

Lines changed: 118 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,24 +29,28 @@
2929
import sys
3030
import time
3131
from os import path
32-
from typing import IO, Any, Dict, List, Tuple
32+
from types import ModuleType
33+
from typing import IO, Any, Dict, List, Optional, Tuple, cast
3334
from urllib.parse import urlsplit, urlunsplit
3435

3536
from docutils import nodes
36-
from docutils.nodes import TextElement
37-
from docutils.utils import relative_path
37+
from docutils.nodes import Node, TextElement, system_message
38+
from docutils.utils import Reporter, relative_path
3839

3940
import sphinx
4041
from sphinx.addnodes import pending_xref
4142
from sphinx.application import Sphinx
4243
from sphinx.builders.html import INVENTORY_FILENAME
4344
from sphinx.config import Config
4445
from sphinx.environment import BuildEnvironment
46+
from sphinx.errors import ExtensionError
4547
from sphinx.locale import _, __
48+
from sphinx.transforms.post_transforms import ReferencesResolver
4649
from sphinx.util import logging, requests
50+
from sphinx.util.docutils import CustomReSTDispatcher, SphinxRole
4751
from sphinx.util.inventory import InventoryFile
4852
from sphinx.util.nodes import find_pending_xref_condition
49-
from sphinx.util.typing import Inventory
53+
from sphinx.util.typing import Inventory, RoleFunction
5054

5155
logger = logging.getLogger(__name__)
5256

@@ -351,6 +355,115 @@ def missing_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref,
351355
return None
352356

353357

358+
class IntersphinxDispatcher(CustomReSTDispatcher):
359+
"""Custom dispatcher for intersphinx role.
360+
361+
This enables :intersphinx:***: roles on parsing reST document.
362+
"""
363+
364+
def __init__(self) -> None:
365+
super().__init__()
366+
367+
def role(self, role_name: str, language_module: ModuleType, lineno: int, reporter: Reporter
368+
) -> Tuple[RoleFunction, List[system_message]]:
369+
if role_name.split(':')[0] == 'intersphinx':
370+
return IntersphinxRole(), []
371+
else:
372+
return super().role(role_name, language_module, lineno, reporter)
373+
374+
375+
class IntersphinxRole(SphinxRole):
376+
def run(self) -> Tuple[List[Node], List[system_message]]:
377+
role_name = self.get_role_name(self.name)
378+
if role_name is None:
379+
logger.warning(__('role not found: %s'), self.name,
380+
location=(self.env.docname, self.lineno))
381+
return [], []
382+
383+
result, messages = self.invoke_role(role_name)
384+
for node in result:
385+
if isinstance(node, pending_xref):
386+
node['intersphinx'] = True
387+
388+
return result, messages
389+
390+
def get_role_name(self, name: str) -> Optional[Tuple[str, str]]:
391+
names = name.split(':')
392+
if len(names) == 2:
393+
# :intersphinx:role:
394+
domain = self.env.temp_data.get('default_domain')
395+
role = names[1]
396+
elif len(names) == 3:
397+
# :intersphinx:domain:role:
398+
domain = names[1]
399+
role = names[2]
400+
else:
401+
return None
402+
403+
if domain and self.is_existent_role(domain, role):
404+
return (domain, role)
405+
elif self.is_existent_role('std', role):
406+
return ('std', role)
407+
else:
408+
return None
409+
410+
def is_existent_role(self, domain_name: str, role_name: str) -> bool:
411+
try:
412+
domain = self.env.get_domain(domain_name)
413+
if role_name in domain.roles:
414+
return True
415+
else:
416+
return False
417+
except ExtensionError:
418+
return False
419+
420+
def invoke_role(self, role: Tuple[str, str]) -> Tuple[List[Node], List[system_message]]:
421+
domain = self.env.get_domain(role[0])
422+
if domain:
423+
role_func = domain.role(role[1])
424+
425+
return role_func(':'.join(role), self.rawtext, self.text, self.lineno,
426+
self.inliner, self.options, self.content)
427+
else:
428+
return [], []
429+
430+
431+
class IntersphinxRoleResolver(ReferencesResolver):
432+
"""pending_xref node resolver for intersphinx role.
433+
434+
This resolves pending_xref nodes generated by :intersphinx:***: role.
435+
"""
436+
437+
default_priority = ReferencesResolver.default_priority - 1
438+
439+
def run(self, **kwargs: Any) -> None:
440+
for node in self.document.traverse(pending_xref):
441+
if 'intersphinx' in node:
442+
contnode = cast(nodes.TextElement, node[0].deepcopy())
443+
refdoc = node.get('refdoc', self.env.docname)
444+
try:
445+
domain = self.env.get_domain(node['refdomain'])
446+
except Exception:
447+
domain = None
448+
449+
newnode = missing_reference(self.app, self.env, node, contnode)
450+
if newnode is None:
451+
self.warn_missing_reference(refdoc, node['reftype'], node['reftarget'],
452+
node, domain)
453+
else:
454+
node.replace_self(newnode)
455+
456+
457+
def install_dispatcher(app: Sphinx, docname: str, source: List[str]) -> None:
458+
"""Enable IntersphinxDispatcher.
459+
460+
.. note:: The installed dispatcher will uninstalled on disabling sphinx_domain
461+
automatically.
462+
"""
463+
dispatcher = IntersphinxDispatcher()
464+
dispatcher.enable()
465+
466+
354467
def normalize_intersphinx_mapping(app: Sphinx, config: Config) -> None:
355468
for key, value in config.intersphinx_mapping.copy().items():
356469
try:
@@ -381,6 +494,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
381494
app.add_config_value('intersphinx_timeout', None, False)
382495
app.connect('config-inited', normalize_intersphinx_mapping, priority=800)
383496
app.connect('builder-inited', load_mappings)
497+
app.connect('source-read', install_dispatcher)
384498
app.connect('missing-reference', missing_reference)
385499
return {
386500
'version': sphinx.__display_version__,

0 commit comments

Comments
 (0)