|
""" |
|
A directive for including a Matplotlib plot in a Sphinx document |
|
================================================================ |
|
|
|
This is a Sphinx extension providing a reStructuredText directive |
|
``.. plot::`` for including a plot in a Sphinx document. |
|
|
|
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. |
|
|
|
The plot content may be defined in one of three 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 |
|
|
|
The plot caption. |
|
|
|
Additionally, one may 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 |
|
plt.plot([1, 2, 3], [4, 5, 6]) |
|
plt.title("A plotting exammple") |
|
|
|
3. Using **doctest** syntax:: |
|
|
|
.. plot:: |
|
|
|
A plotting example: |
|
>>> import matplotlib.pyplot as plt |
|
>>> plt.plot([1, 2, 3], [4, 5, 6]) |
|
|
|
Options |
|
------- |
|
|
|
The ``.. plot::`` directive supports the following options: |
|
|
|
``:format:`` : {'python', 'doctest'} |
|
The format of the input. If unset, the format is auto-detected. |
|
|
|
``:include-source:`` : bool |
|
Whether to display the source code. The default can be changed using |
|
the ``plot_include_source`` variable in :file:`conf.py` (which itself |
|
defaults to False). |
|
|
|
``:show-source-link:`` : bool |
|
Whether to show a link to the source in HTML. The default can be |
|
changed using the ``plot_html_show_source_link`` variable in |
|
:file:`conf.py` (which itself defaults to True). |
|
|
|
``:context:`` : bool or str |
|
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. If |
|
the ``:context: reset`` option is specified, the context is reset |
|
for this and future plots, and previous figures are closed prior to |
|
running the code. ``:context: close-figs`` keeps the context but closes |
|
previous figures before running the code. |
|
|
|
``:nofigs:`` : bool |
|
If specified, the code block will be run, but no figures will be |
|
inserted. This is usually useful with the ``:context:`` option. |
|
|
|
``:caption:`` : str |
|
If specified, the option's argument will be used as a caption for the |
|
figure. This overwrites the caption given in the content, when the plot |
|
is generated from a file. |
|
|
|
Additionally, this directive supports all the options of the `image directive |
|
<https://docutils.sourceforge.io/docs/ref/rst/directives.html#image>`_, |
|
except for ``:target:`` (since plot will add its own target). These include |
|
``:alt:``, ``:height:``, ``:width:``, ``:scale:``, ``:align:`` and ``:class:``. |
|
|
|
Configuration options |
|
--------------------- |
|
|
|
The plot directive has the following configuration options: |
|
|
|
plot_include_source |
|
Default value for the include-source option (default: False). |
|
|
|
plot_html_show_source_link |
|
Whether to show a link to the source in HTML (default: True). |
|
|
|
plot_pre_code |
|
Code that should be executed before each plot. If None (the default), |
|
it will default to a string containing:: |
|
|
|
import numpy as np |
|
from matplotlib import pyplot as plt |
|
|
|
plot_basedir |
|
Base directory, to which ``plot::`` file names are relative to. |
|
If None or empty (the default), file names are relative to the |
|
directory where the file containing the directive is. |
|
|
|
plot_formats |
|
File formats to generate (default: ['png', 'hires.png', 'pdf']). |
|
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. When passing from |
|
the command line through sphinx_build the list should be passed as |
|
suffix:dpi,suffix:dpi, ... |
|
|
|
plot_html_show_formats |
|
Whether to show links to the files in HTML (default: True). |
|
|
|
plot_rcparams |
|
A dictionary containing any non-standard rcParams that should |
|
be applied before each plot (default: {}). |
|
|
|
plot_apply_rcparams |
|
By default, rcParams are applied when ``:context:`` option is not used |
|
in a plot directive. If set, this configuration option overrides this |
|
behavior and applies rcParams before each plot. |
|
|
|
plot_working_directory |
|
By default, the working directory will be changed to the directory of |
|
the example, so the code can get at its data files, if any. Also its |
|
path will be added to `sys.path` so it can import any helper modules |
|
sitting beside it. This configuration option can be used to specify |
|
a central directory (also added to `sys.path`) where data files and |
|
helper modules for all code are located. |
|
|
|
plot_template |
|
Provide a customized template for preparing restructured text. |
|
|
|
plot_srcset |
|
Allow the srcset image option for responsive image resolutions. List of |
|
strings with the multiplicative factors followed by an "x". |
|
e.g. ["2.0x", "1.5x"]. "2.0x" will create a png with the default "png" |
|
resolution from plot_formats, multiplied by 2. If plot_srcset is |
|
specified, the plot directive uses the |
|
:doc:`/api/sphinxext_figmpl_directive_api` (instead of the usual figure |
|
directive) in the intermediary rst file that is generated. |
|
The plot_srcset option is incompatible with *singlehtml* builds, and an |
|
error will be raised. |
|
|
|
Notes on how it works |
|
--------------------- |
|
|
|
The plot directive runs the code it is given, either in the source file or the |
|
code under the directive. The figure created (if any) is saved in the sphinx |
|
build directory under a subdirectory named ``plot_directive``. It then creates |
|
an intermediate rst file that calls a ``.. figure:`` directive (or |
|
``.. figmpl::`` directive if ``plot_srcset`` is being used) and has links to |
|
the ``*.png`` files in the ``plot_directive`` directory. These translations can |
|
be customized by changing the *plot_template*. See the source of |
|
:doc:`/api/sphinxext_plot_directive_api` for the templates defined in *TEMPLATE* |
|
and *TEMPLATE_SRCSET*. |
|
""" |
|
|
|
import contextlib |
|
import doctest |
|
from io import StringIO |
|
import itertools |
|
import os |
|
from os.path import relpath |
|
from pathlib import Path |
|
import re |
|
import shutil |
|
import sys |
|
import textwrap |
|
import traceback |
|
|
|
from docutils.parsers.rst import directives, Directive |
|
from docutils.parsers.rst.directives.images import Image |
|
import jinja2 |
|
|
|
from sphinx.errors import ExtensionError |
|
|
|
import matplotlib |
|
from matplotlib.backend_bases import FigureManagerBase |
|
import matplotlib.pyplot as plt |
|
from matplotlib import _pylab_helpers, cbook |
|
|
|
matplotlib.use("agg") |
|
|
|
__version__ = 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _option_boolean(arg): |
|
if not arg or not arg.strip(): |
|
|
|
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(f'{arg!r} unknown boolean') |
|
|
|
|
|
def _option_context(arg): |
|
if arg in [None, 'reset', 'close-figs']: |
|
return arg |
|
raise ValueError("Argument should be None or 'reset' or 'close-figs'") |
|
|
|
|
|
def _option_format(arg): |
|
return directives.choice(arg, ('python', 'doctest')) |
|
|
|
|
|
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.items(): |
|
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 |
|
|
|
|
|
class PlotDirective(Directive): |
|
"""The ``.. plot::`` directive, as documented in the module's docstring.""" |
|
|
|
has_content = True |
|
required_arguments = 0 |
|
optional_arguments = 2 |
|
final_argument_whitespace = False |
|
option_spec = { |
|
'alt': directives.unchanged, |
|
'height': directives.length_or_unitless, |
|
'width': directives.length_or_percentage_or_unitless, |
|
'scale': directives.nonnegative_int, |
|
'align': Image.align, |
|
'class': directives.class_option, |
|
'include-source': _option_boolean, |
|
'show-source-link': _option_boolean, |
|
'format': _option_format, |
|
'context': _option_context, |
|
'nofigs': directives.flag, |
|
'caption': directives.unchanged, |
|
} |
|
|
|
def run(self): |
|
"""Run the plot directive.""" |
|
try: |
|
return run(self.arguments, self.content, self.options, |
|
self.state_machine, self.state, self.lineno) |
|
except Exception as e: |
|
raise self.error(str(e)) |
|
|
|
|
|
def _copy_css_file(app, exc): |
|
if exc is None and app.builder.format == 'html': |
|
src = cbook._get_data_path('plot_directive/plot_directive.css') |
|
dst = app.outdir / Path('_static') |
|
dst.mkdir(exist_ok=True) |
|
|
|
shutil.copyfile(src, dst / Path('plot_directive.css')) |
|
|
|
|
|
def setup(app): |
|
setup.app = app |
|
setup.config = app.config |
|
setup.confdir = app.confdir |
|
app.add_directive('plot', PlotDirective) |
|
app.add_config_value('plot_pre_code', None, True) |
|
app.add_config_value('plot_include_source', False, True) |
|
app.add_config_value('plot_html_show_source_link', True, 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) |
|
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_srcset', [], True) |
|
app.connect('doctree-read', mark_plot_labels) |
|
app.add_css_file('plot_directive.css') |
|
app.connect('build-finished', _copy_css_file) |
|
metadata = {'parallel_read_safe': True, 'parallel_write_safe': True, |
|
'version': matplotlib.__version__} |
|
return metadata |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def contains_doctest(text): |
|
try: |
|
|
|
compile(text, '<string>', 'exec') |
|
return False |
|
except SyntaxError: |
|
pass |
|
r = re.compile(r'^\s*>>>', re.M) |
|
m = r.search(text) |
|
return bool(m) |
|
|
|
|
|
def _split_code_at_show(text, function_name): |
|
"""Split code at plt.show().""" |
|
|
|
is_doctest = contains_doctest(text) |
|
if function_name is None: |
|
parts = [] |
|
part = [] |
|
for line in text.split("\n"): |
|
if ((not is_doctest and line.startswith('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)) |
|
else: |
|
parts = [text] |
|
return is_doctest, parts |
|
|
|
|
|
|
|
|
|
|
|
|
|
_SOURCECODE = """ |
|
{{ source_code }} |
|
|
|
.. only:: html |
|
|
|
{% if src_name or (html_show_formats and not multi_image) %} |
|
( |
|
{%- if src_name -%} |
|
:download:`Source code <{{ build_dir }}/{{ src_name }}>` |
|
{%- endif -%} |
|
{%- if html_show_formats and not multi_image -%} |
|
{%- for img in images -%} |
|
{%- for fmt in img.formats -%} |
|
{%- if src_name or not loop.first -%}, {% endif -%} |
|
:download:`{{ fmt }} <{{ build_dir }}/{{ img.basename }}.{{ fmt }}>` |
|
{%- endfor -%} |
|
{%- endfor -%} |
|
{%- endif -%} |
|
) |
|
{% endif %} |
|
""" |
|
|
|
TEMPLATE_SRCSET = _SOURCECODE + """ |
|
{% for img in images %} |
|
.. figure-mpl:: {{ build_dir }}/{{ img.basename }}.{{ default_fmt }} |
|
{% for option in options -%} |
|
{{ option }} |
|
{% endfor %} |
|
{%- if caption -%} |
|
{{ caption }} {# appropriate leading whitespace added beforehand #} |
|
{% endif -%} |
|
{%- if srcset -%} |
|
:srcset: {{ build_dir }}/{{ img.basename }}.{{ default_fmt }} |
|
{%- for sr in srcset -%} |
|
, {{ build_dir }}/{{ img.basename }}.{{ sr }}.{{ default_fmt }} {{sr}} |
|
{%- endfor -%} |
|
{% endif %} |
|
|
|
{% if html_show_formats and multi_image %} |
|
( |
|
{%- for fmt in img.formats -%} |
|
{%- if not loop.first -%}, {% endif -%} |
|
:download:`{{ fmt }} <{{ build_dir }}/{{ img.basename }}.{{ fmt }}>` |
|
{%- endfor -%} |
|
) |
|
{% endif %} |
|
|
|
|
|
{% endfor %} |
|
|
|
.. only:: not html |
|
|
|
{% for img in images %} |
|
.. figure-mpl:: {{ build_dir }}/{{ img.basename }}.* |
|
{% for option in options -%} |
|
{{ option }} |
|
{% endfor -%} |
|
|
|
{{ caption }} {# appropriate leading whitespace added beforehand #} |
|
{% endfor %} |
|
|
|
""" |
|
|
|
TEMPLATE = _SOURCECODE + """ |
|
|
|
{% for img in images %} |
|
.. figure:: {{ build_dir }}/{{ img.basename }}.{{ default_fmt }} |
|
{% for option in options -%} |
|
{{ option }} |
|
{% endfor %} |
|
|
|
{% if html_show_formats and multi_image -%} |
|
( |
|
{%- for fmt in img.formats -%} |
|
{%- if not loop.first -%}, {% endif -%} |
|
:download:`{{ fmt }} <{{ build_dir }}/{{ img.basename }}.{{ fmt }}>` |
|
{%- endfor -%} |
|
) |
|
{%- endif -%} |
|
|
|
{{ caption }} {# appropriate leading whitespace added beforehand #} |
|
{% endfor %} |
|
|
|
.. only:: not html |
|
|
|
{% for img in images %} |
|
.. figure:: {{ build_dir }}/{{ img.basename }}.* |
|
{% for option in options -%} |
|
{{ option }} |
|
{% endfor -%} |
|
|
|
{{ caption }} {# appropriate leading whitespace added beforehand #} |
|
{% endfor %} |
|
|
|
""" |
|
|
|
exception_template = """ |
|
.. only:: html |
|
|
|
[`source code <%(linkdir)s/%(basename)s.py>`__] |
|
|
|
Exception occurred rendering plot. |
|
|
|
""" |
|
|
|
|
|
|
|
plot_context = dict() |
|
|
|
|
|
class ImageFile: |
|
def __init__(self, basename, dirname): |
|
self.basename = basename |
|
self.dirname = dirname |
|
self.formats = [] |
|
|
|
def filename(self, format): |
|
return os.path.join(self.dirname, f"{self.basename}.{format}") |
|
|
|
def filenames(self): |
|
return [self.filename(fmt) for fmt in self.formats] |
|
|
|
|
|
def out_of_date(original, derived, includes=None): |
|
""" |
|
Return whether *derived* is out-of-date relative to *original* or any of |
|
the RST files included in it using the RST include directive (*includes*). |
|
*derived* and *original* are full paths, and *includes* is optionally a |
|
list of full paths which may have been included in the *original*. |
|
""" |
|
if not os.path.exists(derived): |
|
return True |
|
|
|
if includes is None: |
|
includes = [] |
|
files_to_check = [original, *includes] |
|
|
|
def out_of_date_one(original, derived_mtime): |
|
return (os.path.exists(original) and |
|
derived_mtime < os.stat(original).st_mtime) |
|
|
|
derived_mtime = os.stat(derived).st_mtime |
|
return any(out_of_date_one(f, derived_mtime) for f in files_to_check) |
|
|
|
|
|
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. |
|
""" |
|
|
|
|
|
|
|
|
|
pwd = os.getcwd() |
|
if setup.config.plot_working_directory is not None: |
|
try: |
|
os.chdir(setup.config.plot_working_directory) |
|
except OSError as err: |
|
raise OSError(f'{err}\n`plot_working_directory` option in ' |
|
f'Sphinx configuration file must be a valid ' |
|
f'directory path') from err |
|
except TypeError as err: |
|
raise TypeError(f'{err}\n`plot_working_directory` option in ' |
|
f'Sphinx configuration file must be a string or ' |
|
f'None') from err |
|
elif code_path is not None: |
|
dirname = os.path.abspath(os.path.dirname(code_path)) |
|
os.chdir(dirname) |
|
|
|
with cbook._setattr_cm( |
|
sys, argv=[code_path], path=[os.getcwd(), *sys.path]), \ |
|
contextlib.redirect_stdout(StringIO()): |
|
try: |
|
if ns is None: |
|
ns = {} |
|
if not ns: |
|
if setup.config.plot_pre_code is None: |
|
exec('import numpy as np\n' |
|
'from matplotlib import pyplot as plt\n', ns) |
|
else: |
|
exec(str(setup.config.plot_pre_code), ns) |
|
if "__main__" in code: |
|
ns['__name__'] = '__main__' |
|
|
|
|
|
with cbook._setattr_cm(FigureManagerBase, show=lambda self: None): |
|
exec(code, ns) |
|
if function_name is not None: |
|
exec(function_name + "()", ns) |
|
|
|
except (Exception, SystemExit) as err: |
|
raise PlotError(traceback.format_exc()) from err |
|
finally: |
|
os.chdir(pwd) |
|
return ns |
|
|
|
|
|
def clear_state(plot_rcparams, close=True): |
|
if close: |
|
plt.close('all') |
|
matplotlib.rc_file_defaults() |
|
matplotlib.rcParams.update(plot_rcparams) |
|
|
|
|
|
def get_plot_formats(config): |
|
default_dpi = {'png': 80, 'hires.png': 200, 'pdf': 200} |
|
formats = [] |
|
plot_formats = config.plot_formats |
|
for fmt in plot_formats: |
|
if isinstance(fmt, str): |
|
if ':' in fmt: |
|
suffix, dpi = fmt.split(':') |
|
formats.append((str(suffix), int(dpi))) |
|
else: |
|
formats.append((fmt, default_dpi.get(fmt, 80))) |
|
elif isinstance(fmt, (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) |
|
return formats |
|
|
|
|
|
def _parse_srcset(entries): |
|
""" |
|
Parse srcset for multiples... |
|
""" |
|
srcset = {} |
|
for entry in entries: |
|
entry = entry.strip() |
|
if len(entry) >= 2: |
|
mult = entry[:-1] |
|
srcset[float(mult)] = entry |
|
else: |
|
raise ExtensionError(f'srcset argument {entry!r} is invalid.') |
|
return srcset |
|
|
|
|
|
def render_figures(code, code_path, output_dir, output_base, context, |
|
function_name, config, context_reset=False, |
|
close_figs=False, |
|
code_includes=None): |
|
""" |
|
Run a pyplot script and save the images in *output_dir*. |
|
|
|
Save the images under *output_dir* with file names derived from |
|
*output_base* |
|
""" |
|
|
|
if function_name is not None: |
|
output_base = f'{output_base}_{function_name}' |
|
formats = get_plot_formats(config) |
|
|
|
|
|
|
|
is_doctest, code_pieces = _split_code_at_show(code, function_name) |
|
|
|
img = ImageFile(output_base, output_dir) |
|
for format, dpi in formats: |
|
if context or out_of_date(code_path, img.filename(format), |
|
includes=code_includes): |
|
all_exists = False |
|
break |
|
img.formats.append(format) |
|
else: |
|
all_exists = True |
|
|
|
if all_exists: |
|
return [(code, [img])] |
|
|
|
|
|
results = [] |
|
for i, code_piece in enumerate(code_pieces): |
|
images = [] |
|
for j in itertools.count(): |
|
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 fmt, dpi in formats: |
|
if context or out_of_date(code_path, img.filename(fmt), |
|
includes=code_includes): |
|
all_exists = False |
|
break |
|
img.formats.append(fmt) |
|
|
|
|
|
if not all_exists: |
|
all_exists = (j > 0) |
|
break |
|
images.append(img) |
|
if not all_exists: |
|
break |
|
results.append((code_piece, images)) |
|
else: |
|
all_exists = True |
|
|
|
if all_exists: |
|
return results |
|
|
|
|
|
|
|
results = [] |
|
ns = plot_context if context else {} |
|
|
|
if context_reset: |
|
clear_state(config.plot_rcparams) |
|
plot_context.clear() |
|
|
|
close_figs = not context or close_figs |
|
|
|
for i, code_piece in enumerate(code_pieces): |
|
|
|
if not context or config.plot_apply_rcparams: |
|
clear_state(config.plot_rcparams, close_figs) |
|
elif close_figs: |
|
plt.close('all') |
|
|
|
_run_code(doctest.script_from_examples(code_piece) if is_doctest |
|
else 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: |
|
img = ImageFile("%s_%02d_%02d" % (output_base, i, j), |
|
output_dir) |
|
images.append(img) |
|
|
|
for fmt, dpi in formats: |
|
try: |
|
figman.canvas.figure.savefig(img.filename(fmt), dpi=dpi) |
|
if fmt == formats[0][0] and config.plot_srcset: |
|
|
|
srcset = _parse_srcset(config.plot_srcset) |
|
for mult, suffix in srcset.items(): |
|
fm = f'{suffix}.{fmt}' |
|
img.formats.append(fm) |
|
figman.canvas.figure.savefig(img.filename(fm), |
|
dpi=int(dpi * mult)) |
|
except Exception as err: |
|
raise PlotError(traceback.format_exc()) from err |
|
img.formats.append(fmt) |
|
|
|
results.append((code_piece, images)) |
|
|
|
if not context or config.plot_apply_rcparams: |
|
clear_state(config.plot_rcparams, close=not context) |
|
|
|
return results |
|
|
|
|
|
def run(arguments, content, options, state_machine, state, lineno): |
|
document = state_machine.document |
|
config = document.settings.env.config |
|
nofigs = 'nofigs' in options |
|
|
|
if config.plot_srcset and setup.app.builder.name == 'singlehtml': |
|
raise ExtensionError( |
|
'plot_srcset option not compatible with single HTML writer') |
|
|
|
formats = get_plot_formats(config) |
|
default_fmt = formats[0][0] |
|
|
|
options.setdefault('include-source', config.plot_include_source) |
|
options.setdefault('show-source-link', config.plot_html_show_source_link) |
|
|
|
if 'class' in options: |
|
|
|
|
|
|
|
options['class'] = ['plot-directive'] + options['class'] |
|
else: |
|
options.setdefault('class', ['plot-directive']) |
|
keep_context = 'context' in options |
|
context_opt = None if not keep_context else options['context'] |
|
|
|
rst_file = document.attributes['source'] |
|
rst_dir = os.path.dirname(rst_file) |
|
|
|
if len(arguments): |
|
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])) |
|
|
|
caption = '\n'.join(content) |
|
|
|
|
|
if "caption" in options: |
|
if caption: |
|
raise ValueError( |
|
'Caption specified in both content and options.' |
|
' Please remove ambiguity.' |
|
) |
|
|
|
caption = options["caption"] |
|
|
|
|
|
if len(arguments) == 2: |
|
function_name = arguments[1] |
|
else: |
|
function_name = None |
|
|
|
code = Path(source_file_name).read_text(encoding='utf-8') |
|
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) |
|
function_name = None |
|
caption = options.get('caption', '') |
|
|
|
base, source_ext = os.path.splitext(output_base) |
|
if source_ext in ('.py', '.rst', '.txt'): |
|
output_base = base |
|
else: |
|
source_ext = '' |
|
|
|
|
|
output_base = output_base.replace('.', '-') |
|
|
|
|
|
is_doctest = contains_doctest(code) |
|
if 'format' in options: |
|
if options['format'] == 'python': |
|
is_doctest = False |
|
else: |
|
is_doctest = True |
|
|
|
|
|
source_rel_name = relpath(source_file_name, setup.confdir) |
|
source_rel_dir = os.path.dirname(source_rel_name).lstrip(os.path.sep) |
|
|
|
|
|
build_dir = os.path.join(os.path.dirname(setup.app.doctreedir), |
|
'plot_directive', |
|
source_rel_dir) |
|
|
|
|
|
|
|
build_dir = os.path.normpath(build_dir) |
|
os.makedirs(build_dir, exist_ok=True) |
|
|
|
|
|
try: |
|
build_dir_link = relpath(build_dir, rst_dir).replace(os.path.sep, '/') |
|
except ValueError: |
|
|
|
|
|
build_dir_link = build_dir |
|
|
|
|
|
|
|
|
|
try: |
|
source_file_includes = [os.path.join(os.getcwd(), t[0]) |
|
for t in state.document.include_log] |
|
except AttributeError: |
|
|
|
|
|
possible_sources = {os.path.join(setup.confdir, t[0]) |
|
for t in state_machine.input_lines.items} |
|
source_file_includes = [f for f in possible_sources |
|
if os.path.isfile(f)] |
|
|
|
try: |
|
source_file_includes.remove(source_file_name) |
|
except ValueError: |
|
pass |
|
|
|
|
|
if options['show-source-link']: |
|
Path(build_dir, output_base + source_ext).write_text( |
|
doctest.script_from_examples(code) |
|
if source_file_name == rst_file and is_doctest |
|
else code, |
|
encoding='utf-8') |
|
|
|
|
|
try: |
|
results = render_figures(code=code, |
|
code_path=source_file_name, |
|
output_dir=build_dir, |
|
output_base=output_base, |
|
context=keep_context, |
|
function_name=function_name, |
|
config=config, |
|
context_reset=context_opt == 'reset', |
|
close_figs=context_opt == 'close-figs', |
|
code_includes=source_file_includes) |
|
errors = [] |
|
except PlotError as err: |
|
reporter = state.memo.reporter |
|
sm = reporter.system_message( |
|
2, "Exception occurred in plotting {}\n from {}:\n{}".format( |
|
output_base, source_file_name, err), |
|
line=lineno) |
|
results = [(code, [])] |
|
errors = [sm] |
|
|
|
|
|
if caption and config.plot_srcset: |
|
caption = f':caption: {caption}' |
|
elif caption: |
|
caption = '\n' + '\n'.join(' ' + line.strip() |
|
for line in caption.split('\n')) |
|
|
|
total_lines = [] |
|
for j, (code_piece, images) in enumerate(results): |
|
if options['include-source']: |
|
if is_doctest: |
|
lines = ['', *code_piece.splitlines()] |
|
else: |
|
lines = ['.. code-block:: python', '', |
|
*textwrap.indent(code_piece, ' ').splitlines()] |
|
source_code = "\n".join(lines) |
|
else: |
|
source_code = "" |
|
|
|
if nofigs: |
|
images = [] |
|
|
|
opts = [ |
|
f':{key}: {val}' for key, val in options.items() |
|
if key in ('alt', 'height', 'width', 'scale', 'align', 'class')] |
|
|
|
|
|
|
|
if j == 0 and options['show-source-link']: |
|
src_name = output_base + source_ext |
|
else: |
|
src_name = None |
|
if config.plot_srcset: |
|
srcset = [*_parse_srcset(config.plot_srcset).values()] |
|
template = TEMPLATE_SRCSET |
|
else: |
|
srcset = None |
|
template = TEMPLATE |
|
|
|
result = jinja2.Template(config.plot_template or template).render( |
|
default_fmt=default_fmt, |
|
build_dir=build_dir_link, |
|
src_name=src_name, |
|
multi_image=len(images) > 1, |
|
options=opts, |
|
srcset=srcset, |
|
images=images, |
|
source_code=source_code, |
|
html_show_formats=config.plot_html_show_formats and len(images), |
|
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) |
|
|
|
return errors |
|
|