47
47
48
48
The ``.. plot::`` directive supports the following options:
49
49
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
+
50
56
``:format:`` : {'python', 'doctest'}
51
57
The format of the input. If unset, the format is auto-detected.
52
58
163
169
be customized by changing the *plot_template*. See the source of
164
170
:doc:`/api/sphinxext_plot_directive_api` for the templates defined in *TEMPLATE*
165
171
and *TEMPLATE_SRCSET*.
172
+
166
173
"""
167
174
175
+ from collections import defaultdict
168
176
import contextlib
169
177
import doctest
170
178
from io import StringIO
182
190
from docutils .parsers .rst .directives .images import Image
183
191
import jinja2 # Sphinx dependency.
184
192
193
+ from sphinx .environment .collectors import EnvironmentCollector
185
194
from sphinx .errors import ExtensionError
186
195
187
196
import matplotlib
@@ -265,6 +274,7 @@ class PlotDirective(Directive):
265
274
'scale' : directives .nonnegative_int ,
266
275
'align' : Image .align ,
267
276
'class' : directives .class_option ,
277
+ 'filename-prefix' : directives .unchanged ,
268
278
'include-source' : _option_boolean ,
269
279
'show-source-link' : _option_boolean ,
270
280
'format' : _option_format ,
@@ -312,9 +322,35 @@ def setup(app):
312
322
app .connect ('build-finished' , _copy_css_file )
313
323
metadata = {'parallel_read_safe' : True , 'parallel_write_safe' : True ,
314
324
'version' : matplotlib .__version__ }
325
+ app .connect ('builder-inited' , init_filename_registry )
326
+ app .add_env_collector (_FilenameCollector )
315
327
return metadata
316
328
317
329
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
+
318
354
# -----------------------------------------------------------------------------
319
355
# Doctest handling
320
356
# -----------------------------------------------------------------------------
@@ -600,6 +636,25 @@ def _parse_srcset(entries):
600
636
return srcset
601
637
602
638
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
+
603
658
def render_figures (code , code_path , output_dir , output_base , context ,
604
659
function_name , config , context_reset = False ,
605
660
close_figs = False ,
@@ -722,7 +777,8 @@ def render_figures(code, code_path, output_dir, output_base, context,
722
777
723
778
def run (arguments , content , options , state_machine , state , lineno ):
724
779
document = state_machine .document
725
- config = document .settings .env .config
780
+ env = document .settings .env
781
+ config = env .config
726
782
nofigs = 'nofigs' in options
727
783
728
784
if config .plot_srcset and setup .app .builder .name == 'singlehtml' :
@@ -734,6 +790,7 @@ def run(arguments, content, options, state_machine, state, lineno):
734
790
735
791
options .setdefault ('include-source' , config .plot_include_source )
736
792
options .setdefault ('show-source-link' , config .plot_html_show_source_link )
793
+ options .setdefault ('filename-prefix' , None )
737
794
738
795
if 'class' in options :
739
796
# 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):
775
832
function_name = None
776
833
777
834
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 )
779
840
else :
780
841
source_file_name = rst_file
781
842
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 )
786
851
function_name = None
787
852
caption = options .get ('caption' , '' )
788
853
@@ -846,7 +911,7 @@ def run(arguments, content, options, state_machine, state, lineno):
846
911
847
912
# save script (if necessary)
848
913
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 (
850
915
doctest .script_from_examples (code )
851
916
if source_file_name == rst_file and is_doctest
852
917
else code ,
@@ -906,7 +971,7 @@ def run(arguments, content, options, state_machine, state, lineno):
906
971
# Not-None src_name signals the need for a source download in the
907
972
# generated html
908
973
if j == 0 and options ['show-source-link' ]:
909
- src_name = output_base + source_ext
974
+ src_name = output_base + ( source_ext or '.py' )
910
975
else :
911
976
src_name = None
912
977
if config .plot_srcset :
0 commit comments