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