Skip to content

Commit a4253d5

Browse files
authored
Merge pull request #20374 from yongrenjie/17860-plotdirective-include
Check modification times of included RST files
2 parents 92825fe + 829f92e commit a4253d5

File tree

4 files changed

+110
-26
lines changed

4 files changed

+110
-26
lines changed

lib/matplotlib/sphinxext/plot_directive.py

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -436,14 +436,26 @@ def filenames(self):
436436
return [self.filename(fmt) for fmt in self.formats]
437437

438438

439-
def out_of_date(original, derived):
439+
def out_of_date(original, derived, includes=None):
440440
"""
441-
Return whether *derived* is out-of-date relative to *original*, both of
442-
which are full file paths.
441+
Return whether *derived* is out-of-date relative to *original* or any of
442+
the RST files included in it using the RST include directive (*includes*).
443+
*derived* and *original* are full paths, and *includes* is optionally a
444+
list of full paths which may have been included in the *original*.
443445
"""
444-
return (not os.path.exists(derived) or
445-
(os.path.exists(original) and
446-
os.stat(derived).st_mtime < os.stat(original).st_mtime))
446+
if not os.path.exists(derived):
447+
return True
448+
449+
if includes is None:
450+
includes = []
451+
files_to_check = [original, *includes]
452+
453+
def out_of_date_one(original, derived_mtime):
454+
return (os.path.exists(original) and
455+
derived_mtime < os.stat(original).st_mtime)
456+
457+
derived_mtime = os.stat(derived).st_mtime
458+
return any(out_of_date_one(f, derived_mtime) for f in files_to_check)
447459

448460

449461
class PlotError(RuntimeError):
@@ -539,7 +551,8 @@ def get_plot_formats(config):
539551

