diff --git a/CHANGELOG b/CHANGELOG index 980b6dac3a32..8e5be4a00701 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +2011-04-01 The plot directive Sphinx extension now supports all of the + features in the Numpy fork of that extension. These + include doctest formatting, an 'include-source' option, and + a number of new configuration options. - MGD + 2011-03-10 Update pytz version to 2011c, thanks to Simon Cross. - JKS 2011-03-06 Add standalone tests.py test runner script. - JKS diff --git a/doc/devel/documenting_mpl.rst b/doc/devel/documenting_mpl.rst index e46c1784d62f..14a8c34b9d11 100644 --- a/doc/devel/documenting_mpl.rst +++ b/doc/devel/documenting_mpl.rst @@ -213,19 +213,6 @@ Figures can be automatically generated from scripts and included in the docs. It is not necessary to explicitly save the figure in the script, this will be done automatically at build time to ensure that the code that is included runs and produces the advertised figure. -Several figures will be saved with the same basename as the filename -when the documentation is generated (low and high res PNGs, a PDF). -Matplotlib includes a Sphinx extension -(:file:`sphinxext/plot_directive.py`) for generating the images from -the python script and including either a png copy for html or a pdf -for latex:: - - .. plot:: pyplots/pyplot_simple.py - :include-source: - -If the script produces multiple figures (through multiple calls to -:func:`pyplot.figure`), each will be given a numbered file name and -included. The path should be relative to the ``doc`` directory. Any plots specific to the documentation should be added to the ``doc/pyplots`` @@ -238,8 +225,12 @@ directory. e.g.:: The ``:scale:`` directive rescales the image to some percentage of the original size, though we don't recommend using this in most cases since it is probably better to choose the correct figure size and dpi -in mpl and let it handle the scaling. ``:include-source:`` will -present the contents of the file, marked up as source code. +in mpl and let it handle the scaling. + +Plot directive documentation +'''''''''''''''''''''''''''' + +.. automodule:: matplotlib.sphinxext.plot_directive Static figures -------------- diff --git a/doc/users/image_tutorial.rst b/doc/users/image_tutorial.rst index f34ceb9338c7..606588f2bb0f 100644 --- a/doc/users/image_tutorial.rst +++ b/doc/users/image_tutorial.rst @@ -152,7 +152,7 @@ plot from the prompt. import matplotlib.pyplot as plt import matplotlib.image as mpimg import numpy as np - img = mpimg.imread('_static/stinkbug.png') + img = mpimg.imread('../_static/stinkbug.png') imgplot = plt.imshow(img) You can also plot any numpy array - just remember that the datatype @@ -188,7 +188,7 @@ This is array slicing. You can read more in the `Numpy tutorial >> import matplotlib.pyplot as plt + >>> plt.plot([1,2,3], [4,5,6]) + +Options +------- + +The ``plot`` directive supports the following options: + + format : {'python', 'doctest'} + Specify the format of the input + + include-source : bool + Whether to display the source code. The default can be changed + using the `plot_include_source` variable in conf.py + + encoding : str + If this source file is in a non-UTF8 or non-ASCII encoding, + the encoding must be specified using the `:encoding:` option. + The encoding will not be inferred using the ``-*- coding -*-`` + metacomment. + + context : bool + If provided, the code will be run in the context of all + previous plot directives for which the `:context:` option was + specified. This only applies to inline code plot directives, + not those run from files. + + nofigs : bool + If specified, the code block will be run, but no figures will + be inserted. This is usually useful with the ``:context:`` + option. + +Additionally, 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`. -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`. +Configuration options +--------------------- -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 plot directive has the following configuration options: -If the `:context:` option is plotted, the code will be run in the -context of all previous plot directives for which the context option -was specified. This only applies to inline code plot directives, not -those run from files. + plot_include_source + Default value for the include-source option -If the ``:nofigs:`` option is specified, the code block will be run, -but no figures will be inserted. This is usually useful with the -``:context:`` option. + plot_pre_code + Code that should be executed before each plot. -The set of file formats to generate can be specified with the -`plot_formats` configuration variable. + plot_basedir + Base directory, to which ``plot::`` file names are relative + to. (If None or empty, file names are relative to the + directoly where the file containing the directive is.) + plot_formats + File formats to generate. List of tuples or strings:: -Error handling: + [(suffix, dpi), suffix, ...] -Any errors generated during the running of the code are emitted as warnings -using the Python `warnings` module, using a custom category called -`PlotWarning`. To turn the warnings into fatal errors that stop the -documentation build, after adjusting your `sys.path` in your `conf.py` Sphinx -configuration file, use:: + that determine the file format and the DPI. For entries whose + DPI was omitted, sensible defaults are chosen. + + plot_html_show_formats + Whether to show links to the files in HTML. + + plot_rcparams + A dictionary containing any non-standard rcParams that should + be applied before each plot. - import plot_directive - warnings.simplefilter('error', plot_directive.PlotWarning) """ -import sys, os, shutil, imp, warnings, cStringIO, re -try: - from hashlib import md5 -except ImportError: - from md5 import md5 +import sys, os, glob, shutil, imp, warnings, cStringIO, re, textwrap, \ + traceback, exceptions 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 +from docutils import nodes +from docutils.parsers.rst.directives.images import Image +align = Image.align import sphinx sphinx_version = sphinx.__version__.split(".") @@ -89,88 +121,276 @@ sphinx_version = tuple([int(re.split('[a-z]', x)[0]) for x in sphinx_version[:2]]) +try: + # Sphinx depends on either Jinja or Jinja2 + import jinja2 + def format_template(template, **kw): + return jinja2.Template(template).render(**kw) +except ImportError: + import jinja + def format_template(template, **kw): + return jinja.from_string(template, **kw) + 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 +__version__ = 2 + +#------------------------------------------------------------------------------ +# Relative pathnames +#------------------------------------------------------------------------------ + +# os.path.relpath is new in Python 2.6 +try: + from os.path import relpath +except ImportError: + # Copied from Python 2.7 + if 'posix' in sys.builtin_module_names: + def relpath(path, start=os.path.curdir): + """Return a relative version of a path""" + from os.path import sep, curdir, join, abspath, commonprefix, \ + pardir + + if not path: + raise ValueError("no path specified") + + start_list = abspath(start).split(sep) + path_list = abspath(path).split(sep) + + # Work out how much of the filepath is shared by start and path. + i = len(commonprefix([start_list, path_list])) + + rel_list = [pardir] * (len(start_list)-i) + path_list[i:] + if not rel_list: + return curdir + return join(*rel_list) + elif 'nt' in sys.builtin_module_names: + def relpath(path, start=os.path.curdir): + """Return a relative version of a path""" + from os.path import sep, curdir, join, abspath, commonprefix, \ + pardir, splitunc + + if not path: + raise ValueError("no path specified") + start_list = abspath(start).split(sep) + path_list = abspath(path).split(sep) + if start_list[0].lower() != path_list[0].lower(): + unc_path, rest = splitunc(path) + unc_start, rest = splitunc(start) + if bool(unc_path) ^ bool(unc_start): + raise ValueError("Cannot mix UNC and non-UNC paths (%s and %s)" + % (path, start)) + else: + raise ValueError("path is on drive %s, start on drive %s" + % (path_list[0], start_list[0])) + # Work out how much of the filepath is shared by start and path. + for i in range(min(len(start_list), len(path_list))): + if start_list[i].lower() != path_list[i].lower(): + break + else: + i += 1 + + rel_list = [pardir] * (len(start_list)-i) + path_list[i:] + if not rel_list: + return curdir + return join(*rel_list) + else: + raise RuntimeError("Unsupported platform (no relpath available!)") -class PlotWarning(Warning): - """Warning category for all warnings generated by this directive. +#------------------------------------------------------------------------------ +# Registration hook +#------------------------------------------------------------------------------ - By printing our warnings with this category, it becomes possible to turn - them into errors by using in your conf.py:: +def plot_directive(name, arguments, options, content, lineno, + content_offset, block_text, state, state_machine): + return run(arguments, content, options, state_machine, state, lineno) +plot_directive.__doc__ = __doc__ + +def _option_boolean(arg): + if not arg or not arg.strip(): + # no argument given, assume used as a flag + return True + elif arg.strip().lower() in ('no', '0', 'false'): + return False + elif arg.strip().lower() in ('yes', '1', 'true'): + return True + else: + raise ValueError('"%s" unknown boolean' % arg) - warnings.simplefilter('error', plot_directive.PlotWarning) +def _option_format(arg): + return directives.choice(arg, ('python', 'doctest')) - This way, you can ensure that your docs only build if all your examples - actually run successfully. +def _option_align(arg): + return directives.choice(arg, ("top", "middle", "bottom", "left", "center", + "right")) + +def mark_plot_labels(app, document): """ - pass + 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 -# 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 "" +def setup(app): + setup.app = app + setup.config = app.config + setup.confdir = app.confdir -template = """ -.. htmlonly:: + options = {'alt': directives.unchanged, + 'height': directives.length_or_unitless, + 'width': directives.length_or_percentage_or_unitless, + 'scale': directives.nonnegative_int, + 'align': _option_align, + 'class': directives.class_option, + 'include-source': _option_boolean, + 'format': _option_format, + 'context': directives.flag, + 'nofigs': directives.flag, + 'encoding': directives.encoding + } - %(links)s + app.add_directive('plot', plot_directive, True, (0, 2, False), **options) + app.add_config_value('plot_pre_code', None, True) + app.add_config_value('plot_include_source', False, True) + app.add_config_value('plot_formats', ['png', 'hires.png', 'pdf'], True) + app.add_config_value('plot_basedir', None, True) + app.add_config_value('plot_html_show_formats', True, True) + app.add_config_value('plot_rcparams', {}, True) - .. figure:: %(prefix)s%(tmpdir)s/%(outname)s.png -%(options)s + app.connect('doctree-read', mark_plot_labels) -%(caption)s +#------------------------------------------------------------------------------ +# Doctest handling +#------------------------------------------------------------------------------ -.. latexonly:: - .. figure:: %(prefix)s%(tmpdir)s/%(outname)s.pdf -%(options)s +def contains_doctest(text): + try: + # check if it's valid Python as-is + compile(text, '', 'exec') + return False + except SyntaxError: + pass + r = re.compile(r'^\s*>>>', re.M) + m = r.search(text) + return bool(m) + +def unescape_doctest(text): + """ + Extract code from a piece of text, which contains either Python code + or doctests. -%(caption)s + """ + if not contains_doctest(text): + return text + + code = "" + for line in text.split("\n"): + m = re.match(r'^\s*(>>>|\.\.\.) (.*)$', line) + if m: + code += m.group(2) + "\n" + elif line.strip(): + code += "# " + line.strip() + "\n" + else: + code += "\n" + return code + +def split_code_at_show(text): + """ + Split code at plt.show() + + """ + + parts = [] + is_doctest = contains_doctest(text) + + part = [] + for line in text.split("\n"): + if (not is_doctest and line.strip() == 'plt.show()') or \ + (is_doctest and line.strip() == '>>> plt.show()'): + part.append(line) + parts.append("\n".join(part)) + part = [] + else: + part.append(line) + if "\n".join(part).strip(): + parts.append("\n".join(part)) + return parts + +#------------------------------------------------------------------------------ +# Template +#------------------------------------------------------------------------------ + + +TEMPLATE = """ +{{ source_code }} + +{{ only_html }} + + {% if source_link or (html_show_formats and not multi_image) %} + ( + {%- if source_link -%} + `Source code <{{ source_link }}>`__ + {%- endif -%} + {%- if html_show_formats and not multi_image -%} + {%- for img in images -%} + {%- for fmt in img.formats -%} + {%- if source_link or not loop.first -%}, {% endif -%} + `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ + {%- endfor -%} + {%- endfor -%} + {%- endif -%} + ) + {% endif %} + + {% for img in images %} + .. figure:: {{ build_dir }}/{{ img.basename }}.png + {%- for option in options %} + {{ option }} + {% endfor %} + + {% if html_show_formats and multi_image -%} + ( + {%- for fmt in img.formats -%} + {%- if not loop.first -%}, {% endif -%} + `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ + {%- endfor -%} + ) + {%- endif -%} + + {{ caption }} + {% endfor %} + +{{ only_latex }} + + {% for img in images %} + .. image:: {{ build_dir }}/{{ img.basename }}.pdf + {% endfor %} """ @@ -183,12 +403,22 @@ def relpath(target, base=os.curdir): """ -template_content_indent = ' ' - # the context of the plot for all directives specified with the # :context: option plot_context = dict() +class ImageFile(object): + def __init__(self, basename, dirname): + self.basename = basename + self.dirname = dirname + self.formats = [] + + def filename(self, format): + return os.path.join(self.dirname, "%s.%s" % (self.basename, format)) + + def filenames(self): + return [self.filename(fmt) for fmt in self.formats] + def out_of_date(original, derived): """ Returns True if derivative is out-of-date wrt original, @@ -198,7 +428,10 @@ def out_of_date(original, derived): (os.path.exists(original) and os.stat(derived).st_mtime < os.stat(original).st_mtime)) -def run_code(plot_path, function_name, plot_code, context=False): +class PlotError(RuntimeError): + pass + +def run_code(code, code_path, ns=None, function_name=None): """ Import a Python module from a path, and run the function given by name, if function_name is not None. @@ -208,269 +441,172 @@ def run_code(plot_path, function_name, plot_code, context=False): # 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_code = 'import numpy as np; import matplotlib.pyplot as plt\n%s'%plot_code - #print 'CONTEXT', context, plot_context, exec_code - if context: - exec(exec_code, None, plot_context) - else: - exec(exec_code) + pwd = os.getcwd() + old_sys_path = list(sys.path) + if code_path is not None: + dirname = os.path.abspath(os.path.dirname(code_path)) + os.chdir(dirname) + sys.path.insert(0, dirname) - 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 + # Redirect stdout + stdout = sys.stdout + sys.stdout = cStringIO.StringIO() + + # Reset sys.argv + old_sys_argv = sys.argv + sys.argv = [code_path] + + try: 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(): + code = unescape_doctest(code) + if ns is None: + ns = {} + if not ns: + if setup.config.plot_pre_code is None: + exec "import numpy as np\nfrom matplotlib import pyplot as plt\n" in ns + else: + exec setup.config.plot_pre_code in ns + exec code in ns + if function_name is not None: + exec function_name + "()" in ns + except (Exception, SystemExit), err: + raise PlotError(traceback.format_exc()) + finally: + os.chdir(pwd) + sys.argv = old_sys_argv + sys.path[:] = old_sys_path + sys.stdout = stdout + return ns + +def clear_state(plot_rcparams): plt.close('all') matplotlib.rc_file_defaults() + matplotlib.rcParams.update(plot_rcparams) -def render_figures(plot_path, function_name, plot_code, tmpdir, destdir, - formats, context=False): +def render_figures(code, code_path, output_dir, output_base, context, + function_name, config): """ 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) + Save the images under *output_dir* with file names derived from + *output_base* + """ + # -- Parse format list + default_dpi = {'png': 80, 'hires.png': 200, 'pdf': 200} + formats = [] + for fmt in config.plot_formats: + if isinstance(fmt, str): + formats.append((fmt, default_dpi.get(fmt, 80))) + elif type(fmt) in (tuple, list) and len(fmt)==2: + formats.append((str(fmt[0]), int(fmt[1]))) + else: + raise PlotError('invalid image format "%r" in plot_formats' % fmt) + # -- Try to determine if all images already exist - all_exists = True + code_pieces = split_code_at_show(code) # Look for single-figure output files first + # Look for single-figure output files first + all_exists = True + img = ImageFile(output_base, output_dir) for format, dpi in formats: - outname = os.path.join(tmpdir, '%s.%s' % (basename, format)) - if out_of_date(plot_path, outname): + if out_of_date(code_path, img.filename(format)): all_exists = False break + img.formats.append(format) + + if all_exists: + return [(code, [img])] - if not context and 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 + # Then look for multi-figure output files + results = [] + all_exists = True + for i, code_piece in enumerate(code_pieces): + images = [] + for j in xrange(1000): + if len(code_pieces) > 1: + img = ImageFile('%s_%02d_%02d' % (output_base, i, j), output_dir) + else: + img = ImageFile('%s_%02d' % (output_base, j), output_dir) + for format, dpi in formats: + if out_of_date(code_path, img.filename(format)): + all_exists = False + break + img.formats.append(format) + + # assume that if we have one, we have them all + if not all_exists: + all_exists = (j > 0) break - if all_exists: - i += 1 - else: + images.append(img) + if not all_exists: break + results.append((code_piece, images)) - if not context and i != 0: - return i + if all_exists: + return results # We didn't find the files, so build them - if not context: - clear_state() - try: - run_code(plot_path, function_name, plot_code, context=context) - except: - s = cbook.exception_to_str("Exception running plot %s" % plot_path) - warnings.warn(s, PlotWarning) - return 0 - - if not all_exists: - num_figs = run_savefig(plot_path, basename, tmpdir, destdir, formats) - - if '__plot__' in sys.modules: - del sys.modules['__plot__'] - - return num_figs - else: - return 1 - - -def _plot_directive(plot_path, basedir, function_name, plot_code, caption, - options, state_machine): - context = options.has_key('context') + results = [] if context: - # remove for figure directive - del options['context'] - - nofigs = options.has_key('nofigs') - if nofigs: - # remove for figure directive - del options['nofigs'] - - formats = setup.config.plot_formats - if isinstance(formats, basestring): - 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 = '' + ns = plot_context 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, context=context) - - # 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: - if sphinx_version > (1,): - include_prefix = '/' + ns = {} + + for i, code_piece in enumerate(code_pieces): + if not context: + clear_state(config.plot_rcparams) + run_code(code_piece, code_path, ns, function_name) + + images = [] + fig_managers = _pylab_helpers.Gcf.get_all_fig_managers() + for j, figman in enumerate(fig_managers): + if len(fig_managers) == 1 and len(code_pieces) == 1: + img = ImageFile(output_base, output_dir) + elif len(code_pieces) == 1: + img = ImageFile("%s_%02d" % (output_base, j), output_dir) else: - include_prefix = setup.app.builder.srcdir - - lines.extend( - ['.. include:: %s' % os.path.join(include_prefix, 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 = [] + img = ImageFile("%s_%02d_%02d" % (output_base, i, j), + output_dir) + images.append(img) + for format, dpi in formats: + try: + figman.canvas.figure.savefig(img.filename(format), dpi=dpi) + except exceptions.BaseException as err: + raise PlotError(traceback.format_exc()) + img.formats.append(format) - if not nofigs: - if num_figs > 0: - options = ['%s:%s: %s' % (template_content_indent, key, val) - for key, val in options.items()] - options = "\n".join(options) + results.append((code_piece, images)) - 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 = '' + return results - lines.extend((template % locals()).split('\n')) - else: - lines.extend((exception_template % locals()).split('\n')) +def run(arguments, content, options, state_machine, state, lineno): + # The user may provide a filename *or* Python code content, but not both + if arguments and content: + raise RuntimeError("plot:: directive can't have both args and content") + + document = state_machine.document + config = document.settings.env.config + nofigs = options.has_key('nofigs') - if len(lines): - state_machine.insert_input( - lines, state_machine.input_lines.source(0)) + options.setdefault('include-source', config.plot_include_source) + context = options.has_key('context') - return [] + rst_file = document.attributes['source'] + rst_dir = os.path.dirname(rst_file) -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 not config.plot_basedir: + source_file_name = os.path.join(setup.app.builder.srcdir, + directives.uri(arguments[0])) + else: + source_file_name = os.path.join(setup.confdir, config.plot_basedir, + directives.uri(arguments[0])) # 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 @@ -479,67 +615,143 @@ def plot_directive(name, arguments, options, content, lineno, else: function_name = None - return _plot_directive(plot_path, basedir, function_name, None, caption, - options, state_machine) + fd = open(source_file_name, 'r') + code = fd.read() + fd.close() + output_base = os.path.basename(source_file_name) else: - plot_code = '\n'.join(content) + source_file_name = rst_file + code = textwrap.dedent("\n".join(map(str, content))) + counter = document.attributes.get('_plot_counter', 0) + 1 + document.attributes['_plot_counter'] = counter + base, ext = os.path.splitext(os.path.basename(source_file_name)) + output_base = '%s-%d.py' % (base, counter) + function_name = None + caption = '' + + base, source_ext = os.path.splitext(output_base) + if source_ext in ('.py', '.rst', '.txt'): + output_base = base + else: + source_ext = '' - # Since we don't have a filename, use a hash based on the content - plot_path = md5(plot_code).hexdigest()[-10:] + # ensure that LaTeX includegraphics doesn't choke in foo.bar.pdf filenames + output_base = output_base.replace('.', '-') - return _plot_directive(plot_path, 'inline', None, plot_code, '', options, - state_machine) + # is it in doctest format? + is_doctest = contains_doctest(code) + if options.has_key('format'): + if options['format'] == 'python': + is_doctest = False + else: + is_doctest = True + + # determine output directory name fragment + source_rel_name = relpath(source_file_name, setup.confdir) + source_rel_dir = os.path.dirname(source_rel_name) + while source_rel_dir.startswith(os.path.sep): + source_rel_dir = source_rel_dir[1:] + + # build_dir: where to place output files (temporarily) + build_dir = os.path.join(os.path.dirname(setup.app.doctreedir), + 'plot_directive', + source_rel_dir) + if not os.path.exists(build_dir): + cbook.mkdirs(build_dir) + + # output_dir: final location in the builder's directory + dest_dir = os.path.abspath(os.path.join(setup.app.builder.outdir, + source_rel_dir)) + if not os.path.exists(dest_dir): + cbook.mkdirs(dest_dir) + + # how to link to files from the RST file + dest_dir_link = os.path.join(relpath(setup.confdir, rst_dir), + source_rel_dir).replace(os.path.sep, '/') + build_dir_link = relpath(build_dir, rst_dir).replace(os.path.sep, '/') + source_link = dest_dir_link + '/' + output_base + source_ext + + # make figures + try: + results = render_figures(code, source_file_name, build_dir, output_base, + context, function_name, config) + errors = [] + except PlotError, err: + reporter = state.memo.reporter + sm = reporter.system_message( + 2, "Exception occurred in plotting %s: %s" % (output_base, err), + line=lineno) + results = [(code, [])] + errors = [sm] -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 + # Properly indent the caption + caption = '\n'.join(' ' + line.strip() + for line in caption.split('\n')) - 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 + # generate output restructuredtext + total_lines = [] + for j, (code_piece, images) in enumerate(results): + if options['include-source']: + if is_doctest: + lines = [''] + lines += [row.rstrip() for row in code_piece.split('\n')] + else: + lines = ['.. code-block:: python', ''] + lines += [' %s' % row.rstrip() + for row in code_piece.split('\n')] + source_code = "\n".join(lines) + else: + source_code = "" -def setup(app): - setup.app = app - setup.config = app.config - setup.confdir = app.confdir + if nofigs: + images = [] - 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, - 'context': directives.flag, - 'nofigs': directives.flag, - 'encoding': directives.encoding } + opts = [':%s: %s' % (key, val) for key, val in options.items() + if key in ('alt', 'height', 'width', 'scale', 'align', 'class')] - 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) + only_html = ".. only:: html" + only_latex = ".. only:: latex" - app.connect('doctree-read', mark_plot_labels) + if j == 0: + src_link = source_link + else: + src_link = None + + result = format_template( + TEMPLATE, + dest_dir=dest_dir_link, + build_dir=build_dir_link, + source_link=src_link, + multi_image=len(images) > 1, + only_html=only_html, + only_latex=only_latex, + options=opts, + images=images, + source_code=source_code, + html_show_formats=config.plot_html_show_formats, + caption=caption) + + total_lines.extend(result.split("\n")) + total_lines.extend("\n") + + if total_lines: + state_machine.insert_input(total_lines, source=source_file_name) + + # copy image files to builder's output directory + if not os.path.exists(dest_dir): + cbook.mkdirs(dest_dir) + + for code_piece, images in results: + for img in images: + for fn in img.filenames(): + shutil.copyfile(fn, os.path.join(dest_dir, + os.path.basename(fn))) + + # copy script (if necessary) + if source_file_name == rst_file: + target_name = os.path.join(dest_dir, output_base + source_ext) + f = open(target_name, 'w') + f.write(unescape_doctest(code)) + f.close() + + return errors