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

Skip to content

Commit 99e2e27

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 99e2e27

File tree

5 files changed

+230
-1
lines changed

5 files changed

+230
-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: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,155 @@
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
911

10-
12+
# Only run the tests if Sphinx is installed.
1113
pytest.importorskip('sphinx')
1214

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

14155
def test_tinypages(tmpdir):
15156
html_dir = pjoin(str(tmpdir), 'html')

0 commit comments

Comments
 (0)