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

Skip to content

Commit ca36e55

Browse files
committed
Sphinx extension: support captions in inline plots.
This commit adds a :caption: option to the plot directive provided by the Sphinx extension (matplotlib.sphinxext.plot_directive). Without this option, there is no way to specify a caption for a plot generated from inline content. This is fully backwards-compatible. If a plot directive with a path to a source file has both a :caption: option and content provided, the content is used for the caption and the option is ignored.
1 parent 030157c commit ca36e55

File tree

5 files changed

+228
-1
lines changed

5 files changed

+228
-1
lines changed

lib/matplotlib/sphinxext/plot_directive.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@
1818
1919
This is the caption for the plot
2020
21+
Alternatively, the caption may be given using the :caption: option::
22+
23+
.. plot:: path/to/plot.py
24+
:caption: This is the caption for the plot
25+
26+
If content is given, then the :caption: option is ignored::
27+
28+
.. plot:: path/to/plot.py
29+
:caption: This caption is not used
30+
31+
This is the actual caption used for the plot
32+
2133
Additionally, one may specify the name of a function to call (with
2234
no arguments) immediately after importing the module::
2335
@@ -33,6 +45,17 @@
3345
img = mpimg.imread('_static/stinkbug.png')
3446
imgplot = plt.imshow(img)
3547
48+
To add a caption to an inline plot, the :caption: option must be used::
49+
50+
.. plot::
51+
:caption: This is the caption for the plot.
52+
53+
import matplotlib.pyplot as plt
54+
import matplotlib.image as mpimg
55+
import numpy as np
56+
img = mpimg.imread('_static/stinkbug.png')
57+
imgplot = plt.imshow(img)
58+
3659
3. Using **doctest** syntax::
3760
3861
.. plot::
@@ -70,6 +93,11 @@
7093
If specified, the code block will be run, but no figures will be
7194
inserted. This is usually useful with the ``:context:`` option.
7295
96+
caption : str
97+
If given, the caption to add to the plot. If the code to generate the
98+
plot is specified by a external file and the directive has content,
99+
then this option is ignored.
100+
73101
Additionally, this directive supports all of the options of the `image`
74102
directive, except for *target* (since plot will add its own target). These
75103
include `alt`, `height`, `width`, `scale`, `align` and `class`.
@@ -252,6 +280,7 @@ class PlotDirective(Directive):
252280
'context': _option_context,
253281
'nofigs': directives.flag,
254282
'encoding': directives.encoding,
283+
'caption': directives.unchanged,
255284
}
256285

257286
def run(self):
@@ -666,6 +695,11 @@ def run(arguments, content, options, state_machine, state, lineno):
666695
function_name = None
667696
caption = ''
668697

698+
# We didn't get a caption from the directive content.
699+
# See if the options contains one.
700+
if not caption:
701+
caption = options.get('caption', '')
702+
669703
base, source_ext = os.path.splitext(output_base)
670704
if source_ext in ('.py', '.rst', '.txt'):
671705
output_base = base
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
extensions = ['matplotlib.sphinxext.plot_directive']
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
Plot directive caption tests
2+
============================
3+
4+
Inline plot with no caption:
5+
6+
.. plot::
7+
8+
import matplotlib.pyplot as plt
9+
import numpy as np
10+
f = 3
11+
t = np.linspace(0, 1, 100)
12+
s = np.sin(2 * np.pi * f * t)
13+
plt.plot(t, s)
14+
15+
Inline plot with caption:
16+
17+
.. plot::
18+
:caption: Caption for inline plot.
19+
20+
import matplotlib.pyplot as plt
21+
import numpy as np
22+
f = 3
23+
t = np.linspace(0, 1, 100)
24+
s = np.sin(2 * np.pi * f * t)
25+
plt.plot(t, s)
26+
27+
Included file with no caption:
28+
29+
.. plot:: test_plot.py
30+
31+
Included file with caption in the directive content:
32+
33+
.. plot:: test_plot.py
34+
35+
This is a caption in the content.
36+
37+
Included file with caption option:
38+
39+
.. plot:: test_plot.py
40+
:caption: This is a caption in the options.
41+
42+
If both content and options have a caption, the one in the content should prevail:
43+
44+
.. plot:: test_plot.py
45+
:caption: This should be ignored.
46+
47+
The content caption should be used instead.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import matplotlib.pyplot as plt
2+
import numpy as np
3+
f = 3
4+
t = np.linspace(0, 1, 100)
5+
s = np.sin(2 * np.pi * f * t)
6+
plt.plot(t, s)

lib/matplotlib/tests/test_sphinxext.py

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,154 @@
22

33
import filecmp
44
from os.path import join as pjoin, dirname, isdir
5+
import pathlib
56
from subprocess import Popen, PIPE
67
import sys
8+
import warnings
79

