Skip to content

Commit 2368fca

Browse files
committedMay 5, 2021
intersphinx: Add :intersphinx:***: role
1 parent 4d76204 commit 2368fca

File tree

1 file changed

+120
-4
lines changed

1 file changed

+120
-4
lines changed
 

‎sphinx/ext/intersphinx.py

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

0 commit comments

Comments
 (0)