Passed
Push — master ( 4497b8...dc2c24 )
by Fernando
01:19
created

plot_directive   F

Complexity

Total Complexity 113

Size/Duplication

Total Lines 811
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 415
dl 0
loc 811
rs 2
c 0
b 0
f 0
wmc 113

How to fix   Complexity   

Complexity

Complex classes like plot_directive often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# flake8: noqa
2
"""
3
A directive for including a Matplotlib plot in a Sphinx document
4
================================================================
5
6
By default, in HTML output, `plot` will include a .png file with a link to a
7
high-res .png and .pdf.  In LaTeX output, it will include a .pdf.
8
9
The source code for the plot may be included in one of three ways:
10
11
1. **A path to a source file** as the argument to the directive::
12
13
     .. plot:: path/to/plot.py
14
15
   When a path to a source file is given, the content of the
16
   directive may optionally contain a caption for the plot::
17
18
     .. plot:: path/to/plot.py
19
20
        The plot's caption.
21
22
   Additionally, one may specify the name of a function to call (with
23
   no arguments) immediately after importing the module::
24
25
     .. plot:: path/to/plot.py plot_function1
26
27
2. Included as **inline content** to the directive::
28
29
     .. plot::
30
31
        import matplotlib.pyplot as plt
32
        import matplotlib.image as mpimg
33
        import numpy as np
34
        img = mpimg.imread('_static/stinkbug.png')
35
        imgplot = plt.imshow(img)
36
37
3. Using **doctest** syntax::
38
39
     .. plot::
40
41
        A plotting example:
42
        >>> import matplotlib.pyplot as plt
43
        >>> plt.plot([1, 2, 3], [4, 5, 6])
44
45
Options
46
-------
47
48
The ``plot`` directive supports the following options:
49
50
    format : {'python', 'doctest'}
51
        The format of the input.
52
53
    include-source : bool
54
        Whether to display the source code. The default can be changed
55
        using the `plot_include_source` variable in :file:`conf.py`.
56
57
    encoding : str
58
        If this source file is in a non-UTF8 or non-ASCII encoding, the
59
        encoding must be specified using the ``:encoding:`` option.  The
60
        encoding will not be inferred using the ``-*- coding -*-`` metacomment.
61
62
    context : bool or str
63
        If provided, the code will be run in the context of all previous plot
64
        directives for which the ``:context:`` option was specified.  This only
65
        applies to inline code plot directives, not those run from files. If
66
        the ``:context: reset`` option is specified, the context is reset
67
        for this and future plots, and previous figures are closed prior to
68
        running the code. ``:context: close-figs`` keeps the context but closes
69
        previous figures before running the code.
70
71
    nofigs : bool
72
        If specified, the code block will be run, but no figures will be
73
        inserted.  This is usually useful with the ``:context:`` option.
74
75
    caption : str
76
        If specified, the option's argument will be used as a caption for the
77
        figure. This overwrites the caption given in the content, when the plot
78
        is generated from a file.
79
80
Additionally, this directive supports all of the options of the `image`
81
directive, except for *target* (since plot will add its own target).  These
82
include *alt*, *height*, *width*, *scale*, *align* and *class*.
83
84
Configuration options
85
---------------------
86
87
The plot directive has the following configuration options:
88
89
    plot_include_source
90
        Default value for the include-source option
91
92
    plot_html_show_source_link
93
        Whether to show a link to the source in HTML.
94
95
    plot_pre_code
96
        Code that should be executed before each plot. If not specified or None
97
        it will default to a string containing::
98
99
            import numpy as np
100
            from matplotlib import pyplot as plt
101
102
    plot_basedir
103
        Base directory, to which ``plot::`` file names are relative
104
        to.  (If None or empty, file names are relative to the
105
        directory where the file containing the directive is.)
106
107
    plot_formats
108
        File formats to generate. List of tuples or strings::
109
110
            [(suffix, dpi), suffix, ...]
111
112
        that determine the file format and the DPI. For entries whose
113
        DPI was omitted, sensible defaults are chosen. When passing from
114
        the command line through sphinx_build the list should be passed as
115
        suffix:dpi,suffix:dpi, ...
116
117
    plot_html_show_formats
118
        Whether to show links to the files in HTML.
119
120
    plot_rcparams
121
        A dictionary containing any non-standard rcParams that should
122
        be applied before each plot.
123
124
    plot_apply_rcparams
125
        By default, rcParams are applied when ``:context:`` option is not used
126
        in a plot directive.  This configuration option overrides this behavior
127
        and applies rcParams before each plot.
128
129
    plot_working_directory
130
        By default, the working directory will be changed to the directory of
131
        the example, so the code can get at its data files, if any.  Also its
132
        path will be added to `sys.path` so it can import any helper modules
133
        sitting beside it.  This configuration option can be used to specify
134
        a central directory (also added to `sys.path`) where data files and
135
        helper modules for all code are located.
136
137
    plot_template
138
        Provide a customized template for preparing restructured text.
139
"""
140
141
import contextlib
142
from io import StringIO
143
import itertools
144
import os
145
from os.path import relpath
146
from pathlib import Path
147
import re
148
import shutil
149
import sys
150
import textwrap
151
import traceback
152
153
from docutils.parsers.rst import directives, Directive
154
from docutils.parsers.rst.directives.images import Image
155
import jinja2  # Sphinx dependency.
156
157
import matplotlib
158
from matplotlib.backend_bases import FigureManagerBase
159
import matplotlib.pyplot as plt
160
from matplotlib import _pylab_helpers, cbook
161
162
matplotlib.use('agg')
163
align = cbook.deprecated(
164
    '3.4', alternative='docutils.parsers.rst.directives.images.Image.align')(
165
        Image.align)
