diff --git a/doc/users/next_whats_new/2018_12_03_sphinx_plot_preserve.rst b/doc/users/next_whats_new/2018_12_03_sphinx_plot_preserve.rst new file mode 100644 index 000000000000..396c481a8815 --- /dev/null +++ b/doc/users/next_whats_new/2018_12_03_sphinx_plot_preserve.rst @@ -0,0 +1,61 @@ +Plot Directive `outname` and `plot_preserve_dir` +---------------------------------------------------- + +The Sphinx plot directive can be used to automagically generate figures for +documentation like so: + +.. code-block:: rst + + .. 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) + +But, if you reorder the figures in the documentation then all the figures may +need to be rebuilt. This takes time. The names given to the figures are also +fairly meaningless, making them more difficult to index by search engines or to +find on a filesystem. + +Alternatively, if you are compiling on a limited-resource service like +ReadTheDocs, you may wish to build imagery locally to avoid hitting resource +limits on the server. Using the new changes allows extensive dynamically +generated imagery to be used on services like ReadTheDocs. + +The ``:outname:`` property +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These problems are addressed through two new features in the plot directive. +The first is the introduction of the ``:outname:`` property. It is used like +so: + +.. code-block:: rst + + .. plot:: + :outname: stinkbug_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) + +Without ``:outname:``, the figure generated above would normally be called, +e.g. :file:`docfile3-4-01.png` or something equally mysterious. With +``:outname:`` the figure generated will instead be named +:file:`stinkbug_plot-01.png` or even :file:`stinkbug_plot.png`. This makes it +easy to understand which output image is which and, more importantly, uniquely +keys output images to code snippets. + +The ``plot_preserve_dir`` configuration value +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Setting the ``plot_preserve_dir`` configuration value to the name of a +directory will cause all images with ``:outname:`` set to be copied to this +directory upon generation. + +If an image is already in ``plot_preserve_dir`` when documentation is being +generated, this image is copied to the build directory thereby pre-empting +generation and reducing computation time in low-resource environments. diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 31dc1a6ff414..f1f8fdda87c9 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -70,10 +70,18 @@ If specified, the code block will be run, but no figures will be inserted. This is usually useful with the ``:context:`` option. + outname : str + If specified, the names of the generated plots will start with the + value of `:outname:`. This is handy for preserving output results if + code is reordered between runs. The value of `:outname:` must be + unique across the generated documentation. + 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`. + + Configuration options --------------------- @@ -129,12 +137,25 @@ plot_template Provide a customized template for preparing restructured text. + + plot_preserve_dir + Files with outnames are copied to this directory and files in this + directory are copied back into the build directory prior to the build + beginning. + """ import contextlib from io import StringIO import itertools import os +import sys +import shutil +import io +import re +import textwrap +import glob +import logging from os.path import relpath from pathlib import Path import re @@ -157,8 +178,14 @@ __version__ = 2 +_log = logging.getLogger(__name__) -# ----------------------------------------------------------------------------- +#Outnames must be unique. This variable stores the outnames that +#have been seen so we can guarantee this and warn the user if a +#duplicate is encountered. +_outname_list = set() + +#------------------------------------------------------------------------------ # Registration hook # ----------------------------------------------------------------------------- @@ -252,6 +279,7 @@ class PlotDirective(Directive): 'context': _option_context, 'nofigs': directives.flag, 'encoding': directives.encoding, + 'outname': str, } def run(self): @@ -276,6 +304,7 @@ def setup(app): app.add_config_value('plot_apply_rcparams', False, True) app.add_config_value('plot_working_directory', None, True) app.add_config_value('plot_template', None, True) + app.add_config_value('plot_preserve_dir', '', True) app.connect('doctree-read', mark_plot_labels) @@ -519,7 +548,7 @@ def get_plot_formats(config): def render_figures(code, code_path, output_dir, output_base, context, function_name, config, context_reset=False, - close_figs=False): + close_figs=False, outname=''): """ Run a pyplot script and save the images in *output_dir*. @@ -610,7 +639,13 @@ def render_figures(code, code_path, output_dir, output_base, context, for fmt, dpi in formats: try: figman.canvas.figure.savefig(img.filename(fmt), dpi=dpi) - except Exception: + if config.plot_preserve_dir and outname: + _log.info( + "Preserving '{0}' into '{1}'".format( + img.filename(fmt), config.plot_preserve_dir)) + shutil.copy2(img.filename(fmt), + config.plot_preserve_dir) + except Exception as err: raise PlotError(traceback.format_exc()) img.formats.append(fmt) @@ -637,6 +672,21 @@ def run(arguments, content, options, state_machine, state, lineno): rst_file = document.attributes['source'] rst_dir = os.path.dirname(rst_file) + # Get output name of the images, if the option was provided + outname = options.get('outname', '') + + # Ensure that the outname is unique, otherwise copied images will + # not be what user expects + if outname and outname in _outname_list: + raise Exception("The outname '{0}' is not unique!".format(outname)) + else: + _outname_list.add(outname) + + if config.plot_preserve_dir: + # Ensure `preserve_dir` ends with a slash, otherwise `copy2` + # will misbehave + config.plot_preserve_dir = os.path.join(config.plot_preserve_dir, '') + if len(arguments): if not config.plot_basedir: source_file_name = os.path.join(setup.app.builder.srcdir, @@ -672,6 +722,11 @@ def run(arguments, content, options, state_machine, state, lineno): else: source_ext = '' + # outname, if present, overrides output_base, but preserve + # numbering of multi-figure code snippets + if outname: + output_base = re.sub('^[^-]*', outname, output_base) + # ensure that LaTeX includegraphics doesn't choke in foo.bar.pdf filenames output_base = output_base.replace('.', '-') @@ -718,6 +773,16 @@ def run(arguments, content, options, state_machine, state, lineno): build_dir_link = build_dir source_link = dest_dir_link + '/' + output_base + source_ext + # If we previously preserved copies of the generated figures this copies + # them into the build directory so that they will not be remade. + if config.plot_preserve_dir and outname: + outfiles = glob.glob( + os.path.join(config.plot_preserve_dir, outname) + '*') + for of in outfiles: + _log.info("Copying preserved copy of '{0}' into '{1}'".format( + of, build_dir)) + shutil.copy2(of, build_dir) + # make figures try: results = render_figures(code, @@ -728,7 +793,8 @@ def run(arguments, content, options, state_machine, state, lineno): function_name, config, context_reset=context_opt == 'reset', - close_figs=context_opt == 'close-figs') + close_figs=context_opt == 'close-figs', + outname=outname) errors = [] except PlotError as err: reporter = state.memo.reporter diff --git a/lib/matplotlib/tests/tinypages/some_plots.rst b/lib/matplotlib/tests/tinypages/some_plots.rst index 615908b0107f..7aed2881898a 100644 --- a/lib/matplotlib/tests/tinypages/some_plots.rst +++ b/lib/matplotlib/tests/tinypages/some_plots.rst @@ -126,4 +126,11 @@ Plot 16 uses a specific function in a file with plot commands: .. plot:: range6.py range6 +Plot 17 has an outname +.. plot:: + :context: close-figs + :outname: plot17out + + plt.figure() + plt.plot(range(4))