1 """ 2 Handlers for IPythonDirective's @doctest pseudo-decorator. 3 4 The Sphinx extension that provides support for embedded IPython code provides 5 a pseudo-decorator @doctest, which treats the input/output block as a 6 doctest, raising a RuntimeError during doc generation if the actual output 7 (after running the input) does not match the expected output. 8 9 An example usage is: 10 11 .. code-block:: rst 12 13 .. ipython:: 14 15 In [1]: x = 1 16 17 @doctest 18 In [2]: x + 2 19 Out[3]: 3 20 21 One can also provide arguments to the decorator. The first argument should be 22 the name of a custom handler. The specification of any other arguments is 23 determined by the handler. For example, 24 25 .. code-block:: rst 26 27 .. ipython:: 28 29 @doctest float 30 In [154]: 0.1 + 0.2 31 Out[154]: 0.3 32 33 allows the actual output ``0.30000000000000004`` to match the expected output 34 due to a comparison with `np.allclose`. 35 36 This module contains handlers for the @doctest pseudo-decorator. Handlers 37 should have the following function signature:: 38 39 handler(sphinx_shell, args, input_lines, found, submitted) 40 41 where `sphinx_shell` is the embedded Sphinx shell, `args` contains the list 42 of arguments that follow: '@doctest handler_name', `input_lines` contains 43 a list of the lines relevant to the current doctest, `found` is a string 44 containing the output from the IPython shell, and `submitted` is a string 45 containing the expected output from the IPython shell. 46 47 Handlers must be registered in the `doctests` dict at the end of this module. 48 49 """ 50 51 def str_to_array(s): 52 """ 53 Simplistic converter of strings from repr to float NumPy arrays. 54 55 If the repr representation has ellipsis in it, then this will fail. 56 57 Parameters 58 ---------- 59 s : str 60 The repr version of a NumPy array. 61 62 Examples 63 -------- 64 >>> s = "array([ 0.3, inf, nan])" 65 >>> a = str_to_array(s) 66 67 """ 68 import numpy as np 69 70 # Need to make sure eval() knows about inf and nan. 71 # This also assumes default printoptions for NumPy. 72 from numpy import inf, nan 73 74 if s.startswith(u'array'): 75 # Remove array( and ) 76 s = s[6:-1] 77 78 if s.startswith(u'['): 79 a = np.array(eval(s), dtype=float) 80 else: 81 # Assume its a regular float. Force 1D so we can index into it. 82 a = np.atleast_1d(float(s)) 83 return a 84 85 def float_doctest(sphinx_shell, args, input_lines, found, submitted): 86 """ 87 Doctest which allow the submitted output to vary slightly from the input. 88 89 Here is how it might appear in an rst file: 90 91 .. code-block:: rst 92 93 .. ipython:: 94 95 @doctest float 96 In [1]: 0.1 + 0.2 97 Out[1]: 0.3 98 99 """ 100 import numpy as np 101 102 if len(args) == 2: 103 rtol = 1e-05 104 atol = 1e-08 105 else: 106 # Both must be specified if any are specified. 107 try: 108 rtol = float(args[2]) 109 atol = float(args[3]) 110 except IndexError: 111 e = ("Both `rtol` and `atol` must be specified " 112 "if either are specified: {0}".format(args)) 113 raise IndexError(e) from e 114 115 try: 116 submitted = str_to_array(submitted) 117 found = str_to_array(found) 118 except: 119 # For example, if the array is huge and there are ellipsis in it. 120 error = True 121 else: 122 found_isnan = np.isnan(found) 123 submitted_isnan = np.isnan(submitted) 124 error = not np.allclose(found_isnan, submitted_isnan) 125 error |= not np.allclose(found[~found_isnan], 126 submitted[~submitted_isnan], 127 rtol=rtol, atol=atol) 128 129 TAB = ' ' * 4 130 directive = sphinx_shell.directive 131 if directive is None: 132 source = 'Unavailable' 133 content = 'Unavailable' 134 else: 135 source = directive.state.document.current_source 136 # Add tabs and make into a single string. 137 content = '\n'.join([TAB + line for line in directive.content]) 138 139 if error: 140 141 e = ('doctest float comparison failure\n\n' 142 'Document source: {0}\n\n' 143 'Raw content: \n{1}\n\n' 144 'On input line(s):\n{TAB}{2}\n\n' 145 'we found output:\n{TAB}{3}\n\n' 146 'instead of the expected:\n{TAB}{4}\n\n') 147 e = e.format(source, content, '\n'.join(input_lines), repr(found), 148 repr(submitted), TAB=TAB) 149 raise RuntimeError(e) 150 151 # dict of allowable doctest handlers. The key represents the first argument 152 # that must be given to @doctest in order to activate the handler. 153 doctests = { 154 'float': float_doctest, 155 } 156