diff options
Diffstat (limited to 'numpy')
-rw-r--r-- | numpy/testing/noseclasses.py | 249 | ||||
-rw-r--r-- | numpy/testing/nosetester.py | 261 |
2 files changed, 324 insertions, 186 deletions
diff --git a/numpy/testing/noseclasses.py b/numpy/testing/noseclasses.py new file mode 100644 index 000000000..6b26a58c5 --- /dev/null +++ b/numpy/testing/noseclasses.py @@ -0,0 +1,249 @@ +# These classes implement a doctest runner plugin for nose. +# Because this module imports nose directly, it should not +# be used except by nosetester.py to avoid a general NumPy +# dependency on nose. + +import os +import doctest + +from nose.plugins import doctests as npd +from nose.plugins.base import Plugin +from nose.util import src, tolist +import numpy +from nosetester import get_package_name +import inspect + +_doctest_ignore = ['generate_numpy_api.py', 'scons_support.py', + 'setupscons.py', 'setup.py'] + +# All the classes in this module begin with 'numpy' to clearly distinguish them +# from the plethora of very similar names from nose/unittest/doctest + + +#----------------------------------------------------------------------------- +# Modified version of the one in the stdlib, that fixes a python bug (doctests +# not found in extension modules, http://bugs.python.org/issue3158) +class numpyDocTestFinder(doctest.DocTestFinder): + + def _from_module(self, module, object): + """ + Return true if the given object is defined in the given + module. + """ + if module is None: + #print '_fm C1' # dbg + return True + elif inspect.isfunction(object): + #print '_fm C2' # dbg + return module.__dict__ is object.func_globals + elif inspect.isbuiltin(object): + #print '_fm C2-1' # dbg + return module.__name__ == object.__module__ + elif inspect.isclass(object): + #print '_fm C3' # dbg + return module.__name__ == object.__module__ + elif inspect.ismethod(object): + # This one may be a bug in cython that fails to correctly set the + # __module__ attribute of methods, but since the same error is easy + # to make by extension code writers, having this safety in place + # isn't such a bad idea + #print '_fm C3-1' # dbg + return module.__name__ == object.im_class.__module__ + elif inspect.getmodule(object) is not None: + #print '_fm C4' # dbg + #print 'C4 mod',module,'obj',object # dbg + return module is inspect.getmodule(object) + elif hasattr(object, '__module__'): + #print '_fm C5' # dbg + return module.__name__ == object.__module__ + elif isinstance(object, property): + #print '_fm C6' # dbg + return True # [XX] no way not be sure. + else: + raise ValueError("object must be a class or function") + + + + def _find(self, tests, obj, name, module, source_lines, globs, seen): + """ + Find tests for the given object and any contained objects, and + add them to `tests`. + """ + + doctest.DocTestFinder._find(self,tests, obj, name, module, + source_lines, globs, seen) + + # Below we re-run pieces of the above method with manual modifications, + # because the original code is buggy and fails to correctly identify + # doctests in extension modules. + + # Local shorthands + from inspect import isroutine, isclass, ismodule + + # Look for tests in a module's contained objects. + if inspect.ismodule(obj) and self._recurse: + for valname, val in obj.__dict__.items(): + valname1 = '%s.%s' % (name, valname) + if ( (isroutine(val) or isclass(val)) + and self._from_module(module, val) ): + + self._find(tests, val, valname1, module, source_lines, + globs, seen) + + + # Look for tests in a class's contained objects. + if inspect.isclass(obj) and self._recurse: + #print 'RECURSE into class:',obj # dbg + for valname, val in obj.__dict__.items(): + #valname1 = '%s.%s' % (name, valname) # dbg + #print 'N',name,'VN:',valname,'val:',str(val)[:77] # dbg + # Special handling for staticmethod/classmethod. + if isinstance(val, staticmethod): + val = getattr(obj, valname) + if isinstance(val, classmethod): + val = getattr(obj, valname).im_func + + # Recurse to methods, properties, and nested classes. + if ((inspect.isfunction(val) or inspect.isclass(val) or + inspect.ismethod(val) or + isinstance(val, property)) and + self._from_module(module, val)): + valname = '%s.%s' % (name, valname) + self._find(tests, val, valname, module, source_lines, + globs, seen) + + +class numpyDocTestCase(npd.DocTestCase): + """Proxy for DocTestCase: provides an address() method that + returns the correct address for the doctest case. Otherwise + acts as a proxy to the test case. To provide hints for address(), + an obj may also be passed -- this will be used as the test object + for purposes of determining the test address, if it is provided. + """ + + # doctests loaded via find(obj) omit the module name + # so we need to override id, __repr__ and shortDescription + # bonus: this will squash a 2.3 vs 2.4 incompatiblity + def id(self): + name = self._dt_test.name + filename = self._dt_test.filename + if filename is not None: + pk = getpackage(filename) + if pk is not None and not name.startswith(pk): + name = "%s.%s" % (pk, name) + return name + + +# second-chance checker; if the default comparison doesn't +# pass, then see if the expected output string contains flags that +# tell us to ignore the output +class numpyOutputChecker(doctest.OutputChecker): + def check_output(self, want, got, optionflags): + ret = doctest.OutputChecker.check_output(self, want, got, + optionflags) + if not ret: + if "#random" in want: + return True + + return ret + + +# Subclass nose.plugins.doctests.DocTestCase to work around a bug in +# its constructor that blocks non-default arguments from being passed +# down into doctest.DocTestCase +class numpyDocTestCase(npd.DocTestCase): + def __init__(self, test, optionflags=0, setUp=None, tearDown=None, + checker=None, obj=None, result_var='_'): + self._result_var = result_var + self._nose_obj = obj + doctest.DocTestCase.__init__(self, test, + optionflags=optionflags, + setUp=setUp, tearDown=tearDown, + checker=checker) + + +print_state = numpy.get_printoptions() + +class numpyDoctest(npd.Doctest): + name = 'numpydoctest' # call nosetests with --with-numpydoctest + enabled = True + + def options(self, parser, env=os.environ): + Plugin.options(self, parser, env) + + def configure(self, options, config): + Plugin.configure(self, options, config) + self.doctest_tests = True + self.extension = tolist(options.doctestExtension) + self.finder = numpyDocTestFinder() + self.parser = doctest.DocTestParser() + + # Turns on whitespace normalization, set a minimal execution context + # for doctests, implement a "#random" directive to allow executing a + # command while ignoring its output. + def loadTestsFromModule(self, module): + if not self.matches(module.__name__): + npd.log.debug("Doctest doesn't want module %s", module) + return + try: + tests = self.finder.find(module) + except AttributeError: + # nose allows module.__test__ = False; doctest does not and + # throws AttributeError + return + if not tests: + return + tests.sort() + module_file = src(module.__file__) + for test in tests: + if not test.examples: + continue + if not test.filename: + test.filename = module_file + + pkg_name = get_package_name(os.path.dirname(test.filename)) + + # Each doctest should execute in an environment equivalent to + # starting Python and executing "import numpy as np", and, + # for SciPy packages, an additional import of the local + # package (so that scipy.linalg.basic.py's doctests have an + # implicit "from scipy import linalg" as well. + # + # Note: __file__ allows the doctest in NoseTester to run + # without producing an error + test.globs = {'__builtins__':__builtins__, + '__file__':'__main__', + '__name__':'__main__', + 'np':numpy} + + # add appropriate scipy import for SciPy tests + if 'scipy' in pkg_name: + p = pkg_name.split('.') + p1 = '.'.join(p[:-1]) + p2 = p[-1] + test.globs[p2] = __import__(pkg_name, fromlist=[p2]) + + print 'additional import for %s: from %s import %s' % (test.filename, p1, p2) + print ' (%s): %r' % (pkg_name, test.globs[p2]) + + # always use whitespace and ellipsis options + optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS + + yield numpyDocTestCase(test, + optionflags=optionflags, + checker=numpyOutputChecker()) + + + # Add an afterContext method to nose.plugins.doctests.Doctest in order + # to restore print options to the original state after each doctest + def afterContext(self): + numpy.set_printoptions(**print_state) + + + # Implement a wantFile method so that we can ignore NumPy-specific + # build files that shouldn't be searched for tests + def wantFile(self, file): + bn = os.path.basename(file) + if bn in _doctest_ignore: + return False + return npd.Doctest.wantFile(self, file) diff --git a/numpy/testing/nosetester.py b/numpy/testing/nosetester.py index 05abb57b5..0c54a8b64 100644 --- a/numpy/testing/nosetester.py +++ b/numpy/testing/nosetester.py @@ -7,125 +7,26 @@ import os import sys import warnings -# Patches nose functionality to add NumPy-specific features -# Note: This class should only be instantiated if nose has already -# been successfully imported -class NoseCustomizer: - __patched = False - - def __init__(self): - if NoseCustomizer.__patched: - return - - NoseCustomizer.__patched = True - - # used to monkeypatch the nose doctest classes - def monkeypatch_method(cls): - def decorator(func): - setattr(cls, func.__name__, func) - return func - return decorator - - from nose.plugins import doctests as npd - from nose.plugins.base import Plugin - from nose.util import src, tolist - import numpy - import doctest +def get_package_name(filepath): + # find the package name given a path name that's part of the package + fullpath = filepath[:] + pkg_name = [] + while 'site-packages' in filepath: + filepath, p2 = os.path.split(filepath) + if p2 == 'site-packages': + break + pkg_name.append(p2) + + # if package name determination failed, just default to numpy/scipy + if not pkg_name: + if 'scipy' in fullpath: + return 'scipy' + else: + return 'numpy' - # second-chance checker; if the default comparison doesn't - # pass, then see if the expected output string contains flags that - # tell us to ignore the output - class NumpyDoctestOutputChecker(doctest.OutputChecker): - def check_output(self, want, got, optionflags): - ret = doctest.OutputChecker.check_output(self, want, got, - optionflags) - if not ret: - if "#random" in want: - return True - - return ret - - - # Subclass nose.plugins.doctests.DocTestCase to work around a bug in - # its constructor that blocks non-default arguments from being passed - # down into doctest.DocTestCase - class NumpyDocTestCase(npd.DocTestCase): - def __init__(self, test, optionflags=0, setUp=None, tearDown=None, - checker=None, obj=None, result_var='_'): - self._result_var = result_var - self._nose_obj = obj - doctest.DocTestCase.__init__(self, test, - optionflags=optionflags, - setUp=setUp, tearDown=tearDown, - checker=checker) - - - - # This will replace the existing loadTestsFromModule method of - # nose.plugins.doctests.Doctest. It turns on whitespace normalization, - # adds an implicit "import numpy as np" for doctests, and adds a - # "#random" directive to allow executing a command while ignoring its - # output. - @monkeypatch_method(npd.Doctest) - def loadTestsFromModule(self, module): - if not self.matches(module.__name__): - npd.log.debug("Doctest doesn't want module %s", module) - return - try: - tests = self.finder.find(module) - except AttributeError: - # nose allows module.__test__ = False; doctest does not and - # throws AttributeError - return - if not tests: - return - tests.sort() - module_file = src(module.__file__) - for test in tests: - if not test.examples: - continue - if not test.filename: - test.filename = module_file - - # Each doctest should execute in an environment equivalent to - # starting Python and executing "import numpy as np" - # - # Note: __file__ allows the doctest in NoseTester to run - # without producing an error - test.globs = {'__builtins__':__builtins__, - '__file__':'__main__', - '__name__':'__main__', - 'np':numpy} - - # always use whitespace and ellipsis options - optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS - - yield NumpyDocTestCase(test, - optionflags=optionflags, - checker=NumpyDoctestOutputChecker()) - - # get original print options - print_state = numpy.get_printoptions() - - # Add an afterContext method to nose.plugins.doctests.Doctest in order - # to restore print options to the original state after each doctest - @monkeypatch_method(npd.Doctest) - def afterContext(self): - numpy.set_printoptions(**print_state) - - # Replace the existing wantFile method of nose.plugins.doctests.Doctest - # so that we can ignore NumPy-specific build files that shouldn't - # be searched for tests - old_wantFile = npd.Doctest.wantFile - ignore_files = ['generate_numpy_api.py', 'scons_support.py', - 'setupscons.py', 'setup.py'] - def wantFile(self, file): - bn = os.path.basename(file) - if bn in ignore_files: - return False - return old_wantFile(self, file) - - npd.Doctest.wantFile = wantFile + # otherwise, reverse to get correct order and return + pkg_name.reverse() + return '.'.join(pkg_name) def import_nose(): @@ -143,12 +44,11 @@ def import_nose(): fine_nose = False if not fine_nose: - raise ImportError('Need nose >=%d.%d.%d for tests - see ' - 'http://somethingaboutorange.com/mrl/projects/nose' % - minimum_nose_version) + msg = 'Need nose >= %d.%d.%d for tests - see ' \ + 'http://somethingaboutorange.com/mrl/projects/nose' % \ + minimum_nose_version - # nose was successfully imported; make customizations for doctests - NoseCustomizer() + raise ImportError(msg) return nose @@ -160,6 +60,30 @@ def run_module_suite(file_to_run = None): import_nose().run(argv=['',file_to_run]) +# contructs NoseTester method docstrings +def _docmethod(meth, testtype): + test_header = \ + '''Parameters + ---------- + label : {'fast', 'full', '', attribute identifer} + Identifies the %(testtype)ss to run. This can be a string to + pass to the nosetests executable with the '-A' option, or one of + several special values. + Special values are: + 'fast' - the default - which corresponds to nosetests -A option + of 'not slow'. + 'full' - fast (as above) and slow %(testtype)ss as in the + no -A option to nosetests - same as '' + None or '' - run all %(testtype)ss + attribute_identifier - string passed directly to nosetests as '-A' + verbose : integer + verbosity value for test outputs, 1-10 + extra_argv : list + List with any extra args to pass to nosetests''' \ + % {'testtype': testtype} + + meth.__doc__ = meth.__doc__ % {'test_header':test_header} + class NoseTester(object): """ Nose test runner. @@ -201,60 +125,8 @@ class NoseTester(object): # find the package name under test; this name is used to limit coverage # reporting (if enabled) - pkg_temp = package - pkg_name = [] - while 'site-packages' in pkg_temp: - pkg_temp, p2 = os.path.split(pkg_temp) - if p2 == 'site-packages': - break - pkg_name.append(p2) - - # if package name determination failed, just default to numpy/scipy - if not pkg_name: - if 'scipy' in self.package_path: - self.package_name = 'scipy' - else: - self.package_name = 'numpy' - else: - pkg_name.reverse() - self.package_name = '.'.join(pkg_name) - - def _add_doc(testtype): - ''' Decorator to add docstring to functions using test labels + self.package_name = get_package_name(package) - Parameters - ---------- - testtype : string - Type of test for function docstring - ''' - def docit(func): - test_header = \ - '''Parameters - ---------- - label : {'fast', 'full', '', attribute identifer} - Identifies %(testtype)s to run. This can be a string to pass to - the nosetests executable with the'-A' option, or one of - several special values. - Special values are: - 'fast' - the default - which corresponds to - nosetests -A option of - 'not slow'. - 'full' - fast (as above) and slow %(testtype)s as in - no -A option to nosetests - same as '' - None or '' - run all %(testtype)ss - attribute_identifier - string passed directly to - nosetests as '-A' - verbose : integer - verbosity value for test outputs, 1-10 - extra_argv : list - List with any extra args to pass to nosetests''' \ - % {'testtype': testtype} - func.__doc__ = func.__doc__ % { - 'test_header': test_header} - return func - return docit - - @_add_doc('(testtype)') def _test_argv(self, label, verbose, extra_argv): ''' Generate argv for nosetest command @@ -271,8 +143,8 @@ class NoseTester(object): if extra_argv: argv += extra_argv return argv + - @_add_doc('test') def test(self, label='fast', verbose=1, extra_argv=None, doctests=False, coverage=False, **kwargs): ''' Run tests for module using nose @@ -285,7 +157,9 @@ class NoseTester(object): (Requires the coverage module: http://nedbatchelder.com/code/modules/coverage.html) ''' - old_args = set(['level', 'verbosity', 'all', 'sys_argv', 'testcase_pattern']) + + old_args = set(['level', 'verbosity', 'all', 'sys_argv', + 'testcase_pattern']) unexpected_args = set(kwargs.keys()) - old_args if len(unexpected_args) > 0: ua = ', '.join(unexpected_args) @@ -316,7 +190,10 @@ class NoseTester(object): argv = self._test_argv(label, verbose, extra_argv) if doctests: - argv+=['--with-doctest','--doctest-tests'] + argv += ['--with-numpydoctest'] + print "Running unit tests and doctests for %s" % self.package_name + else: + print "Running unit tests for %s" % self.package_name if coverage: argv+=['--cover-package=%s' % self.package_name, '--with-coverage', @@ -342,8 +219,8 @@ class NoseTester(object): """ if self.testRunner is None: self.testRunner = nose.core.TextTestRunner(stream=self.config.stream, - verbosity=self.config.verbosity, - config=self.config) + verbosity=self.config.verbosity, + config=self.config) plug_runner = self.config.plugins.prepareTestRunner(self.testRunner) if plug_runner is not None: self.testRunner = plug_runner @@ -351,14 +228,21 @@ class NoseTester(object): self.success = self.result.wasSuccessful() return self.success - # reset doctest state + # reset doctest state on every run import doctest doctest.master = None - - t = NumpyTestProgram(argv=argv, exit=False) + + import nose.plugins.builtin + from noseclasses import numpyDoctest + plugins = [numpyDoctest(), ] + for m, p in nose.plugins.builtin.builtins: + mod = __import__(m,fromlist=[p]) + plug = getattr(mod, p) + plugins.append(plug()) + + t = NumpyTestProgram(argv=argv, exit=False, plugins=plugins) return t.result - @_add_doc('benchmark') def bench(self, label='fast', verbose=1, extra_argv=None): ''' Run benchmarks for module using nose @@ -368,9 +252,14 @@ class NoseTester(object): argv += ['--match', r'(?:^|[\\b_\\.%s-])[Bb]ench' % os.sep] return nose.run(argv=argv) + # generate method docstrings + _docmethod(_test_argv, '(testtype)') + _docmethod(test, 'test') + _docmethod(bench, 'benchmark') + ######################################################################## -# Doctests for NumPy-specific doctest modifications +# Doctests for NumPy-specific nose/doctest modifications # try the #random directive on the output line def check_random_directive(): |