Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Sphinx extension: support captions in inline plots. #15304

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions lib/matplotlib/sphinxext/plot_directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@

This is the caption for the plot

Alternatively, the caption may be given using the :caption: option::

.. plot:: path/to/plot.py
:caption: This is the caption for the plot

If content is given, then the :caption: option is ignored::

.. plot:: path/to/plot.py
:caption: This caption is not used

This is the actual caption used for the plot

Additionally, one may specify the name of a function to call (with
no arguments) immediately after importing the module::

Expand All @@ -33,6 +45,17 @@
img = mpimg.imread('_static/stinkbug.png')
imgplot = plt.imshow(img)

To add a caption to an inline plot, the :caption: option must be used::

.. plot::
:caption: This is the caption for the plot.

import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import numpy as np
img = mpimg.imread('_static/stinkbug.png')
imgplot = plt.imshow(img)

3. Using **doctest** syntax::

.. plot::
Expand Down Expand Up @@ -70,6 +93,11 @@
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 given, the caption to add to the plot. If the code to generate the
plot is specified by a external file and the directive has content,
then this option is ignored.

Additionally, this directive supports all of the options of the `image`
directive, except for *target* (since plot will add its own target). These
include `alt`, `height`, `width`, `scale`, `align` and `class`.
Expand Down Expand Up @@ -252,6 +280,7 @@ class PlotDirective(Directive):
'context': _option_context,
'nofigs': directives.flag,
'encoding': directives.encoding,
'caption': directives.unchanged,
}

def run(self):
Expand Down Expand Up @@ -666,6 +695,11 @@ def run(arguments, content, options, state_machine, state, lineno):
function_name = None
caption = ''

# We didn't get a caption from the directive content.
# See if the options contains one.
if not caption:
caption = options.get('caption', '')

base, source_ext = os.path.splitext(output_base)
if source_ext in ('.py', '.rst', '.txt'):
output_base = base
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
extensions = ['matplotlib.sphinxext.plot_directive']
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
Plot directive caption tests
============================

Inline plot with no caption:

.. plot::

import matplotlib.pyplot as plt
import numpy as np
f = 3
t = np.linspace(0, 1, 100)
s = np.sin(2 * np.pi * f * t)
plt.plot(t, s)

Inline plot with caption:

.. plot::
:caption: Caption for inline plot.

import matplotlib.pyplot as plt
import numpy as np
f = 3
t = np.linspace(0, 1, 100)
s = np.sin(2 * np.pi * f * t)
plt.plot(t, s)

Included file with no caption:

.. plot:: test_plot.py

Included file with caption in the directive content:

.. plot:: test_plot.py

This is a caption in the content.

Included file with caption option:

.. plot:: test_plot.py
:caption: This is a caption in the options.

If both content and options have a caption, the one in the content should prevail:

.. plot:: test_plot.py
:caption: This should be ignored.

The content caption should be used instead.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import matplotlib.pyplot as plt
import numpy as np
f = 3
t = np.linspace(0, 1, 100)
s = np.sin(2 * np.pi * f * t)
plt.plot(t, s)
143 changes: 142 additions & 1 deletion lib/matplotlib/tests/test_sphinxext.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,155 @@

import filecmp
from os.path import join as pjoin, dirname, isdir
import pathlib
from subprocess import Popen, PIPE
import sys
import warnings

import pytest


# Only run the tests if Sphinx is installed.
pytest.importorskip('sphinx')

# Docutils is a dependency of Sphinx so it is safe to
# import after we know Sphinx is available.
from docutils.nodes import caption, figure # noqa: E402

# Sphinx has some deprecation warnings we don't want to turn into errors.
with warnings.catch_warnings():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are these warnings? I feel like long term we don't want to just ignore these.

Copy link
Author

@bcbnz bcbnz Sep 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's Jinja (imported by Sphinx) giving a DeprecationWarning: "Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop." This has been fixed in Jinja's master and should be included in the next release; see pallets/jinja#1028. The other warning (in build_test_doc()) is the same warning, also from Jinja and should also be fixed in the next release.

These warnings are only raised when importing Sphinx for test purposes (not in the actual library) so I don't think matplotlib needs to convert them to test failures.

warnings.simplefilter('ignore')
from sphinx.application import Sphinx


#: Directory of sources for testing the Sphinx extension.
SRCDIR = pathlib.Path(__file__).parent / 'sphinxext_sources'


class NodeFilter:
"""Test utility class to filter nodes from a Sphinx doctree.

This is designed to be used with the walkabout() method of nodes. You
probably want to use the filter_children() class method.

Parameters
----------
document : node
The document node.
classes : list of classes
The node classes to filter from the document. If None, all classes will
be accepted resulting in a flattened list of all nodes.

"""
def __init__(self, document, classes=None):
self.document = document
self.nodes = []
if classes:
self.classes = tuple(classes)
else:
self.classes = None

def dispatch_visit(self, obj):
if not self.classes or isinstance(obj, self.classes):
self.nodes.append(obj)

def dispatch_departure(self, obj):
pass

@classmethod
def filter_children(cls, document, parent, classes=None):
"""Filter child nodes from a parent node.

Parameters
----------
document : node
The main document node.
parent : node
The parent node to work on.
classes : list of classes
The node classes to filter.

Returns
-------
children : list
A list of the nodes which are instances of the given classes or
their subclasses.

"""
obj = cls(document, classes=classes)
parent.walkabout(obj)
return obj.nodes


def build_test_doc(src_dir, build_dir, builder='html'):
"""Build a test document.

Parameters
----------
src_dir : pathlib.Path
The location of the sources.
build_dir : pathlib.Path
The build directory to use.
builder : str
Which builder to use.

Returns
-------
app : sphinx.application.Sphinx
The Sphinx application that built the document.

"""
doctree_dir = build_dir / "doctrees"
output_dir = build_dir / "html"

# Avoid some internal Sphinx deprecation warnings being turned into errors.
with warnings.catch_warnings():
warnings.simplefilter('ignore')
app = Sphinx(src_dir, src_dir, output_dir, doctree_dir, builder)
app.build()
return app


def test_plot_directive_caption(tmpdir):
"""Test the :caption: option of the plot directive.

"""
# Build the test document.
localsrc = SRCDIR / "plot_directive_caption"
build_dir = pathlib.Path(tmpdir)
app = build_test_doc(localsrc, build_dir)

# Get the main document and filter out the figures in it.
index = app.env.get_doctree('index')
figures = NodeFilter.filter_children(index, index, [figure])

# The captions we expect to find.
expected = [
None,
'Caption for inline plot.',
None,
'This is a caption in the content.',
'This is a caption in the options.',
'The content caption should be used instead.',
]

# N.B., each plot directive generates two figures:
# one HTML only and one for other builders.
assert len(figures) == 2 * len(expected), \
"Wrong number of figures in document."

# Check the caption nodes are correct.
for i, figurenode in enumerate(figures):
n = i // 2
captions = NodeFilter.filter_children(index, figurenode, [caption])

if expected[n]:
assert len(captions) > 0, f"Figure {n+1}: no caption found."
assert len(captions) < 2, f"Figure {n+1}: too many captions."
assert captions[0].astext().strip() == expected[n], \
f"Figure {n+1}: wrong caption"
else:
assert len(captions) == 0, f"Figure {n+1}: unexpected caption."


def test_tinypages(tmpdir):
html_dir = pjoin(str(tmpdir), 'html')
Expand Down