Skip to content
Open
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
31 changes: 21 additions & 10 deletions lib/matplotlib/sphinxext/plot_directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@
The plot_srcset option is incompatible with *singlehtml* builds, and an
error will be raised.

plot_exclude_patterns
List of non-recursive glob-style patterns for selectively skipping plot
directives. If any pattern matches the relative path of a documentation
file, then plot directives in that file will be skipped.
Matches are computed using :external+python:meth:`pathlib.PurePath.match`.

Notes on how it works
---------------------

Expand Down Expand Up @@ -323,6 +329,7 @@ def setup(app):
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.add_config_value('plot_exclude_patterns', [], True)
app.connect('doctree-read', mark_plot_labels)
app.add_css_file('plot_directive.css')
app.connect('build-finished', _copy_css_file)
Expand Down Expand Up @@ -925,16 +932,20 @@ def run(arguments, content, options, state_machine, state, lineno):

# 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)
if any([Path(source_rel_name).match(pattern) for pattern in
config.plot_exclude_patterns]):
results = [(code, [])]
else:
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
Expand Down
92 changes: 92 additions & 0 deletions lib/matplotlib/tests/test_sphinxext.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,95 @@ def plot_file(num, suff=''):
st = ('srcset="../_images/nestedpage2-index-2.png, '
'../_images/nestedpage2-index-2.2x.png 2.00x"')
assert st in (html_dir / 'nestedpage2/index.html').read_text(encoding='utf-8')


@pytest.mark.parametrize('plot_exclude_patterns', [False, "*index*",
"index*", "ndex*", "?ndex*",
"*nonmatch*",
"range*", "range6*",
"index*,range*", "*",
"*script*"])
def test_plot_exclude_patterns(tmp_path, plot_exclude_patterns):
# test that modifying plot_exclude_patterns in config leads to skipping files
shutil.copyfile(tinypages / 'conf.py', tmp_path / 'conf.py')
shutil.copytree(tinypages / '_static', tmp_path / '_static')
shutil.copyfile(tinypages / 'range4.py', tmp_path / 'range4.py')
shutil.copyfile(tinypages / 'range6.py', tmp_path / 'range6.py')

html_dir = tmp_path / '_build' / 'html'
img_dir = html_dir / '_images'
doctree_dir = tmp_path / 'doctrees'

(tmp_path / 'index.rst').write_text("""
.. plot::

plt.plot(range(2))

.. toctree::

script_func
script_nofunc
""")
(tmp_path / 'script_func.rst').write_text("""
##########
Some plots
##########

.. plot:: range6.py range6

.. plot:: range6.py range10
""")
(tmp_path / 'script_nofunc.rst').write_text("""
##########
Some plots
##########

.. plot:: range4.py
""")

if plot_exclude_patterns is not False:
extra_args = ["-D", f"plot_exclude_patterns={plot_exclude_patterns}"]
else:
extra_args = []
# Build the pages with warnings turned into errors
build_sphinx_html(tmp_path, doctree_dir, html_dir,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm slightly worried on the runtime cost 12x (parametrized) building a documentation seems quite expensive. Have you tested locally how long this test takes?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

When I run this locally with pytest lib/matplotlib/tests/test_sphinxext.py -k 'exclude_patterns' -n 0, it takes a total of 23 seconds, with the call to each test taking 1.5 to 3 seconds:

image

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is on my laptop, not a workstation or cluster

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I appreciate the rigor of your testing, but that’s is too much. Imagine a developer runs the whole test suite, they’ll have to wait 23s longer just so that this comparably niche feature is tested.

Please condense to one test with a single documentation build. A basic test that this works is sufficient. By adding some variants to the list you can still test the most relevant cases: exclude by full filename, exclude full directory, exclude files with a specific pattern, e.g. a specific prefix.

extra_args=extra_args)

# default behavior and non-matching patterns mean all plots created. while script
# matches the name of the rst file, it does not match the relative path (which comes
# from the name of the script containing the plotting function), and thus doesn't
# match. ndex* doesn't match because we're not matching substrings, and thus need
# wildcards for missing characters
if plot_exclude_patterns in ["*script*", "*nonmatch*", False, "ndex*"]:
assert (img_dir / "index-1.png").exists()
assert (img_dir / "range6_range6.png").exists()
assert (img_dir / "range6_range10.png").exists()
assert (img_dir / "range4.png").exists()
# relative path is the name of the rst file when it contains plotting code. thus,
# index is skipped. we match against relative path
elif plot_exclude_patterns in ["*index*", "index*", "?ndex*"]:
assert not (img_dir / "index-1.png").exists()
assert (img_dir / "range6_range6.png").exists()
assert (img_dir / "range6_range10.png").exists()
assert (img_dir / "range4.png").exists()
# name of the script used by the plot directive in the scripts rst files all match
# this pattern and thus are all skipped
elif plot_exclude_patterns == "range*":
assert (img_dir / "index-1.png").exists()
assert not (img_dir / "range6_range6.png").exists()
assert not (img_dir / "range6_range10.png").exists()
assert not (img_dir / "range4.png").exists()
# matches the name of one script, but not the other
elif plot_exclude_patterns == "range6*":
assert (img_dir / "index-1.png").exists()
assert not (img_dir / "range6_range6.png").exists()
assert not (img_dir / "range6_range10.png").exists()
assert (img_dir / "range4.png").exists()
# matches all relative paths, so no images created.
elif plot_exclude_patterns in ["index*,range*", "*"]:
assert not (img_dir / "index-1.png").exists()
assert not (img_dir / "range6_range6.png").exists()
assert not (img_dir / "range6_range10.png").exists()
assert not (img_dir / "range4.png").exists()
else:
raise ValueError(f"unsure how to check {plot_exclude_patterns=}")
Loading