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

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. 

5 

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. 

9 

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'. 

15 

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. 

19 

20Simple Example 

21-------------- 

22``autoplot`` turns 

23 

24.. autoplot-disable:: 

25 

26.. code-block:: text 

27 

28 >>> sc.arange('x',5).plot() 

29 

30into 

31 

32.. code-block:: text 

33 

34 .. plot:: 

35 

36 >>> sc.arange('x',5).plot() 

37 

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""" 

44 

45import re 

46 

47from sphinx.application import Sphinx 

48from sphinx.util.docutils import SphinxDirective 

49 

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::.*') 

57 

58 

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 

64 

65 

66def _indentation_of(s: str) -> int: 

67 return len(s) - len(s.lstrip()) 

68 

69 

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 

85 

86 

87def _is_start_of_block(line: str) -> bool: 

88 return line.lstrip().startswith('>>>') 

89 

90 

91def _is_part_of_block(line: str) -> bool: 

92 stripped = line.lstrip() 

93 return stripped.startswith('>>>') or stripped.startswith('...') 

94 

95 

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. 

123 

124 lines[:] = new_lines 

125 

126 

127class AutoplotDisable(SphinxDirective): 

128 has_content = False 

129 

130 def run(self) -> list[object]: 

131 return [] 

132 

133 

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}