8000 Add a filename-prefix option to the Sphinx plot directive by asmeurer · Pull Request #28187 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content

Add a filename-prefix option to the Sphinx plot directive #28187

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Jun 16, 2025
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
615017a
Add an output-base-name option to the Sphinx plot directive
asmeurer May 8, 2024
b432962
Add tests for output-base-name
asmeurer May 9, 2024
426abc7
Remove {counter} from output-base-name and remove the global config
asmeurer Oct 11, 2024
8485bfd
Check for duplicate output-base-name in the Sphinx extension
asmeurer Oct 11, 2024
f94a932
Fix flake8 errors
asmeurer Oct 12, 2024
19daf49
Merge branch 'main' into output-base-name
asmeurer Oct 12, 2024
8f05ba6
Make an internal class private
asmeurer Oct 12, 2024
1fa88dd
Fix small code nit
asmeurer Oct 14, 2024
a22fcc3
Add a test for output-base-name with a .py file
asmeurer Oct 14, 2024
8000 86fb167
Remove a redundant test
asmeurer Oct 17, 2024
e0be21e
Disallow / or . in output-base-name
asmeurer Oct 17, 2024
f322125
Rename output-base-name to image-basename
asmeurer Oct 18, 2024
fc33c38
Use a better variable name
asmeurer Oct 21, 2024
7d416cf
Simplify logic in merge_other
asmeurer Oct 21, 2024
ce23c88
Merge branch 'main' into output-base-name
asmeurer Feb 24, 2025
f654a74
Various small code cleanups from review
asmeurer Jun 6, 2025
13e5291
Merge branch 'main' into output-base-name
asmeurer Jun 6, 2025
20bed26
Add a test for image-basename with multiple figures
asmeurer Jun 6, 2025
7f56c94
Disallow \ in output_base in the plot directive
asmeurer Jun 6, 2025
550e382
Fix the source link when using a custom basename to include the .py e…
asmeurer Jun 6, 2025
d4f2440
Make the sphinx extension tests more robust to manually building the …
asmeurer Jun 6, 2025
bab3aaf
Use shutil.ignore_patterns
asmeurer Jun 6, 2025
c8eba90
Rename image-basename to filename-prefix
asmeurer Jun 8, 2025
f4f1fbf
Merge branch 'main' into output-base-name
asmeurer Jun 13, 2025
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
76 changes: 71 additions & 5 deletions lib/matplotlib/sphinxext/plot_directive.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@

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.

``:format:`` : {'python', 'doctest'}
The format of the input. If unset, the format is auto-detected.

Expand Down Expand Up @@ -165,6 +171,7 @@
and *TEMPLATE_SRCSET*.
"""

from collections import defaultdict
import contextlib
import doctest
from io import StringIO
Expand All @@ -182,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
Expand Down Expand Up @@ -265,6 +273,7 @@
'scale': directives.nonnegative_int,
'align': Image.align,
'class': directives.class_option,
'image-basename': directives.unchanged,
'include-source': _option_boolean,
'show-source-link': _option_boolean,
'format': _option_format,
Expand Down Expand Up @@ -312,9 +321,35 @@
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_plot_image_basenames'):
env.mpl_plot_image_basenames = defaultdict(set)


class _FilenameCollector(EnvironmentCollector):
def process_doc(self, app, doctree):
pass

def clear_doc(self, app, env, 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 other.mpl_plot_image_basenames:
env.mpl_plot_image_basenames[docname].update(

Check warning on line 349 in lib/matplotlib/sphinxext/plot_directive.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/sphinxext/plot_directive.py#L349

Added line #L349 was not covered by tests
other.mpl_plot_image_basenames[docname])


# -----------------------------------------------------------------------------
# Doctest handling
# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -600,6 +635,27 @@
return srcset


def check_output_base_name(env, output_base):
docname = env.docname

if '.' in output_base or '/' in output_base:
raise PlotError(

Check warning on line 642 in lib/matplotlib/sphinxext/plot_directive.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/sphinxext/plot_directive.py#L642

Added line #L642 was not covered by tests
f"The image-basename '{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(

Check warning on line 649 in lib/matplotlib/sphinxext/plot_directive.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/sphinxext/plot_directive.py#L649

Added line #L649 was not covered by tests
f"The image-basename "
f"{output_base}' is used multiple times.")
raise PlotError(f"The image-basename "

Check warning on line 652 in lib/matplotlib/sphinxext/plot_directive.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/sphinxext/plot_directive.py#L652

Added line #L652 was not covered by tests
f"'{output_base}' is used multiple times "
f"(it is also used in {env.doc2path(d)}).")

env.mpl_plot_image_basenames[docname].add(output_base)


def render_figures(code, code_path, output_dir, output_base, context,
function_name, config, context_reset=False,
close_figs=False,
Expand Down Expand Up @@ -723,6 +779,7 @@
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':
Expand All @@ -734,6 +791,7 @@

options.setdefault('include-source', config.plot_include_source)
options.setdefault('show-source-link', config.plot_html_show_source_link)
options.setdefault('image-basename', None)

if 'class' in options:
# classes are parsed into a list of string, and output by simply
Expand Down Expand Up @@ -775,14 +833,22 @@
function_name = None

code = Path(source_file_name).read_text(encoding='utf-8')
output_base = os.path.basename(source_file_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)))
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['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))
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', '')

Expand Down
3 changes: 3 additions & 0 deletions lib/matplotlib/tests/test_sphinxext.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +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 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()
Expand Down
10 changes: 10 additions & 0 deletions lib/matplotlib/tests/tinypages/some_plots.rst
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,13 @@ 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 image-basename.

.. plot::
:image-basename: custom-basename-6

plt.plot(range(6))

.. plot:: range4.py
:image-basename: custom-basename-4
Loading
0