Chris@87
|
1 # These classes implement a doctest runner plugin for nose, a "known failure"
|
Chris@87
|
2 # error class, and a customized TestProgram for NumPy.
|
Chris@87
|
3
|
Chris@87
|
4 # Because this module imports nose directly, it should not
|
Chris@87
|
5 # be used except by nosetester.py to avoid a general NumPy
|
Chris@87
|
6 # dependency on nose.
|
Chris@87
|
7 from __future__ import division, absolute_import, print_function
|
Chris@87
|
8
|
Chris@87
|
9 import os
|
Chris@87
|
10 import doctest
|
Chris@87
|
11
|
Chris@87
|
12 import nose
|
Chris@87
|
13 from nose.plugins import doctests as npd
|
Chris@87
|
14 from nose.plugins.errorclass import ErrorClass, ErrorClassPlugin
|
Chris@87
|
15 from nose.plugins.base import Plugin
|
Chris@87
|
16 from nose.util import src
|
Chris@87
|
17 import numpy
|
Chris@87
|
18 from .nosetester import get_package_name
|
Chris@87
|
19 import inspect
|
Chris@87
|
20
|
Chris@87
|
21 # Some of the classes in this module begin with 'Numpy' to clearly distinguish
|
Chris@87
|
22 # them from the plethora of very similar names from nose/unittest/doctest
|
Chris@87
|
23
|
Chris@87
|
24 #-----------------------------------------------------------------------------
|
Chris@87
|
25 # Modified version of the one in the stdlib, that fixes a python bug (doctests
|
Chris@87
|
26 # not found in extension modules, http://bugs.python.org/issue3158)
|
Chris@87
|
27 class NumpyDocTestFinder(doctest.DocTestFinder):
|
Chris@87
|
28
|
Chris@87
|
29 def _from_module(self, module, object):
|
Chris@87
|
30 """
|
Chris@87
|
31 Return true if the given object is defined in the given
|
Chris@87
|
32 module.
|
Chris@87
|
33 """
|
Chris@87
|
34 if module is None:
|
Chris@87
|
35 #print '_fm C1' # dbg
|
Chris@87
|
36 return True
|
Chris@87
|
37 elif inspect.isfunction(object):
|
Chris@87
|
38 #print '_fm C2' # dbg
|
Chris@87
|
39 return module.__dict__ is object.__globals__
|
Chris@87
|
40 elif inspect.isbuiltin(object):
|
Chris@87
|
41 #print '_fm C2-1' # dbg
|
Chris@87
|
42 return module.__name__ == object.__module__
|
Chris@87
|
43 elif inspect.isclass(object):
|
Chris@87
|
44 #print '_fm C3' # dbg
|
Chris@87
|
45 return module.__name__ == object.__module__
|
Chris@87
|
46 elif inspect.ismethod(object):
|
Chris@87
|
47 # This one may be a bug in cython that fails to correctly set the
|
Chris@87
|
48 # __module__ attribute of methods, but since the same error is easy
|
Chris@87
|
49 # to make by extension code writers, having this safety in place
|
Chris@87
|
50 # isn't such a bad idea
|
Chris@87
|
51 #print '_fm C3-1' # dbg
|
Chris@87
|
52 return module.__name__ == object.__self__.__class__.__module__
|
Chris@87
|
53 elif inspect.getmodule(object) is not None:
|
Chris@87
|
54 #print '_fm C4' # dbg
|
Chris@87
|
55 #print 'C4 mod',module,'obj',object # dbg
|
Chris@87
|
56 return module is inspect.getmodule(object)
|
Chris@87
|
57 elif hasattr(object, '__module__'):
|
Chris@87
|
58 #print '_fm C5' # dbg
|
Chris@87
|
59 return module.__name__ == object.__module__
|
Chris@87
|
60 elif isinstance(object, property):
|
Chris@87
|
61 #print '_fm C6' # dbg
|
Chris@87
|
62 return True # [XX] no way not be sure.
|
Chris@87
|
63 else:
|
Chris@87
|
64 raise ValueError("object must be a class or function")
|
Chris@87
|
65
|
Chris@87
|
66 def _find(self, tests, obj, name, module, source_lines, globs, seen):
|
Chris@87
|
67 """
|
Chris@87
|
68 Find tests for the given object and any contained objects, and
|
Chris@87
|
69 add them to `tests`.
|
Chris@87
|
70 """
|
Chris@87
|
71
|
Chris@87
|
72 doctest.DocTestFinder._find(self, tests, obj, name, module,
|
Chris@87
|
73 source_lines, globs, seen)
|
Chris@87
|
74
|
Chris@87
|
75 # Below we re-run pieces of the above method with manual modifications,
|
Chris@87
|
76 # because the original code is buggy and fails to correctly identify
|
Chris@87
|
77 # doctests in extension modules.
|
Chris@87
|
78
|
Chris@87
|
79 # Local shorthands
|
Chris@87
|
80 from inspect import isroutine, isclass, ismodule, isfunction, \
|
Chris@87
|
81 ismethod
|
Chris@87
|
82
|
Chris@87
|
83 # Look for tests in a module's contained objects.
|
Chris@87
|
84 if ismodule(obj) and self._recurse:
|
Chris@87
|
85 for valname, val in obj.__dict__.items():
|
Chris@87
|
86 valname1 = '%s.%s' % (name, valname)
|
Chris@87
|
87 if ( (isroutine(val) or isclass(val))
|
Chris@87
|
88 and self._from_module(module, val) ):
|
Chris@87
|
89
|
Chris@87
|
90 self._find(tests, val, valname1, module, source_lines,
|
Chris@87
|
91 globs, seen)
|
Chris@87
|
92
|
Chris@87
|
93
|
Chris@87
|
94 # Look for tests in a class's contained objects.
|
Chris@87
|
95 if isclass(obj) and self._recurse:
|
Chris@87
|
96 #print 'RECURSE into class:',obj # dbg
|
Chris@87
|
97 for valname, val in obj.__dict__.items():
|
Chris@87
|
98 #valname1 = '%s.%s' % (name, valname) # dbg
|
Chris@87
|
99 #print 'N',name,'VN:',valname,'val:',str(val)[:77] # dbg
|
Chris@87
|
100 # Special handling for staticmethod/classmethod.
|
Chris@87
|
101 if isinstance(val, staticmethod):
|
Chris@87
|
102 val = getattr(obj, valname)
|
Chris@87
|
103 if isinstance(val, classmethod):
|
Chris@87
|
104 val = getattr(obj, valname).__func__
|
Chris@87
|
105
|
Chris@87
|
106 # Recurse to methods, properties, and nested classes.
|
Chris@87
|
107 if ((isfunction(val) or isclass(val) or
|
Chris@87
|
108 ismethod(val) or isinstance(val, property)) and
|
Chris@87
|
109 self._from_module(module, val)):
|
Chris@87
|
110 valname = '%s.%s' % (name, valname)
|
Chris@87
|
111 self._find(tests, val, valname, module, source_lines,
|
Chris@87
|
112 globs, seen)
|
Chris@87
|
113
|
Chris@87
|
114
|
Chris@87
|
115 # second-chance checker; if the default comparison doesn't
|
Chris@87
|
116 # pass, then see if the expected output string contains flags that
|
Chris@87
|
117 # tell us to ignore the output
|
Chris@87
|
118 class NumpyOutputChecker(doctest.OutputChecker):
|
Chris@87
|
119 def check_output(self, want, got, optionflags):
|
Chris@87
|
120 ret = doctest.OutputChecker.check_output(self, want, got,
|
Chris@87
|
121 optionflags)
|
Chris@87
|
122 if not ret:
|
Chris@87
|
123 if "#random" in want:
|
Chris@87
|
124 return True
|
Chris@87
|
125
|
Chris@87
|
126 # it would be useful to normalize endianness so that
|
Chris@87
|
127 # bigendian machines don't fail all the tests (and there are
|
Chris@87
|
128 # actually some bigendian examples in the doctests). Let's try
|
Chris@87
|
129 # making them all little endian
|
Chris@87
|
130 got = got.replace("'>", "'<")
|
Chris@87
|
131 want= want.replace("'>", "'<")
|
Chris@87
|
132
|
Chris@87
|
133 # try to normalize out 32 and 64 bit default int sizes
|
Chris@87
|
134 for sz in [4, 8]:
|
Chris@87
|
135 got = got.replace("'<i%d'"%sz, "int")
|
Chris@87
|
136 want= want.replace("'<i%d'"%sz, "int")
|
Chris@87
|
137
|
Chris@87
|
138 ret = doctest.OutputChecker.check_output(self, want,
|
Chris@87
|
139 got, optionflags)
|
Chris@87
|
140
|
Chris@87
|
141 return ret
|
Chris@87
|
142
|
Chris@87
|
143
|
Chris@87
|
144 # Subclass nose.plugins.doctests.DocTestCase to work around a bug in
|
Chris@87
|
145 # its constructor that blocks non-default arguments from being passed
|
Chris@87
|
146 # down into doctest.DocTestCase
|
Chris@87
|
147 class NumpyDocTestCase(npd.DocTestCase):
|
Chris@87
|
148 def __init__(self, test, optionflags=0, setUp=None, tearDown=None,
|
Chris@87
|
149 checker=None, obj=None, result_var='_'):
|
Chris@87
|
150 self._result_var = result_var
|
Chris@87
|
151 self._nose_obj = obj
|
Chris@87
|
152 doctest.DocTestCase.__init__(self, test,
|
Chris@87
|
153 optionflags=optionflags,
|
Chris@87
|
154 setUp=setUp, tearDown=tearDown,
|
Chris@87
|
155 checker=checker)
|
Chris@87
|
156
|
Chris@87
|
157
|
Chris@87
|
158 print_state = numpy.get_printoptions()
|
Chris@87
|
159
|
Chris@87
|
160 class NumpyDoctest(npd.Doctest):
|
Chris@87
|
161 name = 'numpydoctest' # call nosetests with --with-numpydoctest
|
Chris@87
|
162 score = 1000 # load late, after doctest builtin
|
Chris@87
|
163
|
Chris@87
|
164 # always use whitespace and ellipsis options for doctests
|
Chris@87
|
165 doctest_optflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
|
Chris@87
|
166
|
Chris@87
|
167 # files that should be ignored for doctests
|
Chris@87
|
168 doctest_ignore = ['generate_numpy_api.py',
|
Chris@87
|
169 'setup.py']
|
Chris@87
|
170
|
Chris@87
|
171 # Custom classes; class variables to allow subclassing
|
Chris@87
|
172 doctest_case_class = NumpyDocTestCase
|
Chris@87
|
173 out_check_class = NumpyOutputChecker
|
Chris@87
|
174 test_finder_class = NumpyDocTestFinder
|
Chris@87
|
175
|
Chris@87
|
176 # Don't use the standard doctest option handler; hard-code the option values
|
Chris@87
|
177 def options(self, parser, env=os.environ):
|
Chris@87
|
178 Plugin.options(self, parser, env)
|
Chris@87
|
179 # Test doctests in 'test' files / directories. Standard plugin default
|
Chris@87
|
180 # is False
|
Chris@87
|
181 self.doctest_tests = True
|
Chris@87
|
182 # Variable name; if defined, doctest results stored in this variable in
|
Chris@87
|
183 # the top-level namespace. None is the standard default
|
Chris@87
|
184 self.doctest_result_var = None
|
Chris@87
|
185
|
Chris@87
|
186 def configure(self, options, config):
|
Chris@87
|
187 # parent method sets enabled flag from command line --with-numpydoctest
|
Chris@87
|
188 Plugin.configure(self, options, config)
|
Chris@87
|
189 self.finder = self.test_finder_class()
|
Chris@87
|
190 self.parser = doctest.DocTestParser()
|
Chris@87
|
191 if self.enabled:
|
Chris@87
|
192 # Pull standard doctest out of plugin list; there's no reason to run
|
Chris@87
|
193 # both. In practice the Unplugger plugin above would cover us when
|
Chris@87
|
194 # run from a standard numpy.test() call; this is just in case
|
Chris@87
|
195 # someone wants to run our plugin outside the numpy.test() machinery
|
Chris@87
|
196 config.plugins.plugins = [p for p in config.plugins.plugins
|
Chris@87
|
197 if p.name != 'doctest']
|
Chris@87
|
198
|
Chris@87
|
199 def set_test_context(self, test):
|
Chris@87
|
200 """ Configure `test` object to set test context
|
Chris@87
|
201
|
Chris@87
|
202 We set the numpy / scipy standard doctest namespace
|
Chris@87
|
203
|
Chris@87
|
204 Parameters
|
Chris@87
|
205 ----------
|
Chris@87
|
206 test : test object
|
Chris@87
|
207 with ``globs`` dictionary defining namespace
|
Chris@87
|
208
|
Chris@87
|
209 Returns
|
Chris@87
|
210 -------
|
Chris@87
|
211 None
|
Chris@87
|
212
|
Chris@87
|
213 Notes
|
Chris@87
|
214 -----
|
Chris@87
|
215 `test` object modified in place
|
Chris@87
|
216 """
|
Chris@87
|
217 # set the namespace for tests
|
Chris@87
|
218 pkg_name = get_package_name(os.path.dirname(test.filename))
|
Chris@87
|
219
|
Chris@87
|
220 # Each doctest should execute in an environment equivalent to
|
Chris@87
|
221 # starting Python and executing "import numpy as np", and,
|
Chris@87
|
222 # for SciPy packages, an additional import of the local
|
Chris@87
|
223 # package (so that scipy.linalg.basic.py's doctests have an
|
Chris@87
|
224 # implicit "from scipy import linalg" as well.
|
Chris@87
|
225 #
|
Chris@87
|
226 # Note: __file__ allows the doctest in NoseTester to run
|
Chris@87
|
227 # without producing an error
|
Chris@87
|
228 test.globs = {'__builtins__':__builtins__,
|
Chris@87
|
229 '__file__':'__main__',
|
Chris@87
|
230 '__name__':'__main__',
|
Chris@87
|
231 'np':numpy}
|
Chris@87
|
232 # add appropriate scipy import for SciPy tests
|
Chris@87
|
233 if 'scipy' in pkg_name:
|
Chris@87
|
234 p = pkg_name.split('.')
|
Chris@87
|
235 p2 = p[-1]
|
Chris@87
|
236 test.globs[p2] = __import__(pkg_name, test.globs, {}, [p2])
|
Chris@87
|
237
|
Chris@87
|
238 # Override test loading to customize test context (with set_test_context
|
Chris@87
|
239 # method), set standard docstring options, and install our own test output
|
Chris@87
|
240 # checker
|
Chris@87
|
241 def loadTestsFromModule(self, module):
|
Chris@87
|
242 if not self.matches(module.__name__):
|
Chris@87
|
243 npd.log.debug("Doctest doesn't want module %s", module)
|
Chris@87
|
244 return
|
Chris@87
|
245 try:
|
Chris@87
|
246 tests = self.finder.find(module)
|
Chris@87
|
247 except AttributeError:
|
Chris@87
|
248 # nose allows module.__test__ = False; doctest does not and
|
Chris@87
|
249 # throws AttributeError
|
Chris@87
|
250 return
|
Chris@87
|
251 if not tests:
|
Chris@87
|
252 return
|
Chris@87
|
253 tests.sort()
|
Chris@87
|
254 module_file = src(module.__file__)
|
Chris@87
|
255 for test in tests:
|
Chris@87
|
256 if not test.examples:
|
Chris@87
|
257 continue
|
Chris@87
|
258 if not test.filename:
|
Chris@87
|
259 test.filename = module_file
|
Chris@87
|
260 # Set test namespace; test altered in place
|
Chris@87
|
261 self.set_test_context(test)
|
Chris@87
|
262 yield self.doctest_case_class(test,
|
Chris@87
|
263 optionflags=self.doctest_optflags,
|
Chris@87
|
264 checker=self.out_check_class(),
|
Chris@87
|
265 result_var=self.doctest_result_var)
|
Chris@87
|
266
|
Chris@87
|
267 # Add an afterContext method to nose.plugins.doctests.Doctest in order
|
Chris@87
|
268 # to restore print options to the original state after each doctest
|
Chris@87
|
269 def afterContext(self):
|
Chris@87
|
270 numpy.set_printoptions(**print_state)
|
Chris@87
|
271
|
Chris@87
|
272 # Ignore NumPy-specific build files that shouldn't be searched for tests
|
Chris@87
|
273 def wantFile(self, file):
|
Chris@87
|
274 bn = os.path.basename(file)
|
Chris@87
|
275 if bn in self.doctest_ignore:
|
Chris@87
|
276 return False
|
Chris@87
|
277 return npd.Doctest.wantFile(self, file)
|
Chris@87
|
278
|
Chris@87
|
279
|
Chris@87
|
280 class Unplugger(object):
|
Chris@87
|
281 """ Nose plugin to remove named plugin late in loading
|
Chris@87
|
282
|
Chris@87
|
283 By default it removes the "doctest" plugin.
|
Chris@87
|
284 """
|
Chris@87
|
285 name = 'unplugger'
|
Chris@87
|
286 enabled = True # always enabled
|
Chris@87
|
287 score = 4000 # load late in order to be after builtins
|
Chris@87
|
288
|
Chris@87
|
289 def __init__(self, to_unplug='doctest'):
|
Chris@87
|
290 self.to_unplug = to_unplug
|
Chris@87
|
291
|
Chris@87
|
292 def options(self, parser, env):
|
Chris@87
|
293 pass
|
Chris@87
|
294
|
Chris@87
|
295 def configure(self, options, config):
|
Chris@87
|
296 # Pull named plugin out of plugins list
|
Chris@87
|
297 config.plugins.plugins = [p for p in config.plugins.plugins
|
Chris@87
|
298 if p.name != self.to_unplug]
|
Chris@87
|
299
|
Chris@87
|
300
|
Chris@87
|
301 class KnownFailureTest(Exception):
|
Chris@87
|
302 '''Raise this exception to mark a test as a known failing test.'''
|
Chris@87
|
303 pass
|
Chris@87
|
304
|
Chris@87
|
305
|
Chris@87
|
306 class KnownFailure(ErrorClassPlugin):
|
Chris@87
|
307 '''Plugin that installs a KNOWNFAIL error class for the
|
Chris@87
|
308 KnownFailureClass exception. When KnownFailureTest is raised,
|
Chris@87
|
309 the exception will be logged in the knownfail attribute of the
|
Chris@87
|
310 result, 'K' or 'KNOWNFAIL' (verbose) will be output, and the
|
Chris@87
|
311 exception will not be counted as an error or failure.'''
|
Chris@87
|
312 enabled = True
|
Chris@87
|
313 knownfail = ErrorClass(KnownFailureTest,
|
Chris@87
|
314 label='KNOWNFAIL',
|
Chris@87
|
315 isfailure=False)
|
Chris@87
|
316
|
Chris@87
|
317 def options(self, parser, env=os.environ):
|
Chris@87
|
318 env_opt = 'NOSE_WITHOUT_KNOWNFAIL'
|
Chris@87
|
319 parser.add_option('--no-knownfail', action='store_true',
|
Chris@87
|
320 dest='noKnownFail', default=env.get(env_opt, False),
|
Chris@87
|
321 help='Disable special handling of KnownFailureTest '
|
Chris@87
|
322 'exceptions')
|
Chris@87
|
323
|
Chris@87
|
324 def configure(self, options, conf):
|
Chris@87
|
325 if not self.can_configure:
|
Chris@87
|
326 return
|
Chris@87
|
327 self.conf = conf
|
Chris@87
|
328 disable = getattr(options, 'noKnownFail', False)
|
Chris@87
|
329 if disable:
|
Chris@87
|
330 self.enabled = False
|
Chris@87
|
331
|
Chris@87
|
332
|
Chris@87
|
333 # Class allows us to save the results of the tests in runTests - see runTests
|
Chris@87
|
334 # method docstring for details
|
Chris@87
|
335 class NumpyTestProgram(nose.core.TestProgram):
|
Chris@87
|
336 def runTests(self):
|
Chris@87
|
337 """Run Tests. Returns true on success, false on failure, and
|
Chris@87
|
338 sets self.success to the same value.
|
Chris@87
|
339
|
Chris@87
|
340 Because nose currently discards the test result object, but we need
|
Chris@87
|
341 to return it to the user, override TestProgram.runTests to retain
|
Chris@87
|
342 the result
|
Chris@87
|
343 """
|
Chris@87
|
344 if self.testRunner is None:
|
Chris@87
|
345 self.testRunner = nose.core.TextTestRunner(stream=self.config.stream,
|
Chris@87
|
346 verbosity=self.config.verbosity,
|
Chris@87
|
347 config=self.config)
|
Chris@87
|
348 plug_runner = self.config.plugins.prepareTestRunner(self.testRunner)
|
Chris@87
|
349 if plug_runner is not None:
|
Chris@87
|
350 self.testRunner = plug_runner
|
Chris@87
|
351 self.result = self.testRunner.run(self.test)
|
Chris@87
|
352 self.success = self.result.wasSuccessful()
|
Chris@87
|
353 return self.success
|