diff options
Diffstat (limited to 'doc/sphinxext/plot_directive.py')
-rw-r--r-- | doc/sphinxext/plot_directive.py | 489 |
1 files changed, 489 insertions, 0 deletions
diff --git a/doc/sphinxext/plot_directive.py b/doc/sphinxext/plot_directive.py new file mode 100644 index 0000000..3f0963b --- /dev/null +++ b/doc/sphinxext/plot_directive.py @@ -0,0 +1,489 @@ +"""A special directive for including a matplotlib plot. + +The source code for the plot may be included in one of two ways: + + 1. A path to a source file as the argument to the directive:: + + .. plot:: path/to/plot.py + + When a path to a source file is given, the content of the + directive may optionally contain a caption for the plot:: + + .. plot:: path/to/plot.py + + This is the caption for the plot + + Additionally, one my specify the name of a function to call (with + no arguments) immediately after importing the module:: + + .. plot:: path/to/plot.py plot_function1 + + 2. Included as inline content to the directive:: + + .. plot:: + + import matplotlib.pyplot as plt + import matplotlib.image as mpimg + import numpy as np + img = mpimg.imread('_static/stinkbug.png') + imgplot = plt.imshow(img) + +In HTML output, `plot` will include a .png file with a link to a high-res +.png and .pdf. In LaTeX output, it will include a .pdf. + +To customize the size of the plot, this directive supports all of the +options of the `image` directive, except for `target` (since plot will +add its own target). These include `alt`, `height`, `width`, `scale`, +`align` and `class`. + +Additionally, if the `:include-source:` option is provided, the +literal source will be displayed inline in the text, (as well as a +link to the source in HTML). If this source file is in a non-UTF8 or +non-ASCII encoding, the encoding must be specified using the +`:encoding:` option. + +The set of file formats to generate can be specified with the +`plot_formats` configuration variable. +""" + +import sys, os, shutil, imp, warnings, cStringIO, re +try: + from hashlib import md5 +except ImportError: + from md5 import md5 + +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 sphinx + +sphinx_version = sphinx.__version__.split(".") +# The split is necessary for sphinx beta versions where the string is +# '6b1' +sphinx_version = tuple([int(re.split('[a-z]', x)[0]) + for x in sphinx_version[:2]]) + +import matplotlib +import matplotlib.cbook as cbook +matplotlib.use('Agg') +import matplotlib.pyplot as plt +import matplotlib.image as image +from matplotlib import _pylab_helpers +from matplotlib.sphinxext import only_directives + + +class PlotWarning(Warning): + """Warning category for all warnings generated by this directive. + + By printing our warnings with this category, it becomes possible to turn + them into errors by using in your conf.py:: + + warnings.simplefilter('error', plot_directive.PlotWarning) + + This way, you can ensure that your docs only build if all your examples + actually run successfully. + """ + pass + + +# os.path.relpath is new in Python 2.6 +if hasattr(os.path, 'relpath'): + relpath = os.path.relpath +else: + # This code is snagged from Python 2.6 + + def relpath(target, base=os.curdir): + """ + Return a relative path to the target from either the current dir or an optional base dir. + Base can be a directory specified either as absolute or relative to current dir. + """ + + if not os.path.exists(target): + raise OSError, 'Target does not exist: '+target + + if not os.path.isdir(base): + raise OSError, 'Base is not a directory or does not exist: '+base + + base_list = (os.path.abspath(base)).split(os.sep) + target_list = (os.path.abspath(target)).split(os.sep) + + # On the windows platform the target may be on a completely + # different drive from the base. + if os.name in ['nt','dos','os2'] and base_list[0] <> target_list[0]: + raise OSError, 'Target is on a different drive to base. Target: '+target_list[0].upper()+', base: '+base_list[0].upper() + + # Starting from the filepath root, work out how much of the + # filepath is shared by base and target. + for i in range(min(len(base_list), len(target_list))): + if base_list[i] <> target_list[i]: break + else: + # If we broke out of the loop, i is pointing to the first + # differing path elements. If we didn't break out of the + # loop, i is pointing to identical path elements. + # Increment i so that in all cases it points to the first + # differing path elements. + i+=1 + + rel_list = [os.pardir] * (len(base_list)-i) + target_list[i:] + if rel_list: + return os.path.join(*rel_list) + else: + return "" + +template = """ +.. htmlonly:: + + %(links)s + + .. figure:: %(prefix)s%(tmpdir)s/%(outname)s.png +%(options)s + +%(caption)s + +.. latexonly:: + .. figure:: %(prefix)s%(tmpdir)s/%(outname)s.pdf +%(options)s + +%(caption)s + +""" + +exception_template = """ +.. htmlonly:: + + [`source code <%(linkdir)s/%(basename)s.py>`__] + +Exception occurred rendering plot. + +""" + +template_content_indent = ' ' + +def out_of_date(original, derived): + """ + Returns True if derivative is out-of-date wrt original, + both of which are full file paths. + """ + return (not os.path.exists(derived) or + (os.path.exists(original) and + os.stat(derived).st_mtime < os.stat(original).st_mtime)) + +def run_code(plot_path, function_name, plot_code): + """ + Import a Python module from a path, and run the function given by + name, if function_name is not None. + """ + # Change the working directory to the directory of the example, so + # it can get at its data files, if any. Add its path to sys.path + # so it can import any helper modules sitting beside it. + if plot_code is not None: + exec(plot_code) + else: + pwd = os.getcwd() + path, fname = os.path.split(plot_path) + sys.path.insert(0, os.path.abspath(path)) + stdout = sys.stdout + sys.stdout = cStringIO.StringIO() + os.chdir(path) + fd = None + try: + fd = open(fname) + module = imp.load_module( + "__plot__", fd, fname, ('py', 'r', imp.PY_SOURCE)) + finally: + del sys.path[0] + os.chdir(pwd) + sys.stdout = stdout + if fd is not None: + fd.close() + + if function_name is not None: + getattr(module, function_name)() + +def run_savefig(plot_path, basename, tmpdir, destdir, formats): + """ + Once a plot script has been imported, this function runs savefig + on all of the figures in all of the desired formats. + """ + fig_managers = _pylab_helpers.Gcf.get_all_fig_managers() + for i, figman in enumerate(fig_managers): + for j, (format, dpi) in enumerate(formats): + if len(fig_managers) == 1: + outname = basename + else: + outname = "%s_%02d" % (basename, i) + outname = outname + "." + format + outpath = os.path.join(tmpdir, outname) + try: + figman.canvas.figure.savefig(outpath, dpi=dpi) + except: + s = cbook.exception_to_str("Exception saving plot %s" % plot_path) + warnings.warn(s, PlotWarning) + return 0 + if j > 0: + shutil.copyfile(outpath, os.path.join(destdir, outname)) + + return len(fig_managers) + +def clear_state(): + plt.close('all') + matplotlib.rcdefaults() + # Set a default figure size that doesn't overflow typical browser + # windows. The script is free to override it if necessary. + matplotlib.rcParams['figure.figsize'] = (5.5, 4.5) + +def render_figures(plot_path, function_name, plot_code, tmpdir, destdir, + formats): + """ + Run a pyplot script and save the low and high res PNGs and a PDF + in outdir. + """ + plot_path = str(plot_path) # todo, why is unicode breaking this + basedir, fname = os.path.split(plot_path) + basename, ext = os.path.splitext(fname) + + all_exists = True + + # Look for single-figure output files first + for format, dpi in formats: + outname = os.path.join(tmpdir, '%s.%s' % (basename, format)) + if out_of_date(plot_path, outname): + all_exists = False + break + + if all_exists: + return 1 + + # Then look for multi-figure output files, assuming + # if we have some we have all... + i = 0 + while True: + all_exists = True + for format, dpi in formats: + outname = os.path.join( + tmpdir, '%s_%02d.%s' % (basename, i, format)) + if out_of_date(plot_path, outname): + all_exists = False + break + if all_exists: + i += 1 + else: + break + + if i != 0: + return i + + # We didn't find the files, so build them + + clear_state() + try: + run_code(plot_path, function_name, plot_code) + except: + s = cbook.exception_to_str("Exception running plot %s" % plot_path) + warnings.warn(s, PlotWarning) + return 0 + + num_figs = run_savefig(plot_path, basename, tmpdir, destdir, formats) + + if '__plot__' in sys.modules: + del sys.modules['__plot__'] + + return num_figs + +def _plot_directive(plot_path, basedir, function_name, plot_code, caption, + options, state_machine): + formats = setup.config.plot_formats + if type(formats) == str: + formats = eval(formats) + + fname = os.path.basename(plot_path) + basename, ext = os.path.splitext(fname) + + # Get the directory of the rst file, and determine the relative + # path from the resulting html file to the plot_directive links + # (linkdir). This relative path is used for html links *only*, + # and not the embedded image. That is given an absolute path to + # the temporary directory, and then sphinx moves the file to + # build/html/_images for us later. + rstdir, rstfile = os.path.split(state_machine.document.attributes['source']) + outdir = os.path.join('plot_directive', basedir) + reldir = relpath(setup.confdir, rstdir) + linkdir = os.path.join(reldir, outdir) + + # tmpdir is where we build all the output files. This way the + # plots won't have to be redone when generating latex after html. + + # Prior to Sphinx 0.6, absolute image paths were treated as + # relative to the root of the filesystem. 0.6 and after, they are + # treated as relative to the root of the documentation tree. We + # need to support both methods here. + tmpdir = os.path.join('build', outdir) + tmpdir = os.path.abspath(tmpdir) + if sphinx_version < (0, 6): + prefix = '' + else: + prefix = '/' + if not os.path.exists(tmpdir): + cbook.mkdirs(tmpdir) + + # destdir is the directory within the output to store files + # that we'll be linking to -- not the embedded images. + destdir = os.path.abspath(os.path.join(setup.app.builder.outdir, outdir)) + if not os.path.exists(destdir): + cbook.mkdirs(destdir) + + # Properly indent the caption + caption = '\n'.join(template_content_indent + line.strip() + for line in caption.split('\n')) + + # Generate the figures, and return the number of them + num_figs = render_figures(plot_path, function_name, plot_code, tmpdir, + destdir, formats) + + # Now start generating the lines of output + lines = [] + + if plot_code is None: + shutil.copyfile(plot_path, os.path.join(destdir, fname)) + + if options.has_key('include-source'): + if plot_code is None: + lines.extend( + ['.. include:: %s' % os.path.join(setup.app.builder.srcdir, plot_path), + ' :literal:']) + if options.has_key('encoding'): + lines.append(' :encoding: %s' % options['encoding']) + del options['encoding'] + else: + lines.extend(['::', '']) + lines.extend([' %s' % row.rstrip() + for row in plot_code.split('\n')]) + lines.append('') + del options['include-source'] + else: + lines = [] + + if num_figs > 0: + options = ['%s:%s: %s' % (template_content_indent, key, val) + for key, val in options.items()] + options = "\n".join(options) + + for i in range(num_figs): + if num_figs == 1: + outname = basename + else: + outname = "%s_%02d" % (basename, i) + + # Copy the linked-to files to the destination within the build tree, + # and add a link for them + links = [] + if plot_code is None: + links.append('`source code <%(linkdir)s/%(basename)s.py>`__') + for format, dpi in formats[1:]: + links.append('`%s <%s/%s.%s>`__' % (format, linkdir, outname, format)) + if len(links): + links = '[%s]' % (', '.join(links) % locals()) + else: + links = '' + + lines.extend((template % locals()).split('\n')) + else: + lines.extend((exception_template % locals()).split('\n')) + + if len(lines): + state_machine.insert_input( + lines, state_machine.input_lines.source(0)) + + return [] + +def plot_directive(name, arguments, options, content, lineno, + content_offset, block_text, state, state_machine): + """ + Handle the arguments to the plot directive. The real work happens + in _plot_directive. + """ + # The user may provide a filename *or* Python code content, but not both + if len(arguments): + plot_path = directives.uri(arguments[0]) + basedir = relpath(os.path.dirname(plot_path), setup.app.builder.srcdir) + + # If there is content, it will be passed as a caption. + + # Indent to match expansion below. XXX - The number of spaces matches + # that of the 'options' expansion further down. This should be moved + # to common code to prevent them from diverging accidentally. + caption = '\n'.join(content) + + # If the optional function name is provided, use it + if len(arguments) == 2: + function_name = arguments[1] + else: + function_name = None + + return _plot_directive(plot_path, basedir, function_name, None, caption, + options, state_machine) + else: + plot_code = '\n'.join(content) + + # Since we don't have a filename, use a hash based on the content + plot_path = md5(plot_code).hexdigest()[-10:] + + return _plot_directive(plot_path, 'inline', None, plot_code, '', options, + state_machine) + +def mark_plot_labels(app, document): + """ + To make plots referenceable, we need to move the reference from + the "htmlonly" (or "latexonly") node to the actual figure node + itself. + """ + for name, explicit in document.nametypes.iteritems(): + if not explicit: + continue + labelid = document.nameids[name] + if labelid is None: + continue + node = document.ids[labelid] + if node.tagname in ('html_only', 'latex_only'): + for n in node: + if n.tagname == 'figure': + sectname = name + for c in n: + if c.tagname == 'caption': + sectname = c.astext() + break + + node['ids'].remove(labelid) + node['names'].remove(name) + n['ids'].append(labelid) + n['names'].append(name) + document.settings.env.labels[name] = \ + document.settings.env.docname, labelid, sectname + break + +def setup(app): + setup.app = app + setup.config = app.config + setup.confdir = app.confdir + + 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, + 'encoding': directives.encoding } + + app.add_directive('plot', plot_directive, True, (0, 2, 0), **options) + app.add_config_value( + 'plot_formats', + [('png', 80), ('hires.png', 200), ('pdf', 50)], + True) + + app.connect('doctree-read', mark_plot_labels) |