From 615017a31e49cfbe542d11c49428822f46ae1232 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Wed, 8 May 2024 14:28:21 -0600 Subject: [PATCH 01/20] Add an output-base-name option to the Sphinx plot directive This allows specifying the output base name of the generated image files. The name can include '{counter}', which is automatically string formatted to an incrementing counter. The default if it is not specified is left intact as the current behavior, which is to use the base name of the provided script or the RST document. This is required to use the plot directive with MyST, because the directive is broken with MyST (an issue I don't want to fix), requiring the use of eval-rst. But the way eval-rst works, the incrementing counter is not maintained across different eval-rst directives, meaning if you try to include multiple of them in the same document, the images will overwrite each other. This allows you to manually work around this with something like ```{eval-rst} .. plot:: :output-base-name: plot-1 ... ``` ```{eval-rst} .. plot:: :output-base-name: plot-2 ... ``` Aside from this, it's generally useful to be able to specify the image name used for a plot, as a more informative name can be used rather than just '-1.png'. --- lib/matplotlib/sphinxext/plot_directive.py | 26 ++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 65b25fb913a5..249979c942ad 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -47,6 +47,15 @@ The ``.. plot::`` directive supports the following options: +``:output-base-name:`` : str + The base name (without the extension) of the outputted image files. The + default is to use the same name as the input script, or the name of + the RST document if no script is provided. The string can include the + format ``{counter}`` to use an incremented counter. For example, + ``'plot-{counter}'`` will create files like ``plot-1.png``, ``plot-2.png``, + and so on. If the ``{counter}`` is not provided, two plots with the same + output-base-name may overwrite each other. + ``:format:`` : {'python', 'doctest'} The format of the input. If unset, the format is auto-detected. @@ -88,6 +97,10 @@ The plot directive has the following configuration options: +plot_output_base_name + Default value for the output-base-name option (default is to use the name + of the input script, or the name of the RST file if no script is provided) + plot_include_source Default value for the include-source option (default: False). @@ -265,6 +278,7 @@ class PlotDirective(Directive): 'scale': directives.nonnegative_int, 'align': Image.align, 'class': directives.class_option, + 'output-base-name': directives.unchanged, 'include-source': _option_boolean, 'show-source-link': _option_boolean, 'format': _option_format, @@ -299,6 +313,7 @@ def setup(app): app.add_config_value('plot_pre_code', None, True) app.add_config_value('plot_include_source', False, True) app.add_config_value('plot_html_show_source_link', True, True) + app.add_config_value('plot_output_base_name', None, True) app.add_config_value('plot_formats', ['png', 'hires.png', 'pdf'], True) app.add_config_value('plot_basedir', None, True) app.add_config_value('plot_html_show_formats', True, True) @@ -734,6 +749,7 @@ def run(arguments, content, options, state_machine, state, lineno): options.setdefault('include-source', config.plot_include_source) options.setdefault('show-source-link', config.plot_html_show_source_link) + options.setdefault('output-base-name', config.plot_output_base_name) if 'class' in options: # classes are parsed into a list of string, and output by simply @@ -775,14 +791,20 @@ def run(arguments, content, options, state_machine, state, lineno): function_name = None code = Path(source_file_name).read_text(encoding='utf-8') - output_base = os.path.basename(source_file_name) + if options['output-base-name']: + output_base = options['output-base-name'] + else: + output_base = os.path.basename(source_file_name) else: source_file_name = rst_file code = textwrap.dedent("\n".join(map(str, content))) counter = document.attributes.get('_plot_counter', 0) + 1 document.attributes['_plot_counter'] = counter base, ext = os.path.splitext(os.path.basename(source_file_name)) - output_base = '%s-%d.py' % (base, counter) + if options['output-base-name']: + output_base = options['output-base-name'].format(counter=counter) + else: + output_base = '%s-%d.py' % (base, counter) function_name = None caption = options.get('caption', '') From b4329626dde5665918c4b42fe6e9669d68aa4c73 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Wed, 8 May 2024 23:15:19 -0600 Subject: [PATCH 02/20] Add tests for output-base-name --- lib/matplotlib/tests/test_sphinxext.py | 3 +++ lib/matplotlib/tests/tinypages/some_plots.rst | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index 6624e3b17ba5..29d6f3168621 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -97,6 +97,9 @@ def plot_directive_file(num): assert filecmp.cmp(range_6, plot_file(17)) # plot 22 is from the range6.py file again, but a different function assert filecmp.cmp(range_10, img_dir / 'range6_range10.png') + # plots 23 and 24 use a custom base name with {counter} + assert filecmp.cmp(range_4, img_dir / 'custom-base-name-18.png') + assert filecmp.cmp(range_6, img_dir / 'custom-base-name-19.png') # Modify the included plot contents = (tmp_path / 'included_plot_21.rst').read_bytes() diff --git a/lib/matplotlib/tests/tinypages/some_plots.rst b/lib/matplotlib/tests/tinypages/some_plots.rst index dd1f79892b0e..b484d705ae1c 100644 --- a/lib/matplotlib/tests/tinypages/some_plots.rst +++ b/lib/matplotlib/tests/tinypages/some_plots.rst @@ -174,3 +174,15 @@ Plot 21 is generated via an include directive: Plot 22 uses a different specific function in a file with plot commands: .. plot:: range6.py range10 + +Plots 23 and 24 use output-base-name with a {counter}. + +.. plot:: + :output-base-name: custom-base-name-{counter} + + plt.plot(range(4)) + +.. plot:: + :output-base-name: custom-base-name-{counter} + + plt.plot(range(6)) From 426abc7ec938f8de77eb5eb0760d262bfe3f5491 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Fri, 11 Oct 2024 20:56:44 +0200 Subject: [PATCH 03/20] Remove {counter} from output-base-name and remove the global config The idea of allowing the use of the counter for custom base names is flawed because the counter would always be incremented even for custom names that don't use it. Also, the global base name is difficult to get working because we don't have a good global view of the plots to create a counter, especially in the case of partial rebuilds. Instead, we just simplify things by just allowing setting a custom-base-name. If two plot directives use the same custom-base-name, then one will end up overwriting the other (I'm still looking into whether it's possible to at least detect this and give an error). --- lib/matplotlib/sphinxext/plot_directive.py | 18 +++++------------- lib/matplotlib/tests/test_sphinxext.py | 6 +++--- lib/matplotlib/tests/tinypages/some_plots.rst | 6 +++--- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 249979c942ad..4e7b0ffd20c7 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -50,10 +50,7 @@ ``:output-base-name:`` : str The base name (without the extension) of the outputted image files. The default is to use the same name as the input script, or the name of - the RST document if no script is provided. The string can include the - format ``{counter}`` to use an incremented counter. For example, - ``'plot-{counter}'`` will create files like ``plot-1.png``, ``plot-2.png``, - and so on. If the ``{counter}`` is not provided, two plots with the same + the RST document if no script is provided. Note: two plots with the same output-base-name may overwrite each other. ``:format:`` : {'python', 'doctest'} @@ -97,10 +94,6 @@ The plot directive has the following configuration options: -plot_output_base_name - Default value for the output-base-name option (default is to use the name - of the input script, or the name of the RST file if no script is provided) - plot_include_source Default value for the include-source option (default: False). @@ -313,7 +306,6 @@ def setup(app): app.add_config_value('plot_pre_code', None, True) app.add_config_value('plot_include_source', False, True) app.add_config_value('plot_html_show_source_link', True, True) - app.add_config_value('plot_output_base_name', None, True) app.add_config_value('plot_formats', ['png', 'hires.png', 'pdf'], True) app.add_config_value('plot_basedir', None, True) app.add_config_value('plot_html_show_formats', True, True) @@ -749,7 +741,7 @@ def run(arguments, content, options, state_machine, state, lineno): options.setdefault('include-source', config.plot_include_source) options.setdefault('show-source-link', config.plot_html_show_source_link) - options.setdefault('output-base-name', config.plot_output_base_name) + options.setdefault('output-base-name', None) if 'class' in options: # classes are parsed into a list of string, and output by simply @@ -798,12 +790,12 @@ def run(arguments, content, options, state_machine, state, lineno): else: source_file_name = rst_file code = textwrap.dedent("\n".join(map(str, content))) - counter = document.attributes.get('_plot_counter', 0) + 1 - document.attributes['_plot_counter'] = counter base, ext = os.path.splitext(os.path.basename(source_file_name)) if options['output-base-name']: - output_base = options['output-base-name'].format(counter=counter) + output_base = options['output-base-name'] else: + counter = document.attributes.get('_plot_counter', 0) + 1 + document.attributes['_plot_counter'] = counter output_base = '%s-%d.py' % (base, counter) function_name = None caption = options.get('caption', '') diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index 29d6f3168621..3555ab3462c3 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -97,9 +97,9 @@ def plot_directive_file(num): assert filecmp.cmp(range_6, plot_file(17)) # plot 22 is from the range6.py file again, but a different function assert filecmp.cmp(range_10, img_dir / 'range6_range10.png') - # plots 23 and 24 use a custom base name with {counter} - assert filecmp.cmp(range_4, img_dir / 'custom-base-name-18.png') - assert filecmp.cmp(range_6, img_dir / 'custom-base-name-19.png') + # plots 23 and 24 use a custom base name + assert filecmp.cmp(range_4, img_dir / 'custom-base-name-1.png') + assert filecmp.cmp(range_6, img_dir / 'custom-base-name-2.png') # Modify the included plot contents = (tmp_path / 'included_plot_21.rst').read_bytes() diff --git a/lib/matplotlib/tests/tinypages/some_plots.rst b/lib/matplotlib/tests/tinypages/some_plots.rst index b484d705ae1c..05a7fc34c92a 100644 --- a/lib/matplotlib/tests/tinypages/some_plots.rst +++ b/lib/matplotlib/tests/tinypages/some_plots.rst @@ -175,14 +175,14 @@ Plot 22 uses a different specific function in a file with plot commands: .. plot:: range6.py range10 -Plots 23 and 24 use output-base-name with a {counter}. +Plots 23 and 24 use output-base-name. .. plot:: - :output-base-name: custom-base-name-{counter} + :output-base-name: custom-base-name-1 plt.plot(range(4)) .. plot:: - :output-base-name: custom-base-name-{counter} + :output-base-name: custom-base-name-2 plt.plot(range(6)) From 8485bfd9f1dcd1feabefb8cda0595a58e73321be Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Fri, 11 Oct 2024 21:41:39 +0200 Subject: [PATCH 04/20] Check for duplicate output-base-name in the Sphinx extension Previously it would just overwrite one plot with the other. I did not add tests for this because I would have to create a whole separate docs build just to test the error (but I have tested it manually). --- lib/matplotlib/sphinxext/plot_directive.py | 45 +++++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 4e7b0ffd20c7..7b3383e34840 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -50,8 +50,8 @@ ``:output-base-name:`` : str The base name (without the extension) of the outputted image files. The default is to use the same name as the input script, or the name of - the RST document if no script is provided. Note: two plots with the same - output-base-name may overwrite each other. + the RST document if no script is provided. The output-base-name for each + plot directive must be unique. ``:format:`` : {'python', 'doctest'} The format of the input. If unset, the format is auto-detected. @@ -171,6 +171,7 @@ and *TEMPLATE_SRCSET*. """ +from collections import defaultdict import contextlib import doctest from io import StringIO @@ -188,6 +189,7 @@ from docutils.parsers.rst.directives.images import Image import jinja2 # Sphinx dependency. +from sphinx.environment.collectors import EnvironmentCollector from sphinx.errors import ExtensionError import matplotlib @@ -319,9 +321,35 @@ def setup(app): app.connect('build-finished', _copy_css_file) metadata = {'parallel_read_safe': True, 'parallel_write_safe': True, 'version': matplotlib.__version__} + app.connect('builder-inited', init_filename_registry) + app.add_env_collector(FilenameCollector) return metadata +# ----------------------------------------------------------------------------- +# Handle Duplicate Filenames +# ----------------------------------------------------------------------------- + +def init_filename_registry(app): + env = app.builder.env + if not hasattr(env, 'mpl_custom_base_names'): + env.mpl_custom_base_names = defaultdict(set) + +class FilenameCollector(EnvironmentCollector): + def process_doc(self, app, doctree): + pass + + def clear_doc(self, app, env, docname): + if docname in env.mpl_custom_base_names: + del env.mpl_custom_base_names[docname] + + def merge_other(self, app, env, docnames, other): + for docname in docnames: + if docname in other.mpl_custom_base_names: + if docname not in env.mpl_custom_base_names: + env.mpl_custom_base_names[docname] = set() + env.mpl_custom_base_names[docname].update(other.mpl_custom_base_names[docname]) + # ----------------------------------------------------------------------------- # Doctest handling # ----------------------------------------------------------------------------- @@ -606,6 +634,16 @@ def _parse_srcset(entries): raise ExtensionError(f'srcset argument {entry!r} is invalid.') return srcset +def check_output_base_name(env, output_base): + docname = env.docname + + for d in env.mpl_custom_base_names: + if output_base in env.mpl_custom_base_names[d]: + if d == docname: + raise PlotError(f"The output-base-name '{output_base}' is used multiple times.") + raise PlotError(f"The output-base-name '{output_base}' is used multiple times (it is also used in {env.doc2path(d)}).") + + env.mpl_custom_base_names[docname].add(output_base) def render_figures(code, code_path, output_dir, output_base, context, function_name, config, context_reset=False, @@ -730,6 +768,7 @@ def render_figures(code, code_path, output_dir, output_base, context, def run(arguments, content, options, state_machine, state, lineno): document = state_machine.document config = document.settings.env.config + env = document.settings.env nofigs = 'nofigs' in options if config.plot_srcset and setup.app.builder.name == 'singlehtml': @@ -785,6 +824,7 @@ def run(arguments, content, options, state_machine, state, lineno): code = Path(source_file_name).read_text(encoding='utf-8') if options['output-base-name']: output_base = options['output-base-name'] + check_output_base_name(env, output_base) else: output_base = os.path.basename(source_file_name) else: @@ -793,6 +833,7 @@ def run(arguments, content, options, state_machine, state, lineno): base, ext = os.path.splitext(os.path.basename(source_file_name)) if options['output-base-name']: output_base = options['output-base-name'] + check_output_base_name(env, output_base) else: counter = document.attributes.get('_plot_counter', 0) + 1 document.attributes['_plot_counter'] = counter From f94a9324036a8da1141e4c275eb231c26c20cccc Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Fri, 11 Oct 2024 19:04:02 -0500 Subject: [PATCH 05/20] Fix flake8 errors --- lib/matplotlib/sphinxext/plot_directive.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 7b3383e34840..abce90dfc0e3 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -335,6 +335,7 @@ def init_filename_registry(app): if not hasattr(env, 'mpl_custom_base_names'): env.mpl_custom_base_names = defaultdict(set) + class FilenameCollector(EnvironmentCollector): def process_doc(self, app, doctree): pass @@ -348,7 +349,8 @@ def merge_other(self, app, env, docnames, other): if docname in other.mpl_custom_base_names: if docname not in env.mpl_custom_base_names: env.mpl_custom_base_names[docname] = set() - env.mpl_custom_base_names[docname].update(other.mpl_custom_base_names[docname]) + env.mpl_custom_base_names[docname].update( + other.mpl_custom_base_names[docname]) # ----------------------------------------------------------------------------- # Doctest handling @@ -634,17 +636,23 @@ def _parse_srcset(entries): raise ExtensionError(f'srcset argument {entry!r} is invalid.') return srcset + def check_output_base_name(env, output_base): docname = env.docname for d in env.mpl_custom_base_names: if output_base in env.mpl_custom_base_names[d]: if d == docname: - raise PlotError(f"The output-base-name '{output_base}' is used multiple times.") - raise PlotError(f"The output-base-name '{output_base}' is used multiple times (it is also used in {env.doc2path(d)}).") + raise PlotError( + f"The output-base-name " + f"{output_base}' is used multiple times.") + raise PlotError(f"The output-base-name " + f"'{output_base}' is used multiple times " + f"(it is also used in {env.doc2path(d)}).") env.mpl_custom_base_names[docname].add(output_base) + def render_figures(code, code_path, output_dir, output_base, context, function_name, config, context_reset=False, close_figs=False, From 8f05ba6eb541cb7f19ec74f8c6e934a32fdd9380 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Sat, 12 Oct 2024 16:31:22 -0600 Subject: [PATCH 06/20] Make an internal class private This will hopefully fix Sphinx trying and failing to include it. --- lib/matplotlib/sphinxext/plot_directive.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index abce90dfc0e3..71fe0e7d7850 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -322,7 +322,7 @@ def setup(app): metadata = {'parallel_read_safe': True, 'parallel_write_safe': True, 'version': matplotlib.__version__} app.connect('builder-inited', init_filename_registry) - app.add_env_collector(FilenameCollector) + app.add_env_collector(_FilenameCollector) return metadata @@ -336,7 +336,7 @@ def init_filename_registry(app): env.mpl_custom_base_names = defaultdict(set) -class FilenameCollector(EnvironmentCollector): +class _FilenameCollector(EnvironmentCollector): def process_doc(self, app, doctree): pass From 1fa88dd1e3936f3fe2847b1b532f8cae214428d1 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Mon, 14 Oct 2024 10:32:31 -0600 Subject: [PATCH 07/20] Fix small code nit --- lib/matplotlib/sphinxext/plot_directive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 71fe0e7d7850..16866459e784 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -838,11 +838,11 @@ def run(arguments, content, options, state_machine, state, lineno): else: source_file_name = rst_file code = textwrap.dedent("\n".join(map(str, content))) - base, ext = os.path.splitext(os.path.basename(source_file_name)) if options['output-base-name']: output_base = options['output-base-name'] check_output_base_name(env, output_base) else: + base, ext = os.path.splitext(os.path.basename(source_file_name)) counter = document.attributes.get('_plot_counter', 0) + 1 document.attributes['_plot_counter'] = counter output_base = '%s-%d.py' % (base, counter) From a22fcc304e251b28b46f9f69884fe053a7790e81 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Mon, 14 Oct 2024 10:32:41 -0600 Subject: [PATCH 08/20] Add a test for output-base-name with a .py file --- lib/matplotlib/tests/test_sphinxext.py | 7 ++++--- lib/matplotlib/tests/tinypages/some_plots.rst | 13 ++++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index 758b440d4dc0..4370d9cea47d 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -96,9 +96,10 @@ def plot_directive_file(num): assert filecmp.cmp(range_6, plot_file(17)) # plot 22 is from the range6.py file again, but a different function assert filecmp.cmp(range_10, img_dir / 'range6_range10.png') - # plots 23 and 24 use a custom base name - assert filecmp.cmp(range_4, img_dir / 'custom-base-name-1.png') - assert filecmp.cmp(range_6, img_dir / 'custom-base-name-2.png') + # plots 23 through 25 use a custom base name + assert filecmp.cmp(range_6, img_dir / 'custom-base-name-6.png') + assert filecmp.cmp(range_10, img_dir / 'custom-base-name-10.png') + assert filecmp.cmp(range_4, img_dir / 'custom-base-name-4.png') # Modify the included plot contents = (tmp_path / 'included_plot_21.rst').read_bytes() diff --git a/lib/matplotlib/tests/tinypages/some_plots.rst b/lib/matplotlib/tests/tinypages/some_plots.rst index 05a7fc34c92a..7652c7301563 100644 --- a/lib/matplotlib/tests/tinypages/some_plots.rst +++ b/lib/matplotlib/tests/tinypages/some_plots.rst @@ -175,14 +175,17 @@ Plot 22 uses a different specific function in a file with plot commands: .. plot:: range6.py range10 -Plots 23 and 24 use output-base-name. +Plots 23 through 25 use output-base-name. .. plot:: - :output-base-name: custom-base-name-1 + :output-base-name: custom-base-name-6 - plt.plot(range(4)) + plt.plot(range(6)) .. plot:: - :output-base-name: custom-base-name-2 + :output-base-name: custom-base-name-10 - plt.plot(range(6)) + plt.plot(range(10)) + +.. plot:: range4.py + :output-base-name: custom-base-name-4 From 86fb16710c43a5e6c2354371ff3124d39178b876 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Thu, 17 Oct 2024 12:37:25 -0600 Subject: [PATCH 09/20] Remove a redundant test --- lib/matplotlib/tests/test_sphinxext.py | 3 +-- lib/matplotlib/tests/tinypages/some_plots.rst | 7 +------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index 4370d9cea47d..64759f650b06 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -96,9 +96,8 @@ def plot_directive_file(num): assert filecmp.cmp(range_6, plot_file(17)) # plot 22 is from the range6.py file again, but a different function assert filecmp.cmp(range_10, img_dir / 'range6_range10.png') - # plots 23 through 25 use a custom base name + # plots 23 and 24 use a custom base name assert filecmp.cmp(range_6, img_dir / 'custom-base-name-6.png') - assert filecmp.cmp(range_10, img_dir / 'custom-base-name-10.png') assert filecmp.cmp(range_4, img_dir / 'custom-base-name-4.png') # Modify the included plot diff --git a/lib/matplotlib/tests/tinypages/some_plots.rst b/lib/matplotlib/tests/tinypages/some_plots.rst index 7652c7301563..51348415f128 100644 --- a/lib/matplotlib/tests/tinypages/some_plots.rst +++ b/lib/matplotlib/tests/tinypages/some_plots.rst @@ -175,17 +175,12 @@ Plot 22 uses a different specific function in a file with plot commands: .. plot:: range6.py range10 -Plots 23 through 25 use output-base-name. +Plots 23 and 24 use output-base-name. .. plot:: :output-base-name: custom-base-name-6 plt.plot(range(6)) -.. plot:: - :output-base-name: custom-base-name-10 - - plt.plot(range(10)) - .. plot:: range4.py :output-base-name: custom-base-name-4 From e0be21e57d68e49d1380bae4af22a44575abca55 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Thu, 17 Oct 2024 12:37:35 -0600 Subject: [PATCH 10/20] Disallow / or . in output-base-name --- lib/matplotlib/sphinxext/plot_directive.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 16866459e784..3e10aac176c7 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -640,6 +640,11 @@ def _parse_srcset(entries): def check_output_base_name(env, output_base): docname = env.docname + if '.' in output_base or '/' in output_base: + raise PlotError( + f"The output-base-name '{output_base}' is invalid. " + f"It must not contain dots or slashes.") + for d in env.mpl_custom_base_names: if output_base in env.mpl_custom_base_names[d]: if d == docname: From f322125f9fdd2734831130de9d975d73bbff7a92 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Fri, 18 Oct 2024 12:27:34 -0600 Subject: [PATCH 11/20] Rename output-base-name to image-basename --- lib/matplotlib/sphinxext/plot_directive.py | 22 +++++++++---------- lib/matplotlib/tests/test_sphinxext.py | 6 ++--- lib/matplotlib/tests/tinypages/some_plots.rst | 6 ++--- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 3e10aac176c7..d4b547686283 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -47,10 +47,10 @@ The ``.. plot::`` directive supports the following options: -``:output-base-name:`` : str +``:image-basename:`` : str The base name (without the extension) of the outputted image files. The default is to use the same name as the input script, or the name of - the RST document if no script is provided. The output-base-name for each + the RST document if no script is provided. The image-basename for each plot directive must be unique. ``:format:`` : {'python', 'doctest'} @@ -273,7 +273,7 @@ class PlotDirective(Directive): 'scale': directives.nonnegative_int, 'align': Image.align, 'class': directives.class_option, - 'output-base-name': directives.unchanged, + 'image-basename': directives.unchanged, 'include-source': _option_boolean, 'show-source-link': _option_boolean, 'format': _option_format, @@ -642,16 +642,16 @@ def check_output_base_name(env, output_base): if '.' in output_base or '/' in output_base: raise PlotError( - f"The output-base-name '{output_base}' is invalid. " + f"The image-basename '{output_base}' is invalid. " f"It must not contain dots or slashes.") for d in env.mpl_custom_base_names: if output_base in env.mpl_custom_base_names[d]: if d == docname: raise PlotError( - f"The output-base-name " + f"The image-basename " f"{output_base}' is used multiple times.") - raise PlotError(f"The output-base-name " + raise PlotError(f"The image-basename " f"'{output_base}' is used multiple times " f"(it is also used in {env.doc2path(d)}).") @@ -793,7 +793,7 @@ def run(arguments, content, options, state_machine, state, lineno): options.setdefault('include-source', config.plot_include_source) options.setdefault('show-source-link', config.plot_html_show_source_link) - options.setdefault('output-base-name', None) + options.setdefault('image-basename', None) if 'class' in options: # classes are parsed into a list of string, and output by simply @@ -835,16 +835,16 @@ def run(arguments, content, options, state_machine, state, lineno): function_name = None code = Path(source_file_name).read_text(encoding='utf-8') - if options['output-base-name']: - output_base = options['output-base-name'] + if options['image-basename']: + output_base = options['image-basename'] check_output_base_name(env, output_base) else: output_base = os.path.basename(source_file_name) else: source_file_name = rst_file code = textwrap.dedent("\n".join(map(str, content))) - if options['output-base-name']: - output_base = options['output-base-name'] + if options['image-basename']: + output_base = options['image-basename'] check_output_base_name(env, output_base) else: base, ext = os.path.splitext(os.path.basename(source_file_name)) diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index 64759f650b06..139bd56d8fe3 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -96,9 +96,9 @@ def plot_directive_file(num): assert filecmp.cmp(range_6, plot_file(17)) # plot 22 is from the range6.py file again, but a different function assert filecmp.cmp(range_10, img_dir / 'range6_range10.png') - # plots 23 and 24 use a custom base name - assert filecmp.cmp(range_6, img_dir / 'custom-base-name-6.png') - assert filecmp.cmp(range_4, img_dir / 'custom-base-name-4.png') + # plots 23 and 24 use a custom basename + assert filecmp.cmp(range_6, img_dir / 'custom-basename-6.png') + assert filecmp.cmp(range_4, img_dir / 'custom-basename-4.png') # Modify the included plot contents = (tmp_path / 'included_plot_21.rst').read_bytes() diff --git a/lib/matplotlib/tests/tinypages/some_plots.rst b/lib/matplotlib/tests/tinypages/some_plots.rst index 51348415f128..85e53086e792 100644 --- a/lib/matplotlib/tests/tinypages/some_plots.rst +++ b/lib/matplotlib/tests/tinypages/some_plots.rst @@ -175,12 +175,12 @@ Plot 22 uses a different specific function in a file with plot commands: .. plot:: range6.py range10 -Plots 23 and 24 use output-base-name. +Plots 23 and 24 use image-basename. .. plot:: - :output-base-name: custom-base-name-6 + :image-basename: custom-basename-6 plt.plot(range(6)) .. plot:: range4.py - :output-base-name: custom-base-name-4 + :image-basename: custom-basename-4 From fc33c3887dd5a8455d06b329a4a32c302771f267 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Mon, 21 Oct 2024 12:30:41 -0600 Subject: [PATCH 12/20] Use a better variable name --- lib/matplotlib/sphinxext/plot_directive.py | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index d4b547686283..535b4b5b5fd9 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -332,8 +332,8 @@ def setup(app): def init_filename_registry(app): env = app.builder.env - if not hasattr(env, 'mpl_custom_base_names'): - env.mpl_custom_base_names = defaultdict(set) + if not hasattr(env, 'mpl_plot_image_basenames'): + env.mpl_plot_image_basenames = defaultdict(set) class _FilenameCollector(EnvironmentCollector): @@ -341,16 +341,16 @@ def process_doc(self, app, doctree): pass def clear_doc(self, app, env, docname): - if docname in env.mpl_custom_base_names: - del env.mpl_custom_base_names[docname] + if docname in env.mpl_plot_image_basenames: + del env.mpl_plot_image_basenames[docname] def merge_other(self, app, env, docnames, other): for docname in docnames: - if docname in other.mpl_custom_base_names: - if docname not in env.mpl_custom_base_names: - env.mpl_custom_base_names[docname] = set() - env.mpl_custom_base_names[docname].update( - other.mpl_custom_base_names[docname]) + if docname in other.mpl_plot_image_basenames: + if docname not in env.mpl_plot_image_basenames: + env.mpl_plot_image_basenames[docname] = set() + env.mpl_plot_image_basenames[docname].update( + other.mpl_plot_image_basenames[docname]) # ----------------------------------------------------------------------------- # Doctest handling @@ -645,8 +645,8 @@ def check_output_base_name(env, output_base): f"The image-basename '{output_base}' is invalid. " f"It must not contain dots or slashes.") - for d in env.mpl_custom_base_names: - if output_base in env.mpl_custom_base_names[d]: + for d in env.mpl_plot_image_basenames: + if output_base in env.mpl_plot_image_basenames[d]: if d == docname: raise PlotError( f"The image-basename " @@ -655,7 +655,7 @@ def check_output_base_name(env, output_base): f"'{output_base}' is used multiple times " f"(it is also used in {env.doc2path(d)}).") - env.mpl_custom_base_names[docname].add(output_base) + env.mpl_plot_image_basenames[docname].add(output_base) def render_figures(code, code_path, output_dir, output_base, context, From 7d416cf7cc90151c80435df583bf16f3524c565f Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Mon, 21 Oct 2024 12:34:48 -0600 Subject: [PATCH 13/20] Simplify logic in merge_other --- lib/matplotlib/sphinxext/plot_directive.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 535b4b5b5fd9..d6034ac9cd9a 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -345,12 +345,10 @@ def clear_doc(self, app, env, docname): del env.mpl_plot_image_basenames[docname] def merge_other(self, app, env, docnames, other): - for docname in docnames: - if docname in other.mpl_plot_image_basenames: - if docname not in env.mpl_plot_image_basenames: - env.mpl_plot_image_basenames[docname] = set() - env.mpl_plot_image_basenames[docname].update( - other.mpl_plot_image_basenames[docname]) + for docname in other.mpl_plot_image_basenames: + env.mpl_plot_image_basenames[docname].update( + other.mpl_plot_image_basenames[docname]) + # ----------------------------------------------------------------------------- # Doctest handling From f654a74e023ec5c1d9b85d95f6097ce31aa00ef9 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Fri, 6 Jun 2025 10:55:06 -0600 Subject: [PATCH 14/20] Various small code cleanups from review Co-authored-by: Elliott Sales de Andrade --- lib/matplotlib/sphinxext/plot_directive.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index fe65a1aa966d..8e5b80ba1599 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -647,11 +647,9 @@ def check_output_base_name(env, output_base): if output_base in env.mpl_plot_image_basenames[d]: if d == docname: raise PlotError( - f"The image-basename " - f"{output_base}' is used multiple times.") - raise PlotError(f"The image-basename " - f"'{output_base}' is used multiple times " - f"(it is also used in {env.doc2path(d)}).") + f"The image-basename {output_base!r} is used multiple times.") + raise PlotError(f"The image-basename {output_base!r} is used multiple" + f"times (it is also used in {env.doc2path(d)}).") env.mpl_plot_image_basenames[docname].add(output_base) @@ -778,8 +776,8 @@ def render_figures(code, code_path, output_dir, output_base, context, def run(arguments, content, options, state_machine, state, lineno): document = state_machine.document - config = document.settings.env.config env = document.settings.env + config = env.config nofigs = 'nofigs' in options if config.plot_srcset and setup.app.builder.name == 'singlehtml': From 20bed265798c1ac62229235ced8ca286154b6b09 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Fri, 6 Jun 2025 11:25:24 -0600 Subject: [PATCH 15/20] Add a test for image-basename with multiple figures --- lib/matplotlib/tests/data/tinypages/some_plots.rst | 11 ++++++++++- lib/matplotlib/tests/test_sphinxext.py | 4 +++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/data/tinypages/some_plots.rst b/lib/matplotlib/tests/data/tinypages/some_plots.rst index b1c7aee18d8c..71a99585dd83 100644 --- a/lib/matplotlib/tests/data/tinypages/some_plots.rst +++ b/lib/matplotlib/tests/data/tinypages/some_plots.rst @@ -180,7 +180,7 @@ Plot 22 uses a different specific function in a file with plot commands: .. plot:: range6.py range10 -Plots 23 and 24 use image-basename. +Plots 23--25 use image-basename. .. plot:: :image-basename: custom-basename-6 @@ -189,3 +189,12 @@ Plots 23 and 24 use image-basename. .. plot:: range4.py :image-basename: custom-basename-4 + +.. plot:: + :image-basename: custom-basename-4-6 + + plt.figure() + plt.plot(range(4)) + + plt.figure() + plt.plot(range(6)) diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index 5efff4b1555f..c3e444d57861 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -92,9 +92,11 @@ def plot_directive_file(num): assert filecmp.cmp(range_6, plot_file(17)) # plot 22 is from the range6.py file again, but a different function assert filecmp.cmp(range_10, img_dir / 'range6_range10.png') - # plots 23 and 24 use a custom basename + # plots 23--25 use a custom basename assert filecmp.cmp(range_6, img_dir / 'custom-basename-6.png') assert filecmp.cmp(range_4, img_dir / 'custom-basename-4.png') + assert filecmp.cmp(range_4, img_dir / 'custom-basename-4-6_00.png') + assert filecmp.cmp(range_6, img_dir / 'custom-basename-4-6_01.png') # Modify the included plot contents = (tmp_path / 'included_plot_21.rst').read_bytes() From 7f56c947f8b001158321e3bd15b8a90c61169302 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Fri, 6 Jun 2025 11:26:48 -0600 Subject: [PATCH 16/20] Disallow \ in output_base in the plot directive --- lib/matplotlib/sphinxext/plot_directive.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 2380c1ddaa28..860525d587c2 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -638,7 +638,7 @@ def _parse_srcset(entries): def check_output_base_name(env, output_base): docname = env.docname - if '.' in output_base or '/' in output_base: + if '.' in output_base or '/' in output_base or '\\' in output_base: raise PlotError( f"The image-basename '{output_base}' is invalid. " f"It must not contain dots or slashes.") From 550e3824c5fdba8777760434154de1b1ba02e795 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Fri, 6 Jun 2025 11:43:29 -0600 Subject: [PATCH 17/20] Fix the source link when using a custom basename to include the .py extension --- lib/matplotlib/sphinxext/plot_directive.py | 4 ++-- lib/matplotlib/tests/test_sphinxext.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 860525d587c2..a9c72683dd9d 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -910,7 +910,7 @@ def run(arguments, content, options, state_machine, state, lineno): # save script (if necessary) if options['show-source-link']: - Path(build_dir, output_base + source_ext).write_text( + Path(build_dir, output_base + (source_ext or '.py')).write_text( doctest.script_from_examples(code) if source_file_name == rst_file and is_doctest else code, @@ -970,7 +970,7 @@ def run(arguments, content, options, state_machine, state, lineno): # Not-None src_name signals the need for a source download in the # generated html if j == 0 and options['show-source-link']: - src_name = output_base + source_ext + src_name = output_base + (source_ext or '.py') else: src_name = None if config.plot_srcset: diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index c3e444d57861..fb287c1d603a 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -181,6 +181,28 @@ def test_show_source_link_false(tmp_path, plot_html_show_source_link): assert len(list(html_dir.glob("**/index-1.py"))) == 0 +def test_plot_html_show_source_link_custom_basename(tmp_path): + # Test that source link filename includes .py extension when using custom basename + shutil.copyfile(tinypages / 'conf.py', tmp_path / 'conf.py') + shutil.copytree(tinypages / '_static', tmp_path / '_static') + doctree_dir = tmp_path / 'doctrees' + (tmp_path / 'index.rst').write_text(""" +.. plot:: + :image-basename: custom-name + + plt.plot(range(2)) +""") + html_dir = tmp_path / '_build' / 'html' + build_sphinx_html(tmp_path, doctree_dir, html_dir) + + # Check that source file with .py extension is generated + assert len(list(html_dir.glob("**/custom-name.py"))) == 1 + + # Check that the HTML contains the correct link with .py extension + html_content = (html_dir / 'index.html').read_text() + assert 'custom-name.py' in html_content + + def test_srcset_version(tmp_path): html_dir = tmp_path / '_build' / 'html' img_dir = html_dir / '_images' From d4f244075b3d3b4cea595ab02f6283da9c63b43f Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Fri, 6 Jun 2025 11:58:53 -0600 Subject: [PATCH 18/20] Make the sphinx extension tests more robust to manually building the docs in tinypages The tests now don't copy any of the build files, meaning the tests should pass even if tinypages has stale build state from a manual build. --- lib/matplotlib/tests/data/tinypages/.gitignore | 2 ++ lib/matplotlib/tests/test_sphinxext.py | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/data/tinypages/.gitignore b/lib/matplotlib/tests/data/tinypages/.gitignore index 69fa449dd96e..739e1d9ce65d 100644 --- a/lib/matplotlib/tests/data/tinypages/.gitignore +++ b/lib/matplotlib/tests/data/tinypages/.gitignore @@ -1 +1,3 @@ _build/ +doctrees/ +plot_directive/ diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index fb287c1d603a..7079b96ca074 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -16,6 +16,17 @@ tinypages = Path(__file__).parent / 'data/tinypages' +def _ignore_build_artifacts(dir, files): + """ + Ignore function for shutil.copytree to exclude build artifacts. + + This prevents stale build artifacts from the source tinypages directory + from being copied to test directories, which could cause inconsistent + test results due to cached or outdated files. + """ + return {'_build', 'doctrees', 'plot_directive'} & set(files) + + def build_sphinx_html(source_dir, doctree_dir, html_dir, extra_args=None): # Build the pages with warnings turned into errors extra_args = [] if extra_args is None else extra_args @@ -40,7 +51,8 @@ def build_sphinx_html(source_dir, doctree_dir, html_dir, extra_args=None): def test_tinypages(tmp_path): - shutil.copytree(tinypages, tmp_path, dirs_exist_ok=True) + shutil.copytree(tinypages, tmp_path, dirs_exist_ok=True, + ignore=_ignore_build_artifacts) html_dir = tmp_path / '_build' / 'html' img_dir = html_dir / '_images' doctree_dir = tmp_path / 'doctrees' @@ -204,11 +216,13 @@ def test_plot_html_show_source_link_custom_basename(tmp_path): def test_srcset_version(tmp_path): + shutil.copytree(tinypages, tmp_path, dirs_exist_ok=True, + ignore=_ignore_build_artifacts) html_dir = tmp_path / '_build' / 'html' img_dir = html_dir / '_images' doctree_dir = tmp_path / 'doctrees' - build_sphinx_html(tinypages, doctree_dir, html_dir, + build_sphinx_html(tmp_path, doctree_dir, html_dir, extra_args=['-D', 'plot_srcset=2x']) def plot_file(num, suff=''): From bab3aafbff2a23dfbd220b2f4b909176eb94377f Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Fri, 6 Jun 2025 13:58:52 -0600 Subject: [PATCH 19/20] Use shutil.ignore_patterns --- lib/matplotlib/tests/test_sphinxext.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index 7079b96ca074..3b1a40e428c7 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -16,17 +16,6 @@ tinypages = Path(__file__).parent / 'data/tinypages' -def _ignore_build_artifacts(dir, files): - """ - Ignore function for shutil.copytree to exclude build artifacts. - - This prevents stale build artifacts from the source tinypages directory - from being copied to test directories, which could cause inconsistent - test results due to cached or outdated files. - """ - return {'_build', 'doctrees', 'plot_directive'} & set(files) - - def build_sphinx_html(source_dir, doctree_dir, html_dir, extra_args=None): # Build the pages with warnings turned into errors extra_args = [] if extra_args is None else extra_args @@ -52,7 +41,8 @@ def build_sphinx_html(source_dir, doctree_dir, html_dir, extra_args=None): def test_tinypages(tmp_path): shutil.copytree(tinypages, tmp_path, dirs_exist_ok=True, - ignore=_ignore_build_artifacts) + ignore=shutil.ignore_patterns('_build', 'doctrees', + 'plot_directive')) html_dir = tmp_path / '_build' / 'html' img_dir = html_dir / '_images' doctree_dir = tmp_path / 'doctrees' @@ -217,7 +207,8 @@ def test_plot_html_show_source_link_custom_basename(tmp_path): def test_srcset_version(tmp_path): shutil.copytree(tinypages, tmp_path, dirs_exist_ok=True, - ignore=_ignore_build_artifacts) + ignore=shutil.ignore_patterns('_build', 'doctrees', + 'plot_directive')) html_dir = tmp_path / '_build' / 'html' img_dir = html_dir / '_images' doctree_dir = tmp_path / 'doctrees' From c8eba9034259e65d84f226c035777f4bf4b5c3a7 Mon Sep 17 00:00:00 2001 From: Aaron Meurer Date: Sun, 8 Jun 2025 10:33:11 -0600 Subject: [PATCH 20/20] Rename image-basename to filename-prefix --- lib/matplotlib/sphinxext/plot_directive.py | 29 ++++++++++--------- .../tests/data/tinypages/some_plots.rst | 8 ++--- lib/matplotlib/tests/test_sphinxext.py | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index a9c72683dd9d..b5f10d851182 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -47,11 +47,11 @@ The ``.. plot::`` directive supports the following options: -``:image-basename:`` : str - The base name (without the extension) of the outputted image files. The - default is to use the same name as the input script, or the name of - the RST document if no script is provided. The image-basename for each - plot directive must be unique. +``:filename-prefix:`` : str + The base name (without the extension) of the outputted image and script + files. The default is to use the same name as the input script, or the + name of the RST document if no script is provided. The filename-prefix for + each plot directive must be unique. ``:format:`` : {'python', 'doctest'} The format of the input. If unset, the format is auto-detected. @@ -169,6 +169,7 @@ be customized by changing the *plot_template*. See the source of :doc:`/api/sphinxext_plot_directive_api` for the templates defined in *TEMPLATE* and *TEMPLATE_SRCSET*. + """ from collections import defaultdict @@ -273,7 +274,7 @@ class PlotDirective(Directive): 'scale': directives.nonnegative_int, 'align': Image.align, 'class': directives.class_option, - 'image-basename': directives.unchanged, + 'filename-prefix': directives.unchanged, 'include-source': _option_boolean, 'show-source-link': _option_boolean, 'format': _option_format, @@ -640,15 +641,15 @@ def check_output_base_name(env, output_base): if '.' in output_base or '/' in output_base or '\\' in output_base: raise PlotError( - f"The image-basename '{output_base}' is invalid. " + f"The filename-prefix '{output_base}' is invalid. " f"It must not contain dots or slashes.") for d in env.mpl_plot_image_basenames: if output_base in env.mpl_plot_image_basenames[d]: if d == docname: raise PlotError( - f"The image-basename {output_base!r} is used multiple times.") - raise PlotError(f"The image-basename {output_base!r} is used multiple" + f"The filename-prefix {output_base!r} is used multiple times.") + raise PlotError(f"The filename-prefix {output_base!r} is used multiple" f"times (it is also used in {env.doc2path(d)}).") env.mpl_plot_image_basenames[docname].add(output_base) @@ -789,7 +790,7 @@ def run(arguments, content, options, state_machine, state, lineno): options.setdefault('include-source', config.plot_include_source) options.setdefault('show-source-link', config.plot_html_show_source_link) - options.setdefault('image-basename', None) + options.setdefault('filename-prefix', None) if 'class' in options: # classes are parsed into a list of string, and output by simply @@ -831,16 +832,16 @@ def run(arguments, content, options, state_machine, state, lineno): function_name = None code = Path(source_file_name).read_text(encoding='utf-8') - if options['image-basename']: - output_base = options['image-basename'] + if options['filename-prefix']: + output_base = options['filename-prefix'] check_output_base_name(env, output_base) else: output_base = os.path.basename(source_file_name) else: source_file_name = rst_file code = textwrap.dedent("\n".join(map(str, content))) - if options['image-basename']: - output_base = options['image-basename'] + if options['filename-prefix']: + output_base = options['filename-prefix'] check_output_base_name(env, output_base) else: base, ext = os.path.splitext(os.path.basename(source_file_name)) diff --git a/lib/matplotlib/tests/data/tinypages/some_plots.rst b/lib/matplotlib/tests/data/tinypages/some_plots.rst index 71a99585dd83..17de8f1d742e 100644 --- a/lib/matplotlib/tests/data/tinypages/some_plots.rst +++ b/lib/matplotlib/tests/data/tinypages/some_plots.rst @@ -180,18 +180,18 @@ Plot 22 uses a different specific function in a file with plot commands: .. plot:: range6.py range10 -Plots 23--25 use image-basename. +Plots 23--25 use filename-prefix. .. plot:: - :image-basename: custom-basename-6 + :filename-prefix: custom-basename-6 plt.plot(range(6)) .. plot:: range4.py - :image-basename: custom-basename-4 + :filename-prefix: custom-basename-4 .. plot:: - :image-basename: custom-basename-4-6 + :filename-prefix: custom-basename-4-6 plt.figure() plt.plot(range(4)) diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index 3b1a40e428c7..ede3166a2e1b 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -190,7 +190,7 @@ def test_plot_html_show_source_link_custom_basename(tmp_path): doctree_dir = tmp_path / 'doctrees' (tmp_path / 'index.rst').write_text(""" .. plot:: - :image-basename: custom-name + :filename-prefix: custom-name plt.plot(range(2)) """)