#413: Added a way for intersphinx to refer to named labels in other

projects, and to specify the project you want to link to.
This commit is contained in:
Georg Brandl 2010-05-23 13:06:01 +02:00
parent 0dd83d9ee4
commit 5b7e794e2b
16 changed files with 135 additions and 75 deletions

View File

@ -8,6 +8,11 @@ Release 1.0 (in development)
* Added a JavaScript domain.
* Added a reStructuredText domain.
* Added a way for intersphinx to refer to named labels in other
projects, and to specify the project you want to link to.
* Support for docutils 0.4 has been removed.
* Added Croatian translation, thanks to Bojan Mihelač.

View File

@ -8,17 +8,16 @@
.. versionadded:: 0.5
This extension can generate automatic links to the documentation of Python
objects in other projects. This works as follows:
This extension can generate automatic links to the documentation of objects in
other projects. This works as follows:
* Each Sphinx HTML build creates a file named :file:`objects.inv` that
contains a mapping from Python identifiers to URIs relative to the HTML set's
root.
* Each Sphinx HTML build creates a file named :file:`objects.inv` that contains
a mapping from object names to URIs relative to the HTML set's root.
* Projects using the Intersphinx extension can specify the location of such
mapping files in the :confval:`intersphinx_mapping` config value. The mapping
will then be used to resolve otherwise missing references to Python objects
into links to the other documentation.
will then be used to resolve otherwise missing references to objects into
links to the other documentation.
* By default, the mapping file is assumed to be at the same location as the rest
of the documentation; however, the location of the mapping file can also be
@ -31,37 +30,63 @@ linking:
.. confval:: intersphinx_mapping
This config value contains the locations and names of other projects that
should be linked to in this documentation.
Relative local paths for target locations are taken as relative to the base
of the built documentation, while relative local paths for inventory
locations are taken as relative to the source directory.
When fetching remote inventory files, proxy settings will be read from
the ``$HTTP_PROXY`` environment variable.
**Old format for this config value**
This is the format used before Sphinx 1.0. It is still recognized.
A dictionary mapping URIs to either ``None`` or an URI. The keys are the
base URI of the foreign Sphinx documentation sets and can be local paths or
HTTP URIs. The values indicate where the inventory file can be found: they
can be ``None`` (at the same location as the base URI) or another local or
HTTP URI.
Relative local paths in the keys are taken as relative to the base of the
built documentation, while relative local paths in the values are taken as
relative to the source directory.
**New format for this config value**
An example, to add links to modules and objects in the Python standard
library documentation::
.. versionadded:: 1.0
intersphinx_mapping = {'http://docs.python.org/': None}
A dictionary mapping unique identifiers to a tuple ``(target, inventory)``.
Each ``target`` is the base URI of a foreign Sphinx documentation set and can
be a local path or an HTTP URI. The ``inventory`` indicates where the
inventory file can be found: it can be ``None`` (at the same location as
the base URI) or another local or HTTP URI.
The unique identifier can be used to prefix cross-reference targets, so that
it is clear which intersphinx set the target belongs to. A link like
``:ref:`comparison manual <python:comparisons>``` will link to the label
"comparisons" in the doc set "python", if it exists.
**Example**
To add links to modules and objects in the Python standard library
documentation, use::
intersphinx_mapping = {'python': ('http://docs.python.org/', None)}
This will download the corresponding :file:`objects.inv` file from the
Internet and generate links to the pages under the given URI. The downloaded
inventory is cached in the Sphinx environment, so it must be redownloaded
whenever you do a full rebuild.
A second example, showing the meaning of a non-``None`` value::
A second example, showing the meaning of a non-``None`` value of the second
tuple item::
intersphinx_mapping = {'http://docs.python.org/': 'python-inv.txt'}
intersphinx_mapping = {'python': ('http://docs.python.org/',
'python-inv.txt')}
This will read the inventory from :file:`python-inv.txt` in the source
directory, but still generate links to the pages under
``http://docs.python.org/``. It is up to you to update the inventory file
as new objects are added to the Python documentation.
When fetching remote inventory files, proxy settings will be read from
the ``$HTTP_PROXY`` environment variable.
``http://docs.python.org/``. It is up to you to update the inventory file as
new objects are added to the Python documentation.
.. confval:: intersphinx_cache_limit