166
167
__version__ = 2
168
169
170
# -----------------------------------------------------------------------------
171
# Registration hook
172
# -----------------------------------------------------------------------------
173
174
175
def _option_boolean(arg):
176
    if not arg or not arg.strip():
177
        # no argument given, assume used as a flag
178
        return True
179
    elif arg.strip().lower() in ('no', '0', 'false'):
180
        return False
181
    elif arg.strip().lower() in ('yes', '1', 'true'):
182
        return True
183
    else:
184
        raise ValueError('"%s" unknown boolean' % arg)
185
186
187
def _option_context(arg):
188
    if arg in [None, 'reset', 'close-figs']:
189
        return arg
190
    raise ValueError("Argument should be None or 'reset' or 'close-figs'")
191
192
193
def _option_format(arg):
194
    return directives.choice(arg, ('python', 'doctest'))
195
196
197
def mark_plot_labels(app, document):
198
    """
199
    To make plots referenceable, we need to move the reference from the
200
    "htmlonly" (or "latexonly") node to the actual figure node itself.
201
    """
202
    for name, explicit in document.nametypes.items():
203
        if not explicit:
204
            continue
205
        labelid = document.nameids[name]
206
        if labelid is None:
207
            continue
208
        node = document.ids[labelid]
209
        if node.tagname in ('html_only', 'latex_only'):
210
            for n in node:
211
                if n.tagname == 'figure':
212
                    sectname = name
213
                    for c in n:
214
                        if c.tagname == 'caption':
215
                            sectname = c.astext()
216
                            break
217
218
                    node['ids'].remove(labelid)
219
                    node['names'].remove(name)
220
                    n['ids'].append(labelid)
221
                    n['names'].append(name)
222
                    document.settings.env.labels[name] = \
223
                        document.settings.env.docname, labelid, sectname
224
                    break
225
226
227
class PlotDirective(Directive):
228
    """The ``.. plot::`` directive, as documented in the module's docstring."""
229
230
    has_content = True
231
    required_arguments = 0
232
    optional_arguments = 2
233
    final_argument_whitespace = False
