|
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') |