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 }  
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}  )
652+             raise  PlotError (f"The filename-prefix { output_base !r}  
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