Show More
@@ -0,0 +1,423 | |||
|
1 | """ | |
|
2 | Defines a docutils directive for inserting inheritance diagrams. | |
|
3 | ||
|
4 | Provide the directive with one or more classes or modules (separated | |
|
5 | by whitespace). For modules, all of the classes in that module will | |
|
6 | be used. | |
|
7 | ||
|
8 | Example:: | |
|
9 | ||
|
10 | Given the following classes: | |
|
11 | ||
|
12 | class A: pass | |
|
13 | class B(A): pass | |
|
14 | class C(A): pass | |
|
15 | class D(B, C): pass | |
|
16 | class E(B): pass | |
|
17 | ||
|
18 | .. inheritance-diagram: D E | |
|
19 | ||
|
20 | Produces a graph like the following: | |
|
21 | ||
|
22 | A | |
|
23 | / \ | |
|
24 | B C | |
|
25 | / \ / | |
|
26 | E D | |
|
27 | ||
|
28 | The graph is inserted as a PNG+image map into HTML and a PDF in | |
|
29 | LaTeX. | |
|
30 | """ | |
|
31 | ||
|
32 | import inspect | |
|
33 | import os | |
|
34 | import re | |
|
35 | import subprocess | |
|
36 | try: | |
|
37 | from hashlib import md5 | |
|
38 | except ImportError: | |
|
39 | from md5 import md5 | |
|
40 | ||
|
41 | from docutils.nodes import Body, Element | |
|
42 | from docutils.writers.html4css1 import HTMLTranslator | |
|
43 | from sphinx.latexwriter import LaTeXTranslator | |
|
44 | from docutils.parsers.rst import directives | |
|
45 | from sphinx.roles import xfileref_role | |
|
46 | ||
|
47 | class DotException(Exception): | |
|
48 | pass | |
|
49 | ||
|
50 | class InheritanceGraph(object): | |
|
51 | """ | |
|
52 | Given a list of classes, determines the set of classes that | |
|
53 | they inherit from all the way to the root "object", and then | |
|
54 | is able to generate a graphviz dot graph from them. | |
|
55 | """ | |
|
56 | def __init__(self, class_names, show_builtins=False): | |
|
57 | """ | |
|
58 | *class_names* is a list of child classes to show bases from. | |
|
59 | ||
|
60 | If *show_builtins* is True, then Python builtins will be shown | |
|
61 | in the graph. | |
|
62 | """ | |
|
63 | self.class_names = class_names | |
|
64 | self.classes = self._import_classes(class_names) | |
|
65 | self.all_classes = self._all_classes(self.classes) | |
|
66 | if len(self.all_classes) == 0: | |
|
67 | raise ValueError("No classes found for inheritance diagram") | |
|
68 | self.show_builtins = show_builtins | |
|
69 | ||
|
70 | py_sig_re = re.compile(r'''^([\w.]*\.)? # class names | |
|
71 | (\w+) \s* $ # optionally arguments | |
|
72 | ''', re.VERBOSE) | |
|
73 | ||
|
74 | def _import_class_or_module(self, name): | |
|
75 | """ | |
|
76 | Import a class using its fully-qualified *name*. | |
|
77 | """ | |
|
78 | try: | |
|
79 | path, base = self.py_sig_re.match(name).groups() | |
|
80 | except: | |
|
81 | raise ValueError( | |
|
82 | "Invalid class or module '%s' specified for inheritance diagram" % name) | |
|
83 | fullname = (path or '') + base | |
|
84 | path = (path and path.rstrip('.')) | |
|
85 | if not path: | |
|
86 | path = base | |
|
87 | if not path: | |
|
88 | raise ValueError( | |
|
89 | "Invalid class or module '%s' specified for inheritance diagram" % name) | |
|
90 | try: | |
|
91 | module = __import__(path, None, None, []) | |
|
92 | except ImportError: | |
|
93 | raise ValueError( | |
|
94 | "Could not import class or module '%s' specified for inheritance diagram" % name) | |
|
95 | ||
|
96 | try: | |
|
97 | todoc = module | |
|
98 | for comp in fullname.split('.')[1:]: | |
|
99 | todoc = getattr(todoc, comp) | |
|
100 | except AttributeError: | |
|
101 | raise ValueError( | |
|
102 | "Could not find class or module '%s' specified for inheritance diagram" % name) | |
|
103 | ||
|
104 | # If a class, just return it | |
|
105 | if inspect.isclass(todoc): | |
|
106 | return [todoc] | |
|
107 | elif inspect.ismodule(todoc): | |
|
108 | classes = [] | |
|
109 | for cls in todoc.__dict__.values(): | |
|
110 | if inspect.isclass(cls) and cls.__module__ == todoc.__name__: | |
|
111 | classes.append(cls) | |
|
112 | return classes | |
|
113 | raise ValueError( | |
|
114 | "'%s' does not resolve to a class or module" % name) | |
|
115 | ||
|
116 | def _import_classes(self, class_names): | |
|
117 | """ | |
|
118 | Import a list of classes. | |
|
119 | """ | |
|
120 | classes = [] | |
|
121 | for name in class_names: | |
|
122 | classes.extend(self._import_class_or_module(name)) | |
|
123 | return classes | |
|
124 | ||
|
125 | def _all_classes(self, classes): | |
|
126 | """ | |
|
127 | Return a list of all classes that are ancestors of *classes*. | |
|
128 | """ | |
|
129 | all_classes = {} | |
|
130 | ||
|
131 | def recurse(cls): | |
|
132 | all_classes[cls] = None | |
|
133 | for c in cls.__bases__: | |
|
134 | if c not in all_classes: | |
|
135 | recurse(c) | |
|
136 | ||
|
137 | for cls in classes: | |
|
138 | recurse(cls) | |
|
139 | ||
|
140 | return all_classes.keys() | |
|
141 | ||
|
142 | def class_name(self, cls, parts=0): | |
|
143 | """ | |
|
144 | Given a class object, return a fully-qualified name. This | |
|
145 | works for things I've tested in matplotlib so far, but may not | |
|
146 | be completely general. | |
|
147 | """ | |
|
148 | module = cls.__module__ | |
|
149 | if module == '__builtin__': | |
|
150 | fullname = cls.__name__ | |
|
151 | else: | |
|
152 | fullname = "%s.%s" % (module, cls.__name__) | |
|
153 | if parts == 0: | |
|
154 | return fullname | |
|
155 | name_parts = fullname.split('.') | |
|
156 | return '.'.join(name_parts[-parts:]) | |
|
157 | ||
|
158 | def get_all_class_names(self): | |
|
159 | """ | |
|
160 | Get all of the class names involved in the graph. | |
|
161 | """ | |
|
162 | return [self.class_name(x) for x in self.all_classes] | |
|
163 | ||
|
164 | # These are the default options for graphviz | |
|
165 | default_graph_options = { | |
|
166 | "rankdir": "LR", | |
|
167 | "size": '"8.0, 12.0"' | |
|
168 | } | |
|
169 | default_node_options = { | |
|
170 | "shape": "box", | |
|
171 | "fontsize": 10, | |
|
172 | "height": 0.25, | |
|
173 | "fontname": "Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans", | |
|
174 | "style": '"setlinewidth(0.5)"' | |
|
175 | } | |
|
176 | default_edge_options = { | |
|
177 | "arrowsize": 0.5, | |
|
178 | "style": '"setlinewidth(0.5)"' | |
|
179 | } | |
|
180 | ||
|
181 | def _format_node_options(self, options): | |
|
182 | return ','.join(["%s=%s" % x for x in options.items()]) | |
|
183 | def _format_graph_options(self, options): | |
|
184 | return ''.join(["%s=%s;\n" % x for x in options.items()]) | |
|
185 | ||
|
186 | def generate_dot(self, fd, name, parts=0, urls={}, | |
|
187 | graph_options={}, node_options={}, | |
|
188 | edge_options={}): | |
|
189 | """ | |
|
190 | Generate a graphviz dot graph from the classes that | |
|
191 | were passed in to __init__. | |
|
192 | ||
|
193 | *fd* is a Python file-like object to write to. | |
|
194 | ||
|
195 | *name* is the name of the graph | |
|
196 | ||
|
197 | *urls* is a dictionary mapping class names to http urls | |
|
198 | ||
|
199 | *graph_options*, *node_options*, *edge_options* are | |
|
200 | dictionaries containing key/value pairs to pass on as graphviz | |
|
201 | properties. | |
|
202 | """ | |
|
203 | g_options = self.default_graph_options.copy() | |
|
204 | g_options.update(graph_options) | |
|
205 | n_options = self.default_node_options.copy() | |
|
206 | n_options.update(node_options) | |
|
207 | e_options = self.default_edge_options.copy() | |
|
208 | e_options.update(edge_options) | |
|
209 | ||
|
210 | fd.write('digraph %s {\n' % name) | |
|
211 | fd.write(self._format_graph_options(g_options)) | |
|
212 | ||
|
213 | for cls in self.all_classes: | |
|
214 | if not self.show_builtins and cls in __builtins__.values(): | |
|
215 | continue | |
|
216 | ||
|
217 | name = self.class_name(cls, parts) | |
|
218 | ||
|
219 | # Write the node | |
|
220 | this_node_options = n_options.copy() | |
|
221 | url = urls.get(self.class_name(cls)) | |
|
222 | if url is not None: | |
|
223 | this_node_options['URL'] = '"%s"' % url | |
|
224 | fd.write(' "%s" [%s];\n' % | |
|
225 | (name, self._format_node_options(this_node_options))) | |
|
226 | ||
|
227 | # Write the edges | |
|
228 | for base in cls.__bases__: | |
|
229 | if not self.show_builtins and base in __builtins__.values(): | |
|
230 | continue | |
|
231 | ||
|
232 | base_name = self.class_name(base, parts) | |
|
233 | fd.write(' "%s" -> "%s" [%s];\n' % | |
|
234 | (base_name, name, | |
|
235 | self._format_node_options(e_options))) | |
|
236 | fd.write('}\n') | |
|
237 | ||
|
238 | def run_dot(self, args, name, parts=0, urls={}, | |
|
239 | graph_options={}, node_options={}, edge_options={}): | |
|
240 | """ | |
|
241 | Run graphviz 'dot' over this graph, returning whatever 'dot' | |
|
242 | writes to stdout. | |
|
243 | ||
|
244 | *args* will be passed along as commandline arguments. | |
|
245 | ||
|
246 | *name* is the name of the graph | |
|
247 | ||
|
248 | *urls* is a dictionary mapping class names to http urls | |
|
249 | ||
|
250 | Raises DotException for any of the many os and | |
|
251 | installation-related errors that may occur. | |
|
252 | """ | |
|
253 | try: | |
|
254 | dot = subprocess.Popen(['dot'] + list(args), | |
|
255 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, | |
|
256 | close_fds=True) | |
|
257 | except OSError: | |
|
258 | raise DotException("Could not execute 'dot'. Are you sure you have 'graphviz' installed?") | |
|
259 | except ValueError: | |
|
260 | raise DotException("'dot' called with invalid arguments") | |
|
261 | except: | |
|
262 | raise DotException("Unexpected error calling 'dot'") | |
|
263 | ||
|
264 | self.generate_dot(dot.stdin, name, parts, urls, graph_options, | |
|
265 | node_options, edge_options) | |
|
266 | dot.stdin.close() | |
|
267 | result = dot.stdout.read() | |
|
268 | returncode = dot.wait() | |
|
269 | if returncode != 0: | |
|
270 | raise DotException("'dot' returned the errorcode %d" % returncode) | |
|
271 | return result | |
|
272 | ||
|
273 | class inheritance_diagram(Body, Element): | |
|
274 | """ | |
|
275 | A docutils node to use as a placeholder for the inheritance | |
|
276 | diagram. | |
|
277 | """ | |
|
278 | pass | |
|
279 | ||
|
280 | def inheritance_diagram_directive_run(class_names, options, state): | |
|
281 | """ | |
|
282 | Run when the inheritance_diagram directive is first encountered. | |
|
283 | """ | |
|
284 | node = inheritance_diagram() | |
|
285 | ||
|
286 | # Create a graph starting with the list of classes | |
|
287 | graph = InheritanceGraph(class_names) | |
|
288 | ||
|
289 | # Create xref nodes for each target of the graph's image map and | |
|
290 | # add them to the doc tree so that Sphinx can resolve the | |
|
291 | # references to real URLs later. These nodes will eventually be | |
|
292 | # removed from the doctree after we're done with them. | |
|
293 | for name in graph.get_all_class_names(): | |
|
294 | refnodes, x = xfileref_role( | |
|
295 | 'class', ':class:`%s`' % name, name, 0, state) | |
|
296 | node.extend(refnodes) | |
|
297 | # Store the graph object so we can use it to generate the | |
|
298 | # dot file later | |
|
299 | node['graph'] = graph | |
|
300 | # Store the original content for use as a hash | |
|
301 | node['parts'] = options.get('parts', 0) | |
|
302 | node['content'] = " ".join(class_names) | |
|
303 | return [node] | |
|
304 | ||
|
305 | def get_graph_hash(node): | |
|
306 | return md5(node['content'] + str(node['parts'])).hexdigest()[-10:] | |
|
307 | ||
|
308 | def html_output_graph(self, node): | |
|
309 | """ | |
|
310 | Output the graph for HTML. This will insert a PNG with clickable | |
|
311 | image map. | |
|
312 | """ | |
|
313 | graph = node['graph'] | |
|
314 | parts = node['parts'] | |
|
315 | ||
|
316 | graph_hash = get_graph_hash(node) | |
|
317 | name = "inheritance%s" % graph_hash | |
|
318 | png_path = os.path.join('_static', name + ".png") | |
|
319 | ||
|
320 | path = '_static' | |
|
321 | source = self.document.attributes['source'] | |
|
322 | count = source.split('/doc/')[-1].count('/') | |
|
323 | for i in range(count): | |
|
324 | if os.path.exists(path): break | |
|
325 | path = '../'+path | |
|
326 | path = '../'+path #specifically added for matplotlib | |
|
327 | ||
|
328 | # Create a mapping from fully-qualified class names to URLs. | |
|
329 | urls = {} | |
|
330 | for child in node: | |
|
331 | if child.get('refuri') is not None: | |
|
332 | urls[child['reftitle']] = child.get('refuri') | |
|
333 | elif child.get('refid') is not None: | |
|
334 | urls[child['reftitle']] = '#' + child.get('refid') | |
|
335 | ||
|
336 | # These arguments to dot will save a PNG file to disk and write | |
|
337 | # an HTML image map to stdout. | |
|
338 | image_map = graph.run_dot(['-Tpng', '-o%s' % png_path, '-Tcmapx'], | |
|
339 | name, parts, urls) | |
|
340 | return ('<img src="%s/%s.png" usemap="#%s" class="inheritance"/>%s' % | |
|
341 | (path, name, name, image_map)) | |
|
342 | ||
|
343 | def latex_output_graph(self, node): | |
|
344 | """ | |
|
345 | Output the graph for LaTeX. This will insert a PDF. | |
|
346 | """ | |
|
347 | graph = node['graph'] | |
|
348 | parts = node['parts'] | |
|
349 | ||
|
350 | graph_hash = get_graph_hash(node) | |
|
351 | name = "inheritance%s" % graph_hash | |
|
352 | pdf_path = os.path.join('_static', name + ".pdf") | |
|
353 | ||
|
354 | graph.run_dot(['-Tpdf', '-o%s' % pdf_path], | |
|
355 | name, parts, graph_options={'size': '"6.0,6.0"'}) | |
|
356 | return '\\includegraphics{../../%s}' % pdf_path | |
|
357 | ||
|
358 | def visit_inheritance_diagram(inner_func): | |
|
359 | """ | |
|
360 | This is just a wrapper around html/latex_output_graph to make it | |
|
361 | easier to handle errors and insert warnings. | |
|
362 | """ | |
|
363 | def visitor(self, node): | |
|
364 | try: | |
|
365 | content = inner_func(self, node) | |
|
366 | except DotException, e: | |
|
367 | # Insert the exception as a warning in the document | |
|
368 | warning = self.document.reporter.warning(str(e), line=node.line) | |
|
369 | warning.parent = node | |
|
370 | node.children = [warning] | |
|
371 | else: | |
|
372 | source = self.document.attributes['source'] | |
|
373 | self.body.append(content) | |
|
374 | node.children = [] | |
|
375 | return visitor | |
|
376 | ||
|
377 | def do_nothing(self, node): | |
|
378 | pass | |
|
379 | ||
|
380 | options_spec = { | |
|
381 | 'parts': directives.nonnegative_int | |
|
382 | } | |
|
383 | ||
|
384 | # Deal with the old and new way of registering directives | |
|
385 | try: | |
|
386 | from docutils.parsers.rst import Directive | |
|
387 | except ImportError: | |
|
388 | from docutils.parsers.rst.directives import _directives | |
|
389 | def inheritance_diagram_directive(name, arguments, options, content, lineno, | |
|
390 | content_offset, block_text, state, | |
|
391 | state_machine): | |
|
392 | return inheritance_diagram_directive_run(arguments, options, state) | |
|
393 | inheritance_diagram_directive.__doc__ = __doc__ | |
|
394 | inheritance_diagram_directive.arguments = (1, 100, 0) | |
|
395 | inheritance_diagram_directive.options = options_spec | |
|
396 | inheritance_diagram_directive.content = 0 | |
|
397 | _directives['inheritance-diagram'] = inheritance_diagram_directive | |
|
398 | else: | |
|
399 | class inheritance_diagram_directive(Directive): | |
|
400 | has_content = False | |
|
401 | required_arguments = 1 | |
|
402 | optional_arguments = 100 | |
|
403 | final_argument_whitespace = False | |
|
404 | option_spec = options_spec | |
|
405 | ||
|
406 | def run(self): | |
|
407 | return inheritance_diagram_directive_run( | |
|
408 | self.arguments, self.options, self.state) | |
|
409 | inheritance_diagram_directive.__doc__ = __doc__ | |
|
410 | ||
|
411 | directives.register_directive('inheritance-diagram', | |
|
412 | inheritance_diagram_directive) | |
|
413 | ||
|
414 | def setup(app): | |
|
415 | app.add_node(inheritance_diagram) | |
|
416 | ||
|
417 | HTMLTranslator.visit_inheritance_diagram = \ | |
|
418 | visit_inheritance_diagram(html_output_graph) | |
|
419 | HTMLTranslator.depart_inheritance_diagram = do_nothing | |
|
420 | ||
|
421 | LaTeXTranslator.visit_inheritance_diagram = \ | |
|
422 | visit_inheritance_diagram(latex_output_graph) | |
|
423 | LaTeXTranslator.depart_inheritance_diagram = do_nothing |
@@ -0,0 +1,75 | |||
|
1 | from pygments.lexer import Lexer, do_insertions | |
|
2 | from pygments.lexers.agile import PythonConsoleLexer, PythonLexer, \ | |
|
3 | PythonTracebackLexer | |
|
4 | from pygments.token import Comment, Generic | |
|
5 | from sphinx import highlighting | |
|
6 | import re | |
|
7 | ||
|
8 | line_re = re.compile('.*?\n') | |
|
9 | ||
|
10 | class IPythonConsoleLexer(Lexer): | |
|
11 | """ | |
|
12 | For IPython console output or doctests, such as: | |
|
13 | ||
|
14 | Tracebacks are not currently supported. | |
|
15 | ||
|
16 | .. sourcecode:: ipython | |
|
17 | ||
|
18 | In [1]: a = 'foo' | |
|
19 | ||
|
20 | In [2]: a | |
|
21 | Out[2]: 'foo' | |
|
22 | ||
|
23 | In [3]: print a | |
|
24 | foo | |
|
25 | ||
|
26 | In [4]: 1 / 0 | |
|
27 | """ | |
|
28 | name = 'IPython console session' | |
|
29 | aliases = ['ipython'] | |
|
30 | mimetypes = ['text/x-ipython-console'] | |
|
31 | input_prompt = re.compile("(In \[[0-9]+\]: )|( \.\.\.+:)") | |
|
32 | output_prompt = re.compile("(Out\[[0-9]+\]: )|( \.\.\.+:)") | |
|
33 | continue_prompt = re.compile(" \.\.\.+:") | |
|
34 | tb_start = re.compile("\-+") | |
|
35 | ||
|
36 | def get_tokens_unprocessed(self, text): | |
|
37 | pylexer = PythonLexer(**self.options) | |
|
38 | tblexer = PythonTracebackLexer(**self.options) | |
|
39 | ||
|
40 | curcode = '' | |
|
41 | insertions = [] | |
|
42 | for match in line_re.finditer(text): | |
|
43 | line = match.group() | |
|
44 | input_prompt = self.input_prompt.match(line) | |
|
45 | continue_prompt = self.continue_prompt.match(line.rstrip()) | |
|
46 | output_prompt = self.output_prompt.match(line) | |
|
47 | if line.startswith("#"): | |
|
48 | insertions.append((len(curcode), | |
|
49 | [(0, Comment, line)])) | |
|
50 | elif input_prompt is not None: | |
|
51 | insertions.append((len(curcode), | |
|
52 | [(0, Generic.Prompt, input_prompt.group())])) | |
|
53 | curcode += line[input_prompt.end():] | |
|
54 | elif continue_prompt is not None: | |
|
55 | insertions.append((len(curcode), | |
|
56 | [(0, Generic.Prompt, continue_prompt.group())])) | |
|
57 | curcode += line[continue_prompt.end():] | |
|
58 | elif output_prompt is not None: | |
|
59 | insertions.append((len(curcode), | |
|
60 | [(0, Generic.Output, output_prompt.group())])) | |
|
61 | curcode += line[output_prompt.end():] | |
|
62 | else: | |
|
63 | if curcode: | |
|
64 | for item in do_insertions(insertions, | |
|
65 | pylexer.get_tokens_unprocessed(curcode)): | |
|
66 | yield item | |
|
67 | curcode = '' | |
|
68 | insertions = [] | |
|
69 | yield match.start(), Generic.Output, line | |
|
70 | if curcode: | |
|
71 | for item in do_insertions(insertions, | |
|
72 | pylexer.get_tokens_unprocessed(curcode)): | |
|
73 | yield item | |
|
74 | ||
|
75 | highlighting.lexers['ipython'] = IPythonConsoleLexer() |
@@ -0,0 +1,87 | |||
|
1 | # | |
|
2 | # A pair of directives for inserting content that will only appear in | |
|
3 | # either html or latex. | |
|
4 | # | |
|
5 | ||
|
6 | from docutils.nodes import Body, Element | |
|
7 | from docutils.writers.html4css1 import HTMLTranslator | |
|
8 | from sphinx.latexwriter import LaTeXTranslator | |
|
9 | from docutils.parsers.rst import directives | |
|
10 | ||
|
11 | class html_only(Body, Element): | |
|
12 | pass | |
|
13 | ||
|
14 | class latex_only(Body, Element): | |
|
15 | pass | |
|
16 | ||
|
17 | def run(content, node_class, state, content_offset): | |
|
18 | text = '\n'.join(content) | |
|
19 | node = node_class(text) | |
|
20 | state.nested_parse(content, content_offset, node) | |
|
21 | return [node] | |
|
22 | ||
|
23 | try: | |
|
24 | from docutils.parsers.rst import Directive | |
|
25 | except ImportError: | |
|
26 | from docutils.parsers.rst.directives import _directives | |
|
27 | ||
|
28 | def html_only_directive(name, arguments, options, content, lineno, | |
|
29 | content_offset, block_text, state, state_machine): | |
|
30 | return run(content, html_only, state, content_offset) | |
|
31 | ||
|
32 | def latex_only_directive(name, arguments, options, content, lineno, | |
|
33 | content_offset, block_text, state, state_machine): | |
|
34 | return run(content, latex_only, state, content_offset) | |
|
35 | ||
|
36 | for func in (html_only_directive, latex_only_directive): | |
|
37 | func.content = 1 | |
|
38 | func.options = {} | |
|
39 | func.arguments = None | |
|
40 | ||
|
41 | _directives['htmlonly'] = html_only_directive | |
|
42 | _directives['latexonly'] = latex_only_directive | |
|
43 | else: | |
|
44 | class OnlyDirective(Directive): | |
|
45 | has_content = True | |
|
46 | required_arguments = 0 | |
|
47 | optional_arguments = 0 | |
|
48 | final_argument_whitespace = True | |
|
49 | option_spec = {} | |
|
50 | ||
|
51 | def run(self): | |
|
52 | self.assert_has_content() | |
|
53 | return run(self.content, self.node_class, | |
|
54 | self.state, self.content_offset) | |
|
55 | ||
|
56 | class HtmlOnlyDirective(OnlyDirective): | |
|
57 | node_class = html_only | |
|
58 | ||
|
59 | class LatexOnlyDirective(OnlyDirective): | |
|
60 | node_class = latex_only | |
|
61 | ||
|
62 | directives.register_directive('htmlonly', HtmlOnlyDirective) | |
|
63 | directives.register_directive('latexonly', LatexOnlyDirective) | |
|
64 | ||
|
65 | def setup(app): | |
|
66 | app.add_node(html_only) | |
|
67 | app.add_node(latex_only) | |
|
68 | ||
|
69 | # Add visit/depart methods to HTML-Translator: | |
|
70 | def visit_perform(self, node): | |
|
71 | pass | |
|
72 | def depart_perform(self, node): | |
|
73 | pass | |
|
74 | def visit_ignore(self, node): | |
|
75 | node.children = [] | |
|
76 | def depart_ignore(self, node): | |
|
77 | node.children = [] | |
|
78 | ||
|
79 | HTMLTranslator.visit_html_only = visit_perform | |
|
80 | HTMLTranslator.depart_html_only = depart_perform | |
|
81 | HTMLTranslator.visit_latex_only = visit_ignore | |
|
82 | HTMLTranslator.depart_latex_only = depart_ignore | |
|
83 | ||
|
84 | LaTeXTranslator.visit_html_only = visit_ignore | |
|
85 | LaTeXTranslator.depart_html_only = depart_ignore | |
|
86 | LaTeXTranslator.visit_latex_only = visit_perform | |
|
87 | LaTeXTranslator.depart_latex_only = depart_perform |
@@ -0,0 +1,155 | |||
|
1 | """A special directive for including a matplotlib plot. | |
|
2 | ||
|
3 | Given a path to a .py file, it includes the source code inline, then: | |
|
4 | ||
|
5 | - On HTML, will include a .png with a link to a high-res .png. | |
|
6 | ||
|
7 | - On LaTeX, will include a .pdf | |
|
8 | ||
|
9 | This directive supports all of the options of the `image` directive, | |
|
10 | except for `target` (since plot will add its own target). | |
|
11 | ||
|
12 | Additionally, if the :include-source: option is provided, the literal | |
|
13 | source will be included inline, as well as a link to the source. | |
|
14 | """ | |
|
15 | ||
|
16 | import sys, os, glob, shutil | |
|
17 | from docutils.parsers.rst import directives | |
|
18 | ||
|
19 | try: | |
|
20 | # docutils 0.4 | |
|
21 | from docutils.parsers.rst.directives.images import align | |
|
22 | except ImportError: | |
|
23 | # docutils 0.5 | |
|
24 | from docutils.parsers.rst.directives.images import Image | |
|
25 | align = Image.align | |
|
26 | ||
|
27 | ||
|
28 | import matplotlib | |
|
29 | import IPython.Shell | |
|
30 | matplotlib.use('Agg') | |
|
31 | import matplotlib.pyplot as plt | |
|
32 | ||
|
33 | mplshell = IPython.Shell.MatplotlibShell('mpl') | |
|
34 | ||
|
35 | options = {'alt': directives.unchanged, | |
|
36 | 'height': directives.length_or_unitless, | |
|
37 | 'width': directives.length_or_percentage_or_unitless, | |
|
38 | 'scale': directives.nonnegative_int, | |
|
39 | 'align': align, | |
|
40 | 'class': directives.class_option, | |
|
41 | 'include-source': directives.flag } | |
|
42 | ||
|
43 | template = """ | |
|
44 | .. htmlonly:: | |
|
45 | ||
|
46 | [`source code <../%(srcdir)s/%(basename)s.py>`__, | |
|
47 | `png <../%(srcdir)s/%(basename)s.hires.png>`__, | |
|
48 | `pdf <../%(srcdir)s/%(basename)s.pdf>`__] | |
|
49 | ||
|
50 | .. image:: ../%(srcdir)s/%(basename)s.png | |
|
51 | %(options)s | |
|
52 | ||
|
53 | .. latexonly:: | |
|
54 | .. image:: ../%(srcdir)s/%(basename)s.pdf | |
|
55 | %(options)s | |
|
56 | ||
|
57 | """ | |
|
58 | ||
|
59 | def makefig(fullpath, outdir): | |
|
60 | """ | |
|
61 | run a pyplot script and save the low and high res PNGs and a PDF in _static | |
|
62 | """ | |
|
63 | ||
|
64 | fullpath = str(fullpath) # todo, why is unicode breaking this | |
|
65 | formats = [('png', 100), | |
|
66 | ('hires.png', 200), | |
|
67 | ('pdf', 72), | |
|
68 | ] | |
|
69 | ||
|
70 | basedir, fname = os.path.split(fullpath) | |
|
71 | basename, ext = os.path.splitext(fname) | |
|
72 | all_exists = True | |
|
73 | ||
|
74 | if basedir != outdir: | |
|
75 | shutil.copyfile(fullpath, os.path.join(outdir, fname)) | |
|
76 | ||
|
77 | for format, dpi in formats: | |
|
78 | outname = os.path.join(outdir, '%s.%s' % (basename, format)) | |
|
79 | if not os.path.exists(outname): | |
|
80 | all_exists = False | |
|
81 | break | |
|
82 | ||
|
83 | if all_exists: | |
|
84 | print ' already have %s'%fullpath | |
|
85 | return | |
|
86 | ||
|
87 | print ' building %s'%fullpath | |
|
88 | plt.close('all') # we need to clear between runs | |
|
89 | matplotlib.rcdefaults() | |
|
90 | ||
|
91 | mplshell.magic_run(fullpath) | |
|
92 | for format, dpi in formats: | |
|
93 | outname = os.path.join(outdir, '%s.%s' % (basename, format)) | |
|
94 | if os.path.exists(outname): continue | |
|
95 | plt.savefig(outname, dpi=dpi) | |
|
96 | ||
|
97 | def run(arguments, options, state_machine, lineno): | |
|
98 | reference = directives.uri(arguments[0]) | |
|
99 | basedir, fname = os.path.split(reference) | |
|
100 | basename, ext = os.path.splitext(fname) | |
|
101 | ||
|
102 | # todo - should we be using the _static dir for the outdir, I am | |
|
103 | # not sure we want to corrupt that dir with autogenerated files | |
|
104 | # since it also has permanent files in it which makes it difficult | |
|
105 | # to clean (save an rm -rf followed by and svn up) | |
|
106 | srcdir = 'pyplots' | |
|
107 | ||
|
108 | makefig(os.path.join(srcdir, reference), srcdir) | |
|
109 | ||
|
110 | # todo: it is not great design to assume the makefile is putting | |
|
111 | # the figs into the right place, so we may want to do that here instead. | |
|
112 | ||
|
113 | if options.has_key('include-source'): | |
|
114 | lines = ['.. literalinclude:: ../pyplots/%(reference)s' % locals()] | |
|
115 | del options['include-source'] | |
|
116 | else: | |
|
117 | lines = [] | |
|
118 | ||
|
119 | options = [' :%s: %s' % (key, val) for key, val in | |
|
120 | options.items()] | |
|
121 | options = "\n".join(options) | |
|
122 | ||
|
123 | lines.extend((template % locals()).split('\n')) | |
|
124 | ||
|
125 | state_machine.insert_input( | |
|
126 | lines, state_machine.input_lines.source(0)) | |
|
127 | return [] | |
|
128 | ||
|
129 | ||
|
130 | try: | |
|
131 | from docutils.parsers.rst import Directive | |
|
132 | except ImportError: | |
|
133 | from docutils.parsers.rst.directives import _directives | |
|
134 | ||
|
135 | def plot_directive(name, arguments, options, content, lineno, | |
|
136 | content_offset, block_text, state, state_machine): | |
|
137 | return run(arguments, options, state_machine, lineno) | |
|
138 | plot_directive.__doc__ = __doc__ | |
|
139 | plot_directive.arguments = (1, 0, 1) | |
|
140 | plot_directive.options = options | |
|
141 | ||
|
142 | _directives['plot'] = plot_directive | |
|
143 | else: | |
|
144 | class plot_directive(Directive): | |
|
145 | required_arguments = 1 | |
|
146 | optional_arguments = 0 | |
|
147 | final_argument_whitespace = True | |
|
148 | option_spec = options | |
|
149 | def run(self): | |
|
150 | return run(self.arguments, self.options, | |
|
151 | self.state_machine, self.lineno) | |
|
152 | plot_directive.__doc__ = __doc__ | |
|
153 | ||
|
154 | directives.register_directive('plot', plot_directive) | |
|
155 |
General Comments 0
You need to be logged in to leave comments.
Login now