Skip to content
This repository was archived by the owner on Apr 9, 2025. It is now read-only.

Commit 90fdb72

Browse files
authored
Merge pull request #86 from readthedocs/humitos/support-intersphinx
2 parents b51ff1f + 6ca6dfd commit 90fdb72

File tree

12 files changed

+378
-24
lines changed

12 files changed

+378
-24
lines changed

.readthedocs.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ python:
99

1010
sphinx:
1111
configuration: docs/conf.py
12-
12+
fail_on_warning: true
13+
1314
formats: []

docs/conf.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@
1313
# documentation root, use os.path.abspath to make it absolute, like shown here.
1414
#
1515
import os
16+
import datetime
1617
# import sys
1718
# sys.path.insert(0, os.path.abspath('.'))
1819

1920

2021
# -- Project information -----------------------------------------------------
2122

2223
project = 'sphinx-hoverxref'
23-
copyright = '2019, Manuel Kaufmann'
24+
year = datetime.datetime.now().year
25+
copyright = f'{year}, Manuel Kaufmann'
2426
author = 'Manuel Kaufmann'
2527

2628
# The short X.Y version
@@ -39,6 +41,7 @@
3941
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
4042
# ones.
4143
extensions = [
44+
'sphinx.ext.intersphinx',
4245
'sphinx.ext.autosectionlabel',
4346
'sphinx.ext.mathjax',
4447
'sphinx_tabs.tabs',
@@ -49,6 +52,19 @@
4952
'notfound.extension',
5053
]
5154

55+
intersphinx_mapping = {
56+
'readthedocs': ('https://docs.readthedocs.io/en/stable/', None),
57+
'sphinx': ('https://www.sphinx-doc.org/en/master/', None),
58+
}
59+
hoverxref_intersphinx = [
60+
'readthedocs',
61+
'sphinx',
62+
]
63+
hoverxref_intersphinx_types = {
64+
'readthedocs': 'modal',
65+
'sphinx': 'tooltip',
66+
}
67+
5268
# Used when building the documentation from the terminal and using a local Read
5369
# the Docs instance as backend
5470
hoverxref_api_host = 'http://localhost:8000'

docs/configuration.rst

+41-2
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,49 @@ These settings are global and have effect on both, tooltips and modal dialogues.
9191
'setting',
9292
]
9393
94+
.. confval:: hoverxref_intersphinx
95+
96+
Description: Enable Sphinx's hoverxref extension on intersphinx targets from ``intersphinx_mapping``.
97+
98+
Default: ``[]``
99+
100+
Type: list
101+
102+
.. warning::
103+
104+
The Sphinx's target project **must be hosted on Read the Docs** to work.
105+
This is a current limitation that we hope to remove in the future.
106+
107+
.. confval:: hoverxref_intersphinx_types
108+
109+
Description: Style used for intersphinx links.
110+
111+
Default: ``{}``. It uses :confval:`hoverxref_default_type` if the intersphinx target is not defined in this config.
112+
113+
Type: dict
114+
115+
Example:
116+
117+
.. code-block:: python
118+
119+
{
120+
# make specific links to use a particular tooltip type
121+
'readthdocs': {
122+
'doc': 'modal',
123+
'ref': 'tooltip',
124+
},
125+
'python': {
126+
'class': 'modal',
127+
'ref':, 'tooltip',
128+
},
129+
130+
# make all links for Sphinx to be ``tooltip``
131+
'sphinx': 'tooltip',
132+
}
94133
95134
.. confval:: hoverxref_sphinxtabs
96135

97-
Description: trigger an extra step to render tooltips where its content has a `Sphinx Tabs`_
136+
Description: Trigger an extra step to render tooltips where its content has a `Sphinx Tabs`_
98137

99138
Default: ``False``
100139

@@ -104,7 +143,7 @@ These settings are global and have effect on both, tooltips and modal dialogues.
104143

105144
.. confval:: hoverxref_mathjax
106145

107-
Description: trigger an extra step to render tooltips where its content has a `Mathjax`_
146+
Description: Trigger an extra step to render tooltips where its content has a `Mathjax`_
108147

