##// END OF EJS Templates
Merge pull request #13305 from Kojoley/cleanup-ipdoctest...
Matthias Bussonnier -
r27113:fe9ca176 merge
parent child Browse files
Show More
@@ -1,452 +1,437
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 inspect
24 24 import logging
25 25 import os
26 26 import re
27 27
28 28 from testpath import modified_env
29 29
30 30 #-----------------------------------------------------------------------------
31 31 # Module globals and other constants
32 32 #-----------------------------------------------------------------------------
33 33
34 34 log = logging.getLogger(__name__)
35 35
36 36
37 37 #-----------------------------------------------------------------------------
38 38 # Classes and functions
39 39 #-----------------------------------------------------------------------------
40 40
41 def is_extension_module(filename):
42 """Return whether the given filename is an extension module.
43
44 This simply checks that the extension is either .so or .pyd.
45 """
46 return os.path.splitext(filename)[1].lower() in ('.so','.pyd')
47
48
49 41 class DocTestSkip(object):
50 42 """Object wrapper for doctests to be skipped."""
51 43
52 44 ds_skip = """Doctest to skip.
53 45 >>> 1 #doctest: +SKIP
54 46 """
55 47
56 48 def __init__(self,obj):
57 49 self.obj = obj
58 50
59 51 def __getattribute__(self,key):
60 52 if key == '__doc__':
61 53 return DocTestSkip.ds_skip
62 54 else:
63 55 return getattr(object.__getattribute__(self,'obj'),key)
64 56
65 57 # Modified version of the one in the stdlib, that fixes a python bug (doctests
66 58 # not found in extension modules, http://bugs.python.org/issue3158)
67 59 class DocTestFinder(doctest.DocTestFinder):
68 60
69 61 def _from_module(self, module, object):
70 62 """
71 63 Return true if the given object is defined in the given
72 64 module.
73 65 """
74 66 if module is None:
75 67 return True
76 68 elif inspect.isfunction(object):
77 69 return module.__dict__ is object.__globals__
78 70 elif inspect.isbuiltin(object):
79 71 return module.__name__ == object.__module__
80 72 elif inspect.isclass(object):
81 73 return module.__name__ == object.__module__
82 74 elif inspect.ismethod(object):
83 75 # This one may be a bug in cython that fails to correctly set the
84 76 # __module__ attribute of methods, but since the same error is easy
85 77 # to make by extension code writers, having this safety in place
86 78 # isn't such a bad idea
87 79 return module.__name__ == object.__self__.__class__.__module__
88 80 elif inspect.getmodule(object) is not None:
89 81 return module is inspect.getmodule(object)
90 82 elif hasattr(object, '__module__'):
91 83 return module.__name__ == object.__module__
92 84 elif isinstance(object, property):
93 85 return True # [XX] no way not be sure.
94 86 elif inspect.ismethoddescriptor(object):
95 87 # Unbound PyQt signals reach this point in Python 3.4b3, and we want
96 88 # to avoid throwing an error. See also http://bugs.python.org/issue3158
97 89 return False
98 90 else:
99 91 raise ValueError("object must be a class or function, got %r" % object)
100 92
101 93 def _find(self, tests, obj, name, module, source_lines, globs, seen):
102 94 """
103 95 Find tests for the given object and any contained objects, and
104 96 add them to `tests`.
105 97 """
106 98 print('_find for:', obj, name, module) # dbg
107 99 if bool(getattr(obj, "__skip_doctest__", False)):
108 100 #print 'SKIPPING DOCTEST FOR:',obj # dbg
109 101 obj = DocTestSkip(obj)
110 102
111 103 doctest.DocTestFinder._find(self,tests, obj, name, module,
112 104 source_lines, globs, seen)
113 105
114 106 # Below we re-run pieces of the above method with manual modifications,
115 107 # because the original code is buggy and fails to correctly identify
116 108 # doctests in extension modules.
117 109
118 110 # Local shorthands
119 111 from inspect import isroutine, isclass
120 112
121 113 # Look for tests in a module's contained objects.
122 114 if inspect.ismodule(obj) and self._recurse:
123 115 for valname, val in obj.__dict__.items():
124 116 valname1 = '%s.%s' % (name, valname)
125 117 if ( (isroutine(val) or isclass(val))
126 118 and self._from_module(module, val) ):
127 119
128 120 self._find(tests, val, valname1, module, source_lines,
129 121 globs, seen)
130 122
131 123 # Look for tests in a class's contained objects.
132 124 if inspect.isclass(obj) and self._recurse:
133 125 #print 'RECURSE into class:',obj # dbg
134 126 for valname, val in obj.__dict__.items():
135 127 # Special handling for staticmethod/classmethod.
136 128 if isinstance(val, staticmethod):
137 129 val = getattr(obj, valname)
138 130 if isinstance(val, classmethod):
139 131 val = getattr(obj, valname).__func__
140 132
141 133 # Recurse to methods, properties, and nested classes.
142 134 if ((inspect.isfunction(val) or inspect.isclass(val) or
143 135 inspect.ismethod(val) or
144 136 isinstance(val, property)) and
145 137 self._from_module(module, val)):
146 138 valname = '%s.%s' % (name, valname)
147 139 self._find(tests, val, valname, module, source_lines,
148 140 globs, seen)
149 141
150 142
151 143 class IPDoctestOutputChecker(doctest.OutputChecker):
152 144 """Second-chance checker with support for random tests.
153 145
154 146 If the default comparison doesn't pass, this checker looks in the expected
155 147 output string for flags that tell us to ignore the output.
156 148 """
157 149
158 150 random_re = re.compile(r'#\s*random\s+')
159 151
160 152 def check_output(self, want, got, optionflags):
161 153 """Check output, accepting special markers embedded in the output.
162 154
163 155 If the output didn't pass the default validation but the special string
164 156 '#random' is included, we accept it."""
165 157
166 158 # Let the original tester verify first, in case people have valid tests
167 159 # that happen to have a comment saying '#random' embedded in.
168 160 ret = doctest.OutputChecker.check_output(self, want, got,
169 161 optionflags)
170 162 if not ret and self.random_re.search(want):
171 163 #print >> sys.stderr, 'RANDOM OK:',want # dbg
172 164 return True
173 165
174 166 return ret
175 167
176 168
177 169 # A simple subclassing of the original with a different class name, so we can
178 170 # distinguish and treat differently IPython examples from pure python ones.
179 171 class IPExample(doctest.Example): pass
180 172
181 173
182 174 class IPExternalExample(doctest.Example):
183 175 """Doctest examples to be run in an external process."""
184 176
185 177 def __init__(self, source, want, exc_msg=None, lineno=0, indent=0,
186 178 options=None):
187 179 # Parent constructor
188 180 doctest.Example.__init__(self,source,want,exc_msg,lineno,indent,options)
189 181
190 182 # An EXTRA newline is needed to prevent pexpect hangs
191 183 self.source += '\n'
192 184
193 185
194 186 class IPDocTestParser(doctest.DocTestParser):
195 187 """
196 188 A class used to parse strings containing doctest examples.
197 189
198 190 Note: This is a version modified to properly recognize IPython input and
199 191 convert any IPython examples into valid Python ones.
200 192 """
201 193 # This regular expression is used to find doctest examples in a
202 194 # string. It defines three groups: `source` is the source code
203 195 # (including leading indentation and prompts); `indent` is the
204 196 # indentation of the first (PS1) line of the source code; and
205 197 # `want` is the expected output (including leading indentation).
206 198
207 199 # Classic Python prompts or default IPython ones
208 200 _PS1_PY = r'>>>'
209 201 _PS2_PY = r'\.\.\.'
210 202
211 203 _PS1_IP = r'In\ \[\d+\]:'
212 204 _PS2_IP = r'\ \ \ \.\.\.+:'
213 205
214 206 _RE_TPL = r'''
215 207 # Source consists of a PS1 line followed by zero or more PS2 lines.
216 208 (?P<source>
217 209 (?:^(?P<indent> [ ]*) (?P<ps1> %s) .*) # PS1 line
218 210 (?:\n [ ]* (?P<ps2> %s) .*)*) # PS2 lines
219 211 \n? # a newline
220 212 # Want consists of any non-blank lines that do not start with PS1.
221 213 (?P<want> (?:(?![ ]*$) # Not a blank line
222 214 (?![ ]*%s) # Not a line starting with PS1
223 215 (?![ ]*%s) # Not a line starting with PS2
224 216 .*$\n? # But any other line
225 217 )*)
226 218 '''
227 219
228 220 _EXAMPLE_RE_PY = re.compile( _RE_TPL % (_PS1_PY,_PS2_PY,_PS1_PY,_PS2_PY),
229 221 re.MULTILINE | re.VERBOSE)
230 222
231 223 _EXAMPLE_RE_IP = re.compile( _RE_TPL % (_PS1_IP,_PS2_IP,_PS1_IP,_PS2_IP),
232 224 re.MULTILINE | re.VERBOSE)
233 225
234 226 # Mark a test as being fully random. In this case, we simply append the
235 227 # random marker ('#random') to each individual example's output. This way
236 228 # we don't need to modify any other code.
237 229 _RANDOM_TEST = re.compile(r'#\s*all-random\s+')
238 230
239 231 # Mark tests to be executed in an external process - currently unsupported.
240 232 _EXTERNAL_IP = re.compile(r'#\s*ipdoctest:\s*EXTERNAL')
241 233
242 234 def ip2py(self,source):
243 235 """Convert input IPython source into valid Python."""
244 236 block = _ip.input_transformer_manager.transform_cell(source)
245 237 if len(block.splitlines()) == 1:
246 238 return _ip.prefilter(block)
247 239 else:
248 240 return block
249 241
250 242 def parse(self, string, name='<string>'):
251 243 """
252 244 Divide the given string into examples and intervening text,
253 245 and return them as a list of alternating Examples and strings.
254 246 Line numbers for the Examples are 0-based. The optional
255 247 argument `name` is a name identifying this string, and is only
256 248 used for error messages.
257 249 """
258 250
259 251 #print 'Parse string:\n',string # dbg
260 252
261 253 string = string.expandtabs()
262 254 # If all lines begin with the same indentation, then strip it.
263 255 min_indent = self._min_indent(string)
264 256 if min_indent > 0:
265 257 string = '\n'.join([l[min_indent:] for l in string.split('\n')])
266 258
267 259 output = []
268 260 charno, lineno = 0, 0
269 261
270 262 # We make 'all random' tests by adding the '# random' mark to every
271 263 # block of output in the test.
272 264 if self._RANDOM_TEST.search(string):
273 265 random_marker = '\n# random'
274 266 else:
275 267 random_marker = ''
276 268
277 269 # Whether to convert the input from ipython to python syntax
278 270 ip2py = False
279 271 # Find all doctest examples in the string. First, try them as Python
280 272 # examples, then as IPython ones
281 273 terms = list(self._EXAMPLE_RE_PY.finditer(string))
282 274 if terms:
283 275 # Normal Python example
284 276 #print '-'*70 # dbg
285 277 #print 'PyExample, Source:\n',string # dbg
286 278 #print '-'*70 # dbg
287 279 Example = doctest.Example
288 280 else:
289 281 # It's an ipython example. Note that IPExamples are run
290 282 # in-process, so their syntax must be turned into valid python.
291 283 # IPExternalExamples are run out-of-process (via pexpect) so they
292 284 # don't need any filtering (a real ipython will be executing them).
293 285 terms = list(self._EXAMPLE_RE_IP.finditer(string))
294 286 if self._EXTERNAL_IP.search(string):
295 287 #print '-'*70 # dbg
296 288 #print 'IPExternalExample, Source:\n',string # dbg
297 289 #print '-'*70 # dbg
298 290 Example = IPExternalExample
299 291 else:
300 292 #print '-'*70 # dbg
301 293 #print 'IPExample, Source:\n',string # dbg
302 294 #print '-'*70 # dbg
303 295 Example = IPExample
304 296 ip2py = True
305 297
306 298 for m in terms:
307 299 # Add the pre-example text to `output`.
308 300 output.append(string[charno:m.start()])
309 301 # Update lineno (lines before this example)
310 302 lineno += string.count('\n', charno, m.start())
311 303 # Extract info from the regexp match.
312 304 (source, options, want, exc_msg) = \
313 305 self._parse_example(m, name, lineno,ip2py)
314 306
315 307 # Append the random-output marker (it defaults to empty in most
316 308 # cases, it's only non-empty for 'all-random' tests):
317 309 want += random_marker
318 310
319 311 if Example is IPExternalExample:
320 312 options[doctest.NORMALIZE_WHITESPACE] = True
321 313 want += '\n'
322 314
323 315 # Create an Example, and add it to the list.
324 316 if not self._IS_BLANK_OR_COMMENT(source):
325 317 output.append(Example(source, want, exc_msg,
326 318 lineno=lineno,
327 319 indent=min_indent+len(m.group('indent')),
328 320 options=options))
329 321 # Update lineno (lines inside this example)
330 322 lineno += string.count('\n', m.start(), m.end())
331 323 # Update charno.
332 324 charno = m.end()
333 325 # Add any remaining post-example text to `output`.
334 326 output.append(string[charno:])
335 327 return output
336 328
337 329 def _parse_example(self, m, name, lineno,ip2py=False):
338 330 """
339 331 Given a regular expression match from `_EXAMPLE_RE` (`m`),
340 332 return a pair `(source, want)`, where `source` is the matched
341 333 example's source code (with prompts and indentation stripped);
342 334 and `want` is the example's expected output (with indentation
343 335 stripped).
344 336
345 337 `name` is the string's name, and `lineno` is the line number
346 338 where the example starts; both are used for error messages.
347 339
348 340 Optional:
349 341 `ip2py`: if true, filter the input via IPython to convert the syntax
350 342 into valid python.
351 343 """
352 344
353 345 # Get the example's indentation level.
354 346 indent = len(m.group('indent'))
355 347
356 348 # Divide source into lines; check that they're properly
357 349 # indented; and then strip their indentation & prompts.
358 350 source_lines = m.group('source').split('\n')
359 351
360 352 # We're using variable-length input prompts
361 353 ps1 = m.group('ps1')
362 354 ps2 = m.group('ps2')
363 355 ps1_len = len(ps1)
364 356
365 357 self._check_prompt_blank(source_lines, indent, name, lineno,ps1_len)
366 358 if ps2:
367 359 self._check_prefix(source_lines[1:], ' '*indent + ps2, name, lineno)
368 360
369 361 source = '\n'.join([sl[indent+ps1_len+1:] for sl in source_lines])
370 362
371 363 if ip2py:
372 364 # Convert source input from IPython into valid Python syntax
373 365 source = self.ip2py(source)
374 366
375 367 # Divide want into lines; check that it's properly indented; and
376 368 # then strip the indentation. Spaces before the last newline should
377 369 # be preserved, so plain rstrip() isn't good enough.
378 370 want = m.group('want')
379 371 want_lines = want.split('\n')
380 372 if len(want_lines) > 1 and re.match(r' *$', want_lines[-1]):
381 373 del want_lines[-1] # forget final newline & spaces after it
382 374 self._check_prefix(want_lines, ' '*indent, name,
383 375 lineno + len(source_lines))
384 376
385 377 # Remove ipython output prompt that might be present in the first line
386 378 want_lines[0] = re.sub(r'Out\[\d+\]: \s*?\n?','',want_lines[0])
387 379
388 380 want = '\n'.join([wl[indent:] for wl in want_lines])
389 381
390 382 # If `want` contains a traceback message, then extract it.
391 383 m = self._EXCEPTION_RE.match(want)
392 384 if m:
393 385 exc_msg = m.group('msg')
394 386 else:
395 387 exc_msg = None
396 388
397 389 # Extract options from the source.
398 390 options = self._find_options(source, name, lineno)
399 391
400 392 return source, options, want, exc_msg
401 393
402 394 def _check_prompt_blank(self, lines, indent, name, lineno, ps1_len):
403 395 """
404 396 Given the lines of a source string (including prompts and
405 397 leading indentation), check to make sure that every prompt is
406 398 followed by a space character. If any line is not followed by
407 399 a space character, then raise ValueError.
408 400
409 401 Note: IPython-modified version which takes the input prompt length as a
410 402 parameter, so that prompts of variable length can be dealt with.
411 403 """
412 404 space_idx = indent+ps1_len
413 405 min_len = space_idx+1
414 406 for i, line in enumerate(lines):
415 407 if len(line) >= min_len and line[space_idx] != ' ':
416 408 raise ValueError('line %r of the docstring for %s '
417 409 'lacks blank after %s: %r' %
418 410 (lineno+i+1, name,
419 411 line[indent:space_idx], line))
420 412
421 413
422 414 SKIP = doctest.register_optionflag('SKIP')
423 415
424 416
425 417 class IPDocTestRunner(doctest.DocTestRunner,object):
426 418 """Test runner that synchronizes the IPython namespace with test globals.
427 419 """
428 420
429 421 def run(self, test, compileflags=None, out=None, clear_globs=True):
430 422
431 423 # Hack: ipython needs access to the execution context of the example,
432 424 # so that it can propagate user variables loaded by %run into
433 425 # test.globs. We put them here into our modified %run as a function
434 426 # attribute. Our new %run will then only make the namespace update
435 427 # when called (rather than unconditionally updating test.globs here
436 428 # for all examples, most of which won't be calling %run anyway).
437 429 #_ip._ipdoctest_test_globs = test.globs
438 430 #_ip._ipdoctest_test_filename = test.filename
439 431
440 432 test.globs.update(_ip.user_ns)
441 433
442 434 # Override terminal size to standardise traceback format
443 435 with modified_env({'COLUMNS': '80', 'LINES': '24'}):
444 436 return super(IPDocTestRunner,self).run(test,
445 437 compileflags,out,clear_globs)
446
447
448 class DocFileCase(doctest.DocFileCase):
449 """Overrides to provide filename
450 """
451 def address(self):
452 return (self._dt_test.filename, None, None)
General Comments 0
You need to be logged in to leave comments. Login now