summaryrefslogtreecommitdiff
path: root/doc/sphinxext/plot_directive.py
diff options
context:
space:
mode:
authorPauli Virtanen <pav@iki.fi>2009-05-21 13:35:05 +0000
committerPauli Virtanen <pav@iki.fi>2009-05-21 13:35:05 +0000
commite3377d609ae70ad2bbe312b983a41c8bcae8bd6b (patch)
tree6283aba9cf91673cf44859864709605ef27e0c1d /doc/sphinxext/plot_directive.py
parent1cf7e661bced0b4ea3ea2e4fa1a4d08dc271e83c (diff)
downloadnumpy-e3377d609ae70ad2bbe312b983a41c8bcae8bd6b.tar.gz
sphinxext: revise plot_directive
Diffstat (limited to 'doc/sphinxext/plot_directive.py')
-rw-r--r--doc/sphinxext/plot_directive.py575
1 files changed, 325 insertions, 250 deletions
diff --git a/doc/sphinxext/plot_directive.py b/doc/sphinxext/plot_directive.py
index 45b4c3f78..b33da37e2 100644
--- a/doc/sphinxext/plot_directive.py
+++ b/doc/sphinxext/plot_directive.py
@@ -6,6 +6,7 @@ A special directive for generating a matplotlib plot.
This is a hacked version of plot_directive.py from Matplotlib.
It's very much subject to change!
+
Usage
-----
@@ -32,6 +33,7 @@ The ``plot`` directive supports the options
format : {'python', 'doctest'}
Specify the format of the input
+
include-source : bool
Whether to display the source code. Default can be changed in conf.py
@@ -43,36 +45,34 @@ Configuration options
The plot directive has the following configuration options:
- plot_output_dir
- Directory (relative to config file) where to store plot output.
- Should be inside the static directory. (Default: 'static')
+ plot_include_source
+ Default value for the include-source option
plot_pre_code
Code that should be executed before each plot.
- plot_rcparams
- Dictionary of Matplotlib rc-parameter overrides.
- Has 'sane' defaults.
-
- plot_include_source
- Default value for the include-source option
+ 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
- The set of files to generate. Default: ['png', 'pdf', 'hires.png'],
- ie. everything.
+ File formats to generate. List of tuples or strings::
+
+ [(suffix, dpi), suffix, ...]
+
+ that determine the file format and the DPI. For entries whose
+ DPI was omitted, sensible defaults are chosen.
TODO
----
-* Don't put temp files to _static directory, but do function in the way
- the pngmath directive works, and plot figures only during output writing.
-
* Refactor Latex output; now it's plain images, but it would be nice
to make them appear side-by-side, or in floats.
"""
-import sys, os, glob, shutil, imp, warnings, cStringIO, re, textwrap
+import sys, os, glob, shutil, imp, warnings, cStringIO, re, textwrap, traceback
import warnings
warnings.warn("A plot_directive module is also available under "
@@ -81,48 +81,281 @@ warnings.warn("A plot_directive module is also available under "
"integrated there.",
FutureWarning, stacklevel=2)
+
+#------------------------------------------------------------------------------
+# Registration hook
+#------------------------------------------------------------------------------
+
def setup(app):
setup.app = app
setup.config = app.config
setup.confdir = app.confdir
-
- static_path = '_static'
- if hasattr(app.config, 'html_static_path') and app.config.html_static_path:
- static_path = app.config.html_static_path[0]
-
- app.add_config_value('plot_output_dir', static_path, True)
+
app.add_config_value('plot_pre_code', '', True)
- app.add_config_value('plot_rcparams', sane_rcparameters, 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_directive('plot', plot_directive, True, (0, 1, False),
**plot_directive_options)
-sane_rcparameters = {
- 'font.size': 9,
- 'axes.titlesize': 9,
- 'axes.labelsize': 9,
- 'xtick.labelsize': 9,
- 'ytick.labelsize': 9,
- 'legend.fontsize': 9,
- 'figure.figsize': (4, 3),
-}
+#------------------------------------------------------------------------------
+# plot:: directive
+#------------------------------------------------------------------------------
+from docutils.parsers.rst import directives
+from docutils import nodes
+
+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)
+
+def _option_format(arg):
+ return directives.choice(arg, ('python', 'lisp'))
+
+def _option_align(arg):
+ return directives.choice(arg, ("top", "middle", "bottom", "left", "center",
+ "right"))
+
+plot_directive_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,
+ }
+
+#------------------------------------------------------------------------------
+# Generating output
+#------------------------------------------------------------------------------
+
+from docutils import nodes, utils
+
+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)
+
+TEMPLATE = """
+{{ source_code }}
+
+.. htmlonly::
+
+ {% if source_code %}
+ (`Source code <{{ source_link }}>`__)
+
+ .. admonition:: Output
+ :class: plot-output
+
+ {% endif %}
+
+ {% for img in images %}
+ .. figure:: {{ build_dir }}/{{ img.basename }}.png
+ {%- for option in options %}
+ {{ option }}
+ {% endfor %}
+
+ (
+ {%- if not source_code -%}
+ `Source code <{{source_link}}>`__
+ {%- for fmt in img.formats -%}
+ , `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__
+ {%- endfor -%}
+ {%- else -%}
+ {%- for fmt in img.formats -%}
+ {%- if not loop.first -%}, {% endif -%}
+ `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__
+ {%- endfor -%}
+ {%- endif -%}
+ )
+ {% endfor %}
+
+.. latexonly::
+
+ {% for img in images %}
+ .. image:: {{ build_dir }}/{{ img.basename }}.pdf
+ {% endfor %}
+
+"""
+
+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 run(arguments, content, options, state_machine, state, lineno):
+ if arguments and content:
+ raise RuntimeError("plot:: directive can't have both args and content")
+
+ document = state_machine.document
+ config = document.settings.env.config
+
+ options.setdefault('include-source', config.plot_include_source)
+
+ # determine input
+ rst_file = document.attributes['source']
+ rst_dir = os.path.dirname(rst_file)
+
+ if arguments:
+ if not config.plot_basedir:
+ source_file_name = os.path.join(rst_dir,
+ directives.uri(arguments[0]))
+ else:
+ source_file_name = os.path.join(setup.confdir, config.plot_basedir,
+ directives.uri(arguments[0]))
+ code = open(source_file_name, 'r').read()
+ output_base = os.path.basename(source_file_name)
+ else:
+ 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)
+
+ base, source_ext = os.path.splitext(output_base)
+ if source_ext in ('.py', '.rst', '.txt'):
+ output_base = base
+ else:
+ source_ext = ''
+
+ # ensure that LaTeX includegraphics doesn't choke in foo.bar.pdf filenames
+ output_base = output_base.replace('.', '-')
+
+ # 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):
+ os.makedirs(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))
+
+ # 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:
+ images = makefig(code, source_file_name, build_dir, output_base,
+ config)
+ except PlotError, err:
+ reporter = state.memo.reporter
+ sm = reporter.system_message(
+ 3, "Exception occurred in plotting %s: %s" % (output_base, err),
+ line=lineno)
+ return [sm]
+
+ # generate output restructuredtext
+ if options['include-source']:
+ if is_doctest:
+ lines = ['']
+ else:
+ lines = ['.. code-block:: python', '']
+ lines += [' %s' % row.rstrip() for row in code.split('\n')]
+ source_code = "\n".join(lines)
+ else:
+ source_code = ""
+
+ opts = [':%s: %s' % (key, val) for key, val in options.items()
+ if key in ('alt', 'height', 'width', 'scale', 'align', 'class')]
+
+ result = format_template(
+ TEMPLATE,
+ dest_dir=dest_dir_link,
+ build_dir=build_dir_link,
+ source_link=source_link,
+ options=opts,
+ images=images,
+ source_code=source_code)
+
+ lines = result.split("\n")
+ if len(lines):
+ state_machine.insert_input(
+ lines, state_machine.input_lines.source(0))
+
+ # copy image files to builder's output directory
+ if not os.path.exists(dest_dir):
+ os.makedirs(dest_dir)
+
+ 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 []
+
#------------------------------------------------------------------------------
# Run code and capture figures
#------------------------------------------------------------------------------
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
+import exceptions
+
def contains_doctest(text):
+ try:
+ # check if it's valid Python as-is
+ compile(text, '<string>', 'exec')
+ return False
+ except SyntaxError:
+ pass
r = re.compile(r'^\s*>>>', re.M)
- m = r.match(text)
+ m = r.search(text)
return bool(m)
def unescape_doctest(text):
@@ -136,7 +369,7 @@ def unescape_doctest(text):
code = ""
for line in text.split("\n"):
- m = re.match(r'^\s*(>>>|...) (.*)$', line)
+ m = re.match(r'^\s*(>>>|\.\.\.) (.*)$', line)
if m:
code += m.group(2) + "\n"
elif line.strip():
@@ -145,6 +378,9 @@ def unescape_doctest(text):
code += "\n"
return code
+class PlotError(RuntimeError):
+ pass
+
def run_code(code, code_path):
# Change the working directory to the directory of the example, so
# it can get at its data files, if any.
@@ -158,14 +394,22 @@ def run_code(code, code_path):
# Redirect stdout
stdout = sys.stdout
sys.stdout = cStringIO.StringIO()
+
+ # Reset sys.argv
+ old_sys_argv = sys.argv
+ sys.argv = [code_path]
try:
- code = unescape_doctest(code)
- ns = {}
- exec setup.config.plot_pre_code in ns
- exec code in ns
+ try:
+ code = unescape_doctest(code)
+ ns = {}
+ exec setup.config.plot_pre_code in ns
+ exec code in ns
+ except exceptions.BaseException, 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
@@ -186,218 +430,91 @@ def out_of_date(original, derived):
def makefig(code, code_path, output_dir, output_base, config):
"""
- run a pyplot script and save the low and high res PNGs and a PDF in _static
+ Run a pyplot script *code* and save the images under *output_dir*
+ with file names derived from *output_base*
"""
- included_formats = config.plot_formats
- if type(included_formats) is str:
- included_formats = eval(included_formats)
-
- formats = [x for x in [('png', 80), ('hires.png', 200), ('pdf', 50)]
- if x[0] in config.plot_formats]
+ # -- Parse format list
+ default_dpi = {'png': 80, 'hires.png': 200, 'pdf': 50}
+ 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)
- all_exists = True
+ # -- Try to determine if all images already exist
# Look for single-figure output files first
+ all_exists = True
+ img = ImageFile(output_base, output_dir)
for format, dpi in formats:
- output_path = os.path.join(output_dir, '%s.%s' % (output_base, format))
- if out_of_date(code_path, output_path):
+ if out_of_date(code_path, img.filename(format)):
all_exists = False
break
+ img.formats.append(format)
if all_exists:
- return [output_base]
+ return [img]
# Then look for multi-figure output files
- image_names = []
+ images = []
+ all_exists = True
for i in xrange(1000):
- image_names.append('%s_%02d' % (output_base, i))
+ img = ImageFile('%s_%02d' % (output_base, i), output_dir)
for format, dpi in formats:
- output_path = os.path.join(output_dir,
- '%s.%s' % (image_names[-1], format))
- if out_of_date(code_path, output_path):
+ 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:
- # assume that if we have one, we have them all
all_exists = (i > 0)
break
+ images.append(img)
if all_exists:
- return image_names
+ return images
- # We didn't find the files, so build them
- print "-- Plotting figures %s" % output_base
+ # -- We didn't find the files, so build them
# Clear between runs
plt.close('all')
- # Reset figure parameters
- matplotlib.rcdefaults()
- matplotlib.rcParams.update(config.plot_rcparams)
-
# Run code
run_code(code, code_path)
# Collect images
- image_names = []
+ images = []
fig_managers = _pylab_helpers.Gcf.get_all_fig_managers()
for i, figman in enumerate(fig_managers):
if len(fig_managers) == 1:
- name = output_base
+ img = ImageFile(output_base, output_dir)
else:
- name = "%s_%02d" % (output_base, i)
- image_names.append(name)
+ img = ImageFile("%s_%02d" % (output_base, i), output_dir)
+ images.append(img)
for format, dpi in formats:
- path = os.path.join(output_dir, '%s.%s' % (name, format))
- figman.canvas.figure.savefig(path, dpi=dpi)
+ try:
+ figman.canvas.figure.savefig(img.filename(format), dpi=dpi)
+ except exceptions.BaseException, err:
+ raise PlotError(traceback.format_exc())
+ img.formats.append(format)
+
+ return images
- return image_names
#------------------------------------------------------------------------------
-# Generating output
+# Relative pathnames
#------------------------------------------------------------------------------
-from docutils import nodes, utils
-import jinja
-
-TEMPLATE = """
-{{source_code}}
-
-.. htmlonly::
-
- {% if source_code %}
- (`Source code <{{source_link}}>`__)
- {% endif %}
-
- .. admonition:: Output
- :class: plot-output
-
- {% for name in image_names %}
- .. figure:: {{link_dir}}/{{name}}.png
- {%- for option in options %}
- {{option}}
- {% endfor %}
-
- (
- {%- if not source_code %}`Source code <{{source_link}}>`__, {% endif -%}
- `PNG <{{link_dir}}/{{name}}.hires.png>`__,
- `PDF <{{link_dir}}/{{name}}.pdf>`__)
- {% endfor %}
-
-.. latexonly::
-
- {% for name in image_names %}
- .. image:: {{link_dir}}/{{name}}.pdf
- {% endfor %}
-
-"""
-
-def run(arguments, content, options, state_machine, state, lineno):
- if arguments and content:
- raise RuntimeError("plot:: directive can't have both args and content")
-
- document = state_machine.document
- config = document.settings.env.config
-
- options.setdefault('include-source', config.plot_include_source)
- if options['include-source'] is None:
- options['include-source'] = config.plot_include_source
-
- # determine input
- rst_file = document.attributes['source']
- rst_dir = os.path.dirname(rst_file)
-
- if arguments:
- file_name = os.path.join(rst_dir, directives.uri(arguments[0]))
- code = open(file_name, 'r').read()
- output_base = os.path.basename(file_name)
- else:
- 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
- output_base = '%d-%s' % (counter, os.path.basename(file_name))
-
- rel_name = relpath(file_name, setup.confdir)
-
- base, ext = os.path.splitext(output_base)
- if ext in ('.py', '.rst', '.txt'):
- output_base = base
-
- # 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
- file_rel_dir = os.path.dirname(rel_name)
- while file_rel_dir.startswith(os.path.sep):
- file_rel_dir = file_rel_dir[1:]
-
- output_dir = os.path.join(setup.confdir, setup.config.plot_output_dir,
- file_rel_dir)
-
- if not os.path.exists(output_dir):
- cbook.mkdirs(output_dir)
-
- # copy script
- target_name = os.path.join(output_dir, output_base)
- f = open(target_name, 'w')
- f.write(unescape_doctest(code))
- f.close()
-
- source_link = relpath(target_name, rst_dir)
-
- # determine relative reference
- link_dir = relpath(output_dir, rst_dir)
-
- # make figures
- try:
- image_names = makefig(code, file_name, output_dir, output_base, config)
- except RuntimeError, err:
- reporter = state.memo.reporter
- sm = reporter.system_message(3, "Exception occurred rendering plot",
- line=lineno)
- return [sm]
-
- # generate output
- if options['include-source']:
- if is_doctest:
- lines = ['']
- else:
- lines = ['.. code-block:: python', '']
- lines += [' %s' % row.rstrip() for row in code.split('\n')]
- source_code = "\n".join(lines)
- else:
- source_code = ""
-
- opts = [':%s: %s' % (key, val) for key, val in options.items()
- if key in ('alt', 'height', 'width', 'scale', 'align', 'class')]
-
- result = jinja.from_string(TEMPLATE).render(
- link_dir=link_dir.replace(os.path.sep, '/'),
- source_link=source_link,
- options=opts,
- image_names=image_names,
- source_code=source_code)
-
- lines = result.split("\n")
- if len(lines):
- state_machine.insert_input(
- lines, state_machine.input_lines.source(0))
-
- return []
-
-
-if hasattr(os.path, 'relpath'):
- relpath = os.path.relpath
-else:
+try:
+ from os.path import relpath
+except ImportError:
def relpath(target, base=os.curdir):
"""
Return a relative path to the target from either the current
@@ -433,45 +550,3 @@ else:
rel_list = [os.pardir] * (len(base_list)-i) + target_list[i:]
return os.path.join(*rel_list)
-
-#------------------------------------------------------------------------------
-# plot:: directive registration etc.
-#------------------------------------------------------------------------------
-
-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
-
-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():
- return None
- 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)
-
-def _option_format(arg):
- return directives.choice(arg, ('python', 'lisp'))
-
-plot_directive_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': _option_boolean,
- 'format': _option_format,
- }