diff --git a/docs/sphinxext/inheritance_diagram.py b/docs/sphinxext/inheritance_diagram.py new file mode 100644 index 0000000..213abf8 --- /dev/null +++ b/docs/sphinxext/inheritance_diagram.py @@ -0,0 +1,423 @@ +""" +Defines a docutils directive for inserting inheritance diagrams. + +Provide the directive with one or more classes or modules (separated +by whitespace). For modules, all of the classes in that module will +be used. + +Example:: + + Given the following classes: + + class A: pass + class B(A): pass + class C(A): pass + class D(B, C): pass + class E(B): pass + + .. inheritance-diagram: D E + + Produces a graph like the following: + + A + / \ + B C + / \ / + E D + +The graph is inserted as a PNG+image map into HTML and a PDF in +LaTeX. +""" + +import inspect +import os +import re +import subprocess +try: + from hashlib import md5 +except ImportError: + from md5 import md5 + +from docutils.nodes import Body, Element +from docutils.writers.html4css1 import HTMLTranslator +from sphinx.latexwriter import LaTeXTranslator +from docutils.parsers.rst import directives +from sphinx.roles import xfileref_role + +class DotException(Exception): + pass + +class InheritanceGraph(object): + """ + Given a list of classes, determines the set of classes that + they inherit from all the way to the root "object", and then + is able to generate a graphviz dot graph from them. + """ + def __init__(self, class_names, show_builtins=False): + """ + *class_names* is a list of child classes to show bases from. + + If *show_builtins* is True, then Python builtins will be shown + in the graph. + """ + self.class_names = class_names + self.classes = self._import_classes(class_names) + self.all_classes = self._all_classes(self.classes) + if len(self.all_classes) == 0: + raise ValueError("No classes found for inheritance diagram") + self.show_builtins = show_builtins + + py_sig_re = re.compile(r'''^([\w.]*\.)? # class names + (\w+) \s* $ # optionally arguments + ''', re.VERBOSE) + + def _import_class_or_module(self, name): + """ + Import a class using its fully-qualified *name*. + """ + try: + path, base = self.py_sig_re.match(name).groups() + except: + raise ValueError( + "Invalid class or module '%s' specified for inheritance diagram" % name) + fullname = (path or '') + base + path = (path and path.rstrip('.')) + if not path: + path = base + if not path: + raise ValueError( + "Invalid class or module '%s' specified for inheritance diagram" % name) + try: + module = __import__(path, None, None, []) + except ImportError: + raise ValueError( + "Could not import class or module '%s' specified for inheritance diagram" % name) + + try: + todoc = module + for comp in fullname.split('.')[1:]: + todoc = getattr(todoc, comp) + except AttributeError: + raise ValueError( + "Could not find class or module '%s' specified for inheritance diagram" % name) + + # If a class, just return it + if inspect.isclass(todoc): + return [todoc] + elif inspect.ismodule(todoc): + classes = [] + for cls in todoc.__dict__.values(): + if inspect.isclass(cls) and cls.__module__ == todoc.__name__: + classes.append(cls) + return classes + raise ValueError( + "'%s' does not resolve to a class or module" % name) + + def _import_classes(self, class_names): + """ + Import a list of classes. + """ + classes = [] + for name in class_names: + classes.extend(self._import_class_or_module(name)) + return classes + + def _all_classes(self, classes): + """ + Return a list of all classes that are ancestors of *classes*. + """ + all_classes = {} + + def recurse(cls): + all_classes[cls] = None + for c in cls.__bases__: + if c not in all_classes: + recurse(c) + + for cls in classes: + recurse(cls) + + return all_classes.keys() + + def class_name(self, cls, parts=0): + """ + Given a class object, return a fully-qualified name. This + works for things I've tested in matplotlib so far, but may not + be completely general. + """ + module = cls.__module__ + if module == '__builtin__': + fullname = cls.__name__ + else: + fullname = "%s.%s" % (module, cls.__name__) + if parts == 0: + return fullname + name_parts = fullname.split('.') + return '.'.join(name_parts[-parts:]) + + def get_all_class_names(self): + """ + Get all of the class names involved in the graph. + """ + return [self.class_name(x) for x in self.all_classes] + + # These are the default options for graphviz + default_graph_options = { + "rankdir": "LR", + "size": '"8.0, 12.0"' + } + default_node_options = { + "shape": "box", + "fontsize": 10, + "height": 0.25, + "fontname": "Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans", + "style": '"setlinewidth(0.5)"' + } + default_edge_options = { + "arrowsize": 0.5, + "style": '"setlinewidth(0.5)"' + } + + def _format_node_options(self, options): + return ','.join(["%s=%s" % x for x in options.items()]) + def _format_graph_options(self, options): + return ''.join(["%s=%s;\n" % x for x in options.items()]) + + def generate_dot(self, fd, name, parts=0, urls={}, + graph_options={}, node_options={}, + edge_options={}): + """ + Generate a graphviz dot graph from the classes that + were passed in to __init__. + + *fd* is a Python file-like object to write to. + + *name* is the name of the graph + + *urls* is a dictionary mapping class names to http urls + + *graph_options*, *node_options*, *edge_options* are + dictionaries containing key/value pairs to pass on as graphviz + properties. + """ + g_options = self.default_graph_options.copy() + g_options.update(graph_options) + n_options = self.default_node_options.copy() + n_options.update(node_options) + e_options = self.default_edge_options.copy() + e_options.update(edge_options) + + fd.write('digraph %s {\n' % name) + fd.write(self._format_graph_options(g_options)) + + for cls in self.all_classes: + if not self.show_builtins and cls in __builtins__.values(): + continue + + name = self.class_name(cls, parts) + + # Write the node + this_node_options = n_options.copy() + url = urls.get(self.class_name(cls)) + if url is not None: + this_node_options['URL'] = '"%s"' % url + fd.write(' "%s" [%s];\n' % + (name, self._format_node_options(this_node_options))) + + # Write the edges + for base in cls.__bases__: + if not self.show_builtins and base in __builtins__.values(): + continue + + base_name = self.class_name(base, parts) + fd.write(' "%s" -> "%s" [%s];\n' % + (base_name, name, + self._format_node_options(e_options))) + fd.write('}\n') + + def run_dot(self, args, name, parts=0, urls={}, + graph_options={}, node_options={}, edge_options={}): + """ + Run graphviz 'dot' over this graph, returning whatever 'dot' + writes to stdout. + + *args* will be passed along as commandline arguments. + + *name* is the name of the graph + + *urls* is a dictionary mapping class names to http urls + + Raises DotException for any of the many os and + installation-related errors that may occur. + """ + try: + dot = subprocess.Popen(['dot'] + list(args), + stdin=subprocess.PIPE, stdout=subprocess.PIPE, + close_fds=True) + except OSError: + raise DotException("Could not execute 'dot'. Are you sure you have 'graphviz' installed?") + except ValueError: + raise DotException("'dot' called with invalid arguments") + except: + raise DotException("Unexpected error calling 'dot'") + + self.generate_dot(dot.stdin, name, parts, urls, graph_options, + node_options, edge_options) + dot.stdin.close() + result = dot.stdout.read() + returncode = dot.wait() + if returncode != 0: + raise DotException("'dot' returned the errorcode %d" % returncode) + return result + +class inheritance_diagram(Body, Element): + """ + A docutils node to use as a placeholder for the inheritance + diagram. + """ + pass + +def inheritance_diagram_directive_run(class_names, options, state): + """ + Run when the inheritance_diagram directive is first encountered. + """ + node = inheritance_diagram() + + # Create a graph starting with the list of classes + graph = InheritanceGraph(class_names) + + # Create xref nodes for each target of the graph's image map and + # add them to the doc tree so that Sphinx can resolve the + # references to real URLs later. These nodes will eventually be + # removed from the doctree after we're done with them. + for name in graph.get_all_class_names(): + refnodes, x = xfileref_role( + 'class', ':class:`%s`' % name, name, 0, state) + node.extend(refnodes) + # Store the graph object so we can use it to generate the + # dot file later + node['graph'] = graph + # Store the original content for use as a hash + node['parts'] = options.get('parts', 0) + node['content'] = " ".join(class_names) + return [node] + +def get_graph_hash(node): + return md5(node['content'] + str(node['parts'])).hexdigest()[-10:] + +def html_output_graph(self, node): + """ + Output the graph for HTML. This will insert a PNG with clickable + image map. + """ + graph = node['graph'] + parts = node['parts'] + + graph_hash = get_graph_hash(node) + name = "inheritance%s" % graph_hash + png_path = os.path.join('_static', name + ".png") + + path = '_static' + source = self.document.attributes['source'] + count = source.split('/doc/')[-1].count('/') + for i in range(count): + if os.path.exists(path): break + path = '../'+path + path = '../'+path #specifically added for matplotlib + + # Create a mapping from fully-qualified class names to URLs. + urls = {} + for child in node: + if child.get('refuri') is not None: + urls[child['reftitle']] = child.get('refuri') + elif child.get('refid') is not None: + urls[child['reftitle']] = '#' + child.get('refid') + + # These arguments to dot will save a PNG file to disk and write + # an HTML image map to stdout. + image_map = graph.run_dot(['-Tpng', '-o%s' % png_path, '-Tcmapx'], + name, parts, urls) + return ('%s' % + (path, name, name, image_map)) + +def latex_output_graph(self, node): + """ + Output the graph for LaTeX. This will insert a PDF. + """ + graph = node['graph'] + parts = node['parts'] + + graph_hash = get_graph_hash(node) + name = "inheritance%s" % graph_hash + pdf_path = os.path.join('_static', name + ".pdf") + + graph.run_dot(['-Tpdf', '-o%s' % pdf_path], + name, parts, graph_options={'size': '"6.0,6.0"'}) + return '\\includegraphics{../../%s}' % pdf_path + +def visit_inheritance_diagram(inner_func): + """ + This is just a wrapper around html/latex_output_graph to make it + easier to handle errors and insert warnings. + """ + def visitor(self, node): + try: + content = inner_func(self, node) + except DotException, e: + # Insert the exception as a warning in the document + warning = self.document.reporter.warning(str(e), line=node.line) + warning.parent = node + node.children = [warning] + else: + source = self.document.attributes['source'] + self.body.append(content) + node.children = [] + return visitor + +def do_nothing(self, node): + pass + +options_spec = { + 'parts': directives.nonnegative_int + } + +# Deal with the old and new way of registering directives +try: + from docutils.parsers.rst import Directive +except ImportError: + from docutils.parsers.rst.directives import _directives + def inheritance_diagram_directive(name, arguments, options, content, lineno, + content_offset, block_text, state, + state_machine): + return inheritance_diagram_directive_run(arguments, options, state) + inheritance_diagram_directive.__doc__ = __doc__ + inheritance_diagram_directive.arguments = (1, 100, 0) + inheritance_diagram_directive.options = options_spec + inheritance_diagram_directive.content = 0 + _directives['inheritance-diagram'] = inheritance_diagram_directive +else: + class inheritance_diagram_directive(Directive): + has_content = False + required_arguments = 1 + optional_arguments = 100 + final_argument_whitespace = False + option_spec = options_spec + + def run(self): + return inheritance_diagram_directive_run( + self.arguments, self.options, self.state) + inheritance_diagram_directive.__doc__ = __doc__ + + directives.register_directive('inheritance-diagram', + inheritance_diagram_directive) + +def setup(app): + app.add_node(inheritance_diagram) + + HTMLTranslator.visit_inheritance_diagram = \ + visit_inheritance_diagram(html_output_graph) + HTMLTranslator.depart_inheritance_diagram = do_nothing + + LaTeXTranslator.visit_inheritance_diagram = \ + visit_inheritance_diagram(latex_output_graph) + LaTeXTranslator.depart_inheritance_diagram = do_nothing diff --git a/docs/sphinxext/ipython_console_highlighting.py b/docs/sphinxext/ipython_console_highlighting.py new file mode 100644 index 0000000..4f0104d --- /dev/null +++ b/docs/sphinxext/ipython_console_highlighting.py @@ -0,0 +1,75 @@ +from pygments.lexer import Lexer, do_insertions +from pygments.lexers.agile import PythonConsoleLexer, PythonLexer, \ + PythonTracebackLexer +from pygments.token import Comment, Generic +from sphinx import highlighting +import re + +line_re = re.compile('.*?\n') + +class IPythonConsoleLexer(Lexer): + """ + For IPython console output or doctests, such as: + + Tracebacks are not currently supported. + + .. sourcecode:: ipython + + In [1]: a = 'foo' + + In [2]: a + Out[2]: 'foo' + + In [3]: print a + foo + + In [4]: 1 / 0 + """ + name = 'IPython console session' + aliases = ['ipython'] + mimetypes = ['text/x-ipython-console'] + input_prompt = re.compile("(In \[[0-9]+\]: )|( \.\.\.+:)") + output_prompt = re.compile("(Out\[[0-9]+\]: )|( \.\.\.+:)") + continue_prompt = re.compile(" \.\.\.+:") + tb_start = re.compile("\-+") + + def get_tokens_unprocessed(self, text): + pylexer = PythonLexer(**self.options) + tblexer = PythonTracebackLexer(**self.options) + + curcode = '' + insertions = [] + for match in line_re.finditer(text): + line = match.group() + input_prompt = self.input_prompt.match(line) + continue_prompt = self.continue_prompt.match(line.rstrip()) + output_prompt = self.output_prompt.match(line) + if line.startswith("#"): + insertions.append((len(curcode), + [(0, Comment, line)])) + elif input_prompt is not None: + insertions.append((len(curcode), + [(0, Generic.Prompt, input_prompt.group())])) + curcode += line[input_prompt.end():] + elif continue_prompt is not None: + insertions.append((len(curcode), + [(0, Generic.Prompt, continue_prompt.group())])) + curcode += line[continue_prompt.end():] + elif output_prompt is not None: + insertions.append((len(curcode), + [(0, Generic.Output, output_prompt.group())])) + curcode += line[output_prompt.end():] + else: + if curcode: + for item in do_insertions(insertions, + pylexer.get_tokens_unprocessed(curcode)): + yield item + curcode = '' + insertions = [] + yield match.start(), Generic.Output, line + if curcode: + for item in do_insertions(insertions, + pylexer.get_tokens_unprocessed(curcode)): + yield item + +highlighting.lexers['ipython'] = IPythonConsoleLexer() diff --git a/docs/sphinxext/only_directives.py b/docs/sphinxext/only_directives.py new file mode 100644 index 0000000..e4dfd5c --- /dev/null +++ b/docs/sphinxext/only_directives.py @@ -0,0 +1,87 @@ +# +# A pair of directives for inserting content that will only appear in +# either html or latex. +# + +from docutils.nodes import Body, Element +from docutils.writers.html4css1 import HTMLTranslator +from sphinx.latexwriter import LaTeXTranslator +from docutils.parsers.rst import directives + +class html_only(Body, Element): + pass + +class latex_only(Body, Element): + pass + +def run(content, node_class, state, content_offset): + text = '\n'.join(content) + node = node_class(text) + state.nested_parse(content, content_offset, node) + return [node] + +try: + from docutils.parsers.rst import Directive +except ImportError: + from docutils.parsers.rst.directives import _directives + + def html_only_directive(name, arguments, options, content, lineno, + content_offset, block_text, state, state_machine): + return run(content, html_only, state, content_offset) + + def latex_only_directive(name, arguments, options, content, lineno, + content_offset, block_text, state, state_machine): + return run(content, latex_only, state, content_offset) + + for func in (html_only_directive, latex_only_directive): + func.content = 1 + func.options = {} + func.arguments = None + + _directives['htmlonly'] = html_only_directive + _directives['latexonly'] = latex_only_directive +else: + class OnlyDirective(Directive): + has_content = True + required_arguments = 0 + optional_arguments = 0 + final_argument_whitespace = True + option_spec = {} + + def run(self): + self.assert_has_content() + return run(self.content, self.node_class, + self.state, self.content_offset) + + class HtmlOnlyDirective(OnlyDirective): + node_class = html_only + + class LatexOnlyDirective(OnlyDirective): + node_class = latex_only + + directives.register_directive('htmlonly', HtmlOnlyDirective) + directives.register_directive('latexonly', LatexOnlyDirective) + +def setup(app): + app.add_node(html_only) + app.add_node(latex_only) + + # Add visit/depart methods to HTML-Translator: + def visit_perform(self, node): + pass + def depart_perform(self, node): + pass + def visit_ignore(self, node): + node.children = [] + def depart_ignore(self, node): + node.children = [] + + HTMLTranslator.visit_html_only = visit_perform + HTMLTranslator.depart_html_only = depart_perform + HTMLTranslator.visit_latex_only = visit_ignore + HTMLTranslator.depart_latex_only = depart_ignore + + LaTeXTranslator.visit_html_only = visit_ignore + LaTeXTranslator.depart_html_only = depart_ignore + LaTeXTranslator.visit_latex_only = visit_perform + LaTeXTranslator.depart_latex_only = depart_perform diff --git a/docs/sphinxext/plot_directive.py b/docs/sphinxext/plot_directive.py new file mode 100644 index 0000000..a1a0621 --- /dev/null +++ b/docs/sphinxext/plot_directive.py @@ -0,0 +1,155 @@ +"""A special directive for including a matplotlib plot. + +Given a path to a .py file, it includes the source code inline, then: + +- On HTML, will include a .png with a link to a high-res .png. + +- On LaTeX, will include a .pdf + +This directive supports all of the options of the `image` directive, +except for `target` (since plot will add its own target). + +Additionally, if the :include-source: option is provided, the literal +source will be included inline, as well as a link to the source. +""" + +import sys, os, glob, shutil +from docutils.parsers.rst import directives + +try: + # docutils 0.4 + from docutils.parsers.rst.directives.images import align +except ImportError: + # docutils 0.5 + from docutils.parsers.rst.directives.images import Image + align = Image.align + + +import matplotlib +import IPython.Shell +matplotlib.use('Agg') +import matplotlib.pyplot as plt + +mplshell = IPython.Shell.MatplotlibShell('mpl') + +options = {'alt': directives.unchanged, + 'height': directives.length_or_unitless, + 'width': directives.length_or_percentage_or_unitless, + 'scale': directives.nonnegative_int, + 'align': align, + 'class': directives.class_option, + 'include-source': directives.flag } + +template = """ +.. htmlonly:: + + [`source code <../%(srcdir)s/%(basename)s.py>`__, + `png <../%(srcdir)s/%(basename)s.hires.png>`__, + `pdf <../%(srcdir)s/%(basename)s.pdf>`__] + + .. image:: ../%(srcdir)s/%(basename)s.png +%(options)s + +.. latexonly:: + .. image:: ../%(srcdir)s/%(basename)s.pdf +%(options)s + +""" + +def makefig(fullpath, outdir): + """ + run a pyplot script and save the low and high res PNGs and a PDF in _static + """ + + fullpath = str(fullpath) # todo, why is unicode breaking this + formats = [('png', 100), + ('hires.png', 200), + ('pdf', 72), + ] + + basedir, fname = os.path.split(fullpath) + basename, ext = os.path.splitext(fname) + all_exists = True + + if basedir != outdir: + shutil.copyfile(fullpath, os.path.join(outdir, fname)) + + for format, dpi in formats: + outname = os.path.join(outdir, '%s.%s' % (basename, format)) + if not os.path.exists(outname): + all_exists = False + break + + if all_exists: + print ' already have %s'%fullpath + return + + print ' building %s'%fullpath + plt.close('all') # we need to clear between runs + matplotlib.rcdefaults() + + mplshell.magic_run(fullpath) + for format, dpi in formats: + outname = os.path.join(outdir, '%s.%s' % (basename, format)) + if os.path.exists(outname): continue + plt.savefig(outname, dpi=dpi) + +def run(arguments, options, state_machine, lineno): + reference = directives.uri(arguments[0]) + basedir, fname = os.path.split(reference) + basename, ext = os.path.splitext(fname) + + # todo - should we be using the _static dir for the outdir, I am + # not sure we want to corrupt that dir with autogenerated files + # since it also has permanent files in it which makes it difficult + # to clean (save an rm -rf followed by and svn up) + srcdir = 'pyplots' + + makefig(os.path.join(srcdir, reference), srcdir) + + # todo: it is not great design to assume the makefile is putting + # the figs into the right place, so we may want to do that here instead. + + if options.has_key('include-source'): + lines = ['.. literalinclude:: ../pyplots/%(reference)s' % locals()] + del options['include-source'] + else: + lines = [] + + options = [' :%s: %s' % (key, val) for key, val in + options.items()] + options = "\n".join(options) + + lines.extend((template % locals()).split('\n')) + + state_machine.insert_input( + lines, state_machine.input_lines.source(0)) + return [] + + +try: + from docutils.parsers.rst import Directive +except ImportError: + from docutils.parsers.rst.directives import _directives + + def plot_directive(name, arguments, options, content, lineno, + content_offset, block_text, state, state_machine): + return run(arguments, options, state_machine, lineno) + plot_directive.__doc__ = __doc__ + plot_directive.arguments = (1, 0, 1) + plot_directive.options = options + + _directives['plot'] = plot_directive +else: + class plot_directive(Directive): + required_arguments = 1 + optional_arguments = 0 + final_argument_whitespace = True + option_spec = options + def run(self): + return run(self.arguments, self.options, + self.state_machine, self.lineno) + plot_directive.__doc__ = __doc__ + + directives.register_directive('plot', plot_directive) +