810
import pytest
11+
from docutils.nodes import caption, figure
912

10-
13+
# Only run the tests if Sphinx is installed.
1114
pytest.importorskip('sphinx')
1215

1316

17+
# Sphinx has some deprecation warnings we don't want to turn into errors.
18+
with warnings.catch_warnings():
19+
warnings.simplefilter('ignore')
20+
from sphinx.application import Sphinx
21+
22+
23+
#: Directory of sources for testing the Sphinx extension.
24+
SRCDIR = pathlib.Path(__file__).parent / 'sphinxext_sources'
25+
26+
27+
class NodeFilter:
28+
"""Test utility class to filter nodes from a Sphinx doctree.
29+
30+
This is designed to be used with the walkabout() method of nodes. You
31+
probably want to use the filter_children() class method.
32+
33+
Parameters
34+
----------
35+
document : node
36+
The document node.
37+
classes : list of classes
38+
The node classes to filter from the document. If None, all classes will
39+
be accepted resulting in a flattened list of all nodes.
40+
41+
"""
42+
def __init__(self, document, classes=None):
43+
self.document = document
44+
self.nodes = []
45+
if classes:
46+
self.classes = tuple(classes)
47+
else:
48+
self.classes = None
49+
50+
def dispatch_visit(self, obj):
51+
if not self.classes or isinstance(obj, self.classes):
52+
self.nodes.append(obj)
53+
54+
def dispatch_departure(self, obj):
55+
pass
56+
57+
@classmethod
58+
def filter_children(cls, document, parent, classes=None):
59+
"""Filter child nodes from a parent node.
60+
61+
Parameters
62+
----------
63+
document : node
64+
The main document node.
65+
parent : node
66+
The parent node to work on.
67+
classes : list of classes
68+
The node classes to filter.
69+
70+
Returns
71+
-------
72+
children : list
73+
A list of the nodes which are instances of the given classes or
74+
their subclasses.
75+
76+
"""
77+
obj = cls(document, classes=classes)
78+
parent.walkabout(obj)
79+
return obj.nodes
80+
81+
82+
def build_test_doc(src_dir, build_dir, builder='html'):
83+
"""Build a test document.
84+
85+
Parameters
86+
----------
87+
src_dir : pathlib.Path
88+
The location of the sources.
89+
build_dir : pathlib.Path
90+
The build directory to use.
91+
builder : str
92+
Which builder to use.
93+
94+
Returns
95+
-------
96+
app : sphinx.application.Sphinx
97+
The Sphinx application that built the document.
98+
99+
"""
100+
doctree_dir = build_dir / "doctrees"
101+
output_dir = build_dir / "html"
102+
103+
# Avoid some internal Sphinx deprecation warnings being turned into errors.
104+
with warnings.catch_warnings():
105+
warnings.simplefilter('ignore')
106+
app = Sphinx(src_dir, src_dir, output_dir, doctree_dir, builder)
107+
app.build()
108+
return app
109+
110+
111+
def test_plot_directive_caption(tmpdir):
112+
"""Test the :caption: option of the plot directive.
113+
114+
"""
115+
# Build the test document.
116+
localsrc = SRCDIR / "plot_directive_caption"
117+
build_dir = pathlib.Path(tmpdir)
118+
app = build_test_doc(localsrc, build_dir)
119+
120+
# Get the main document and filter out the figures in it.
121+
index = app.env.get_doctree('index')
122+
figures = NodeFilter.filter_children(index, index, [figure])
123+
124+
# The captions we expect to find.
125+
expected = [
126+
None,
127+
'Caption for inline plot.',
128+
None,
129+
'This is a caption in the content.',
130+
'This is a caption in the options.',
131+
'The content caption should be used instead.',
132+
]
133+
134+
# N.B., each plot directive generates two figures:
135+
# one HTML only and one for other builders.
136+
assert len(figures) == 2 * len(expected), \
137+
"Wrong number of figures in document."
138+
139+
# Check the caption nodes are correct.
140+
for i, figurenode in enumerate(figures):
141+
n = i // 2
142+
captions = NodeFilter.filter_children(index, figurenode, [caption])
143+
144+
if expected[n]:
145+
assert len(captions) > 0, f"Figure {n+1}: no caption found."
146+
assert len(captions) < 2, f"Figure {n+1}: too many captions."
147+
assert captions[0].astext().strip() == expected[n], \
148+
f"Figure {n+1}: wrong caption"
149+
else:
150+
assert len(captions) == 0, f"Figure {n+1}: unexpected caption."
151+
152+
14153
def test_tinypages(tmpdir):
15154
html_dir = pjoin(str(tmpdir), 'html')
16155
doctree_dir = pjoin(str(tmpdir), 'doctrees')

0 commit comments

Comments
 (0)