109148
Default: ``False``
110149

docs/usage.rst

+25-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,30 @@ will render to this:
2222
This will :hoverxref:`show a tooltip <hoverxref:hoverxref>` in the linked words to ``hoverxref``.
2323

2424

25+
Tooltip on intersphinx content
26+
------------------------------
27+
28+
Sphinx comes with a nice built-in extension called :doc:`sphinx.ext.intersphinx <sphinx:usage/extensions/intersphinx>`
29+
that allows you to generate links to specific objects in other project's documentation pages.
30+
31+
You can combine this extension with ``sphinx-hoverxref`` to show tooltips over these links to other projects.
32+
For example, this documentation itself configures intersphinx with Read the Docs documentation and allow us
33+
to do the following:
34+
35+
.. code-block:: rst
36+
37+
Show a tooltip for :doc:`Read the Docs automation rules <readthedocs:automation-rules>`.
38+
39+
That will render to:
40+
41+
Show a tooltip for :doc:`Read the Docs automation rules <readthedocs:automation-rules>`.
42+
43+
.. note::
44+
45+
Keep in mind that the linked project should be hosted at Read the Docs.
46+
This is a limitation that will be removed in the future.
47+
48+
2549
Tooltip on custom object
2650
------------------------
2751

@@ -99,7 +123,7 @@ These actions are usually calling a Javascript function.
99123
This `may affect the rendering of tooltips`_ that includes content requiring extra rendering steps.
100124
**Make sure you are using Sphinx 3.4.x** if you require rendering this type of content in your tooltips.
101125

102-
.. _a feature to only include JS/CS in pages where they are used: https://github.com/sphinx-doc/sphinx/pull/8631
126+
.. _a feature to only include JS/CSS in pages where they are used: https://github.com/sphinx-doc/sphinx/pull/8631
103127
.. _may affect the rendering of tooltips: https://github.com/sphinx-doc/sphinx/issues/9115
104128

105129

hoverxref/_static/js/hoverxref.js_t

+17-9
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,19 @@ function reLoadSphinxTabs() {
7979
};
8080
};
8181