234
    option_spec = {
235
        'alt': directives.unchanged,
236
        'height': directives.length_or_unitless,
237
        'width': directives.length_or_percentage_or_unitless,
238
        'scale': directives.nonnegative_int,
239
        'align': Image.align,
240
        'class': directives.class_option,
241
        'include-source': _option_boolean,
242
        'format': _option_format,
243
        'context': _option_context,
244
        'nofigs': directives.flag,
245
        'encoding': directives.encoding,
246
        'caption': directives.unchanged,
247
        }
248
249
    def run(self):
250
        """Run the plot directive."""
251
        try:
252
            return run(self.arguments, self.content, self.options,
253
                       self.state_machine, self.state, self.lineno)
254
        except Exception as e:
255
            raise self.error(str(e))
256
257
258
def setup(app):
259
    setup.app = app
260
    setup.config = app.config
261
    setup.confdir = app.confdir
262
    app.add_directive('plot', PlotDirective)
263
    app.add_config_value('plot_pre_code', None, True)
264
    app.add_config_value('plot_include_source', False, True)
265
    app.add_config_value('plot_html_show_source_link', True, True)
266
    app.add_config_value('plot_formats', ['png', 'hires.png', 'pdf'], True)
267
    app.add_config_value('plot_basedir', None, True)
268
    app.add_config_value('plot_html_show_formats', True, True)
269
    app.add_config_value('plot_rcparams', {}, True)
270
    app.add_config_value('plot_apply_rcparams', False, True)
271
    app.add_config_value('plot_working_directory', None, True)
272
    app.add_config_value('plot_template', None, True)
273
274
    app.connect('doctree-read', mark_plot_labels)
275
276
    metadata = {'parallel_read_safe': True, 'parallel_write_safe': True,
277
                'version': matplotlib.__version__}
278
    return metadata
279
280
281
# -----------------------------------------------------------------------------
282
# Doctest handling
283
# -----------------------------------------------------------------------------
284
285
286
def contains_doctest(text):
287
    try:
288
        # check if it's valid Python as-is
289
        compile(text, '<string>', 'exec')
290
        return False
291
    except SyntaxError:
292
        pass
293
    r = re.compile(r'^\s*>>>', re.M)
294
    m = r.search(text)
295
    return bool(m)
296
297
298
def unescape_doctest(text):
299
    """
300
    Extract code from a piece of text, which contains either Python code
301
    or doctests.
302
    """
303
    if not contains_doctest(text):
304
        return text
305
306
    code = ''
307
    for line in text.split('\n'):
308
        m = re.match(r'^\s*(>>>|\.\.\.) (.*)$', line)
309
        if m:
310
            code += m.group(2) + '\n'
311
        elif line.strip():
312
            code += '# ' + line.strip() + '\n'
313
        else:
314
            code += '\n'
315
    return code
316
317
318
def split_code_at_show(text):
319
    """Split code at plt.show()."""
320
    parts = []
321
    is_doctest = contains_doctest(text)
322
323
    part = []
324
    for line in text.split('\n'):
325
        if (not is_doctest and line.strip() == 'plt.show()') or \
326
               (is_doctest and line.strip() == '>>> plt.show()'):
327
            part.append(line)
328
            parts.append('\n'.join(part))
329
            part = []
330
        else:
331
            part.append(line)
332
    if '\n'.join(part).strip():
333
        parts.append('\n'.join(part))
334
    return parts
