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