82-
function getEmbedURL(project, version, doc, docpath, section) {
83-
var params = {
84-
'project': project,
85-
'version': version,
86-
'doc': doc,
87-
'path': docpath,
88-
'section': section,
82+
function getEmbedURL(project, version, doc, docpath, section, url) {
83+
if (url) {
84+
var params = {
85+
'url': url,
86+
}
87+
} else {
88+
var params = {
89+
'project': project,
90+
'version': version,
91+
'doc': doc,
92+
'path': docpath,
93+
'section': section,
94+
}
8995
}
9096
console.debug('Data: ' + JSON.stringify(params));
9197
var url = '{{ hoverxref_api_host }}' + '/api/v2/embed/?' + $.param(params);
@@ -111,11 +117,12 @@ $(document).ready(function() {
111117
var doc = $origin.data('doc');
112118
var docpath = $origin.data('docpath');
113119
var section = $origin.data('section');
120+
var url = $origin.data('url');
114121

115122

116123
// we set a variable so the data is only loaded once via Ajax, not every time the tooltip opens
117124
if ($origin.data('loaded') !== true) {
118-
var url = getEmbedURL(project, version, doc, docpath, section);
125+
var url = getEmbedURL(project, version, doc, docpath, section, url);
119126
$.get(url, function(data) {
120127
// call the 'content' method to update the content of our tooltip with the returned data.
121128
// note: this content update will trigger an update animation (see the updateAnimation option)
@@ -184,8 +191,9 @@ $(document).ready(function() {
184191
var doc = element.data('doc');
185192
var docpath = element.data('docpath');
186193
var section = element.data('section');
194+
var url = element.data('url');
187195

188-
var url = getEmbedURL(project, version, doc, docpath, section);
196+
var url = getEmbedURL(project, version, doc, docpath, section, url);
189197
$.get(url, function(data) {
190198
var content = $('<div></div>');
191199
content.html(data['content'][0]);

hoverxref/extension.py

+115
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import types
44
from docutils import nodes
55
import sphinx
6+
from sphinx.ext.intersphinx import InventoryAdapter
7+
from sphinx.ext.intersphinx import missing_reference as sphinx_missing_reference
68
from sphinx.roles import XRefRole
79
from sphinx.util.fileutil import copy_asset
810
from sphinx.util import logging
@@ -139,6 +141,114 @@ def setup_sphinx_tabs(app, config):
139141
app.disconnect(listener_id)
140142

141143

144+
def setup_intersphinx(app, config):
145+
"""
146+
Disconnect ``missing-reference`` from ``sphinx.ext.intershinx``.
147+
148+
As there is no way to hook into the ``missing_referece`` function to add
149+
some extra data to the docutils node returned by this function, we
150+
disconnect the original listener and add our custom one.
151+
152+
https://github.com/sphinx-doc/sphinx/blob/53c1dff/sphinx/ext/intersphinx.py
153+
"""
154+
if not app.config.hoverxref_intersphinx:
155+
# Do not disconnect original intersphinx missing-reference if the user
156+
# does not have hoverxref intersphinx enabled
157+
return
158+
159+
if sphinx.version_info < (3, 0, 0):
160+
listeners = list(app.events.listeners.get('missing-reference').items())
161+
else:
162+
listeners = [
163+
(listener.id, listener.handler)
164+
for listener in app.events.listeners.get('missing-reference')
165+
]
166+
for listener_id, function in listeners:
167+
module_name = inspect.getmodule(function).__name__
168+
if module_name == 'sphinx.ext.intersphinx':
169+
app.disconnect(listener_id)
170+
171+
172+
def missing_reference(app, env, node, contnode):
173+
"""
174+
Override original ``missing_referece`` to add data into the node.
175+
176+
We call the original intersphinx extension and add hoverxref CSS classes
177+
plus the ``data-url`` to the node returned from it.
178+
179+
Sphinx intersphinx downloads all the ``objects.inv`` and load each of them
180+
into a "named inventory" and also updates the "main inventory". We check if
181+
reference is part of any of the "named invetories" the user defined in
182+
``hoverxref_intersphinx`` and we add hoverxref to the node **only if** the
183+
reference is on those inventories.
184+
185+
See https://github.com/sphinx-doc/sphinx/blob/4d90277c/sphinx/ext/intersphinx.py#L244-L250
186+
"""
187+
if not app.config.hoverxref_intersphinx:
188+
# Do nothing if the user doesn't have hoverxref intersphinx enabled
189+
return
190+
191+
# We need to grab all the attributes before calling
192+
# ``sphinx_missing_reference`` because it modifies the node in-place
193+
domain = node.get('refdomain') # ``std`` if used on ``:ref:``
194+
target = node['reftarget']
195+
reftype = node['reftype']
196+
197+
# By default we skip adding hoverxref to the node to avoid possible
198+
# problems. We want to be sure we have to add hoverxref on it
199+
skip_node = True
200+
inventory_name_matched = None
201+
202+
if domain == 'std':
203+
# Using ``:ref:`` manually, we could write intersphinx like:
204+
# :ref:`datetime <python:datetime.datetime>`
205+
# and the node will have these attribues:
206+
# refdomain: std
207+
# reftype: ref
208+
# reftarget: python:datetime.datetime
209+
# refexplicit: True
210+
if ':' in target:
211+
inventory_name, _ = target.split(':', 1)
212+
if inventory_name in app.config.hoverxref_intersphinx:
213+
skip_node = False
214+
inventory_name_matched = inventory_name
215+
else:
216+
# Using intersphinx via ``sphinx.ext.autodoc`` generates links for docstrings like:
217+
# :py:class:`float`
218+
# and the node will have these attribues:
219+
# refdomain: py
220+
# reftype: class
221+
# reftarget: float
222+
# refexplicit: False
223+
inventories = InventoryAdapter(env)
224+
225+
for inventory_name in app.config.hoverxref_intersphinx:
226+
inventory = inventories.named_inventory.get(inventory_name, {})
227+
if inventory.get(f'{domain}:{reftype}', {}).get(target) is not None:
228+
# The object **does** exist on the inventories defined by the
229+
# user: enable hoverxref on this node
230+
skip_node = False
231+
inventory_name_matched = inventory_name
232+
break
233+
234+
newnode = sphinx_missing_reference(app, env, node, contnode)
235+
if newnode is not None and not skip_node:
236+
hoverxref_type = app.config.hoverxref_intersphinx_types.get(inventory_name_matched)
237+
if isinstance(hoverxref_type, dict):
238+
# Specific style for a particular reftype
239+
hoverxref_type = hoverxref_type.get(reftype)
240+
hoverxref_type = hoverxref_type or app.config.hoverxref_default_type
241+
242+
classes = newnode.get('classes')
243+
classes.extend(['hoverxref', hoverxref_type])
244+
newnode.replace_attr('classes', classes)
245+
newnode._hoverxref = {
246+
'data-url': newnode.get('refuri'),
247+
}
248+
249+
return newnode
250+
251+
142252
def setup_translators(app):
143253
"""
144254
Override translators respecting the one defined (if any).
@@ -255,6 +365,8 @@ def setup(app):
255365
app.add_config_value('hoverxref_ignore_refs', ['genindex', 'modindex', 'search'], 'env')
256366
app.add_config_value('hoverxref_role_types', {}, 'env')
257367
app.add_config_value('hoverxref_default_type', 'tooltip', 'env')
368+
app.add_config_value('hoverxref_intersphinx', [], 'env')
369+
app.add_config_value('hoverxref_intersphinx_types', {}, 'env')
258370
app.add_config_value('hoverxref_api_host', 'https://readthedocs.org', 'env')
259371

260372
# Tooltipster settings
@@ -288,10 +400,13 @@ def setup(app):
288400

289401
app.connect('config-inited', setup_domains)
290402
app.connect('config-inited', setup_sphinx_tabs)
403+
app.connect('config-inited', setup_intersphinx)
291404
app.connect('config-inited', is_hoverxref_configured)
292405
app.connect('config-inited', setup_theme)
293406
app.connect('build-finished', copy_asset_files)
294407

408+
app.connect('missing-reference', missing_reference)
409+
295410
for f in ASSETS_FILES:
296411
if f.endswith('.js') or f.endswith('.js_t'):
297412
app.add_js_file(f.replace('.js_t', '.js'))

tests/conftest.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
import shutil
33
import pytest
44

5-
from .utils import srcdir, prefixdocumentsrcdir, customobjectsrcdir, pythondomainsrcdir
5+
from .utils import srcdir, prefixdocumentsrcdir, customobjectsrcdir, pythondomainsrcdir, intersphinxsrc
66

77

88
@pytest.fixture(autouse=True, scope='function')
99
def remove_sphinx_build_output():
1010
"""Remove _build/ folder, if exist."""
11-
for path in (srcdir, prefixdocumentsrcdir, customobjectsrcdir, pythondomainsrcdir):
11+
for path in (srcdir, prefixdocumentsrcdir, customobjectsrcdir, pythondomainsrcdir, intersphinxsrc):
1212
build_path = os.path.join(path, '_build')
1313
if os.path.exists(build_path):
1414
shutil.rmtree(build_path)

tests/examples/intersphinx/conf.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# conf.py to run tests
2+
3+
master_doc = 'index'
4+
extensions = [
5+
'hoverxref.extension',
6+
'sphinx.ext.intersphinx',
7+
'sphinx.ext.autodoc',
8+
]
9+
10+
hoverxref_project = 'myproject'
11+
hoverxref_version = 'myversion'
12+
13+
# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html
14+
intersphinx_mapping = {
15+
'python': ('https://docs.python.org/3', None),
16+
'readthedocs': ('https://docs.readthedocs.io/en/stable/', None),
17+
}
18+
intersphinx_cache_limit = 0

0 commit comments

Comments
 (0)