4747
4848The ``.. plot::`` directive supports the following options:
4949
50+ ``:filename-prefix:`` : str
51+ The base name (without the extension) of the outputted image and script
52+ files. The default is to use the same name as the input script, or the
53+ name of the RST document if no script is provided. The filename-prefix for
54+ each plot directive must be unique.
55+
5056``:format:`` : {'python', 'doctest'}
5157 The format of the input. If unset, the format is auto-detected.
5258
163169be customized by changing the *plot_template*. See the source of
164170:doc:`/api/sphinxext_plot_directive_api` for the templates defined in *TEMPLATE*
165171and *TEMPLATE_SRCSET*.
172+
166173"""
167174
175+ from collections import defaultdict
168176import contextlib
169177import doctest
170178from io import StringIO
182190from docutils .parsers .rst .directives .images import Image
183191import jinja2 # Sphinx dependency.
184192
193+ from sphinx .environment .collectors import EnvironmentCollector
185194from sphinx .errors import ExtensionError
186195
187196import matplotlib
@@ -265,6 +274,7 @@ class PlotDirective(Directive):
265274 'scale' : directives .nonnegative_int ,
266275 'align' : Image .align ,
267276 'class' : directives .class_option ,
277+ 'filename-prefix' : directives .unchanged ,
268278 'include-source' : _option_boolean ,
269279 'show-source-link' : _option_boolean ,
270280 'format' : _option_format ,
@@ -312,9 +322,35 @@ def setup(app):
312322 app .connect ('build-finished' , _copy_css_file )
313323 metadata = {'parallel_read_safe' : True , 'parallel_write_safe' : True ,
314324 'version' : matplotlib .__version__ }
325+ app .connect ('builder-inited' , init_filename_registry )
326+ app .add_env_collector (_FilenameCollector )
315327 return metadata
316328
317329
330+ # -----------------------------------------------------------------------------
331+ # Handle Duplicate Filenames
332+ # -----------------------------------------------------------------------------
333+
334+ def init_filename_registry (app ):
335+ env = app .builder .env
336+ if not hasattr (env , 'mpl_plot_image_basenames' ):
337+ env .mpl_plot_image_basenames = defaultdict (set )
338+
339+
340+ class _FilenameCollector (EnvironmentCollector ):
341+ def process_doc (self , app , doctree ):
342+ pass
343+
344+ def clear_doc (self , app , env , docname ):
345+ if docname in env .mpl_plot_image_basenames :
346+ del env .mpl_plot_image_basenames [docname ]
347+
348+ def merge_other (self , app , env , docnames , other ):
349+ for docname in other .mpl_plot_image_basenames :
350+ env .mpl_plot_image_basenames [docname ].update (
351+ other .mpl_plot_image_basenames [docname ])
352+
353+
318354# -----------------------------------------------------------------------------
319355# Doctest handling
320356# -----------------------------------------------------------------------------
@@ -600,6 +636,25 @@ def _parse_srcset(entries):
600636 return srcset
601637
602638
639+ def check_output_base_name (env , output_base ):
640+ docname = env .docname
641+
642+ if '.' in output_base or '/' in output_base or '\\ ' in output_base :
643+ raise PlotError (
644+ f"The filename-prefix '{ output_base } ' is invalid. "
645+ f"It must not contain dots or slashes." )
646+
647+ for d in env .mpl_plot_image_basenames :
648+ if output_base in env .mpl_plot_image_basenames [d ]:
649+ if d == docname :
650+ raise PlotError (
651+ f"The filename-prefix { output_base !r} is used multiple times." )
652+ raise PlotError (f"The filename-prefix { output_base !r} is used multiple"
653+ f"times (it is also used in { env .doc2path (d )} )." )
654+
655+ env .mpl_plot_image_basenames [docname ].add (output_base )
656+
657+
603658def render_figures (code , code_path , output_dir , output_base , context ,
604659 function_name , config , context_reset = False ,
605660 close_figs = False ,
@@ -722,7 +777,8 @@ def render_figures(code, code_path, output_dir, output_base, context,
722777
723778def run (arguments , content , options , state_machine , state , lineno ):
724779 document = state_machine .document
725- config = document .settings .env .config
780+ env = document .settings .env
781+ config = env .config
726782 nofigs = 'nofigs' in options
727783
728784 if config .plot_srcset and setup .app .builder .name == 'singlehtml' :
@@ -734,6 +790,7 @@ def run(arguments, content, options, state_machine, state, lineno):
734790
735791 options .setdefault ('include-source' , config .plot_include_source )
736792 options .setdefault ('show-source-link' , config .plot_html_show_source_link )
793+ options .setdefault ('filename-prefix' , None )
737794
738795 if 'class' in options :
739796 # classes are parsed into a list of string, and output by simply
@@ -775,14 +832,22 @@ def run(arguments, content, options, state_machine, state, lineno):
775832 function_name = None
776833
777834 code = Path (source_file_name ).read_text (encoding = 'utf-8' )
778- output_base = os .path .basename (source_file_name )
835+ if options ['filename-prefix' ]:
836+ output_base = options ['filename-prefix' ]
837+ check_output_base_name (env , output_base )
838+ else :
839+ output_base = os .path .basename (source_file_name )
779840 else :
780841 source_file_name = rst_file
781842 code = textwrap .dedent ("\n " .join (map (str , content )))
782- counter = document .attributes .get ('_plot_counter' , 0 ) + 1
783- document .attributes ['_plot_counter' ] = counter
784- base , ext = os .path .splitext (os .path .basename (source_file_name ))
785- output_base = '%s-%d.py' % (base , counter )
843+ if options ['filename-prefix' ]:
844+ output_base = options ['filename-prefix' ]
845+ check_output_base_name (env , output_base )
846+ else :
847+ base , ext = os .path .splitext (os .path .basename (source_file_name ))
848+ counter = document .attributes .get ('_plot_counter' , 0 ) + 1
849+ document .attributes ['_plot_counter' ] = counter
850+ output_base = '%s-%d.py' % (base , counter )
786851 function_name = None
787852 caption = options .get ('caption' , '' )
788853
@@ -846,7 +911,7 @@ def run(arguments, content, options, state_machine, state, lineno):
846911
847912 # save script (if necessary)
848913 if options ['show-source-link' ]:
849- Path (build_dir , output_base + source_ext ).write_text (
914+ Path (build_dir , output_base + ( source_ext or '.py' ) ).write_text (
850915 doctest .script_from_examples (code )
851916 if source_file_name == rst_file and is_doctest
852917 else code ,
@@ -906,7 +971,7 @@ def run(arguments, content, options, state_machine, state, lineno):
906971 # Not-None src_name signals the need for a source download in the
907972 # generated html
908973 if j == 0 and options ['show-source-link' ]:
909- src_name = output_base + source_ext
974+ src_name = output_base + ( source_ext or '.py' )
910975 else :
911976 src_name = None
912977 if config .plot_srcset :
0 commit comments