335
336
337
# -----------------------------------------------------------------------------
338
# Template
339
# -----------------------------------------------------------------------------
340
341
342
TEMPLATE = """
343
{{ source_code }}
344
345
.. only:: html
346
347
   {% if source_link or (html_show_formats and not multi_image) %}
348
   (
349
   {%- if source_link -%}
350
   `Source code <{{ source_link }}>`__
351
   {%- endif -%}
352
   {%- if html_show_formats and not multi_image -%}
353
     {%- for img in images -%}
354
       {%- for fmt in img.formats -%}
355
         {%- if source_link or not loop.first -%}, {% endif -%}
356
         `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__
357
       {%- endfor -%}
358
     {%- endfor -%}
359
   {%- endif -%}
360
   )
361
   {% endif %}
362
363
   {% for img in images %}
364
   .. figure:: {{ build_dir }}/{{ img.basename }}.{{ default_fmt }}
365
      {% for option in options -%}
366
      {{ option }}
367
      {% endfor %}
368
369
      {% if html_show_formats and multi_image -%}
370
        (
371
        {%- for fmt in img.formats -%}
372
        {%- if not loop.first -%}, {% endif -%}
373
        `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__
374
        {%- endfor -%}
375
        )
376
      {%- endif -%}
377
378
      {{ caption }}
379
   {% endfor %}
380
381
.. only:: not html
382
383
   {% for img in images %}
384
   .. figure:: {{ build_dir }}/{{ img.basename }}.*
385
      {% for option in options -%}
386
      {{ option }}
387
      {% endfor %}
388
389
      {{ caption }}
390
   {% endfor %}
391
392
"""
393
394
exception_template = """
395
.. only:: html
396
397
   [`source code <%(linkdir)s/%(basename)s.py>`__]
398
399
Exception occurred rendering plot.
400
401
"""
402
403
# the context of the plot for all directives specified with the
404
# :context: option
405
plot_context = {}
406
407
408
class ImageFile:
409
    def __init__(self, basename, dirname):
410
        self.basename = basename
411
        self.dirname = dirname
412
        self.formats = []
413
414
    def filename(self, format):
415
        return os.path.join(self.dirname, f'{self.basename}.{format}')
416
417
    def filenames(self):
418
        return [self.filename(fmt) for fmt in self.formats]
419
420
421
def out_of_date(original, derived):
422
    """
423
    Return whether *derived* is out-of-date relative to *original*, both of
424
    which are full file paths.
425
    """
426
    return (not os.path.exists(derived) or
427
            (os.path.exists(original) and
428
             os.stat(derived).st_mtime < os.stat(original).st_mtime))
429
430
431
class PlotError(RuntimeError):
432
    pass
433
434
435
def run_code(code, code_path, ns=None, function_name=None):
436
    """
437
    Import a Python module from a path, and run the function given by
438
    name, if function_name is not None.
439
    """
440
441
    # Change the working directory to the directory of the example, so
442
    # it can get at its data files, if any.  Add its path to sys.path
443
    # so it can import any helper modules sitting beside it.
444
    pwd = os.getcwd()
445
    if setup.config.plot_working_directory is not None:
446
        try:
447
            os.chdir(setup.config.plot_working_directory)
448
        except OSError as err:
449
            raise OSError(str(err) + '\n`plot_working_directory` option in'
450
                          'Sphinx configuration file must be a valid '
451
                          'directory path') from err
452
        except TypeError as err:
453
            raise TypeError(str(err) + '\n`plot_working_directory` option in '
454
                            'Sphinx configuration file must be a string or '
455
                            'None') from err
456
    elif code_path is not None:
457
        dirname = os.path.abspath(os.path.dirname(code_path))
458
        os.chdir(dirname)
459
460
    with cbook._setattr_cm(
461
            sys, argv=[code_path], path=[os.getcwd(), *sys.path]), \
462
            contextlib.redirect_stdout(StringIO()):
463
        try:
464
            code = unescape_doctest(code)
465
            if ns is None:
466
                ns = {}
467
            if not ns:
468
                if setup.config.plot_pre_code is None:
469
                    exec('import numpy as np\n'
470
                         'from matplotlib import pyplot as plt\n', ns)
471
                else:
472
                    exec(str(setup.config.plot_pre_code), ns)
473
            if '__main__' in code:
474
                ns['__name__'] = '__main__'
475
476
            # Patch out non-interactive show() to avoid triggering a warning.
477
            with cbook._setattr_cm(FigureManagerBase, show=lambda self: None):
