##// END OF EJS Templates
use pathlib in testing/plugin/ipdoctest.py
rushabh-v -
Show More
@@ -1,761 +1,762 b''
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 builtins as builtin_mod
23 23 import doctest
24 24 import inspect
25 25 import logging
26 26 import os
27 27 import re
28 28 import sys
29 29 from importlib import import_module
30 30 from io import StringIO
31 31
32 32 from testpath import modified_env
33 33
34 34 from inspect import getmodule
35 35
36 from pathlib import Path, PurePath
37
36 38 # We are overriding the default doctest runner, so we need to import a few
37 39 # things from doctest directly
38 40 from doctest import (REPORTING_FLAGS, REPORT_ONLY_FIRST_FAILURE,
39 41 _unittest_reportflags, DocTestRunner,
40 42 _extract_future_flags, pdb, _OutputRedirectingPdb,
41 43 _exception_traceback,
42 44 linecache)
43 45
44 46 # Third-party modules
45 47
46 48 from nose.plugins import doctests, Plugin
47 49 from nose.util import anyp, tolist
48 50
49 51 #-----------------------------------------------------------------------------
50 52 # Module globals and other constants
51 53 #-----------------------------------------------------------------------------
52 54
53 55 log = logging.getLogger(__name__)
54 56
55 57
56 58 #-----------------------------------------------------------------------------
57 59 # Classes and functions
58 60 #-----------------------------------------------------------------------------
59 61
60 62 def is_extension_module(filename):
61 63 """Return whether the given filename is an extension module.
62 64
63 65 This simply checks that the extension is either .so or .pyd.
64 66 """
65 67 return os.path.splitext(filename)[1].lower() in ('.so','.pyd')
66 68
67 69
68 70 class DocTestSkip(object):
69 71 """Object wrapper for doctests to be skipped."""
70 72
71 73 ds_skip = """Doctest to skip.
72 74 >>> 1 #doctest: +SKIP
73 75 """
74 76
75 77 def __init__(self,obj):
76 78 self.obj = obj
77 79
78 80 def __getattribute__(self,key):
79 81 if key == '__doc__':
80 82 return DocTestSkip.ds_skip
81 83 else:
82 84 return getattr(object.__getattribute__(self,'obj'),key)
83 85
84 86 # Modified version of the one in the stdlib, that fixes a python bug (doctests
85 87 # not found in extension modules, http://bugs.python.org/issue3158)
86 88 class DocTestFinder(doctest.DocTestFinder):
87 89
88 90 def _from_module(self, module, object):
89 91 """
90 92 Return true if the given object is defined in the given
91 93 module.
92 94 """
93 95 if module is None:
94 96 return True
95 97 elif inspect.isfunction(object):
96 98 return module.__dict__ is object.__globals__
97 99 elif inspect.isbuiltin(object):
98 100 return module.__name__ == object.__module__
99 101 elif inspect.isclass(object):
100 102 return module.__name__ == object.__module__
101 103 elif inspect.ismethod(object):
102 104 # This one may be a bug in cython that fails to correctly set the
103 105 # __module__ attribute of methods, but since the same error is easy
104 106 # to make by extension code writers, having this safety in place
105 107 # isn't such a bad idea
106 108 return module.__name__ == object.__self__.__class__.__module__
107 109 elif inspect.getmodule(object) is not None:
108 110 return module is inspect.getmodule(object)
109 111 elif hasattr(object, '__module__'):
110 112 return module.__name__ == object.__module__
111 113 elif isinstance(object, property):
112 114 return True # [XX] no way not be sure.
113 115 elif inspect.ismethoddescriptor(object):
114 116 # Unbound PyQt signals reach this point in Python 3.4b3, and we want
115 117 # to avoid throwing an error. See also http://bugs.python.org/issue3158
116 118 return False
117 119 else:
118 120 raise ValueError("object must be a class or function, got %r" % object)
119 121
120 122 def _find(self, tests, obj, name, module, source_lines, globs, seen):
121 123 """
122 124 Find tests for the given object and any contained objects, and
123 125 add them to `tests`.
124 126 """
125 127 print('_find for:', obj, name, module) # dbg
126 128 if hasattr(obj,"skip_doctest"):
127 129 #print 'SKIPPING DOCTEST FOR:',obj # dbg
128 130 obj = DocTestSkip(obj)
129 131
130 132 doctest.DocTestFinder._find(self,tests, obj, name, module,
131 133 source_lines, globs, seen)
132 134
133 135 # Below we re-run pieces of the above method with manual modifications,
134 136 # because the original code is buggy and fails to correctly identify
135 137 # doctests in extension modules.
136 138
137 139 # Local shorthands
138 140 from inspect import isroutine, isclass
139 141
140 142 # Look for tests in a module's contained objects.
141 143 if inspect.ismodule(obj) and self._recurse:
142 144 for valname, val in obj.__dict__.items():
143 145 valname1 = '%s.%s' % (name, valname)
144 146 if ( (isroutine(val) or isclass(val))
145 147 and self._from_module(module, val) ):
146 148
147 149 self._find(tests, val, valname1, module, source_lines,
148 150 globs, seen)
149 151
150 152 # Look for tests in a class's contained objects.
151 153 if inspect.isclass(obj) and self._recurse:
152 154 #print 'RECURSE into class:',obj # dbg
153 155 for valname, val in obj.__dict__.items():
154 156 # Special handling for staticmethod/classmethod.
155 157 if isinstance(val, staticmethod):
156 158 val = getattr(obj, valname)
157 159 if isinstance(val, classmethod):
158 160 val = getattr(obj, valname).__func__
159 161
160 162 # Recurse to methods, properties, and nested classes.
161 163 if ((inspect.isfunction(val) or inspect.isclass(val) or
162 164 inspect.ismethod(val) or
163 165 isinstance(val, property)) and
164 166 self._from_module(module, val)):
165 167 valname = '%s.%s' % (name, valname)
166 168 self._find(tests, val, valname, module, source_lines,
167 169 globs, seen)
168 170
169 171
170 172 class IPDoctestOutputChecker(doctest.OutputChecker):
171 173 """Second-chance checker with support for random tests.
172 174
173 175 If the default comparison doesn't pass, this checker looks in the expected
174 176 output string for flags that tell us to ignore the output.
175 177 """
176 178
177 179 random_re = re.compile(r'#\s*random\s+')
178 180
179 181 def check_output(self, want, got, optionflags):
180 182 """Check output, accepting special markers embedded in the output.
181 183
182 184 If the output didn't pass the default validation but the special string
183 185 '#random' is included, we accept it."""
184 186
185 187 # Let the original tester verify first, in case people have valid tests
186 188 # that happen to have a comment saying '#random' embedded in.
187 189 ret = doctest.OutputChecker.check_output(self, want, got,
188 190 optionflags)
189 191 if not ret and self.random_re.search(want):
190 192 #print >> sys.stderr, 'RANDOM OK:',want # dbg
191 193 return True
192 194
193 195 return ret
194 196
195 197
196 198 class DocTestCase(doctests.DocTestCase):
197 199 """Proxy for DocTestCase: provides an address() method that
198 200 returns the correct address for the doctest case. Otherwise
199 201 acts as a proxy to the test case. To provide hints for address(),
200 202 an obj may also be passed -- this will be used as the test object
201 203 for purposes of determining the test address, if it is provided.
202 204 """
203 205
204 206 # Note: this method was taken from numpy's nosetester module.
205 207
206 208 # Subclass nose.plugins.doctests.DocTestCase to work around a bug in
207 209 # its constructor that blocks non-default arguments from being passed
208 210 # down into doctest.DocTestCase
209 211
210 212 def __init__(self, test, optionflags=0, setUp=None, tearDown=None,
211 213 checker=None, obj=None, result_var='_'):
212 214 self._result_var = result_var
213 215 doctests.DocTestCase.__init__(self, test,
214 216 optionflags=optionflags,
215 217 setUp=setUp, tearDown=tearDown,
216 218 checker=checker)
217 219 # Now we must actually copy the original constructor from the stdlib
218 220 # doctest class, because we can't call it directly and a bug in nose
219 221 # means it never gets passed the right arguments.
220 222
221 223 self._dt_optionflags = optionflags
222 224 self._dt_checker = checker
223 225 self._dt_test = test
224 226 self._dt_test_globs_ori = test.globs
225 227 self._dt_setUp = setUp
226 228 self._dt_tearDown = tearDown
227 229
228 230 # XXX - store this runner once in the object!
229 231 runner = IPDocTestRunner(optionflags=optionflags,
230 232 checker=checker, verbose=False)
231 233 self._dt_runner = runner
232 234
233 235
234 236 # Each doctest should remember the directory it was loaded from, so
235 237 # things like %run work without too many contortions
236 238 self._ori_dir = os.path.dirname(test.filename)
237 239
238 240 # Modified runTest from the default stdlib
239 241 def runTest(self):
240 242 test = self._dt_test
241 243 runner = self._dt_runner
242 244
243 245 old = sys.stdout
244 246 new = StringIO()
245 247 optionflags = self._dt_optionflags
246 248
247 249 if not (optionflags & REPORTING_FLAGS):
248 250 # The option flags don't include any reporting flags,
249 251 # so add the default reporting flags
250 252 optionflags |= _unittest_reportflags
251 253
252 254 try:
253 255 # Save our current directory and switch out to the one where the
254 256 # test was originally created, in case another doctest did a
255 257 # directory change. We'll restore this in the finally clause.
256 258 curdir = os.getcwd()
257 259 #print 'runTest in dir:', self._ori_dir # dbg
258 260 os.chdir(self._ori_dir)
259 261
260 262 runner.DIVIDER = "-"*70
261 263 failures, tries = runner.run(test,out=new.write,
262 264 clear_globs=False)
263 265 finally:
264 266 sys.stdout = old
265 267 os.chdir(curdir)
266 268
267 269 if failures:
268 270 raise self.failureException(self.format_failure(new.getvalue()))
269 271
270 272 def setUp(self):
271 273 """Modified test setup that syncs with ipython namespace"""
272 274 #print "setUp test", self._dt_test.examples # dbg
273 275 if isinstance(self._dt_test.examples[0], IPExample):
274 276 # for IPython examples *only*, we swap the globals with the ipython
275 277 # namespace, after updating it with the globals (which doctest
276 278 # fills with the necessary info from the module being tested).
277 279 self.user_ns_orig = {}
278 280 self.user_ns_orig.update(_ip.user_ns)
279 281 _ip.user_ns.update(self._dt_test.globs)
280 282 # We must remove the _ key in the namespace, so that Python's
281 283 # doctest code sets it naturally
282 284 _ip.user_ns.pop('_', None)
283 285 _ip.user_ns['__builtins__'] = builtin_mod
284 286 self._dt_test.globs = _ip.user_ns
285 287
286 288 super(DocTestCase, self).setUp()
287 289
288 290 def tearDown(self):
289 291
290 292 # Undo the test.globs reassignment we made, so that the parent class
291 293 # teardown doesn't destroy the ipython namespace
292 294 if isinstance(self._dt_test.examples[0], IPExample):
293 295 self._dt_test.globs = self._dt_test_globs_ori
294 296 _ip.user_ns.clear()
295 297 _ip.user_ns.update(self.user_ns_orig)
296 298
297 299 # XXX - fperez: I am not sure if this is truly a bug in nose 0.11, but
298 300 # it does look like one to me: its tearDown method tries to run
299 301 #
300 302 # delattr(builtin_mod, self._result_var)
301 303 #
302 304 # without checking that the attribute really is there; it implicitly
303 305 # assumes it should have been set via displayhook. But if the
304 306 # displayhook was never called, this doesn't necessarily happen. I
305 307 # haven't been able to find a little self-contained example outside of
306 308 # ipython that would show the problem so I can report it to the nose
307 309 # team, but it does happen a lot in our code.
308 310 #
309 311 # So here, we just protect as narrowly as possible by trapping an
310 312 # attribute error whose message would be the name of self._result_var,
311 313 # and letting any other error propagate.
312 314 try:
313 315 super(DocTestCase, self).tearDown()
314 316 except AttributeError as exc:
315 317 if exc.args[0] != self._result_var:
316 318 raise
317 319
318 320
319 321 # A simple subclassing of the original with a different class name, so we can
320 322 # distinguish and treat differently IPython examples from pure python ones.
321 323 class IPExample(doctest.Example): pass
322 324
323 325
324 326 class IPExternalExample(doctest.Example):
325 327 """Doctest examples to be run in an external process."""
326 328
327 329 def __init__(self, source, want, exc_msg=None, lineno=0, indent=0,
328 330 options=None):
329 331 # Parent constructor
330 332 doctest.Example.__init__(self,source,want,exc_msg,lineno,indent,options)
331 333
332 334 # An EXTRA newline is needed to prevent pexpect hangs
333 335 self.source += '\n'
334 336
335 337
336 338 class IPDocTestParser(doctest.DocTestParser):
337 339 """
338 340 A class used to parse strings containing doctest examples.
339 341
340 342 Note: This is a version modified to properly recognize IPython input and
341 343 convert any IPython examples into valid Python ones.
342 344 """
343 345 # This regular expression is used to find doctest examples in a
344 346 # string. It defines three groups: `source` is the source code
345 347 # (including leading indentation and prompts); `indent` is the
346 348 # indentation of the first (PS1) line of the source code; and
347 349 # `want` is the expected output (including leading indentation).
348 350
349 351 # Classic Python prompts or default IPython ones
350 352 _PS1_PY = r'>>>'
351 353 _PS2_PY = r'\.\.\.'
352 354
353 355 _PS1_IP = r'In\ \[\d+\]:'
354 356 _PS2_IP = r'\ \ \ \.\.\.+:'
355 357
356 358 _RE_TPL = r'''
357 359 # Source consists of a PS1 line followed by zero or more PS2 lines.
358 360 (?P<source>
359 361 (?:^(?P<indent> [ ]*) (?P<ps1> %s) .*) # PS1 line
360 362 (?:\n [ ]* (?P<ps2> %s) .*)*) # PS2 lines
361 363 \n? # a newline
362 364 # Want consists of any non-blank lines that do not start with PS1.
363 365 (?P<want> (?:(?![ ]*$) # Not a blank line
364 366 (?![ ]*%s) # Not a line starting with PS1
365 367 (?![ ]*%s) # Not a line starting with PS2
366 368 .*$\n? # But any other line
367 369 )*)
368 370 '''
369 371
370 372 _EXAMPLE_RE_PY = re.compile( _RE_TPL % (_PS1_PY,_PS2_PY,_PS1_PY,_PS2_PY),
371 373 re.MULTILINE | re.VERBOSE)
372 374
373 375 _EXAMPLE_RE_IP = re.compile( _RE_TPL % (_PS1_IP,_PS2_IP,_PS1_IP,_PS2_IP),
374 376 re.MULTILINE | re.VERBOSE)
375 377
376 378 # Mark a test as being fully random. In this case, we simply append the
377 379 # random marker ('#random') to each individual example's output. This way
378 380 # we don't need to modify any other code.
379 381 _RANDOM_TEST = re.compile(r'#\s*all-random\s+')
380 382
381 383 # Mark tests to be executed in an external process - currently unsupported.
382 384 _EXTERNAL_IP = re.compile(r'#\s*ipdoctest:\s*EXTERNAL')
383 385
384 386 def ip2py(self,source):
385 387 """Convert input IPython source into valid Python."""
386 388 block = _ip.input_transformer_manager.transform_cell(source)
387 389 if len(block.splitlines()) == 1:
388 390 return _ip.prefilter(block)
389 391 else:
390 392 return block
391 393
392 394 def parse(self, string, name='<string>'):
393 395 """
394 396 Divide the given string into examples and intervening text,
395 397 and return them as a list of alternating Examples and strings.
396 398 Line numbers for the Examples are 0-based. The optional
397 399 argument `name` is a name identifying this string, and is only
398 400 used for error messages.
399 401 """
400 402
401 403 #print 'Parse string:\n',string # dbg
402 404
403 405 string = string.expandtabs()
404 406 # If all lines begin with the same indentation, then strip it.
405 407 min_indent = self._min_indent(string)
406 408 if min_indent > 0:
407 409 string = '\n'.join([l[min_indent:] for l in string.split('\n')])
408 410
409 411 output = []
410 412 charno, lineno = 0, 0
411 413
412 414 # We make 'all random' tests by adding the '# random' mark to every
413 415 # block of output in the test.
414 416 if self._RANDOM_TEST.search(string):
415 417 random_marker = '\n# random'
416 418 else:
417 419 random_marker = ''
418 420
419 421 # Whether to convert the input from ipython to python syntax
420 422 ip2py = False
421 423 # Find all doctest examples in the string. First, try them as Python
422 424 # examples, then as IPython ones
423 425 terms = list(self._EXAMPLE_RE_PY.finditer(string))
424 426 if terms:
425 427 # Normal Python example
426 428 #print '-'*70 # dbg
427 429 #print 'PyExample, Source:\n',string # dbg
428 430 #print '-'*70 # dbg
429 431 Example = doctest.Example
430 432 else:
431 433 # It's an ipython example. Note that IPExamples are run
432 434 # in-process, so their syntax must be turned into valid python.
433 435 # IPExternalExamples are run out-of-process (via pexpect) so they
434 436 # don't need any filtering (a real ipython will be executing them).
435 437 terms = list(self._EXAMPLE_RE_IP.finditer(string))
436 438 if self._EXTERNAL_IP.search(string):
437 439 #print '-'*70 # dbg
438 440 #print 'IPExternalExample, Source:\n',string # dbg
439 441 #print '-'*70 # dbg
440 442 Example = IPExternalExample
441 443 else:
442 444 #print '-'*70 # dbg
443 445 #print 'IPExample, Source:\n',string # dbg
444 446 #print '-'*70 # dbg
445 447 Example = IPExample
446 448 ip2py = True
447 449
448 450 for m in terms:
449 451 # Add the pre-example text to `output`.
450 452 output.append(string[charno:m.start()])
451 453 # Update lineno (lines before this example)
452 454 lineno += string.count('\n', charno, m.start())
453 455 # Extract info from the regexp match.
454 456 (source, options, want, exc_msg) = \
455 457 self._parse_example(m, name, lineno,ip2py)
456 458
457 459 # Append the random-output marker (it defaults to empty in most
458 460 # cases, it's only non-empty for 'all-random' tests):
459 461 want += random_marker
460 462
461 463 if Example is IPExternalExample:
462 464 options[doctest.NORMALIZE_WHITESPACE] = True
463 465 want += '\n'
464 466
465 467 # Create an Example, and add it to the list.
466 468 if not self._IS_BLANK_OR_COMMENT(source):
467 469 output.append(Example(source, want, exc_msg,
468 470 lineno=lineno,
469 471 indent=min_indent+len(m.group('indent')),
470 472 options=options))
471 473 # Update lineno (lines inside this example)
472 474 lineno += string.count('\n', m.start(), m.end())
473 475 # Update charno.
474 476 charno = m.end()
475 477 # Add any remaining post-example text to `output`.
476 478 output.append(string[charno:])
477 479 return output
478 480
479 481 def _parse_example(self, m, name, lineno,ip2py=False):
480 482 """
481 483 Given a regular expression match from `_EXAMPLE_RE` (`m`),
482 484 return a pair `(source, want)`, where `source` is the matched
483 485 example's source code (with prompts and indentation stripped);
484 486 and `want` is the example's expected output (with indentation
485 487 stripped).
486 488
487 489 `name` is the string's name, and `lineno` is the line number
488 490 where the example starts; both are used for error messages.
489 491
490 492 Optional:
491 493 `ip2py`: if true, filter the input via IPython to convert the syntax
492 494 into valid python.
493 495 """
494 496
495 497 # Get the example's indentation level.
496 498 indent = len(m.group('indent'))
497 499
498 500 # Divide source into lines; check that they're properly
499 501 # indented; and then strip their indentation & prompts.
500 502 source_lines = m.group('source').split('\n')
501 503
502 504 # We're using variable-length input prompts
503 505 ps1 = m.group('ps1')
504 506 ps2 = m.group('ps2')
505 507 ps1_len = len(ps1)
506 508
507 509 self._check_prompt_blank(source_lines, indent, name, lineno,ps1_len)
508 510 if ps2:
509 511 self._check_prefix(source_lines[1:], ' '*indent + ps2, name, lineno)
510 512
511 513 source = '\n'.join([sl[indent+ps1_len+1:] for sl in source_lines])
512 514
513 515 if ip2py:
514 516 # Convert source input from IPython into valid Python syntax
515 517 source = self.ip2py(source)
516 518
517 519 # Divide want into lines; check that it's properly indented; and
518 520 # then strip the indentation. Spaces before the last newline should
519 521 # be preserved, so plain rstrip() isn't good enough.
520 522 want = m.group('want')
521 523 want_lines = want.split('\n')
522 524 if len(want_lines) > 1 and re.match(r' *$', want_lines[-1]):
523 525 del want_lines[-1] # forget final newline & spaces after it
524 526 self._check_prefix(want_lines, ' '*indent, name,
525 527 lineno + len(source_lines))
526 528
527 529 # Remove ipython output prompt that might be present in the first line
528 530 want_lines[0] = re.sub(r'Out\[\d+\]: \s*?\n?','',want_lines[0])
529 531
530 532 want = '\n'.join([wl[indent:] for wl in want_lines])
531 533
532 534 # If `want` contains a traceback message, then extract it.
533 535 m = self._EXCEPTION_RE.match(want)
534 536 if m:
535 537 exc_msg = m.group('msg')
536 538 else:
537 539 exc_msg = None
538 540
539 541 # Extract options from the source.
540 542 options = self._find_options(source, name, lineno)
541 543
542 544 return source, options, want, exc_msg
543 545
544 546 def _check_prompt_blank(self, lines, indent, name, lineno, ps1_len):
545 547 """
546 548 Given the lines of a source string (including prompts and
547 549 leading indentation), check to make sure that every prompt is
548 550 followed by a space character. If any line is not followed by
549 551 a space character, then raise ValueError.
550 552
551 553 Note: IPython-modified version which takes the input prompt length as a
552 554 parameter, so that prompts of variable length can be dealt with.
553 555 """
554 556 space_idx = indent+ps1_len
555 557 min_len = space_idx+1
556 558 for i, line in enumerate(lines):
557 559 if len(line) >= min_len and line[space_idx] != ' ':
558 560 raise ValueError('line %r of the docstring for %s '
559 561 'lacks blank after %s: %r' %
560 562 (lineno+i+1, name,
561 563 line[indent:space_idx], line))
562 564
563 565
564 566 SKIP = doctest.register_optionflag('SKIP')
565 567
566 568
567 569 class IPDocTestRunner(doctest.DocTestRunner,object):
568 570 """Test runner that synchronizes the IPython namespace with test globals.
569 571 """
570 572
571 573 def run(self, test, compileflags=None, out=None, clear_globs=True):
572 574
573 575 # Hack: ipython needs access to the execution context of the example,
574 576 # so that it can propagate user variables loaded by %run into
575 577 # test.globs. We put them here into our modified %run as a function
576 578 # attribute. Our new %run will then only make the namespace update
577 579 # when called (rather than unconditionally updating test.globs here
578 580 # for all examples, most of which won't be calling %run anyway).
579 581 #_ip._ipdoctest_test_globs = test.globs
580 582 #_ip._ipdoctest_test_filename = test.filename
581 583
582 584 test.globs.update(_ip.user_ns)
583 585
584 586 # Override terminal size to standardise traceback format
585 587 with modified_env({'COLUMNS': '80', 'LINES': '24'}):
586 588 return super(IPDocTestRunner,self).run(test,
587 589 compileflags,out,clear_globs)
588 590
589 591
590 592 class DocFileCase(doctest.DocFileCase):
591 593 """Overrides to provide filename
592 594 """
593 595 def address(self):
594 596 return (self._dt_test.filename, None, None)
595 597
596 598
597 599 class ExtensionDoctest(doctests.Doctest):
598 600 """Nose Plugin that supports doctests in extension modules.
599 601 """
600 602 name = 'extdoctest' # call nosetests with --with-extdoctest
601 603 enabled = True
602 604
603 605 def options(self, parser, env=os.environ):
604 606 Plugin.options(self, parser, env)
605 607 parser.add_option('--doctest-tests', action='store_true',
606 608 dest='doctest_tests',
607 609 default=env.get('NOSE_DOCTEST_TESTS',True),
608 610 help="Also look for doctests in test modules. "
609 611 "Note that classes, methods and functions should "
610 612 "have either doctests or non-doctest tests, "
611 613 "not both. [NOSE_DOCTEST_TESTS]")
612 614 parser.add_option('--doctest-extension', action="append",
613 615 dest="doctestExtension",
614 616 help="Also look for doctests in files with "
615 617 "this extension [NOSE_DOCTEST_EXTENSION]")
616 618 # Set the default as a list, if given in env; otherwise
617 619 # an additional value set on the command line will cause
618 620 # an error.
619 621 env_setting = env.get('NOSE_DOCTEST_EXTENSION')
620 622 if env_setting is not None:
621 623 parser.set_defaults(doctestExtension=tolist(env_setting))
622 624
623 625
624 626 def configure(self, options, config):
625 627 Plugin.configure(self, options, config)
626 628 # Pull standard doctest plugin out of config; we will do doctesting
627 629 config.plugins.plugins = [p for p in config.plugins.plugins
628 630 if p.name != 'doctest']
629 631 self.doctest_tests = options.doctest_tests
630 632 self.extension = tolist(options.doctestExtension)
631 633
632 634 self.parser = doctest.DocTestParser()
633 635 self.finder = DocTestFinder()
634 636 self.checker = IPDoctestOutputChecker()
635 637 self.globs = None
636 638 self.extraglobs = None
637 639
638 640
639 641 def loadTestsFromExtensionModule(self,filename):
640 642 bpath,mod = os.path.split(filename)
641 643 modname = os.path.splitext(mod)[0]
642 644 try:
643 645 sys.path.append(bpath)
644 646 module = import_module(modname)
645 647 tests = list(self.loadTestsFromModule(module))
646 648 finally:
647 649 sys.path.pop()
648 650 return tests
649 651
650 652 # NOTE: the method below is almost a copy of the original one in nose, with
651 653 # a few modifications to control output checking.
652 654
653 655 def loadTestsFromModule(self, module):
654 656 #print '*** ipdoctest - lTM',module # dbg
655 657
656 658 if not self.matches(module.__name__):
657 659 log.debug("Doctest doesn't want module %s", module)
658 660 return
659 661
660 662 tests = self.finder.find(module,globs=self.globs,
661 663 extraglobs=self.extraglobs)
662 664 if not tests:
663 665 return
664 666
665 667 # always use whitespace and ellipsis options
666 668 optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
667 669
668 670 tests.sort()
669 671 module_file = module.__file__
670 672 if module_file[-4:] in ('.pyc', '.pyo'):
671 673 module_file = module_file[:-1]
672 674 for test in tests:
673 675 if not test.examples:
674 676 continue
675 677 if not test.filename:
676 678 test.filename = module_file
677 679
678 680 yield DocTestCase(test,
679 681 optionflags=optionflags,
680 682 checker=self.checker)
681 683
682 684
683 685 def loadTestsFromFile(self, filename):
684 686 #print "ipdoctest - from file", filename # dbg
685 687 if is_extension_module(filename):
686 688 for t in self.loadTestsFromExtensionModule(filename):
687 689 yield t
688 690 else:
689 691 if self.extension and anyp(filename.endswith, self.extension):
690 name = os.path.basename(filename)
691 with open(filename) as dh:
692 doc = dh.read()
692 name = PurePath(filename).name
693 doc = Path(filename).read_text()
693 694 test = self.parser.get_doctest(
694 695 doc, globs={'__file__': filename}, name=name,
695 696 filename=filename, lineno=0)
696 697 if test.examples:
697 698 #print 'FileCase:',test.examples # dbg
698 699 yield DocFileCase(test)
699 700 else:
700 701 yield False # no tests to load
701 702
702 703
703 704 class IPythonDoctest(ExtensionDoctest):
704 705 """Nose Plugin that supports doctests in extension modules.
705 706 """
706 707 name = 'ipdoctest' # call nosetests with --with-ipdoctest
707 708 enabled = True
708 709
709 710 def makeTest(self, obj, parent):
710 711 """Look for doctests in the given object, which will be a
711 712 function, method or class.
712 713 """
713 714 #print 'Plugin analyzing:', obj, parent # dbg
714 715 # always use whitespace and ellipsis options
715 716 optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
716 717
717 718 doctests = self.finder.find(obj, module=getmodule(parent))
718 719 if doctests:
719 720 for test in doctests:
720 721 if len(test.examples) == 0:
721 722 continue
722 723
723 724 yield DocTestCase(test, obj=obj,
724 725 optionflags=optionflags,
725 726 checker=self.checker)
726 727
727 728 def options(self, parser, env=os.environ):
728 729 #print "Options for nose plugin:", self.name # dbg
729 730 Plugin.options(self, parser, env)
730 731 parser.add_option('--ipdoctest-tests', action='store_true',
731 732 dest='ipdoctest_tests',
732 733 default=env.get('NOSE_IPDOCTEST_TESTS',True),
733 734 help="Also look for doctests in test modules. "
734 735 "Note that classes, methods and functions should "
735 736 "have either doctests or non-doctest tests, "
736 737 "not both. [NOSE_IPDOCTEST_TESTS]")
737 738 parser.add_option('--ipdoctest-extension', action="append",
738 739 dest="ipdoctest_extension",
739 740 help="Also look for doctests in files with "
740 741 "this extension [NOSE_IPDOCTEST_EXTENSION]")
741 742 # Set the default as a list, if given in env; otherwise
742 743 # an additional value set on the command line will cause
743 744 # an error.
744 745 env_setting = env.get('NOSE_IPDOCTEST_EXTENSION')
745 746 if env_setting is not None:
746 747 parser.set_defaults(ipdoctest_extension=tolist(env_setting))
747 748
748 749 def configure(self, options, config):
749 750 #print "Configuring nose plugin:", self.name # dbg
750 751 Plugin.configure(self, options, config)
751 752 # Pull standard doctest plugin out of config; we will do doctesting
752 753 config.plugins.plugins = [p for p in config.plugins.plugins
753 754 if p.name != 'doctest']
754 755 self.doctest_tests = options.ipdoctest_tests
755 756 self.extension = tolist(options.ipdoctest_extension)
756 757
757 758 self.parser = IPDocTestParser()
758 759 self.finder = DocTestFinder(parser=self.parser)
759 760 self.checker = IPDoctestOutputChecker()
760 761 self.globs = None
761 762 self.extraglobs = None
General Comments 0
You need to be logged in to leave comments. Login now