View File

@ -742,13 +742,17 @@ class StandaloneHTMLBuilder(Builder):
f.write('# The remainder of this file is compressed using zlib.\n')
compressor = zlib.compressobj(9)
for domainname, domain in self.env.domains.iteritems():
for name, type, docname, anchor, prio in domain.get_objects():
for name, dispname, type, docname, anchor, prio in \
domain.get_objects():
if anchor.endswith(name):
# this can shorten the inventory by as much as 25%
anchor = anchor[:-len(name)] + '$'
uri = self.get_target_uri(docname) + '#' + anchor
if dispname == name:
dispname = '-'
f.write(compressor.compress(
'%s %s:%s %s %s\n' % (name, domainname, type, prio,
self.get_target_uri(docname) + '#' + anchor)))
'%s %s:%s %s %s %s\n' % (name, domainname, type, prio,
uri, dispname)))
f.write(compressor.flush())
finally:
f.close()

View File

@ -18,7 +18,6 @@ from os import path
from docutils import nodes
from sphinx import addnodes
from sphinx.locale import _
from sphinx.builders.html import StandaloneHTMLBuilder

View File

@ -18,9 +18,9 @@ from os import path
from docutils import nodes
from sphinx import addnodes
from sphinx.locale import _
from sphinx.builders.html import StandaloneHTMLBuilder
_idpattern = re.compile(
r'(?P<title>.+) (\((?P<id>[\w\.]+)( (?P<descr>\w+))?\))$')

View File

@ -229,6 +229,7 @@ class Domain(object):
five items:
* `name` -- fully qualified name
* `dispname` -- name to display when searching/linking
* `type` -- object type, a key in ``self.object_types``
* `docname` -- the document where it is to be found
* `anchor` -- the anchor name for the object

View File

@ -210,4 +210,4 @@ class CDomain(Domain):
def get_objects(self):
for refname, (docname, type) in self.data['objects'].iteritems():
yield (refname, type, docname, refname, 1)
yield (refname, refname, type, docname, refname, 1)

View File

@ -1094,4 +1094,4 @@ class CPPDomain(Domain):
def get_objects(self):
for refname, (docname, type) in self.data['objects'].iteritems():
yield (refname, type, docname, refname, 1)
yield (refname, refname, type, docname, refname, 1)

View File

@ -8,7 +8,6 @@
:copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import re
from sphinx import addnodes
from sphinx.domains import Domain, ObjType
@ -215,4 +214,4 @@ class JavaScriptDomain(Domain):
def get_objects(self):
for refname, (docname, type) in self.data['objects'].iteritems():
yield refname, type, docname, refname, 1
yield refname, refname, type, docname, refname, 1

View File

@ -617,6 +617,6 @@ class PythonDomain(Domain):
def get_objects(self):
for modname, info in self.data['modules'].iteritems():
yield (modname, 'module', info[0], 'module-' + modname, 0)
yield (modname, modname, 'module', info[0], 'module-' + modname, 0)
for refname, (docname, type) in self.data['objects'].iteritems():
yield (refname, type, docname, refname, 1)
yield (refname, refname, type, docname, refname, 1)

View File

@ -131,4 +131,4 @@ class ReSTDomain(Domain):
def get_objects(self):
for (typ, name), docname in self.data['objects'].iteritems():
yield name, typ, docname, name, 1
yield name, name, typ, docname, name, 1

View File

