179
|
1 |
# -*- coding: utf-8 -*-
|
|
2 |
"""
|
|
3 |
sphinx.application
|
|
4 |
~~~~~~~~~~~~~~~~~~
|
|
5 |
|
|
6 |
Sphinx application object.
|
|
7 |
|
|
8 |
Gracefully adapted from the TextPress system by Armin.
|
|
9 |
|
|
10 |
|
|
11 |
:copyright: 2008 by Georg Brandl, Armin Ronacher.
|
|
12 |
:license: BSD.
|
|
13 |
"""
|
|
14 |
|
|
15 |
import sys
|
|
16 |
import posixpath
|
|
17 |
from cStringIO import StringIO
|
|
18 |
|
|
19 |
from docutils import nodes
|
|
20 |
from docutils.parsers.rst import directives, roles
|
|
21 |
|
|
22 |
import sphinx
|
|
23 |
from sphinx.roles import xfileref_role, innernodetypes
|
|
24 |
from sphinx.config import Config
|
|
25 |
from sphinx.builder import builtin_builders, StandaloneHTMLBuilder
|
|
26 |
from sphinx.directives import desc_directive, target_directive, additional_xref_types
|
|
27 |
from sphinx.environment import SphinxStandaloneReader
|
|
28 |
from sphinx.util.console import bold
|
|
29 |
|
|
30 |
|
|
31 |
class SphinxError(Exception):
|
|
32 |
"""
|
|
33 |
Base class for Sphinx errors that are shown to the user in a nicer
|
|
34 |
way than normal exceptions.
|
|
35 |
"""
|
|
36 |
category = 'Sphinx error'
|
|
37 |
|
|
38 |
class ExtensionError(SphinxError):
|
|
39 |
"""Raised if something's wrong with the configuration."""
|
|
40 |
category = 'Extension error'
|
|
41 |
|
|
42 |
def __init__(self, message, orig_exc=None):
|
|
43 |
super(ExtensionError, self).__init__(message)
|
|
44 |
self.orig_exc = orig_exc
|
|
45 |
|
|
46 |
def __repr__(self):
|
|
47 |
if self.orig_exc:
|
|
48 |
return '%s(%r, %r)' % (self.__class__.__name__,
|
|
49 |
self.message, self.orig_exc)
|
|
50 |
return '%s(%r)' % (self.__class__.__name__, self.message)
|
|
51 |
|
|
52 |
def __str__(self):
|
|
53 |
parent_str = super(ExtensionError, self).__str__()
|
|
54 |
if self.orig_exc:
|
|
55 |
return '%s (exception: %s)' % (parent_str, self.orig_exc)
|
|
56 |
return parent_str
|
|
57 |
|
|
58 |
|
|
59 |
# List of all known core events. Maps name to arguments description.
|
|
60 |
events = {
|
|
61 |
'builder-inited': '',
|
|
62 |
'env-purge-doc': 'env, docname',
|
|
63 |
'source-read': 'docname, source text',
|
|
64 |
'doctree-read': 'the doctree before being pickled',
|
|
65 |
'missing-reference': 'env, node, contnode',
|
|
66 |
'doctree-resolved': 'doctree, docname',
|
|
67 |
'env-updated': 'env',
|
|
68 |
'html-page-context': 'pagename, context, doctree or None',
|
|
69 |
'build-finished': 'exception',
|
|
70 |
}
|
|
71 |
|
|
72 |
CONFIG_FILENAME = 'conf.py'
|
|
73 |
|
|
74 |
class Sphinx(object):
|
|
75 |
|
|
76 |
def __init__(self, srcdir, confdir, outdir, doctreedir, buildername,
|
|
77 |
confoverrides, status, warning=sys.stderr, freshenv=False):
|
|
78 |
self.next_listener_id = 0
|
|
79 |
self._listeners = {}
|
|
80 |
self.builderclasses = builtin_builders.copy()
|
|
81 |
self.builder = None
|
|
82 |
|
|
83 |
self.srcdir = srcdir
|
|
84 |
self.confdir = confdir
|
|
85 |
self.outdir = outdir
|
|
86 |
self.doctreedir = doctreedir
|
|
87 |
|
|
88 |
if status is None:
|
|
89 |
self._status = StringIO()
|
|
90 |
self.quiet = True
|
|
91 |
else:
|
|
92 |
self._status = status
|
|
93 |
self.quiet = False
|
|
94 |
if warning is None:
|
|
95 |
self._warning = StringIO()
|
|
96 |
else:
|
|
97 |
self._warning = warning
|
|
98 |
self._warncount = 0
|
|
99 |
|
|
100 |
self._events = events.copy()
|
|
101 |
|
|
102 |
# status code for command-line application
|
|
103 |
self.statuscode = 0
|
|
104 |
|
|
105 |
# read config
|
|
106 |
self.config = Config(confdir, CONFIG_FILENAME, confoverrides)
|
|
107 |
|
|
108 |
# load all extension modules
|
|
109 |
for extension in self.config.extensions:
|
|
110 |
self.setup_extension(extension)
|
|
111 |
# the config file itself can be an extension
|
|
112 |
if self.config.setup:
|
|
113 |
self.config.setup(self)
|
|
114 |
|
|
115 |
# now that we know all config values, collect them from conf.py
|
|
116 |
self.config.init_values()
|
|
117 |
|
|
118 |
if buildername is None:
|
|
119 |
print >>status, 'No builder selected, using default: html'
|
|
120 |
buildername = 'html'
|
|
121 |
if buildername not in self.builderclasses:
|
|
122 |
raise SphinxError('Builder name %s not registered' % buildername)
|
|
123 |
|
|
124 |
self.info(bold('Sphinx v%s, building %s' % (sphinx.__released__,
|
|
125 |
buildername)))
|
|
126 |
|
|
127 |
builderclass = self.builderclasses[buildername]
|
|
128 |
self.builder = builderclass(self, freshenv=freshenv)
|
|
129 |
self.emit('builder-inited')
|
|
130 |
|
|
131 |
def build(self, all_files, filenames):
|
|
132 |
try:
|
|
133 |
if all_files:
|
|
134 |
self.builder.build_all()
|
|
135 |
elif filenames:
|
|
136 |
self.builder.build_specific(filenames)
|
|
137 |
else:
|
|
138 |
self.builder.build_update()
|
|
139 |
except Exception, err:
|
|
140 |
self.emit('build-finished', err)
|
|
141 |
raise
|
|
142 |
else:
|
|
143 |
self.emit('build-finished', None)
|
|
144 |
|
|
145 |
def warn(self, message):
|
|
146 |
self._warncount += 1
|
|
147 |
try:
|
|
148 |
self._warning.write('WARNING: %s\n' % message)
|
|
149 |
except UnicodeEncodeError:
|
|
150 |
encoding = getattr(self._warning, 'encoding', 'ascii')
|
|
151 |
self._warning.write(('WARNING: %s\n' % message).encode(encoding, 'replace'))
|
|
152 |
|
|
153 |
def info(self, message='', nonl=False):
|
|
154 |
try:
|
|
155 |
self._status.write(message)
|
|
156 |
except UnicodeEncodeError:
|
|
157 |
encoding = getattr(self._status, 'encoding', 'ascii')
|
|
158 |
self._status.write(message.encode(encoding, 'replace'))
|
|
159 |
if not nonl:
|
|
160 |
self._status.write('\n')
|
|
161 |
self._status.flush()
|
|
162 |
|
|
163 |
# general extensibility interface
|
|
164 |
|
|
165 |
def setup_extension(self, extension):
|
|
166 |
"""Import and setup a Sphinx extension module."""
|
|
167 |
try:
|
|
168 |
mod = __import__(extension, None, None, ['setup'])
|
|
169 |
except ImportError, err:
|
|
170 |
raise ExtensionError('Could not import extension %s' % extension, err)
|
|
171 |
if hasattr(mod, 'setup'):
|
|
172 |
mod.setup(self)
|
|
173 |
|
|
174 |
def import_object(self, objname, source=None):
|
|
175 |
"""Import an object from a 'module.name' string."""
|
|
176 |
try:
|
|
177 |
module, name = objname.rsplit('.', 1)
|
|
178 |
except ValueError, err:
|
|
179 |
raise ExtensionError('Invalid full object name %s' % objname +
|
|
180 |
(source and ' (needed for %s)' % source or ''), err)
|
|
181 |
try:
|
|
182 |
return getattr(__import__(module, None, None, [name]), name)
|
|
183 |
except ImportError, err:
|
|
184 |
raise ExtensionError('Could not import %s' % module +
|
|
185 |
(source and ' (needed for %s)' % source or ''), err)
|
|
186 |
except AttributeError, err:
|
|
187 |
raise ExtensionError('Could not find %s' % objname +
|
|
188 |
(source and ' (needed for %s)' % source or ''), err)
|
|
189 |
|
|
190 |
# event interface
|
|
191 |
|
|
192 |
def _validate_event(self, event):
|
|
193 |
event = intern(event)
|
|
194 |
if event not in self._events:
|
|
195 |
raise ExtensionError('Unknown event name: %s' % event)
|
|
196 |
|
|
197 |
def connect(self, event, callback):
|
|
198 |
self._validate_event(event)
|
|
199 |
listener_id = self.next_listener_id
|
|
200 |
if event not in self._listeners:
|
|
201 |
self._listeners[event] = {listener_id: callback}
|
|
202 |
else:
|
|
203 |
self._listeners[event][listener_id] = callback
|
|
204 |
self.next_listener_id += 1
|
|
205 |
return listener_id
|
|
206 |
|
|
207 |
def disconnect(self, listener_id):
|
|
208 |
for event in self._listeners.itervalues():
|
|
209 |
event.pop(listener_id, None)
|
|
210 |
|
|
211 |
def emit(self, event, *args):
|
|
212 |
result = []
|
|
213 |
if event in self._listeners:
|
|
214 |
for _, callback in self._listeners[event].iteritems():
|
|
215 |
result.append(callback(self, *args))
|
|
216 |
return result
|
|
217 |
|
|
218 |
def emit_firstresult(self, event, *args):
|
|
219 |
for result in self.emit(event, *args):
|
|
220 |
if result is not None:
|
|
221 |
return result
|
|
222 |
return None
|
|
223 |
|
|
224 |
# registering addon parts
|
|
225 |
|
|
226 |
def add_builder(self, builder):
|
|
227 |
if not hasattr(builder, 'name'):
|
|
228 |
raise ExtensionError('Builder class %s has no "name" attribute' % builder)
|
|
229 |
if builder.name in self.builderclasses:
|
|
230 |
raise ExtensionError('Builder %r already exists (in module %s)' % (
|
|
231 |
builder.name, self.builderclasses[builder.name].__module__))
|
|
232 |
self.builderclasses[builder.name] = builder
|
|
233 |
|
|
234 |
def add_config_value(self, name, default, rebuild_env):
|
|
235 |
if name in self.config.values:
|
|
236 |
raise ExtensionError('Config value %r already present' % name)
|
|
237 |
self.config.values[name] = (default, rebuild_env)
|
|
238 |
|
|
239 |
def add_event(self, name):
|
|
240 |
if name in self._events:
|
|
241 |
raise ExtensionError('Event %r already present' % name)
|
|
242 |
self._events[name] = ''
|
|
243 |
|
|
244 |
def add_node(self, node, **kwds):
|
|
245 |
nodes._add_node_class_names([node.__name__])
|
|
246 |
for key, val in kwds.iteritems():
|
|
247 |
try:
|
|
248 |
visit, depart = val
|
|
249 |
except ValueError:
|
|
250 |
raise ExtensionError('Value for key %r must be a (visit, depart) '
|
|
251 |
'function tuple' % key)
|
|
252 |
if key == 'html':
|
|
253 |
from sphinx.htmlwriter import HTMLTranslator as translator
|
|
254 |
elif key == 'latex':
|
|
255 |
from sphinx.latexwriter import LaTeXTranslator as translator
|
|
256 |
elif key == 'text':
|
|
257 |
from sphinx.textwriter import TextTranslator as translator
|
|
258 |
else:
|
|
259 |
# ignore invalid keys for compatibility
|
|
260 |
continue
|
|
261 |
setattr(translator, 'visit_'+node.__name__, visit)
|
|
262 |
if depart:
|
|
263 |
setattr(translator, 'depart_'+node.__name__, depart)
|
|
264 |
|
|
265 |
def add_directive(self, name, func, content, arguments, **options):
|
|
266 |
func.content = content
|
|
267 |
func.arguments = arguments
|
|
268 |
func.options = options
|
|
269 |
directives.register_directive(name, func)
|
|
270 |
|
|
271 |
def add_role(self, name, role):
|
|
272 |
roles.register_canonical_role(name, role)
|
|
273 |
|
|
274 |
def add_description_unit(self, directivename, rolename, indextemplate='',
|
|
275 |
parse_node=None, ref_nodeclass=None):
|
|
276 |
additional_xref_types[directivename] = (rolename, indextemplate, parse_node)
|
|
277 |
directives.register_directive(directivename, desc_directive)
|
|
278 |
roles.register_canonical_role(rolename, xfileref_role)
|
|
279 |
if ref_nodeclass is not None:
|
|
280 |
innernodetypes[rolename] = ref_nodeclass
|
|
281 |
|
|
282 |
def add_crossref_type(self, directivename, rolename, indextemplate='',
|
|
283 |
ref_nodeclass=None):
|
|
284 |
additional_xref_types[directivename] = (rolename, indextemplate, None)
|
|
285 |
directives.register_directive(directivename, target_directive)
|
|
286 |
roles.register_canonical_role(rolename, xfileref_role)
|
|
287 |
if ref_nodeclass is not None:
|
|
288 |
innernodetypes[rolename] = ref_nodeclass
|
|
289 |
|
|
290 |
def add_transform(self, transform):
|
|
291 |
SphinxStandaloneReader.transforms.append(transform)
|
|
292 |
|
|
293 |
def add_javascript(self, filename):
|
|
294 |
StandaloneHTMLBuilder.script_files.append(
|
|
295 |
posixpath.join('_static', filename))
|
|
296 |
|
|
297 |
|
|
298 |
class TemplateBridge(object):
|
|
299 |
"""
|
|
300 |
This class defines the interface for a "template bridge", that is, a class
|
|
301 |
that renders templates given a template name and a context.
|
|
302 |
"""
|
|
303 |
|
|
304 |
def init(self, builder):
|
|
305 |
"""
|
|
306 |
Called by the builder to initialize the template system. *builder*
|
|
307 |
is the builder object; you'll probably want to look at the value of
|
|
308 |
``builder.config.templates_path``.
|
|
309 |
"""
|
|
310 |
raise NotImplementedError('must be implemented in subclasses')
|
|
311 |
|
|
312 |
def newest_template_mtime(self):
|
|
313 |
"""
|
|
314 |
Called by the builder to determine if output files are outdated
|
|
315 |
because of template changes. Return the mtime of the newest template
|
|
316 |
file that was changed. The default implementation returns ``0``.
|
|
317 |
"""
|
|
318 |
return 0
|
|
319 |
|
|
320 |
def render(self, template, context):
|
|
321 |
"""
|
|
322 |
Called by the builder to render a *template* with a specified
|
|
323 |
context (a Python dictionary).
|
|
324 |
"""
|
|
325 |
raise NotImplementedError('must be implemented in subclasses')
|