|
1 # -*- coding: utf-8 -*- |
|
2 """ |
|
3 sphinx.ext.intersphinx |
|
4 ~~~~~~~~~~~~~~~~~~~~~~ |
|
5 |
|
6 Insert links to Python objects documented in remote Sphinx documentation. |
|
7 |
|
8 This works as follows: |
|
9 |
|
10 * Each Sphinx HTML build creates a file named "objects.inv" that contains |
|
11 a mapping from Python identifiers to URIs relative to the HTML set's root. |
|
12 |
|
13 * Projects using the Intersphinx extension can specify links to such mapping |
|
14 files in the `intersphinx_mapping` config value. The mapping will then be |
|
15 used to resolve otherwise missing references to Python objects into links |
|
16 to the other documentation. |
|
17 |
|
18 * By default, the mapping file is assumed to be at the same location as the |
|
19 rest of the documentation; however, the location of the mapping file can |
|
20 also be specified individually, e.g. if the docs should be buildable |
|
21 without Internet access. |
|
22 |
|
23 :copyright: 2008 by Georg Brandl. |
|
24 :license: BSD. |
|
25 """ |
|
26 |
|
27 import time |
|
28 import urllib |
|
29 import posixpath |
|
30 from os import path |
|
31 |
|
32 from docutils import nodes |
|
33 |
|
34 from sphinx.builder import INVENTORY_FILENAME |
|
35 |
|
36 |
|
37 def fetch_inventory(app, uri, inv): |
|
38 """Fetch, parse and return an intersphinx inventory file.""" |
|
39 invdata = {} |
|
40 # both *uri* (base URI of the links to generate) and *inv* (actual |
|
41 # location of the inventory file) can be local or remote URIs |
|
42 localuri = uri.find('://') == -1 |
|
43 try: |
|
44 if inv.find('://') != -1: |
|
45 f = urllib.urlopen(inv) |
|
46 else: |
|
47 f = open(path.join(app.srcdir, inv)) |
|
48 except Exception, err: |
|
49 app.warn('intersphinx inventory %r not fetchable due to ' |
|
50 '%s: %s' % (inv, err.__class__, err)) |
|
51 return |
|
52 try: |
|
53 line = f.next() |
|
54 if line.rstrip() != '# Sphinx inventory version 1': |
|
55 raise ValueError('unknown or unsupported inventory version') |
|
56 line = f.next() |
|
57 projname = line.rstrip()[11:].decode('utf-8') |
|
58 line = f.next() |
|
59 version = line.rstrip()[11:] |
|
60 for line in f: |
|
61 name, type, location = line.rstrip().split(None, 2) |
|
62 if localuri: |
|
63 location = path.join(uri, location) |
|
64 else: |
|
65 location = posixpath.join(uri, location) |
|
66 invdata[name] = (type, projname, version, location) |
|
67 f.close() |
|
68 except Exception, err: |
|
69 app.warn('intersphinx inventory %r not readable due to ' |
|
70 '%s: %s' % (inv, err.__class__, err)) |
|
71 else: |
|
72 return invdata |
|
73 |
|
74 |
|
75 def load_mappings(app): |
|
76 """Load all intersphinx mappings into the environment.""" |
|
77 now = int(time.time()) |
|
78 cache_time = now - app.config.intersphinx_cache_limit * 86400 |
|
79 env = app.builder.env |
|
80 if not hasattr(env, 'intersphinx_cache'): |
|
81 env.intersphinx_cache = {} |
|
82 cache = env.intersphinx_cache |
|
83 update = False |
|
84 for uri, inv in app.config.intersphinx_mapping.iteritems(): |
|
85 # we can safely assume that the uri<->inv mapping is not changed |
|
86 # during partial rebuilds since a changed intersphinx_mapping |
|
87 # setting will cause a full environment reread |
|
88 if not inv: |
|
89 inv = posixpath.join(uri, INVENTORY_FILENAME) |
|
90 # decide whether the inventory must be read: always read local |
|
91 # files; remote ones only if the cache time is expired |
|
92 if '://' not in inv or uri not in cache \ |
|
93 or cache[uri][0] < cache_time: |
|
94 invdata = fetch_inventory(app, uri, inv) |
|
95 cache[uri] = (now, invdata) |
|
96 update = True |
|
97 if update: |
|
98 env.intersphinx_inventory = {} |
|
99 for _, invdata in cache.itervalues(): |
|
100 if invdata: |
|
101 env.intersphinx_inventory.update(invdata) |
|
102 |
|
103 |
|
104 def missing_reference(app, env, node, contnode): |
|
105 """Attempt to resolve a missing reference via intersphinx references.""" |
|
106 type = node['reftype'] |
|
107 target = node['reftarget'] |
|
108 if type == 'mod': |
|
109 type, proj, version, uri = env.intersphinx_inventory.get(target, |
|
110 ('','','','')) |
|
111 if type != 'mod': |
|
112 return None |
|
113 target = 'module-' + target # for link anchor |
|
114 else: |
|
115 if target[-2:] == '()': |
|
116 target = target[:-2] |
|
117 target = target.rstrip(' *') |
|
118 # special case: exceptions and object methods |
|
119 if type == 'exc' and '.' not in target and \ |
|
120 'exceptions.' + target in env.intersphinx_inventory: |
|
121 target = 'exceptions.' + target |
|
122 elif type in ('func', 'meth') and '.' not in target and \ |
|
123 'object.' + target in env.intersphinx_inventory: |
|
124 target = 'object.' + target |
|
125 if target not in env.intersphinx_inventory: |
|
126 return None |
|
127 type, proj, version, uri = env.intersphinx_inventory[target] |
|
128 print "Intersphinx hit:", target, uri |
|
129 newnode = nodes.reference('', '') |
|
130 newnode['refuri'] = uri + '#' + target |
|
131 newnode['reftitle'] = '(in %s v%s)' % (proj, version) |
|
132 newnode['class'] = 'external-xref' |
|
133 newnode.append(contnode) |
|
134 return newnode |
|
135 |
|
136 |
|
137 def setup(app): |
|
138 app.add_config_value('intersphinx_mapping', {}, True) |
|
139 app.add_config_value('intersphinx_cache_limit', 5, False) |
|
140 app.connect('missing-reference', missing_reference) |
|
141 app.connect('builder-inited', load_mappings) |