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