ipdoctest.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. """Nose Plugin that supports IPython doctests.
  2. Limitations:
  3. - When generating examples for use as doctests, make sure that you have
  4. pretty-printing OFF. This can be done either by setting the
  5. ``PlainTextFormatter.pprint`` option in your configuration file to False, or
  6. by interactively disabling it with %Pprint. This is required so that IPython
  7. output matches that of normal Python, which is used by doctest for internal
  8. execution.
  9. - Do not rely on specific prompt numbers for results (such as using
  10. '_34==True', for example). For IPython tests run via an external process the
  11. prompt numbers may be different, and IPython tests run as normal python code
  12. won't even have these special _NN variables set at all.
  13. """
  14. #-----------------------------------------------------------------------------
  15. # Module imports
  16. # From the standard library
  17. import doctest
  18. import logging
  19. import re
  20. from testpath import modified_env
  21. #-----------------------------------------------------------------------------
  22. # Module globals and other constants
  23. #-----------------------------------------------------------------------------
  24. log = logging.getLogger(__name__)
  25. #-----------------------------------------------------------------------------
  26. # Classes and functions
  27. #-----------------------------------------------------------------------------
  28. class DocTestFinder(doctest.DocTestFinder):
  29. def _get_test(self, obj, name, module, globs, source_lines):
  30. test = super()._get_test(obj, name, module, globs, source_lines)
  31. if bool(getattr(obj, "__skip_doctest__", False)) and test is not None:
  32. for example in test.examples:
  33. example.options[doctest.SKIP] = True
  34. return test
  35. class IPDoctestOutputChecker(doctest.OutputChecker):
  36. """Second-chance checker with support for random tests.
  37. If the default comparison doesn't pass, this checker looks in the expected
  38. output string for flags that tell us to ignore the output.
  39. """
  40. random_re = re.compile(r'#\s*random\s+')
  41. def check_output(self, want, got, optionflags):
  42. """Check output, accepting special markers embedded in the output.
  43. If the output didn't pass the default validation but the special string
  44. '#random' is included, we accept it."""
  45. # Let the original tester verify first, in case people have valid tests
  46. # that happen to have a comment saying '#random' embedded in.
  47. ret = doctest.OutputChecker.check_output(self, want, got,
  48. optionflags)
  49. if not ret and self.random_re.search(want):
  50. # print('RANDOM OK:',want, file=sys.stderr) # dbg
  51. return True
  52. return ret
  53. # A simple subclassing of the original with a different class name, so we can
  54. # distinguish and treat differently IPython examples from pure python ones.
  55. class IPExample(doctest.Example): pass
  56. class IPDocTestParser(doctest.DocTestParser):
  57. """
  58. A class used to parse strings containing doctest examples.
  59. Note: This is a version modified to properly recognize IPython input and
  60. convert any IPython examples into valid Python ones.
  61. """
  62. # This regular expression is used to find doctest examples in a
  63. # string. It defines three groups: `source` is the source code
  64. # (including leading indentation and prompts); `indent` is the
  65. # indentation of the first (PS1) line of the source code; and
  66. # `want` is the expected output (including leading indentation).
  67. # Classic Python prompts or default IPython ones
  68. _PS1_PY = r'>>>'
  69. _PS2_PY = r'\.\.\.'
  70. _PS1_IP = r'In\ \[\d+\]:'
  71. _PS2_IP = r'\ \ \ \.\.\.+:'
  72. _RE_TPL = r'''
  73. # Source consists of a PS1 line followed by zero or more PS2 lines.
  74. (?P<source>
  75. (?:^(?P<indent> [ ]*) (?P<ps1> %s) .*) # PS1 line
  76. (?:\n [ ]* (?P<ps2> %s) .*)*) # PS2 lines
  77. \n? # a newline
  78. # Want consists of any non-blank lines that do not start with PS1.
  79. (?P<want> (?:(?![ ]*$) # Not a blank line
  80. (?![ ]*%s) # Not a line starting with PS1
  81. (?![ ]*%s) # Not a line starting with PS2
  82. .*$\n? # But any other line
  83. )*)
  84. '''
  85. _EXAMPLE_RE_PY = re.compile( _RE_TPL % (_PS1_PY,_PS2_PY,_PS1_PY,_PS2_PY),
  86. re.MULTILINE | re.VERBOSE)
  87. _EXAMPLE_RE_IP = re.compile( _RE_TPL % (_PS1_IP,_PS2_IP,_PS1_IP,_PS2_IP),
  88. re.MULTILINE | re.VERBOSE)
  89. # Mark a test as being fully random. In this case, we simply append the
  90. # random marker ('#random') to each individual example's output. This way
  91. # we don't need to modify any other code.
  92. _RANDOM_TEST = re.compile(r'#\s*all-random\s+')
  93. def ip2py(self,source):
  94. """Convert input IPython source into valid Python."""
  95. block = _ip.input_transformer_manager.transform_cell(source)
  96. if len(block.splitlines()) == 1:
  97. return _ip.prefilter(block)
  98. else:
  99. return block
  100. def parse(self, string, name='<string>'):
  101. """
  102. Divide the given string into examples and intervening text,
  103. and return them as a list of alternating Examples and strings.
  104. Line numbers for the Examples are 0-based. The optional
  105. argument `name` is a name identifying this string, and is only
  106. used for error messages.
  107. """
  108. # print('Parse string:\n',string) # dbg
  109. string = string.expandtabs()
  110. # If all lines begin with the same indentation, then strip it.
  111. min_indent = self._min_indent(string)
  112. if min_indent > 0:
  113. string = '\n'.join([l[min_indent:] for l in string.split('\n')])
  114. output = []
  115. charno, lineno = 0, 0
  116. # We make 'all random' tests by adding the '# random' mark to every
  117. # block of output in the test.
  118. if self._RANDOM_TEST.search(string):
  119. random_marker = '\n# random'
  120. else:
  121. random_marker = ''
  122. # Whether to convert the input from ipython to python syntax
  123. ip2py = False
  124. # Find all doctest examples in the string. First, try them as Python
  125. # examples, then as IPython ones
  126. terms = list(self._EXAMPLE_RE_PY.finditer(string))
  127. if terms:
  128. # Normal Python example
  129. Example = doctest.Example
  130. else:
  131. # It's an ipython example.
  132. terms = list(self._EXAMPLE_RE_IP.finditer(string))
  133. Example = IPExample
  134. ip2py = True
  135. for m in terms:
  136. # Add the pre-example text to `output`.
  137. output.append(string[charno:m.start()])
  138. # Update lineno (lines before this example)
  139. lineno += string.count('\n', charno, m.start())
  140. # Extract info from the regexp match.
  141. (source, options, want, exc_msg) = \
  142. self._parse_example(m, name, lineno,ip2py)
  143. # Append the random-output marker (it defaults to empty in most
  144. # cases, it's only non-empty for 'all-random' tests):
  145. want += random_marker
  146. # Create an Example, and add it to the list.
  147. if not self._IS_BLANK_OR_COMMENT(source):
  148. output.append(Example(source, want, exc_msg,
  149. lineno=lineno,
  150. indent=min_indent+len(m.group('indent')),
  151. options=options))
  152. # Update lineno (lines inside this example)
  153. lineno += string.count('\n', m.start(), m.end())
  154. # Update charno.
  155. charno = m.end()
  156. # Add any remaining post-example text to `output`.
  157. output.append(string[charno:])
  158. return output
  159. def _parse_example(self, m, name, lineno,ip2py=False):
  160. """
  161. Given a regular expression match from `_EXAMPLE_RE` (`m`),
  162. return a pair `(source, want)`, where `source` is the matched
  163. example's source code (with prompts and indentation stripped);
  164. and `want` is the example's expected output (with indentation
  165. stripped).
  166. `name` is the string's name, and `lineno` is the line number
  167. where the example starts; both are used for error messages.
  168. Optional:
  169. `ip2py`: if true, filter the input via IPython to convert the syntax
  170. into valid python.
  171. """
  172. # Get the example's indentation level.
  173. indent = len(m.group('indent'))
  174. # Divide source into lines; check that they're properly
  175. # indented; and then strip their indentation & prompts.
  176. source_lines = m.group('source').split('\n')
  177. # We're using variable-length input prompts
  178. ps1 = m.group('ps1')
  179. ps2 = m.group('ps2')
  180. ps1_len = len(ps1)
  181. self._check_prompt_blank(source_lines, indent, name, lineno,ps1_len)
  182. if ps2:
  183. self._check_prefix(source_lines[1:], ' '*indent + ps2, name, lineno)
  184. source = '\n'.join([sl[indent+ps1_len+1:] for sl in source_lines])
  185. if ip2py:
  186. # Convert source input from IPython into valid Python syntax
  187. source = self.ip2py(source)
  188. # Divide want into lines; check that it's properly indented; and
  189. # then strip the indentation. Spaces before the last newline should
  190. # be preserved, so plain rstrip() isn't good enough.
  191. want = m.group('want')
  192. want_lines = want.split('\n')
  193. if len(want_lines) > 1 and re.match(r' *$', want_lines[-1]):
  194. del want_lines[-1] # forget final newline & spaces after it
  195. self._check_prefix(want_lines, ' '*indent, name,
  196. lineno + len(source_lines))
  197. # Remove ipython output prompt that might be present in the first line
  198. want_lines[0] = re.sub(r'Out\[\d+\]: \s*?\n?','',want_lines[0])
  199. want = '\n'.join([wl[indent:] for wl in want_lines])
  200. # If `want` contains a traceback message, then extract it.
  201. m = self._EXCEPTION_RE.match(want)
  202. if m:
  203. exc_msg = m.group('msg')
  204. else:
  205. exc_msg = None
  206. # Extract options from the source.
  207. options = self._find_options(source, name, lineno)
  208. return source, options, want, exc_msg
  209. def _check_prompt_blank(self, lines, indent, name, lineno, ps1_len):
  210. """
  211. Given the lines of a source string (including prompts and
  212. leading indentation), check to make sure that every prompt is
  213. followed by a space character. If any line is not followed by
  214. a space character, then raise ValueError.
  215. Note: IPython-modified version which takes the input prompt length as a
  216. parameter, so that prompts of variable length can be dealt with.
  217. """
  218. space_idx = indent+ps1_len
  219. min_len = space_idx+1
  220. for i, line in enumerate(lines):
  221. if len(line) >= min_len and line[space_idx] != ' ':
  222. raise ValueError('line %r of the docstring for %s '
  223. 'lacks blank after %s: %r' %
  224. (lineno+i+1, name,
  225. line[indent:space_idx], line))
  226. SKIP = doctest.register_optionflag('SKIP')
  227. class IPDocTestRunner(doctest.DocTestRunner,object):
  228. """Test runner that synchronizes the IPython namespace with test globals.
  229. """
  230. def run(self, test, compileflags=None, out=None, clear_globs=True):
  231. # Override terminal size to standardise traceback format
  232. with modified_env({'COLUMNS': '80', 'LINES': '24'}):
  233. return super(IPDocTestRunner,self).run(test,
  234. compileflags,out,clear_globs)