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