478
                exec(code, ns)
479
                if function_name is not None:
480
                    exec(function_name + '()', ns)
481
482
        except (Exception, SystemExit) as err:
483
            raise PlotError(traceback.format_exc()) from err
484
        finally:
485
            os.chdir(pwd)
486
    return ns
487
488
489
def clear_state(plot_rcparams, close=True):
490
    if close:
491
        plt.close('all')
492
    matplotlib.rc_file_defaults()
493
    matplotlib.rcParams.update(plot_rcparams)
494
495
496
def get_plot_formats(config):
497
    default_dpi = {'png': 80, 'hires.png': 200, 'pdf': 200}
498
    formats = []
499
    plot_formats = config.plot_formats
500
    for fmt in plot_formats:
501
        if isinstance(fmt, str):
502
            if ':' in fmt:
503
                suffix, dpi = fmt.split(':')
504
                formats.append((str(suffix), int(dpi)))
505
            else:
506
                formats.append((fmt, default_dpi.get(fmt, 80)))
507
        elif isinstance(fmt, (tuple, list)) and len(fmt) == 2:
508
            formats.append((str(fmt[0]), int(fmt[1])))
509
        else:
510
            raise PlotError('invalid image format "%r" in plot_formats' % fmt)
511
    return formats
512
513
514
def render_figures(code, code_path, output_dir, output_base, context,
515
                   function_name, config, context_reset=False,
516
                   close_figs=False):
517
    """
518
    Run a pyplot script and save the images in *output_dir*.
519
520
    Save the images under *output_dir* with file names derived from
521
    *output_base*
522
    """
523
    formats = get_plot_formats(config)
524
525
    # -- Try to determine if all images already exist
526
527
    code_pieces = split_code_at_show(code)
528
529
    # Look for single-figure output files first
530
    all_exists = True
531
    img = ImageFile(output_base, output_dir)
532
    for format, dpi in formats:
533
        if out_of_date(code_path, img.filename(format)):
534
            all_exists = False
535
            break
536
        img.formats.append(format)
537
538
    if all_exists:
539
        return [(code, [img])]
540
541
    # Then look for multi-figure output files
542
    results = []
543
    all_exists = True
544
    for i, code_piece in enumerate(code_pieces):
545
        images = []
546
        for j in itertools.count():
547
            if len(code_pieces) > 1:
548
                img = ImageFile('%s_%02d_%02d' % (output_base, i, j),
549
                                output_dir)
550
            else:
551
                img = ImageFile('%s_%02d' % (output_base, j), output_dir)
552
            for fmt, dpi in formats:
553
                if out_of_date(code_path, img.filename(fmt)):
554
                    all_exists = False
555
                    break
556
                img.formats.append(fmt)
557
558
            # assume that if we have one, we have them all
559
            if not all_exists:
560
                all_exists = (j > 0)
561
                break
562
            images.append(img)
563
        if not all_exists:
564
            break
565
        results.append((code_piece, images))
566
567
    if all_exists:
568
        return results
569
570
    # We didn't find the files, so build them
571
572
    results = []
573
    if context:
574
        ns = plot_context
575
    else:
576
        ns = {}
577
578
    if context_reset:
579
        clear_state(config.plot_rcparams)
580
        plot_context.clear()
581
582
    close_figs = not context or close_figs
583
584
    for i, code_piece in enumerate(code_pieces):
585
586
        if not context or config.plot_apply_rcparams:
587
            clear_state(config.plot_rcparams, close_figs)
588
        elif close_figs:
589
            plt.close('all')
590
591
        run_code(code_piece, code_path, ns, function_name)
592
593
        images = []
594
        fig_managers = _pylab_helpers.Gcf.get_all_fig_managers()
595
        for j, figman in enumerate(fig_managers):
596
            if len(fig_managers) == 1 and len(code_pieces) == 1:
597
                img = ImageFile(output_base, output_dir)
598
            elif len(code_pieces) == 1:
