Coverage for install/scipp/sphinxext/autoplot.py: 0%
48 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-11-17 01:51 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-11-17 01:51 +0000
1# SPDX-License-Identifier: BSD-3-Clause
2# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
3# @author Jan-Lukas Wynen
4"""Sphinx extension to automatically place plot directives.
6Places `matplotlib plot directives <https://matplotlib.org/stable/api/sphinxext_plot_directive_api.html>`_
7for doctest code blocks (starting with ``>>>``) in docstrings when those
8code blocks create plots.
10Plots are detected by matching a pattern against the source code in code blocks.
11This means that either ``sc.plot`` or any of the corresponding bound methods must be
12called directly in the code block.
13It does not work if ``plot`` is called by another function.
14It does however work with any function called 'plot'.
16It is possible to use the plot directive manually.
17``autoplot`` will detect that and not place an additional directive.
18This is useful, e.g., for specifying options to the directive.
20Simple Example
21--------------
22``autoplot`` turns
24.. autoplot-disable::
26.. code-block:: text
28 >>> sc.arange('x',5).plot()
30into
32.. code-block:: text
34 .. plot::
36 >>> sc.arange('x',5).plot()
38Disabling autoplot
39------------------
40``autoplot`` can be disabled by using the ``.. autoplot-disable::`` directive
41anywhere (on its own line) in a docstring.
42This will disable autoplot for the entire docstring.
43"""
45import re
47from sphinx.application import Sphinx
48from sphinx.util.docutils import SphinxDirective
50# If this pattern matches any line of a code block,
51# that block is assumed to produce plots.
52PLOT_PATTERN = re.compile(r'\.plot\(')
53# Pattern to detect existing plot directives.
54PLOT_DIRECTIVE_PATTERN = re.compile(r'^\s*\.\. plot::.*')
55# Pattern to detect directive to disable autoplot.
56DISABLE_DIRECTIVE_PATTERN = re.compile(r'^\s*\.\. autoplot-disable::.*')
59def _previous_nonempty_line(lines: list[str], index: int) -> str | None:
60 index -= 1
61 while index >= 0 and not lines[index].strip():
62 index -= 1
63 return lines[index] if index >= 0 else None
66def _indentation_of(s: str) -> int:
67 return len(s) - len(s.lstrip())
70def _process_block(lines: list[str], begin: int, end: int) -> list[str]:
71 block = lines[begin:end]
72 if PLOT_PATTERN.search(block[-1]) is not None:
73 prev = _previous_nonempty_line(lines, begin)
74 if prev and PLOT_DIRECTIVE_PATTERN.match(prev) is None:
75 # If the block is not preceded by any text or directives (e.g. comes right
76 # after the 'Examples' header), its indentation is stripped in `lines`.
77 # Adding some indentation here fixes that but also modifies indentation
78 # if the block is already indented. That should not cause problems.
79 return [
80 ' ' * _indentation_of(prev) + '.. plot::',
81 '',
82 *(' ' + x for x in block),
83 ]
84 return block
87def _is_start_of_block(line: str) -> bool:
88 return line.lstrip().startswith('>>>')
91def _is_part_of_block(line: str) -> bool:
92 stripped = line.lstrip()
93 return stripped.startswith('>>>') or stripped.startswith('...')
96def add_plot_directives(
97 app: object,
98 what: object,
99 name: object,
100 obj: object,
101 options: object,
102 lines: list[str],
103) -> None:
104 new_lines = []
105 block_begin: int | None = None
106 for i, line in enumerate(lines):
107 if DISABLE_DIRECTIVE_PATTERN.match(line):
108 return
109 if block_begin is None:
110 if _is_start_of_block(line):
111 # Begin a new block.
112 block_begin = i
113 else:
114 # Not in a block -> copy line into output.
115 new_lines.append(line)
116 else:
117 if not _is_part_of_block(line):
118 # Block has ended.
119 new_lines.extend(_process_block(lines, block_begin, i))
120 new_lines.append(line)
121 block_begin = None
122 # else: Continue block.
124 lines[:] = new_lines
127class AutoplotDisable(SphinxDirective):
128 has_content = False
130 def run(self) -> list[object]:
131 return []
134def setup(app: Sphinx) -> dict[str, int | bool]:
135 app.add_directive('autoplot-disable', AutoplotDisable)
136 app.connect('autodoc-process-docstring', add_plot_directives)
137 return {'version': 1, 'parallel_read_safe': True}