@ -418,25 +418,27 @@ class StandardDomain(Domain):
def resolve_xref(self, env, fromdocname, builder,
typ, target, node, contnode):
if typ == 'ref':
refdoc = node.get('refdoc', fromdocname)
#refdoc = node.get('refdoc', fromdocname)
if node['refexplicit']:
# reference to anonymous label; the reference uses
# the supplied link caption
docname, labelid = self.data['anonlabels'].get(target, ('',''))
sectname = node.astext()
if not docname:
env.warn(refdoc, 'undefined label: %s' %
target, node.line)
# XXX warn somehow if not resolved by intersphinx
#if not docname:
# env.warn(refdoc, 'undefined label: %s' %
# target, node.line)
else:
# reference to named label; the final node will
# contain the section name after the label
docname, labelid, sectname = self.data['labels'].get(target,
('','',''))
if not docname:
env.warn(refdoc,
'undefined label: %s' % target + ' -- if you '
'don\'t give a link caption the label must '
'precede a section header.', node.line)
# XXX warn somehow if not resolved by intersphinx
#if not docname:
# env.warn(refdoc,
# 'undefined label: %s' % target + ' -- if you '
# 'don\'t give a link caption the label must '
# 'precede a section header.', node.line)
if not docname:
return None
newnode = nodes.reference('', '')
@ -487,9 +489,9 @@ class StandardDomain(Domain):
def get_objects(self):
for (prog, option), info in self.data['progoptions'].iteritems():
yield (option, 'option', info[0], info[1], 1)
yield (option, option, 'option', info[0], info[1], 1)
for (type, name), info in self.data['objects'].iteritems():
yield (name, type, info[0], info[1],
yield (name, name, type, info[0], info[1],
self.object_types[type].attrs['searchprio'])
for name, info in self.data['labels'].iteritems():
yield (name, 'label', info[0], info[1], -1)
yield (name, info[2], 'label', info[0], info[1], -1)

View File

@ -61,7 +61,7 @@ default_settings = {
# This is increased every time an environment attribute is added
# or changed to properly invalidate pickle files.
ENV_VERSION = 35
ENV_VERSION = 36
default_substitutions = set([

View File

@ -58,7 +58,7 @@ def read_inventory_v1(f, uri, join):
else:
type = 'py:' + type
location += '#' + name
invdata.setdefault(type, {})[name] = (projname, version, location)
invdata.setdefault(type, {})[name] = (projname, version, location, '-')
return invdata
@ -90,11 +90,12 @@ def read_inventory_v2(f, uri, join, bufsize=16*1024):
assert not buf
for line in split_lines(read_chunks()):
name, type, prio, location = line.rstrip().split(None, 3)
name, type, prio, location, dispname = line.rstrip().split(None, 4)
if location.endswith('$'):
location = location[:-1] + name
location = join(uri, location)
invdata.setdefault(type, {})[name] = (projname, version, location)
invdata.setdefault(type, {})[name] = (projname, version,
location, dispname)
return invdata
@ -142,7 +143,15 @@ def load_mappings(app):
env.intersphinx_cache = {}
cache = env.intersphinx_cache
update = False
for uri, inv in app.config.intersphinx_mapping.iteritems():
for key, value in app.config.intersphinx_mapping.iteritems():
if isinstance(value, tuple):
# new format
name, (uri, inv) = key, value
if not name.isalnum():
env.warn('intersphinx identifier %r is not alphanumeric' % name)
else:
# old format, no name
name, uri, inv = None, key, value
# we can safely assume that the uri<->inv mapping is not changed
# during partial rebuilds since a changed intersphinx_mapping
# setting will cause a full environment reread
@ -152,12 +161,19 @@ def load_mappings(app):
# files; remote ones only if the cache time is expired
if '://' not in inv or uri not in cache \
or cache[uri][0] < cache_time:
app.info('loading intersphinx inventory from %s...' % inv)
invdata = fetch_inventory(app, uri, inv)
cache[uri] = (now, invdata)
if invdata:
cache[uri] = (name, now, invdata)
else:
cache.pop(uri, None)
update = True
if update:
env.intersphinx_inventory = {}
for _, invdata in cache.itervalues():
env.intersphinx_named_inventory = {}
for name, _, invdata in cache.itervalues():
if name:
env.intersphinx_named_inventory[name] = invdata
for type, objects in invdata.iteritems():
env.intersphinx_inventory.setdefault(
type, {}).update(objects)
@ -173,20 +189,27 @@ def missing_reference(app, env, node, contnode):
objtypes = env.domains[domain].objtypes_for_role(node['reftype'])
if not objtypes:
return
for objtype in objtypes:
fulltype = '%s:%s' % (domain, objtype)
if fulltype in env.intersphinx_inventory and \
target in env.intersphinx_inventory[fulltype]:
break
else:
return
proj, version, uri = env.intersphinx_inventory[fulltype][target]
newnode = nodes.reference('', '')
newnode['refuri'] = uri
newnode['reftitle'] = '(in %s v%s)' % (proj, version)
newnode['class'] = 'external-xref'
newnode.append(contnode)
return newnode
objtypes = ['%s:%s' % (domain, objtype) for objtype in objtypes]
to_try = [(env.intersphinx_inventory, target)]
if ':' in target:
# first part may be the foreign doc set name
setname, newtarget = target.split(':', 1)
if setname in env.intersphinx_named_inventory:
to_try.append((env.intersphinx_named_inventory[setname], newtarget))
for inventory, target in to_try:
for objtype in objtypes:
if objtype not in inventory or target not in inventory[objtype]:
continue
proj, version, uri, dispname = inventory[objtype][target]
newnode = nodes.reference('', '')
newnode['refuri'] = uri
newnode['reftitle'] = '(in %s v%s)' % (proj, version)
newnode['class'] = 'external-xref'
if dispname == '-':
newnode.append(contnode)
else:
newnode.append(contnode.__class__(dispname, dispname))
return newnode
def setup(app):

View File

@ -153,7 +153,9 @@ class IndexBuilder(object):
otypes = self._objtypes
onames = self._objnames
for domainname, domain in self.env.domains.iteritems():
for fullname, type, docname, anchor, prio in domain.get_objects():
for fullname, dispname, type, docname, anchor, prio in \
domain.get_objects():
# XXX use dispname?
if docname not in fn2index:
continue
if prio < 0:

View File

@ -36,10 +36,10 @@ inventory_v2 = '''\
# Version: 2.0
# The remainder of this file is compressed with zlib.
''' + zlib.compress('''\
module1 py:module 0 foo.html#module-module1
module2 py:module 0 foo.html#module-$
module1.func py:function 1 sub/foo.html#$
CFunc c:function 2 cfunc.html#CFunc
module1 py:module 0 foo.html#module-module1 Long Module desc
module2 py:module 0 foo.html#module-$ -
module1.func py:function 1 sub/foo.html#$ -
CFunc c:function 2 cfunc.html#CFunc -
''')
@ -48,9 +48,9 @@ def test_read_inventory_v1():
f.readline()
invdata = read_inventory_v1(f, '/util', posixpath.join)
assert invdata['py:module']['module'] == \
('foo', '1.0', '/util/foo.html#module-module')
('foo', '1.0', '/util/foo.html#module-module', '-')
assert invdata['py:class']['module.cls'] == \
('foo', '1.0', '/util/foo.html#module.cls')
('foo', '1.0', '/util/foo.html#module.cls', '-')
def test_read_inventory_v2():
@ -67,9 +67,9 @@ def test_read_inventory_v2():
assert len(invdata1['py:module']) == 2
assert invdata1['py:module']['module1'] == \
('foo', '2.0', '/util/foo.html#module-module1')
('foo', '2.0', '/util/foo.html#module-module1', 'Long Module desc')
assert invdata1['py:module']['module2'] == \
('foo', '2.0', '/util/foo.html#module-module2')
('foo', '2.0', '/util/foo.html#module-module2', '-')
assert invdata1['py:function']['module1.func'][2] == \
'/util/sub/foo.html#module1.func'
assert invdata1['c:function']['CFunc'][2] == '/util/cfunc.html#CFunc'
@ -88,7 +88,7 @@ def test_missing_reference(tempdir, app):
inv = app.env.intersphinx_inventory
assert inv['py:module']['module2'] == \
('foo', '2.0', 'http://docs.python.org/foo.html#module-module2')
('foo', '2.0', 'http://docs.python.org/foo.html#module-module2', '-')
# create fake nodes and check referencing
contnode = nodes.emphasis('foo')