599
                img = ImageFile('%s_%02d' % (output_base, j), output_dir)
600
            else:
601
                img = ImageFile('%s_%02d_%02d' % (output_base, i, j),
602
                                output_dir)
603
            images.append(img)
604
            for fmt, dpi in formats:
605
                try:
606
                    figman.canvas.figure.savefig(img.filename(fmt), dpi=dpi)
607
                except Exception as err:
608
                    raise PlotError(traceback.format_exc()) from err
609
                img.formats.append(fmt)
610
611
        results.append((code_piece, images))
612
613
    if not context or config.plot_apply_rcparams:
614
        clear_state(config.plot_rcparams, close=not context)
615
616
    return results
617
618
619
def run(arguments, content, options, state_machine, state, lineno):
620
    document = state_machine.document
621
    config = document.settings.env.config
622
    nofigs = 'nofigs' in options
623
624
    formats = get_plot_formats(config)
625
    default_fmt = formats[0][0]
626
627
    options.setdefault('include-source', config.plot_include_source)
628
    keep_context = 'context' in options
629
    context_opt = None if not keep_context else options['context']
630
631
    rst_file = document.attributes['source']
632
    rst_dir = os.path.dirname(rst_file)
633
634
    if len(arguments):
635
        if not config.plot_basedir:
636
            source_file_name = os.path.join(setup.app.builder.srcdir,
637
                                            directives.uri(arguments[0]))
638
        else:
639
            source_file_name = os.path.join(setup.confdir, config.plot_basedir,
640
                                            directives.uri(arguments[0]))
641
642
        # If there is content, it will be passed as a caption.
643
        caption = '\n'.join(content)
644
645
        # Enforce unambiguous use of captions.
646
        if 'caption' in options:
647
            if caption:
648
                raise ValueError(
649
                    'Caption specified in both content and options.'
650
                    ' Please remove ambiguity.'
651
                )
652
            # Use caption option
653
            caption = options['caption']
654
655
        # If the optional function name is provided, use it
656
        if len(arguments) == 2:
657
            function_name = arguments[1]
658
        else:
659
            function_name = None
660
661
        code = Path(source_file_name).read_text(encoding='utf-8')
662
        output_base = os.path.basename(source_file_name)
663
    else:
664
        source_file_name = rst_file
665
        code = textwrap.dedent('\n'.join(map(str, content)))
666
        counter = document.attributes.get('_plot_counter', 0) + 1
667
        document.attributes['_plot_counter'] = counter
668
        base, ext = os.path.splitext(os.path.basename(source_file_name))
669
        output_base = '%s-%d.py' % (base, counter)
670
        function_name = None
671
        caption = options.get('caption', '')
672
673
    base, source_ext = os.path.splitext(output_base)
674
    if source_ext in ('.py', '.rst', '.txt'):
675
        output_base = base
676
    else:
677
        source_ext = ''
678
679
    # ensure that LaTeX includegraphics doesn't choke in foo.bar.pdf filenames
680
    output_base = output_base.replace('.', '-')
681
682
    # is it in doctest format?
683
    is_doctest = contains_doctest(code)
684
    if 'format' in options:
685
        if options['format'] == 'python':
686
            is_doctest = False
687
        else:
688
            is_doctest = True
689
690
    # determine output directory name fragment
691
    source_rel_name = relpath(source_file_name, setup.confdir)
692
    source_rel_dir = os.path.dirname(source_rel_name)
693
    while source_rel_dir.startswith(os.path.sep):
694
        source_rel_dir = source_rel_dir[1:]
695
696
    # build_dir: where to place output files (temporarily)
697
    build_dir = os.path.join(os.path.dirname(setup.app.doctreedir),
698
                             'plot_directive',
699
                             source_rel_dir)
700
    # get rid of .. in paths, also changes pathsep
701
    # see note in Python docs for warning about symbolic links on Windows.
702
    # need to compare source and dest paths at end
