Format: text/x-python
⬇ Download File (4.5 KB)
   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