540552
def render_figures(code, code_path, output_dir, output_base, context,
541553
function_name, config, context_reset=False,
542-
close_figs=False):
554+
close_figs=False,
555+
code_includes=None):
543556
"""
544557
Run a pyplot script and save the images in *output_dir*.
545558
@@ -556,7 +569,8 @@ def render_figures(code, code_path, output_dir, output_base, context,
556569
all_exists = True
557570
img = ImageFile(output_base, output_dir)
558571
for format, dpi in formats:
559-
if out_of_date(code_path, img.filename(format)):
572+
if context or out_of_date(code_path, img.filename(format),
573+
includes=code_includes):
560574
all_exists = False
561575
break
562576
img.formats.append(format)
@@ -576,7 +590,8 @@ def render_figures(code, code_path, output_dir, output_base, context,
576590
else:
577591
img = ImageFile('%s_%02d' % (output_base, j), output_dir)
578592
for fmt, dpi in formats:
579-
if out_of_date(code_path, img.filename(fmt)):
593+
if context or out_of_date(code_path, img.filename(fmt),
594+
includes=code_includes):
580595
all_exists = False
581596
break
582597
img.formats.append(fmt)
@@ -749,6 +764,25 @@ def run(arguments, content, options, state_machine, state, lineno):
749764
build_dir_link = build_dir
750765
source_link = dest_dir_link + '/' + output_base + source_ext
751766

767+
# get list of included rst files so that the output is updated when any
768+
# plots in the included files change. These attributes are modified by the
769+
# include directive (see the docutils.parsers.rst.directives.misc module).
770+
try:
771+
source_file_includes = [os.path.join(os.getcwd(), t[0])
772+
for t in state.document.include_log]
773+
except AttributeError:
774+
# the document.include_log attribute only exists in docutils >=0.17,
775+
# before that we need to inspect the state machine
776+
possible_sources = {os.path.join(setup.confdir, t[0])
777+
for t in state_machine.input_lines.items}
778+
source_file_includes = [f for f in possible_sources
779+
if os.path.isfile(f)]
780+
# remove the source file itself from the includes
781+
try:
782+
source_file_includes.remove(source_file_name)
783+
except ValueError:
784+
pass
785+
752786
# make figures
753787
try:
754788
results = render_figures(code,
@@ -759,7 +793,8 @@ def run(arguments, content, options, state_machine, state, lineno):
759793
function_name,
760794
config,
761795
context_reset=context_opt == 'reset',
762-
close_figs=context_opt == 'close-figs')
796+
close_figs=context_opt == 'close-figs',
797+
code_includes=source_file_includes)
763798
errors = []
764799
except PlotError as err:
765800
reporter = state.memo.reporter

lib/matplotlib/tests/test_sphinxext.py

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import filecmp
44
import os
55
from pathlib import Path
6+
import shutil
67
from subprocess import Popen, PIPE
78
import sys
89

@@ -13,27 +14,21 @@
1314

1415

1516
def test_tinypages(tmpdir):
16-
tmp_path = Path(tmpdir)
17-
html_dir = tmp_path / 'html'
18-
doctree_dir = tmp_path / 'doctrees'
19-
# Build the pages with warnings turned into errors
20-
cmd = [sys.executable, '-msphinx', '-W', '-b', 'html',
21-
'-d', str(doctree_dir),
22-
str(Path(__file__).parent / 'tinypages'), str(html_dir)]
23-
proc = Popen(cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True,
24-
env={**os.environ, "MPLBACKEND": ""})
25-
out, err = proc.communicate()
17+
source_dir = Path(tmpdir) / 'src'
18+
shutil.copytree(Path(__file__).parent / 'tinypages', source_dir)
19+
html_dir = source_dir / '_build' / 'html'
20+
doctree_dir = source_dir / 'doctrees'
2621

27-
assert proc.returncode == 0, \
28-
f"sphinx build failed with stdout:\n{out}\nstderr:\n{err}\n"
29-
if err:
30-
pytest.fail(f"sphinx build emitted the following warnings:\n{err}")
31-
32-
assert html_dir.is_dir()
22+
# Build the pages with warnings turned into errors
23+
build_sphinx_html(source_dir, doctree_dir, html_dir)
3324

3425
def plot_file(num):
3526
return html_dir / f'some_plots-{num}.png'
3627

28+
def plot_directive_file(num):
29+
# This is always next to the doctree dir.
30+
return doctree_dir.parent / 'plot_directive' / f'some_plots-{num}.png'
31+
3732
range_10, range_6, range_4 = [plot_file(i) for i in range(1, 4)]
3833
# Plot 5 is range(6) plot
3934
assert filecmp.cmp(range_6, plot_file(5))
@@ -48,6 +43,7 @@ def plot_file(num):
4843
assert filecmp.cmp(range_4, plot_file(13))
4944
# Plot 14 has included source
5045
html_contents = (html_dir / 'some_plots.html').read_bytes()
46+
5147
assert b'# Only a comment' in html_contents
5248
# check plot defined in external file.
5349
assert filecmp.cmp(range_4, html_dir / 'range4.png')
@@ -62,3 +58,45 @@ def plot_file(num):
6258
assert b'plot-directive my-class my-other-class' in html_contents
6359
# check that the multi-image caption is applied twice
6460
assert html_contents.count(b'This caption applies to both plots.') == 2
61+
# Plot 21 is range(6) plot via an include directive. But because some of
62+
# the previous plots are repeated, the argument to plot_file() is only 17.
63+
assert filecmp.cmp(range_6, plot_file(17))
64+
65+
# Modify the included plot
66+
contents = (source_dir / 'included_plot_21.rst').read_text()
67+
contents = contents.replace('plt.plot(range(6))', 'plt.plot(range(4))')
68+
(source_dir / 'included_plot_21.rst').write_text(contents)
69+
# Build the pages again and check that the modified file was updated
70+
modification_times = [plot_directive_file(i).stat().st_mtime
71+
for i in (1, 2, 3, 5)]
72+
build_sphinx_html(source_dir, doctree_dir, html_dir)
73+
assert filecmp.cmp(range_4, plot_file(17))
74+
# Check that the plots in the plot_directive folder weren't changed.
75+
# (plot_directive_file(1) won't be modified, but it will be copied to html/
76+
# upon compilation, so plot_file(1) will be modified)
77+
assert plot_directive_file(1).stat().st_mtime == modification_times[0]
78+
assert plot_directive_file(2).stat().st_mtime == modification_times[1]
79+
assert plot_directive_file(3).stat().st_mtime == modification_times[2]
80+
assert filecmp.cmp(range_10, plot_file(1))
81+
assert filecmp.cmp(range_6, plot_file(2))
82+
assert filecmp.cmp(range_4, plot_file(3))
83+
# Make sure that figures marked with context are re-created (but that the
84+
# contents are the same)
85+
assert plot_directive_file(5).stat().st_mtime > modification_times[3]
86+
assert filecmp.cmp(range_6, plot_file(5))
87+
88+
89+
def build_sphinx_html(source_dir, doctree_dir, html_dir):
90+
# Build the pages with warnings turned into errors
91+
cmd = [sys.executable, '-msphinx', '-W', '-b', 'html',
92+
'-d', str(doctree_dir), str(source_dir), str(html_dir)]
93+
proc = Popen(cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True,
94+
env={**os.environ, "MPLBACKEND": ""})
95+
out, err = proc.communicate()
96+
97+
assert proc.returncode == 0, \
98+
f"sphinx build failed with stdout:\n{out}\nstderr:\n{err}\n"
99+
if err:
100+
pytest.fail(f"sphinx build emitted the following warnings:\n{err}")
101+
102+
assert html_dir.is_dir()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Plot 21 has length 6
2+
3+
.. plot::
4+
5+
plt.plot(range(6))
6+

lib/matplotlib/tests/tinypages/some_plots.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,8 @@ scenario:
168168

169169
plt.figure()
170170
plt.plot(range(4))
171+
172+
Plot 21 is generated via an include directive:
173+
174+
.. include:: included_plot_21.rst
175+

0 commit comments

Comments
 (0)