703
    build_dir = os.path.normpath(build_dir)
704
705
    if not os.path.exists(build_dir):
706
        os.makedirs(build_dir)
707
708
    # output_dir: final location in the builder's directory
709
    dest_dir = os.path.abspath(os.path.join(setup.app.builder.outdir,
710
                                            source_rel_dir))
711
    if not os.path.exists(dest_dir):
712
        os.makedirs(dest_dir)  # no problem here for me, but just use built-ins
713
714
    # how to link to files from the RST file
715
    dest_dir_link = os.path.join(relpath(setup.confdir, rst_dir),
716
                                 source_rel_dir).replace(os.path.sep, '/')
717
    try:
718
        build_dir_link = relpath(build_dir, rst_dir).replace(os.path.sep, '/')
719
    except ValueError:
720
        # on Windows, relpath raises ValueError when path and start are on
721
        # different mounts/drives
722
        build_dir_link = build_dir
723
    source_link = dest_dir_link + '/' + output_base + source_ext
724
725
    # make figures
726
    try:
727
        results = render_figures(code,
728
                                 source_file_name,
729
                                 build_dir,
730
                                 output_base,
731
                                 keep_context,
732
                                 function_name,
733
                                 config,
734
                                 context_reset=context_opt == 'reset',
735
                                 close_figs=context_opt == 'close-figs')
736
        errors = []
737
    except PlotError as err:
738
        reporter = state.memo.reporter
739
        sm = reporter.system_message(
740
            2, 'Exception occurred in plotting {}\n from {}:\n{}'.format(
741
                output_base, source_file_name, err),
742
            line=lineno)
743
        results = [(code, [])]
744
        errors = [sm]
745
746
    # Properly indent the caption
747
    caption = '\n'.join('      ' + line.strip()
748
                        for line in caption.split('\n'))
749
750
    # generate output restructuredtext
751
    total_lines = []
752
    for j, (code_piece, images) in enumerate(results):
753
        if options['include-source']:
754
            if is_doctest:
755
                lines = ['', *code_piece.splitlines()]
756
            else:
757
                lines = ['.. code-block:: python', '',
758
                         *textwrap.indent(code_piece, '    ').splitlines()]
759
            source_code = '\n'.join(lines)
760
        else:
761
            source_code = ''
762
763
        if nofigs:
764
            images = []
765
766
        opts = [
767
            f':{key}: {val}' for key, val in options.items()
768
            if key in ('alt', 'height', 'width', 'scale', 'align', 'class')]
769
770
        # Not-None src_link signals the need for a source link in the generated
771
        # html
772
        if j == 0 and config.plot_html_show_source_link:
773
            src_link = source_link
774
        else:
775
            src_link = None
776
777
        result = jinja2.Template(config.plot_template or TEMPLATE).render(
778
            default_fmt=default_fmt,
779
            dest_dir=dest_dir_link,
780
            build_dir=build_dir_link,
781
            source_link=src_link,
782
            multi_image=len(images) > 1,
783
            options=opts,
784
            images=images,
785
            source_code=source_code,
786
            html_show_formats=config.plot_html_show_formats and len(images),
787
            caption=caption)
788
789
        total_lines.extend(result.split('\n'))
790
        total_lines.extend('\n')
791
792
    if total_lines:
793
        state_machine.insert_input(total_lines, source=source_file_name)
794
795
    # copy image files to builder's output directory, if necessary
796
    Path(dest_dir).mkdir(parents=True, exist_ok=True)
797
798
    for code_piece, images in results:
799
        for img in images:
800
            for fn in img.filenames():
801
                destimg = os.path.join(dest_dir, os.path.basename(fn))
802
                if fn != destimg:
803
                    shutil.copyfile(fn, destimg)
804
805
    # copy script (if necessary)
806
    Path(dest_dir, output_base + source_ext).write_text(
807
        unescape_doctest(code) if source_file_name == rst_file else code,
808
        encoding='utf-8')
809
810
    return errors
811