summaryrefslogtreecommitdiff
path: root/sphinx/ext/intersphinx.py
blob: 4938c041b145df2ec29219003aaab17474babc6c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# -*- coding: utf-8 -*-
"""
    sphinx.ext.intersphinx
    ~~~~~~~~~~~~~~~~~~~~~~

    Insert links to Python objects documented in remote Sphinx documentation.

    This works as follows:

    * Each Sphinx HTML build creates a file named "objects.inv" that contains
      a mapping from Python identifiers to URIs relative to the HTML set's root.

    * Projects using the Intersphinx extension can specify links to such mapping
      files in the `intersphinx_mapping` config value.  The mapping will then be
      used to resolve otherwise missing references to Python 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 specified individually, e.g. if the docs should be buildable
      without Internet access.

    :copyright: Copyright 2007-2009 by the Sphinx team, see AUTHORS.
    :license: BSD, see LICENSE for details.
"""

import time
import urllib
import posixpath
from os import path

from docutils import nodes

from sphinx.builder import INVENTORY_FILENAME


def fetch_inventory(app, uri, inv):
    """Fetch, parse and return an intersphinx inventory file."""
    invdata = {}
    # both *uri* (base URI of the links to generate) and *inv* (actual
    # location of the inventory file) can be local or remote URIs
    localuri = uri.find('://') == -1
    try:
        if inv.find('://') != -1:
            f = urllib.urlopen(inv)
        else:
            f = open(path.join(app.srcdir, inv))
    except Exception, err:
        app.warn('intersphinx inventory %r not fetchable due to '
                 '%s: %s' % (inv, err.__class__, err))
        return
    try:
        line = f.next()
        if line.rstrip() != '# Sphinx inventory version 1':
            raise ValueError('unknown or unsupported inventory version')
        line = f.next()
        projname = line.rstrip()[11:].decode('utf-8')
        line = f.next()
        version = line.rstrip()[11:]
        for line in f:
            name, type, location = line.rstrip().split(None, 2)
            if localuri:
                location = path.join(uri, location)
            else:
                location = posixpath.join(uri, location)
            invdata[name] = (type, projname, version, location)
        f.close()
    except Exception, err:
        app.warn('intersphinx inventory %r not readable due to '
                 '%s: %s' % (inv, err.__class__, err))
    else:
        return invdata


def load_mappings(app):
    """Load all intersphinx mappings into the environment."""
    now = int(time.time())
    cache_time = now - app.config.intersphinx_cache_limit * 86400
    env = app.builder.env
    if not hasattr(env, 'intersphinx_cache'):
        env.intersphinx_cache = {}
    cache = env.intersphinx_cache
    update = False
    for uri, inv in app.config.intersphinx_mapping.iteritems():
        # 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
        if not inv:
            inv = posixpath.join(uri, INVENTORY_FILENAME)
        # decide whether the inventory must be read: always read local
        # 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:
            invdata = fetch_inventory(app, uri, inv)
            cache[uri] = (now, invdata)
            update = True
    if update:
        env.intersphinx_inventory = {}
        for _, invdata in cache.itervalues():
            if invdata:
                env.intersphinx_inventory.update(invdata)


def missing_reference(app, env, node, contnode):
    """Attempt to resolve a missing reference via intersphinx references."""
    type = node['reftype']
    target = node['reftarget']
    if type == 'mod':
        type, proj, version, uri = env.intersphinx_inventory.get(target,
                                                                 ('','','',''))
        if type != 'mod':
            return None
        target = 'module-' + target   # for link anchor
    else:
        if target[-2:] == '()':
            target = target[:-2]
        target = target.rstrip(' *')
        # special case: exceptions and object methods
        if type == 'exc' and '.' not in target and \
           'exceptions.' + target in env.intersphinx_inventory:
            target = 'exceptions.' + target
        elif type in ('func', 'meth') and '.' not in target and \
           'object.' + target in env.intersphinx_inventory:
            target = 'object.' + target
        if target not in env.intersphinx_inventory:
            return None
        type, proj, version, uri = env.intersphinx_inventory[target]
    # print "Intersphinx hit:", target, uri
    newnode = nodes.reference('', '')
    newnode['refuri'] = uri + '#' + target
    newnode['reftitle'] = '(in %s v%s)' % (proj, version)
    newnode['class'] = 'external-xref'
    newnode.append(contnode)
    return newnode


def setup(app):
    app.add_config_value('intersphinx_mapping', {}, True)
    app.add_config_value('intersphinx_cache_limit', 5, False)
    app.connect('missing-reference', missing_reference)
    app.connect('builder-inited', load_mappings)