remove venv
This commit is contained in:
parent
056387013d
commit
0680c7594e
13999 changed files with 0 additions and 2895688 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,286 +0,0 @@
|
|||
"""
|
||||
Add a ``figure-mpl`` directive that is a responsive version of ``figure``.
|
||||
|
||||
This implementation is very similar to ``.. figure::``, except it also allows a
|
||||
``srcset=`` argument to be passed to the image tag, hence allowing responsive
|
||||
resolution images.
|
||||
|
||||
There is no particular reason this could not be used standalone, but is meant
|
||||
to be used with :doc:`/api/sphinxext_plot_directive_api`.
|
||||
|
||||
Note that the directory organization is a bit different than ``.. figure::``.
|
||||
See the *FigureMpl* documentation below.
|
||||
|
||||
"""
|
||||
import os
|
||||
from os.path import relpath
|
||||
from pathlib import PurePath, Path
|
||||
import shutil
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.parsers.rst import directives
|
||||
from docutils.parsers.rst.directives.images import Figure, Image
|
||||
from sphinx.errors import ExtensionError
|
||||
|
||||
import matplotlib
|
||||
|
||||
|
||||
class figmplnode(nodes.General, nodes.Element):
|
||||
pass
|
||||
|
||||
|
||||
class FigureMpl(Figure):
|
||||
"""
|
||||
Implements a directive to allow an optional hidpi image.
|
||||
|
||||
Meant to be used with the *plot_srcset* configuration option in conf.py,
|
||||
and gets set in the TEMPLATE of plot_directive.py
|
||||
|
||||
e.g.::
|
||||
|
||||
.. figure-mpl:: plot_directive/some_plots-1.png
|
||||
:alt: bar
|
||||
:srcset: plot_directive/some_plots-1.png,
|
||||
plot_directive/some_plots-1.2x.png 2.00x
|
||||
:class: plot-directive
|
||||
|
||||
The resulting html (at ``some_plots.html``) is::
|
||||
|
||||
<img src="sphx_glr_bar_001_hidpi.png"
|
||||
srcset="_images/some_plot-1.png,
|
||||
_images/some_plots-1.2x.png 2.00x",
|
||||
alt="bar"
|
||||
class="plot_directive" />
|
||||
|
||||
Note that the handling of subdirectories is different than that used by the sphinx
|
||||
figure directive::
|
||||
|
||||
.. figure-mpl:: plot_directive/nestedpage/index-1.png
|
||||
:alt: bar
|
||||
:srcset: plot_directive/nestedpage/index-1.png
|
||||
plot_directive/nestedpage/index-1.2x.png 2.00x
|
||||
:class: plot_directive
|
||||
|
||||
The resulting html (at ``nestedpage/index.html``)::
|
||||
|
||||
<img src="../_images/nestedpage-index-1.png"
|
||||
srcset="../_images/nestedpage-index-1.png,
|
||||
../_images/_images/nestedpage-index-1.2x.png 2.00x",
|
||||
alt="bar"
|
||||
class="sphx-glr-single-img" />
|
||||
|
||||
where the subdirectory is included in the image name for uniqueness.
|
||||
"""
|
||||
|
||||
has_content = False
|
||||
required_arguments = 1
|
||||
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,
|
||||
'caption': directives.unchanged,
|
||||
'srcset': directives.unchanged,
|
||||
}
|
||||
|
||||
def run(self):
|
||||
|
||||
image_node = figmplnode()
|
||||
|
||||
imagenm = self.arguments[0]
|
||||
image_node['alt'] = self.options.get('alt', '')
|
||||
image_node['align'] = self.options.get('align', None)
|
||||
image_node['class'] = self.options.get('class', None)
|
||||
image_node['width'] = self.options.get('width', None)
|
||||
image_node['height'] = self.options.get('height', None)
|
||||
image_node['scale'] = self.options.get('scale', None)
|
||||
image_node['caption'] = self.options.get('caption', None)
|
||||
|
||||
# we would like uri to be the highest dpi version so that
|
||||
# latex etc will use that. But for now, lets just make
|
||||
# imagenm... maybe pdf one day?
|
||||
|
||||
image_node['uri'] = imagenm
|
||||
image_node['srcset'] = self.options.get('srcset', None)
|
||||
|
||||
return [image_node]
|
||||
|
||||
|
||||
def _parse_srcsetNodes(st):
|
||||
"""
|
||||
parse srcset...
|
||||
"""
|
||||
entries = st.split(',')
|
||||
srcset = {}
|
||||
for entry in entries:
|
||||
spl = entry.strip().split(' ')
|
||||
if len(spl) == 1:
|
||||
srcset[0] = spl[0]
|
||||
elif len(spl) == 2:
|
||||
mult = spl[1][:-1]
|
||||
srcset[float(mult)] = spl[0]
|
||||
else:
|
||||
raise ExtensionError(f'srcset argument "{entry}" is invalid.')
|
||||
return srcset
|
||||
|
||||
|
||||
def _copy_images_figmpl(self, node):
|
||||
|
||||
# these will be the temporary place the plot-directive put the images eg:
|
||||
# ../../../build/html/plot_directive/users/explain/artists/index-1.png
|
||||
if node['srcset']:
|
||||
srcset = _parse_srcsetNodes(node['srcset'])
|
||||
else:
|
||||
srcset = None
|
||||
|
||||
# the rst file's location: eg /Users/username/matplotlib/doc/users/explain/artists
|
||||
docsource = PurePath(self.document['source']).parent
|
||||
|
||||
# get the relpath relative to root:
|
||||
srctop = self.builder.srcdir
|
||||
rel = relpath(docsource, srctop).replace('.', '').replace(os.sep, '-')
|
||||
if len(rel):
|
||||
rel += '-'
|
||||
# eg: users/explain/artists
|
||||
|
||||
imagedir = PurePath(self.builder.outdir, self.builder.imagedir)
|
||||
# eg: /Users/username/matplotlib/doc/build/html/_images/users/explain/artists
|
||||
|
||||
Path(imagedir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# copy all the sources to the imagedir:
|
||||
if srcset:
|
||||
for src in srcset.values():
|
||||
# the entries in srcset are relative to docsource's directory
|
||||
abspath = PurePath(docsource, src)
|
||||
name = rel + abspath.name
|
||||
shutil.copyfile(abspath, imagedir / name)
|
||||
else:
|
||||
abspath = PurePath(docsource, node['uri'])
|
||||
name = rel + abspath.name
|
||||
shutil.copyfile(abspath, imagedir / name)
|
||||
|
||||
return imagedir, srcset, rel
|
||||
|
||||
|
||||
def visit_figmpl_html(self, node):
|
||||
|
||||
imagedir, srcset, rel = _copy_images_figmpl(self, node)
|
||||
|
||||
# /doc/examples/subd/plot_1.rst
|
||||
docsource = PurePath(self.document['source'])
|
||||
# /doc/
|
||||
# make sure to add the trailing slash:
|
||||
srctop = PurePath(self.builder.srcdir, '')
|
||||
# examples/subd/plot_1.rst
|
||||
relsource = relpath(docsource, srctop)
|
||||
# /doc/build/html
|
||||
desttop = PurePath(self.builder.outdir, '')
|
||||
# /doc/build/html/examples/subd
|
||||
dest = desttop / relsource
|
||||
|
||||
# ../../_images/ for dirhtml and ../_images/ for html
|
||||
imagerel = PurePath(relpath(imagedir, dest.parent)).as_posix()
|
||||
if self.builder.name == "dirhtml":
|
||||
imagerel = f'..{imagerel}'
|
||||
|
||||
# make uri also be relative...
|
||||
nm = PurePath(node['uri'][1:]).name
|
||||
uri = f'{imagerel}/{rel}{nm}'
|
||||
img_attrs = {'src': uri, 'alt': node['alt']}
|
||||
|
||||
# make srcset str. Need to change all the prefixes!
|
||||
maxsrc = uri
|
||||
if srcset:
|
||||
maxmult = -1
|
||||
srcsetst = ''
|
||||
for mult, src in srcset.items():
|
||||
nm = PurePath(src[1:]).name
|
||||
# ../../_images/plot_1_2_0x.png
|
||||
path = f'{imagerel}/{rel}{nm}'
|
||||
srcsetst += path
|
||||
if mult == 0:
|
||||
srcsetst += ', '
|
||||
else:
|
||||
srcsetst += f' {mult:1.2f}x, '
|
||||
|
||||
if mult > maxmult:
|
||||
maxmult = mult
|
||||
maxsrc = path
|
||||
|
||||
# trim trailing comma and space...
|
||||
img_attrs['srcset'] = srcsetst[:-2]
|
||||
|
||||
if node['class'] is not None:
|
||||
img_attrs['class'] = ' '.join(node['class'])
|
||||
for style in ['width', 'height', 'scale']:
|
||||
if node[style]:
|
||||
if 'style' not in img_attrs:
|
||||
img_attrs['style'] = f'{style}: {node[style]};'
|
||||
else:
|
||||
img_attrs['style'] += f'{style}: {node[style]};'
|
||||
|
||||
# <figure class="align-default" id="id1">
|
||||
# <a class="reference internal image-reference" href="_images/index-1.2x.png">
|
||||
# <img alt="_images/index-1.2x.png"
|
||||
# src="_images/index-1.2x.png" style="width: 53%;" />
|
||||
# </a>
|
||||
# <figcaption>
|
||||
# <p><span class="caption-text">Figure caption is here....</span>
|
||||
# <a class="headerlink" href="#id1" title="Permalink to this image">#</a></p>
|
||||
# </figcaption>
|
||||
# </figure>
|
||||
self.body.append(
|
||||
self.starttag(
|
||||
node, 'figure',
|
||||
CLASS=f'align-{node["align"]}' if node['align'] else 'align-center'))
|
||||
self.body.append(
|
||||
self.starttag(node, 'a', CLASS='reference internal image-reference',
|
||||
href=maxsrc) +
|
||||
self.emptytag(node, 'img', **img_attrs) +
|
||||
'</a>\n')
|
||||
if node['caption']:
|
||||
self.body.append(self.starttag(node, 'figcaption'))
|
||||
self.body.append(self.starttag(node, 'p'))
|
||||
self.body.append(self.starttag(node, 'span', CLASS='caption-text'))
|
||||
self.body.append(node['caption'])
|
||||
self.body.append('</span></p></figcaption>\n')
|
||||
self.body.append('</figure>\n')
|
||||
|
||||
|
||||
def visit_figmpl_latex(self, node):
|
||||
|
||||
if node['srcset'] is not None:
|
||||
imagedir, srcset = _copy_images_figmpl(self, node)
|
||||
maxmult = -1
|
||||
# choose the highest res version for latex:
|
||||
maxmult = max(srcset, default=-1)
|
||||
node['uri'] = PurePath(srcset[maxmult]).name
|
||||
|
||||
self.visit_figure(node)
|
||||
|
||||
|
||||
def depart_figmpl_html(self, node):
|
||||
pass
|
||||
|
||||
|
||||
def depart_figmpl_latex(self, node):
|
||||
self.depart_figure(node)
|
||||
|
||||
|
||||
def figurempl_addnode(app):
|
||||
app.add_node(figmplnode,
|
||||
html=(visit_figmpl_html, depart_figmpl_html),
|
||||
latex=(visit_figmpl_latex, depart_figmpl_latex))
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.add_directive("figure-mpl", FigureMpl)
|
||||
figurempl_addnode(app)
|
||||
metadata = {'parallel_read_safe': True, 'parallel_write_safe': True,
|
||||
'version': matplotlib.__version__}
|
||||
return metadata
|
||||
|
|
@ -1,242 +0,0 @@
|
|||
r"""
|
||||
A role and directive to display mathtext in Sphinx
|
||||
==================================================
|
||||
|
||||
The ``mathmpl`` Sphinx extension creates a mathtext image in Matplotlib and
|
||||
shows it in html output. Thus, it is a true and faithful representation of what
|
||||
you will see if you pass a given LaTeX string to Matplotlib (see
|
||||
:ref:`mathtext`).
|
||||
|
||||
.. warning::
|
||||
In most cases, you will likely want to use one of `Sphinx's builtin Math
|
||||
extensions
|
||||
<https://www.sphinx-doc.org/en/master/usage/extensions/math.html>`__
|
||||
instead of this one. The builtin Sphinx math directive uses MathJax to
|
||||
render mathematical expressions, and addresses accessibility concerns that
|
||||
``mathmpl`` doesn't address.
|
||||
|
||||
Mathtext may be included in two ways:
|
||||
|
||||
1. Inline, using the role::
|
||||
|
||||
This text uses inline math: :mathmpl:`\alpha > \beta`.
|
||||
|
||||
which produces:
|
||||
|
||||
This text uses inline math: :mathmpl:`\alpha > \beta`.
|
||||
|
||||
2. Standalone, using the directive::
|
||||
|
||||
Here is some standalone math:
|
||||
|
||||
.. mathmpl::
|
||||
|
||||
\alpha > \beta
|
||||
|
||||
which produces:
|
||||
|
||||
Here is some standalone math:
|
||||
|
||||
.. mathmpl::
|
||||
|
||||
\alpha > \beta
|
||||
|
||||
Options
|
||||
-------
|
||||
|
||||
The ``mathmpl`` role and directive both support the following options:
|
||||
|
||||
fontset : str, default: 'cm'
|
||||
The font set to use when displaying math. See :rc:`mathtext.fontset`.
|
||||
|
||||
fontsize : float
|
||||
The font size, in points. Defaults to the value from the extension
|
||||
configuration option defined below.
|
||||
|
||||
Configuration options
|
||||
---------------------
|
||||
|
||||
The mathtext extension has the following configuration options:
|
||||
|
||||
mathmpl_fontsize : float, default: 10.0
|
||||
Default font size, in points.
|
||||
|
||||
mathmpl_srcset : list of str, default: []
|
||||
Additional image sizes to generate when embedding in HTML, to support
|
||||
`responsive resolution images
|
||||
<https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images>`__.
|
||||
The list should contain additional x-descriptors (``'1.5x'``, ``'2x'``,
|
||||
etc.) to generate (1x is the default and always included.)
|
||||
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.parsers.rst import Directive, directives
|
||||
import sphinx
|
||||
from sphinx.errors import ConfigError, ExtensionError
|
||||
|
||||
import matplotlib as mpl
|
||||
from matplotlib import _api, mathtext
|
||||
from matplotlib.rcsetup import validate_float_or_None
|
||||
|
||||
|
||||
# Define LaTeX math node:
|
||||
class latex_math(nodes.General, nodes.Element):
|
||||
pass
|
||||
|
||||
|
||||
def fontset_choice(arg):
|
||||
return directives.choice(arg, mathtext.MathTextParser._font_type_mapping)
|
||||
|
||||
|
||||
def math_role(role, rawtext, text, lineno, inliner,
|
||||
options={}, content=[]):
|
||||
i = rawtext.find('`')
|
||||
latex = rawtext[i+1:-1]
|
||||
node = latex_math(rawtext)
|
||||
node['latex'] = latex
|
||||
node['fontset'] = options.get('fontset', 'cm')
|
||||
node['fontsize'] = options.get('fontsize',
|
||||
setup.app.config.mathmpl_fontsize)
|
||||
return [node], []
|
||||
math_role.options = {'fontset': fontset_choice,
|
||||
'fontsize': validate_float_or_None}
|
||||
|
||||
|
||||
class MathDirective(Directive):
|
||||
"""
|
||||
The ``.. mathmpl::`` directive, as documented in the module's docstring.
|
||||
"""
|
||||
has_content = True
|
||||
required_arguments = 0
|
||||
optional_arguments = 0
|
||||
final_argument_whitespace = False
|
||||
option_spec = {'fontset': fontset_choice,
|
||||
'fontsize': validate_float_or_None}
|
||||
|
||||
def run(self):
|
||||
latex = ''.join(self.content)
|
||||
node = latex_math(self.block_text)
|
||||
node['latex'] = latex
|
||||
node['fontset'] = self.options.get('fontset', 'cm')
|
||||
node['fontsize'] = self.options.get('fontsize',
|
||||
setup.app.config.mathmpl_fontsize)
|
||||
return [node]
|
||||
|
||||
|
||||
# This uses mathtext to render the expression
|
||||
def latex2png(latex, filename, fontset='cm', fontsize=10, dpi=100):
|
||||
with mpl.rc_context({'mathtext.fontset': fontset, 'font.size': fontsize}):
|
||||
try:
|
||||
depth = mathtext.math_to_image(
|
||||
f"${latex}$", filename, dpi=dpi, format="png")
|
||||
except Exception:
|
||||
_api.warn_external(f"Could not render math expression {latex}")
|
||||
depth = 0
|
||||
return depth
|
||||
|
||||
|
||||
# LaTeX to HTML translation stuff:
|
||||
def latex2html(node, source):
|
||||
inline = isinstance(node.parent, nodes.TextElement)
|
||||
latex = node['latex']
|
||||
fontset = node['fontset']
|
||||
fontsize = node['fontsize']
|
||||
name = 'math-{}'.format(
|
||||
hashlib.sha256(
|
||||
f'{latex}{fontset}{fontsize}'.encode(),
|
||||
usedforsecurity=False,
|
||||
).hexdigest()[-10:])
|
||||
|
||||
destdir = Path(setup.app.builder.outdir, '_images', 'mathmpl')
|
||||
destdir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
dest = destdir / f'{name}.png'
|
||||
depth = latex2png(latex, dest, fontset, fontsize=fontsize)
|
||||
|
||||
srcset = []
|
||||
for size in setup.app.config.mathmpl_srcset:
|
||||
filename = f'{name}-{size.replace(".", "_")}.png'
|
||||
latex2png(latex, destdir / filename, fontset, fontsize=fontsize,
|
||||
dpi=100 * float(size[:-1]))
|
||||
srcset.append(
|
||||
f'{setup.app.builder.imgpath}/mathmpl/{filename} {size}')
|
||||
if srcset:
|
||||
srcset = (f'srcset="{setup.app.builder.imgpath}/mathmpl/{name}.png, ' +
|
||||
', '.join(srcset) + '" ')
|
||||
|
||||
if inline:
|
||||
cls = ''
|
||||
else:
|
||||
cls = 'class="center" '
|
||||
if inline and depth != 0:
|
||||
style = 'style="position: relative; bottom: -%dpx"' % (depth + 1)
|
||||
else:
|
||||
style = ''
|
||||
|
||||
return (f'<img src="{setup.app.builder.imgpath}/mathmpl/{name}.png"'
|
||||
f' {srcset}{cls}{style}/>')
|
||||
|
||||
|
||||
def _config_inited(app, config):
|
||||
# Check for srcset hidpi images
|
||||
for i, size in enumerate(app.config.mathmpl_srcset):
|
||||
if size[-1] == 'x': # "2x" = "2.0"
|
||||
try:
|
||||
float(size[:-1])
|
||||
except ValueError:
|
||||
raise ConfigError(
|
||||
f'Invalid value for mathmpl_srcset parameter: {size!r}. '
|
||||
'Must be a list of strings with the multiplicative '
|
||||
'factor followed by an "x". e.g. ["2.0x", "1.5x"]')
|
||||
else:
|
||||
raise ConfigError(
|
||||
f'Invalid value for mathmpl_srcset parameter: {size!r}. '
|
||||
'Must be a list of strings with the multiplicative '
|
||||
'factor followed by an "x". e.g. ["2.0x", "1.5x"]')
|
||||
|
||||
|
||||
def setup(app):
|
||||
setup.app = app
|
||||
app.add_config_value('mathmpl_fontsize', 10.0, True)
|
||||
app.add_config_value('mathmpl_srcset', [], True)
|
||||
try:
|
||||
app.connect('config-inited', _config_inited) # Sphinx 1.8+
|
||||
except ExtensionError:
|
||||
app.connect('env-updated', lambda app, env: _config_inited(app, None))
|
||||
|
||||
# Add visit/depart methods to HTML-Translator:
|
||||
def visit_latex_math_html(self, node):
|
||||
source = self.document.attributes['source']
|
||||
self.body.append(latex2html(node, source))
|
||||
|
||||
def depart_latex_math_html(self, node):
|
||||
pass
|
||||
|
||||
# Add visit/depart methods to LaTeX-Translator:
|
||||
def visit_latex_math_latex(self, node):
|
||||
inline = isinstance(node.parent, nodes.TextElement)
|
||||
if inline:
|
||||
self.body.append('$%s$' % node['latex'])
|
||||
else:
|
||||
self.body.extend(['\\begin{equation}',
|
||||
node['latex'],
|
||||
'\\end{equation}'])
|
||||
|
||||
def depart_latex_math_latex(self, node):
|
||||
pass
|
||||
|
||||
app.add_node(latex_math,
|
||||
html=(visit_latex_math_html, depart_latex_math_html),
|
||||
latex=(visit_latex_math_latex, depart_latex_math_latex))
|
||||
app.add_role('mathmpl', math_role)
|
||||
app.add_directive('mathmpl', MathDirective)
|
||||
if sphinx.version_info < (1, 8):
|
||||
app.add_role('math', math_role)
|
||||
app.add_directive('math', MathDirective)
|
||||
|
||||
metadata = {'parallel_read_safe': True, 'parallel_write_safe': True}
|
||||
return metadata
|
||||
|
|
@ -1,936 +0,0 @@
|
|||
"""
|
||||
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 # Sphinx dependency.
|
||||
|
||||
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
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Registration hook
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
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(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)
|
||||
# Use copyfile because we do not want to copy src's permissions.
|
||||
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
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Doctest handling
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
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.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
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Template
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
_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.
|
||||
|
||||
"""
|
||||
|
||||
# the context of the plot for all directives specified with the
|
||||
# :context: option
|
||||
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.
|
||||
"""
|
||||
|
||||
# Change the working directory to the directory of the example, so
|
||||
# it can get at its data files, if any. Add its path to sys.path
|
||||
# so it can import any helper modules sitting beside it.
|
||||
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__'
|
||||
|
||||
# Patch out non-interactive show() to avoid triggering a warning.
|
||||
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)
|
||||
|
||||
# Try to determine if all images already exist
|
||||
|
||||
is_doctest, code_pieces = _split_code_at_show(code, function_name)
|
||||
# Look for single-figure output files first
|
||||
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])]
|
||||
|
||||
# Then look for multi-figure output files
|
||||
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)
|
||||
|
||||
# assume that if we have one, we have them all
|
||||
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
|
||||
|
||||
# We didn't find the files, so build them
|
||||
|
||||
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:
|
||||
# save a 2x, 3x etc version of the default...
|
||||
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:
|
||||
# classes are parsed into a list of string, and output by simply
|
||||
# printing the list, abusing the fact that RST guarantees to strip
|
||||
# non-conforming characters
|
||||
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]))
|
||||
# If there is content, it will be passed as a caption.
|
||||
caption = '\n'.join(content)
|
||||
|
||||
# Enforce unambiguous use of captions.
|
||||
if "caption" in options:
|
||||
if caption:
|
||||
raise ValueError(
|
||||
'Caption specified in both content and options.'
|
||||
' Please remove ambiguity.'
|
||||
)
|
||||
# Use caption option
|
||||
caption = options["caption"]
|
||||
|
||||
# If the optional function name is provided, use it
|
||||
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 = ''
|
||||
|
||||
# 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 'format' in options:
|
||||
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).lstrip(os.path.sep)
|
||||
|
||||
# build_dir: where to place output files (temporarily)
|
||||
build_dir = os.path.join(os.path.dirname(setup.app.doctreedir),
|
||||
'plot_directive',
|
||||
source_rel_dir)
|
||||
# get rid of .. in paths, also changes pathsep
|
||||
# see note in Python docs for warning about symbolic links on Windows.
|
||||
# need to compare source and dest paths at end
|
||||
build_dir = os.path.normpath(build_dir)
|
||||
os.makedirs(build_dir, exist_ok=True)
|
||||
|
||||
# how to link to files from the RST file
|
||||
try:
|
||||
build_dir_link = relpath(build_dir, rst_dir).replace(os.path.sep, '/')
|
||||
except ValueError:
|
||||
# on Windows, relpath raises ValueError when path and start are on
|
||||
# different mounts/drives
|
||||
build_dir_link = build_dir
|
||||
|
||||
# get list of included rst files so that the output is updated when any
|
||||
# plots in the included files change. These attributes are modified by the
|
||||
# include directive (see the docutils.parsers.rst.directives.misc module).
|
||||
try:
|
||||
source_file_includes = [os.path.join(os.getcwd(), t[0])
|
||||
for t in state.document.include_log]
|
||||
except AttributeError:
|
||||
# the document.include_log attribute only exists in docutils >=0.17,
|
||||
# before that we need to inspect the state machine
|
||||
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)]
|
||||
# remove the source file itself from the includes
|
||||
try:
|
||||
source_file_includes.remove(source_file_name)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# save script (if necessary)
|
||||
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')
|
||||
|
||||
# make figures
|
||||
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]
|
||||
|
||||
# Properly indent the caption
|
||||
if caption and config.plot_srcset:
|
||||
caption = ':caption: ' + caption.replace('\n', ' ')
|
||||
elif caption:
|
||||
caption = '\n' + '\n'.join(' ' + line.strip()
|
||||
for line in caption.split('\n'))
|
||||
# generate output restructuredtext
|
||||
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 = []
|
||||
|
||||
if 'alt' in options:
|
||||
options['alt'] = options['alt'].replace('\n', ' ')
|
||||
|
||||
opts = [
|
||||
f':{key}: {val}' for key, val in options.items()
|
||||
if key in ('alt', 'height', 'width', 'scale', 'align', 'class')]
|
||||
|
||||
# Not-None src_name signals the need for a source download in the
|
||||
# generated html
|
||||
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
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
"""
|
||||
Custom roles for the Matplotlib documentation.
|
||||
|
||||
.. warning::
|
||||
|
||||
These roles are considered semi-public. They are only intended to be used in
|
||||
the Matplotlib documentation.
|
||||
|
||||
However, it can happen that downstream packages end up pulling these roles into
|
||||
their documentation, which will result in documentation build errors. The following
|
||||
describes the exact mechanism and how to fix the errors.
|
||||
|
||||
There are two ways, Matplotlib docstrings can end up in downstream documentation.
|
||||
You have to subclass a Matplotlib class and either use the ``:inherited-members:``
|
||||
option in your autodoc configuration, or you have to override a method without
|
||||
specifying a new docstring; the new method will inherit the original docstring and
|
||||
still render in your autodoc. If the docstring contains one of the custom sphinx
|
||||
roles, you'll see one of the following error messages:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
Unknown interpreted text role "mpltype".
|
||||
Unknown interpreted text role "rc".
|
||||
|
||||
To fix this, you can add this module as extension to your sphinx :file:`conf.py`::
|
||||
|
||||
extensions = [
|
||||
'matplotlib.sphinxext.roles',
|
||||
# Other extensions.
|
||||
]
|
||||
|
||||
.. warning::
|
||||
|
||||
Direct use of these roles in other packages is not officially supported. We
|
||||
reserve the right to modify or remove these roles without prior notification.
|
||||
"""
|
||||
|
||||
from urllib.parse import urlsplit, urlunsplit
|
||||
|
||||
from docutils import nodes
|
||||
|
||||
import matplotlib
|
||||
from matplotlib import rcParamsDefault
|
||||
|
||||
|
||||
class _QueryReference(nodes.Inline, nodes.TextElement):
|
||||
"""
|
||||
Wraps a reference or pending reference to add a query string.
|
||||
|
||||
The query string is generated from the attributes added to this node.
|
||||
|
||||
Also equivalent to a `~docutils.nodes.literal` node.
|
||||
"""
|
||||
|
||||
def to_query_string(self):
|
||||
"""Generate query string from node attributes."""
|
||||
return '&'.join(f'{name}={value}' for name, value in self.attlist())
|
||||
|
||||
|
||||
def _visit_query_reference_node(self, node):
|
||||
"""
|
||||
Resolve *node* into query strings on its ``reference`` children.
|
||||
|
||||
Then act as if this is a `~docutils.nodes.literal`.
|
||||
"""
|
||||
query = node.to_query_string()
|
||||
for refnode in node.findall(nodes.reference):
|
||||
uri = urlsplit(refnode['refuri'])._replace(query=query)
|
||||
refnode['refuri'] = urlunsplit(uri)
|
||||
|
||||
self.visit_literal(node)
|
||||
|
||||
|
||||
def _depart_query_reference_node(self, node):
|
||||
"""
|
||||
Act as if this is a `~docutils.nodes.literal`.
|
||||
"""
|
||||
self.depart_literal(node)
|
||||
|
||||
|
||||
def _rcparam_role(name, rawtext, text, lineno, inliner, options=None, content=None):
|
||||
"""
|
||||
Sphinx role ``:rc:`` to highlight and link ``rcParams`` entries.
|
||||
|
||||
Usage: Give the desired ``rcParams`` key as parameter.
|
||||
|
||||
:code:`:rc:`figure.dpi`` will render as: :rc:`figure.dpi`
|
||||
"""
|
||||
# Generate a pending cross-reference so that Sphinx will ensure this link
|
||||
# isn't broken at some point in the future.
|
||||
title = f'rcParams["{text}"]'
|
||||
target = 'matplotlibrc-sample'
|
||||
ref_nodes, messages = inliner.interpreted(title, f'{title} <{target}>',
|
||||
'ref', lineno)
|
||||
|
||||
qr = _QueryReference(rawtext, highlight=text)
|
||||
qr += ref_nodes
|
||||
node_list = [qr]
|
||||
|
||||
# The default backend would be printed as "agg", but that's not correct (as
|
||||
# the default is actually determined by fallback).
|
||||
if text in rcParamsDefault and text != "backend":
|
||||
node_list.extend([
|
||||
nodes.Text(' (default: '),
|
||||
nodes.literal('', repr(rcParamsDefault[text])),
|
||||
nodes.Text(')'),
|
||||
])
|
||||
|
||||
return node_list, messages
|
||||
|
||||
|
||||
def _mpltype_role(name, rawtext, text, lineno, inliner, options=None, content=None):
|
||||
"""
|
||||
Sphinx role ``:mpltype:`` for custom matplotlib types.
|
||||
|
||||
In Matplotlib, there are a number of type-like concepts that do not have a
|
||||
direct type representation; example: color. This role allows to properly
|
||||
highlight them in the docs and link to their definition.
|
||||
|
||||
Currently supported values:
|
||||
|
||||
- :code:`:mpltype:`color`` will render as: :mpltype:`color`
|
||||
|
||||
"""
|
||||
mpltype = text
|
||||
type_to_link_target = {
|
||||
'color': 'colors_def',
|
||||
}
|
||||
if mpltype not in type_to_link_target:
|
||||
raise ValueError(f"Unknown mpltype: {mpltype!r}")
|
||||
|
||||
node_list, messages = inliner.interpreted(
|
||||
mpltype, f'{mpltype} <{type_to_link_target[mpltype]}>', 'ref', lineno)
|
||||
return node_list, messages
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.add_role("rc", _rcparam_role)
|
||||
app.add_role("mpltype", _mpltype_role)
|
||||
app.add_node(
|
||||
_QueryReference,
|
||||
html=(_visit_query_reference_node, _depart_query_reference_node),
|
||||
latex=(_visit_query_reference_node, _depart_query_reference_node),
|
||||
text=(_visit_query_reference_node, _depart_query_reference_node),
|
||||
)
|
||||
return {"version": matplotlib.__version__,
|
||||
"parallel_read_safe": True, "parallel_write_safe": True}
|
||||
Loading…
Add table
Add a link
Reference in a new issue