custom_doctests.py 4.5 KB

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