Coverage for install/scipp/sphinxext/autoplot.py: 0%

49 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-04-28 01:28 +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 

46from typing import List, Optional 

47 

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) -> Optional[str]: 

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(app, what, name, obj, options, lines: List[str]): 

97 new_lines = [] 

98 block_begin = None 

99 for i, line in enumerate(lines): 

100 if DISABLE_DIRECTIVE_PATTERN.match(line): 

101 return 

102 if block_begin is None: 

103 if _is_start_of_block(line): 

104 # Begin a new block. 

105 block_begin = i 

106 else: 

107 # Not in a block -> copy line into output. 

108 new_lines.append(line) 

109 else: 

110 if not _is_part_of_block(line): 

111 # Block has ended. 

112 new_lines.extend(_process_block(lines, block_begin, i)) 

113 new_lines.append(line) 

114 block_begin = None 

115 # else: Continue block. 

116 

117 lines[:] = new_lines 

118 

119 

120class AutoplotDisable(SphinxDirective): 

121 has_content = False 

122 

123 def run(self): 

124 return [] 

125 

126 

127def setup(app): 

128 app.add_directive('autoplot-disable', AutoplotDisable) 

129 app.connect('autodoc-process-docstring', add_plot_directives) 

130 return {'version': 1, 'parallel_read_safe': True}