From f8a4c445b639cc582a9bd095319010713db7e1b5 Mon Sep 17 00:00:00 2001 From: "William F. Broderick" Date: Tue, 10 Mar 2026 16:10:30 -0400 Subject: [PATCH 1/8] adds plot_exclude_patterns to plot_directive.py --- lib/matplotlib/sphinxext/plot_directive.py | 32 +++++++++++++++------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 7b46b3145e2b..5c2457696131 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -162,6 +162,11 @@ The plot_srcset option is incompatible with *singlehtml* builds, and an error will be raised. +plot_exclude_patterns + List of regex patterns to check against to selectively skip plot + directives. If any pattern matches the name of the file containing the + plot directive, then that plot directive will be skipped. + Notes on how it works --------------------- @@ -323,6 +328,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) @@ -925,16 +931,22 @@ 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([re.match(pattern, output_base) for pattern in + config.plot_exclude_patterns]): + results = [(code, [])] + print(f"skipping {output_base}") + else: + print(f"not skipping {output_base}") + 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 From ce34a45aaa822184a729dbfcde07c64312faa1fc Mon Sep 17 00:00:00 2001 From: "William F. Broderick" Date: Tue, 10 Mar 2026 16:10:57 -0400 Subject: [PATCH 2/8] adds tests for plot_exclude_patterns --- lib/matplotlib/tests/test_sphinxext.py | 95 ++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index c6f4e13c74c2..f0cb2c24a361 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -269,3 +269,98 @@ 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", "nonmatch", + "range", "range6", + "index,range", ".*", + "_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, + extra_args=extra_args) + + print(list(img_dir.glob("*"))) + # default behavior and non-matching patterns mean all plots created. while script + # matches the name of the rst file, it does not match the basename (which comes from + # the name of the script containing the plotting function), and thus doesn't match + if plot_exclude_patterns in ["script", "nonmatch", False]: + 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() + # basename is the name of the rst file when it contains plotting code. thus, index + # is skipped + elif plot_exclude_patterns == "index": + 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 basenames, so no images created. empty pattern is, to me a weird edge + # case: it matches all strings + 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() + # we match against the basename, not the function name, so this doesn't match + # anything + elif plot_exclude_patterns == "_range": + 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() From 292d4632069228534964db1b7e117a8fb90d4e65 Mon Sep 17 00:00:00 2001 From: "William F. Broderick" Date: Tue, 10 Mar 2026 16:21:33 -0400 Subject: [PATCH 3/8] remove print debug statements --- lib/matplotlib/sphinxext/plot_directive.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 5c2457696131..a1e809ffedef 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -934,9 +934,7 @@ def run(arguments, content, options, state_machine, state, lineno): if any([re.match(pattern, output_base) for pattern in config.plot_exclude_patterns]): results = [(code, [])] - print(f"skipping {output_base}") else: - print(f"not skipping {output_base}") results = render_figures(code=code, code_path=source_file_name, output_dir=build_dir, From fdf28e3266e87321653ef2bcbc7846b6a2607f9e Mon Sep 17 00:00:00 2001 From: "William F. Broderick" Date: Wed, 11 Mar 2026 10:57:02 -0400 Subject: [PATCH 4/8] removes print debug --- lib/matplotlib/tests/test_sphinxext.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index f0cb2c24a361..64833312e7e1 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -321,7 +321,6 @@ def test_plot_exclude_patterns(tmp_path, plot_exclude_patterns): build_sphinx_html(tmp_path, doctree_dir, html_dir, extra_args=extra_args) - print(list(img_dir.glob("*"))) # default behavior and non-matching patterns mean all plots created. while script # matches the name of the rst file, it does not match the basename (which comes from # the name of the script containing the plotting function), and thus doesn't match From 8f8befa8d5fa127c6a546d7b31bd7f65268cf2a4 Mon Sep 17 00:00:00 2001 From: "William F. Broderick" Date: Wed, 11 Mar 2026 11:33:34 -0400 Subject: [PATCH 5/8] switch to fnmatch, relative path --- lib/matplotlib/sphinxext/plot_directive.py | 9 +++--- lib/matplotlib/tests/test_sphinxext.py | 37 ++++++++++++---------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index a1e809ffedef..96ce41661b21 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -163,9 +163,9 @@ error will be raised. plot_exclude_patterns - List of regex patterns to check against to selectively skip plot - directives. If any pattern matches the name of the file containing the - plot directive, then that plot directive will be skipped. + List of wildcard (fnmatch) patterns to check against to selectively skip + plot directives. If any pattern matches the relative path of the file + containing the plot directive, then that plot directive will be skipped. Notes on how it works --------------------- @@ -185,6 +185,7 @@ from collections import defaultdict import contextlib import doctest +import fnmatch from io import StringIO import itertools import os @@ -931,7 +932,7 @@ def run(arguments, content, options, state_machine, state, lineno): # make figures try: - if any([re.match(pattern, output_base) for pattern in + if any([fnmatch.fnmatch(source_rel_name, pattern) for pattern in config.plot_exclude_patterns]): results = [(code, [])] else: diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index 64833312e7e1..31d6f1e23271 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -271,10 +271,12 @@ def plot_file(num, suff=''): assert st in (html_dir / 'nestedpage2/index.html').read_text(encoding='utf-8') -@pytest.mark.parametrize('plot_exclude_patterns', [False, "index", "nonmatch", - "range", "range6", - "index,range", ".*", - "_range", "script", ""]) +@pytest.mark.parametrize('plot_exclude_patterns', [False, "*index*", + "index*", "ndex*", "?ndex*", + "*nonmatch*", + "*range*", "*range6*", + "*index*,*range*", "*", + "*_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') @@ -322,43 +324,44 @@ def test_plot_exclude_patterns(tmp_path, plot_exclude_patterns): 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 basename (which comes from - # the name of the script containing the plotting function), and thus doesn't match - if plot_exclude_patterns in ["script", "nonmatch", False]: + # 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() - # basename is the name of the rst file when it contains plotting code. thus, index - # is skipped - elif plot_exclude_patterns == "index": + # 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": + 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": + 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 basenames, so no images created. empty pattern is, to me a weird edge - # case: it matches all strings - elif plot_exclude_patterns in ["index,range", ".*", ""]: + # 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() - # we match against the basename, not the function name, so this doesn't match + # we match against the relative path, not the function name, so this doesn't match # anything - elif plot_exclude_patterns == "_range": + elif plot_exclude_patterns == "*_range*": assert (img_dir / "index-1.png").exists() assert (img_dir / "range6_range6.png").exists() assert (img_dir / "range6_range10.png").exists() From 36024626bcd73909fcaa6bc56dc998651b841129 Mon Sep 17 00:00:00 2001 From: "William F. Broderick" Date: Fri, 20 Mar 2026 16:43:03 -0400 Subject: [PATCH 6/8] replace fnmatch with Path.match --- lib/matplotlib/sphinxext/plot_directive.py | 8 ++++---- lib/matplotlib/tests/test_sphinxext.py | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 96ce41661b21..33f17656cd75 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -163,9 +163,10 @@ error will be raised. plot_exclude_patterns - List of wildcard (fnmatch) patterns to check against to selectively skip - plot directives. If any pattern matches the relative path of the file + List of non-recursive glob-style patterns to check against to selectively + skip plot directives. If any pattern matches the relative path of the file containing the plot directive, then that plot directive will be skipped. + Matches are computed using :external+python:meth:`pathlib.PurePath.match`. Notes on how it works --------------------- @@ -185,7 +186,6 @@ from collections import defaultdict import contextlib import doctest -import fnmatch from io import StringIO import itertools import os @@ -932,7 +932,7 @@ def run(arguments, content, options, state_machine, state, lineno): # make figures try: - if any([fnmatch.fnmatch(source_rel_name, pattern) for pattern in + if any([Path(source_rel_name).match(pattern) for pattern in config.plot_exclude_patterns]): results = [(code, [])] else: diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index 31d6f1e23271..88103bffc4e7 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -274,9 +274,9 @@ def plot_file(num, suff=''): @pytest.mark.parametrize('plot_exclude_patterns', [False, "*index*", "index*", "ndex*", "?ndex*", "*nonmatch*", - "*range*", "*range6*", - "*index*,*range*", "*", - "*_range*", "*script*", ""]) + "range*", "range6*", + "index*,range*", "*", + "*_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') @@ -328,7 +328,7 @@ def test_plot_exclude_patterns(tmp_path, plot_exclude_patterns): # 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*"]: + 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() @@ -342,19 +342,19 @@ def test_plot_exclude_patterns(tmp_path, plot_exclude_patterns): 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*": + 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*": + 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*", "*"]: + 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() From 8378b29da27ea91ca7e5703155b1d0c54067e395 Mon Sep 17 00:00:00 2001 From: "William F. Broderick" Date: Wed, 8 Apr 2026 10:35:10 -0400 Subject: [PATCH 7/8] remove '*_range*' test, add else block --- lib/matplotlib/tests/test_sphinxext.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index 88103bffc4e7..ae287f89a783 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -276,7 +276,7 @@ def plot_file(num, suff=''): "*nonmatch*", "range*", "range6*", "index*,range*", "*", - "*_range*", "*script*"]) + "*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') @@ -359,10 +359,5 @@ def test_plot_exclude_patterns(tmp_path, plot_exclude_patterns): assert not (img_dir / "range6_range6.png").exists() assert not (img_dir / "range6_range10.png").exists() assert not (img_dir / "range4.png").exists() - # we match against the relative path, not the function name, so this doesn't match - # anything - elif plot_exclude_patterns == "*_range*": - 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() + else: + raise ValueError(f"unsure how to check {plot_exclude_patterns=}") From b67b208bb491cd4e27e768b1df0838b0e81bbed1 Mon Sep 17 00:00:00 2001 From: "William F. Broderick" Date: Fri, 10 Apr 2026 12:05:58 -0400 Subject: [PATCH 8/8] Update lib/matplotlib/sphinxext/plot_directive.py Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/sphinxext/plot_directive.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 33f17656cd75..6faedd66a8e6 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -163,9 +163,9 @@ error will be raised. plot_exclude_patterns - List of non-recursive glob-style patterns to check against to selectively - skip plot directives. If any pattern matches the relative path of the file - containing the plot directive, then that plot directive will be skipped. + 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